残差原理
网络退化(degradation):因为梯度弥散等原因,在不断加神经网络的深度时,模型准确率会先上升然后达到饱和,再持续增加深度时则会导致准确率下降。
残差网络ResNet的出现就是为了解决网络深度变深以后的性能退化问题。
ResNet的灵感来源:假设现有一个比较浅的网络(Shallow Net)已达到了饱和的准确率,这时在它后面再加上几个恒等映射层(Identity mapping 即y=x 输出等于输入),这样就增加了网络的深度,并且起码误差不会增加,即更深的网络不应该带来训练集上误差的上升。
某段神经网络的输入是x 期望输出是H(x) ,即H(x)是期望的复杂潜在映射,学习这样的模型,训练难度会比较大。
回想前面的假设,如果已经学习到较饱和的准确率(或者当发现下层的误差变大时),那么接下来的学习目标就转变为恒等映射的学习,也就是使输入x近似于输出H(x) ,以保持在后面的层次中不会造成精度下降。
通过“跳跃连接”(skip)的方式 直接把输入x传到输出作为初始结果,输出结果为 H(x)=F(x)+x。当 F(x)=0 时,那么 H(x)=x,也就是上面所提到的恒等映射。于是,ResNet相当于将学习目标改变了,不再是学习一个完整的输出,而是目标值H(X)和x的差值,也就是所谓的残差 F(x) = H(x)-x,因此,后面的训练目标就是要将残差结果逼近于0,使得随着网络加深,准确率不下降。
为什么有效?
- 链式求导后的结果不会趋近0,避免了梯度弥散。
- 学习 F(x)=0 比学习 H(x)=x 要简单。
一般网络中的参数初始化趋近于0,相比于更新该网络层的参数来学习H(x)=x,该冗余层学习F(x)=0的更新参数能够更快收敛。
网络结构
上图为34层ResNet结构
图中跳跃连接分为实线和虚线,虚线表示F(x)和x的通道数不相同,不可直接相加,需要卷积进行下采样。
=600x)
对于50层以上的网络,卷积层会优化为瓶颈(BottleNeck)结构,保持精度的同时减少参数量。
代码实现
Cifar10数据集图片shape(3, 32, 32)
网络层结构:
conv (3x3, 16)
conv (3x3, 32) * 4
conv (3x3, 64) * 8
conv (3*3, 128) * 4
fc 10
共18层,参数量3.2M。
# 定义3x3卷积层 BatchNormal后 bias不起作用
def conv3x3(in_channels, out_channels, stride=1):
return nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
# 普通残差块
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = conv3x3(in_channels, out_channels, stride)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(out_channels, out_channels)
self.bn2 = nn.BatchNorm2d(out_channels)
self.downsample = downsample
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class ResidualNet(nn.Module):
def __init__(self, block, layers, num_classes=10):
"""
主网络
:param block: 给定残差块 basic/bottleneck
:param layers:每层残差块数量 list类型
:param num_classes:输出分类数
"""
super(ResidualNet, self).__init__()
self.in_channels = 16 # 第一个残差块 输入通道数
self.conv = conv3x3(3, 16)
self.bn = nn.BatchNorm2d(16)
self.relu = nn.ReLU(True)
self.layer1 = self.make_layer(block, 32, layers[0])
self.layer2 = self.make_layer(block, 64, layers[1], stride=2)
self.layer3 = self.make_layer(block, 128, layers[2], stride=2)
# self.avg_pool = nn.AvgPool2d(8)
self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(128, num_classes)
def make_layer(self, block, out_channels, blocks, stride=1):
"""
:param block:
:param out_channels:
:param blocks: 当前层残差块数目
:param stride:
:return:
"""
downsample = None
"""
当stride不为1 特征图尺寸发生变化 identity需要下采样
当残差块输入输出通道不一样时 identity需要通过1x1卷积改变通道数
"""
if stride != 1 or self.in_channels != out_channels:
downsample = nn.Sequential(
nn.Conv2d(self.in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
layers = []
# 添加第一个残差块
layers.append(block(self.in_channels, out_channels, stride, downsample))
# 上一层输出通道数 作为下一层输入通道数
self.in_channels = out_channels
# 循环添加剩余残差块
for _ in range(1, blocks):
layers.append(block(self.in_channels, out_channels))
return nn.Sequential(*layers) # 序列解包
def forward(self, x):
out = self.conv(x)
out = self.bn(out)
out = self.relu(out)
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
# out = self.layer4(out)
out = self.avg_pool(out)
out = out.reshape((out.size()[0], -1))
out = self.fc(out)
return out
r = ResidualNet(BasicBlock, [2, 4, 2]).cuda()
训练集做数据增强
train_transform = transforms.Compose([
transforms.Resize(40), # 缩放
transforms.RandomHorizontalFlip(), # 概率为0.5的水平翻转
transforms.RandomCrop(32), # 随机裁剪
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
使用Adam优化器,测试集准确率为73%。