PyTorch对ResNet网络的实现解析

PyTorch对ResNet网络的实现解析

1.首先导入需要使用的包

import torch.nn as nn
import torch.utils.model_zoo as model_zoo

# 默认的resnet网络,已预训练
model_urls = {
    'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',
    'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',
    'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
    'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
    'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
}

2.定义一个3*3的卷积层

def conv3x3(in_channels,out_channels,stride=1):
    return nn.Conv2d(
        in_channels,  # 输入深度(通道)
        out_channels, # 输出深度
        kernel_size=3,# 滤波器(过滤器)大小为3*3
        stride=stride,# 步长,默认为1
        padding=1,    # 0填充一层
        bias=False    # 不设偏置
        )

下面会重复使用到这个3*3卷积层,虽然只使用了几次...

这里为什么用深度而不用通道,是因为我觉得深度相比通道更有数量上感觉,其实都一样。

3.定义最重要的残差模块

这个是基础块,由两个叠加的3*3卷积组成

class BasicBlock(nn.Module):
    expansion = 1 # 是对输出深度的倍乘,在这里等同于忽略

    def __init__(self,in_channels,out_channels,stride=1,downsample=None):
        super(BasicBlock,self).__init__()
        self.conv1 = conv3x3(in_channels,out_channels,stride) # 3*3卷积层
        self.bn1 = nn.BatchNorm2d(out_channels) # 批标准化层
        self.relu = nn.ReLU(True) # 激活函数

        self.conv2 = conv3x3(out_channels,out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.downsample = downsample # 这个是shortcut的操作
        self.stride = stride # 得到步长

    def forward(self,x):
        residual = 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: # 当shortcut存在的时候
            residual = self.downsample(x)
            # 我们将上一层的输出x输入进这个downsample所拥有一些操作(卷积等),将结果赋给residual
            # 简单说,这个目的就是为了应对上下层输出输入深度不一致问题

        out += residual # 将bn2的输出和shortcut过来加在一起
        out = self.relu(out)

        return out

瓶颈块,有三个卷积层分别是1x1,3x3,1x1,分别用来降低维度,卷积处理,升高维度

class Bottleneck(nn.Module): # 由于bottleneck译意为瓶颈,我这里就称它为瓶颈块
    expansion = 4 # 若我们输入深度为64,那么扩张4倍后就变为了256
    # 其目的在于使得当前块的输出深度与下一个块的输入深度保持一致
    # 而为什么是4,这是因为在设计网络的时候就规定了的
    # 我想应该可以在保证各层之间的输入输出一致的情况下修改扩张的倍数

    def __init__(self,in_channels,out_channels,stride=1,downsample=None):
        super(Bottleneck,self).__init__()
        self.conv1 = nn.Conv2d(in_channels,out_channels,kernel_size=1,bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        # 这层1*1卷积层,是为了降维,把输出深度降到与3*3卷积层的输入深度一致

        self.conv2 = nn.conv3x3(out_channels,out_channels) # 3*3卷积操作
        self.bn2 = nn. BatchNorm2d(out_channels)
        # 这层3*3卷积层的channels是下面_make_layer中的第二个参数规定的

        self.conv3 = nn.Conv2d(out_channels,out_channels*self.expansion,kernel_size=1,bias=False)
        self.bn3 = nn.BatchNorm2d(out_channels*self.expansion)
        # 这层1*1卷积层,是在升维,四倍的升

        self.relu = nn.ReLU(True) # 激活函数
        self.downsample = downsample # shortcut信号
        self.stride = stride # 获取步长

    def forward(self,x):
        residual = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out) # 连接一个激活函数

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        if self.downsample is not None:
            residual = self.downsample(x) # 目的同上

        out += residual
        out = self.relu(out)

        return out

注意:降维只发生在当1*1卷积层的输出深度大于输入深度的时候,当输入输出深度一样时是没有降维的。Resnet中没有降维的情况只发生在刚开始第一个残差块那。

引入Bottleneck的目的是,减少参数的数目,Bottleneck相比较BasicBlock在参数的数目上少了许多,但是精度上却差不多。减少参数同时还会减少计算量,使模型更快的收敛。

4.ResNet主体部分的实现

class ResNet(nn.Module):

    def __init__(self,block,layers,num_classes=10):
        # block:为上边的基础块BasicBlock或瓶颈块Bottleneck,它其实就是一个对象
        # layers:每个大layer中的block个数,设为blocks更好,但每一个block实际上也很是一些小layer
        # num_classes:表示最终分类的种类数
        super(ResNet,self).__init__()
        self.in_channels = 64 # 输入深度为64,我认为把这个理解为每一个残差块块输入深度最好

        self.conv1 = nn.Conv2d(3,64,kernel_size=7,stride=2,padding=3,bias=False)
        # 输入深度为3(正好是彩色图片的3个通道),输出深度为64,滤波器为7*7,步长为2,填充3层,特征图缩小1/2
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(True) # 激活函数
        self.maxpool = nn.MaxPool2d(kernel_size=3,stride=2,padding=1) # 最大池化,滤波器为3*3,步长为2,填充1层,特征图又缩小1/2
        # 此时,特征图的尺寸已成为输入的1/4

        # 下面的每一个layer都是一个大layer
        # 第二个参数是残差块中3*3卷积层的输入输出深度
        self.layer1 = self._make_layer(block,64,layers[0]) # 特征图大小不变
        self.layer2 = self._make_layer(block,128,layers[1],stride=2) # 特征图缩小1/2
        self.layer3 = self._make_layer(block,256,layers[2],stride=2) # 特征图缩小1/2
        self.layer4 = self._make_layer(block,512,layers[3],stride=2) # 特征图缩小1/2
        # 这里只设置了4个大layer是设计网络时规定的,我们也可以视情况自己往上加
        # 这里可以把4个大layer和上边的一起看成是五个阶段

        self.avgpool = nn.AvgPool2d(7,stride=1) # 平均池化,滤波器为7*7,步长为1,特征图大小变为1*1
        self.fc = nn.Linear(512*block.expansion,num_classes) # 全连接层

        # 这里进行的是网络的参数初始化,可以看出卷积层和批标准化层的初始化方法是不一样的
        for m in self.modules():
            # self.modules()采取深度优先遍历的方式,存储了网络的所有模块,包括本身和儿子
            if isinstance(m,nn.Conv2d): # isinstance()判断一个对象是否是一个已知的类型
                nn.init.kaiming_normal_(m.weight,mode='fan_out',nonlinearity='relu')
                # 9. kaiming_normal 初始化 (这里是nn.init初始化函数的源码,有好几种初始化方法)
                # torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')
                # nn.init.kaiming_normal_(w, mode='fan_out', nonlinearity='relu')
                # tensor([[ 0.2530, -0.4382,  1.5995],
                #         [ 0.0544,  1.6392, -2.0752]])
            elif isinstance(m,nn.BatchNorm2d):
                nn.init.constant_(m.weight,1)
                nn.init.constant_(m.bias,0)
                # 3. 常数 - 固定值 val
                # torch.nn.init.constant_(tensor, val)
                # nn.init.constant_(w, 0.3)
                # tensor([[ 0.3000,  0.3000,  0.3000],
                #         [ 0.3000,  0.3000,  0.3000]])

    def _make_layer(self,block,out_channels,blocks,stride=1):
        # 这里的blocks就是该大layer中的残差块数
        # out_channels表示的是这个块中3*3卷积层的输入输出深度
        downsample = None # shortcut内部的跨层实现
        if stride != 1 or self.in_channels != out_channels*block.expansion:
            # 判断步长是否为1,判断当前块的输入深度和当前块卷积层深度乘于残差块的扩张
            # 为何用步长来判断,我现在还不明白,感觉没有也行
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channels,out_channels*block.expansion,kernel_size=1,stride=stride,bias=False),
                nn.BatchNorm2d(out_channels*block.expansion)
                )
            # 一旦判断条件成立,那么给downsample赋予一层1*1卷积层和一层批标准化层。并且这一步将伴随这特征图缩小1/2
            # 而为何要在shortcut中再进行卷积操作?是因为在残差块之间,比如当要从64深度的3*3卷积层阶段过渡到128深度的3*3卷积层阶段,主分支为64深度的输入已经通过128深度的3*3卷积层变成了128深度的输出,而shortcut分支中x的深度仍为64,而主分支和shortcut分支相加的时候,深度不一致会报错。这就需要进行升维操作,使得shortcut分支中的x从64深度升到128深度。
            # 而且需要这样操作的其实只是在基础块BasicBlock中,在瓶颈块Bottleneck中主分支中自己就存在升维操作,那么Bottleneck还在shortcut中引入卷积层的目的是什么?能带来什么帮助?

        layers = []
        layers.append(block(self.in_channels,out_channels,stride,downsample))
        # block()生成上面定义的基础块和瓶颈块的对象,并将dowsample传递给block

        self.in_channels = out_channels*block.expansion # 改变下面的残差块的输入深度
        # 这使得该阶段下面blocks-1个block,即下面循环内构造的block与下一阶段的第一个block的在输入深度上是相同的。
        for i in range(1,blocks):  # 这里面所有的block
            layers.append(block(self.in_channels,out_channels))
        #一定要注意,out_channels一直都是3*3卷积层的深度
        return nn.Sequential(*layers) # 这里表示将layers中的所有block按顺序接在一起

    def forward(self,x):
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.maxpool(out) # 写代码时一定要仔细,别把out写成x了,我在这里吃了好大的亏

        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)

        out = self.avgpool(out)
        out = out.view(out.size(0),-1) # 将原有的多维输出拉回一维
        out = self.fc(out)

        return out

