from fastai.vision.all import *
中层数据 API - Pets
Datasets
、Pipeline
、TfmdLists
和 Transform
概述
在本教程中,我们将深入探讨计算机视觉中用于数据收集的中间层 API。首先,我们将了解如何使用
Transform
处理数据Pipeline
组合 transforms
这些只是添加了功能的函数。对于数据集处理,我们将在第二部分中探讨
通常的规则是,当您的 transforms 将输出元组 (输入, 目标) 时,使用 TfmdLists
;当您为每个输入/目标构建独立的 Pipeline
时,使用 Datasets
。
在本教程之后,您可能会对 siamese 教程感兴趣,它更深入地讲解了数据 API,向您展示如何编写自定义类型以及如何定制 show_batch
和 show_results
的行为。
处理数据
数据清洗和处理是机器学习中最耗时的工作之一,这就是为什么 fastai 竭尽全力为您提供帮助的原因。其核心在于,为模型准备数据可以形式化为对一些原始项目应用一系列变换。例如,在经典的图像分类问题中,我们从文件名开始。我们需要打开相应的图像,调整大小,将它们转换为 tensors,可能还会应用某种数据增强,然后才能进行批处理。而这仅仅是针对模型的输入,对于目标,我们需要从文件名中提取标签并将其转换为整数。
这个过程需要具备一定的可逆性,因为我们经常需要检查数据以再次确认输入到模型中的数据是否真的有意义。这就是为什么 fastai 使用 Transform
来表示所有这些操作,您有时可以使用 decode
方法撤销它们。
Transform
首先,我们将使用一张 MNIST 图像来看看基本步骤。我们将从文件名开始,逐步了解如何将其转换为带标签的图像,以便显示和用于建模。我们使用常用的 untar_data
下载数据集(如果需要)并获取所有图像文件
= untar_data(URLs.MNIST_TINY)/'train'
source = get_image_files(source)
items = items[0]; fn fn
Path('/home/jhoward/.fastai/data/mnist_tiny/train/3/9696.png')
我们将依次查看所需的每个 Transform
。以下是打开图像文件的方法
= PILImage.create(fn); img img
然后我们可以将其转换为 C*H*W
tensor(C 代表通道,H 代表高度,W 代表宽度,这是 PyTorch 中的约定)
= ToTensor()
tconv = tconv(img)
img type(img) img.shape,
(torch.Size([3, 28, 28]), fastai.torch_core.TensorImage)
完成此操作后,我们可以创建标签。首先提取文本标签
= parent_label(fn); lbl lbl
'3'
然后转换为整数用于建模
= Categorize(vocab=['3','7'])
tcat = tcat(lbl); lbl lbl
TensorCategory(0)
我们使用 decode
来反向 transforms 以便显示。反转 Categorize
transform 会得到一个可以显示的类别名称
= tcat.decode(lbl)
lbld lbld
'3'
Pipeline
我们可以使用 Pipeline
组合图像处理步骤
= Pipeline([PILImage.create,tconv])
pipe = pipe(fn)
img img.shape
torch.Size([3, 28, 28])
一个 Pipeline
可以解码并显示一个项目。
=(1,1), cmap='Greys'); pipe.show(img, figsize
show 方法在幕后使用类型。Transforms 将确保它们接收到的元素的类型得以保留。在这里,PILImage.create
返回一个 PILImage
,它知道如何显示自身。tconv
将其转换为一个 TensorImage
,它也知道如何显示自身。
type(img)
fastai.torch_core.TensorImage
这些类型也被用于根据接收到的输入启用不同的行为(例如,您对图像、分割掩码或边界框进行数据增强的方式不同)。
仅使用 Transform
加载 pets 数据集
让我们看看如何使用 fastai.data
处理 Pets 数据集。如果您习惯于编写自己的 PyTorch Dataset
,那么将所有内容写在一个 Transform
中会感觉更自然。我们使用 source 指代数据的底层来源(例如,磁盘上的目录、数据库连接、网络连接等)。然后我们获取项目。
= untar_data(URLs.PETS)/"images"
source = get_image_files(source) items
我们将使用此函数从图像文件创建大小一致的 tensors
def resized_image(fn:Path, sz=128):
= Image.open(fn).convert('RGB').resize((sz,sz))
x # Convert image to tensor for modeling
return tensor(array(x)).permute(2,0,1).float()/255.
在创建 Transform
之前,我们需要一个知道如何显示自身的类型(如果我们想使用 show 方法)。这里我们定义一个 TitledImage
class TitledImage(fastuple):
def show(self, ctx=None, **kwargs): show_titled_image(self, ctx=ctx, **kwargs)
我们来检查它是否有效
= resized_image(items[0])
img 'test title').show() TitledImage(img,
使用 decodes 显示处理后的数据
为了解码数据以供显示(例如对图像进行反归一化或将索引转换回其对应的类别),我们在 Transform
内部实现一个 decodes
方法。
class PetTfm(Transform):
def __init__(self, vocab, o2i, lblr): self.vocab,self.o2i,self.lblr = vocab,o2i,lblr
def encodes(self, o): return [resized_image(o), self.o2i[self.lblr(o)]]
def decodes(self, x): return TitledImage(x[0],self.vocab[x[1]])
Transform
一方面打开并调整图像大小,另一方面对其进行标记并使用 o2i
将该标签转换为索引。在 decodes
方法内部,我们使用 vocab
解码索引。图像保持原样(我们无法真正显示文件名!)。
要使用此 Transform
,我们需要一个标签函数。在这里,我们对文件名的 name
属性使用正则表达式
= using_attr(RegexLabeller(pat = r'^(.*)_\d+.jpg$'), 'name') labeller
然后,我们收集所有可能的标签,去重它们,并使用 bidir=True
获取两个对应关系 (vocab 和 o2i)。然后我们可以使用它们构建我们的宠物 transform。
= list(map(labeller, items))
vals = uniqueify(vals, sort=True, bidir=True)
vocab,o2i = PetTfm(vocab,o2i,labeller) pets
我们可以检查它如何应用于文件名
= pets(items[0])
x,y x.shape,y
(torch.Size([3, 128, 128]), 14)
我们可以解码转换后的版本并显示它
= pets.decode([x,y])
dec dec.show()
注意,与 __call__
和 encodes
一样,我们实现了 decodes
方法,但实际上调用的是 Transform
上的 decode
。
另请注意,我们的 decodes
方法接收到两个对象 (x 和 y)。我们在上一节说过 Transform
会对元组进行 dispatch(无论是编码还是解码),但在这里它将我们的两个元素作为一个整体处理,并没有尝试分别解码 x 和 y。为什么会这样?这是因为我们向 decodes 传递了一个列表 [x,y]
。Transform
只对元组进行 dispatch。而且正如我们所见,要阻止 Transform
对元组进行 dispatch,我们只需将其设为 ItemTransform
即可。
class PetTfm(ItemTransform):
def __init__(self, vocab, o2i, lblr): self.vocab,self.o2i,self.lblr = vocab,o2i,lblr
def encodes(self, o): return (resized_image(o), self.o2i[self.lblr(o)])
def decodes(self, x): return TitledImage(x[0],self.vocab[x[1]])
= pets.decode(pets(items[0]))
dec dec.show()
使用 setups 设置内部状态
现在我们可以让 ItemTransform
自动从数据中推断其状态。这样,当我们将 Transform
与数据结合使用时,它将自动完成设置,无需任何额外操作。这非常容易:只需将之前构建类别的代码复制到 transform 的 setups
方法内部即可。
class PetTfm(ItemTransform):
def setups(self, items):
self.labeller = using_attr(RegexLabeller(pat = r'^(.*)_\d+.jpg$'), 'name')
= map(self.labeller, items)
vals self.vocab,self.o2i = uniqueify(vals, sort=True, bidir=True)
def encodes(self, o): return (resized_image(o), self.o2i[self.labeller(o)])
def decodes(self, x): return TitledImage(x[0],self.vocab[x[1]])
现在我们可以创建 Transform
,调用其 setup 方法,它就可以使用了
= PetTfm()
pets
pets.setup(items)= pets(items[0])
x,y x.shape, y
(torch.Size([3, 128, 128]), 14)
和之前一样,解码它也没有问题
= pets.decode((x,y))
dec dec.show()
在 Pipeline
中将我们的 Transform
与数据增强结合起来。
如果我们的元素类型正确,我们可以利用 fastai 的数据增强 transforms。如果我们的 transform 返回 fastai 类型 PILImage
,而不是标准的 PIL.Image
,那么我们就可以使用 fastai 的任何 transform。让我们只为第一个元素返回一个 PILImage
class PetTfm(ItemTransform):
def setups(self, items):
self.labeller = using_attr(RegexLabeller(pat = r'^(.*)_\d+.jpg$'), 'name')
= map(self.labeller, items)
vals self.vocab,self.o2i = uniqueify(vals, sort=True, bidir=True)
def encodes(self, o): return (PILImage.create(o), self.o2i[self.labeller(o)])
def decodes(self, x): return TitledImage(x[0],self.vocab[x[1]])
然后我们可以将该 transform 与 ToTensor
、Resize
或 FlipItem
结合起来,在 Pipeline
中随机翻转图像
= Pipeline([PetTfm(), Resize(224), FlipItem(p=1), ToTensor()]) tfms
对 Pipeline
调用 setup
将按顺序设置每个 transform
tfms.setup(items)
为了检查 setup 是否正确完成,我们需要看看是否构建了 vocab。Pipeline
的一个很酷的技巧是,当查询一个属性时,它会遍历其每个 Transform
查找该属性,并返回结果(如果该属性存在于多个 transforms 中,则返回结果列表)
tfms.vocab
['Abyssinian',
'Bengal',
'Birman',
'Bombay',
'British_Shorthair',
'Egyptian_Mau',
'Maine_Coon',
'Persian',
'Ragdoll',
'Russian_Blue',
'Siamese',
'Sphynx',
'american_bulldog',
'american_pit_bull_terrier',
'basset_hound',
'beagle',
'boxer',
'chihuahua',
'english_cocker_spaniel',
'english_setter',
'german_shorthaired',
'great_pyrenees',
'havanese',
'japanese_chin',
'keeshond',
'leonberger',
'miniature_pinscher',
'newfoundland',
'pomeranian',
'pug',
'saint_bernard',
'samoyed',
'scottish_terrier',
'shiba_inu',
'staffordshire_bull_terrier',
'wheaten_terrier',
'yorkshire_terrier']
然后我们可以调用我们的 pipeline
= tfms(items[0])
x,y x.shape,y
(torch.Size([3, 224, 224]), 14)
我们可以看到 ToTensor
和 Resize
被应用到了我们元组的第一个元素(类型为 PILImage
),但没有应用到第二个元素。我们甚至可以查看我们的元素,检查 flip 是否也已应用
0])) tfms.show(tfms(items[
Pipeline.show
会对每个 Transform
调用 decode,直到获得一个知道如何显示自身的类型。库认为一个元组知道如何显示自身,当且仅当其所有部分都有一个 show
方法。在这里,在我们到达 PetTfm
之前不会发生这种情况,因为我们元组的第二部分是一个 int。但在解码原始的 PetTfm
之后,我们得到一个带有 show
方法的 TitledImage
。
需要注意的一点是,Pipeline
中的 Transform
是根据它们的内部 order
属性排序的(默认为 order=0
)。您可以通过查看 Pipeline
的表示来检查 transforms 的顺序
tfms
Pipeline: PetTfm -> FlipItem -- {'p': 1} -> Resize -- {'size': (224, 224), 'method': 'crop', 'pad_mode': 'reflection', 'resamples': (<Resampling.BILINEAR: 2>, <Resampling.NEAREST: 0>), 'p': 1.0} -> ToTensor
即使我们将 tfms
中的 Resize
定义在 FlipItem
之前,我们仍然可以看到它们已经被重新排序,因为我们有
FlipItem.order,Resize.order
(0, 1)
要自定义 Transform
的顺序,只需在 __init__
之前设置 order = ...
(这是一个类属性)。让我们将 PetTfm
的 order 设置为 -5,以确保它始终最先运行
class PetTfm(ItemTransform):
= -5
order def setups(self, items):
self.labeller = using_attr(RegexLabeller(pat = r'^(.*)_\d+.jpg$'), 'name')
= map(self.labeller, items)
vals self.vocab,self.o2i = uniqueify(vals, sort=True, bidir=True)
def encodes(self, o): return (PILImage.create(o), self.o2i[self.labeller(o)])
def decodes(self, x): return TitledImage(x[0],self.vocab[x[1]])
然后我们即使打乱 Pipeline
中 transforms 的顺序,它也会自行调整
= Pipeline([Resize(224), PetTfm(), FlipItem(p=1), ToTensor()])
tfms tfms
Pipeline: PetTfm -> FlipItem -- {'p': 1} -> Resize -- {'size': (224, 224), 'method': 'crop', 'pad_mode': 'reflection', 'resamples': (<Resampling.BILINEAR: 2>, <Resampling.NEAREST: 0>), 'p': 1.0} -> ToTensor
现在我们有了不错的 transforms Pipeline
,让我们将其添加到文件名列表中以构建数据集。在 fastai 中,一个 Pipeline
与一个集合组合在一起就是一个 TfmdLists
。
TfmdLists
和 Datasets
TfmdLists
和 Datasets
的主要区别在于您拥有的 Pipeline
数量:TfmdLists
接受一个 Pipeline
来转换一个列表(就像我们目前拥有的那样),而 Datasets
则并行组合多个 Pipeline
,从一组原始项目创建元组,例如 (输入, 目标) 元组。
一个 pipeline 创建一个 TfmdLists
创建 TfmdLists
只需一个项目列表和一个 transforms 列表,这些 transforms 将组合在一个 Pipeline
中
= TfmdLists(items, [Resize(224), PetTfm(), FlipItem(p=0.5), ToTensor()])
tls = tls[0]
x,y x.shape,y
(torch.Size([3, 224, 224]), 14)
由于我们的 setup 方法,我们无需向 PetTfm
传递任何东西:在初始化期间,Pipeline
在 items
上自动进行了 setup,因此 PetTfm
和之前一样创建了它的 vocab
tls.vocab
['Abyssinian',
'Bengal',
'Birman',
'Bombay',
'British_Shorthair',
'Egyptian_Mau',
'Maine_Coon',
'Persian',
'Ragdoll',
'Russian_Blue',
'Siamese',
'Sphynx',
'american_bulldog',
'american_pit_bull_terrier',
'basset_hound',
'beagle',
'boxer',
'chihuahua',
'english_cocker_spaniel',
'english_setter',
'german_shorthaired',
'great_pyrenees',
'havanese',
'japanese_chin',
'keeshond',
'leonberger',
'miniature_pinscher',
'newfoundland',
'pomeranian',
'pug',
'saint_bernard',
'samoyed',
'scottish_terrier',
'shiba_inu',
'staffordshire_bull_terrier',
'wheaten_terrier',
'yorkshire_terrier']
我们可以让 TfmdLists
显示我们获取的项目
tls.show((x,y))
或者我们可以使用 show_at
快捷方式
0) show_at(tls,
训练集和验证集
TfmdLists
名称中带有一个 's',因为它可以表示多个变换后的列表:您的训练集和验证集。要使用该功能,我们只需在初始化时传递 splits
。splits
应该是一个索引列表的列表(每个集合一个列表)。为了帮助创建 splits,我们可以使用 fastai 库中的所有 splitters
= RandomSplitter(seed=42)(items)
splits splits
((#5912) [5643,5317,5806,3460,613,5456,2968,3741,10,4908...],
(#1478) [4512,4290,5770,706,2200,4320,6450,501,1290,6435...])
= TfmdLists(items, [Resize(224), PetTfm(), FlipItem(p=0.5), ToTensor()], splits=splits) tls
然后您的 tls
就有了 train 和 valid 属性(之前也有,但 valid 是空的,train 包含所有内容)。
0) show_at(tls.train,
一件有趣的事情是,除非您传递 train_setup=False
,否则您的 transforms 只会在训练集上进行 setup(这是最佳实践):setups
接收到的 items
仅是训练集中的元素。
获取 DataLoaders
从 TfmdLists
中获取 DataLoaders
对象非常简单,您只需调用 dataloaders
方法即可
= tls.dataloaders(bs=64) dls
然后 show_batch
就能正常工作了
dls.show_batch()
您甚至可以添加数据增强 transforms,因为我们有正确 fastai 类型的图像。只需记住添加处理 int 到 float 转换的 IntToFloatTensor
transform(fastai 在 GPU 上的数据增强 transforms 需要 float tensors)。调用 TfmdLists.dataloaders
时,将 batch_tfms
传递给 after_batch
(并将潜在的新 item_tfms
传递给 after_item
)
= tls.dataloaders(bs=64, after_batch=[IntToFloatTensor(), *aug_transforms()])
dls dls.show_batch()
使用 Datasets
Datasets
将 transforms 列表的列表(或 Pipeline
列表)延迟地应用于集合的项目,为每个 transforms/Pipeline
列表创建一个输出。这使得我们可以更轻松地分离出处理步骤,以便我们可以重用它们并更轻松地修改处理过程。这就是数据块 API 的基础:我们可以轻松地混合和匹配作为输入或输出的类型,因为它们与特定的 transforms pipelines 相关联。
例如,让我们编写自己的 ImageResizer
transform,为图像或掩码提供两种不同的实现
class ImageResizer(Transform):
=1
order"Resize image to `size` using `resample`"
def __init__(self, size, resample=BILINEAR):
if not is_listy(size): size=(size,size)
self.size,self.resample = (size[1],size[0]),resample
def encodes(self, o:PILImage): return o.resize(size=self.size, resample=self.resample)
def encodes(self, o:PILMask): return o.resize(size=self.size, resample=NEAREST)
指定类型注解使得我们的 transform 对既不是 PILImage
也不是 PILMask
的对象不做任何操作,并使用 self.resample
调整图像大小,使用最近邻插值调整掩码大小。要创建 Datasets
,然后我们传递两个 transforms pipelines,一个用于输入,一个用于目标
= [[PILImage.create, ImageResizer(128), ToTensor(), IntToFloatTensor()],
tfms
[labeller, Categorize()]]= Datasets(items, tfms) dsets
我们可以检查输入和输出是否具有正确的类型
= dsets[0]
t type(t[0]),type(t[1])
(fastai.torch_core.TensorImage, fastai.torch_core.TensorCategory)
我们可以使用 dsets
解码并显示
= dsets.decode(t)
x,y x.shape,y
(torch.Size([3, 128, 128]), 'basset_hound')
; dsets.show(t)
我们可以像在 TfmdLists
中一样传递我们的训练/验证 split
= Datasets(items, tfms, splits=splits) dsets
但在这里我们没有利用 Transform
会对元组进行 dispatch 的事实。ImageResizer
、ToTensor
和 IntToFloatTensor
可以作为对元组的 transforms 进行传递。这在 .dataloaders
中通过将它们传递给 after_item
来完成。它们不会对类别做任何事情,只会应用于输入。
= [[PILImage.create], [labeller, Categorize()]]
tfms = Datasets(items, tfms, splits=splits)
dsets = dsets.dataloaders(bs=64, after_item=[ImageResizer(128), ToTensor(), IntToFloatTensor()]) dls
我们可以检查它是否适用于 show_batch
dls.show_batch()
如果我们只想从 Datasets
(或之前的 TfmdLists
)构建一个 DataLoader
,您可以将其直接传递给 TfmdDL
= Datasets(items, tfms)
dsets = TfmdDL(dsets, bs=64, after_item=[ImageResizer(128), ToTensor(), IntToFloatTensor()]) dl
分割
通过在 after_item
中使用相同的 transforms,但针对不同类型的目标(此处为分割掩码),目标将通过类型 dispatch 系统自动按照应有的方式进行处理。
= untar_data(URLs.CAMVID_TINY)
cv_source = get_image_files(cv_source/'images')
cv_items = RandomSplitter(seed=42)
cv_splitter = cv_splitter(cv_items)
cv_split = lambda o: cv_source/'labels'/f'{o.stem}_P{o.suffix}' cv_label
= [[PILImage.create], [cv_label, PILMask.create]]
tfms = Datasets(cv_items, tfms, splits=cv_split)
cv_dsets = cv_dsets.dataloaders(bs=64, after_item=[ImageResizer(128), ToTensor(), IntToFloatTensor()]) dls
/home/jhoward/mambaforge/lib/python3.9/site-packages/torch/_tensor.py:1142: UserWarning: __floordiv__ is deprecated, and its behavior will change in a future version of pytorch. It currently rounds toward 0 (like the 'trunc' function NOT 'floor'). This results in incorrect rounding for negative values. To keep the current behavior, use torch.div(a, b, rounding_mode='trunc'), or for actual floor division, use torch.div(a, b, rounding_mode='floor').
ret = func(*args, **kwargs)
=4) dls.show_batch(max_n
添加用于推理的测试数据加载器
让我们回到我们的 pets 数据集……
= [[PILImage.create], [labeller, Categorize()]]
tfms = Datasets(items, tfms, splits=splits)
dsets = dsets.dataloaders(bs=64, after_item=[ImageResizer(128), ToTensor(), IntToFloatTensor()]) dls
……然后假设我们有一些新的文件需要分类。
= untar_data(URLs.PETS)
path = get_image_files(path/"images") tst_files
len(tst_files)
7390
我们可以创建一个 dataloader,它接受这些文件并应用与验证集相同的 transforms,使用 DataLoaders.test_dl
= dls.test_dl(tst_files) tst_dl
=9) tst_dl.show_batch(max_n
额外内容
您可以调用 learn.get_preds
并传入这个新创建的 dataloaders,对我们的新图像进行预测!
真正酷炫的是,训练完模型后,您可以使用 learn.export
保存它,这也会保存所有需要应用于数据的 transforms。在推理时,您只需使用 load_learner
加载您的 learner,然后立即使用 test_dl
创建一个 dataloader,就可以用它来生成新的预测了!