ResNet解读和1×1卷积介绍

1、Introduction

  深度卷积网络好在哪里?

-----它可以加很多层,把网络变得很深,不同程度的层可以得到不同等级的特征。

 作者提出一个问题--一个网络只是简单的堆叠就好了嘛?

——不是,当网络变得很深的时候,梯度会出现爆炸或者消失。一个解决方法,初始化时设置一个合适的权重,不要太大也不要太小。又或者在中间加入normalization(包括BN),可以使得校验每个层之间的那些输出和它梯度的那些均值和方差。上述两个方法可以让网络收敛,但是网络变深后,性能会变差(不是过拟合造成的,因为训练误差和测试误差都会变得很差),所以现在网络虽然收敛了,但是不能得到一个好的效果。

考虑一个比较浅的网络版本和一个比较深的网络版本(所谓深的版本是在所说的浅的网络中多加一些层进去),作者说如果你的浅的网络效果还不错的话,理论上深的网络是不应该变差的,因为深的网络加的那些层,总是可以把那些层学的变成一个恒等映射(identity mapping)。实际上做不到,SGD找不到这样一个优解。

这篇文章提出了一个方法,使得显示的构造出一个 identity mapping,深的网络不会变得比浅的网络差。作者命名为deep residual learning framework 。

2、结构

X 为浅层网络的输出。如果我们想要得到的映射为H(X),则我们让添加的非线性网络层去拟合残差映射F(X):=H(X)-X。原始的映射就可以写成F(X)+X。

shortcut connection快捷连接通常会跳过 1 个或者多个层,在 ResNet 中快捷连接直接运用了 identity mapping,意思就是将一个卷积栈的输入直接与这个卷积栈的输出相加。

这样有什么好处呢?

  1. 并没有增加新的参数
  2. 整个网络也仍然可以由 SGD 进行训练。
  3. 容易被通用的神经网络框架实现。

F(X)和X直接相加,因此需要保证他们的维度一定要一样。

如何处理输入和输出不同形状的情况?

---第一种,在输入和输出上添加一些额外的零,使得这两个形状能够对应起来,然后可以相加。

--第二种,投影,使用一个1×1卷积调整通道,使得形状可以对应起来。(1×1卷积层特点--在空间维度上不做任何的东西,主要是在通道维度上做改变,选择一个1×1卷积使得输出通道是输入通道的两倍,这样就能将残差连接的输入和输出对应起来。在ResnNet里面,如果把输出通道数翻了两倍,那么输入的高和宽会减小一半,所以这里步幅设置为2,使在高宽和通道上都能匹配上)。第三种,所有连接都做投影。(这里在论文中有做实验比较三种处理方法的优劣)

各种层数的残差结构

残差网络结构(以34层为例)--VGG-19是浅层网络

 更深的残差网络结构(50层以上),结构有所不同,设计了一个bottleneck结构

 通道数为256时,变得很大,出现的问题是计算复杂度会很高,这里做法是通过1×1卷积投影映射回64维,再做一个3×3通道数不变的卷积,然后再通过1×1卷积投影回去256维,因为输入是256维u,输出要匹配上,这样设计之后复杂度就跟左图差不多了。

补充-关于1×1卷积

理解1×1卷积的作用_dxwell6的博客-CSDN博客_1×1卷积

1*1卷积核的作用_nefetaria的博客-CSDN博客_1*1卷积核的作用

详细学习1*1卷积核_来一包板栗的博客-CSDN博客_1*1卷积

1×1卷积就是将卷积核的尺寸设置为1×1,如图所示

 1×1卷积的意义

  1. 跨通道的特征整合

      如果当前层和下一层都只有一个通道那么1×1卷积核确实没什么作用,但是如果它们分别为m层和n层的话,1×1卷积核可以起到一个跨通道聚合的作用,所以进一步可以起到降维(或者升维)的作用,起到减少参数的目的。

    一个例子来直观地介绍1x1卷积。输入6x6x1的矩阵,这里的1x1卷积形式为1x1x1,即为元素2,输出也是6x6x1的矩阵。但输出矩阵中的每个元素值是输入矩阵中每个元素值x2的结果。

上述情况,并没有显示1x1卷积的特殊之处,那是因为上面输入的矩阵channel为1,所以1x1卷积的channel也为1。这时候只能起到升维的作用。这并不是1x1卷积的魅力所在。 

当输入为6x6x32时,1x1卷积的形式是1x1x32,当只有一个1x1卷积核的时候,此时输出为6x6x1。此时便可以体会到1x1卷积的实质作用:降维。当1x1卷积核的个数小于输入channels数量时,即降维。

注:1x1卷积一般只改变输出通道数(channels),而不改变输出的宽度和高度

