Pytorch官方英文文档:https://pytorch.org/docs/stable/torch.html?
Pytorch中文文档:https://pytorch-cn.readthedocs.io/zh/latest/
1. 卷积运算与卷积层
说卷积层,我们得先从卷积运算开始, 卷积运算就是卷积核在输入信号(图像)上滑动, 相应位置上进行乘加。 卷积核又称为滤过器,过滤器, 可认为是某种模式,某种特征。
卷积过程类似于用一个模板去图像上寻找与它相似的区域, 与卷积核模式越相似, 激活值越高, 从而实现特征提取。 好吧,估计依然懵逼,下面我们就看看1维/2维/3维的卷积示意图,通过动图的方式看看卷积操作到底在干啥?
1.1 1维/2维/3维卷积示意
一般情况下,卷积核在几个维度上滑动,就是几维卷积。下面再看几张动图感受一下不同维度的卷积操作, 注意下面都是一个卷积核:
1.一维卷积示意
2.二维卷积示意
3.三维卷积示意
1.2 nn.Conv2d
nn.Conv2d
: 对多个二维信号进行二维卷积
主要参数:
- in_channels: 输入通道数
- out_channels: 输出通道数, 等价于卷积核个数
- kernel_size: 卷积核尺寸, 这个代表着卷积核的大小
- stride: 步长, 这个指的卷积核滑动的时候,每一次滑动几个像素。 下面看个动图来理解步长的概念:左边那个的步长是1, 每一次滑动1个像素,而右边的步长是2,会发现每一次滑动2个像素。
- padding: 填充个数, 通常用来保持输入和输出图像的一个尺寸的匹配, 依然是一个动图展示, 看左边那个图,这个是没有padding的卷积,输入图像是4 * 4, 经过卷积之后,输出图像就变成了2 * 2的了, 这样分辨率会遍变低,并且我们会发现这种情况卷积的时候边缘部分的像素参与计算的机会比较少。所以加入考虑padding的填充方式,这个也比较简单,就是在原输入周围加入像素,这样就可以保证输出的图像尺寸分辨率和输入的一样,并且边缘部分的像素也受到同等的关注了。
- dilation: 孔洞卷积大小, 下面依然是一个动图:
孔洞卷积就可以理解成一个带孔的卷积核, 常用于图像分割任务,主要功能就是提高感受野。也就是输出图像的一个参数,能看到前面图像更大的一个区域。
- groups: 分组卷积设置, 分组卷积常用于模型的轻量化。我们之前的AlexNet其实就可以看到分组的身影, 两组卷积分别进行提取,最后合并。
- bias: 偏置
下面是尺寸计算的方式:
下面我们用代码看看卷积核是怎么提取特征的,毕竟有图才有真相:
接下来,我们改变seed, 也就是相当于换一组卷积核, 看看提取到什么样的特征:
再换一个随机种子:
通过上面,我们会发现不同权重的卷积核代表不同的模式,会关注不同的特征,这样我们只要设置多个卷积核同时对图片的特征进行提取,就可以提取不同的特征。
下面我们看一下图像尺寸的变化:
1 | 卷积前尺寸:torch.Size([1, 3, 512, 512]) |
下面再来看一下卷积层有哪些参数: 我们知道卷积层也是继承于nn.Module的,所以肯定又是那8个字典属性, 我们主要看看它的_modules
参数和_parameters
参数字典。
我们可以看到Conv2d下面的_parameters
存放着权重参数,这里的weight的形状是[1, 3, 3, 3], 这个应该怎么理解呢? 首先1代表着卷积核的个数,第1个3表示的输入通道数,后面两个3是二维卷积核的尺寸。 那么这里有人可能会有疑问,我们这里是3维的卷积核啊,怎么实现的二维卷积呢? 下面再通过一个示意图看看:
我们的图像是RGB3个通道的图像,我们创建3个二维的卷积核,这3个二维的卷积核分别对应一个通道进行卷积,比如红色通道上,只有一个卷积核在上面滑动,每一次滑动,对应元素相乘然后相加得到一个数, 这三个卷积核滑动一次就会得到三个数,这三个数之和加上偏置才是我们的一个输出结果。 这里我们看到了,一个卷积核只在2个维度上滑动,所以最后得到的就是2维卷积。 这也能理解开始的卷积维度的概念了(一般情况下,卷积核在几个维度上滑动,就是几维卷积), 为什么最后会得到的3维的张量呢? 这是因为我们不止这一个卷积核啊,我们有多个卷积核的时候,每个卷积核都产生一个二维的结果,那么最后的输出不就成3维的了,第三个维度就是卷积核的个数了。 下面用一个网站上的神图来看一下多个卷积核的提取特征(图片来自RGB彩色图像的卷积过程(gif动图演示)), 下面每一块扫描都是对应元素相乘再相加得到最后的的结果:
上面这一个是一个三维的卷积示意,并且使用了2个卷积核。 最后会得到2个二维的张量。
二维卷积差不多说到这里吧, 不明白我也没招了, 我这已经浑身解数了,看这些动图也能看到吧,哈哈。 毕竟这里主要讲Pytorch, 关于这些深度学习的基础这里不做过多的描述。 下面再介绍一个转置卷积,看看这又是个啥?
1.3 转置卷积
转置卷积又称为反卷积和部分跨越卷积(当然转置卷积这个名字比逆卷积要好,原因在下面),用于对图像进行上采样。在图像分割任务中经常被使用。 首先为什么它叫转置卷积呢?
在解释这个之前,我们得先来看看正常的卷积在代码实现过程中的一个具体操作: 对于正常的卷积,我们需要实现大量的相乘相加操作,而这种乘加的方式恰好是矩阵乘法所擅长的。 所以在代码实现的时候,通常会借助矩阵乘法快速的实现卷积操作, 那么这是怎么做的呢?
我们假设图像尺寸为4 × 4 卷积核为3 × 3 , padding=0, stride=1, 也就是下面这个图:
下面我们看看转置卷积是怎么样的:
转置卷积是一个上采样,输入的图像尺寸是比较小的,经过转置卷积之后,会输出一个更大的图像,看下面示意图:
nn.ConvTranspose2d
: 转置卷积实现上采样
下面从代码中看看转置卷积怎么用:
转置卷积有个通病叫做“棋盘效应”,看上面图,这是由于不均匀重叠导致的。至于如何解决,这里就不多说了。
关于尺寸变化:
1 | 卷积前尺寸:torch.Size([1, 3, 512, 512]) |
我们发现,输入图像是512的, 卷积核大小是3,stride=2, 所以输出尺寸:( 512 − 1 ) × 2 + 3 = 1025
简单梳理,卷积部分主要是卷积运算, 卷积尺寸的计算,然后又学习了转置卷积。下面我们看看nn中其他常用的层。
2. 池化层
池化运算:对信号进行“收集”并“总结”, 类似水池收集水资源, 因而美其名曰池化层。
- 收集: 多变少,图像的尺寸由大变小
- 总结: 最大值/平均值
下面是一个最大池化的动态图看一下(平均池化就是这些元素去平均值作为最终值):
最大池化就是这些元素里面去最大的值作为最终的结果。
下面看看Pytorch提供的最大池化和平均池化的函数:
nn.MaxPool2d: 对二维信号(图像)进行最大值池化。
- kernel_size: 池化核尺寸
- stride: 步长
- padding: 填充个数
- dilation: 池化核间隔大小
- ceil_mode: 尺寸向上取整
- return_indices: 记录池化像素索引
前四个参数和卷积的其实类似, 最后一个参数常在最大值反池化的时候使用, 那什么叫最大值反池化呢?看下图:
反池化就是将尺寸较小的图片通过上采样得到尺寸较大的图片,看右边那个, 那是这些元素放到什么位置呢? 这时候就需要当时最大值池化记录的索引了。用来记录最大值池化时候元素的位置,然后在最大值反池化的时候把元素放回去。
下面看一下最大池化的效果:
可以发现,图像基本上看不出什么差别,但是图像的尺寸减少了一半, 所以池化层是可以帮助我们剔除一些冗余像素的。
除了最大池化,还有一个叫做平均池化:
nn.AvgPool2d: 对二维信号(图像)进行平均值池化
count_include_pad: 填充值用于计算
divisor_override: 除法因子, 这个是求平均的时候那个分母,默认是有几个数相加就除以几,当然也可以自己通过这个参数设定
下面也是通过代码看一下结果:
这个平均池化和最大池化在这上面好像看不出区别来,其实最大池化的亮度会稍微亮一些,毕竟它都是取的最大值,而平均池化是取平均值。
好了,这就是池化操作了,下面再整理一个反池化操作,就是上面提到的nn.MaxUnpool2d: 这个的功能是对二维信号(图像)进行最大池化上采样
这里的参数与池化层是类似的。唯一的不同就是前向传播的时候我们需要传进一个indices, 我们的索引值,要不然不知道把输入的元素放在输出的哪个位置上呀,就像上面的那张图片:
下面通过代码来看一下反池化操作:
3. 线性层
线性层又称为全连接层,其每个神经元与上一层所有神经元相连实现对前一层的线性组合,线性变换
线性层的具体计算过程在这里不再赘述, 直接学习Pytorch的线性模块。
nn.Linear(in_features, out_features, bias=True)
: 对一维信号(向量)进行线性组合
- in_features: 输入节点数
- out_features: 输出节点数
- bias: 是否需要偏置
下面可以看代码实现:
1 | inputs = torch.tensor([[1., 2, 3]]) |
这个就比较简单了,不多说。
4. 激活函数层
激活函数Udine特征进行非线性变换, 赋予多层神经网络具有深度的意义。
如果没有激活函数,我们可以看一下下面的计算:
这里就可以看到,一个三层的全连接层,其实和一个线性层一样。 这是因为我们线性运算的矩阵乘法的结合性,无论多少个线性层的叠加,其实就是矩阵的一个连乘,最后还是一个矩阵。 所以如果没有激活函数,再深的网络也没有啥意义。
下面介绍几个常用的非线性激活函数:
sigmoid函数
nn.tanh
nn.ReLU
ReLU相对于前面的两个,效果要好一些, 因为不容易造成梯度消失,但是依然存在问题,所以下面就是对ReLU进行的改进。
5. 总结
这篇文章的内容到这里就差不多了, 这次以基础的内容为主,简单的梳理一下,首先我们上次知道了构建神经网络的两个步骤:搭建子模块和拼接子模块。 而这次就是学习各个子模块的使用。 从比较重要的卷积层开始, 学习了1维/2维/3维卷积到底在干什么事情,采用了动图的方式进行演示, 卷积运算其实就是通过不同的卷积核去提取不同的特征。 然后学习了Pytorch的二维卷积运算及转置卷积运算,并进行了对比和分析了代码上如何实现卷积操作。
第二块是池化运算和池化层的学习,关于池化,一般和卷积一块使用,目的是收集和合并卷积提取的特征,去除一些冗余, 分为最大池化和平均池化。 然后学习了全连接层,这个比较简单,不用多说,最后是非线性激活函数,比较常用的sigmoid, tanh, relu等。
下面依然是一张导图把知识拎起来:
今天的内容就到这里,模型模块基本上到这里也差不多了,根据我们的那个步骤:数据模块 -> 模型模块 -> 损失函数 -> 优化器 -> 迭代训练。 所以下一次开始学习损失函数模块,但是在学习损失函数之前,还得先看一下常用的权重初始化方法,这个对于模型来说也是非常重要的。所以下一次整理权值初始化和损失函数。 继续Rush 😉