【卷积神经网络系列】七、ResNet

参考资料:

论文地址:

Deep Residual Learning for Image Recognition

参考博客:

ResNet详解

你必须要知道CNN模型:ResNet

ResNet 详解与复现


一、简介

 ResNet 网络是在 2015年 由微软实验室中的何凯明等几位大神提出,斩获当年ImageNet竞赛中分类任务第一名,目标检测第一名。获得COCO数据集中目标检测第一名,图像分割第一名。

 COCO 是一个大规模的目标检测、分割的数据集,具有以下几个特点:

(1)目标分割 ;(2)图像情景识别 ;(3)超像素分割 ;

(4)拥有 330K 张图像(其中 >200K 张有标签);(5)150 万个实例对象 ;(6)80 个目标类别 ;

(7)91个物品类别 ;(8)每张图片有 5 个描述 ;(9)250K 张人的图像有关键点标注 ;

1. 网络中的亮点

  • 1.超深的网络结构(超过1000层)。

  • 2.提出residual(残差结构)模块。

  • 3.使用Batch Normalization 加速训练(丢弃dropout)。

2. 为什么采用residual

在ResNet提出之前,所有的神经网络都是通过卷积层和池化层的叠加组成的。人们认为卷积层和池化层的层数越多,获取到的图片特征信息越全,学习效果也就越好。但是在实际的试验中发现,随着卷积层和池化层的叠加,不但没有出现学习效果越来越好的情况,反而两种问题:

(1)梯度消失和梯度爆炸

  • 梯度消失:若每一层的误差梯度小于1,反向传播时,网络越深,梯度越趋近于0;
  • 梯度爆炸:若每一层的误差梯度大于1,反向传播时,网络越深,梯度越来越大;

(2)退化问题

 深度网络出现了退化问题:网络深度增加时,网络准确度出现饱和,甚至出现下降。

在这里插入图片描述

  • 为了解决梯度消失或梯度爆炸问题,ResNet论文提出通过数据的预处理以及在网络中使用 BN(Batch Normalization)层来解决
  • 为了解决深层网络中的退化问题,可以人为地让神经网络某些层跳过下一层神经元的连接,隔层相连,弱化每层之间的强联系。这种神经网络被称为残差网络 (ResNet)ResNet论文提出了residual结构(残差结构)来减轻退化问题,下图是使用residual结构的卷积网络,可以看到随着网络的不断加深,效果并没有变差,而是变的更好了。(虚线是train error,实线是test error)

在这里插入图片描述


二、ResNet详解

 对于一个网络,如果简单地增加深度,就会导致 梯度弥散梯度爆炸,我们采取的解决方法是 正则化
 随着网络层数进一步增加,又会出现模型退化问题,在训练集上的 准确率出现饱和甚至下降 的现象 。

 通过利用内部的残差块实现跳跃连接,解决神经网络深度加深带来的模型退化问题:

在这里插入图片描述

 在传统网络中采用的输入输出函数为:F(x)(output1) = x(input)

 在残差网络中利用残差模块使输入输出函数为:F(x)(output2) = F(x)(output1) + x(input)

 x(input) 直接跳过多层 加入到最后的输出 F(x)(output2) 单元当中,解决 F(x)(output1) 可能带来的 模型退化问题

1. Residual Block

在这里插入图片描述

浅层网络: 采用的是左侧的残差结构(18-layer、34-layer),左侧称为BasicBlock

深层网络: 采用的是右侧的残差结构(50-layer、101-layer、152-layer),右侧称为Bottleneck

  • 采用BasicBlock,参数的个数应该是:256×256×3×3×2=1179648;
  • 采用Bottleneck,参数的个数应该是:1×1×256×64+3×3×64×64+1×1×256×64=69632;

(1)通过 1X1卷积 既能够改变通道数,又能大幅减少计算量参数量;可以对比 34-layer50-layer 发现它们的参数量分别为 3.6 X 10^93.8 X 10^9

(2)先降后升为了主分支上输出的特征矩阵和捷径分支上输出的特征矩阵形状相同,以便进行加法操作。

注:CNN参数个数 = 卷积核尺寸×卷积核深度 × 卷积核组数 = 卷积核尺寸 × 输入特征矩阵深度 × 输出特征矩阵深度。