2.  特征通道的升维和降维

 

  1. 3、减少卷积核参数(简化模型)

可以看到1×1卷积核通过控制卷积核的数量来进行降维和升维
网络深度由原来的2层变成了三层,广泛使用1×1卷积可以增加模型的深度,增加模型非线性能力

应用一

Inception网络

按照上面的说法,我们的这层的模型参数与输入的特征维数(28x28x192),卷积核大小以及卷积通道数(包括三种卷积核,分别是1x1x64,3x3x128,5x5x32),所以参数为:

参数:(1×1×192×64) + (3×3×192×128) + (5×5×192×32) = 153600
最终输出的feature map个数:64+128+32+192 = 416

池化层不引人参数!

feature map个数就是filter个数,一个滤波器在前一个feature map上进行一次卷积操作(特征抽取)会产生一个feature map或者叫通道、深度。

上图中在3x3,5x5 卷积层前新加入的1x1的卷积核为96和16通道的,max pooling后加入的1x1卷积为32通道。

图中该层的参数:

参数:(1×1×192×64)+(1×1×192×96+3×3×96×128)+(1×1×192×16+5×5×16×32)+(1x1x32)=15904
最终输出的feature map个数: 64+128+32+32=256

所以,加入1×1的卷积后,在降低大量运算的前提下,降低了维度。

 应用二 

ResNet网络

resnet代码(动手深度学习中的)

# ResNet

#首先来看一下残差块block的实现

'''torch.nn是pytorch中自带的一个函数库,里面包含了神经网络中使用的一些常用函数,
如具有可学习参数的nn.Conv2d(),nn.Linear()和不具有可学习的参数(如ReLU,pool,DropOut等)(后面这几个是在nn.functional中),
这些函数可以放在构造函数中,也可以不放。
torch.nn的应用:
通常引入的时候写成:
import torch.nn as nn
import torch.nn.functional as F'''

import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l  

#一个残差网络层 包括两个卷积层和一个旁路支路
# input_channels通道数  num_channels输出通道数 use_1x1conv是否使用1*1卷积  strides步长

# 声明一个类 并继承自nn.Module  # nn.Module是所有网络模型结构的基类,无论是pytorch自带的模型,还是要自定义模型,都需要继承这个类
class Residual(nn.Module):  #@save
    def __init__(self, input_channels, num_channels,
                 use_1x1conv=False, strides=1):
        # 调用父类构造函数,super指的就是nn.Module这个基类
        super().__init__()
# 一个残差块包含两个卷积层,第一个卷积层通常改变输入输出通道数,并且改变输出的尺寸的形状大小,
# 第二个卷积层输入输出通道数通常不会改变,每一个卷积层会跟着一个批量规范层
'''Conv2d --convolution layers中的一种 常见的还有Conv1d Conv3d
   作用:对由多个输入平面组成的输入信号进行二维卷积
   从整体上看,它是一个类,包含了卷积运算所需要的参数(__init__函数),以及卷积操作(forward函数)。'''
        self.conv1 = nn.Conv2d(input_channels, num_channels,
                               kernel_size=3, padding=1, stride=strides)
        self.conv2 = nn.Conv2d(num_channels, num_channels,
                               kernel_size=3, padding=1)
        if use_1x1conv: # 如果要使用1*1卷积
            self.conv3 = nn.Conv2d(input_channels, num_channels,
                                   kernel_size=1, stride=strides)
# #当输入输出通道数(同时输出的形状大小也会改变)改变后需要加一个1x1卷积层,来改变输入X的形状大小和通道数       
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, X):# 定义执行顺序的默认函数--这里是从里到外
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        Y += X
    # #将输入经过两层卷积层得到的输出Y再与输入X相加后,再经过ReLU()激活函数,必须保证X和Y的通道数和尺寸形状大小相同
        return F.relu(Y)
#查看输入和输出形状一致,不改变输入的通道数和尺寸大小
residual = Residual(3,3)
X = torch.rand(4, 3, 6, 6)
Y = residual (X)
print(Y.shape)
#查看增加输出通道数的同时,并减半输入的高和宽
residual =Residual(3,6,use_1x1conv2d=True,stride=2)
X = torch.randn(size=(4,3,6,6))
Y = residual(X)
print(Y.shape)

'''
输出结果:
torch.Size([4, 3, 6, 6])
torch.Size([4, 6, 3, 3]) 通道加倍  高宽减半
'''
   

上述代码生成两种类型的网络: 一种是当use_1x1conv=False时,应用ReLU非线性函数之前,将输入添加到输出。 
另一种是当use_1x1conv=True时,添加通过卷积调整通道和分辨率。

resnet模型

