第十一章:使用 fastai 的中级 API 进行数据整理
我们已经看到了Tokenizer和Numericalize对文本集合的处理方式,以及它们如何在数据块 API 中使用,该 API 直接使用TextBlock处理这些转换。但是,如果我们只想应用这些转换中的一个,要么是为了查看中间结果,要么是因为我们已经对文本进行了标记化,我们该怎么办?更一般地说,当数据块 API 不足以满足我们特定用例的需求时,我们需要使用 fastai 的中级 API来处理数据。数据块 API 是建立在该层之上的,因此它将允许您执行数据块 API 所做的一切,以及更多更多。
深入了解 fastai 的分层 API
fastai 库是建立在分层 API上的。在最顶层是应用程序,允许我们在五行代码中训练模型,正如我们在第一章中看到的。例如,对于为文本分类器创建DataLoaders,我们使用了这一行:
当您的数据排列方式与 IMDb 数据集完全相同时,工厂方法TextDataLoaders.from_folder非常方便,但实际上,情况通常不会如此。数据块 API 提供了更多的灵活性。正如我们在前一章中看到的,我们可以通过以下方式获得相同的结果:
但有时它并不够灵活。例如,出于调试目的,我们可能需要仅应用与此数据块一起的部分转换。或者我们可能希望为 fastai 不直接支持的应用程序创建一个DataLoaders。在本节中,我们将深入探讨 fastai 内部用于实现数据块 API 的组件。了解这些将使您能够利用这个中间层 API 的强大和灵活性。
中级 API
中级 API 不仅包含用于创建DataLoaders的功能。它还具有回调系统,允许我们以任何我们喜欢的方式自定义训练循环,以及通用优化器。这两者将在第十六章中介绍。
转换
在前一章中研究标记化和数值化时,我们首先获取了一堆文本:
然后我们展示了如何使用Tokenizer对它们进行标记化
以及如何进行数值化,包括自动为我们的语料库创建词汇表:
这些类还有一个decode方法。例如,Numericalize.decode会将字符串标记返回给我们:
Tokenizer.decode将其转换回一个字符串(但可能不完全与原始字符串相同;这取决于标记器是否是可逆的,在我们撰写本书时,默认的单词标记器不是):
decode被 fastai 的show_batch和show_results以及其他一些推断方法使用,将预测和小批量转换为人类可理解的表示。
在前面的示例中,对于tok或num,我们创建了一个名为setup的对象(如果需要为tok训练标记器并为num创建词汇表),将其应用于我们的原始文本(通过将对象作为函数调用),然后最终将结果解码回可理解的表示。大多数数据预处理任务都需要这些步骤,因此 fastai 提供了一个封装它们的类。这就是Transform类。Tokenize和Numericalize都是Transform。
一般来说,Transform是一个行为类似于函数的对象,它具有一个可选的setup方法,用于初始化内部状态(例如num内部的词汇表),以及一个可选的decode方法,用于反转函数(正如我们在tok中看到的那样,这种反转可能不完美)。
decode 的一个很好的例子可以在我们在 第七章 中看到的 Normalize 转换中找到:为了能够绘制图像,它的 decode 方法会撤消归一化(即,乘以标准差并加回均值)。另一方面,数据增强转换没有 decode 方法,因为我们希望展示图像上的效果,以确保数据增强按我们的意愿进行工作。
Transform 的一个特殊行为是它们总是应用于元组。一般来说,我们的数据总是一个元组 (input, target)(有时有多个输入或多个目标)。当对这样的项目应用转换时,例如 Resize,我们不希望整个元组被调整大小;相反,我们希望分别调整输入(如果适用)和目标(如果适用)。对于进行数据增强的批处理转换也是一样的:当输入是图像且目标是分割掩模时,需要将转换(以相同的方式)应用于输入和目标。
如果我们将一个文本元组传递给 tok,我们可以看到这种行为:
编写您自己的转换
如果您想编写一个自定义的转换来应用于您的数据,最简单的方法是编写一个函数。正如您在这个例子中看到的,Transform 只会应用于匹配的类型,如果提供了类型(否则,它将始终被应用)。在下面的代码中,函数签名中的 :int 表示 f 仅应用于 ints。这就是为什么 tfm(2.0) 返回 2.0,但 tfm(2) 在这里返回 3:
在这里,f 被转换为一个没有 setup 和没有 decode 方法的 Transform。
Python 有一种特殊的语法,用于将一个函数(如 f)传递给另一个函数(或类似函数的东西,在 Python 中称为 callable),称为 decorator。通过在可调用对象前加上 @ 并将其放在函数定义之前来使用装饰器(关于 Python 装饰器有很多很好的在线教程,如果这对您来说是一个新概念,请查看其中一个)。以下代码与前面的代码相同:
如果您需要 setup 或 decode,您需要对 Transform 进行子类化,以在 encodes 中实现实际的编码行为,然后(可选)在 setups 中实现设置行为和在 decodes 中实现解码行为:
在这里,NormalizeMean 将在设置期间初始化某个状态(传递的所有元素的平均值);然后转换是减去该平均值。为了解码目的,我们通过添加平均值来实现该转换的反向。这里是 NormalizeMean 的一个示例:
请注意,每个方法的调用和实现是不同的:
| 类 | 调用 | 实现 |
|---|---|---|
nn.Module(PyTorch) | ()(即,作为函数调用) | forward |
Transform | () | encodes |
Transform | decode() | decodes |
Transform | setup() | setups |
因此,例如,您永远不会直接调用 setups,而是会调用 setup。原因是 setup 在为您调用 setups 之前和之后做了一些工作。要了解有关 Transform 及如何使用它们根据输入类型实现不同行为的更多信息,请务必查看 fastai 文档中的教程。
Pipeline
要将几个转换组合在一起,fastai 提供了 Pipeline 类。我们通过向 Pipeline 传递一个 Transform 列表来定义一个 Pipeline;然后它将组合其中的转换。当您在对象上调用 Pipeline 时,它将自动按顺序调用其中的转换:
您可以对编码结果调用 decode,以获取可以显示和分析的内容:
Transform 中与 Transform 不同的部分是设置。要在一些数据上正确设置 Transform 的 Pipeline,您需要使用 TfmdLists。
TfmdLists 和 Datasets:转换的集合
您的数据通常是一组原始项目(如文件名或 DataFrame 中的行),您希望对其应用一系列转换。我们刚刚看到,一系列转换在 fastai 中由Pipeline表示。将这个Pipeline与您的原始项目组合在一起的类称为TfmdLists。
TfmdLists
以下是在前一节中看到的转换的简短方式:
在初始化时,TfmdLists将自动调用每个Transform的setup方法,依次提供每个原始项目而不是由所有先前的Transform转换的项目。我们可以通过索引到TfmdLists中的任何原始元素来获得我们的Pipeline的结果:
而TfmdLists知道如何解码以进行显示:
实际上,它甚至有一个show方法:
TfmdLists以“s”命名,因为它可以使用splits参数处理训练集和验证集。您只需要传递在训练集中的元素的索引和在验证集中的元素的索引:
然后可以通过train和valid属性访问它们:
如果您手动编写了一个Transform,一次执行所有预处理,将原始项目转换为具有输入和目标的元组,那么TfmdLists是您需要的类。您可以使用dataloaders方法直接将其转换为DataLoaders对象。这是我们稍后在本章中将要做的事情。
一般来说,您将有两个(或更多)并行的转换流水线:一个用于将原始项目处理为输入,另一个用于将原始项目处理为目标。例如,在这里,我们定义的流水线仅将原始文本处理为输入。如果我们要进行文本分类,还必须将标签处理为目标。
为此,我们需要做两件事。首先,我们从父文件夹中获取标签名称。有一个名为parent_label的函数:
然后我们需要一个Transform,在设置期间将抓取的唯一项目构建为词汇表,然后在调用时将字符串标签转换为整数。fastai 为我们提供了这个;它被称为Categorize:
要在我们的文件列表上自动执行整个设置,我们可以像以前一样创建一个TfmdLists:
但是然后我们得到了两个分开的对象用于我们的输入和目标,这不是我们想要的。这就是Datasets发挥作用的地方。
Datasets
Datasets将并行应用两个(或更多)流水线到相同的原始对象,并构建一个包含结果的元组。与TfmdLists一样,它将自动为我们进行设置,当我们索引到Datasets时,它将返回一个包含每个流水线结果的元组:
像TfmdLists一样,我们可以将splits传递给Datasets以在训练和验证集之间拆分我们的数据:
它还可以解码任何处理过的元组或直接显示它:
最后一步是将我们的Datasets对象转换为DataLoaders,可以使用dataloaders方法完成。在这里,我们需要传递一个特殊参数来解决填充问题(正如我们在前一章中看到的)。这需要在我们批处理元素之前发生,所以我们将其传递给before_batch:
dataloaders直接在我们的Datasets的每个子集上调用DataLoader。fastai 的DataLoader扩展了 PyTorch 中同名类,并负责将我们的数据集中的项目整理成批次。它有很多自定义点,但您应该知道的最重要的是:
after_item
在数据集中抓取项目后应用于每个项目。这相当于DataBlock中的item_tfms。
before_batch
在整理之前应用于项目列表上。这是将项目填充到相同大小的理想位置。
after_batch
在构建后对整个批次应用。这相当于DataBlock中的batch_tfms。
最后,这是为了准备文本分类数据所需的完整代码:
与之前的代码的两个不同之处是使用GrandparentSplitter来分割我们的训练和验证数据,以及dl_type参数。这是告诉dataloaders使用DataLoader的SortedDL类,而不是通常的类。SortedDL通过将大致相同长度的样本放入批次来构建批次。
这与我们之前的DataBlock完全相同:
但现在你知道如何定制每一个部分了!
让我们现在通过一个计算机视觉示例练习刚学到的关于使用这个中级 API 进行数据预处理。
应用中级数据 API:SiamesePair
一个暹罗模型需要两张图片,并且必须确定它们是否属于同一类。在这个例子中,我们将再次使用宠物数据集,并准备数据用于一个模型,该模型将预测两张宠物图片是否属于同一品种。我们将在这里解释如何为这样的模型准备数据,然后我们将在第十五章中训练该模型。
首先要做的是-让我们获取数据集中的图片:
如果我们根本不关心显示我们的对象,我们可以直接创建一个转换来完全预处理那个文件列表。但是我们想要查看这些图片,因此我们需要创建一个自定义类型。当您在TfmdLists或Datasets对象上调用show方法时,它将解码项目,直到达到包含show方法的类型,并使用它来显示对象。该show方法会传递一个ctx,它可以是图像的matplotlib轴,也可以是文本的 DataFrame 行。
在这里,我们创建了一个SiameseImage对象,它是Tuple的子类,旨在包含三个东西:两张图片和一个布尔值,如果图片是同一品种则为True。我们还实现了特殊的show方法,使其将两张图片与中间的黑线连接起来。不要太担心if测试中的部分(这是在 Python 图片而不是张量时显示SiameseImage的部分);重要的部分在最后三行:
让我们创建一个第一个SiameseImage并检查我们的show方法是否有效:

