zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【Pytorch with fastai】第 11 章 :使用 fastai 的中级 API 进行数据处理

PyTorchAPI 进行 with 11 数据处理 中级 使用
2023-09-14 09:14:47 时间

🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​

📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

 🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

文章目录

深入了解 fastai 的分层 API

转换

编写自己的转换

管道

TfmdLists 和数据集:转换后的集合

TfmdLists

数据集

应用中级数据 API:SiamesePair

结论


我们已经看到了对一系列的内容Tokenizer和做了什么Numericalize 文本,以及它们如何在数据块 API 中使用,它直接使用TextBlock. 但是,如果我们只想应用这些转换中的一个怎么办,要么是为了查看中间结果,要么是因为我们已经对文本进行了标记化?更一般地说,当数据块 API 不够灵活以适应我们的特定用例时,我们该怎么办?为此,我们需要使用 fastai 的中级 API来处理数据。数据块 API 建立在该层之上,因此它允许您执行数据块 API 所做的一切,甚至更多。

深入了解 fastai 的分层 API

fastai 库建立在分层 API之上。在最顶层是允许我们在五行中训练模型的应用程序代码,正如我们在第 1 章 中看到的那样。DataLoaders例如,在为文本分类器创建的情况 下,我们使用了这一行:

from fastai.text.all import *

dls = TextDataLoaders.from_folder(untar_data(URLs.IMDB), valid='test')

 工厂方法TextDataLoaders.from_folder的时候很方便 您的数据的排列方式与 IMDb 数据集的排列方式完全相同,但在实践中,情况通常并非如此。数据块 API 提供了更大的灵活性。正如我们在前一章中看到的,我们可以通过以下方式获得相同的结果:

path = untar_data(URLs.IMDB)
dls = DataBlock(
    blocks=(TextBlock.from_folder(path),CategoryBlock),
    get_y = parent_label,
    get_items=partial(get_text_files, folders=['train', 'test']),
    splitter=GrandparentSplitter(valid_name='test')
).dataloaders(path)

但它有时不够灵活。例如,出于调试目的,我们可能只需要应用此数据块附带的部分转换。或者我们可能想为 DataLoadersfastai 不直接支持的应用程序创建一个。在本节中,我们将深入研究 fastai 内部用于实现数据块 API 的部分。了解这些将使您能够利用此中间层 API 的强大功能和灵活性。

中级API

中级 API 不仅包含用于创建DataLoaders. 它还具有回调系统,它允许我们以我们喜欢的任何方式自定义训练循环,以及通用优化器。两者都将在第 16 章中介绍。

转换

我们在前一章学习标记化和数值化时, 我们从抓取一堆文本开始:

files = get_text_files(path, folders = ['train', 'test'])
txts = L(o.open().read() for o in files[:2000])

然后我们展示了如何用Tokenizer