注意:搭建深层次网络时,采用三层的残差结构。

2. ResNet50详解

ResNet18ResNet34采用的是BasicBlockResNet50ResNet101ResNet152采用的是Bottleneck

在这里插入图片描述

 以下以 ResNet50 为代表进行介绍:

  • 它是由4个 大block 组成 ;
  • 每个 大block 分别由 [3, 4, 6, 3]小block 组成 ;每个小block都有三个卷积操作 ;
  • 在网络开始前还有 一个 卷积操作 ;
  • 层数:(3+4+6+3)x 3 + 1 = 50layer

 其中每个大的 block 里面都是由两部分组成:Conv BlockIdentity Block

(1)Conv Block输入和输出维度不相同,不能串联,主要用于改变网络维度
在这里插入图片描述

(2)Identity Block输入和输出维度相同,可以串联,主要用于加深网络层数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ResNet50:[3, 4, 6, 3]可以表示为:

  • conv2_xConv Block+ Identity Block + Identity Block
  • conv3_xConv Block+ Identity Block+ Identity Block+ Identity Block
  • conv4_xConv Block+ Identity Block+ Identity Block+ Identity Block+ Identity Block+Identity Block
  • conv5_xConv Block+ Identity Block+ Identity Block

ResNet50结构框图:

在这里插入图片描述


三、论文复现

参考:

ResNet 详解与复现

理论+实践 | ResNet、ResNeXt好兄代码复现和解析


(1)定义不带ReLu的卷积:卷积+BN