我们也可以尝试一个不属于同一类的第二张图片:

我们之前看到的转换的重要之处是它们会分派到元组或其子类。这正是为什么在这种情况下我们选择子类化Tuple的原因-这样,我们可以将适用于图像的任何转换应用于我们的SiameseImage,并且它将应用于元组中的每个图像:

这里Resize转换应用于两个图片中的每一个,但不应用于布尔标志。即使我们有一个自定义类型,我们也可以从库中的所有数据增强转换中受益。
现在我们准备构建Transform,以便为暹罗模型准备数据。首先,我们需要一个函数来确定所有图片的类别:
对于每张图片,我们的转换将以 0.5 的概率从同一类中绘制一张图片,并返回一个带有真标签的SiameseImage,或者从另一类中绘制一张图片并返回一个带有假标签的SiameseImage。这一切都在私有的_draw函数中完成。训练集和验证集之间有一个区别,这就是为什么转换需要用拆分初始化:在训练集上,我们将每次读取一张图片时进行随机选择,而在验证集上,我们将在初始化时进行一次性随机选择。这样,在训练期间我们会得到更多不同的样本,但始终是相同的验证集:
然后我们可以创建我们的主要转换:

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

最后,我们可以通过调用dataloaders方法在DataLoaders中获取我们的数据。这里需要注意的一点是,这个方法不像DataBlock那样接受item_tfms和batch_tfms。fastai 的DataLoader有几个钩子,这些钩子以事件命名;在我们抓取项目后应用的内容称为after_item,在构建批次后应用的内容称为after_batch:
请注意,我们需要传递比通常更多的转换,这是因为数据块 API 通常会自动添加它们:
ToTensor是将图像转换为张量的函数(再次,它应用于元组的每个部分)。IntToFloatTensor将包含 0 到 255 之间整数的图像张量转换为浮点数张量,并除以 255,使值在 0 到 1 之间。
现在我们可以使用这个DataLoaders来训练模型。与cnn_learner提供的通常模型相比,它需要更多的定制,因为它必须接受两个图像而不是一个,但我们将看到如何创建这样的模型并在第十五章中进行训练。
结论
fastai 提供了分层 API。当数据处于通常设置之一时,只需一行代码即可获取数据,这使得初学者可以专注于训练模型,而无需花费太多时间组装数据。然后,高级数据块 API 通过允许您混合和匹配构建块来提供更多灵活性。在其下面,中级 API 为您提供更大的灵活性,以在项目上应用转换。在您的实际问题中,这可能是您需要使用的内容,我们希望它使数据处理步骤尽可能简单。
问卷调查
为什么我们说 fastai 具有“分层”API?这是什么意思?
Transform为什么有一个decode方法?它是做什么的?Transform为什么有一个setup方法?它是做什么的?当在元组上调用
Transform时,它是如何工作的?编写自己的
Transform时需要实现哪些方法?编写一个完全规范化项目的
Normalize转换(减去数据集的平均值并除以标准差),并且可以解码该行为。尽量不要偷看!编写一个
Transform,用于对标记化文本进行数字化(它应该从已见数据集自动设置其词汇,并具有decode方法)。如果需要帮助,请查看 fastai 的源代码。什么是
Pipeline?什么是
TfmdLists?什么是
Datasets?它与TfmdLists有什么不同?为什么
TfmdLists和Datasets带有“s”这个名字?如何从
TfmdLists或Datasets构建DataLoaders?在从
TfmdLists或Datasets构建DataLoaders时,如何传递item_tfms和batch_tfms?当您希望自定义项目与
show_batch或show_results等方法一起使用时,您需要做什么?为什么我们可以轻松地将 fastai 数据增强转换应用于我们构建的
SiamesePair?
进一步研究
使用中级 API 在自己的数据集上准备
DataLoaders中的数据。尝试在 Pet 数据集和 Adult 数据集上进行此操作,这两个数据集来自第一章。查看fastai 文档中的 Siamese 教程,了解如何为新类型的项目自定义
show_batch和show_results的行为。在您自己的项目中实现它。
理解 fastai 的应用:总结
恭喜你——你已经完成了本书中涵盖训练模型和使用深度学习的关键实用部分的所有章节!你知道如何使用所有 fastai 内置的应用程序,以及如何使用数据块 API 和损失函数进行定制。你甚至知道如何从头开始创建神经网络并训练它!(希望你现在也知道一些问题要问,以确保你的创作有助于改善社会。)
你已经掌握的知识足以创建许多类型的神经网络应用的完整工作原型。更重要的是,它将帮助你了解深度学习模型的能力和局限性,以及如何设计一个适应它们的系统。
在本书的其余部分,我们将逐个拆解这些应用程序,以了解它们构建在哪些基础之上。这对于深度学习从业者来说是重要的知识,因为它使您能够检查和调试您构建的模型,并创建定制的新应用程序,以适应您特定的项目。