1、ResNet的前两层跟之前介绍的GoogLeNet中的一样: 在输出通道数为64、步幅为2的 7×7 卷积层后,接步幅为2的 3×3 的最大汇聚层。 不同之处在于ResNet每个卷积层后增加了批量规范化层。

#ResNet第一个模块跟GoogleNet第一个模块相同

'''torch.nn.Sequential

一个序列容器,用于搭建神经网络的模块被按照被传入构造器的顺序添加到nn.Sequential()容器中。
除此之外,一个包含神经网络模块的OrderedDict也可以被传入nn.Sequential()容器中。
利用nn.Sequential()搭建好模型架构,模型前向传播时调用forward()方法,
模型接收的输入首先被传入nn.Sequential()包含的第一个网络模块中。
然后,第一个网络模块的输出传入第二个网络模块作为输入,按照顺序依次计算并传播,
直到nn.Sequential()里的最后一个模块输出结果。'''

b1 = nn.Sequential(nn.Conv2d(in_channels=1,out_channels=64,kernel_size=7,padding=3,stride=2),
                   nn.BatchNorm2d(64),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3,padding=1,stride=2))

2、GoogLeNet在后面接了4个由Inception块组成的模块。 ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。 第一个模块的输出通道数同输入通道数一致。 由于之前已经使用了步幅为2的最大汇聚层,所以无须减小高和宽。 之后的每个模块在第一个残差块里将上一个模块的输出通道数翻倍,并将高和宽减半, 下面代码来实现这个模块。注意,我们对第一个残差网络模块做了特别处理。

#定义一个ResNet块,通常包含两个残差块Residul块(也即是包含两个残差网络层),
#一个ResNet块通常通道数加倍,尺寸形状高和宽减半,对应到由第一个残差块输出通道是输入通道两倍,尺寸大小减半,
# 第二个残差块输入输出通道数相同,输入输出尺寸形状大小不变,
# 但除开第二个ResNet块,因为第一个ResNet块将输入尺寸形状大小降低了4倍(已经减半过了)
def resnet_block(input_channels,output_channels,num_residuls,first_block=False):
    block = []
    for i in range(num_residuls):
        if i==0 and not first_block:
            block.append(Residul(input_channels=input_channels,output_channels=output_channels,use_1x1conv2d=True,stride=2))
        else:
            block.append(Residul(input_channels=output_channels,output_channels=output_channels))
    return block

3、接着在ResNet加入所有残差块,每个模块使用2个残差块。

# *在python中是解包
b2 = nn.Sequential(*resnet_block(64,64,2,True))#第二个ResNet块,输入输出通道数不变,输入输出尺寸形状大小不变
b3 = nn.Sequential(*resnet_block(64,128,2,False))#第三个ResNet块,输出通道数是输入通道数2倍,则输出尺寸形状是输入尺寸形状高和宽的1/2
b4 = nn.Sequential(*resnet_block(128,256,2,False))#第二个ResNet块,输出通道数是输入通道数2倍,则输出尺寸形状是输入尺寸形状高和宽的1/2
b5 = nn.Sequential(*resnet_block(256,512,2,False))#第二个ResNet块,输出通道数是输入通道数2倍,则输出尺寸形状是输入尺寸形状高和宽的1/2

4、最后,与GoogLeNet一样,在ResNet中加入全局平均汇聚层,以及全连接层输出。

resnet = nn.Sequential(b1,b2,b3,b4,b5,
                       nn.AdaptiveAvgPool2d((1,1)),
                       nn.Flatten(),
                       nn.Linear(in_features=512,out_features=10))

5、在训练ResNet之前看一下ResNet中不同模块的输出形状是如何变化的。 基本在所有架构中都是将分辨率降低,通道数量增加,直到全局平均汇聚层聚集所有特征。

#查看每一层输出的通道数和形状尺寸大小
X = torch.randn(size=(1,1,224,224))
for layer in resnet:
    X = layer(X)
    print(layer.__class__.__name__," output shape :\t",X.shape)



'''
输出结果如下:
Sequential output shape:	 torch.Size([1, 64, 56, 56])
Sequential output shape:	 torch.Size([1, 64, 56, 56])
Sequential output shape:	 torch.Size([1, 128, 28, 28])
Sequential output shape:	 torch.Size([1, 256, 14, 14])
Sequential output shape:	 torch.Size([1, 512, 7, 7])
AdaptiveAvgPool2d output shape:	 torch.Size([1, 512, 1, 1])
Flatten output shape:	 torch.Size([1, 512])
Linear output shape:	 torch.Size([1, 10])
'''

6、ResNet模型训练和测试