tok = Tokenizer.from_folder(path)
tok.setup(txts)
toks = txts.map(tok)
toks[0]
(#374) ['xxbos','xxmaj','well',',','"','cube','"','(','1997',')'...]

以及如何数值化,包括为我们的语料库自动创建词汇:

num = Numericalize()
num.setup(toks)
nums = toks.map(num)
nums[0][:10]
tensor([   2,    8,   76,   10,   23, 3112,   23,   34, 3113,   33])

这些类也有一个decode方法。例如, Numericalize.decode给我们返回字符串标记:

nums_dec = num.decode(nums[0][:10]); nums_dec
(#10) ['xxbos','xxmaj','well',',','"','cube','"','(','1997',')']

Tokenizer.decode将其变回单个字符串(但是,它可能与原始字符串不完全相同;这取决于分词器是否可逆,而在我们撰写本书时,默认词分词器不是可逆的):

tok.decode(nums_dec)
'xxbos xxmaj well , "cube" ( 1997 )'

decode由 fastaishow_batch和 show_results以及其他一些推理方法使用,将预测和小批量转换为人类可理解的表示。

对于前面的每个示例toknum在前面的示例中,我们创建了一个名为setup 方法的对象(如果需要,它会训练分词器tok并为 创建词汇表num),将其应用于我们的原始文本(通过将对象作为函数调用),最后将结果解码回可理解的表示形式。大多数数据预处理任务都需要这些步骤,所以fastai提供了一个类来封装它们。这是 Transform班级。Tokenize和都是s NumericalizeTransform

一般来说,aTransform是一个行为类似于函数的对象,它有一个可选的setup方法来初始化一个内部状态(就像里面的词汇num)和一个可选的decode方法来反转函数(这种反转可能并不完美,正如我们在tok).

一个很好的例子decode是在Normalize我们的变换中在第 7 章 中看到:为了能够绘制图像,它的decode方法取消了归一化(即,它乘以标准偏差并加回平均值)。另一方面,数据增强转换没有decode方法,因为我们想显示对图像的影响以确保数据增强按我们想要的方式工作。

s 的一个特殊行为Transform是它们总是应用于元组。通常,我们的数据总是一个元组(input,target) (有时有多个输入或多个目标)。当对像这样的项目应用转换时Resize,我们不想调整整个元组的大小;相反,我们希望分别调整输入(如果适用)和目标(如果适用)的大小。对于进行数据增强的批处理变换也是如此:当输入是图像并且目标是分割掩码时,需要将变换(以相同的方式)应用于输入和目标。

如果我们将一个文本元组传递给tok

tok((txts[0], txts[1]))
((#374) ['xxbos','xxmaj','well',',','"','cube','"','(','1997',')'...],
 (#207)
 > ['xxbos','xxmaj','conrad','xxmaj','hall','went','out','with','a','bang'...])

编写自己的转换

如果您想编写一个自定义转换以应用于您的数据,最简单的方法是编写一个函数。正如你在这个例子中看到的,一个 Transform如果提供了类型,将仅应用于匹配类型(否则,它将始终被应用)。在以下代码:int中,函数签名中的 表示f仅应用于ints. 这就是tfm(2.0)returns的原因2.0,但 在这里tfm(2)返回3

def f(x:int): return x+1
tfm = Transform(f)
tfm(2),tfm(2.0)
(3, 2.0)

在这里,f被转换为一个Transform没有setup和没有decode 方法。

Python 有一种特殊的语法,用于将函数(如f)传递给另一个函数(或行为类似于函数的东西, 在 Python 中称为可调用对象),称为装饰器。装饰器的使用方法是在可调用函数前面加上一个可调用@项并将其放在函数定义之前(有很多关于 Python 装饰器的在线教程,如果这对您来说是一个新概念,请看一看)。以下代码与前面的代码相同:

@Transform
def f(x:int): return x+1
f(2),f(2.0)
(3, 2.0)

如果您需要setupdecode,则需要子类 Transform化以实现 中的实际编码行为encodes,然后(可选)实现中的设置行为setups和 中的解码行为decodes

class NormalizeMean(Transform):
    def setups(self, items): self.mean = sum(items)/len(items)
    def encodes(self, x): return x-self.mean
    def decodes(self, x): return x+self.mean

这里,NormalizeMean会在setup过程中初始化某个状态(所有元素通过的均值);那么变换就是减去那个均值。出于解码目的,我们通过添加均值来实现该转换的逆向。这是一个实际的例子NormalizeMean :

tfm = NormalizeMean()
tfm.setup([1,2,3,4,5])
start = 2
y = tfm(start)
z = tfm.decode(y)
tfm.mean,y,z
(3.0, -1.0, 2.0)

请注意,对于这些方法中的每一个,调用的方法和实现的方法是不同的:

ClassTo call To implement

nn.Module(PyTorch)

()i.e., call as function

forward

Transform

()

encodes

Transform

decode()

decodes

Transform

setup()

setups

因此,例如,您永远不会setups直接调用,而是调用setup. 原因是setup在调用setups你之前和之后做了一些工作。要了解有关 Transforms 的更多信息以及如何使用它们根据输入类型实现不同的行为,请务必查看 fastai 文档中的教程。

管道

为了将多个转换组合在一起,fastai 提供了这个Pipeline类。我们Pipeline通过将 s 的列表传递给 a 来 定义 a Transform;然后它将在其中组合转换。当你Pipeline在一个对象上调用 a 时,它会自动调用里面的转换,顺序是:

tfms = Pipeline([tok, num])
t = tfms(txts[0]); t[:20]
tensor([   2,    8,   76,   10,   23, 3112,   23,   34, 3113,   33,   10,    8,
 > 4477,   22,   88,   32,   10,   27,   42,   14])

你可以调用decode你的编码结果,取回你可以显示和分析的东西:

tfms.decode(t)[:100]
'xxbos xxmaj well , " cube " ( 1997 ) , xxmaj vincenzo \'s first movie , was one
 > of the most interesti'

唯一与其中工作方式不同的部分 Transform是设置。要在某些数据上正确设置 a Pipelineof Transform,您需要使用 a TfmdLists

TfmdLists 和数据集:转换后的集合

您的数据通常是一组原始项目(如文件名或 DataFrame 中的行),您要对其应用一系列转换。 我们刚刚看到连续的转换由 Pipelinefastai 表示。将它与您的原始项目分组的类Pipeline称为TfmdLists.

TfmdLists

这是我们在上一节中看到的进行转换的简短方法:

tls = TfmdLists(files, [Tokenizer.from_folder(path), Numericalize])

在初始化时,TfmdLists将自动按顺序调用setup each 的方法,Transform按顺序为每个提供的不是原始项目,而是由所有先前Transforms 转换的项目。我们可以Pipeline通过索引到任何原始元素来获得我们的结果TfmdLists

t = tls[0]; t[:20]
tensor([    2,     8,    91,    11,    22,  5793,    22,    37,  4910,    34,
 > 11,     8, 13042,    23,   107,    30,    11,    25,    44,    14])

并且TfmdLists知道如何解码以用于展示目的:

tls.decode(t)[:100]
'xxbos xxmaj well , " cube " ( 1997 ) , xxmaj vincenzo \'s first movie , was one
 > of the most interesti'

其实它还有一个show方法:

tls.show(t)
xxbos xxmaj well , " cube " ( 1997 ) , xxmaj vincenzo 's first movie , was one
 > of the most interesting and tricky ideas that xxmaj i 've ever seen when
 > talking about movies . xxmaj they had just one scenery , a bunch of actors
 > and a plot . xxmaj so , what made it so special were all the effective
 > direction , great dialogs and a bizarre condition that characters had to deal
 > like rats in a labyrinth . xxmaj his second movie , " cypher " ( 2002 ) , was
 > all about its story , but it was n't so good as " cube " but here are the
 > characters being tested like rats again .

 " nothing " is something very interesting and gets xxmaj vincenzo coming back
 > to his ' cube days ' , locking the characters once again in a very different
 > space with no time once more playing with the characters like playing with
 > rats in an experience room . xxmaj but instead of a thriller sci - fi ( even
 > some of the promotional teasers and trailers erroneous seemed like that ) , "
 > nothing " is a loose and light comedy that for sure can be called a modern
 > satire about our society and also about the intolerant world we 're living .
 > xxmaj once again xxmaj xxunk amaze us with a great idea into a so small kind
 > of thing . 2 actors and a blinding white scenario , that 's all you got most
 > part of time and you do n't need more than that . xxmaj while " cube " is a
 > claustrophobic experience and " cypher " confusing , " nothing " is
 > completely the opposite but at the same time also desperate .

 xxmaj this movie proves once again that a smart idea means much more than just
 > a millionaire budget . xxmaj of course that the movie fails sometimes , but
 > its prime idea means a lot and offsets any flaws . xxmaj there 's nothing
 > more to be said about this movie because everything is a brilliant surprise
 > and a totally different experience that i had in movies since " cube " .

TfmdLists以“s”命名,因为它可以处理带有splits参数的训练集和验证集。您只需要传递训练集中元素的索引和验证集中元素的索引:

cut = int(len(files)*0.8)
splits = [list(range(cut)), list(range(cut,len(files)))]
tls = TfmdLists(files, [Tokenizer.from_folder(path), Numericalize],
                splits=splits)

然后您可以通过trainvalid属性访问它们:

tls.valid[0][:20]
tensor([    2,     8,    20,    30,    87,   510,  1570,    12,   408,   379,
 > 4196,    10,     8,    20,    30,    16,    13, 12216,   202,   509])

如果您手动编写了一次Transform执行所有预处理的程序,将原始项目转换为包含输入和目标的元组,TfmdLists那么 你需要的课程。您可以使用 方法将其直接转换为DataLoaders对象dataloaders。这就是我们将在本章后面的连体示例中做的事情。

不过,一般来说,您将有两个(或更多)并行的转换管道:一个用于将原始项目处理为输入,另一个用于将原始项目处理为目标。例如,在这里,我们定义的管道仅将原始文本处理为输入。如果我们要做文本分类,我们还必须将标签处理成 目标。

为此,我们需要做两件事。首先,我们从父文件夹中获取标签名称。parent_label为此,有一个函数:

lbls = files.map(parent_label)
lbls
(#50000) ['pos','pos','pos','pos','pos','pos','pos','pos','pos','pos'...]

然后我们需要一个Transform在设置过程中获取独特项目并用它们构建词汇表的工具,然后在调用时将字符串标签转换为整数。fastai 为我们提供了这个;它被称为Categorize

cat = Categorize()
cat.setup(lbls)
cat.vocab, cat(lbls[0])
((#2) ['neg','pos'], TensorCategory(1))

要在我们的文件列表中自动完成整个设置,我们可以TfmdLists像以前一样创建一个:

tls_y = TfmdLists(files, [parent_label, Categorize()])
tls_y[0]
TensorCategory(1)

但是我们最终会得到两个单独的对象作为输入和目标,这不是我们想要的。这是Datasets救援的地方。

数据集

Datasets将对同一个原始对象并行应用两个(或更多)管道,并用结果构建一个元组。就像TfmdLists,它会 自动为我们进行设置,当我们索引到 a 时Datasets,它会返回一个包含每个管道结果的元组:

x_tfms = [Tokenizer.from_folder(path), Numericalize]
y_tfms = [parent_label, Categorize()]
dsets = Datasets(files, [x_tfms, y_tfms])
x,y = dsets[0]
x[:20],y

像 a 一样TfmdLists,我们可以传递splits给 aDatasets以在训练集和验证集之间拆分我们的数据:

x_tfms = [Tokenizer.from_folder(path), Numericalize]
y_tfms = [parent_label, Categorize()]
dsets = Datasets(files, [x_tfms, y_tfms], splits=splits)
x,y = dsets.valid[0]
x[:20],y
(tensor([    2,     8,    20,    30,    87,   510,  1570,    12,   408,   379,
 > 4196,    10,     8,    20,    30,    16,    13, 12216,   202,   509]),
 TensorCategory(0))

它还可以解码任何处理过的元组或直接显示它:

t = dsets.valid[0]
dsets.decode(t)
('xxbos xxmaj this movie had horrible lighting and terrible camera movements .
 > xxmaj this movie is a jumpy horror flick with no meaning at all . xxmaj the
 > slashes are totally fake looking . xxmaj it looks like some 17 year - old
 > idiot wrote this movie and a 10 year old kid shot it . xxmaj with the worst
 > acting you can ever find . xxmaj people are tired of knives . xxmaj at least
 > move on to guns or fire . xxmaj it has almost exact lines from " when a xxmaj
 > stranger xxmaj calls " . xxmaj with gruesome killings , only crazy people
 > would enjoy this movie . xxmaj it is obvious the writer does n\'t have kids
 > or even care for them . i mean at show some mercy . xxmaj just to sum it up ,
 > this movie is a " b " movie and it sucked . xxmaj just for your own sake , do
 > n\'t even think about wasting your time watching this crappy movie .',
 'neg')

最后一步是将我们的Datasets对象转换为 a DataLoaders,这可以通过dataloaders方法来完成。这里我们需要传递一个特殊的参数来处理填充问题(正如我们在前一章中看到的)。这需要在我们批处理元素之前发生,所以我们将它传递给before_batch

dls = dsets.dataloaders(bs=64, before_batch=pad_input)

dataloaders直接调用DataLoader我们的每个子集 Datasets。fastai DataLoader扩展了同名的 PyTorch 类,并负责将我们数据集中的项目整理成批次。它有很多定制点,但您应该了解的最重要的点如下:

after_item

在数据集中抓取每个项目后应用于每个项目。这相当于item_tfmsDataBlock.

before_batch

在整理之前应用于项目列表。这是将物品填充到相同尺寸的理想位置。

after_batch

施工后应用于整个批次。这相当于batch_tfmsDataBlock.

作为结论,这里是为文本 分类准备数据所需的完整代码:

tfms = [[Tokenizer.from_folder(path), Numericalize], [parent_label, Categorize]]
files = get_text_files(path, folders = ['train', 'test'])
splits = GrandparentSplitter(valid_name='test')(files)
dsets = Datasets(files, tfms, splits=splits)
dls = dsets.dataloaders(dl_type=SortedDL, before_batch=pad_input)

与之前代码的两个不同之处在于使用 GrandparentSplitter来拆分我们的训练和验证数据,以及 dl_type参数。这是告诉 dataloaders使用SortedDL 类DataLoader,而不是通常的类。SortedDL通过将大致相同长度的样本放入批次中来构建批次。

这与我们之前的完全相同DataBlock

path = untar_data(URLs.IMDB)
dls = DataBlock(
    blocks=(TextBlock.from_folder(path),CategoryBlock),
    get_y = parent_label,
    get_items=partial(get_text_files, folders=['train', 'test']),
    splitter=GrandparentSplitter(valid_name='test')
).dataloaders(path)

但现在您知道如何自定义它的每一部分了!

现在让我们在计算机视觉示例上练习我们刚刚学到的使用这个中级 API 进行数据预处理的知识。

应用中级数据 API:SiamesePair

Siamese 模型拍摄两张图像,必须确定它们是否属于 同一个班级。对于此示例,我们将再次使用 Pet 数据集并为模型准备数据,该模型必须预测两张宠物图像是否属于同一品种。我们将在这里解释如何为这样的模型准备数据,然后我们将在 第 15 章中训练该模型。

首先,让我们获取数据集中的图像:

from fastai.vision.all import *
path = untar_data(URLs.PETS)
files = get_image_files(path/"images")

如果我们根本不关心显示我们的对象,我们可以直接创建一个转换来完全预处理该文件列表。但是,我们想要查看这些图像,因此我们需要创建一个自定义类型。当您在 a或 对象上调用该show方法时,它将解码项目,直到它到达包含方法的类型并使用它来显示对象。该 方法传递了一个,它可以是图像的轴或文本的 DataFrame 行。TfmdListsDatasetsshowshowctxmatplotlib

在这里,我们创建了一个SiameseImage子类对象,fastuple旨在包含三样东西:两张图像和一个布尔值,表示True图像是否属于同一品种。我们还实现了特殊的show方法,这样它将两个图像连接起来,中间有一条黑线。不要太担心if测试中的部分(这是为了显示SiameseImage 图像何时是 Python 图像,而不是张量);重要的部分在最后三行:

class SiameseImage(fastuple):
    def show(self, ctx=None, **kwargs):
        img1,img2,same_breed = self
        if not isinstance(img1, Tensor):
            if img2.size != img1.size: img2 = img2.resize(img1.size)
            t1,t2 = tensor(img1),tensor(img2)
            t1,t2 = t1.permute(2,0,1),t2.permute(2,0,1)
        else: t1,t2 = img1,img2
        line = t1.new_zeros(t1.shape[0], t1.shape[1], 10)
        return show_image(torch.cat([t1,line,t2], dim=2),
                          title=same_breed, ctx=ctx)

让我们先创建一个SiameseImage并检查我们的 show方法是否有效:

img = PILImage.create(files[0])
s = SiameseImage(img, img, True)
s.show();

我们还可以尝试使用不属于同一类的第二张图片:

img1 = PILImage.create(files[1])
s1 = SiameseImage(img, img1, False)
s1.show();

我们之前看到的转换的重要之处在于它们在元组或其子类上进行分派。这正是我们选择fastuple在这个实例中进行子类化的原因——这样,我们可以将任何适用于图像的转换应用于我们的SiameseImage,并且它将应用于元组中的每个图像:

s2 = Resize(224)(s1)
s2.show();

这里Resize变换应用于两个图像中的每一个,但不是布尔标志。即使我们有自定义类型,我们也可以从库中的所有数据增强转换中受益。

我们现在准备好构建Transform我们将用来为 Siamese 模型准备好数据的 。首先,我们需要一个函数来确定我们所有图像的类别:

def label_func(fname):
    return re.match(r'^(.*)_\d+.jpg$', fname.name).groups()[0]

对于每张图像,我们的变换将以 0.5 的概率从同一类中绘制图像并返回 SiameseImage带有真实标签的图像,或者从另一个类中绘制图像并返回SiameseImage带有错误标签的图像。这都是在私有_draw函数中完成的。训练集和验证集之间有一个区别,这就是为什么需要用分割来初始化转换:在训练集上,我们会在每次读取图像时随机选择,而在验证集上,我们会这个随机选择在初始化时一劳永逸。这样,我们在训练过程中获得了更多不同的样本,但始终使用相同的验证集:

class SiameseTransform(Transform):
    def __init__(self, files, label_func, splits):
        self.labels = files.map(label_func).unique()
        self.lbl2files = {l: L(f for f in files if label_func(f) == l)
                          for l in self.labels}
        self.label_func = label_func
        self.valid = {f: self._draw(f) for f in files[splits[1]]}

    def encodes(self, f):
        f2,t = self.valid.get(f, self._draw(f))
        img1,img2 = PILImage.create(f),PILImage.create(f2)
        return SiameseImage(img1, img2, t)

    def _draw(self, f):
        same = random.random() < 0.5
        cls = self.label_func(f)
        if not same:
            cls = random.choice(L(l for l in self.labels if l != cls))
        return random.choice(self.lbl2files[cls]),same

然后我们可以创建我们的主要转换:

splits = RandomSplitter()(files)
tfm = SiameseTransform(files, label_func, splits)
tfm(files[0]).show();

在用于数据收集的中级 API 中,我们有两个对象可以帮助我们对一组项目应用转换:TfmdLists和 Datasets。如果您还记得我们刚刚看到的内容,一个应用一个 Pipeline变换,另一个并行应用几个Pipeline变换,以构建元组。在这里,我们的主要转换已经构建了元组,因此我们使用TfmdLists

tls = TfmdLists(files, tfm, splits=splits)
show_at(tls.valid, 0);

我们终于可以DataLoaders通过调用该 dataloaders方法来获取我们的数据了。这里要注意的一件事是此方法不采用item_tfmsbatch_tfms喜欢DataBlock. fastaiDataLoader有几个以事件命名的钩子;在这里,我们在项目被抓取后应用到它们上的东西被称为 after_item,我们在批次构建后应用到它上的东西被称为after_batch

dls = tls.dataloaders(after_item=[Resize(224), ToTensor],
    after_batch=[IntToFloatTensor, Normalize.from_stats(*imagenet_stats)])

请注意,我们需要传递比平时更多的转换——这是因为数据块 API 通常会自动添加它们:

  • ToTensor是将图像转换为张量的那个(同样,它应用于元组的每个部分)。

  • IntToFloatTensor将包含从 0 到 255 的整数的图像张量转换为浮点数张量,并除以 255 使值介于 0 和 1 之间。

我们现在可以使用它来训练模型DataLoaders。它需要比 提供的通常模型更多的定制,因为它必须拍摄两张图像而不是一张,但我们将在第 15 章cnn_learner中看到如何创建这样的模型并对其进行训练。

结论

fastai 提供了一个分层的 API。当数据处于常用设置之一时,只需一行代码即可获取数据,这使初学者可以轻松地专注于模型的训练,而无需花费太多时间来组装数据。然后,高级数据块 API 允许您混合和匹配构建块,从而为您提供更大的灵活性。在它之下,中级 API 为您提供了更大的灵活性,可以在您的项目上应用转换。在您的实际问题中,这可能是您需要使用的,我们希望它能使数据处理步骤尽可能简单。