目录
一 理论学习
1. ResNet
ResNet在2015年由微软实验室提出,斩获当年ImageNet竞赛中分类任务第一名,目标检测第一名。获得COCO数据集中目标检测第一名,图像分割第一名。
论文亮点:
- 超深的网络结构(突破1000层):随着网络层度的不断加深,会出现梯度消失或者梯度爆炸和退化问题,可以用残差解决
- 提出残差模块
- 使用Batch Normalization加速训练(丢弃dropout)
背景
论文作者认为,网络的深度对于网络而言至关重要,然而,随着网络层度的不断加深,会出现梯度消失或者梯度爆炸和退化问题
随着网络深度的增加,准确率达到饱和然后迅速退化。即网络达到一定层数后继续加深模型会导致模型表现下降。然而,这种退化并不是由过拟合造成的,也不是由梯度消失和爆炸造成的,在一个合理的深度模型中增加更多的层却导致了更高的错误率。作者提出这样的问题可以用残差解决。

残差结构
通过跳跃连接来实现了一个简单的恒等映射。作者通过优化F(x),使得F(x)→0,从而使得F(x)+x→x ,最终达到一个恒等映射的关系。
论文提出的残差结构:
左边结构针对于Resnet34,右边针对于ResNet50/101/152,不同之处在于多了两个1*1的卷积层,其作用在于降维和升维。
网络结构
网络由一系列残差结构组成
Plain network:卷积层主要为3*3的filter,设计遵循:(i) 输出特征尺寸相同的层含有相同数量的filter;(ii) 如果特征尺寸减半,则filter的数量增加一倍来保证每层的时间复杂度相同。论文直接通过stride为2的卷积层来进行下采样。
Residual network在以上plain网络的基础上,插入shortcut连接,将网络变成了对应的残差版本。如果输入和输出的维度相同时,可以直接使用恒等shortcuts(实线),当维度增加时(虚线),考虑两个选项:(A) shortcut仍然使用恒等映射,在增加的维度上使用0来填充,这样做不会增加额外的参数;(B) 通过通过1*1的卷积改变x的维度,来使维度保持一致。
34层的网络结构:7*7卷积核-3*3最大池化下采样-3层Conv2残差层-4层Conv3残差层-6层Conv4残差层-3层Conv5残差层-平均池化下采样-全连接
实验表明,引入残差学习后,深层的网络更容易优化并且不会产生更高的训练错误率,甚至还能降低错误率。
注意:实线和虚线
实线输入的shape和输出的shape一样,所以可以直接相加;虚线的残差结构输出和输入shape不一样,右图先利用为2的步长,把长和宽缩减为原来的一半,然后通过128的卷积核,改变输入的深度(卷积结构交接的第一层)
注意原论文中,右侧虚线残差结构的主分支 上,第一个1x1卷积层的步距是2,第二个3x3卷积层步距是1。但在pytorch官方实现过程中是第一个1x1卷积层的步距是1,第二个3x3卷积层步距是2,这样能够在imagenet的top1.上提升大概0.5%的准确率
Batch Normalization
Batch Normalization的目的是使一批( Batch )feature map满足均值为0,方差为1的分布规律。
我们在图像预处理过程中通常会对图像进行标准化处理,这样能够加速网络的收敛,如下图所示对于Conv1来说,它的输入就足满足某一分布的特征矩阵,但对Conv2而言输入的feature map就不一定满足某一分布规律了(注意这里所说满足某一分布规律并不是指某一个feature map的数据要满足分布规律,理论上是指整个训练样本集所对应feature map的数据要满足分布规律) 。而Batch Normalization的目的就是去调整feature map,使fature map满足均值为0,方差为1的分布规律。
下面看一个例子,对下面两个图进行bn处理,首先要计算均值和方差,通过公式进行计算
使用BN需要注意:
- 训练时要将traning参数设置为True,在验证时将trainning参数设置为False。在pytorch中可通过创建模型的model.train()和model.eval()方法控制。
- batch size尽可能设置大点,设置小后表现可能很糟糕,设置的越大求的均值和方差越接近整个训练集的均值和方差。
- 建议将bn层放在卷积层(Conv) 和激活层(例如Relu) 之间,且卷积层不要使用偏置bias,因为没有用,参考下图推理,即使使用了偏置bias求出的结果也是一样的
迁移学习
使用迁移学习的优势:
- 能够快速的训练出一个理想的结果
- 当数据集较小时也能训练出理想的效果
网络前几层学到的是比较通用的信息
常见的迁移学习方式:
- 载入权重后训练所有参数
- 载入权重后只训练最后几层参数
- 载入权重后在原网络基础上再添加一层全连接层,仅训练最后-一个全连接层.
2.ResNetX
论文亮点:
采用 VGGs/ResNets 的网络的 depth 加深方式,同时利用 split-transform-merge 策略
组卷积(Group Convolution):
假设输入特征矩阵channel等于4,分为两个组,对每个组分别进行卷积操作,假设对每个Group使用n/2个卷积核,通过第每个Group的卷积可以得到对应的channel是n/2的特征矩阵,再对两组进行concat拼接,那么最终特征矩阵得到的channel是n。
再假设输入矩阵的channel等于cin,对输入特征矩阵分为g个组,那么对于每个group而言,每个group采用卷积核的参数是(k×k×cin/g×n/g)。
当g=cin,n=cin,这就相当于对输入特征矩阵的每一个channel分配了一个channel为1的卷积核进行卷积
block
将原ResNet中的block替换成新的block
这三个block在数学计算上完全等价,
c:先通过1*1卷积层进行降维处理(channel 256-128),再通过group对它进行处理(group数32,大小3*3,输出channel128),最后通过一个1*1的卷积升维
b:第一层有32个分支(32*4=128,与c第一层等价),第二层和c的group卷积一样,对于每个path理解为一个Group
a:第三层对每个path,先通过1*1的卷积,在进行相加(和b的第三层先通过concat拼接,在进行1*1卷积等价)
abc第三层的等价示例如下图所示,(a)假设path=2,对于每个path采用1*1卷积,再把feature进行相加。(b)chennel为4的特征图进行1*1卷积


