pytorch入坑笔记1: 从ResNet出发引发的几点思考

终于转到pytorch的学习了,也算是开启了一个新的篇章了。

1. 基本的学习资料

    网上的学习资料很多, 基本的语法这里也就不介绍了,推荐几个个人感觉比较好的文章或者网站。

  1. pytorch官网
  2. pytorch中文网站
  3. 莫烦pytorch教程
  4. pytorch discuss

网上pytorch教程、博客都是很多,有问题就请Google啦~

2. 一个简单的例子: ResNet

ResNet是Kaiming He 在《Deep Residual Learning for Image Recognition》上发表的文章,截止到现在在google学术上已经有14737次引用,不用多说就知道这篇文章的分量了。这里不对原始论文做过多的解读,这里侧重是使用pytorch实现这个网络。

2.0 先导

如果多Resnet还不是太懂的同学,可以参考

  1. Deep Residual Learning for Image Recognition
  2. Deep Learning-TensorFlow (14) CNN卷积神经网络_深度残差网络 ResNet
  3. 深度学习——残差神经网络ResNet在分别在Keras和tensorflow框架下的应用案例

把ResNet主要的思想看一下在看我们的文章可能会好一点。

2.1 网络架构

在这里插入图片描述
    这个就是这篇文章最重要的图了,其实resnet出发点还是比较简单的,就是我们不去学习x->F(x)的直接映射,而是去学习x到残差的映射关系。在某乎看到这样一篇博客,说是:残差网络让我们对微小的扰动敏感性更高,也更容易训练,更适合deeper网络。 至于这里我们选择identity映射,或者为什么采用这种结构,在作者后面文章Identity Mappings in Deep Residual Networks, 经过实践发现这种架构是最好的~

    接下来我们用pytorch实现我们resnet网络。由于这篇文章出来的也是比较早,这里我们就不造轮子了,而是站在巨人的肩膀上,重点解释一下为什么这么写。

    我们参考的resnet博客是这篇Pytorch实战2:ResNet-18实现Cifar-10图像分类(测试集分类准确率95.170%), 这里作者实现的是ResNet-18,下面贴一下网络和作者的代码。
ResNet18

2. 2 ResNet代码

这里我们主要是详解代码为什么这么写,以及自己的几点思考,训练部分代码就不做过多的介绍了,常规操作~

import torch
import torch.nn as nn
import torch.nn.functional as F

class ResidualBlock(nn.Module):
    def __init__(self, inchannel, outchannel, stride=1):
        super(ResidualBlock, self).__init__()
        self.left = nn.Sequential(
            nn.Conv2d(inchannel, outchannel, kernel_size=3, stride=stride, padding=1, bias=False),
            nn.BatchNorm2d(outchannel),
            nn.ReLU(inplace=True),
            nn.Conv2d(outchannel, outchannel, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(outchannel)
        )
        self.shortcut = nn.Sequential()
        if stride != 1 or inchannel != outchannel:
            self.shortcut = nn.Sequential(
                nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(outchannel)
            )

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

class ResNet(nn.Module):
    def __init__(self, ResidualBlock, num_classes=10):
        super(ResNet, self).__init__()
        self.inchannel = 64
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(),
        )
        self.layer1 = self.make_layer(ResidualBlock, 64,  2, stride=1)
        self.layer2 = self.make_layer(ResidualBlock, 128, 2, stride=2)
        self.layer3 = self.make_layer(ResidualBlock, 256, 2, stride=2)
        self.layer4 = self.make_layer(ResidualBlock, 512, 2, stride=2)
        self.fc = nn.Linear(512, num_classes)

    def make_layer(self, block, channels, num_blocks, stride):
        strides = [stride] + [1] * (num_blocks - 1)   #strides=[1,1]
        layers = []
        for stride in strides:
            layers.append(block(self.inchannel, channels, stride))
            self.inchannel = channels
        return nn.Sequential(*layers)

    def forward(self, x):
        out = self.conv1(x)
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = F.avg_pool2d(out, 4)
        out = out.view(out.size(0), -1)
        out = self.fc(out)
        return out


def ResNet18():

    return ResNet(ResidualBlock)

   代码写的还是很漂亮的(漂亮警告~~)

2. 3 几点思考

因为接触pytorch没有太长时间,这里就把我学习过程产生的疑惑以及如何解决的和大家分享一下

2.3.1 其实pytorch的网络无非就是下面的范式:

import torch
import torch.nn as nn

class MyNetwork(nn.Module):
	def __init__(self):
		pass
	def forward(self, x):
		pass

  首先我们定义一个我们自己的类MyNetwork,然后它继承了nn.Module这个类, 我们可以转到nn.Module这个父类,发现里面会有很多方法,其中就有__init__(self)forward(self) 这两个函数,这样就很明白了,这不就是c++的重写嘛(override), 那么我们就重写就好了。Python中self用法详解

   但是如果想要一些自己定义的一些外面参数,那么我们就需要在__init__(self)中添加一些外部参数,然后变成这样def __init__(self, inchannel, outchannel, stride=1),很方便的,但是我们仍然需要继承nn.Module.__init__(self)的相关方法啊,有同学就想这还不简单,我们直接显示调用一下就好了啊,对于这个例子是没错的, 但是实际中并不这么写,而是使用super(ResidualBlock, self).__init__(), 至于为什么这么写,这里还是有一点原因的。

   比如下面这个图,存在多继承的问题。
