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

残差原理

Alt

网络退化(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,使得随着网络加深,准确率不下降。

为什么有效?

  1. 链式求导后的结果不会趋近0,避免了梯度弥散。
  2. 学习 F(x)=0 比学习 H(x)=x 要简单。
    一般网络中的参数初始化趋近于0,相比于更新该网络层的参数来学习H(x)=x,该冗余层学习F(x)=0的更新参数能够更快收敛。

网络结构


上图为34层ResNet结构

图中跳跃连接分为实线和虚线,虚线表示F(x)和x的通道数不相同,不可直接相加,需要卷积进行下采样。
![](https://img-blog.csdnimg.cn/20201021155911306.png#pic_center
=600x)

对于50层以上的网络,卷积层会优化为瓶颈(BottleNeck)结构,保持精度的同时减少参数量。
Alt

代码实现

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%。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值