手动实现ResNet残差网络 Pytorch代码

it2024-10-27  38

残差原理

网络退化(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%。

最新回复(0)