常用卷积神经网络(CNN)
LeNet
LeNet最早由YannLeCun在1989年提出,目的是识别图像中的手写数字。
总体来看,LeNet(LeNet-5)由两个部分组成:
- 卷积编码器:由两个卷积层组成
- 全连接层密集快:由三个全连接层组成
该网络的结构如下:
每个卷积块中的基本单元包含以下结构:
- 一个卷积层
- 一个sigmoid激活函数
- 平均池化层
每个卷积层使用5 * 5卷积核和一个sigmoid激活函数。这些层将输入映射到多个二维特征输出,通常同时增加通道的数量。
每个kernel=2, stride=2的池化操作通过空间下采样将维数减少4倍。
接下来使用Pytorch实现以下LeNet:
1 | import torch |
我们使用模拟数据查看以下各层的输出维度:
1 | Reshape output shape: torch.Size([1, 1, 28, 28]) |
在Fashion-MNIST数据集上的训练(GPU训练)效果如下:
AlexNet
2000 - 2010年机器学习主流的方法是基于核方法的SVM,它对调参不敏感,且具有一套完整的论证方法。
ImageNet
2010年提出,自然物体的彩色图片大小为469 * 387,样本数为1.2M,类别数为1000,手写数字的黑白图片大小为28*28,样本数为60K,类别为10类。
- AlexNet赢得了2012年ImageNet竞赛
- 是一个更深更大的LeNet
- 主要改进:
- 丢弃算法
- ReLu
- MaxPooling
- 将计算机视觉的方法论更改为端到端的学习过程
Alex架构
- 将激活函数从Sigmoid转化为ReLu(延缓梯度消失)
- 隐藏全连接层后加入了丢弃层
- 对图片进行了数据增强
下面我们用PyTorch来实现一下AlexNet
1 | import torch |
VGG
VGG块
经典的卷积神经网络有以下几个部分组成:
- 带填充以保证分辨率的卷积层
- 非线性激活函数,入ReLU
- 池化层,如最大池化层
一个VGG块与之类似,有一系列卷积层组成,最后再加上用于空间下采样的最大汇聚层。
VGG块被定义为:
- n层,m个通道的kernel=3, padding=1的卷积层
- 一个kernel=2, stride=2的MaxPooling层
经过实验发现使用更深的小卷积核会比使用更浅的大卷积核效果更好,因此VGG块仍然使用了3*3的卷积核。
VGG架构
在多个VGG块后接全连接层,不同次数的重复快得到不同的架构VGG-16、VGG-19
- VGG使用可重复使用的卷积块来构建深度卷积神经网络
- 不同的卷积块个数和超参数可以得到不同复杂度的变种
代码实现
1 | import torch |
每个块的输入情况如下:
1 | Sequential output shape: torch.Size([1, 64, 112, 112]) |
小模型训练的结果如下:
1 | loss 0.177, train acc 0.933, test acc 0.911 |
NiN
根据计算,模型的参数大多出现在第一层全连接层中,那么我们希望使用卷积层去替代全连接层以达到降低模型复杂度,加快训练速度的目的。
NiN块
- 一个卷积层后跟两个全连接层:
- stride=1,输出形状与卷积层输出相同
- 起到全连接层的作用
即按照输入像素逐一连接的全连接层
NiN架构
- 无全连接层
- 交替使用NiN块和stride=2的MaxPooling层
- 逐步减小高宽和增大通道数
- 最后使用全局平均池化层得到输出(池化层核的高宽等于输入每一个通道的高宽)
- 输入通道数是类别数
代码实现
1 | import torch |
GoogLeNet
Inception块
- 四个路径从不同层面抽取信息,然后再输出通道维合并
- Inception块不改变高宽,只改变通道数
既然我们要对各个通道的输出再维度上进行合并,那么我们就来看看每个路径输出的通道数是如何变化的:
其中更多的通道数意味着该条路径的权重更大。
使用Inception块的另一个重要原因是,与3*3或5 * 5 的直接卷积相比,Inception块具有更少的参数(同为输入192,输出256通道计算得到):
parameters | FLOPS | |
---|---|---|
Inception | 0.16M | 128M |
3*3 Conv | 0.44M | 346M |
5*5 Conv | 1.22M | 963M |
GoogLeNet架构
5段,9个Inception块
此处Stage的划分是根据是否将高宽减半进行划分的
stage1&2
stage3
stage4&5
Inception各种变种
代码实现
对于GoogLeNet我们先实现Inception块,进而实现每一个Stage,最后将每一个Stage连接成一个网络:
1 | import torch |
各层的参数如下:
1 | Sequential output shape: torch.Size([1, 64, 24, 24]) |
1 | loss 0.256, train acc 0.903, test acc 0.886 |
批量归一化(Batch Normalization)
提出背景
随着神经网络的层级逐渐变深,对于网络的训练,越靠近输出的梯度越大,而越高金输入的梯度则会越小,因此会导致:
- 靠近输出(顶部)的层级训练快
- 靠近输入(底部)的层级训练缓慢
- 每次更新底层是会使得顶部也需要更新,导致顶部重新学习多次
- 导致收敛变慢
思想
固定小批量里的均值和方差。
首先计算出小批量的均值和方差:
然后再做额外的调整(可学习的参数)
其中为表示方差的参数,为调整均值的参数
- 可学习参数和
- 作用:
- 全连接层和卷积层输出上,激活函数前
- 全连接层和卷积层输入上
- 对全连接层作用在特征维上
- 对于卷积层作用在通道维上(事实上通道维即卷积层的特征维)
批量归一化在做什么
- 最初论文想用它减少内部协变量的转移
- 后续有论文指出它可能就是通过在每个小批量里加入噪音来控制模型复杂度
- 即由于每个mini-batch是随机取得,那么我们就可以认为,即mini-batch的均值,和,即mini-batch的方差,是两个随机数,它们分别对原样本进行了随即偏移和随机缩放
- 因此没必要跟丢弃法混合使用
总结
- 批量归一化固定小批量的均值和方差,然后学习出适合的偏移和缩放
- 可以加速收敛速度,但一般不改变模型精度
代码实现
首先实现batch norm的计算操作,然后实现Batch Norm层,接着将其加入LeNet中看效果:
1 | import torch |
在LeNet上的结果:
1 | loss 0.264, train acc 0.902, test acc 0.830 |
查看学习到的均值和方差:
1 | (tensor([0.3362, 4.0349, 0.4496, 3.7056, 3.7774, 2.6762], device='cuda:0', |
ResNet
核心问题
随着层数的加深,网络一定会越来越好吗?
模型偏差:指深度学习中,随着模型复杂的上升,学习得出的最优模型反而离目标模型更远的现象
如图所示的每一个我们认为其是一类特定的神经网络框架,其包括学习率和其他超参数,那么我们可以通过学习从区域中学习到一些函数,那么我们在周围划定一些区域,代表所有的集合,通常我们需要找到一个能够完美拟合我们的目标的函数。而事实往往没有那么幸运,我们也许只能在这些区域中找到一个近似函数。
假设我们有一些具有X特性和y标签的数据,那么我们可以列出以下式子:
如下图所示,如果我们此时需要设计一个比原模型更接近结果的模型,例如比更接近,那么假设其区域如图所示,,则随着模型不断的迭代我们将得到越来越复杂的模型,并在该模型上找到近似最终目标的解。但如果新的模型无法覆盖旧的模型,最终的结果可能离目标函数越来越远。
但如果更复杂的模型是完全包含以前的小模型的话:
那么我们的模型将随着复杂度的增加而向着目标模型接近。
残差块
残差块的思想是在模型的训练过程中扩大函数类,而不是训练新的函数。
残差快通过串联一个层来改变函数类,计入快速通道来得到的结构
而实际使用中需要使用1*1的卷积层来调整输出的通道数:
完整的结构是:
- 高宽减半ResNet块(步幅为2)
- 后接多个高宽不变ResNet块
ResNet架构
类似GoogleNet的总体架构,分为了若干个Stage,但是其中的块替换为了ResNet块,最后添加了全局池化层。
ResNet整体结构
总结
- 残差快使得很深的网络更容易训练
- 残差网络对其后的网络设计产生了深远的影响,无论是卷积类网络还是全连接类网络
RestNet如何处理梯度消失
一个最基本的避免梯度消失的操作是将乘法变成加法。
首先假设我们预测的,此处为了方便讨论我们省略Loss,那么对于某一个层的参数的梯度计算为,那么每次更新的公式为:
根据这个式子,我们不希望梯度过小,导致w几乎不变。
现在我们考虑在f上增加一层:
那么它的导数为:
那么对于这一乘法而言,如果每一项都比较小,即小于1,那么累乘的结果将会越来越小。
那么ResNet是怎么解决的呢?
事实上对于ResNet的下一层来说,它的形式是这样的:
则对它求导数将会得到:
因此就算梯度很小,至少还有这一项不会消失。
代码实现
同样我们首先实现残差块,该块有两种情况:
- 对于宽高减半,通道加倍的块,残差需要改变通道数
- 对于宽高不变的块,则不需要
接着,每一个Stage将包含两个残差快,除了Stage1以外,其余Stage的第一个残差快都需要将宽高减半。
1 | import torch |
首先来看一下两类残差快对输出的变化:
1 | # 输入为4, 3, 6, 6维向量 |
可见第一类残差块的通道数和宽高并未发生变化
使用第二类残差块会将高宽减半,通道数加倍
而每一个Stage的输出结构如下:
可见第一个Stage将224维1通道的输入经过7*7卷积和3 * 3池化降到了56 * 56并将通道数变为64。
而第二个Stage并没有将通道数变化。
之后每一个Stage都会将通道数加倍,宽高减半。
1 | Sequential output shape: torch.Size([1, 64, 56, 56]) |
最后训练结果如下:
1 | loss 0.010, train acc 0.998, test acc 0.915 |
DenseNet
思想
DenseNet是在ResNet上的逻辑展开,对于ResNet而言,他是将函数展开为:
而DenseNet则是借用了泰勒展开的思想:
DenseNet将ResNet中重新加上输入的操作变更为了与输入进行连接:
此处我们使用[,]
来表示这种连接操作,而不是简单相加,因此我们执行从x到其展开式的映射:
最后,将这些展开式结合到MLP中即转化为稠密连接:
DenseNet主要由两部分构成:
- 稠密快Dense Block
- 过渡层Transition Layer
前者定义如何连接输入输出,后者则控制通道数量,使其不会过于复杂。
代码实现
1 | import torch |
经过Dense块后的输出形状如下:
1 | torch.Size([4, 23, 8, 8]) |
经过过度层的输出形状如下
1 | torch.Size([4, 10, 4, 4]) |
训练结果:
1 | loss 0.140, train acc 0.950, test acc 0.882 |