ResNet详解与实现

姓名:刘慧林;学号:21021210619;学院:电子工程学院

1、前言

ResNet是何恺明等人于2015年提出的神经网络结构,该网络凭借其优秀的性能夺得了多项机器视觉领域竞赛的冠军,而后在2016年发表的论文《Deep Residual Learning for Image Recognition》也获得了CVPR2016最佳论文奖。本文整理了笔者对ResNet的理解,详细解释了ResNet34、ResNet50等具体结构,并使用PyTorch实现了一个使用ResNet训练CIFAR-10数据集的具体实例。

2、深度残差学习(Deep Residual Learning)

卷积神经网络在图像分类领域具有非常广泛的应用,从理论上讲,越深的网络结构,其拟合能力应该越强,如16层的VGG16的拟合能力要强于5层的LeNet。然而,何恺明等人通过实验发现,当网络的深度达到一定程度时,网络的性能不升反降,并且这种性能的下降不是由过拟合引起的,因为深度网络的训练误差和测试误差都比浅层网络高,如图1所示。

ResNet1.png

为改善上述问题,何恺明等人提出了深度残差学习框架。

2.1 残差学习

常规的神经网络,是将卷积层、全连接层等结构按照一定的顺序简单地连接到一起,每层结构仅接受来自上一层的信息,并在本层处理后传递给下一层。当网络层次加深后,这种单一的连接方式会导致神经网络性能退化。所谓的残差学习,就是在上述的单一连接方式的基础上,加入了“短连接”(shortcut connections),如图所示。

ResNet2.png

短连接能跨越几个层,将输入x直接映射到输出端(类似于电路中的短路),与输出相加。这样做导致的直接结果就是,在神经网络中加入上述的结构不再会导致神经网络的退化,考虑最坏的情况也不过是F(x)为0,这相当于网络没有加深,与之前一致。但是如果加入的层有效,就能使网络的性能得到提升。从数学的角度来看,上述结构的输入为x,输出H(x)为:

其中F(x)称为残差函数,变换上式可得:

显然,该结构中加入的短连接是没有参数的,因此学习H(x)的参数与学习F(x)的参数本质上是一样的,但是将函数优化为0或者在0附近显然更容易。因此,残差学习,学习的就是残差函数F(x)的参数。

2.2 ResNet基本单元

论文原文中给出了两种ResNet的基本单元结构,其中左边的单元用于较浅的网络,如ResNet18、ResNet34,右边的单元则用于较深的网络,如ResNet50、ResNet101等。

ResNet3.png

首先,无论是左边还是右边的单元,都是由卷积层和激活函数构成的。左边的单元由2个大小为3x3的卷积层与2个ReLU激活函数构成,右边的单元由2个1x1、1个3x3的卷积层和3个ReLU激活函数构成。至于卷积核的的通道(channel)数,则由单元的具体位置决定。通常卷积神经网络在提取图像特征的过程种,卷积核都会从“大卷积核、低通道数量“层层递进到到”小卷积核、多通道数量“。因此,在实际应用的过程种,短连接跨越的卷积层的通道数量有所改变是非常常见的。原论文中给出了直接连接、填零连接和映射连接三种具体的短连接形式。当实际单元的输入通道数与输出相同时,如上图左,则短连接直接连接即可。若输入通道数与输出不相同,如上图右,输入的维度为64,输出却是256,此时输入x无法与F(x)直接相加。填零连接就是将多出的维度全部填充0后再进行相加,这个方法不会引入额外的参数。映射连接则是通过矩阵变换,利用大小为1x1的卷积层扩展输入的维度后再进行相加。短连接跨越通道数不同的卷积层时,卷积执行的步长为2,这刚好缩小特征矩阵的大小,并增加了特征的通道数。具体的细节将在下文中的代码实现,此处不再赘述。

2.3 ResNet

原论文中给出了如下5个具体的ResNet网络,这些网络都由上述的基本单元构成。

ResNet4.png

为了便于阐述,我们直接来讨论ResNet34,该网络主要由16个基本单元、1个7x7卷积层与1个全连接层构成,共计34层。