class ConvBN(nn.Module):
    def __init__(self, in_channels, out_channels, **kwargs):
        super(ConvBN, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
        self.bn = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        x = self.conv(x)
        x = self.bn(x)
        return x

(2)BasicBlock

  • 如果stride!=1,说明输入和输出的WxH发生了改变,此时的Shortcut也需要改变WxH;
  • 如果输入通道数in_channels!=输出通道数expansion*out_channels,此时的Shortcut也需要扩展通道数;
  • 如果输入和输出的Channel一样,并且stride=1,说明输入和输出可以直接相加;
    在这里插入图片描述
    在这里插入图片描述
class BasicBlock(nn.Module):      # 左侧的 residual block 结构(18-layer、34-layer)

    # 如果是BasicBlock,每个小block的输入=输出
    expansion = 1
    
    def __init__(self, in_channels, out_channels, stride=1):      # 两层卷积 Conv2d + Shutcuts
        super(BasicBlock, self).__init__()
        
        # 两个3x3卷积核
        self.conv = nn.Sequential(
            # 第一个cov是用来改变WxH的(stride可指定)
            ConvBN(in_channels=in_channels, out_channels=self.expansion*out_channels, 
                       kernel_size=3, stride=stride, padding=1),
            
            nn.ReLU(inplace=True),
            
            # 第二个conv的stride恒为1,不改变WxH
            ConvBN(in_channels=self.expansion*out_channels, out_channels=self.expansion*out_channels, 
                   kernel_size=3, stride=1, padding=1),     
        )

        self.shortcut = nn.Sequential()
        
        # Shortcuts用于构建 Conv Block 和 Identity Block
        if stride != 1 or in_channels != self.expansion*out_channels:
            self.shortcut = nn.Sequential(
                # 卷积+BN,不激活
                ConvBN(in_channels=in_channels, out_channels=self.expansion*out_channels, 
                   kernel_size=1, stride=stride)
            )
            
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        out = self.conv(x)
        out += self.shortcut(x)
        return self.relu(out)

(3)Bottleneck:

  • 如果stride!=1,说明输入和输出的WxH发生了改变,此时的Shortcut也需要改变WxH;
  • 如果输入通道数in_channels!=输出通道数out_channels,此时的Shortcut也需要扩展通道数;
  • 如果输入和输出的Channel一样,并且stride=1,说明输入和输出可以直接相加;

在这里插入图片描述
在这里插入图片描述

class Bottleneck(nn.Module):      # 右侧的 residual block 结构(50-layer、101-layer、152-layer)
    
    # 观察50-layer可以发现,各个block内部卷积核通道数是4倍的关系
    expansion = 4
    
    def __init__(self, in_channels, out_channels, stride=1):      # 三层卷积 Conv2d + Shutcuts
        super(Bottleneck, self).__init__()
        
        # 1x1 -> 3x3 -> 1x1
        self.conv = nn.Sequential(
            # 第一个cov是用来降维的,减少参数量
            ConvBN(in_channels=in_channels, out_channels=out_channels, 
                   kernel_size=1),
            nn.ReLU(inplace=True),
            
            # 第二个conv是用来改变WxH的(stride可指定)
            ConvBN(in_channels=out_channels, out_channels=out_channels, 
                   kernel_size=3, stride=stride, padding=1),
            nn.ReLU(inplace=True),  
            
            # 第三个conv用来升维
            ConvBN(in_channels=out_channels, out_channels=self.expansion*out_channels, 
                   kernel_size=1)       
        )  
        self.shortcut = nn.Sequential()
        
        # Shortcuts用于构建 Conv Block 和 Identity Block
        if stride != 1 or in_channels != self.expansion*out_channels:
            self.shortcut = nn.Sequential(
                # 卷积+BN,不激活
                ConvBN(in_channels=in_channels, out_channels=self.expansion*out_channels, 
                   kernel_size=1, stride=stride)
            )
            
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        out = self.conv(x)
        out += self.shortcut(x)
        return self.relu(out)

(4)主体结构:

在这里插入图片描述
在这里插入图片描述

class ResNet(nn.Module):
    def __init__(self, block, numlist_blocks, num_classes=2):
        """
        Args:
            block:      选用BasicBlock还是Bottleneck这两种残差结构
            num_blocks: 针对不同数量的layers,有不同的组合,比如ResNet50为[3, 4, 6, 3]
            num_classes:最终分类数量
        """
        super(ResNet, self).__init__()
        
        self.in_channels = 64

        # 原始输入为229x229x3 -> 112x112x64
        self.conv1 = ConvBN(in_channels=3, out_channels=64, kernel_size=7, stride=2) # conv1
        # 112x112x64 -> 56x56x64
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)         # maxpool
        
        self.layer1 = self._make_layer(block, 64,  numlist_blocks[0], stride=1)      # conv2_x
        self.layer2 = self._make_layer(block, 128, numlist_blocks[1], stride=2)      # conv3_x
        self.layer3 = self._make_layer(block, 256, numlist_blocks[2], stride=2)      # conv4_x
        self.layer4 = self._make_layer(block, 512, numlist_blocks[3], stride=2)      # conv5_x
        
        
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))     # 平均池化
        self.linear = nn.Linear(2048, num_classes)      # 线性层
        self.relu = nn.ReLU(inplace=True)

    def _make_layer(self, block, in_channels, num_blocks, stride):
        # 虽然每个convn_x由多个block组成,但是其中只有某个block的stride为2,剩余的为1
        strides = [stride] + [1]*(num_blocks-1)
        layers = []
        
        for stride in strides:
            layers.append(block(self.in_channels, in_channels, stride))
            
            # 经过某个convn_x之后,in_channels被放大对应expansion倍
            self.in_channels = in_channels * block.expansion
        
        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.relu(self.conv1(x))    # conv1
        x = self.maxpool(x)             # maxpool
        x = self.layer1(x)              # conv2_x
        x = self.layer2(x)              # conv3_x
        x = self.layer3(x)              # conv4_x
        x = self.layer4(x)              # conv5_x
        
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        out = self.linear(x)
        
        return out

(5)构建不同的网络

在这里插入图片描述

def make_net(net, num_classes):
    if net == 'ResNet18':
        return ResNet(BasicBlock, [2, 2, 2, 2], num_classes)
    if net == 'ResNet34':
        return ResNet(BasicBlock, [3, 4, 6, 3], num_classes)
    if net == 'ResNet50':
        return ResNet(Bottleneck, [3, 4, 6, 3], num_classes)
    if net == 'ResNet101':
        return ResNet(Bottleneck, [3, 4, 23, 3], num_classes)
    if net == 'ResNet152':
        return ResNet(Bottleneck, [3, 8, 36, 3], num_classes)
        
def test():
    ResNet50 = make_net('ResNet50', num_classes=2)
    #创建模型,部署gpu
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    ResNet50.to(device)
    summary(ResNet50, (3, 229, 229))
    
# test()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

travellerss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值