解读
Abstract—摘要
翻译
更深的神经网络往往更难以训练,我们在此提出一个残差学习的框架,以减轻网络的训练负担,这是个比以往的网络要深的多的网络。我们明确地将层作为输入学习残差函数,而不是学习未知的函数。我们提供了非常全面的实验数据来证明,残差网络更容易被优化,并且可以在深度增加的情况下让精度也增加。在ImageNet的数据集上我们评测了一个深度152层(是VGG的8倍)的残差网络,但依旧拥有比VGG更低的复杂度。残差网络整体达成了3.57%的错误率,这个结果获得了ILSVRC2015的分类任务第一名,我们还用CIFAR-10数据集分析了100层和1000层的网络。
在一些计算机视觉识别方向的任务当中,深度表示往往是重点。我们极深的网络让我们得到了28%的相对提升(对COCO的对象检测数据集)。我们在深度残差网络的基础上做了提交的版本参加ILSVRC和COCO2015的比赛,我们还获得了ImageNet对象检测,Imagenet对象定位,COCO对象检测和COCO图像分割的第一名。
精读
主要内容
**背景:**神经网络的深度越深,越难以训练
**本文贡献:**本文展示了一种残差学习框架,能够简化使那些非常深的网络的训练,该框架能够将层作为输入学习残差函数,而不是学习未知的函数。
**结果:**本文提供了全面的依据表明,这些残差网络更容易被优化,而且可以在深度增加的情况下让精度也增加。
**成绩:**2015年的ILSVRC分类任务上以及获得了第一名的成绩,后来在ImageNet检测、ImageNet定位、COCO检测以及COCO分割上均获得了第一名的成绩。
Introduction—介绍
翻译
深度卷积神经网络在图像分类领域取得了一系列的突破 。 深度网络很好的将一个端到端的多层模型中的低/中/高级特征以及分类器整合起来,特征的等级可以通过所堆叠层的数量(深度)来丰富。最近有结果显示,模型的深度发挥着至关重要的作用,这样导致了ImageNet竞赛的参赛模型都趋向于“非常深”——16 层 到30层 。许多其它的视觉识别任务的都得益于非常深的模型。
在深度的重要性的驱使下,出现了一个新的问题:训练一个更好的网络是否和堆叠更多的层一样简单呢?解决这一问题的障碍便是困扰人们很久的梯度消失/梯度爆炸,这从一开始便阻碍了模型的收敛。归一初始化(normalized initialization)和中间归一化(intermediate normalization)在很大程度上解决了这一问题,它使得数十层的网络在反向传播的随机梯度下降(SGD)上能够收敛。
当深层网络能够收敛时,一个退化问题又出现了:随着网络深度的增加,准确率达到饱和(不足为奇)然后迅速退化。意外的是,这种退化并不是由过拟合造成的,并且在一个合理的深度模型中增加更多的层却导致了更高的错误率,我们的实验也证明了这点。
退化的出现(训练准确率)表明了并非所有的系统都是很容易优化的。让我们来比较一个浅层的框架和它的深层版本。对于更深的模型,这有一种通过构建的解决方案:恒等映射(identity mapping)来构建增加的层,而其它层直接从浅层模型中复制而来。这个构建的解决方案也表明了,一个更深的模型不应当产生比它的浅层版本更高的训练错误率。实验表明,我们目前无法找到一个与这种构建的解决方案相当或者更好的方案(或者说无法在可行的时间内实现)。
本文中,我们提出了一种深度残差学习框架来解决这个退化问题。我们明确的让这些层来拟合残差映射(residual mapping),而不是让每一个堆叠的层直接来拟合所需的底层映射(desired underlying mapping)。假设所需的底层映射为 H(x)H(x),我们让堆叠的非线性层来拟合另一个映射: F(x):=H(x)−xF(x):=H(x)−x。 因此原来的映射转化为: F(x)+xF(x)+x。我们推断残差映射比原始未参考的映射(unreferenced mapping)更容易优化。在极端的情况下,如果某个恒等映射是最优的,那么将残差变为0 比用非线性层的堆叠来拟合恒等映射更简单。
公式 F(x)+xF(x)+x 可以通过前馈神经网络的“shortcut连接”来实现(Fig.2)。Shortcut连接就是跳过一个或者多个层。在我们的例子中,shortcut 连接只是简单的执行恒等映射,再将它们的输出和堆叠层的输出叠加在一起(Fig.2)。恒等的shortcut连接并不增加额外的参数和计算复杂度。完整的网络仍然能通过端到端的SGD反向传播进行训练,并且能够简单的通过公共库(例如,Caffe)来实现而无需修改求解器(solvers)。
我们在ImageNet数据集上进行了综合性的实验来展示这个退化问题并评估了我们提出的方法。本文表明了: 1) 我们极深的残差网络是很容易优化的,但是对应的“plain”网络(仅是堆叠了层)在深度增加时却出现了更高的错误率。 2) 我们的深度残差网络能够轻易的由增加层来提高准确率,并且结果也大大优于以前的网络。
CIFAR-10数据集上也出现了类似的现象,这表明了我们提出的方法的优化难度和效果并不仅仅是对于一个特定数据集而言的。我们在这个数据集上成功的提出了超过100层的训练模型,并探索了超过1000层的模型。
在ImageNet分类数据集上,极深的残差网络获得了优异的成绩。我们的152层的残差网络是目前ImageNet尚最深的网络,并且别VGG网络的复杂度还要低。在ImageNet测试集上,我们的组合模型(ensemble)的top-5错误率仅为3.57%,并赢得了ILSVRC 2015分类竞赛的第一名。这个极深的模型在其他识别任务上同样也具有非常好的泛化性能,这让我们在ILSVRC & COCO 2015 竞赛的ImageNet检测、ImageNet定位、COCO检测以及COCO分割上均获得了第一名的成绩。这强有力的证明了残差学习法则的通用性,因此我们将把它应用到其他视觉甚至非视觉问题上。
精读
背景
模型的深度发挥着至关重要的作用,这样导致了ImageNet竞赛的参赛模型都趋向于“非常深”——16 层 到30层
问题一: 模型深度太大时,会存在梯度消失/梯度爆炸的问题
**梯度消失/梯度爆炸概念:**二者问题问题都是因为网络太深,网络权值更新不稳定造成的。本质上是因为梯度反向传播中的连乘效应(小于1连续相乘多次)。梯度消失时,越靠近输入层的参数w越是几乎纹丝不动;梯度爆炸时,越是靠近输入层的参数w越是上蹿下跳。
**解决方法:**归一初始化(normalized initialization)和中间归一化(intermediate normalization)+BN,加快网络收敛。
问题二: 随着网络深度的增加,准确率达到饱和然后迅速退化
**网络退化概念:**神经网络随着层数加深,首先训练准确率会逐渐趋于饱和;若层数继续加深,反而训练准确率下降,效果不好了,而这种下降不是由过拟合造成的(因为如果是过拟合的话,训练时误差应该很低而测试时很高)。
Q:为啥会出现网络退化?
由于非线性激活函数Relu的存在,每次输入到输出的过程都几乎是不可逆的,这也造成了许多不可逆的信息损失。一个特征的一些有用的信息损失了,得到的结果肯定不尽人意。说通俗一点就是中间商赚差价。层数增多之后,信息在中间层损失掉了。
**解决方法:**深度残差学习
(具体方法会在3.1章节讲解)
结果:
(1)残差网络的结构更利于优化收敛
(2)解决了退化问题
(3)残差网络可以在扩展网络深度的同时,提高网络性能
Related Work—相关工作
Residual Representations—残差表达
翻译
残差表达
在图像识别中,VLAD是残差向量对应于字典进行编码的一种表达形式,Fisher Vector可以看做是VLAD 的一个概率版本。对于图像检索和分类它们都是强力的浅层表达。对于向量量化,残差向量编码比原始向量编码更加有效。
在低级视觉和计算机图形学中,为了求解偏微分方程(PDEs),通常使用Multigrid法将系统重新表达成多尺度的子问题来解决,每一个子问题就是解决粗细尺度之间的残差问题。Multigrid的另外一种方式是分层基预处理,它依赖于代表着两个尺度之间残差向量的变量。实验证明 这些求解器比其他标准求解器的收敛要快得多,却并没有意识到这是该方法的残差特性所致。这些方法表明了一个好的重新表达或者预处理能够简化优化问题**。**
精读
主要内容
(1)对于向量量化,残差向量编码比原始向量编码更加有效。
(2)Multigrid的残差特性使得求解器比其他标准求解器的收敛要快得多,表明了一个好的重新表达或者预处理能够简化优化问题。
Shortcut Connections—短路连接
翻译
Shortcut连接
Shortcut连接已经经过了很长的一段实践和理论研究过程。训练多层感知器(MLPs)的一个早期实践就是添加一个连接输入和输出的线性层。在Szegedy2015Going及Lee2015deeply中,将一些中间层直接与辅助分类器相连接可以解决梯度消失/爆炸问题。在 Szegedy2015Going中,一个“inception”层由一个shortcut分支和一些更深的分支组合而成。
与此同时,“highway networks”将shortcut连接与门控函数 结合起来。这些门是数据相关并且是有额外参数的,而我们的恒等shortcuts是无参数的。当一个门的shortcut是“closed”(接近于0)时,highway网络中的层表示非残差函数。相反的,我们的模型总是学习残差函数;我们的恒等shortcuts从不关闭,在学习额外的残差函数时,所有的信息总是通过的。此外,highway网络并不能由增加层的深度(例如, 超过100层)来提高准确率。
精读
主要内容
(1)Shortcut连接已经经过了很长的一段实践和理论研究过程,证明是有效的。
(2)和highway networks(门控函数)对比:当一个门的shortcut是“closed”(接近于0)时,highway networks中的层表示非残差函数。相反的,我们的模型总是学习残差函数;我们的恒等shortcuts从不关闭,是无参数的,在学习额外的残差函数时,所有的信息总是通过的。此外,highway networks并不能由增加层的深度(例如,超过100层)来提高准确率。
Deep Residual Learning—深度残差学习
Residual Learning—残差学习
翻译
我们将H(x)看作一个由部分堆叠的层(并不一定是全部的网络)来拟合的底层映射,其中x是这些层的输入。假设多个非线性层能够逼近复杂的函数,这就等价于这些层能够逼近复杂的残差函数,例如, H(x)−x(假设输入和输出的维度相同)。所以我们明确的让这些层来估计一个残差函数:F(x)=H(x)−x而不是H(x)。因此原始函数变成了:F(x)+x。尽管这两个形式应该都能够逼近所需的函数(正如假设),但是学习的难易程度并不相同。
这个重新表达的动机是由退化问题这个反常的现象(Fig.1,左)。正如我们在Introduction中讨论的,如果增加的层能以恒等映射来构建,一个更深模型的训练错误率不应该比它对应的浅层模型的更大。退化问题表明了,求解器在通过多个非线性层来估计恒等映射上可能是存在困难的。而伴随着残差学习的重新表达,如果恒等映射是最优的,那么求解器驱使多个非线性层的权重趋向于零来逼近恒等映射。
在实际情况下,恒等映射不太可能达到最优,但是我们的重新表达对于这个问题的预处理是有帮助的。如果最优函数更趋近于恒等映射而不是0映射,那么对于求解器来说寻找关于恒等映射的扰动比学习一个新的函数要容易的多。通过实验(Fig.7)表明,学习到的残差函数通常只有很小的响应,说明了恒等映射提供了合理的预处理。
精读
ResNet目的
我们选择加深网络的层数,是希望深层的网络的表现能比浅层好,或者是希望它的表现至少和浅层网络持平(相当于直接复制浅层网络的特征)
以前方法
在正常的网络中,应该传递给下一层网络的输入是 H(x)=F(x),即直接拟合H(x)
本文改进
在ResNet中,传递给下一层的输入变为H(x)=F(x)+x,即拟合残差F(x)=H(x)-x
**残差模块:**一条路不变(恒等映射);另一条路负责拟合相对于原始网络的残差,去纠正原始网络的偏差,而不是让整体网络去拟合全部的底层映射,这样网络只需要纠正偏差。
本质
(1)加了残差结构后,给了输入x一个多的选择。若神经网络学习到这层的参数是冗余的时候,它可以选择直接走这条“跳接”曲线(shortcut connection),跳过这个冗余层,而不需要再去拟合参数使得H(x)=F(x)=x
(2)加了恒等映射后,深层网络至少不会比浅层网络更差。
(3)而在Resnet中,只需要把F(x)变为0即可,输出变为F(x)+x=0+x=x很明显,将网络的输出优化为0比将其做一个恒等变换要容易得多。
Q:为什么H(x)=F(x)+x中F(x)为0才有效?
模型在训练过程中,F(x)是训练出来的,如果F(x)对于提高模型的训练精度无作用,自然梯度下降算法就调整该部分的参数,使该部分的效果趋近于0.这样整个模型就不会出现深度越深反而效果越差的情况了。
3.2. Identity Mapping by Shortcuts—通过短路连接进行恒等映射
翻译
我们在堆叠层上采取残差学习算法。一个构建块如Fig.2所示。本文中的构建块定义如下(Eq.1):y=F(x,{Wi})+x.
其中x和y分别表示层的输入和输出。函数F(x,{Wi})代表着学到的残差映射。Fig.2中的例子包含两层,F=W2σ(W1x),其中 σ代表ReLU,为了简化省略了偏置项。F+x操作由一个shortcut连接和元素级(element-wise)的加法来表示。在加法之后我们再执行另一个非线性操作(例如, σ(y),如Fig.2。
Eq.1中的shortcut连接没有增加额外的参数和计算复杂度。这不仅是一个很有吸引力的做法,同时在对“plain”网络和残差网络进行比较时也是非常重要的。我们可以在参数、深度、宽度以及计算成本都相同的基础上对两个网络进行公平的比较(除了可以忽略不计的元素级的加法)。
在Eq.1中,x和F的维度必须相同。如果不相同(例如, 当改变了输入/输出的通道),我们可以通过shortcut连接执行一个线性映射Ws来匹配两者的维度(Eq.2):y=F(x,{Wi})+Wsx.
在Eq.1中同样可以使用方阵Ws。但我们的实验表明,恒等映射已足够解决退化问题,并且是经济的,因此Ws只是用来解决维度不匹配的问题。
残差函数F的形势是灵活可变的。本文实验中涉及到的函数FF是两层或者三层的(Fig.5),当然更多层也是可行的。但是如果F只含有一层,Eq.1就和线性函数:y=W1x+x一致,因此并不具有任何优势。
我们还发现不仅是对于全连接层,对于卷积层也是同样适用的。函数F(x,{Wi})可以表示多个卷积层,在两个特征图的通道之间执行元素级的加法。
精读
Shortcuts Connection的两种方式:
(1)shortcuts同等维度映射,F(x)与x相加就是就是逐元素相加
- y=F(x,Wi)+x
- F=W2σ(W1x)
其中 x 和 y 分别表示层的输入和输出。函数 F(x,Wi)代表着学到的残差映射,σ 代表ReLU
这种方式通过shortcuts直接传递输入x,不会引入额外的参数也不会增加模块的计算复杂性,因此可以公平地将残差网络和plain网络作比较。
(2)如果两者维度不同(改变了输入/输出的通道),需要给x执行一个线性映射来匹配维度
- y=F(x,Wi)+Wsx.
- F=W2σ(W1x)
这种方式的目的仅仅是为了保持x与F(x)之间的维度一致,所以通常只在相邻残差块之间通道数改变时使用,绝大多数情况下仅使用第一种方式。
用卷积层进行残差学习:以上的公式表示为了简化,都是基于全连接层的,实际上当然可以用于卷积层。加法随之变为对应channel间的两个feature map逐元素相加。
3.3. Network Architectures—网络架构
翻译
我们在多个plain网络和残差网络上进行了测试,并都观测到了一致的现象。接下来我们将在ImageNet上对两个模型进行讨论。
Plain网络
我们的plain网络结构(Fig.3,中)主要受VGG网络 (Fig.3,左)的启发。
卷积层主要为3*3的滤波器,并遵循以下两点要求:(i) 输出特征尺寸相同的层含有相同数量的滤波器;(ii) 如果特征尺寸减半,则滤波器的数量增加一倍来保证每层的时间复杂度相同。我们直接通过stride 为2的卷积层来进行下采样。在网络的最后是一个全局的平均pooling层和一个1000 类的包含softmax的全连接层。加权层的层数为34,如Fig.3(中)所示。
值得注意的是,我们的模型比VGG网络(Fig.3,左)有更少的滤波器和更低的计算复杂度。我们34层的结构含有36亿个FLOPs(乘-加),而这仅仅只有VGG-19 (196亿个FLOPs)的18%。
残差网络
在以上plain网络的基础上,我们插入shortcut连接(Fig.3,右),将网络变成了对应的残差版本。如果输入和输出的维度相同时,可以直接使用恒等shortcuts (Eq.1)(Fig.3中的实线部分)。当维度增加时(Fig.3中的虚线部分),考虑两个选项:
(A) shortcut仍然使用恒等映射,在增加的维度上使用0来填充,这样做不会增加额外的参数;
(B) 使用Eq.2的映射shortcut来使维度保持一致(通过1*1的卷积)。
对于这两个选项,当shortcut跨越两种尺寸的特征图时,均使用stride为2的卷积。
Fig.3 对应于ImageNet的网络框架举例。 左:VGG-19模型 (196亿个FLOPs)作为参考。中:plain网络,含有34个参数层(36 亿个FLOPs)。右:残差网络,含有34个参数层(36亿个FLOPs)。虚线表示的shortcuts增加了维度。Table 1展示了更多细节和其它变体。
Table 1 对应于ImageNet的结构框架。括号中为构建块的参数(同样见Fig.5),数个构建块进行堆叠。下采样由stride为2的conv3_1、conv4_1和conv5_1 来实现。
精读
下面将以ImageNet数据集为例,将plain网络和残差网络作对比讨论。
Plain网络
plain网络结构主要受VGG网络的启发。 卷积层主要为3*3的卷积核,直接通过stride为2的卷积层来进行下采样。在网络的最后是一个全局的平均pooling层和一个1000类的包含softmax的全连接层。加权层的层数为34。
两条设计准则:
(i)同样输出大小的特征图,有着相同数量的卷积核;
(ii)如果特征图大小减半,为了保证相同的时间复杂度,卷积核个数加倍。
与VGG对比:
我们的模型比VGG有更少的卷积核和更低的计算复杂度。我们34层的结构含有36亿个FLOPs(乘-加),而这仅仅只有VGG-19 (196亿个FLOPs)的18%。
残差网络
在plain网络的基础上,加入shortcuts连接,就变成了相应的残差网络
如上图,实线代表维度一样,直接相加 。虚线代表维度不一样(出现了下采样,步长为2的卷积),使用残差网络
调整维度的方法有两种:
(1)zero-padding:对多出来的通道padding补零填充,这种方法不会引入额外的参数;
(2)线性投影变换:用1*1卷积升维,是需要学习的参数,精度比zero-padding更好,但是耗时更长,占用更多内存。
这两种方法都使用stride为2的卷积。
3.4. Implementation—实现
翻译
针对ImageNet的网络实现遵循了Krizhevsky2012ImageNet和Simonyan2014Very。调整图像的大小使它的短边长度随机的从[256,480]中采样来增大图像的尺寸。 从一张图像或者它的水平翻转图像中随机采样一个224*224的crop,每个像素都减去均值。图像使用标准的颜色增强。我们在每一个卷积层之后,激活层之前均使用batch normalization(BN)。我们根据He2014spatial来初始化权值然后从零开始训练所有plain/残差网络。
我们使用的mini-batch的尺寸为256。学习率从0.1开始,每当错误率平稳时将学习率除以10,整个模型进行60∗104次迭代训练。我们将权值衰减设置为0.0001,a 动量为0.9。根据 Ioffe2015Batch,我们并没有使用Dropout。
在测试中,为了进行比较,我们采取标准的10-crop测试。
为了达到最佳的结果,我们使用Simonyan2014Very及He2014spatial中的全卷积形式,并在多个尺度的结果上取平均分(调整图像的大小使它的短边长度分别为{224,256,384,480,640})。
精读
方法
(1)图像分别随机被压缩到256到480之间,之后做图像增强
(2)输出处理过程:用224 * 224 随机裁出一个小图,在做水平的镜像来做图像增强(不同尺度维度),10个小图汇总成一个大图(可使用多尺度裁剪和结果融合)。
(3)每个卷积层后面或者激活层之前都使用BN
参数: mini-batch为256,学习率为0.1,训练60万的迭代次数,正则化0.0001,动量是0.9。没有使用dropout(BN和dropout不能混合使用,单独使用效果更佳,原因:方差偏移)
Experiments—实验
ImageNet Classification—ImageNet分类
本文在1000类的ImageNet2012数据集上对我们的方法进行评估。训练集包含128万张图像,验证集包含5万张图像。我们在10万张测试图像上进行测试,并对top-1和top-5 的错误率进行评估。
Plain网络
翻译
我们首先评估了18层和34层的plain网络。34层的网络如图Fig.3(中)所示。18层的结构很相似,具体细节参见Table 1。
Table 2中展示的结果表明了34层的网络比18层的网络具有更高的验证错误率。为了揭示产生这种现象的原因,在Fig.4(左)中我们比较了整个训练过程中的训练及验证错误率。从结果中我们观测到了明显的退化问题——在整个训练过程中34 层的网络具有更高的训练错误率,即使18层网络的解空间为34层解空间的一个子空间。
我们认为这种优化上的困难不太可能是由梯度消失所造成的。因为这些plain网络的训练使用了BN,这能保证前向传递的信号是具有非零方差的。我们同样验证了在反向传递阶段的梯度由于BN而具有良好的范式,所以在前向和反向阶段的信号不会存在消失的问题。事实上34层的plain网络仍然具有不错的准确率(Table 3),这表明了求解器在某种程度上也是有效的。我们推测,深层的plain网络的收敛率是指数衰减的,这可能会影响训练错误率的降低。这种优化困难的原因我们将在以后的工作中进行研究。
精读
首先进行的实验是18层和34层的plain网络,实验结果如下表所示,产生了一种退化现象:在训练过程中34层的网络比18层的网络有着更高的训练错误率。
(细线:训练集上的误差; 粗线:测试集上的误差)
残差网络
翻译
接下来我们对18层和34层的残差网络ResNets进行评估。如Fig.3 (右)所示,ResNets的基本框架和plain网络的基本相同,除了在每一对3*3的滤波器上添加了一个shortcut连接。在Table 2以及Fig.4(右)的比较中,所有的shortcuts都是恒等映射,并且使用0对增加的维度进行填充(选项 A)。因此他们并没有增加额外的参数。
我们从Table 2和Fig.4中观测到以下三点:
第一,与plain网络相反,34层的ResNet比18层ResNet的结果更优(2.8%)。更重要的是,34 层的ResNet在训练集和验证集上均展现出了更低的错误率。这表明了这种设置可以很好的解决退化问题,并且我们可以由增加的深度来提高准确率。
第二,与对应的plain网络相比,34层的ResNet在top-1 错误率上降低了3.5% (Table 2),这得益于训练错误率的降低(Fig.4 右 vs 左)。这也验证了在极深的网络中残差学习的有效性。
最后,我们同样注意到,18层的plain网络和残差网络的准确率很接近 (Table 2),但是ResNet 的收敛速度要快得多。(Fig.4 右 vs 左)。
如果网络“并不是特别深” (如18层),现有的SGD能够很好的对plain网络进行求解,而ResNet能够使优化得到更快的收敛。
精读
接着对18层和34层的残差网络进行评估,为了保证变量的一致性,其基本框架结构和plain网络的结构相同,只是在每一对卷积层上添加了shortcuts连接来实现残差结构,对于维度不匹配的情况,使用0来填充维度(即3.3介绍过的方法1),因此也并没有添加额外的参数。训练结果如下图所示
【Table 2 ImageNet验证集上的Top-1错误率 (%, 10-crop testing)】
结论
(1)与plain网络相反,34层的resnet网络比18层的错误率更低,表明可以通过增加深度提高准确率,解决了退化问题。
(2)与plain网络相比,层次相同的resnet网络上错误率更低,表明残差网络在深层次下仍然有效。
(3)对于18层的plain网络,它和残差网络的准确率很接近,但是残差网络的收敛速度要更快。
Identity vs. Projection Shortcuts—恒等 vs 映射 Shortcuts
翻译
我们已经验证了无参数的恒等shortcuts是有助于训练的。接下来我们研究映射shortcut(Eq.2)。在Table 3中,我们比较了三种选项:
(A) 对增加的维度使用0填充,所有的shortcuts是无参数的(与Table 2 和 Fig.4 (右)相同);
(B) 对增加的维度使用映射shortcuts,其它使用恒等shortcuts;
© 所有的都是映射shortcuts。
Table 3表明了三种选项的模型都比对于的plain模型要好。B略好于A,我们认为这是因为A中的0填充并没有进行残差学习。C略好于B,我们把这个归结于更多的(13个)映射shortcuts所引入的参数。在A、B、C三个结果中细小的差距也表明了映射shortcuts对于解决退化问题并不是必需的。所以我们在本文接下来的内容中,为了减少复杂度和模型尺寸,并不使用选项C的模型。恒等shortcuts因其无额外复杂度而对以下介绍的瓶颈结构尤为重要。
精读
无参数的恒等shortcuts肯定是有助于提高训练效果的,针对映射shortcuts,有三种方法可供选择:
(1)ResNet - 34 A: 所有的shortcut都使用恒等映射,也就是多出来的通道补0,没有额外的参
(2)ResNet - 34 B: 对需要调整维度的使用卷积映射shortcut来实现,不需要调整维度的使用恒等shortcut,升维的时候使用1 * 1卷积
(3)ResNet - 34 C: 所有的shortcut都使用1 * 1卷积(效果最好,但引入更多的参数,不经济)
下面的表格中表明了三种选项的模型都比plain模型要好,按效果好坏排序为C>B>A,
【Table 3 在ImageNet验证集上的错误率 (%, 10-crop testing)】
B比A好,因为A在升维的时候用padding补零,丢失了shortcut学习,没有进行残差学习
C比B好,因为C的13个非下采样残差模块的shortcut都有参数,模型能力比较强
但是ABC都差不多,说明恒等映射的shortcut可以解决退化问题
Deeper Bottleneck Architectures—深度瓶颈结构
翻译
接下来我们介绍更深的模型。考虑到训练时间的限制,我们将构建块修改成瓶颈的设计。对于每一个残差函数F,我们使用了三个叠加层而不是两个(Fig.5)。 这三层分别是11、33 和11 的卷积,11 的层主要负责减少然后增加(恢复)维度,剩下的3*3的层来减少输入和输出的维度。Fig.5展示了一个例子,这两种设计具有相似的时间复杂度。
无参数的恒等shortcuts对于瓶颈结构尤为重要。如果使用映射shortcuts来替代Fig.5(右)中的恒等shortcuts,将会发现时间复杂度和模型尺寸都会增加一倍,因为shortcut连接了两个高维端,所以恒等shortcuts对于瓶颈设计是更加有效的。
50层 ResNet:我们将34层网络中2层的模块替换成3层的瓶颈模块,整个模型也就变成了50层的ResNet (Table 1)。对于增加的维度我们使用选项B来处理。整个模型含有38亿个FLOPs。
101层和152层 ResNets:我们使用更多的3层模块来构建101层和152层的ResNets (Table 1)。值得注意的是,虽然层的深度明显增加了,但是152层ResNet的计算复杂度(113亿个FLOPs)仍然比VGG-16(153 亿个FLOPs)和VGG-19(196亿个FLOPs)的小很多。
50/101/152层ResNets比34层ResNet的准确率要高得多(Table 3 和4)。而且我们并没有观测到退化问题。所有的指标都证实了深度带来的好处。 (Table 3 和4)。
精读
接下来介绍层次更多的模型,对于每一个残差块,不再使用两层卷积,而是使用三层卷积来实现,如下图所示。
50层的残差网络: 将其34层的残差网络的2个卷积层替换成了3个bottleneck残差块,就变成了50层残差网络,下采样使用的是1 * 1 的卷积
【Table 4 单一模型在ImageNet验证集上的错误率(%)(除了 ++ 是在验证集上的结果)
结论
50/101/152层的resnet比34层resnet的准确率要高很多,解决了深层的退化问题。同时即使是152层resnet的计算复杂度仍然比VGG-16和VGG-19要小。
Comparisons with State-of-the-art Methods—与最优秀方法的比较
翻译
在Table 4中我们比较了目前最好的单模型结果。我们的34层ResNets取得了非常好的结果,152层的ResNet的单模型top-5验证错误率仅为 4.49%,甚至比先前组合模型的结果还要好 (Table 5)。我们将6个不同深度的ResNets合成一个组合模型(在提交结果时只用到2个152层的模型)。这在测试集上的top-5错误率仅为3.57% (Table 5),这一项在ILSVRC 2015 上获得了第一名的成绩。
精读
将6个不同深度的ResNets合成一个组合模型(在提交结果时只用到2个152层的模型)。这在测试集上的top-5错误率仅为3.57% (Table 5),这一项在ILSVRC 2015 上获得了第一名的成绩。
【Table 5 组合模型在ImageNet测试集上的top-5错误率】
4.2. CIFAR-10 and Analysis— CIFAR-10和分析
翻译
我们在包含5万张训练图像和1万张测试图像的10类CIFAR-10数据集上进行了更多的研究。我们在训练集上进行训练,在测试集上进行验证。我们关注的是验证极深模型的效果,而不是追求最好的结果,因此我们只使用简单的框架如下。
Plain网络和残差网络的框架如 Fig.3(中/右)所示。网络的输入是3232的减掉像素均值的图像。第一层是33的卷积层。然后我们使用6n个3*3的卷积层的堆叠,卷积层对应的特征图有三种:{32,16,8},每一种卷积层的数量为2n 个,对应的滤波器数量分别为{16,32,64}。使用strde为2的卷积层进行下采样。在网络的最后是一个全局的平均pooling层和一个10类的包含softmax的全连接层。一共有6n+2个堆叠的加权层。
权重的衰减设置为0.0001,动量为0.9,采用了He2015Delving中的权值初始化以及BN,但是不使用Dropout,mini-batch的大小为128,模型在2块GPU 上进行训练。学习率初始为0.1,在第32000和48000次迭代时将其除以10,总的迭代次数为64000,这是由45000/5000的训练集/验证集分配所决定的。我们在训练阶段遵循Lee2015deeply中的数据增强法则:在图像的每条边填充4个像素,然后在填充后的图像或者它的水平翻转图像上随机采样一个3232 的crop。在测试阶段,我们只使用原始3232的图像进行评估。
我们比较了n={3,5,7,9},也就是20、32、44以及56层的网络。Fig.6(左) 展示了plain网络的结果。深度plain网络随着层数的加深,训练错误率也变大。这个现象与在ImageNet(Fig.4, 左)和MNIST上的结果很相似,表明了优化上的难度确实是一个很重要的问题。
Fig.6(中)展示了ResNets的效果。与ImageNet(Fig.4, 右)中类似,我们的ResNets能够很好的克服优化难题,并且随着深度加深,准确率也得到了提升。
我们进一步探索了n=18,也就是110层的ResNet。在这里,我们发现0.1的初始学习率有点太大而不能很好的收敛。所以我们刚开始使用0.01的学习率,当训练错误率在80%以下(大约400次迭代)之后,再将学习率调回0.1继续训练。剩余的学习和之前的一致。110层的ResNets很好的收敛了 (Fig.6, 中)。它与其他的深层窄模型,如FitNet和 Highway (Table 6)相比,具有更少的参数,然而却达到了最好的结果 (6.43%, Table 6)。
精读
**CIFAR-10 数据集:**50w的训练集,10w的测试集,一共10个类别
对比plain网络和残差网络的做法
(1)输入的图像为32*32的像素,此时的图像做了预处理(每个像素减去均值)
(2)第一个卷积层为33 ,使用6n的卷积层,分别都是33的,feature map为(3232/1616/ 8*8)。一共有6n+2的卷积层(最后一层为池化层:1 +2n,2n,2n,1)
(3)卷积核个数分别为16/32/64,feature map个数减半,channel数翻倍
Q:为什么下采样之后feature map尺寸减半,通道个数翻倍?
因为池化会让长宽方向减半,卷积核个数对应通道加倍(详情见《MobileNet》)
下采样用的是步长为2的卷积,最后加一个全局池化,10个神经元的全连接层和softmax
(1)残差是由2层神经网络(每一个shortcut都由3 * 3的卷积组成)来拟合的,总共有6n,所以一共有3n的shortcut。
(2)下采样是由0补充(下采样的残差和不带残差的计算量是一样的)
(3)训练过程中的正则化为0.0001 ,动量化为0.9 ,论文中提出的权重进行初始化,使用了BN没有使用dropout,批次处理为128,起始的学习率为0.1,在3.2w和4.8w迭代时除以10,最终在6.4w终止训练
(4)把训练集划分为4.5w训练和5k的验证,使用图像增强方法,分别在图像外边补4个pixel,再用32 *32 的图像进行剪裁(水平翻转的图像增强)。测试的时候,直接使用32 * 32的图像进行测试即可
【图6:CIFAR-10培训。虚线表示训练错误,粗体表示测试错误。左:plain。plain-110的误差大于60%,不显示。中间:ResNets。右图:ResNets110层和1202层。】
Analysis of Layer Responses—分析每一层的网络的响应分布
翻译
Fig.7展示了层响应的标准方差(std)。 响应是每一个3*3卷积层的BN之后、非线性层(ReLU/addition)之前的输出。对于ResNets,这个分析结果也揭示了残差函数的响应强度。Fig.7表明了ResNets的响应比它对应的plain网络的响应要小。这些结果也验证了我们的基本动机(Sec3.1),即残差函数比非残差函数更接近于0。从Fig.7中ResNet-20、56和110的结果,我们也注意到,越深的ResNet的响应幅度越小。当使用更多层是,ResNets中单个层对信号的改变越少。
精读
残差网络是修正输入。响应的标准差如下图:
【图7:CIFAR- 10层响应的标准偏差(std)。响应为BN后和非线性前各33层的输出。顶部:图层按原来的顺序显示。底部:响应按降序排列。】
方法
BN处理,均值已被调整为0。标准差衡量数据的离散程度(标准差越大,表明响应越大) 响应是每一层都是3 * 3的卷积层,介于BN后和激活之前。
结论
(1)ResNets的响应比它对应的plain网络的响应要小
(2)残差函数比非残差函数更接近于0
(3)越深的ResNet的响应幅度越小
(4)越靠近起始层,输出越大
Exploring Over 1000 layers—过深层网络
翻译
我们探索了一个超过1000层的极其深的模型。我们设置n=200,也就是1202层的网络模型,按照上述进行训练。我们的方法对103103层的模型并不难优化,并且达到了<0.1%的训练错误率(Fig.6, 右),它的测试错误率也相当低(7.93%, Table 6)。
但是在这样一个极其深的模型上,仍然存在很多问题。1202层模型的测试结果比110层的结果要差,尽管它们的训练错误率差不多。我们认为这是过拟合导致的。这样一个1202层的模型对于小的数据集来说太大了(19.4M)。在这个数据集上应用了强大的正则化方法,如maxout或者 dropout,才获得了最好的结果。
本文中,我们并没有使用maxout/dropout,只是简单的通过设计深层窄模型来进行正则化,而且不用担心优化的难度。但是通过强大的正则化或许能够提高实验结果,我们会在以后进行研究。
精读
取n等于200 ,也就是1202的残差卷积网络(6 * 200 + 2),和之前的训练方式一样,误差小于0.1,表明了没有退化,没优化困难。
但测试集的性能没有110层的好,文中表明这是过拟合了(模型太深参数过多,对于这个小数据集没有必要)
此论文没有使用maxout或者是dropout来正则化,因为核心任务是为了解决退化问题。
4.3. Object Detection on PASCAL and MS COCO—PASCAL和MS COCO上的对象检测
翻译
我们的方法在其它识别任务上展现出了很好的泛化能力。Table 7和8展示了在PASCAL VOC 2007 和 2012以及 COCO上的目标检测结果。我们使用Faster R-CNN作为检测方法。在这里,我们比较关注由ResNet-101 替换VGG-16所带来的的提升。使用不同网络进行检测的实现是一样的,所以检测结果只能得益于更好的网络。最值得注意的是,在COCO数据集上,我们在COCO的标准指标(mAP@[.5, .95])上比先前的结果增加了6.0%,这相当于28%的相对提升。而这完全得益于所学到的表达。
基于深度残差网络,我们在ILSVRC & COCO 2015竞赛的ImageNet检测、ImageNet定位、COCO检测以及COCO分割上获得了第一名。
精读
【Table 7 在PASCAL VOC 2007/2012测试集上使用Faster R-CNN的目标检测 mAP (%)。有关更好的结果,请参见附录。】
【Table 8 在COCO 验证集上使用Faster R-CNN的目标检测 mAP (%)。】
基于深度残差网络,我们在ILSVRC & COCO 2015竞赛的ImageNet检测、ImageNet定位、COCO检测以及COCO分割上获得了第一名。
论文十问
Q1:论文试图解决什么问题?
该论文主要解决的深层神经网络的训练问题。随着网络的深度的增加,模型的效果反而变差了,论文提出了残差学习的方式来训练深层的神经网络。
Q2:这是否是一个新的问题?
不是新问题,是优化问题
Q3:这篇文章要验证一个什么科学假设?
研究深度模型中的退化问题,累积的非线性层可能难以学习到线性映射
Q4:有哪些相关研究?如何归类?谁是这一课题在领域内值得关注的研究员?
- 为了求解偏微分方程(PDEs),通常使用Multigrid法将系统重新表达成多尺度的子问题来解决。数学类问题。
- 附录部分,作者针对ResNet在目标检测、目标定位上的应用进行了研究。
Q5:论文中提到的解决方案之关键是什么?
ResNet 其实就是通过shortcut connections,将x直接传递到后面的层,使得网络可以很容易的学习恒等变换,从而解决网络退化的问题,同时也使得学习效率更高。
Q6:论文中的实验是如何设计的?
1.ImageNet2012:
- 首先对plain网络和残差网络分别训练,对比它们不同层在训练集和测试集的误差,以及是否退化。
- 然后恒等 vs 映射 Shortcuts
- 接着加深深度,对改进后的残差网络训练和评估错误率
- 最后和VGG、GoogLeNet等优秀方法对比
2.CIFAR-10:
- 首先对plain网络和残差网络分别训练,对比它们不同层在训练集和测试集的误差,以及是否退化。
- 然后研究更深的模型
3.PASCAL和MS COCO:
和VGG对比
Q7:用于定量评估的数据集是什么?代码有没有开源?
ImageNet2012、CIFAR-10、PASCAL VOC 2007,2012、COCO
开源
Q8:论文中的实验及结果有没有很好地支持需要验证的科学假设?
支持了,解决了退化的问题,也取得了第一。
Q9:这篇论文到底有什么贡献?
1.研究深度模型中的退化问题,提出了ResNet网络
2.提出了残差学习,助力深度模型的学习,且没有增加学习的参数
3.ResNet为目标检测和目标定位提供了优化思路
Q10:下一步呢?有什么工作可以继续深入?
1.网络收敛率问题需要继续探究。 plain层我们推测,深层的plain网络的收敛率是指数衰减的,这可能会影响训练错误率的降低。这种优化困难的原因我们将在以后的工作中进行研究。
2.解决过深层退化问题。 ResNet在1202层的优化不再明显反而还出现了退化。
代码复现
源代码比较复杂,感兴趣的同学可以上官网学习:
https://github.com/pytorch/vision/tree/master/torchvision
本篇是简化版本
一、BasicBlock模块
BasicBlock结构图如图所示:
BasicBlock是基础版本,主要用来构建ResNet18和ResNet34网络,里面只包含两个卷积层,使用了两个 3*3 的卷积,通道数都是64,卷积后接着 BN 和 ReLU。
右边的曲线就是Shortcut Connections,将输入x加到输出。
代码:
'''-------------一、BasicBlock模块-----------------------------'''
# 用于ResNet18和ResNet34基本残差结构块
class BasicBlock(nn.Module):
def __init__(self, inchannel, outchannel, stride=1):
super(BasicBlock, self).__init__()
self.left = nn.Sequential(
nn.Conv2d(inchannel, outchannel, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(outchannel),
nn.ReLU(inplace=True), #inplace=True表示进行原地操作,一般默认为False,表示新建一个变量存储操作
nn.Conv2d(outchannel, outchannel, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(outchannel)
)
self.shortcut = nn.Sequential()
#论文中模型架构的虚线部分,需要下采样
if stride != 1 or inchannel != outchannel:
self.shortcut = nn.Sequential(
nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(outchannel)
)
def forward(self, x):
out = self.left(x) #这是由于残差块需要保留原始输入
out += self.shortcut(x)#这是ResNet的核心,在输出上叠加了输入x
out = F.relu(out)
return out
二、Bottleneck 模块
Bottleneck结构图如图所示:
Bottleneck主要用在ResNet50及以上的网络结构,与BasicBlock不同的是这里有 3 个卷积,分别为 11,33,11大小的卷积核,分别用于压缩维度、卷积处理、恢复维度。
这里的通道数是变化的,11卷积层的作用就是用于改变特征图的通数,使得可以和恒等映射x相叠加,另外这里的1*1卷积层改变维度的很重要的一点是可以降低网络参数量,这也是为什么更深层的网络采用BottleNeck而不是BasicBlock的原因。
**注意:**这里outchannel / 4是因为Bottleneck层输出通道都是输入的4倍
代码:
'''-------------二、Bottleneck模块-----------------------------'''
# 用于ResNet50及以上的残差结构块
class Bottleneck(nn.Module):
def __init__(self, inchannel, outchannel, stride=1):
super(Bottleneck, self).__init__()
self.left = nn.Sequential(
nn.Conv2d(inchannel, int(outchannel / 4), kernel_size=1, stride=stride, padding=0, bias=False),
nn.BatchNorm2d(int(outchannel / 4)),
nn.ReLU(inplace=True),
nn.Conv2d(int(outchannel / 4), int(outchannel / 4), kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(int(outchannel / 4)),
nn.ReLU(inplace=True),
nn.Conv2d(int(outchannel / 4), outchannel, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(outchannel),
)
self.shortcut = nn.Sequential()
if stride != 1 or inchannel != outchannel:
self.shortcut = nn.Sequential(
nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(outchannel)
)
def forward(self, x):
out = self.left(x)
y = self.shortcut(x)
out += self.shortcut(x)
out = F.relu(out)
return out
三、ResNet主体
介绍了上述BasicBlock基础块和BotteNeck结构后,我们就可以搭建ResNet结构了。
5种不同层数的ResNet结构图如图所示:
代码:
ResNet18
'''----------ResNet18----------'''
class ResNet_18(nn.Module):
def __init__(self, ResidualBlock, num_classes=10):
super(ResNet_18, self).__init__()
self.inchannel = 64
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
)
self.layer1 = self.make_layer(ResidualBlock, 64, 2, stride=1)
self.layer2 = self.make_layer(ResidualBlock, 128, 2, stride=2)
self.layer3 = self.make_layer(ResidualBlock, 256, 2, stride=2)
self.layer4 = self.make_layer(ResidualBlock, 512, 2, stride=2)
self.fc = nn.Linear(512, num_classes)
def make_layer(self, block, channels, num_blocks, stride):
strides = [stride] + [1] * (num_blocks - 1) # strides=[1,1]
layers = []
for stride in strides:
layers.append(block(self.inchannel, channels, stride))
self.inchannel = channels
return nn.Sequential(*layers)
def forward(self, x): # 3*32*32
out = self.conv1(x) # 64*32*32
out = self.layer1(out) # 64*32*32
out = self.layer2(out) # 128*16*16
out = self.layer3(out) # 256*8*8
out = self.layer4(out) # 512*4*4
out = F.avg_pool2d(out, 4) # 512*1*1
out = out.view(out.size(0), -1) # 512
out = self.fc(out)
return out
ResNet34
'''----------ResNet34----------'''
class ResNet_34(nn.Module):
def __init__(self, ResidualBlock, num_classes=10):
super(ResNet_34, self).__init__()
self.inchannel = 64
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
)
self.layer1 = self.make_layer(ResidualBlock, 64, 3, stride=1)
self.layer2 = self.make_layer(ResidualBlock, 128, 4, stride=2)
self.layer3 = self.make_layer(ResidualBlock, 256, 6, stride=2)
self.layer4 = self.make_layer(ResidualBlock, 512, 3, stride=2)
self.fc = nn.Linear(512, num_classes)
def make_layer(self, block, channels, num_blocks, stride):
strides = [stride] + [1] * (num_blocks - 1) # strides=[1,1]
layers = []
for stride in strides:
layers.append(block(self.inchannel, channels, stride))
self.inchannel = channels
return nn.Sequential(*layers)
def forward(self, x): # 3*32*32
out = self.conv1(x) # 64*32*32
out = self.layer1(out) # 64*32*32
out = self.layer2(out) # 128*16*16
out = self.layer3(out) # 256*8*8
out = self.layer4(out) # 512*4*4
out = F.avg_pool2d(out, 4) # 512*1*1
out = out.view(out.size(0), -1) # 512
out = self.fc(out)
return out
ResNet50
'''---------ResNet50--------'''
class ResNet_50(nn.Module):
def __init__(self, ResidualBlock, num_classes=10):
super(ResNet_50, self).__init__()
self.inchannel = 64
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
)
self.layer1 = self.make_layer(ResidualBlock, 256, 3, stride=1)
self.layer2 = self.make_layer(ResidualBlock, 512, 4, stride=2)
self.layer3 = self.make_layer(ResidualBlock, 1024, 6, stride=2)
self.layer4 = self.make_layer(ResidualBlock, 2048, 3, stride=2)
self.fc = nn.Linear(512 * 4, num_classes)
# **************************
def make_layer(self, block, channels, num_blocks, stride):
strides = [stride] + [1] * (num_blocks - 1) # strides=[1,1]
layers = []
for stride in strides:
layers.append(block(self.inchannel, channels, stride))
self.inchannel = channels
return nn.Sequential(*layers)
def forward(self, x): # 3*32*32
out = self.conv1(x) # 64*32*32
out = self.layer1(out) # 64*32*32
out = self.layer2(out) # 128*16*16
out = self.layer3(out) # 256*8*8
out = self.layer4(out) # 512*4*4
out = F.avg_pool2d(out, 4) # 512*1*1
# print(out.size())
out = out.view(out.size(0), -1) # 512
out = self.fc(out)
return out
四、完整代码
import torch
import torch.nn as nn
import torch.nn.functional as F
'''-------------一、BasicBlock模块-----------------------------'''
# 用于ResNet18和ResNet34基本残差结构块
class BasicBlock(nn.Module):
def __init__(self, inchannel, outchannel, stride=1):
super(BasicBlock, self).__init__()
self.left = nn.Sequential(
nn.Conv2d(inchannel, outchannel, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(outchannel),
nn.ReLU(inplace=True), #inplace=True表示进行原地操作,一般默认为False,表示新建一个变量存储操作
nn.Conv2d(outchannel, outchannel, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(outchannel)
)
self.shortcut = nn.Sequential()
#论文中模型架构的虚线部分,需要下采样
if stride != 1 or inchannel != outchannel:
self.shortcut = nn.Sequential(
nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(outchannel)
)
def forward(self, x):
out = self.left(x) #这是由于残差块需要保留原始输入
out += self.shortcut(x)#这是ResNet的核心,在输出上叠加了输入x
out = F.relu(out)
return out
'''-------------二、Bottleneck模块-----------------------------'''
# 用于ResNet50及以上的残差结构块
class Bottleneck(nn.Module):
def __init__(self, inchannel, outchannel, stride=1):
super(Bottleneck, self).__init__()
self.left = nn.Sequential(
nn.Conv2d(inchannel, int(outchannel / 4), kernel_size=1, stride=stride, padding=0, bias=False),
nn.BatchNorm2d(int(outchannel / 4)),
nn.ReLU(inplace=True),
nn.Conv2d(int(outchannel / 4), int(outchannel / 4), kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(int(outchannel / 4)),
nn.ReLU(inplace=True),
nn.Conv2d(int(outchannel / 4), outchannel, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(outchannel),
)
self.shortcut = nn.Sequential()
if stride != 1 or inchannel != outchannel:
self.shortcut = nn.Sequential(
nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(outchannel)
)
def forward(self, x):
out = self.left(x)
y = self.shortcut(x)
out += self.shortcut(x)
out = F.relu(out)
return out
'''-------------ResNet18---------------'''
class ResNet_18(nn.Module):
def __init__(self, ResidualBlock, num_classes=10):
super(ResNet_18, self).__init__()
self.inchannel = 64
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
)
self.layer1 = self.make_layer(ResidualBlock, 64, 2, stride=1)
self.layer2 = self.make_layer(ResidualBlock, 128, 2, stride=2)
self.layer3 = self.make_layer(ResidualBlock, 256, 2, stride=2)
self.layer4 = self.make_layer(ResidualBlock, 512, 2, stride=2)
self.fc = nn.Linear(512, num_classes)
def make_layer(self, block, channels, num_blocks, stride):
strides = [stride] + [1] * (num_blocks - 1) # strides=[1,1]
layers = []
for stride in strides:
layers.append(block(self.inchannel, channels, stride))
self.inchannel = channels
return nn.Sequential(*layers)
def forward(self, x): # 3*32*32
out = self.conv1(x) # 64*32*32
out = self.layer1(out) # 64*32*32
out = self.layer2(out) # 128*16*16
out = self.layer3(out) # 256*8*8
out = self.layer4(out) # 512*4*4
out = F.avg_pool2d(out, 4) # 512*1*1
out = out.view(out.size(0), -1) # 512
out = self.fc(out)
return out
'''-------------ResNet34---------------'''
class ResNet_34(nn.Module):
def __init__(self, ResidualBlock, num_classes=10):
super(ResNet_34, self).__init__()
self.inchannel = 64
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
)
self.layer1 = self.make_layer(ResidualBlock, 64, 3, stride=1)
self.layer2 = self.make_layer(ResidualBlock, 128, 4, stride=2)
self.layer3 = self.make_layer(ResidualBlock, 256, 6, stride=2)
self.layer4 = self.make_layer(ResidualBlock, 512, 3, stride=2)
self.fc = nn.Linear(512, num_classes)
def make_layer(self, block, channels, num_blocks, stride):
strides = [stride] + [1] * (num_blocks - 1) # strides=[1,1]
layers = []
for stride in strides:
layers.append(block(self.inchannel, channels, stride))
self.inchannel = channels
return nn.Sequential(*layers)
def forward(self, x): # 3*32*32
out = self.conv1(x) # 64*32*32
out = self.layer1(out) # 64*32*32
out = self.layer2(out) # 128*16*16
out = self.layer3(out) # 256*8*8
out = self.layer4(out) # 512*4*4
out = F.avg_pool2d(out, 4) # 512*1*1
out = out.view(out.size(0), -1) # 512
out = self.fc(out)
return out
'''-------------ResNet50---------------'''
class ResNet_50(nn.Module):
def __init__(self, ResidualBlock, num_classes=10):
super(ResNet_50, self).__init__()
self.inchannel = 64
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
)
self.layer1 = self.make_layer(ResidualBlock, 256, 3, stride=1)
self.layer2 = self.make_layer(ResidualBlock, 512, 4, stride=2)
self.layer3 = self.make_layer(ResidualBlock, 1024, 6, stride=2)
self.layer4 = self.make_layer(ResidualBlock, 2048, 3, stride=2)
self.fc = nn.Linear(512 * 4, num_classes)
# **************************
def make_layer(self, block, channels, num_blocks, stride):
strides = [stride] + [1] * (num_blocks - 1) # strides=[1,1]
layers = []
for stride in strides:
layers.append(block(self.inchannel, channels, stride))
self.inchannel = channels
return nn.Sequential(*layers)
def forward(self, x): # 3*32*32
out = self.conv1(x) # 64*32*32
out = self.layer1(out) # 64*32*32
out = self.layer2(out) # 128*16*16
out = self.layer3(out) # 256*8*8
out = self.layer4(out) # 512*4*4
out = F.avg_pool2d(out, 4) # 512*1*1
# print(out.size())
out = out.view(out.size(0), -1) # 512
out = self.fc(out)
return out
def ResNet18():
return ResNet_18(BasicBlock)
def ResNet34():
return ResNet_34(BasicBlock)
def ResNet50():
return ResNet_50(Bottleneck)
本篇到这里就结束啦,有什么问题,欢迎大家留言讨论~