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