5.定义各种ResNet网络

resnet18,共有18层卷积层

def resnet18(pretrained=False,**kwargs):
    '''
    pretrained:若为True,则返回在ImageNet数据集上预先训练的模型
    **kwargs:应该只包括两个参数,一个是输入x,一个是输出分类个数num_classes
    '''
    model = ResNet(BasicBlock,[2,2,2,2],**kwargs)
    # block对象为 基础块BasicBlock
    # layers列表为 [2,2,2,2],这表示网络中每个大layer阶段都是由两个BasicBlock组成

    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))

    return model

resnet34,共有34层卷积层

def resnet34(pretrained=False,**kwargs):
    model = ResNet(BasicBlock,[3,4,6,3],**kwargs)
    # block对象为 基础块BasicBlock
    # layers列表 [3,4,6,3]
    # 这表示layer1、layer2、layer3、layer4分别由3、4、6、3个BasicBlock组成

    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet34']))

    return model

resnet50,共有50层卷积层

def resnet50(pretrained=False,**kwargs):
    model = ResNet(Bottleneck,[3,4,6,3],**kwargs)
    # block对象为 瓶颈块Bottleneck
    # layers列表 [3,4,6,3]
    # 这表示layer1、layer2、layer3、layer4分别由3、4、6、3个Bottleneck组成

    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))

    return model