网络结构
32是Group数,4对应每个组卷积核的个数
论文作者还研究了不同的Group个数和Group内卷积核个数的搭配效果
只有block层数大于等于3的时候,才能构建出一个比较有意义的block,所以对之前的浅层block而言,还是使用下图的方式
二 代码学习
1.LeNet
网络结构如下:
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Conv2d(3, 16, 5)
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(16, 32, 5)
self.pool2 = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(32*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 2)
def forward(self, x):
x = F.relu(self.conv1(x)) # input(3, 32, 32) output(16, 28, 28)
x = self.pool1(x) # output(16, 14, 14)
x = F.relu(self.conv2(x)) # output(32, 10, 10)
x = self.pool2(x) # output(32, 5, 5)
x = x.view(-1, 32*5*5) # output(32*5*5)
x = F.relu(self.fc1(x)) # output(120)
x = F.relu(self.fc2(x)) # output(84)
x = self.fc3(x) # output(10)
return x
训练结果:
生成csv文件后提交平台,评审结果如下:
2.ResNet
ResNet18和ResNet34的网络结构:
#18、34层的残差网络
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):
super(BasicBlock, self).__init__()
# 不使用偏执参数(使用BN时不需要)
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=3, stride=stride, padding=1, bias=False)
# output - (input3 +2*1)/ 2 +1= input /2 +0.5= input/ 2 (向下取整)
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 ResNet(nn.Module):
def __init__(self,
block, #定义的残差结构
blocks_num, #列表,对应残差结构的数目(34:3 4 6 3)
num_classes=1000, #训练集的分类个数
include_top=True,
groups=1, #X
width_per_group=64):
super(ResNet, self).__init__()
self.include_top = include_top
self.in_channel = 64 #max pooling之后得到的特征矩阵的深度
self.groups = groups
self.width_per_group = width_per_group
# 1.对应表格中7*7的卷积层
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)
# 2.最大池化下采样操作
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 3.conv2
self.layer1 = self._make_layer(block, 64, blocks_num[0])
# 4.conv3
self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
# 5.conv4
self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
# 6.conv4
self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
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')
#channel对应第一个卷积核的个数
#block_num该层一共包含了多少残差结构
def _make_layer(self, block, channel, block_num, stride=1):
downsample = None
# 34的conv2: 64 == 64*1 18和34会跳过这个if;50、101、152的64 !=64*4
# 从conv3开始,由于stride变成2,所以都会生成下采样函数
if stride != 1 or self.in_channel != channel * block.expansion:
# stride=1时高和宽不变,从conv3开始会调整高宽
downsample = nn.Sequential(
#(64,64*4,)
nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(channel * block.expansion))
layers = []
#先把第一层残差结构添加进来。这个部分实现了1834和50们实线虚线的区别
layers.append(block(self.in_channel,
channel,
downsample=downsample,
stride=stride,
groups=self.groups, #X
width_per_group=self.width_per_group))
# 18和34不变,其他的要*4
self.in_channel = channel * block.expansion
#把剩下一系列实线残差结构添加进来(从第二层开始全是实线)
for _ in range(1, block_num):
layers.append(block(self.in_channel,
channel,
groups=self.groups,
width_per_group=self.width_per_group))
#把列表转换成非关键字参数(可变参数就是传入的参数个数是可变的)
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
采用ResNet34,训练效果如下:
提交网站后得分如下,可以看出ResNet相比LeNet可以得到更好的分类效果
三 问题
1、Residual learning
残差学习包括两种映射,分别是
- identity mapping,指的是上图右边那条弯的曲线。identity mapping指的就是本身的映射,也就是x自身;
- residual mapping,指的是另一条分支,也就是F(x)部分,这部分称为残差映射,也就是 y-x。
2、Batch Normailization 的原理
通过归一化手段,使一批( Batch )feature map满足均值为0,方差为1的分布规律,这样使得激活输入值分布在非线性函数梯度敏感区域,从而避免梯度消失问题,大大加快训练速度。
3、为什么分组卷积可以提升准确率?即然分组卷积可以提升准确率,同时还能降低计算量,分数数量尽量多不行吗?
分组卷积可以减少参数量,它用少量的参数量和运算量就可以生成大量的特征图,由此可以提取到更充分的模型特征,从而提升准确率。
当分组数目多到与输入层通道数相同(一个卷积核负责一个通道),卷积完成后的特征图数量与输入层的通道数也就相同,无法扩展特征图,而且这种运算对输入层的每个通道独立进行卷积运算,没法效的利用不同通道在相同空间位置上的特征信息。