12.ResNet
在我们的前一章中,我们讨论了GoogLeNet架构和Inception模块,这是一个微架构,在整个宏观架构中充当构建块。现在我们将讨论另一种依赖于微架构的网络架构——ResNet。
//截止到P173页
截止到2022.1.29日晚上23:25
//2022.1.30日上午12:07开始阅读
ResNet使用所谓的残差模块来训练卷积神经网络达到以前认为不可能达到的深度。例如,在2014年,VGG16和VGG19架构被认为是非常深的[11]。然而,通过ResNet,我们已经成功地在具有挑战性的ImageNet数据集上训练了100层>网络,在CIFAR-10[24]上训练了1000层以上的>网络。
只有使用“更智能”的权重初始化算法(如Xavier/Glorot[44]和MSRA/He等人。[45])以及身份映射(我们将在本章后面讨论这个概念)才能实现这些深度。鉴于ResNet网络的深度,ResNet在ILSVRC 2015年的所有三项挑战(分类、检测和定位)中都占据了首位,这也许并不令人惊讶。
在本章中,我们将讨论ResNet架构,残差模块,以及残差模块的更新,使其能够获得更高的分类精度。从那里,我们将在CIFAR-10数据集和Tiny ImageNet挑战上实现和训练ResNet的变体——在每种情况下,我们的ResNet实现将超过我们在本书中执行的每一个实验。
12.1 ResNet和残差模块
ResNet架构首先由He等人在2015年的论文《Deep Residual Learning for Image Recognition[24]》中介绍,它已经成为一项很有意义的工作,证明了可以使用标准的SGD和合理的初始化函数来训练极深网络。为了训练深度超过50-100层(在某些情况下,1000层)的网络,ResNet依赖于一种称为剩余模块的微架构。
ResNet的另一个有趣的组件是池层的使用非常少。基于Springenberg et al.[41]的工作,ResNet并不严格依赖最大池操作来减少卷大小。相反,步数为> 1的卷积不仅用于学习权重,但减少了输出体积的空间维度。事实上,在整个架构的实现中,池化只出现了两次:
- 最大池的第一个(也是唯一的)货币出现在网络的早期,以帮助减少空间维度。
- 第二个池化操作实际上是一个平均池化层,用来代替完全连接的层,就像GoogLeNet。
严格地说,只有一个最大池化层——所有其他空间维度的缩减都由卷积层处理。
在本节中,我们将回顾原始残差模块,以及用于训练更深层网络的瓶颈残差模块。在此基础上,我们将讨论He等人在其2016年出版的《深度残留网络[33]中的身份映射》(Identity Mappings in Deep residual Networks[33])中对原始残留模块的扩展和更新,这使我们能够进一步提高分类精度。在本章的后面,我们将使用Keras从头开始实现ResNet。
12.1.1深入分析:残差模块和瓶颈
He等人在2015年引入的原始残差模块依赖于恒等映射,即将原始输入输入到模块中,并将其添加到一系列操作的输出中。这个模块的图形描述如图12.1(左)所示。注意这个模块是如何只有两个分支的,不像GoogLeNet的Inception模块中的四个分支。此外,这个模块是高度简单化的。
在模块的顶部,我们接受一个输入到模块(即网络中的前一层)。右边的分支是一个“线性捷径”——它将输入连接到模块底部的加法操作。然后,在残差模块的左分支上,我们应用一系列卷积(所有卷积都是3 × 3)、激活和批量归一化。在构造卷积神经网络时,这是一个相当标准的模式。
但ResNet的有趣之处在于,He等人建议在CONV、RELU和BN层的输出中加入原始输入。我们称这种添加为恒等式映射,因为输入(恒等式)被添加到一系列操作的输出中。这也是使用“剩余”一词的原因。“残留”输入被添加到一系列层操作的输出中。输入节点和添加节点之间的连接称为快捷方式。请注意,我们并没有像前几章那样,在通道维度上进行连接。相反,我们在模块底部两个分支之间执行简单的1 + 1 = 2的加法。
传统的神经网络层可以被看作是学习函数y = f (x),而残差层试图通过f (x) + id(x) = f (x) + x逼近y,其中id(x)是恒等函数。这些剩余的层从身份函数开始,随着网络的学习,逐渐变得更加复杂。这种类型的剩余学习框架允许我们训练的网络实质上比以前提出的网络架构更深。
此外,由于每个剩余模块都包含了输入,因此网络学习速度更快,学习速率也更高。ResNet实现的基本学习速率从1e−1开始是很常见的。对于大多数架构,如AlexNet或VGGNet,这样高的学习速率几乎可以保证网络不会收敛。但由于ResNet通过标识映射依赖于剩余模块,因此这种更高的学习率是完全可能的。
//截止到P175页
截止到2022.1.30日晚上15:45
//2022.1.30日晚上18:05开始阅读
在2015年的同样工作中,He等人还包括了对原始剩余模块的扩展,称为瓶颈(图12.1,右)。这里我们可以看到同样的恒等式映射正在发生,只是现在残留模块左分支的CONV层已经更新:
- 我们使用了三层而不是两层CONV层。2. 第一个和最后一个CONV层是1 × 1卷积。3.在前两个CONV层中学习的数字滤波器是最后一个CONV中学习的数字滤波器的1/4。
为了理解为什么我们称其为“瓶颈”,考虑下面的图,其中两个剩余模块相互堆叠,一个剩余模块馈入下一个(图12.2)。
第一个剩余模块接受大小为M × N × 64的输入体积(本例中的实际宽度和高度是任意的)。第一个残差模块中的三个CONV层分别学习了K = 32、32和128个滤波器。在应用第一个剩余模块后,我们的输出体积大小是M × N × 128,然后输入到第二个剩余模块。
在第二个剩余模块中,三个CONV层中每个层学习的数量滤波器在K分别为32、32和128时保持不变。然而,请注意32 < 128,这意味着我们实际上在1 × 1和3 × 3 CONV层中减小了体积大小。这个结果的好处是使3 × 3的瓶颈层具有更小的输入和输出尺寸。
最后的1 × 1 CONV比前两个CONV层应用4倍数量的滤波器,因此再次增加维度,这就是为什么我们把这个对剩余模块的更新称为“瓶颈”技术。当构建我们自己的剩余模块时,通常会提供伪代码,如residual_module(K=128),这意味着最后的CONV层将学习128过滤器,而前两个将学习128/4 = 32过滤器。这种符号通常更容易使用,因为我们知道,瓶颈CONV层将学习数字滤波器的1/4作为最后的CONV层。
当涉及到ResNet训练时,我们通常使用残留模块的瓶颈变体,而不是原始版本,特别是对于带有> 50层的ResNet实现。
12.1.2 重新思考剩余模块
2016年,He等人发表了第二篇关于残差模块的论文《深度残差网络[33]中的身份映射》。该出版物描述了一项全面的研究,从理论上和经验上,对卷积、激活和批归一化层在剩余模块本身的排序。最初,剩余模块(带有瓶颈)看起来像图12.3(左)。
原剩余模块与瓶颈接受一个输入(ReLU激活地图),然后应用一系列(CONV = > BN = > ReLU) * 2 = > CONV = > BN之前将该输出添加到原始输入和应用afinal ReLU激活(然后送入下一个网络中的剩余模块)。然而,He等人在2016年的研究中发现,有一种更优的层序能够获得更高的准确性——这种方法被称为预激活。
在剩余模块的预激活版本中,我们删除了模块底部的ReLU,并重新排序批量归一化和激活,使它们出现在卷积之前(图12.3,右)。
现在,我们不再从卷积开始,而是应用一系列(BN => RELU => CONV) * 3(当然,假设使用了瓶颈)。剩余模块的输出现在是附加操作,该操作随后被馈送到网络中的下一个剩余模块(因为剩余模块相互堆叠)。
我们称这一层排序为预激活,因为我们的ReLUs和批归一化放在卷积之前,这与在卷积之后应用ReLUs和批归一化的典型方法相反。在下一节中,我们将使用瓶颈和预激活从头开始实现ResNet。
12.2 实现ResNet
现在我们已经回顾了ResNet架构,让我们继续在Keras中实现它。对于这个特定的实现,我们将使用残留模块的最新版本,包括瓶颈和预激活。要更新项目结构,请在nn中创建一个名为resnet.py的新文件。pyimagesearch的conv子模块-这是我们ResNet实现的地方:
# import the necessary packages
2 from keras.layers.normalization import BatchNormalization
3 from keras.layers.convolutional import Conv2D
4 from keras.layers.convolutional import AveragePooling2D
5 from keras.layers.convolutional import MaxPooling2D
6 from keras.layers.convolutional import ZeroPadding2D
7 from keras.layers.core import Activation
8 from keras.layers.core import Dense
9 from keras.layers import Flatten
10 from keras.layers import Input
11 from keras.models import Model
在构建卷积神经网络时,我们首先引入相当标准的类和函数集。但是,我想让您注意第12行,在这里我们导入了add函数。在剩余模块中,我们需要将两个分支的输出相加,这将通过这个add方法来完成。我们还将在第13行导入l2函数,以便执行l2权值衰减。正则化在训练ResNet时非常重要,因为由于网络的深度,它很容易发生过拟合。
class ResNet:
17 @staticmethod
18 def residual_module(data, K, stride, chanDim, red=False,
19 reg=0.0001, bnEps=2e-5, bnMom=0.9):
ResNet的具体实现受到了He等人Caffe分发[46]和Wei Wu[47]的mxnet实现的启发,因此我们将尽可能密切关注他们的参数选择。查看residual_module,我们可以看到这个函数接受的参数比之前的任何一个函数都多——让我们详细回顾一下每个函数。
数据参数只是剩余模块的输入。K值定义了最终的CONV在瓶颈中学习的滤波器数量。根据He等人的论文,前两个CONV层将学习K / 4滤波器。步幅控制卷积的步幅。我们将使用这个参数来帮助我们减少体积的空间维度,而不需要使用max pooling。
然后,我们有chanDim参数,它定义了执行批量归一化的轴——这个值稍后在构建函数中指定,基于我们是使用“通道最后”还是“通道优先”排序。
并不是所有剩余的模块都将负责减少我们空间体积的维数——red(即“reduce”)布尔值将控制我们是否在减少空间维数(True)或否(False)。
然后,我们可以通过reg为剩余模块中的所有CONV层提供正则化强度。bnEps参数控制在归一化输入时避免“除零”误差的ε。在Keras中,ε的默认值为0.001;但是,对于我们的特定实现,我们将允许大幅减少这个值。bnMom控制移动平均线的动量。在Keras内部,这个值通常默认为0.99,但He等人和Wei Wu建议将这个值降低到0.9。
现在residual_module的参数已经定义好了,让我们来看看函数体:
# the shortcut branch of the ResNet module should be
21 # initialize as the input (identity) data
22 shortcut = data
23
24 # the first block of the ResNet module are the 1x1 CONVs
25 bn1 = BatchNormalization(axis=chanDim, epsilon=bnEps,
26 momentum=bnMom)(data)
27 act1 = Activation("relu")(bn1)
conv1 = Conv2D(int(K * 0.25), (1, 1), use_bias=False,
29 kernel_regularizer=l2(reg))(act1)
在第22行,我们在剩余模块中初始化快捷方式,它只是一个对输入数据的引用。稍后我们将把这个快捷方式添加到瓶颈+预激活分支的输出中。
瓶颈分支的第一个预激活可以在第25-29行看到。在这里,我们应用了一个批量归一化层,然后是ReLU激活,然后使用K/4 totalfilters进行1 × 1卷积。你还会注意到我们通过use_bias=False从CONV层中排除了偏差项。为什么我们要故意省略偏见一项呢?根据He等人的研究,偏差存在于紧随卷积[48]的BN层中,因此不需要引入第二个偏差项。
接下来,我们在瓶颈中有第二个CONV层,这一层负责学习K/ 4,3 × 3个滤波器:
# the second block of the ResNet module are the 3x3 CONVs
32 bn2 = BatchNormalization(axis=chanDim, epsilon=bnEps,
33 momentum=bnMom)(conv1)
34 act2 = Activation("relu")(bn2)
35 conv2 = Conv2D(int(K * 0.25), (3, 3), strides=stride,
36 padding="same", use_bias=False,
37 kernel_regularizer=l2(reg))(act2
瓶颈中的最后一个块学习了k个滤波器,每个滤波器为1 × 1:
# the third block of the ResNet module is another set of 1x1
40 # CONVs
41 bn3 = BatchNormalization(axis=chanDim, epsilon=bnEps,
42 momentum=bnMom)(conv2)
43 act3 = Activation("relu")(bn3)
44 conv3 = Conv2D(K, (1, 1), use_bias=False,
45 kernel_regularizer=l2(reg))(act3)
关于为什么我们称其为带有“预激活”的“瓶颈”的更多细节,请参见上面的12.1节。
下一步是看看我们是否需要减少空间维度,从而减轻应用max pooling的需要:
# if we are to reduce the spatial size, apply a CONV layer to
48 # the shortcut
49 if red:
50 shortcut = Conv2D(K, (1, 1), strides=stride,
51 use_bias=False, kernel_regularizer=l2(reg))(act1)
如果我们被要求减少空间维度,我们将使用步幅> 1的卷积层(应用于捷径)来这么做。
瓶颈中最终的conv3的输出是与快捷方式相加,从而作为residual_module的输出:
# add together the shortcut and the final CONV
54 x = add([conv3, shortcut])
55
56 # return the addition as the output of the ResNet module
57 return x
residual_module将作为我们创建深度残留网络的构建块。让我们继续在build方法中使用这个构建块:
@staticmethod
60 def build(width, height, depth, classes, stages, filters,
61 reg=0.0001, bnEps=2e-5, bnMom=0.9, dataset="cifar"):
正如我们的residual_module需要比以前的微架构实现更多的参数一样,我们的构建函数也是如此。宽度、高度和深度类都控制数据集中图像的输入空间维度。类变量决定了我们的网络应该学习多少类——这些变量你已经见过了。
有趣的是stage和filter参数,它们都是列表。构建ResNet架构时,我们会堆积很多剩余模块上的彼此(使用相同数量为每个堆栈offilters),其次是减少体积的空间维度,这个过程一直持续到我们准备申请平均池和softmax分类器。
为了更清楚地说明这一点,让我们假设stage =(3, 4, 6), filters=(64, 128, 256, 512)。第一个filter值64,将被应用于唯一一个不属于剩余模块的CONV层(即网络中的第一个卷积层)。然后,我们将三个剩余模块叠加在一起——每个剩余模块将学习K = 128滤波器。体积的空间维度将被降低,然后我们将进入第二个阶段,我们将把四个剩余模块堆叠在一起,每个模块负责学习K = 256滤波器。在这四个剩余模块之后,我们将再次降维,并进入阶段列表的最后一个条目,指示我们将六个剩余模块叠加在一起,每个剩余模块将学习K = 512。
在列表中指定两个阶段和过滤器(而不是硬编码它们)的好处是,我们可以轻松地利用循环来构建非常深入的网络体系结构,而不引入代码膨胀——这一点将在后面的实现中变得更加清楚。
最后,我们有数据集参数,它被假设为一个字符串。根据我们正在构建ResNet的数据集,我们可能希望在开始堆叠剩余模块之前应用更多/更少的卷积和批处理归一化。在下面的12.5节中,我们将了解为什么我们可能想要改变卷积层的数量,但是目前,您可以安全地忽略这个参数。
接下来,让我们根据我们是使用“channels last”(第64和65行)还是“channelsfirst”(第69-71行)的顺序来初始化我们的inputShape和chanDim。
# initialize the input shape to be "channels last" and the
63 # channels dimension itself
64 inputShape = (height, width, depth)
65 chanDim = -1
66
67 # if we are using "channels first", update the input shape
68 # and channels dimension
//2022.1.31日上午10:35开始阅读
if K.image_data_format() == "channels_first":
70 inputShape = (depth, height, width)
71 chanDim = 1
现在我们已经准备好为ResNet实现定义Input了:
# set the input and apply BN
74 inputs = Input(shape=inputShape)
75 x = BatchNormalization(axis=chanDim, epsilon=bnEps,
76 momentum=bnMom)(inputs)
77
78 # check if we are utilizing the CIFAR dataset
79 if dataset == "cifar":
80 # apply a single CONV layer
81 x = Conv2D(filters[0], (3, 3), use_bias=False,
82 padding="same", kernel_regularizer=l2(reg))(x)
不像我们在这本书中看到的以前的网络架构(其中第一层通常是CONV),我们看到ResNet使用BN作为第一层。对输入应用批处理规范化的原因是增加了规范化级别。事实上,对输入本身执行批处理规范化有时可以消除对输入应用平均规范化的需要。在这两种情况下,第75行和76行上的BN作为一个增加的规范化级别。
然后,我们在第81行和第82行上应用一个CONV层。这个CONV层将学习总共的过滤器[0],3 × 3过滤器(记住,过滤器是一个列表,所以这个值在构建体系结构时通过构建方法指定)。
您还会注意到,我检查了一下是否使用了CIFAR-10数据集(第79行)。在本章的后面,我们将更新这个if块来包含一个用于Tiny ImageNet的elif语句。由于Tiny ImageNet的输入维度更大,在我们开始堆叠剩余模块之前,我们将应用一系列卷积、批处理标准化和max pooling (ResNet体系结构中唯一的max pooling)。然而,目前我们只使用CIFAR-10数据集。
让我们继续,开始把残留层叠加在一起,这是ResNet架构的基石:
# loop over the number of stages
85 for i in range(0, len(stages)):
86 # initialize the stride, then apply a residual module
87 # used to reduce the spatial size of the input volume
88 stride = (1, 1) if i == 0 else (2, 2)
89 x = ResNet.residual_module(x, filters[i + 1], stride,
90 chanDim, red=True, bnEps=bnEps, bnMom=bnMom)
91
92 # loop over the number of layers in the stage
93 for j in range(0, stages[i] - 1):
94 # apply a ResNet module
95 x = ResNet.residual_module(x, filters[i + 1],
96 (1, 1), chanDim, bnEps=bnEps, bnMom=bnMom)
在第85行,我们开始循环遍历阶段列表。记住,阶段列表中的每个条目都是一个整数,表示剩余模块堆叠在一起的数量。在Springenberg等人的工作之后,ResNet试图尽可能减少池的使用,依靠CONV层来减少体积的空间维度。
为了在不共用层的情况下减小卷大小,我们必须在第88行设置卷积的步幅。如果这是该阶段的第一个入口,我们将stride设置为(1,1),表示不应执行下采样。然而,对于后续的每个阶段,我们将应用一个步幅为(2,2)的剩余模块,这将允许我们减少体积大小。
从那里开始,我们将循环查看93行当前阶段的层数(即,将相互堆叠的剩余模块的数量)。每个剩余模块将学习的过滤器数量由过滤器列表中相应的条目控制。我们使用i + 1作为过滤器的索引是因为第一个过滤器值在第81和82行使用。其余的过滤器值对应于每个阶段的数量过滤器。一旦我们将stage [i]剩余模块堆叠在一起,我们的for循环将我们带回到第88-90行,在那里我们减少了体积的空间维度,并重复这个过程。
此时,我们的卷大小已经减少到8 x 8 x类(您可以通过计算每个层的输入/输出卷大小来验证这一点,或者更好的方法是使用Starter Bundle第19章中的plot_model函数)。
为了避免使用密集的全连接层,我们将使用平均池来减少体积大小为1 x 1 x类:
# apply BN => ACT => POOL
99 x = BatchNormalization(axis=chanDim, epsilon=bnEps,
100 momentum=bnMom)(x)
101 x = Activation("relu")(x)
102 x = AveragePooling2D((8, 8))(x)
从那里,我们创建一个密集的层,包含我们将要学习的类的总数,然后应用softmax激活来获得最终的输出概率:
# softmax classifier
105 x = Flatten()(x)
106 x = Dense(classes, kernel_regularizer=l2(reg))(x)
107 x = Activation("softmax")(x)
108
109 # create the model
110 model = Model(inputs, x, name="resnet")
111
112 # return the constructed network architecture
113 return model
然后将构造完整的ResNet模型返回给第113行上的调用函数。
12.3 ResNet在CIFAR-10数据集上的测试
除了在完整的ImageNet数据集上训练ResNet上较小的变体外,我从未尝试在CIFAR-10上训练ResNet(或斯坦福的Tiny ImageNet挑战,我们将在本节中看到)。由于这个事实,我决定将本节和下一节作为坦率的案例研究,在这里我将展示我个人的经验法则和多年来训练神经网络的最佳实践。
这些最佳实践允许我用最初的计划来处理新问题,对其进行迭代,并最终得出具有良好准确性的解决方案。以CIFAR-10为例,我们将能够复制He等人的性能,并在其他最先进的方法[49]中占有一席之地。
12.3.1 使用CTRL + C暂停机制在CIFAR-10数据集上训练ResNet
每当我开始一组新的实验时,无论是用我不熟悉的网络架构,还是用我从未使用过的数据集,或者两者都用,我总是从ctrl + c训练方法开始。使用这种方法,我可以以一个初始学习速率(以及相关的超参数集)开始训练,监控训练,并根据结果快速调整学习速率。当我完全不确定给定的体系结构需要多少个epoch才能获得合理的精度或特定的数据集时,这种方法尤其有用。
对于CIFAR-10,我有过经验(在阅读了本书的所有其他章节后,您也有过经验),所以我很有信心它将需要60-100个时代,但我不太确定,因为我以前从未就CIFAR-10培训过ResNet。
因此,我们的前几个实验将依赖于ctrl + c训练方法来缩小我们应该使用的超参数。一旦我们适应了我们的超参数集,我们将切换到一个特定的学习速率衰减计划,希望从训练过程中榨取最后一点准确性。
首先在第6-17行导入所需的Python包。因为我们将使用ctrl + c方法进行训练,所以我们将确保在训练过程中导入EpochCheckpoint类(第8行)来将ResNet权重序列化到磁盘,从而允许我们从一个特定的检查点停止和重新开始训练。由于这是我们第一次使用ResNet进行实验,我们将使用SGD优化器(第11行)——时间会告诉我们是否决定切换并使用不同的优化器(我们将让结果决定这一点)。
下一步是从磁盘加载CIFAR-10数据集(预拆分为训练和测试),进行均值减法,对整数标签进行一次热编码为向量:
# load the training and testing data, converting the images from
# integers to floats
print("[INFO] loading CIFAR-10 data...")
((trainX, trainY), (testX, testY)) = cifar10.load_data()
trainX = trainX.astype("float")
testX = testX.astype("float")
# apply mean subtraction to the data
mean = np.mean(trainX, axis=0)
trainX -= mean
testX -= mean
# convert the labels from integers to vectors
lb = LabelBinarizer()
trainY = lb.fit_transform(trainY)
testY = lb.transform(testY)
在这种情况下,我们从第一个时代就开始训练ResNet,我们需要实例化网络架构:
# if there is no specific model checkpoint supplied, then initialize
# the network (ResNet-56) and compile the model
if args["model"] is None:
print("[INFO] compiling model...")
opt = SGD(lr=1e-1)
model = ResNet.build(32, 32, 3, 10, (9, 9, 9),(64, 64, 128, 256), reg=0.0005)
model.compile(loss="categorical_crossentropy", optimizer=opt,
metrics=["accuracy"])
首先,看看第58行我们的SGD优化器的学习速度——以1e−1的速度计算,这个学习速度是我们在本书中使用过的最大的(一个数量级)。我们能够获得如此高的学习率的原因是由于剩余模块内建的标识映射。如此高的学习率(通常)不适用于AlexNet、VGG等网络。
然后在第59行和第60行实例化ResNet模型。这里我们可以看到,网络将接受宽度为32像素、高度为32像素、深度为3的输入图像(CIFAR-10数据集中的每个RGB通道都有一个)。由于CIFAR-10数据集有10个类,我们将学习10个输出标签。
我们需要提供的下一个参数是(9,9,9),或者说是体系结构中阶段的数量。这个元组表明我们将学习三个阶段,每个阶段包含9个相互叠加的剩余模块。在每个阶段之间,我们将应用一个额外的剩余模块,以减少体积大小。
下一个参数(64,64,128,256)是CONV层将学习的滤波器数量。第一个CONV层(在应用任何残差模型之前)将学习K = 64滤波器。其余的条目64、128和256对应于每个剩余模块阶段将学习的数量过滤器。例如,前9个剩余模块将学习K = 64filters。第二组9个剩余模块将学习K = 128个过滤器。最后,最后一组九个剩余模块将学习K = 256个滤波器。我们将提供给ResNet的最后一个参数是reg,或我们用于权重衰减的L2正则化强度-这个值是至关重要的,因为它将使我们能够防止过拟合。
当我们从一个特定的epoch开始重新开始训练时,我们需要从磁盘加载网络权值,并更新学习速率:
# otherwise, load the checkpoint from disk
65 else:
66 print("[INFO] loading {}...".format(args["model"]))
67 model = load_model(args["model"])
68
69 # update the learning rate
70 print("[INFO] old learning rate: {}".format(
71 K.get_value(model.optimizer.lr)))
72 K.set_value(model.optimizer.lr, 1e-5)
73 print("[INFO] new learning rate: {}".format(
74 K.get_value(model.optimizer.lr)))
让我们也构建一组回调函数,这样我们就可以(1)每五个epoch设置一个检查点ResNet权值,(2)监控训练:
# construct the set of callbacks
77 callbacks = [
78 EpochCheckpoint(args["checkpoints"], every=5,
79 startAt=args["start_epoch"]),
80 TrainingMonitor("output/resnet56_cifar10.png",
81 jsonPath="output/resnet56_cifar10.json",
82 startAt=args["start_epoch"])]
最后,我们将以128个批处理的规模训练我们的网络:
# train the network
85 print("[INFO] training network...")
86 model.fit_generator(
87 aug.flow(trainX, trainY, batch_size=128),
88 validation_data=(testX, testY),
89 steps_per_epoch=len(trainX) // 128, epochs=10,
90 callbacks=callbacks, verbose=1)
现在resnet_cifar10.py脚本已经编写好,让我们继续使用它运行实验。
实验1
在我第一次使用CIFAR-10的试验中,我担心网络中的过滤器数量,尤其是过度拟合。由于这种考虑,我的初始过滤器列表由(16,16,32,64)以及剩余模块的(9,9,9)级组成。我还应用了非常少量的L2正则化(reg=0.0001)——我知道需要正则化,但我不确定正确的数量(还)。ResNet使用SGD进行训练,基本学习率为1e−1,动量项为0.9。
我开始使用以下命令进行训练:
在过去的时代50中,我注意到训练损失开始放缓,验证损失也开始出现波动(两者之间的差距越来越大)(图12.4,左上角)。我停止训练,将学习率降低到1e−2,然后继续训练:
学习率的下降被证明是非常有效的,稳定了验证损失,但在第75纪元左右,训练集上的过拟合开始出现(在使用cifar10时不可避免)(图12.4,右上)。在纪元75之后,我再次停止训练,将学习速率降低到1e−3,并允许ResNet继续训练10个纪元:
最后的图如图12.4(底部)所示,我们在验证集中达到了89.06%的准确性。对于我们的第一个实验来说,89.06%是一个好的开始;然而,这并没有谷歌网络在第11章中达到的90.81%高。此外,He等人报道了ResNet在CIFAR-10上的准确率为93%,所以我们显然还有一些工作要做。
12.3.2 实验2
我们之前的实验达到了89.06%的合理精度,但我们需要更高的精度。我没有增加网络的深度(通过添加更多的阶段),而是决定为每个CONV层添加更多的过滤器。因此,我的过滤器列表被更新为(16、64、128、256)。
注意,所有剩余模块中的滤波器数量比之前的实验增加了一倍(第一个CONV层中的滤波器数量保持不变)。再次使用SGD来训练动量项为0.9的网络。我还把正则化项保持在0.0001。
在图12.5(左上)中,您可以找到我的前40个时代的图表:我们可以清楚地看到训练损失和验证损失之间的差距,但总体上,验证准确性仍然与训练准确性保持一致。为了提高准确性,我决定将学习速度从1e−1降低到1e−2,并再训练5个阶段——结果是学习完全停滞不前(右上)。再次将学习率从1e−2降低到1e−3,甚至由于验证损失的轻微增加而导致过拟合(下)。
实验3
在这个时候,我开始更加适应在CIFAR-10上训练ResNet。显然,增加滤镜起到了作用,但第一次学习速率下降后的学习停滞仍然令人不安。我确信学习率的缓慢线性下降将有助于解决这个问题,但我不相信我已经获得了一组良好的超参数来保证切换到学习率衰减。
//2022.2.1下午16:07开始阅读
相反,我决定增加在第一个CONV层学到的过滤器数量到64(从16个),把过滤器列表变成(64,64,128,256)。过滤器的增加在第二个实验中起到了帮助作用,第一个CONV层也没有理由错过这些好处。SGD优化器的初始学习速率为1e−1,动量为0.9。
此外,我还决定大幅度地将正则化从0.0001增加到0.0005。我怀疑允许网络训练更长的时间将导致更高的验证准确性-使用更大的正则化术语将可能使我训练更长的时间。
我也在考虑降低我的学习速度,但考虑到网络在1e−1的学习率时创造没有问题的牵引,似乎不值得降低到1e−2。这样做可能会使培训稳定(即验证损失/准确性的波动较小),但最终会导致培训完成后的准确性较低。如果我更大的学习率+更大的正则化项的怀疑被证明是正确的,那么切换到学习率衰减以避免在数量级下降后停滞是有意义的。但在我做出这个转变之前,我首先需要证明我的直觉。
正如我对前80个时代的描述(图12.6,左上)所示,当验证很快偏离训练时,肯定会出现过度拟合。然而,这里有趣的是,虽然过度拟合无疑会发生(因为训练损失比验证损失下降得快得多),但我们能够在没有验证损失开始增加的情况下训练网络更长时间。
在80世纪之后,我停止了训练,将学习速率降低到1e−2,然后再训练10个时代(图12.6,右上)。我们看到了准确性的下降、损失和增加,但从那时起,验证损失/准确性趋于平稳。此外,我们可以开始看到验证损失增加,这是过度拟合的明显迹象。
为了验证过拟合确实存在,我在第90阶段停止训练,将学习速率降低到1e-3,然后再训练10个阶段(图12.6,底部)。当然,这是过度拟合的信号:验证损失增加,而训练损失减少/保持不变。
然而,非常有趣的是,在第100个epoch之后,我们获得了91.83%的验证精度,高于我们的第三个实验。缺点是我们过度拟合——我们需要一种方法在不过度拟合的情况下保持这个精度水平(并增加它)。为了做到这一点,我决定从ctrl + c训练切换到学习速率衰减。
12.4 在CIFAR-10数据集上使用学习率衰减训练ResNet
在这一点上,似乎我们已经得到了我们可以使用标准的ctrl + c训练。我们也能看到,当我们训练的时间更长时,我们最成功的实验发生在80-100个时代的范围内。然而,我们需要克服两个主要问题:
- 每当我们将学习速率降低一个数量级并重新开始训练时,我们就会在准确性上获得一个不错的提升,但随后我们很快就会停滞不前。
- 我们过度拟合。
为了解决这些问题,并进一步提高准确性,一个很好的实验是在大量时间内线性降低学习速率,通常与你最长的ctrl + c实验相同(如果不是稍微长一点的话)。首先,我们打开一个新文件,命名为resnet_cifar10_decay.py,并插入以下代码:
第2行配置了matplotlib,这样我们就可以在后台保存图形。然后在第6-16行导入其余的Python包。看一下第10行,在那里我们导入了LearningRateScheduler,这样我们就可以为训练过程定义一个自定义的学习速率衰减。然后,我们设置了一个较高的系统递归限制,以避免Theano后端出现任何问题(以防您正在使用它)。
下一步是定义学习速率衰减时间表:
我们将以1e−1的基本学习速率对我们的网络进行总计100个epoch的训练。poly_decay函数将在100个epoch的过程中线性衰减我们的1e−1学习速率。这是一个线性衰减,因为我们设置幂=1。有关学习速率计划的更多信息,请参阅Starter Bundle的第16章和实践者Bundle的第11章。
# construct the argument parse and parse the arguments
40 ap = argparse.ArgumentParser()
41 ap.add_argument("-m", "--model", required=True,
42 help="path to output model")
43 ap.add_argument("-o", "--output", required=True,
44 help="path to output directory (logs, plots, etc.)")
45 args = vars(ap.parse_args())
——model开关控制训练后最终序列化模型的路径,而——output是基本目录,我们将存储任何日志、图表等。
我们现在可以加载CIFAR-10数据集并对其进行均值归一化:
以及初始化我们的ImageDataGenerator的数据参数:
# construct the image generator for data augmentation
65 aug = ImageDataGenerator(width_shift_range=0.1,
66 height_shift_range=0.1, horizontal_flip=True,
67 fill_mode="nearest")
我们的回调列表将由一个TrainingMonitor和一个LearningRateScheduler组成,其中poly_decay函数是唯一的参数——这个类将允许我们在训练时降低学习速率。
# construct the set of callbacks
70 figPath = os.path.sep.join([args["output"], "{}.png".format(
71 os.getpid())])
72 jsonPath = os.path.sep.join([args["output"], "{}.json".format(
73 os.getpid())])
74 callbacks = [TrainingMonitor(figPath, jsonPath=jsonPath),
75 LearningRateScheduler(poly_decay)]
然后,我们将用12.6节中找到的最佳参数实例化ResNet(三个堆栈,9个剩余模块:在剩余模块之前的第一CONV层有64个过滤器,每个堆栈各自的剩余模块有64个、128个和256个过滤器):
实验4
正如前一节的代码所示,我们将使用SGD优化器,其基本学习率为1e−1,动量项为0.9。我们将训练ResNet总共100个epoch,线性地将线性速率从1e−1降低到零。为了在CIFAR-10上使用学习率衰减来训练ResNet,我执行了以下命令:
在培训完成后,我查看了一下图表(图12.7)。在之前的实验中,训练和验证损失在早期就开始发散,但更重要的是,在初始发散之后,差距大致保持不变。这个结果很重要,因为它表明我们的过拟合是被控制的。我们必须接受在CIFAR-10训练时我们会过度拟合的事实,但我们需要控制这种过度拟合。通过应用学习率衰减,我们能够成功地做到这一点。
问题是,我们是否获得了更高的分类精度?要回答这个问题,我们来看看过去几个时代的产出:
在100世纪之后,ResNet在我们的测试集上的准确率达到了93.58%。这个结果大大高于我们之前的两个实验,更重要的是,它允许我们在CIFAR-10上训练ResNet时复制He等人的结果。
看看CIFAR-10排行榜[49],我们看到He等人达到了93.57%的准确度[24],与我们的结果几乎相同(图12.8)。红色箭头表示我们的准确性,安全进入排行榜前10名。
12.5 ResNet在Tiny ImageNet
在本节中,我们将在斯坦福大学的cs231n Tiny ImageNet挑战中训练ResNet架构(带有瓶颈和预激活)。与本章前面的ResNet + CIFAR-10实验相似,我以前从来没有在Tiny ImageNet上训练过ResNet,所以我将使用相同的实验过程:
- 以ctrl + c为基础的训练开始,以获得一个基线。
- 如果在学习速率下降一个数量级后出现停滞/稳定,那么切换到学习速率衰减。
鉴于我们已经在cifar10中应用了类似的技术,我们应该能够节省一些时间,更早地注意到过度拟合和停滞不前的迹象。也就是说,让我们从回顾这个项目的目录结构开始,它与前一章的googlet和Tiny ImageNet挑战几乎相同:
12.5.1 更新ResNet架构
如果我们正在为Tiny ImageNet实例化ResNet,我们需要添加一些额外的层。首先,我们应用5 × 5的CONV层来学习更大的特征图(在完整的ImageNet数据集实现中,我们实际上是学习7 × 7的过滤器)。
接下来,我们在ReLU激活之后应用批处理规范化。Max pooling, ResNet架构中唯一的Max pooling层,应用于93行,大小为3 × 3,步幅为2 × 2。结合前面的零填充层(第92行),池化确保我们的输出空间体积大小为32 × 32,与来自cifar10的输入图像的空间尺寸完全相同。验证输出体积大小为32 × 32确保我们可以轻松重用ResNet实现的其余部分,而无需做任何额外的更改。
12.5.2 使用CTRL + C 方法在Tiny ImageNet数据集上训练ResNet
考虑到Tiny ImageNet将需要比CIFAR-10更有鉴别性的过滤器,我还更新了过滤器列表,以了解每个CONV层的更多过滤器:(64,128,256,512)。这个列表滤波器意味着第一个CONV层(在任何剩余模块之前)将学习总共64个5 × 5滤波器。在那里,三个剩余的模块将相互叠加,每个模块负责学习K = 128个滤波器。然后降维,然后堆叠四个剩余模块,这次学习K = 256个滤波器。再次降低体积的空间维数,然后将6个剩余模块进行堆叠,每个模块学习K = 512filters。
我们还将应用0.0005的正则化强度,因为正则化似乎有助于我们在CIFAR-10上训练ResNet。为了训练网络,SGD将以1e−1的基本学习率和0.9的动量项使用。
如果我们已经停止训练,更新了任何超参数(比如学习速率),并且希望重新开始训练,下面的代码块将为我们处理这个过程:
# otherwise, load the checkpoint from disk
65 else:
66 print("[INFO] loading {}...".format(args["model"]))
67 model = load_model(args["model"])
68
69 # update the learning rate
70 print("[INFO] old learning rate: {}".format(
71 K.get_value(model.optimizer.lr)))
72 K.set_value(model.optimizer.lr, 1e-5)
73 print("[INFO] new learning rate: {}".format(
74 K.get_value(model.optimizer.lr)))
我们训练ResNet所需要的epoch的确切数量目前还不清楚,所以我们会将epoch设置为一个较大的数字,并根据需要进行调整。
实验1
在监测了前25个阶段的训练后,很明显,训练损失开始有点停滞(图12.9,左上角)。为了克服这种停滞状态,我停止了训练,把我的学习速度从1e−1降低到1e−2,然后继续训练:
从25-35时代开始,继续以这种较低的学习速度进行训练。我们可以立即看到将学习速率降低一个数量级的好处——损失急剧下降,准确性得到了很好的提升。右上的(图12.9)。
然而,在最初的撞击之后,训练损失继续以比验证损失快得多的速度下降。我再次在第35阶段停止训练,将学习速度从1e−2降低到1e−3,然后继续训练:
当我开始注意到明显的过度拟合迹象时,我只允许ResNet再训练5个时期(图12.9,底部)。在1e−3变化时,损失略有下降,然后开始增加,一直以来,训练损失以更快的速度下降——这是过度拟合的一个迹象。在这一点上,我完全停止了训练,并注意到验证的准确性为53.14%。
为了确定测试集的准确性,我执行了以下命令(记住rank_accuracy.py脚本与第11章GoogLeNet中的脚本相同:
左上角:实验1中在Tiny ImageNet上训练ResNet时的前25个epoch。右上方:未来10个时代。下图:最后的5个纪元。当验证损失在最后阶段开始增加时,请注意过度拟合的迹象。
如输出所示,我们在测试集中获得了53.10%的rank-1准确度。这个第一个实验还不错,因为我们已经接近google网络+ Tiny ImageNet的准确性。鉴于在Tiny ImageNet上应用学习速率衰减的成功,我立即决定将同样的过程应用到ResNet上。
12.5.3 在Tiny ImageNet数据集上使用学习率下降训练ResNet
这里我们指出,我们将以1e−1的基本学习速率(第27行)训练最多75个epoch(第26行)。poly_decay函数在第29行定义,它只接受一个参数,即当前历元数。我们在第34行设置power=1.0来将多项式衰减转换为线性衰减(更多细节请参阅第11章)。新的学习率(基于当前epoch)在第37行计算,然后返回到第40行上的调用函数。
实验2
得到的学习图可以在图12.11中找到。在这里,我们可以看到学习速率衰减对训练过程的巨大影响。虽然训练损失和验证损失彼此不同,但直到60年代,这一差距才扩大。此外,我们的验证损失也在继续减少。在第75 epoch结束时,我获得了58.32%的rank-1准确率。
使用ResNet,我们能够在Tiny ImageNet排行榜上达到第5名的位置,击败了所有试图从零开始训练网络的其他方法。所有误差比我们低的方法都应用了微调/特征提取。
结果表明,我们在测试集中达到了58.03%的rank-1和80.46%的rank-5准确度,比我们在GoogLeNet上的前一章有了很大的改进。这一结果导致测试误差为1−0.5803 = 0.4197,这在Tiny ImageNet排行榜上很容易排到第5位(图12.10)。
这是在不执行某种形式的迁移学习或微调的Tiny ImageNet数据集上获得的最佳精度。考虑到位置1-4的精度是通过对已经在完整的ImageNet数据集上训练过的网络进行微调获得的,因此很难将经过微调的网络与完全从零开始训练的网络进行比较。如果我们(公平地)将我们从零开始训练的网络与排行榜上其他从零开始训练的网络进行比较,我们可以看到ResNet在群组中获得了最高的准确性。
12.6 总结
在本章中,我们详细讨论了ResNet体系结构,包括剩余模块微体系结构。He等人在2015年的论文《Deep residual Learning for Image Recognition[24]》中提出的原始残差模块经过多次修改。最初该模块由两个CONV层和一个标识映射“捷径”组成。在同一篇论文中发现,添加1 × 1,3 × 3和1 × 1 CONV层的“瓶颈”序列提高了精度。
然后,在他们2016年的研究中,深度残差网络[33]中的身份映射,引入了预激活残差模块。我们称这种更新为“预激活”,因为我们在卷积之前应用了激活和批量归一化,这与构建卷积神经网络时的“传统智慧”背道而驰。
从那里开始,我们使用瓶颈和使用Keras框架的预激活来实现ResNet架构。然后,这个实现被用于在CIFAR-10和Tiny ImageNet数据集上训练ResNet。在CIFAR-10中,我们能够复制He等人的结果,准确率达到93.58%。然后,在Tiny ImageNet上,达到了58.03%的准确率,这是迄今为止在斯坦福大学的cs231n Tiny ImageNet挑战上从零开始训练的网络中最高的准确率。
在稍后的ImageNet Bundle中,我们将研究ResNet并在完整的ImageNet数据集上训练它,再次复制He等人的工作。
//截止到2022.2.1日晚上17:39