🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
在本章中,我们将在上一章介绍的 CNN 之上构建,并向您解释 ResNet(残差网络)架构。它由 Kaiming He 等人于 2015 年推出。在文章“Deep Residual Learning for Image Recognition”中,它是目前最常用的模型架构。图像的最新发展 模型几乎总是使用相同的残差连接技巧,而且大多数时候,它们只是对原始 ResNet 的微调。
我们将首先向您展示最初设计的基本 ResNet,然后解释使其性能更高的现代调整。但首先,我们需要一个比 MNIST 数据集更难的问题,因为我们已经接近 100% 的准确率,在其上使用常规 CNN。
当我们已经达到与上一章在 MNIST 上看到的一样高的准确度时,很难判断我们对模型所做的任何改进,因此我们将通过回到 Imagenette 来解决更棘手的图像分类问题。我们将坚持使用小图像以保持速度相当快。
让我们抓取数据——我们将使用已经调整大小的 160 像素版本来使速度更快,并将随机裁剪为 128 像素:
- def get_data(url, presize, resize):
- path = untar_data(url)
- return DataBlock(
- blocks=(ImageBlock, CategoryBlock), get_items=get_image_files,
- splitter=GrandparentSplitter(valid_name='val'),
- get_y=parent_label, item_tfms=Resize(presize),
- batch_tfms=[*aug_transforms(min_scale=0.5, size=resize),
- Normalize.from_stats(*imagenet_stats)],
- ).dataloaders(path, bs=128)
dls = get_data(URLs.IMAGENETTE_160, 160, 128)
dls.show_batch(max_n=4)
当我们查看 MNIST 时,我们处理的是 28×28 像素的图像。为了 Imagenette,我们将使用 128×128 像素的图像进行训练。后来,我们还希望能够使用更大的图像——至少像 ImageNet 标准的 224×224 像素一样大。你还记得吗 我们如何设法从 MNIST 卷积神经网络中为每个图像获得单个激活向量?
我们使用的方法是确保有足够的 stride-2 卷积,以便最后一层的网格大小为 1。然后我们只是展平我们最终得到的单位轴,以获得每个图像的向量(所以,一个小批量的激活矩阵)。我们可以为 Imagenette 做同样的事情,但这会导致两个问题:
我们需要很多 stride-2 层来使我们的网格在最后变成 1×1——可能比我们原本选择的要多。
该模型不适用于我们最初训练的尺寸以外的任何尺寸的图像。
处理第一个问题的一种方法是以处理 1×1 以外的网格大小的方式展平最终的卷积层。我们可以像以前一样简单地将矩阵展平为向量,方法是将每一行布置在前一行之后。事实上,直到 2013 年,卷积神经网络几乎总是采用这种方法。最著名的例子是 2013 年 ImageNet 获胜者 VGG,时至今日仍在使用。但是这种架构还有另一个问题:它不仅不能处理与训练集中使用的相同大小的图像不同的图像,而且还需要大量内存,因为展平卷积层会导致许多激活被馈送进入最后几层。因此,最后一层的权重矩阵是巨大的。
通过创建全卷积网络解决了这个问题。全卷积网络的技巧是取卷积网格中激活的平均值。换句话说,我们可以简单地使用这个函数:
def avg_pool(x): return x.mean((2,3))
如您所见,它取 x 轴和 y 轴的平均值。此函数始终将激活网格转换为每个图像的单个激活。PyTorch 提供了一个稍微更通用的模块,称为 nn.AdaptiveAvgPool2d
,它将激活网格平均到您需要的任何大小的目的地(尽管我们几乎总是使用 1 的大小)。
因此,全卷积网络具有多个卷积层,其中一些卷积层的步幅为 2,最后是一个自适应平均池化层、一个用于移除单位轴的展平层,最后是一个线性层。这是我们的第一个全卷积网络:
- def block(ni, nf): return ConvLayer(ni, nf, stride=2)
- def get_model():
- return nn.Sequential(
- block(3, 16),
- block(16, 32),
- block(32, 64),
- block(64, 128),
- block(128, 256),
- nn.AdaptiveAvgPool2d(1),
- Flatten(),
- nn.Linear(256, dls.c))
稍后我们将block
用其他变体替换网络中的实现,这就是我们不再调用它的原因conv
。我们还通过利用 fastai 节省了一些时间ConvLayer
,它已经提供了conv
前一章的功能(还有更多!)。
想一想
一旦我们完成了我们的卷积层,我们将获得大小的激活 bs x ch x h x w
(批量大小、一定数量的通道、高度和宽度)。我们想将其转换为大小为 的张量 bs x ch
,因此我们取最后两个维度的平均值,并像我们在之前的模型中所做的那样展平尾随的 1×1 维度。
这与常规池化不同,因为这些层通常会采用给定大小的窗口的平均值(对于平均池化)或最大值(对于最大池化)。例如,大小为 2 的最大池化层在较早的 CNN 中非常流行,通过取每个 2×2 窗口(步幅为 2)的最大值,将每个维度上的图像大小减少一半。
和以前一样,我们可以Learner
用我们的自定义模型定义一个然后 在我们之前抓取的数据上训练它:
- def get_learner(m):
- return Learner(dls, m, loss_func=nn.CrossEntropyLoss(), metrics=accuracy
- ).to_fp16()
-
- learn = get_learner(get_model())
learn.lr_find()
(0.47863011360168456, 3.981071710586548)
3e-3 通常是 CNN 的一个很好的学习率,这里似乎也是这种情况,所以让我们试试看:
learn.fit_one_cycle(5, 3e-3)
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 1.901582 | 2.155090 | 0.325350 | 00:07 |
1 | 1.559855 | 1.586795 | 0.507771 | 00:07 |
2 | 1.296350 | 1.295499 | 0.571720 | 00:07 |
3 | 1.144139 | 1.139257 | 0.639236 | 00:07 |
4 | 1.049770 | 1.092619 | 0.659108 | 00:07 |
这是一个很好的开始,考虑到我们必须从 10 个类别中选择正确的一个,而且我们从头开始训练仅 5 个时期!我们可以使用更深的模型做得比这更好,但只是堆叠新层并不能真正做到 改进我们的结果(您可以自己尝试看看!)。为了解决这个问题,ResNets 引入了skip connections的想法。我们将在下一节探讨 ResNet 的这些和其他方面。
我们现在拥有构建模型所需的所有部分,自本书开始以来我们一直在计算机视觉任务中使用:ResNets。 我们将介绍它们背后的主要思想,并展示与我们之前的模型相比它如何提高 Imagenette 的准确性,然后再构建一个包含所有最新调整的版本。
2015 年,ResNet 论文的作者注意到了一些令他们感到好奇的事情。即使在使用 batchnorm 之后,他们也发现网络使用 更多层的效果不如使用更少层的网络——而且模型之间没有其他差异。最有趣的是,这种差异不仅出现在验证集中,而且出现在训练集中;所以这不仅仅是一个泛化问题,而是一个培训问题。正如论文所解释的那样:
出乎意料的是,这种退化不是由过度拟合引起的,并且向适当深度的模型添加更多层会导致更高的训练误差,正如 [先前报道] 并通过我们的实验彻底验证的那样。
图 14-1中的图表说明了这种现象,左侧是训练误差,右侧是测试误差。
图 14-1。不同深度网络的训练(由 Kaiming He 等人提供)
正如作者在这里提到的,他们并不是第一个注意到这个奇怪事实的人。但他们是第一个做出非常重要飞跃的人:
让我们考虑一个较浅的架构及其在其上添加更多层的较深的对应物。对于更深层次的模型,存在一种构造解决方案:添加的层是身份映射,其他层是从学习到的较浅模型中复制的。
由于这是一篇学术论文,这个过程以一种相当难以理解的方式描述,但这个概念实际上非常简单:从一个训练有素的 20 层神经网络开始,然后添加另外 36 层什么都不做(例如例如,它们可以是线性层,单个权重等于 1,偏差等于 0)。结果将是一个 56 层网络,其功能与 20 层网络完全相同,证明总是有深度网络至少应该与任何浅层网络一样好。但是由于某种原因,SGD 似乎无法找到它们。
身份映射
实际上,还有另一种方法可以创建这些额外的 36 层,这更有趣。如果我们将每次出现的替换为conv(x)
,x + conv(x)
其中conv
是上一章的函数,它添加了第二个卷积,然后是 batchnorm 层,然后是 ReLU。此外,回想一下 batchnorm 做gamma*y + beta
. 如果我们将gamma
每个最终的 batchnorm 层都初始化为零怎么办?由于beta
已经初始化为零,我们conv(x)
对于那些额外的 36 层将始终等于零,这意味着x+conv(x)
将始终等于x
。
这给我们带来了什么?关键是这 36 个额外的层,就目前而言,是一个恒等映射,但它们有 参数,这意味着它们是可训练的。所以,我们可以从我们最好的 20 层模型开始,添加这 36 个最初什么都不做的额外层,然后微调整个 56 层模型。这些额外的 36 层然后可以学习使它们最有用的参数!
ResNet 论文提出了一个变体,即“跳过”每一秒的卷积,因此我们有效地得到 x+conv2(conv1(x))
. 图 14-2(来自论文)中的图表显示了这一点 。
图 14-2。一个简单的 ResNet 块(由 Kaiming He 等人提供)
右边的那个箭头只是 的x
一部分,x+conv2(conv1(x))
被称为恒等分支,或跳过连接。左边的路径是conv2(conv1(x))
部分。您可以将身份路径视为提供从输入到输出的直接路径。
在 ResNet 中,我们不会先训练较少的层数,然后在最后添加新层,然后 微调。相反,我们在整个 CNN 中使用如图 14-2中的 ResNet 块,以通常的方式从头开始初始化,并以通常的方式使用 SGD 进行训练。我们依靠跳过连接来使网络更容易使用 SGD 进行训练。
还有另一种(大致相同的)方式来考虑这些 ResNet 块。论文是这样描述的:
我们不是希望每几个堆叠层直接拟合所需的底层映射,而是明确让这些层拟合残差映射。形式上,将所需的 基础映射表示为H ( x ),我们让堆叠的非线性层拟合F ( x ) := H( x ) − x的另一个映射。原始映射重铸为F ( x )+ x. 我们假设优化残差映射比优化原始的、未引用的映射更容易。在极端情况下,如果恒等映射是最优的,则将残差推到零比通过一堆非线性层拟合恒等映射更容易。
同样,这是相当难以理解的散文——所以让我们尝试用通俗易懂的英语重述一下!如果给定层的结果是x
并且我们使用的是返回 的 ResNet 块 y = x + block(x)
,则我们不要求该块进行预测y
;y
我们要求它预测和之间的差异x
。所以这些块的工作不是预测某些特征,而是最小化x
和所需之间的误差y
。因此,ResNet 擅长了解什么都不做和通过两个卷积层(具有可训练权重)的块之间的细微差别。这些模型就是这样得名的:它们预测残差(提醒:“残差”是预测减去目标)。
这两种思考 ResNet 的方式都有一个关键概念,即易于学习。这是一个重要的主题。回想一下万能逼近定理,它指出 足够大的网络可以学到任何东西。这仍然是正确的,但事实证明,网络 原则上可以学习的内容与通过现实数据和训练机制可以轻松学习的内容之间存在非常重要的区别。过去十年神经网络的许多进步就像 ResNet 模块:意识到如何使总是可能的事情变得切实可行的结果。
真实身份路径
原始论文实际上并没有
gamma
在每个块的最后一个 batchnorm 层中使用零作为初始值的技巧;几年后。因此,原始版本的 ResNet 并没有完全开始使用通过 ResNet 块的真实身份路径进行训练,但是尽管如此,具有“导航”跳过连接的能力确实使其训练得更好。添加 batchnormgamma
init 技巧使模型以更高的学习率进行训练。
这是一个简单的 ResNet 块的定义(fastaigamma
将最后一个 batchnorm 层的权重初始化为零,因为norm_type=NormType.BatchZero
):
- class ResBlock(Module):
- def __init__(self, ni, nf):
- self.convs = nn.Sequential(
- ConvLayer(ni,nf),
- ConvLayer(nf,nf, norm_type=NormType.BatchZero))
- def forward(self, x): return x + self.convs(x)
然而,这有两个问题:它不能处理 1 以外的步幅,并且它需要ni==nf
. 停下来仔细想想这是为什么。
问题在于,如果其中一个卷积的步幅为 2,则输出激活的网格大小将是输入每个轴上大小的一半。所以我们不能把它加回x
inforward
因为x
输出激活有不同的维度。如果出现相同的基本问题ni!=nf
:输入和输出连接的形状不允许我们将它们加在一起。
要解决此问题,我们需要一种方法来更改形状x
以匹配的结果self.convs
。可以使用步长为 2 的平均池化层将网格大小减半:也就是说,该层从输入中获取 2×2 块并用它们的平均值替换它们。
可以使用卷积来更改通道数。然而,我们希望这种跳跃连接尽可能接近恒等映射,这意味着要使这种卷积尽可能简单。最简单的卷积是内核大小为 1 的卷积。这意味着内核的大小为ni
× nf
× 1
× 1
,因此它只是在每个输入像素的通道上进行点积——它根本不会跨像素进行组合。这种 1x1 卷积在现代 CNN 中被广泛使用,所以花点时间考虑一下它是如何工作的。
1X1的卷积
核大小为 1 的卷积。
这是一个 ResBlock 使用这些技巧来处理跳过连接中的形状变化:
- def _conv_block(ni,nf,stride):
- return nn.Sequential(
- ConvLayer(ni, nf, stride=stride),
- ConvLayer(nf, nf, act_cls=None, norm_type=NormType.BatchZero))
- class ResBlock(Module):
- def __init__(self, ni, nf, stride=1):
- self.convs = _conv_block(ni,nf,stride)
- self.idconv = noop if ni==nf else ConvLayer(ni, nf, 1, act_cls=None)
- self.pool = noop if stride==1 else nn.AvgPool2d(2, ceil_mode=True)
-
- def forward(self, x):
- return F.relu(self.convs(x) + self.idconv(self.pool(x)))
请注意,我们在noop
这里使用的是函数,它只是简单地返回其输入不变(noop是一个计算机科学术语,代表“无操作”)。在这种情况下,idconv
如果 什么都不做,如果ni==nf
什么pool
也不做stride==1
,这就是我们在跳过连接中想要的。
此外,您会看到我们已经从和 act_cls=None
from 的最终卷积中删除了 ReLU ( ) ,并在我们添加跳过连接之后将其移动到。这背后的想法是整个 ResNet 块就像一个层,你希望你的激活在你的层之后。convs
idconv
让我们替换我们block
的ResBlock
并尝试一下:
- def block(ni,nf): return ResBlock(ni, nf, stride=2)
- learn = get_learner(get_model())
learn.fit_one_cycle(5, 3e-3)
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 1.973174 | 1.845491 | 0.373248 | 00:08 |
1 | 1.678627 | 1.778713 | 0.439236 | 00:08 |
2 | 1.386163 | 1.596503 | 0.507261 | 00:08 |
3 | 1.177839 | 1.102993 | 0.644841 | 00:09 |
4 | 1.052435 | 1.038013 | 0.667771 | 00:09 |
也好不了多少。但这的全部意义在于让我们能够训练更深层次的模型,而我们还没有真正利用它。要创建一个比方说两倍深的模型,我们需要做的就是 连续block
用两个ResBlock
s 替换 our :
- def block(ni, nf):
- return nn.Sequential(ResBlock(ni, nf, stride=2), ResBlock(nf, nf))
learn = get_learner(get_model())
learn.fit_one_cycle(5, 3e-3)
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 1.964076 | 1.864578 | 0.355159 | 00:12 |
1 | 1.636880 | 1.596789 | 0.502675 | 00:12 |
2 | 1.335378 | 1.304472 | 0.588535 | 00:12 |
3 | 1.089160 | 1.065063 | 0.663185 | 00:12 |
4 | 0.942904 | 0.963589 | 0.692739 | 00:12 |
现在我们取得了很好的进展!
ResNet 论文的作者继续赢得 2015 年 ImageNet 挑战赛。当时,这是迄今为止最重要的年度活动 在计算机视觉中。我们已经看到了另一位 ImageNet 赢家:2013 年的赢家 Zeiler 和 Fergus。值得注意的是,在这两种情况下,突破的起点都是实验观察:在 Zeiler 和 Fergus 的案例中,关于层实际学习的观察,以及在ResNet 作者。这种设计和分析深思熟虑的实验,甚至只是看到意想不到的结果的能力,说,“嗯,这很有趣,”然后,最重要的是,以极大的毅力着手弄清楚地球上正在发生什么,是在许多科学发现的核心。深度学习不像纯数学。这是一个实验性很强的领域,因此重要的是要成为一名强大的实践者,而不仅仅是理论家。
自从 ResNet 被引入以来,它被广泛研究并应用于许多领域。2018 年发表的最有趣的论文之一是 Hao Li 等人的“可视化神经网络的损失景观” 。它表明使用跳跃连接有助于平滑损失函数,这使得训练更容易,因为它避免了陷入非常尖锐的区域。图 14-3显示了论文中的一张令人惊叹的图片,说明了 SGD 必须导航以优化常规 CNN 的崎岖地形(左)与 ResNet 的光滑表面(右)之间的区别。
图 14-3。ResNet 对 loss landscape 的影响(由 Hao Li 等人提供)
在“Bag of Tricks for Image Classification with Convolutional Neural Networks”中,Tong He 等人。研究几乎没有的 ResNet 架构的变体 在参数数量或计算方面的额外成本。通过使用经过调整的 ResNet-50 架构和 Mixup,他们在 ImageNet 上实现了 94.6% 的前 5 准确率,而常规 ResNet-50 的准确率为 92.2% 没有混合。这一结果优于常规 ResNet 模型所获得的结果,后者的深度是原来的两倍(也是速度的两倍,而且更容易过度拟合)。
TOP5-准确率
一个度量标准,用于测试我们想要的标签出现在我们模型的前 5 个预测中的频率。它被用在 ImageNet 竞赛中,因为许多图像包含多个对象,或者包含容易混淆甚至可能被错误标记为类似标签的对象。在这些情况下,查看 top-1 准确性可能是不合适的。然而,最近 CNN 变得非常好,top-5 准确率接近 100%,因此一些研究人员现在也将 top-1 准确率用于 ImageNet。
我们将在扩展到完整的 ResNet 时使用这个调整后的版本,因为它要好得多。它与我们之前的实现略有不同,因为它不是从 ResNet 块开始,而是从几个卷积层开始,然后是一个最大池化层。这就是第一层,称为网络的主干,看起来像:
- def _resnet_stem(*sizes):
- return [
- ConvLayer(sizes[i], sizes[i+1], 3, stride = 2 if i==0 else 1)
- for i in range(len(sizes)-1)
- ] + [nn.MaxPool2d(kernel_size=3, stride=2, padding=1)]
_resnet_stem(3,32,32,64)
[ConvLayer( (0): Conv2d(3, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1)) (1): BatchNorm2d(32, eps=1e-05, momentum=0.1) (2): ReLU() ), ConvLayer( (0): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): BatchNorm2d(32, eps=1e-05, momentum=0.1) (2): ReLU() ), ConvLayer( (0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): BatchNorm2d(64, eps=1e-05, momentum=0.1) (2): ReLU() ), MaxPool2d(kernel_size=3, stride=2, padding=1, ceil_mode=False)]
茎
CNN 的前几层。通常,主干与 CNN 的主体具有不同的结构。
我们使用普通卷积层而不是 ResNet 块的原因是基于对所有深度卷积神经网络的重要认识:绝大多数计算发生在早期层中。因此,我们应该尽可能保持早期层的快速和简单。
要了解为什么在早期层中会发生如此多的计算,请考虑对 128 像素输入图像进行的第一个卷积。如果它是步幅为 1 的卷积,它会将内核应用于 128×128 像素中的每一个像素。这是很多工作!然而,在后面的层中,网格大小可能小到 4×4 甚至 2×2,因此要执行的内核应用程序要少得多。
另一方面,第一层卷积只有 3 个输入特征和 32 个输出特征。由于它是一个 3×3 内核,因此权重中有 3×32×3×3 = 864 个参数。但是最后一个卷积将有 256 个输入特征和 512 个输出特征,产生 1,179,648 个权重!所以第一层包含绝大多数计算,但最后一层包含绝大多数参数。
ResNet 块比普通卷积块需要更多的计算,因为(在 stride-2 的情况下)ResNet 块具有三个卷积和一个池化层。这就是为什么我们想要简单的卷积来开始我们的 ResNet。
我们现在准备展示现代 ResNet 的实现,其中包含“技巧包”。它使用四组 ResNet 块,分别有 64、128、256,然后是 512 个过滤器。每个组都以一个 stride-2 块开始,除了第一个,因为它就在MaxPooling
一层之后:
- class ResNet(nn.Sequential):
- def __init__(self, n_out, layers, expansion=1):
- stem = _resnet_stem(3,32,32,64)
- self.block_szs = [64, 64, 128, 256, 512]
- for i in range(1,5): self.block_szs[i] *= expansion
- blocks = [self._make_layer(*o) for o in enumerate(layers)]
- super().__init__(*stem, *blocks,
- nn.AdaptiveAvgPool2d(1), Flatten(),
- nn.Linear(self.block_szs[-1], n_out))
-
- def _make_layer(self, idx, n_layers):
- stride = 1 if idx==0 else 2
- ch_in,ch_out = self.block_szs[idx:idx+2]
- return nn.Sequential(*[
- ResBlock(ch_in if i==0 else ch_out, ch_out, stride if i==0 else 1)
- for i in range(n_layers)
- ])
该_make_layer
功能只是用来创建一系列 n_layers
块。第一个是从ch_in
到ch_out
与指示的stride
,所有其他都是步幅为 1 的块与ch_out
到ch_out
张量。一旦定义了块,我们的模型就是纯顺序的,这就是为什么我们将它定义为 nn.Sequential
. (暂时忽略expansion
参数;我们将在下一节中讨论它。现在,它是1
,所以它什么都不做。)
各种版本的模型(ResNet-18、-34、-50等)只是变化而已 每个组中的块数。这是 ResNet-18 的定义:
rn = ResNet(dls.c, [2,2,2,2])
让我们稍微训练一下,看看它与之前的模型相比表现如何:
- learn = get_learner(rn)
- learn.fit_one_cycle(5, 3e-3)
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 1.673882 | 1.828394 | 0.413758 | 00:13 |
1 | 1.331675 | 1.572685 | 0.518217 | 00:13 |
2 | 1.087224 | 1.086102 | 0.650701 | 00:13 |
3 | 0.900428 | 0.968219 | 0.684331 | 00:12 |
4 | 0.760280 | 0.782558 | 0.757197 | 00:12 |
尽管我们有更多的通道(因此我们的模型更加准确),但由于我们优化了词干,我们的训练速度和以前一样快。
为了在不占用太多计算或内存的情况下使我们的模型更深,我们可以使用 ResNet 论文为深度为 50 或更多的 ResNet 引入的另一种层:瓶颈层。
瓶颈层不是堆叠内核大小为 3 的两个卷积,而是使用三个卷积:两个 1×1(在beginning 和 end)和一个 3×3,如图 14-4 右边 所示。
图 14-4。常规和瓶颈 ResNet 块的比较(由 Kaiming He 等人提供)
为什么这有用?1×1 卷积要快得多,所以即使这看起来是一个更复杂的设计,这个块的执行速度也比我们看到的第一个 ResNet 块快。这让我们可以使用更多的过滤器:正如我们在插图中看到的,进出过滤器的数量高出四倍(256 而不是 64)。1×1 convs 减少然后恢复通道数量(因此称为bottleneck)。总体影响是我们可以在相同的时间内使用更多的过滤器。
让我们尝试用ResBlock
这个瓶颈设计替换我们的:
- def _conv_block(ni,nf,stride):
- return nn.Sequential(
- ConvLayer(ni, nf//4, 1),
- ConvLayer(nf//4, nf//4, stride=stride),
- ConvLayer(nf//4, nf, 1, act_cls=None, norm_type=NormType.BatchZero))
我们将使用它来创建一个组大小为(3,4,6,3)
. 我们现在需要传入4
的expansion
参数ResNet
,因为我们需要以少四倍的通道开始,以多四倍的通道结束。
像这样的更深层网络在仅训练 5 个时期时通常不会显示出改进,因此我们这次将其增加到 20 个时期以充分利用我们更大的模型。为了真正获得好的结果,让我们也使用更大的图像:
dls = get_data(URLs.IMAGENETTE_320, presize=320, resize=224)
我们不需要做任何事情来解释更大的 224 像素图像;多亏了我们的全卷积网络,它才能正常工作。这也是我们能够在本书前面进行渐进式调整大小的原因——我们使用的模型是完全卷积的,因此我们甚至能够微调使用不同大小训练的模型。我们现在可以训练我们的模型并查看效果:
rn = ResNet(dls.c, [3,4,6,3], 4)
learn = get_learner(rn)
learn.fit_one_cycle(20, 3e-3)
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 1.613448 | 1.473355 | 0.514140 | 00:31 |
1 | 1.359604 | 2.050794 | 0.397452 | 00:31 |
2 | 1.253112 | 4.511735 | 0.387006 | 00:31 |
3 | 1.133450 | 2.575221 | 0.396178 | 00:31 |
4 | 1.054752 | 1.264525 | 0.613758 | 00:32 |
5 | 0.927930 | 2.670484 | 0.422675 | 00:32 |
6 | 0.838268 | 1.724588 | 0.528662 | 00:32 |
7 | 0.748289 | 1.180668 | 0.666497 | 00:31 |
8 | 0.688637 | 1.245039 | 0.650446 | 00:32 |
9 | 0.645530 | 1.053691 | 0.674904 | 00:31 |
10 | 0.593401 | 1.180786 | 0.676433 | 00:32 |
11 | 0.536634 | 0.879937 | 0.713885 | 00:32 |
12 | 0.479208 | 0.798356 | 0.741656 | 00:32 |
13 | 0.440071 | 0.600644 | 0.806879 | 00:32 |
14 | 0.402952 | 0.450296 | 0.858599 | 00:32 |
15 | 0.359117 | 0.486126 | 0.846369 | 00:32 |
16 | 0.313642 | 0.442215 | 0.861911 | 00:32 |
17 | 0.294050 | 0.485967 | 0.853503 | 00:32 |
18 | 0.270583 | 0.408566 | 0.875924 | 00:32 |
19 | 0.266003 | 0.411752 | 0.872611 | 00:33 |
我们现在取得了不错的成绩!尝试添加 Mixup,然后在你去吃午饭的时候训练它一百个 epoch。您将拥有一个非常准确的图像分类器,从头开始训练。
我们在此处展示的瓶颈设计通常仅用于 ResNet-50、-101 和 -152 模型。ResNet-18 和 -34 模型通常使用上一节中看到的非瓶颈设计。然而,我们注意到即使对于较浅的网络,瓶颈层通常也能更好地工作。这只是表明论文中的小细节往往会保留多年,即使它们不是最好的设计!质疑假设和“大家都知道的东西”总是一个好主意,因为这仍然是一个新领域,很多细节并不总是做得很好。