🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
计算机视觉
cnn_learner
unet_learner
A Siamese Network
自然语言处理
Tabular
结论
我们现在处于令人兴奋的位置,我们可以完全理解我们一直用于计算机视觉、自然语言处理和表格分析的最先进模型的体系结构。在本章中,我们将填补所有关于 fastai 应用程序模型如何工作的缺失细节,并向您展示如何构建它们。
我们还将回到我们在 第 11 章中看到的用于 Siamese 网络的自定义数据预处理管道,并向您展示如何使用 fastai 库中的组件为新任务构建自定义预训练模型。
我们将从计算机视觉开始。
计算机视觉
对于计算机视觉应用程序,我们根据任务使用函数cnn_learner
和 构建模型。unet_learner
在本节中,我们将探索如何构建我们在本书第一部分和第二Learner
部分中使用的对象。
cnn_learner
让我们看看当我们使用该 cnn_learner
函数时会发生什么。我们首先将此函数传递给用于网络主体的体系结构 。大多数时候,我们使用你已经知道的 ResNet 如何创建,所以我们不需要再深入研究了。根据需要下载预训练的权重并加载到 ResNet 中。
然后,对于迁移学习,需要对网络进行切割。这是指切掉最后一层,它只负责 ImageNet 特定的分类。事实上,我们不仅切掉了这一层,还切掉了自适应平均池化层之后的所有内容。其原因马上就会明了。由于不同的架构可能使用不同类型的池化层,甚至是完全不同种类的heads,我们不只是搜索自适应池化层来决定在哪里切割预训练模型。相反,我们有一个信息字典,用于每个模型来确定它的身体在哪里结束,它的头从哪里开始。我们称之为model_meta
——这里是为了resnet50
:
model_meta[resnet50]
{'cut': -2, 'split': <function fastai.vision.learner._resnet_split(m)>, 'stats': ([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])}
身体和头部
如果我们在 的切点之前采用所有层-2
,我们将得到 fastai 将保留用于迁移学习的模型部分。现在,我们戴上新的头颅。这是使用函数创建的create_head
:
create_head(20,2)
Sequential( (0): AdaptiveConcatPool2d( (ap): AdaptiveAvgPool2d(output_size=1) (mp): AdaptiveMaxPool2d(output_size=1) ) (1): Flatten() (2): BatchNorm1d(20, eps=1e-05, momentum=0.1, affine=True) (3): Dropout(p=0.25, inplace=False) (4): Linear(in_features=20, out_features=512, bias=False) (5): ReLU(inplace=True) (6): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True) (7): Dropout(p=0.5, inplace=False) (8): Linear(in_features=512, out_features=2, bias=False) )
使用此功能,您可以选择在最后添加多少个额外的线性层,在每个层之后使用多少 dropout,以及使用哪种池化。默认情况下,fastai 将同时应用平均池化和最大池化,并将两者连接在一起(这就是 AdaptiveConcatPool2d
层)。这不是一种特别常见的方法,但它是近年来在 fastai 和其他研究实验室独立开发的,并且倾向于提供比仅使用平均池化的小改进。
fastai 与大多数库有点不同,默认情况下它在 CNN 头部添加两个线性层,而不是一个。原因是,正如我们所见,即使在将预训练模型转移到非常不同的领域时,转移学习仍然有用。然而,在这些情况下,仅仅使用一个线性层是不够的;我们发现使用两个线性层可以让迁移学习在更多情况下更快更容易地使用。
最后一个BATCHNORM
其中一个
create_head
值得关注的参数是bn_final
. 将此设置为True
将导致将 batchnorm 层添加为最后一层。这有助于帮助您的模型针对输出激活进行适当缩放。我们还没有看到这种方法在任何地方发布,但我们发现无论我们在哪里使用它,它在实践中都能很好地发挥作用。
现在让我们看看unet_learner
我们在第 1 章中展示的分割问题做了什么。
unet_learner
深度学习中最有趣的架构之一是我们在第 1 章中用于分割的架构。分割是一项具有挑战性的任务,因为所需的输出实际上是图像, 或像素网格,包含每个像素的预测标签。其他任务共享类似的基本设计,例如增加图像的分辨率(超分辨率)、为黑白图像添加颜色(着色)或将照片转换为合成绘画(风格转换)——这些任务包含在 本书的在线章节中,因此请务必在阅读本章后检查一下。在每种情况下,我们都从一张图像开始,然后将其转换为另一张具有相同尺寸或纵横比的图像,但像素会以某种方式改变。我们将这些称为生成视觉模型。
我们这样做的方法是从与我们在上一节中看到的完全相同的方法开始开发 CNN 头。例如,我们从 ResNet 开始,然后切断自适应池化层和之后的所有内容。然后我们用我们的自定义头部替换这些层,它执行生成任务。
最后一句话中有很多挥手!我们究竟如何创建生成图像的 CNN 头?如果我们从一个 224 像素的输入图像开始,那么在 ResNet 主体的末尾我们将有一个 7×7 的卷积激活网格。我们如何将其转换为 224 像素的分割蒙版?
自然地,我们用神经网络来做到这一点!所以我们需要某种层来增加 CNN 中的网格大小。一种简单的方法是将 7×7 网格中的每个像素替换为 2×2 正方形中的四个像素。这四个像素中的每一个都将具有相同的值——这被称为最近邻插值。PyTorch 提供了一个层来为我们做这件事,所以一个选择是创建一个头部,其中包含 stride-1 卷积层(以及像往常一样的 batchnorm 和 ReLU 层),中间散布着 2×2 最近邻插值层。事实上,你现在就可以试试这个!看看您是否可以创建一个像这样设计的自定义头部,并在 CamVid 分割任务中尝试。您应该会发现您得到了一些合理的结果,尽管它们不如我们第 1 章的结果好。
另一种方法是用转置卷积代替最近邻和卷积组合,也称为跨步半卷积。这与常规卷积相同,但首先在输入中的所有像素之间插入零填充。这通过图片最容易看出——图 15-1显示了我们在第 13 章讨论的出色的卷积算法论文中的图表,显示了应用于 3×3 图像的 3×3 转置卷积。
图 15-1。转置卷积(由 Vincent Dumoulin 和 Francesco Visin 提供)
如您所见,结果是增加了输入的大小。您现在可以使用 fastai 的课程来尝试一下ConvLayer
;传递参数transpose=True
以在您的自定义头部中创建转置卷积,而不是常规卷积。
然而,这两种方法都效果不佳。问题是我们的 7×7 网格根本没有足够的信息来创建 224×224 像素的输出。它要求每个网格单元进行大量激活,以获得足够的信息来完全重新生成输出中的每个像素。
解决方案是使用skip connections,就像在 ResNet 中一样,但是从 ResNet 主体中的激活一直跳过到架构另一侧的转置卷积的激活。如图 15-2所示,这种方法是由 Olaf Ronneberger 等人开发的。在 2015 年的论文“U-Net: Convolutional Networks for Biomedical Image Segmentation”中。虽然论文侧重于医学应用,但 U-Net 已经彻底改变了各种生成视觉模型。
图 15-2。U-Net 架构(由 Olaf Ronneberger、Philipp Fischer 和 Thomas Brox 提供)
此图显示左侧的 CNN 主体(在这种情况下,它是常规 CNN,而不是 ResNet,并且他们使用 2×2 最大池而不是 stride-2 卷积,因为这篇论文是在 ResNet 出现之前写的)和右侧的转置卷积(“up-conv”)层。额外的跳过连接显示为从左到右交叉的灰色箭头(这些有时称为 交叉连接)。你可以明白为什么它被称为U-Net了!
使用这种架构,转置卷积的输入不仅是前一层中的低分辨率网格,而且是 ResNet 头部中的高分辨率网格。这允许 U-Net 根据需要使用原始图像的所有信息。U-Nets 的一个挑战是确切的架构取决于图像大小。fastai 有一个独特的DynamicUnet
类,可以根据提供的数据自动生成大小合适的架构。
现在让我们关注一个示例,在该示例中我们利用 fastai 库编写自定义模型。
A Siamese Network
让我们回到我们在 第 11 章中为孪生网络设置的输入管道。您可能还记得,它由一对图像组成,标签为True
或 False
,具体取决于它们是否属于同一类。
使用我们刚刚看到的内容,让我们为此任务构建一个自定义模型并对其进行训练。如何?我们将使用预训练架构并通过它传递我们的两个图像。然后我们可以连接结果并将它们发送到将返回两个预测的自定义头。在模块方面,这看起来像这样:
class SiameseModel(Module):
def __init__(self, encoder, head):
self.encoder,self.head = encoder,head
def forward(self, x1, x2):
ftrs = torch.cat([self.encoder(x1), self.encoder(x2)], dim=1)
return self.head(ftrs)
要创建我们的编码器,我们只需要采用预训练模型并将其剪切,如前所述。该功能create_body
为我们做到了;我们只需要将它传递到我们想要切割的地方。正如我们之前看到的,根据预训练模型的元数据字典,ResNet 的切割值为–2
:
encoder = create_body(resnet34, cut=-2)
然后我们可以创建我们的头。查看编码器告诉我们最后一层有 512 个特征,所以这个头需要接收512*2
. 为什么是2?首先我们必须乘以 2,因为我们有两个图像。然后由于我们的连接池技巧,我们需要第二次乘以 2。所以我们创建头部如下:
head = create_head(512*2, 2, ps=0.5)
使用我们的编码器和头部,我们现在可以构建我们的模型:
model = SiameseModel(encoder, head)
在使用 之前Learner
,我们还有两件事要定义。首先,我们必须定义我们要使用的损失函数。它是常规交叉熵,但由于我们的目标是布尔值,我们需要将它们转换为整数,否则 PyTorch 会抛出错误:
def loss_func(out, targ):
return nn.CrossEntropyLoss()(out, targ.long())
更重要的是,为了充分利用迁移学习,我们必须定义一个自定义拆分器。拆分器是一个函数,它告诉 fastai 库如何将模型拆分为参数组。当我们进行迁移学习时,这些在幕后仅用于训练模型的头部。
这里我们需要两组参数:一组用于编码器,一组用于头部。因此,我们可以定义以下拆分器(params
只是一个返回给定模块的所有参数的函数):
def siamese_splitter(model):
return [params(model.encoder), params(model.head)]
然后我们可以Learner
通过传递数据、模型、损失函数、拆分器和我们想要的任何指标来定义我们的。由于我们没有使用 fastai 的便利函数进行迁移学习(如 cnn_learner
),我们必须learn.freeze
手动调用。这将确保只训练最后一个参数组(在本例中为头部):
learn = Learner(dls, model, loss_func=loss_func,
splitter=siamese_splitter, metrics=accuracy)
learn.freeze()
然后我们可以直接用通常的方法训练我们的模型:
learn.fit_one_cycle(4, 3e-3)
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 0.367015 | 0.281242 | 0.885656 | 00:26 |
1 | 0.307688 | 0.214721 | 0.915426 | 00:26 |
2 | 0.275221 | 0.170615 | 0.936401 | 00:26 |
3 | 0.223771 | 0.159633 | 0.943843 | 00:26 |
现在我们解冻并微调整个模型,使用判别学习率(即身体的学习率较低,头部的学习率较高):
learn.unfreeze()
learn.fit_one_cycle(4, slice(1e-6,1e-4))
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 0.212744 | 0.159033 | 0.944520 | 00:35 |
1 | 0.201893 | 0.159615 | 0.942490 | 00:35 |
2 | 0.204606 | 0.152338 | 0.945196 | 00:36 |
3 | 0.213203 | 0.148346 | 0.947903 | 00:36 |
当我们记得以相同方式训练的分类器(没有数据增强)的错误率为 7% 时,94.8% 是非常好的。
现在我们已经了解了如何创建完整的最先进的计算机视觉模型,让我们继续讨论 NLP。
自然语言处理
正如我们在第 10 章中所做的那样,将 AWD-LSTM 语言模型转换为迁移学习分类器遵循一个非常cnn_learner
与我们在本章第一部分中 所做的类似的过程。在这种情况下,我们不需要“元”字典,因为我们没有这么多的体系结构来支持身体。我们需要做的就是为语言模型中的编码器选择堆叠式 RNN,这是一个单独的 PyTorch 模块。该编码器将为输入的每个单词提供激活,因为语言模型需要为每个下一个单词输出预测。
为了由此创建分类器,我们使用了ULMFiT 论文中描述为“用于文本分类的 BPTT (BPT3C)”的方法:
我们将文档分成大小为b的固定长度的批次。在每批开始时,模型用前一批的最终状态初始化;我们跟踪均值和最大池化的隐藏状态;梯度被反向传播到其隐藏状态对最终预测有贡献的批次。在实践中,我们使用可变长度的反向传播序列。
换句话说,分类器包含一个for
循环,循环遍历序列的每个批次。跨批维护状态,并存储每个批的激活。最后,我们使用与计算机视觉模型相同的平均和最大串联池化技巧——但这一次,我们不是在 CNN 网格单元上进行池化,而是在 RNN 序列上进行池化。
对于这个for
循环,我们需要分批收集数据,但每个文本都需要单独处理,因为它们都有自己的标签。然而,很可能这些文本的长度并不相同,这意味着我们无法将它们全部放在同一个数组中,就像我们对语言模型所做的那样。
这就是填充的作用所在:当抓取一堆文本时,我们确定长度最大的那个;然后我们用一个叫做 的特殊标记填充较短的那些xxpad
。为了避免在同一批次中包含 2,000 个标记的文本和包含 10 个标记的文本的极端情况(因此大量填充和大量浪费的计算),我们通过确保将大小相当的文本放在一起来改变随机性. 训练集的文本仍将以某种随机顺序排列(对于验证集,我们可以简单地按长度顺序对它们进行排序),但并非完全如此。
这是在创建我们的 .fastai 库时在幕后自动完成的DataLoaders
。
Tabular
最后,让我们来看看fastai.tabular
模型。(我们不需要单独看协同过滤,因为我们已经看到这些模型只是表格 模型或使用我们之前从头开始实施的点积方法。)
这是以下forward
方法TabularModel
:
if self.n_emb != 0:
x = [e(x_cat[:,i]) for i,e in enumerate(self.embeds)]
x = torch.cat(x, 1)
x = self.emb_drop(x)
if self.n_cont != 0:
x_cont = self.bn_cont(x_cont)
x = torch.cat([x, x_cont], 1) if self.n_emb != 0 else x_cont
return self.layers(x)
我们不会在这里展示,因为它不是那么有趣,但会依次__init__
查看每一行代码 。forward
第一行只是测试是否有任何嵌入要处理——如果我们只有连续变量,我们可以跳过这一部分:
if self.n_emb != 0:
self.embeds
包含嵌入矩阵,所以这得到了每个
x = [e(x_cat[:,i]) for i,e in enumerate(self.embeds)]
并将它们连接成一个张量:
x = torch.cat(x, 1)
然后应用dropout。您可以传递embed_p
给__init__
更改此值:
x = self.emb_drop(x)
现在我们测试是否有任何连续变量需要处理:
if self.n_cont != 0:
它们通过 batchnorm 层
x_cont = self.bn_cont(x_cont)
并与嵌入激活连接,如果有的话:
x = torch.cat([x, x_cont], 1) if self.n_emb != 0 else x_cont
最后,它通过线性层(每个层都包括 batchnorm,if use_bn
isTrue
和 dropout,ifps
被设置为某个值或值列表):
return self.layers(x)
恭喜!现在你知道了 fastai 库中使用的每一个架构!
结论
如您所见,深度学习架构的细节现在不必吓到您了。你可以查看 fastai 和 PyTorch 的代码,看看到底发生了什么。更重要的是,试着理解为什么会这样。查看代码中引用的论文,并尝试查看代码如何与所描述的算法相匹配。
现在我们已经研究了模型的所有部分和传递给它的数据,我们可以考虑这对实际深度学习意味着什么。如果你有无限的数据、无限的内存和无限的时间,那么建议很简单:在你所有的数据上训练一个巨大的模型很长时间。但深度学习并不简单的原因是您的数据、内存和时间通常是有限的。如果您的内存或时间不足,解决方案是训练一个较小的模型。如果你不能训练足够长的时间来过度拟合,您没有利用模型的能力。
因此,第 1 步是达到可以过度拟合的程度。那么问题是如何减少过度拟合。图 15-3 显示了我们建议如何从那里确定步骤的优先级。
图 15-3。减少过度拟合的步骤
许多从业者在面对过度拟合模型时,从这张图的错误一端开始。他们的出发点是使用更小的模型或更多的正则化。使用较小的模型绝对应该是您采取的最后一步,除非训练您的模型占用了太多时间或内存。减小模型的大小会降低模型学习数据中微妙关系的能力。
相反,您的第一步应该是寻求创建更多数据。这可能涉及为您已有的数据添加更多标签,寻找您的模型可能需要解决的其他任务(或者,换一种方式思考,识别您可以建模的不同类型的标签),或创建额外的合成数据通过使用更多或不同的数据增强技术。由于 Mixup 和类似方法的发展,现在几乎所有类型的数据都可以进行有效的数据增强。
一旦您获得了您认为可以合理获取的尽可能多的数据,并通过利用您可以找到的所有标签并进行所有有意义的扩充,尽可能有效地使用它,如果您仍然过度拟合,你应该考虑使用更通用的架构。例如,添加批量归一化可能会提高泛化能力。
如果您在尽最大努力使用数据和调整架构后仍然过拟合,您可以看看正则化。一般来说,在最后一两层添加 dropout 可以很好地规范你的模型。然而,正如我们从 AWD-LSTM 的开发故事中了解到的那样,在整个模型中添加不同类型的 dropout 通常会更有帮助。一般来说,具有更多正则化的较大模型更灵活,因此比具有较少正则化的较小模型更准确。
只有在考虑了所有这些选项之后,我们才会建议您尝试使用较小版本的体系结构。