前言
笔者从人工智能小白的角度,力求能够从原文中解析出最高效率的知识。 之前看了很多博客去学习AI,但发现虽然有时候会感觉很省时间,但到了复现的时候就会傻眼,因为太多实现的细节没有提及。而且博客具有很强的主观性,因此我建议还是搭配原文来看。
请下载原文《Deep Residual Learning for Image Recognition》搭配阅读本文,会更高效哦!
一、论文精读
0.摘要标题和结论
《Deep Residual Learning for Image Recognition》通过对标题,摘要,结论的阅读,我得到以下信息:
- 使用残差学习框架使训练深层网络变得更加容易。用了152层网络,比VGG深8倍,但计算复杂度更低(好奇为什么?)
- 将常用模型的CNN主干网络换成了残差网络,且在许多数据集上有不错的效果,例如目标检测数据集COCO。
- 未加残差时,34层网络比18层网络的误差上更高。加上残差后,误差更低。
1.Introduction:
(1)Why Deep?因为不同的层可以得到一些不同的特征,例如低级的视觉特征和高级的语义特征。 (2)深层网络的问题?存在梯度爆炸和梯度消失的情况。如何解决? ① 权重初始化时适当。 ② 在中间加入BN(Batch Normalization),来校验每个层的输出,和梯度的均值和方差。避免层之间大小相差太大。 (3)收敛后精度降低。原因并非模型复杂及层数增加带来的过拟合,因为训练误差也变高了。理论上说更深网络,就可以训练成简单网络,然后加上identity mapping,但是SGD找不到。于是本文提出了残差网络模型。 (4)残差模型:某层输出H(x),新添加的层不去学H(x),而是去学H(x)-x(就是网络已学的知识和真实世界的差距)。最后输出相当于新学层F(x)加上之前的旧网络输出的x。
结果:越深精度越高。
为什么要用Residual?
思考后我认为: ① 不会增加学习参数,不会增加模型复杂度,不会增加计算(只不过是个加法)。 ② 这可以使上一个残差块的信息没有阻碍的流入到下一个残差块,提高了信息流通,并且也避免了由与网络过深所引起的消失梯度问题和退化问题。 ③ 从数学上理解ResNet,相当于在原有的梯度上做了加法,防止梯度消失,让SGD可以一直跑起来。 ④ 加了残差,模型复杂度也许会降低,可能不那么容易overfitting
2.Related Work:
(1)Residual Representations:之前工作当有残差时,比没有残差的标准解法计算要快。 (2)Short connection:highway networks
3.Deep Residual Learning:
(1)残差连接处理输入和输出形状不同的方法: ① 输入和输出分别添加额外的0,使得两个形状所对应。 ② 投影。通过11的卷积层,也就是在空间上不做任何东西(让我联想到了Network Compression里面的Depthwise Separable Convolution中的Pointwise Convolution,只考虑通道间关系,不考虑通道内部关系),主要是在通道维度上做改变,使得输出通道是输出通道的两倍,输入的高和宽被减半,所以步幅为2,使得高宽匹配。(存疑?) ③ 就算输入输出形状一样,在连接时,做11卷积。 (2)Implementation具体实现: ① 数据方面,调了调RGB,然后取的窗口[256,480]比较大,随机性强。测试集也随机sample了10个图片,降低了方差,而且还采用了不同的分辨率,提高了精度。 ② 参数初始化,具体初始化参数设置,然后lr也是错误率比较平的时候就除以10,和AlexNet所用方法一致。(现在目前好像没怎么用,这得守着模型的训练才行。)没有用dropout因为没有全连接层。
4.Experiments:
(1)网络架构 FLOPs:浮点数运算=输入的高宽通道数输出通道数和的高*和的宽 疑问:为什么50层和34层FLOPs差不多?
(2)有无残差误差收敛比较 有残差收敛会快,而且后期更好。
(3)对比输出输出不同形状处理的三种方法: C方法效果最好,但是计算量大,开销大。B方法计算量增加不多,效果也比较好。
(4)如何做到更深呢?Bottleneck
降维,投影映射,相当于特征空间降维,然后最后一层再映射回去,这样计算量和不降维差不多,又能增加这个深度和通道数,以抓取更多特征。
二、代码阅读
1.ResNet网络框架实现
class ResNet(nn.Module):
def __init__(self, block, blocks_num, num_classes=1000, include_top=True):
super(ResNet, self).__init__()
self.include_top = include_top
self.in_channel = 64
self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(self.in_channel)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, blocks_num[0])
self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
if self.include_top:
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
def _make_layer(self, block, channel, block_num, stride=1):
downsample = None
if stride != 1 or self.in_channel != channel * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(channel * block.expansion))
layers = []
layers.append(block(self.in_channel, channel, downsample=downsample, stride=stride))
self.in_channel = channel * block.expansion
for _ in range(1, block_num):
layers.append(block(self.in_channel, channel))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
if self.include_top:
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
2.18层或34层残差网络的残差模块
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, in_channel, out_channel, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channel)
self.relu = nn.ReLU()
self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channel)
self.downsample = downsample
def forward(self, x):
identity = x
if self.downsample is not None:
identity = self.downsample(x)
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out += identity
out = self.relu(out)
return out
3. 50层,101层,152层的残差网络的残差模块
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, in_channel, out_channel, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=1, stride=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channel)
self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
kernel_size=3, stride=stride, bias=False, padding=1)
self.bn2 = nn.BatchNorm2d(out_channel)
self.conv3 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel*self.expansion,
kernel_size=1, stride=1, bias=False)
self.bn3 = nn.BatchNorm2d(out_channel*self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
def forward(self, x):
identity = x
if self.downsample is not None:
identity = self.downsample(x)
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
out += identity
out = self.relu(out)
return out
4.代码心得
主要关注在降维和升维的实现(如2,3,4部分中的第一个残差块的下采样操作)。残差网络块的实现(part 2)。18和34层的残差块是相似的,50/101/152层的残差块是一样的,这两种残差块进行了分开定义。
|