在这里插入图片描述
如果不使用super而显示调用父类的初始化函数,就会出现多次初始化的问题,具体的细节可以参考这篇文章Python super(钻石继承)。其实super本质上使用一种mromromethod resolution order的缩写,表示了类继承体系中的成员解析顺序。使用super帮助我们解决了两大问题:

  1. 查找顺序问题。从Leaf的mro顺序可以看出,如果Leaf类通过super来访问父类成员,那么Medium1的成员会在Medium2之前被首先访问到。如果Medium1和Medium2都没有找到,最后再到Base中查找。

  2. 钻石继承的多次初始化问题。在mro的list中,Base类只出现了一次。事实上任何类都只会在mro list中出现一次。这就确保了super向上调用的过程中,任何祖先类的方法都只会被执行一次。

至于mro的生成算法,可以参考这篇wiki:C3 linearization

2.3.2 为什么需要__init__和forward?

其实从直观说还是比较简单的。init()相当于是我们的名词,然后forward()是我们的动作,就比如定义一个网络,init()定义我们的网络有哪些层,但是没有定义我们的网络是如何定义的,而forward()定义我们的网络是如何连接起来的。

2.3.3. 为什么nn.ReLU()出现inplace=True?

ReLU函数有个inplace参数,如果设为True,它会把输出直接覆盖到输入中,这样可以节省内存/显存。之所以可以覆盖是因为在计算ReLU的反向传播时,只需根据输出就能够推算出反向传播的梯度。但是只有少数的autograd操作支持inplace操作(如tensor.sigmoid_()),除非你明确地知道自己在做什么,否则一般不要使用inplace操作。

在 pytorch 中, 有两种情况不能使用 inplace operation:

  1. 对于 requires_grad=True 的 叶子张量(leaf tensor) 不能使用 inplace operation
  2. 对于在 求梯度阶段需要用到的张量 不能使用 inplace operation

参考:

  1. pytorch-book/chapter4-神经网络工具箱nn/chapter4
  2. pytorch 学习笔记(二十二):关于 inplace operation

2.3.4 pytorch是如何实现shortcut的?

   在第一次看到这个代码实现shortcut的时候自己真的没有看的太明白,想当然的就认为残差不就是 F ( x ) + x F(x)+x F(x)+x 嘛, 也就是上面这句代码out += self.shortcut(x)表述的这样。但是为什么还会出现下面奇怪的代码:

 self.shortcut = nn.Sequential()
        if stride != 1 or inchannel != outchannel:
            self.shortcut = nn.Sequential(
                nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(outchannel)
            )

后来在这一篇文章深度学习——残差神经网络ResNet在分别在Keras和tensorflow框架下的应用案例我才恍然大悟,发现这句代码的精妙之处。

  对于开始的Resnet-18结构来说,shortcut存在二种不同的类型,一种是经过网络之后输出和输入尺寸是一样的,还有一种输出和输入的维度不匹配,这个时候我们通过Conv + BN的方法将输入的尺寸变成输出尺寸!

  1. 这是输入和输出维度匹配的情况:
    在这里插入图片描述

  2. 这是输入和输出维度不匹配的情况(需要借助conv+bn将输入尺寸降低)
    在这里插入图片描述

2.3.5 nn.Sequential(*layers)为什么需要加一个*

如果*号加在了是实参上,代表的是将输入迭代器拆成一个个元素

参考文章:

  1. Pytorch中nn.ModuleList 和 nn.Sequential的不同
  2. Pytorch 容器
  3. When should I use nn.ModuleList and when should I use nn.Sequential?

2.3.6 为什么在__init__中使用nn.Relu,而在forward中使用F.relu,这两个有什么区别吗?

其实这个问题和pytorch discuss有一个问题很像How to choose between torch.nn.Functional and torch.nn module?

    In PyTorch you define your Models as subclasses of torch.nn.Module.
    In the init function, you are supposed to initialize the layers you want to use. Unlike keras, Pytorch goes more low level and you have to specify the sizes of your network so that everything matches.
    In the forward method, you specify the connections of your layers. This means that you will use the layers you already initialized, in order to re-use the same layer for each forward pass of data you make.
torch.nn.Functional contains some useful functions like activation functions a convolution operations you can use. However, these are not full layers so if you want to specify a layer of any kind you should use torch.nn.Module.
    You would use the torch.nn.Functional conv operations to define a custom layer for example with a convolution operation, but not to define a standard convolution layer.

大概什么意思哩,也就是说__init__定义的是标准层,比如这里nn.Relu是标准层。而在forward里面用户F.relu更像是一种操作,不改变网络的参数权值什么的。

2.3.7 net.train()和net.eval()区别

使用PyTorch进行训练和测试时一定注意要把实例化的model指定train/eval,eval()时,框架会自动把BN和DropOut固定住,不会取平均,而是用训练好的值,不然的话,一旦test的batch_size过小,很容易就会被BN层导致生成图片颜色失真极大。原因就是对于BN层来说,它在训练过程中,是对每一个batch去一个样本均值和方差,然后使用滑动指数平均所有的batch的均值和方差来近似整个样本的均值和方差。对于测试阶段,我们固定我们样本和方差,bn相当于一个线性的映射关系。所以说对于pytorch来说,在训练阶段我们net.train相当于打开滑动指数平均按钮,不断的更新;测试阶段我们关闭它,相当于一个线性映射关系。dropout类似的。

参考文章:

  1. CSDN: 使用PyTorch进行训练和测试时一定注意要把实例化的model指定train/eval
  2. 知乎:为什么batch normalization在训练和测试时使用的均值和方差的计算方式不同?
  3. pytorch discuss: Trying to understand the meaning of model.train() and model.eval()
  4. pytorch discuss: Model.train() and model.eval() vs model and model.eval()

Over~好吧,以上就是目前的感想,以后还有新的见解在继续添加吧~

  • 48
    点赞
  • 129
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值