resnet101,共有101层卷积层

def resnet101(pretrained=False,**kwargs):
    model = ResNet(Bottleneck,[3,4,23,3],**kwargs)
    # block对象为 瓶颈块Bottleneck
    # layers列表 [3,4,23,3]
    # 这表示layer1、layer2、layer3、layer4分别由3、4、23、3个Bottleneck组成

    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet101']))

    return model

resnet152,共有152层卷积层

def resnet152(pretrained=False,**kwargs):
    model = ResNet(Bottleneck,[3,8,36,3],**kwargs)
    # block对象为 瓶颈块Bottleneck
    # layers列表 [3,8,36,3]
    # 这表示layer1、layer2、layer3、layer4分别由3、8、36、3个Bottleneck组成

    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet152']))

    return model

6.总结

我们可以从上面看出:

  • resnet18和resnet34只用到了简单的BasicBlock,resnet50、resnet101和resnet152用的是Bottleneck。
  • Bottleneck相比较BasicBlock在参数量上减少了16.94倍。
  • resnet50、resnet101和resnet152三个网络输入输出大小都一样,只是中间的参数个数不一样。
  • resnet网络中第一个残差块的输入深度都为64,其他的为残差块中3*3卷积层的深度乘以block.expansion。
  • 从每一个layer阶段到下一个layer阶段都伴随着特征图缩小1/2,特征图深度加深1/2。这发生在除第一个layer外的每个layer中的第一个残差块中。
  • resnet网络的四个layer前后的操作都是一样,因此resnet网络输入的图片尺寸固定为224*224(还不确定)。
  • 在理解网络的时候最好结合resnet18、resnet50的结构图。

原文地址:https://www.cnblogs.com/jiyou/p/11272650.html

时间: 2024-10-30 13:42:34

PyTorch对ResNet网络的实现解析的相关文章

文件与网络、JSON解析

文件与网络.JSON解析   分类: OC语言基础 同步.异步.JSON解析.XML解析 NSData 字节数据存储,二进制存储,苹果中认为字符型和字节型都可以存,即计算机中所有类型的数据,主要用于网络传输数据的缓冲存储 GET请求,只支持255个字符 ,安全性较差,将请求信息附加在超链接上,较方便  POST请求,上传没有大小限制,请求信息在请求体内,在超链接上不能看到,安全性强,缺点:不方便 同步连接:UI “冻结” 异步连接:UI “不冻结” 异步连接,用代理:下载容易造成UI冻结,耗时,