lr,num_epochs,batch_size = 0.05,10,64
train_iter,test_iter = d2l.torch.load_data_fashion_mnist(batch_size,resize=224)
d2l.torch.train_ch6(resnet,train_iter,test_iter,num_epochs,lr,device=d2l.torch.try_gpu())

上述代码实现的resnet结构如下图:

 

// resnet完整代码(李沐)
import d2l.torch
import torch
from torch.nn import functional as F
from torch import nn

#一个残差网络层,包含两个卷积层和一个旁路支路
class Residul(nn.Module):
    def __init__(self,input_channels,output_channels,use_1x1conv2d=False,stride=1):
        super(Residul, self).__init__()
        #一个残差块包含两个卷积层,第一个卷积层通常改变输入输出通道数,并且改变输出的尺寸的形状大小,第二个卷积层输入输出通道数通常不会改变,每一个卷积层会跟着一个批量规范层
        self.conv2d_1 = nn.Conv2d(in_channels=input_channels,out_channels=output_channels,kernel_size=3,padding=1,stride=stride)
        self.conv2d_2 = nn.Conv2d(in_channels=output_channels,out_channels=output_channels,kernel_size=3,padding=1,stride=1)
        self.bn1 = nn.BatchNorm2d(num_features=output_channels)
        self.bn2 = nn.BatchNorm2d(num_features=output_channels)
        #当输入输出通道数(同时输出的形状大小也会改变)改变后需要加一个1x1卷积层,来改变输入X的形状大小和通道数
        if use_1x1conv2d:
            self.conv2d_3 = nn.Conv2d(in_channels=input_channels,out_channels=output_channels,kernel_size=1,stride=stride)
        else:
            self.conv2d_3 = None
    def forward(self,X):
        Y = F.relu(self.bn1(self.conv2d_1(X)))
        Y = self.bn2(self.conv2d_2(Y))
        if self.conv2d_3:
            X = self.conv2d_3(X)
        Y +=X
        #将输入经过两层卷积层得到的输出Y再与输入X相加后,再经过ReLU()激活函数,必须保证X和Y的通道数和尺寸形状大小相同
        return F.relu(Y)

residul =Residul(3,6,use_1x1conv2d=True,stride=2)
X = torch.randn(size=(4,3,6,6))
Y = residul(X)
print(Y.shape)
#ResNet第一个模块跟GoogleNet第一个模块相同
b1 = nn.Sequential(nn.Conv2d(in_channels=1,out_channels=64,kernel_size=7,padding=3,stride=2),
                   nn.BatchNorm2d(64),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3,padding=1,stride=2))
#定义一个ResNet块,通常包含两个残差块Residul块(也即是包含两个残差网络层),一个ResNet块通常通道数加倍,尺寸形状高和宽减半,对应到由第一个残差块输出通道是输入通道两倍,尺寸大小减半,第二个残差块输入输出通道数相同,输入输出尺寸形状大小不变,但除开第二个ResNet块,因为第一个ResNet块将输入尺寸形状大小降低了4倍
def resnet_block(input_channels,output_channels,num_residuls,first_block=False):
    block = []
    for i in range(num_residuls):
        if i==0 and not first_block:
            block.append(Residul(input_channels=input_channels,output_channels=output_channels,use_1x1conv2d=True,stride=2))
        else:
            block.append(Residul(input_channels=output_channels,output_channels=output_channels))
    return block
#  *在python中是解包
b2 = nn.Sequential(*resnet_block(64,64,2,True))#第二个ResNet块,输入输出通道数不变,输入输出尺寸形状大小不变
b3 = nn.Sequential(*resnet_block(64,128,2,False))#第三个ResNet块,输出通道数是输入通道数2倍,则输出尺寸形状是输入尺寸形状高和宽的1/2
b4 = nn.Sequential(*resnet_block(128,256,2,False))#第二个ResNet块,输出通道数是输入通道数2倍,则输出尺寸形状是输入尺寸形状高和宽的1/2
b5 = nn.Sequential(*resnet_block(256,512,2,False))#第二个ResNet块,输出通道数是输入通道数2倍,则输出尺寸形状是输入尺寸形状高和宽的1/2
resnet = nn.Sequential(b1,b2,b3,b4,b5,
                       nn.AdaptiveAvgPool2d((1,1)),
                       nn.Flatten(),
                       nn.Linear(in_features=512,out_features=10))
X = torch.randn(size=(1,1,224,224))
for layer in resnet:
    X = layer(X)
    print(layer.__class__.__name__," output shape :\t",X.shape)

lr,num_epochs,batch_size = 0.05,10,64
train_iter,test_iter = d2l.torch.load_data_fashion_mnist(batch_size,resize=224)
d2l.torch.train_ch6(resnet,train_iter,test_iter,num_epochs,lr,device=d2l.torch.try_gpu())
  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值