! [ -e /content ] && pip install -Uqq fastai # 在Colab上升级fastai
中级数据 API - 宠物
在计算机视觉中使用
Datasets
、Pipeline
、TfmdLists
和Transform
概述
在本教程中,我们将深入研究计算机视觉中用于收集数据的中级 API。首先,我们将看到如何使用:
Transform
来处理数据Pipeline
来组合变换
这些只是具有附加功能的函数。对于数据集处理,我们将在第二部分中查看
TfmdLists
以在一组项目上应用一个Pipeline
的Transform
Datasets
以并行应用多个Pipeline
的Transform
在一组项目上并生成元组
一般规则是,当您的变换将输出元组 (输入,目标) 时使用 TfmdLists
,而当您为每个输入/目标构建单独的 Pipeline
时使用 Datasets
。
在本教程之后,您可能会对连体网络教程感兴趣,该教程更加深入地介绍了数据 API,向您展示如何编写自定义类型以及如何自定义 show_batch
和 show_results
的行为。
from fastai.vision.all import *
处理数据
清理和处理数据是机器学习中最耗时的任务之一,这就是为什么fastai尽可能帮助您的原因。从本质上讲,为模型准备数据可以形式化为您对某些原始项目应用的一系列转换。例如,在经典的图像分类问题中,我们从文件名开始。我们必须打开相应的图像,调整大小,将它们转换为张量,可能还要应用某种数据增强,然后才能将它们批处理。这仅仅是我们模型的输入,对于目标,我们需要提取文件名的标签并将其转换为整数。
这个过程需要具有一定的可逆性,因为我们通常希望检查我们的数据,以仔细确认我们提供给模型的内容实际上是合理的。这就是为什么fastai通过Transform
s来表示所有这些操作,有时您可以使用decode
方法撤销这些操作。
转换
首先,我们将查看使用单个 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
张量(表示通道 x 高度 x 宽度,这是 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
来逆转变换以进行显示。逆转 Categorize
变换结果会得到一个我们可以显示的类别名称:
= tcat.decode(lbl)
lbld lbld
'3'
管道
我们可以使用 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方法在幕后处理类型。转换将确保接收到的元素的类型得以保留。在这里,PILImage.create
返回一个PILImage
,它知道如何显示自己。tconv
将其转换为一个TensorImage
,它也知道如何显示自己。
type(img)
fastai.torch_core.TensorImage
这些类型也用于根据接收到的输入启用不同的行为(例如,对图像、分割掩码或边界框进行数据增强的方法并不相同)。
仅使用 Transform
加载宠物数据集
让我们看看如何使用 fastai.data
来处理宠物数据集。如果你习惯于编写自己的 PyTorch Dataset
,那么将一切写成一个 Transform
会更自然。我们使用 source 来指代数据的基本来源(例如,磁盘上的一个目录、数据库连接、网络连接等)。然后我们获取这些项。
= untar_data(URLs.PETS)/"images"
source = get_image_files(source) items
我们将使用这个函数从图像文件中创建大小一致的张量:
def resized_image(fn:Path, sz=128):
= Image.open(fn).convert('RGB').resize((sz,sz))
x # 将图像转换为张量以供建模
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,
使用解码来显示处理过的数据
为了解码数据以便显示(例如去归一化图像或将索引转换回其对应的类),我们在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
请求两个对应关系(词汇和o2i)。然后我们可以利用它们构建我们的宠物转换。
= 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
在元组上进行分发(无论是编码还是解码),但这里它将我们的两个元素作为一个整体处理,而没有试图分开解码 x 和 y。这是因为我们将一个列表 [x,y]
传递给 decodes。Transform
在元组上进行分发,但仅限于元组。正如我们所看到的,为了防止 Transform
在元组上进行分发,我们只需将其变为 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()
使用设置来配置内部状态
我们现在可以让我们的 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
,调用它的设置,它将准备好使用:
= 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()
将我们的 Transform
与数据增强结合在一个 Pipeline
中。
我们可以利用fastai的数据增强变换,如果我们为我们的元素提供正确的类型。与其返回标准的PIL.Image
,不如让我们的变换返回fastai类型的PILImage
,这样我们就可以使用任何fastai的变换。让我们先为我们的第一个元素返回一个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]])
我们可以将该转换与 ToTensor
、Resize
或 FlipItem
结合起来,在一个 Pipeline
中随机翻转我们的图像:
= Pipeline([PetTfm(), Resize(224), FlipItem(p=1), ToTensor()]) tfms
在Pipeline
上调用setup
将按顺序设置每个转换:
tfms.setup(items)
为了检查设置是否正确,我们想看看是否构建了词汇。Pipeline
的一个酷妙技巧是,当请求一个属性时,它会在每个 Transform
中查找该属性,并给你结果(如果该属性在多个转换中,则给你结果列表):
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']
然后我们可以调用我们的管道:
= tfms(items[0])
x,y x.shape,y
(torch.Size([3, 224, 224]), 14)
我们可以看到 ToTensor
和 Resize
被应用到了我们元组的第一个元素(它的类型是 PILImage
),但没有应用到第二个元素。我们甚至可以查看我们的元素以检查翻转是否也已应用:
0])) tfms.show(tfms(items[
Pipeline.show
将在每个 Transform
上调用 decode,直到找到一个知道如何显示自己的类型。库将元组视为知道如何显示自己,如果它的所有部分都有一个 show
方法。在到达 PetTfm
之前并不会发生这种情况,因为我们元组的第二部分是一个整数。但在解码原始的 PetTfm
之后,我们得到了一个具有 show
方法的 TitledImage
。
需要注意的是,Pipeline
的 Transform
是根据它们内部的 order
属性排序的(默认值为 order=0
)。您可以通过查看其表示形式,随时检查 Pipeline
中变换的顺序:
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
即使我们在 FlipItem
之前使用 Resize
定义了 tfms
,我们仍然可以看到它们已经被重新排序,因为我们有:
FlipItem.order,Resize.order
(0, 1)
要自定义 Transform
的顺序,只需在 __init__
之前设置 order = ...
(这是一项类属性)。我们将 PetTfm
的顺序设置为 -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
中转换的顺序,但它会自行修复:
= 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
现在我们有了一个良好的Pipeline
转换,让我们将其添加到一个文件名列表中以构建我们的数据集。在fastai中,Pipeline
与集合结合就是TfmdLists
。
TfmdLists
和 Datasets
TfmdLists
和 Datasets
之间的主要区别在于你拥有的 Pipeline
的数量:TfmdLists
使用一个 Pipeline
来转换一个列表(就像我们目前所拥有的),而 Datasets
将多个 Pipeline
并行组合,以从一组原始项中创建一个元组,例如一个元组 (输入, 目标)。
一个管道生成 TfmdLists
创建一个 TfmdLists
只需一个项目列表和一个转换列表,这些将被组合在一个 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)
我们不需要向 PetTfm
传递任何内容,这要感谢我们的设置方法:在初始化时,Pipeline
已自动在 items
上完成设置,因此 PetTfm
像之前一样创建了它的词汇:
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
应该是一个包含索引列表的列表(每个集合对应一个列表)。为了帮助创建拆分,我们可以使用 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
得到了训练和验证属性(之前它也有这些属性,但是验证是空的,训练包含了所有内容)。
0) show_at(tls.train,
有趣的是,除非你传递 train_setup=False
,否则你的转换只会在训练集上设置(这是最佳实践):setups
接收到的 items
仅是训练集的元素。
获取 DataLoaders
从 TfmdLists
获取 DataLoaders
对象非常简单,您只需调用 dataloaders
方法:
= tls.dataloaders(bs=64) dls
而 show_batch
将会正常工作:
dls.show_batch()
您甚至可以添加增强变换,因为我们有一个适当的 fastai 类型图像。只需记得添加 IntToFloatTensor
变换,它处理从整数到浮点数的转换(fastai 在 GPU 上的增强变换需要浮点张量)。在调用 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
将一系列变换(或一系列 Pipeline
)惰性地应用于集合中的项目,针对每一系列变换/Pipeline
创建一个输出。这使我们能够更容易地分离出过程的步骤,从而可以更方便地重用和修改该过程。这为数据块 API 打下了基础:我们可以轻松地将类型混合和匹配作为输入或输出,因为它们与某些变换的管道相关联。
例如,让我们为图像或掩码编写我们自己的 ImageResizer
变换,并提供两种不同的实现:
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)
指定类型注解使得我们的变换对既不是 PILImage
也不是 PILMask
的对象不做任何处理,并且使用 self.resample
来调整图像大小,使用最近邻插值处理掩膜。为了创建 Datasets
,我们随后传递两个变换管道,一个用于输入,一个用于目标:
= [[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
中一样传递我们的训练/验证划分:
= Datasets(items, tfms, splits=splits) dsets
但我们在这里并没有使用 Transform
的元组分发特性。ImageResizer
、ToTensor
和 IntToFloatTensor
可以作为元组的变换传递。这在 .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
中使用相同的变换,但使用不同类型的目标(这里是分割掩模),目标将被自动处理为应有的效果,使用类型调度系统。
= 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
添加用于推理的测试数据加载器
让我们回顾一下我们的宠物数据集……
= [[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
我们可以创建一个数据加载器,它接收这些文件,并使用DataLoaders.test_dl
应用与验证集相同的变换:
= dls.test_dl(tst_files) tst_dl
=9) tst_dl.show_batch(max_n
额外:
您可以调用 learn.get_preds
,传入新创建的数据加载器,以对我们新的图像进行预测!
真正酷的是,在您完成模型训练后,可以使用 learn.export
保存模型,这也会保存应用于数据的所有转换。在推断时,您只需使用 load_learner
加载学习器,即可立即使用 test_dl
创建数据加载器,以生成新的预测!