Android网络之数据解析----使用Google Gson解析Json数据

[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4063452.html 联系方式:[email protected] [正文] 文章回顾: Android网络之数据解析----SAX方式解析XML数据 一.Json数据的介绍                                                             

java网络编程socket解析

转载:http://www.blogjava.net/landon/archive/2013/07/02/401137.html Java网络编程精解笔记2:Socket详解 Socket用法详解 在C/S通信模式中,client需要主动创建于server连接的Socket(套接字).服务器端收到了客户端的连接请求,也会创建与客户连接的Socket.Socket可看做是通信两端的收发器.server与client都通过Socket来收发数据. 1.构造Socket 1.Socket() 2.So

iOS多线程与网络开发之解析xml

郝萌主倾心贡献,尊重作者的劳动成果,请勿转载. 如果文章对您有所帮助,欢迎给作者捐赠,支持郝萌主,捐赠数额随意,重在心意^_^ 我要捐赠: 点击捐赠 Cocos2d-X源码下载:点我传送 游戏官方下载:http://dwz.cn/RwTjl 游戏视频预览:http://dwz.cn/RzHHd 游戏开发博客:http://dwz.cn/RzJzI 游戏源码传送:http://dwz.cn/Nret1 A.XML基本知识 1.xml概念 什么是XML全称是Extensible Markup Lan

[iOS 多线程 & 网络 - 2.1] - 解析json

A.iOS中json的基本使用 1.解析json数据 (1)json反序列化 对象{}格式 {key : value, key : value,...} 的键值对的结构可以反序列化为OC中的NSDictionary数组[]格式 ["java","javascript","vb",...]可以反序列化为OC中的NSArray 提示JSON的数据格式与OC中的快速包装方法非常类似JSON的数据格式同样支持嵌套 (2)json工具 从iOS 5开始,使

swift 网络请求&json解析

做实际项目总是离不开这两步,大多数情况下都是采用第三方框架来辅助完成,swift也不例外,由于swift并不成熟,用swift语言写的第三方框架很不稳定(苹果更新太快了),所幸swift和oc可以混编,所以本次使用的框架都是oc里面常用的.网络请求使用AFNetworking,json解析使用MJExtension. 下面开始正文: 1. swift中使用oc框架 其原理很简单,需要一个.h头文件,里面导入我们需要使用的oc框架的头文件,把这个.h文件配置到项目设置中即可 1).我们采取一个不容

IOS SWIFT 网络请求JSON解析 基础一

前言:移动互联网时代,网络通信已经是手机端必不可少的功能.应用中也必不可少地使用了网络通信,增强客户端与服务器交互.使用NSURLConnection实现HTTP的通信.NSURLConnection 提供了异步请求和同步请求两种通信方式.同步请求数据会造成主线程阻塞,通常在请求大数据或网络不通畅时不建议使用. 不管同步请求还是异步请求,建立通信的步骤是一样的: 1 创建NSURL 2 创建NSURLRequest 3 创建NSURLConnection 当NSURLConnection 创建成

怎样使用ListView实现一个带有网络请求,解析,分页,缓存的公共的List页面来大大的提高工作效率

在寻常的开发中常常会有非常多列表页面.每做一个列表页就须要创建这个布局文件那个Adapter适配器文件等等一大堆与之相关的附属的不必要的冗余文件. 假设版本号更新迭代比較频繁,如此以往,就会使项目project变得无比庞大臃肿. 假设看过这篇文章或者在使用过这样的方式之后呢,全部的工作都能够被压缩成仅仅有两个文件,一个JAVA文件一个XML布局文件. 并且代码还少少的. 咱们来看看实际情况: 寻常的一个列表页面的生成须要下面文件: 一个Activity文件.有时候可能还会忘记注冊 一个包括上下拉

iOS多线程与网络开发之解析json数据

郝萌主倾心贡献,尊重作者的劳动成果,请勿转载. 假设文章对您有所帮助,欢迎给作者捐赠,支持郝萌主,捐赠数额任意.重在心意^_^ 我要捐赠: 点击捐赠 Cocos2d-X源代码下载:点我传送 游戏官方下载:http://dwz.cn/RwTjl 游戏视频预览:http://dwz.cn/RzHHd 游戏开发博客:http://dwz.cn/RzJzI 游戏源代码传送:http://dwz.cn/Nret1 A.iOS中json的基本使用 1.解析json数据 (1)json反序列化 对象{}格式