ResNet5.png

其中,实线的短连接表示直接连接,前后通道数未发生改变。虚线的短连接则表示前后维度发生了改变,需要通过填0或者映射后进行连接。

3、代码实现

上面讲了这么多白开水般的原理,相信各位看官早已疲倦,接下来上紧张刺激的代码,毕竟看了那么多,最终目的还是为了应用。对于ResNet34,其基本单元实现代码如下。

class ResidualBlock(nn.Module):
expansion = 1 # 扩展系数
def init(self, in_channels, out_channels, stride=1):
super(ResidualBlock, self).init()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)

self.shortcut = nn.Sequential()

短连接方式

if stride != 1 or in_channels != out_channels * self.expansion:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels * self.expansion, kernel_size=1,
stride=stride,bias=False),
nn.BatchNorm2d(out_channels * self.expansion)
)

def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x = self.bn2(self.conv2(x))
x = x + self.shortcut(x)
out = F.relu(x)

return out</pre>

只要理解了上文所叙述的原理,代码还是很容易理解的。其中的扩展系数expansion表示的是单元输出与输入张量的通道数之比,对于ResNet34,这个比是1。而对于ResNet50,这个比是4。如果无法理解,可以回顾上文2.2中的图。ResNet50的基本单元代码如下。

class Bottleneck(nn.Module):
expansion = 4
def init(self, in_channels, out_channels, stride=1):
super(Bottleneck, self).init()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.conv3 = nn.Conv2d(out_channels, self.expansion *
out_channels, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(self.expansion*out_channels)

self.shortcut = nn.Sequential()
if stride != 1 or in_channels != self.expansionout_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, self.expansion
out_channels,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(self.expansion*out_channels)
)

def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = F.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
out += self.shortcut(x)
out = F.relu(out)
return out</pre>

有了建房子的”砖块“,就可以着手建房了,ResNet的实现代码如下。

class ResNet(nn.Module):
def init(self, block, num_blocks, num_classes=10):
super(ResNet, self).init()
self.in_channels = 64
self.conv1 = nn.Conv2d(3, 64, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.layer1 = self.__make_layer(block, 64, num_blocks[0], stride=1)
self.layer2 = self.__make_layer(block, 128, num_blocks[1], stride=2)
self.layer3 = self.__make_layer(block, 256, num_blocks[2], stride=2)
self.layer4 = self.__make_layer(block, 512, num_blocks[3], stride=2)
self.fc = nn.Linear(512*block.expansion, num_blocks)

def __make_layer(self, block, out_channels, num_blocks, stride):
strides = [stride] + [1] * (num_blocks-1)
layers = []
for s in strides:
layers.append(block(self.in_channels, out_channels, s))
self.in_channels = out_channels*block.expansion

return nn.Sequential(*layers)

def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = F.avg_pool2d(x, 4)
out = self.fc(x)

return out</pre>

该代码可以实现论文中的5个网络ResNet18、ResNet34、ResNet50、ResNet101、ResNet152。ResNet34的实例化代码如下:

resnet34 = ResNet(ResidualBlock, [3, 4, 6, 3])</pre>

具体的参数num_blocks可查阅2.3中的表确定。

搭建好了神经网络,就可以用于训练模型了。ResNet34训练CIFAR-10的例程请点击此处访问。本文代码参考了Gihub上的项目pytorch-cifar与torchvision.models中的代码,特此声明,以示感谢。

4、总结

本文介绍了一些我在学习过程中对于ResNet的理解,并给出了代码与具体的实例。从实质上讲,ResNet与传统卷积神经网络的主要区别就是引入了短连接,而这个短连接极大地提升了神经网络的性能。至于数学原理,本文并未深究,毕竟包括我在内的许多学习者都是关心如何应用,而不是深挖那些数学公式的来龙去脉。

由于本人才疏学浅,如有疏漏谬误之处,还请指出,万分感谢。

5、参考

文献:He K, Zhang X, Ren S, et al. Deep residual learning for image recognition[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2016: 770-778.

代码:https://github.com/kuangliu/pytorch-cifar

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353