动手学深度学习(5)-卷积神经网络

背景介绍

神经网络的基本模型就是全连接,但是我们同样可以对架构进行改进和升级。首先介绍卷积神经网络(Convolutional Neural Network,CNN),它可以看作是一种特殊的全连接网络。CNN专门被设计并应用在影像方面的,它对Model的限制要比全连接网络要大,因为它的设计是为了适应图像的某些性质。(如果其他问题也有图像的类似性质,同样可以使用CNN)

对于一张图片,在机器内的描述是一个三维的矩阵\([length, weight, channels]\),其中的维度分别为长度,宽度(高度)和通道数。一般图片是RGB类型,即有三个channels,红(R)绿(G)蓝(B),具体到矩阵中的某一个数值,表示的是在相应位置上的像素点,对应通道的强度。

假设我们应用Fully Connected Network来完成图像分类的问题。我们可以将这个三维的矩阵拉长,成为一个一列的长向量\(x = [x_1,x_2,...,x_{length\times weight \times channels}]\),以这个向量作为输入,将其送入Fully Connected Network中,训练得到最终结果。但是这种方法有一个很容易发现的问题,就是网络中的参数实在是太多了。随着参数的增加,我们可以增加模型的弹性,但是同时也增加了overfitting的风险。于是CNN的设计,就是通过结合图片的性质来减少模型的参数。

而CNN的设计实际上结合了一些图像的性质,分别是局部性和平移不变性。

首先,人们观察一张图片的时候,往往利用的并不是图片中所有的信息,而是图像中某些重要部分的信息,将重要部分的信息综合起来,我们就可以完成对图像的分类。由此我们可以观察到图片的一个性质就是,图像中的一些pattern会比整张图片更加重要(局部感知)。利用这个性质,我们就可以减少模型的参数。对于Fully Connected Network来说,每一个神经元都是看了整张图片;而CNN进行设计,让每一个神经元只看图片的某一部分。于是在CNN中,会将图片划分成多个小区域,这个小区域称为感受野(Receptive Field)。每一个Neural只接受自己对应的感受野的输入,而不用去关注整张图片,这样就大大减少了参数的数目。

那么感受野应该如何设计呢?感受野的大小是人为指定的,一般来说也是一个三维的矩阵\([small\ length,small\ weight, channels ]\)​,通道数与输入通道数相同。长宽比原图片小得多,如3个像素。

图像的另一个性质是平移不变性。考虑在两张图片中,可能出现相同的特征,比如说都出现鸟嘴,但是这个特征在图片的不同位置。如果仅按照之前的设计,的确有两个Neural分别检测这两个部分,但是这是两个不同的Neural,最终会训练出两套参数。而都是侦测同样的特征,有必要使用两套不同的参数吗?于是CNN针对这一部分,进行参数共享的设计,进一步减少参数量。对于侦测相同特征的Neural,相互之间进行权值共享(Parameter Sharing)。这样,一个Neural就承担了检测某个特征的任务,而不同的Neural负责的特征也各不相同。一个特征对应一套参数。一个感受野需要接受多个Neural的检测,看其是否有对应的特征。

基础概念

在图像卷积的过程中,我们会给定一个卷积核kernel。此时考虑这个kernel是一个二维的矩阵,其中kernel的大小是一个超参数。卷积的过程可以描述为一个kernel在另一个矩阵上按照步幅进行移动,每次移动可以进行卷积计算,得到对应的值。对于不同的步幅,卷积完成之后矩阵的大小会发生改变。如果不希望矩阵大小发生改变,则需要在周围进行padding填充。

此外,卷积可以处理多通道的输入。我们同样可以设置任意的输出通道数输出可以是任意通道数目。对于每个输入通道,它都有自己独立的卷积核kernel,对于输出的每个通道,它的结果是所有输入通道卷积结果的相加。(每个通道的结果可以看作是对图像中的某种特征的匹配,不同通道则匹配不同的特征。CNN Explainer这个网站较好地可视化了卷积神经网络中每层学习到的特征,可以用作参考。一种常见的设计方式是让输入数据的高宽减半,同时通道数翻倍。)

总结来说,在卷积过程中,有如下的超参数需要设置:

  1. kernel的大小
  2. 步幅stride和填充padding
  3. 输出通道数是卷积层的超参数
  4. 视情况而定可以选择设置对应bias

一种特殊的情况是kernel的大小为\(1\times 1\)。这种特殊情况通常用来融合不同通道的信息。

Pooling池化

Pooling的设计同样来自对图像的观察。假设我们对一张图片进行下采样(subsampling),实际上并不会改变什么,但是我们仍然能够辨别出图像。另一方面,卷积操作对位置非常敏感,我们希望降低这种敏感度,于是引入了Pooling池化层。

对于图像来说,如果是矩阵形式的图像,下采样就是把原始图像中\(s \times s\)窗口内的图像变成一个像素,这个像素点的值就是窗口内所有像素的均值。

Pooling层的操作与卷积层非常类似,都具有填充、步幅等参数,并且输入通道数=输出通道数。可以看到,Pooling本身没有需要学习的参数,参数都是固定的。Pooling也有多种形式,如Max Pooling最大池化,Mean Pooling平均池化等。由于池化往往是为了缓解卷积对位置的敏感性而使用的,因此一般在使用一层卷积之后,会增加一层池化,两者交替使用。

需要注意的是,Pooling实际上还是抛弃了图像的一些原始信息,对训练是有所损害的。Pooling的提出可以帮助我们减少计算量,但是如果设备足以支撑这样的运算量,我们实际上是可以把Pooling抛弃的。

Pytorch实现

卷积层和池化层在Pytorch中都可以非常简单的进行调用:

1
2
3
4
5
6
import torch
from torch import nn

conv = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=3, stride=2, padding=1)
max_pool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
mean_pool = nn.AvgPool2d(kernel_size=3, stride=2, padding=1)

Batch Normalizaton

(Batch Normalization的具体数学表示可以参考机器学习基础(10)-神经网络 - EverNorif

深层神经网络的训练往往是比较困难的,尤其是想要使得网络在较短时间内收敛。Batch Normalization是一种流行且有效的技术,可以加速深层网络的收敛速度。

在深层网络中,梯度是通过反向传播传递到各个层中的。而通常每层的梯度较小,如果网络较深,那么在反向传播的时候会出现类似于梯度消失的现象。这将导致在距离输出的层附近梯度相对较大,收敛更快;而在距离数据更近的层上,因为反向传播中梯度的累乘,所以对应的梯度较小,收敛更慢。但是通常来说,靠近数据的层一般都是用于提取较为基础的特征信息的,上方的层收敛后,由于底部提取基础特征的层仍在变化,上方的层一直在不停的重新训练,导致整个网络难以收敛,训练较慢。另一方面,由于网络深度加深,整个模型复杂度增加,使得网络容易过拟合。

Batch Normalization的操作同样可以抽象为一个层。但是它不是单独考虑单个样本,而是需要对整个mini-batch进行,因此需要考虑多种情况:

  • 对于全连接层,我们将批量规范化层置于全连接层中的仿射变换和激活函数之间
  • 对于卷积层,我们在卷积层之后和非线性激活函数之前应用批量规范化。当卷积有多个输出通道时,我们需要对这些通道的每个输出执行批量规范化,每个通道都有自己的\(\gamma, \beta\)参数
  • 对于预测模式,批量规范化在训练模式和预测模式下的行为通常不同。在预测的时候,我们的模型已经确定下来,可以用在整个训练数据集上得到的均值和方差来对预测时的结果进行归一化。在实际实现时,一般使用指数加权平均来更新小批量的均值和方差,指数加权平均将旧值和当前计算结果不断进行加权平均,最终做到平滑的向更新值靠拢

在批量归一化层中,可学习参数包括\(\gamma, \beta\)。在Pytorch中,也提供对应的API,可以直接使用Batch Normalization,需要提供的超参数为接受的通道数。

1
2
3
4
5
6
7
8
9
10
11
import torch
from torch import nn

net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(256, 120), nn.BatchNorm1d(120), nn.Sigmoid(),
nn.Linear(120, 84), nn.BatchNorm1d(84), nn.Sigmoid(),
nn.Linear(84, 10))

经典卷积网络结构

LeNet

LeNet是最早发布的卷积神经网络之一,最早是应用于手写数字的识别应用。LeNet由两个卷积层,两个池化层和三个全连接层构成:

  • LeNet是早期成功的神经网络,它利用卷积层来学习图片的空间信息,然后使用全连接层来将信息转化到类别空间
  • 为了构造高性能的卷积神经网络,我们通常对卷积层进行排列,逐渐降低其表示的空间分辨率,同时增加通道数

AlexNet

在传统的计算机视觉中,研究人员往往不会直接使用像素数据作为输入,而是会利用精心设计的特征提取算法来提取图片中的特征,然后使用传统机器学习算法进行后续处理。可以说传统的计算机视觉关注的是特征工程。

而2012年,AlexNet的提出证明了学习到的特征可以超越手工设计的特征,打破了计算机视觉研究现状,AlexNet的提出使得研究的方法论发生改变。

  • 传统计算机视觉:图片 -> 人工特征提取 -> 利用传统机器学习算法进行处理,例如利用SVM进行分类
  • AlexNet提出之后:图片 -> 通过CNN学习特征 -> 利用softmax进行回归等

AlexNet的设计理念与LeNet相似,但是仍然存在一些区别:

  1. AlexNet相比于LeNet有更加深层的结构
  2. AlexNet使用ReLU作为激活函数,而不是使用Sigmoid
  3. AlexNet采用dropout来控制全连接层的模型复杂度
  4. AlexNet在训练的时候增加了大量的图像增强数据,例如翻转、裁切和变色等

VGG

虽然AlexNet证明比较有效,但是它对于LeNet的改动可能不那么规则,没有可以参考的地方。而为了将网络做得更深更大,往往需要有更好的设计思想和框架来指导我们。

VGG则是一种使用块的网络,它的网络由多个VGG块构成,而每个VGG块由多个卷积层构成。

VGG的思想在于堆叠可重复的VGG块,然后在最后增加全连接层。不同次数的重复块可以得到不同的架构,例如VGG-16,VGG-19等,后面的数字取决于网络层数。相比于AlexNet,VGG的性能有很大的提升,但是相对地,它的运行速度较慢,对内存的占用和更多。

VGG的提出基本上表明了后续深度学习模型的构建思想:

  1. 使用可重复的块堆叠,来构成更深更大的模型
  2. 使用不同块的个数,不同的超参数来得到模型的不同版本,得到不同复杂度的变种

NiN

LeNet、AlexNet和VGG都有一个共同的设计模型,就是通过一系列的卷积层来获取空间结构特征,最后通过若干全连接层来对特征的表征进行处理。其中,卷积层的参数较少,而全连接层的参数要多得多,这说明模型的参数中,绝大多数都属于全连接层。

而NiN的思想在于在一个卷积层之后跟两个\(1\times 1\)的卷积层,这种卷积层可以融合多个通道的信息,起到全连接层的作用。一个NiN块如下图所示:

最终的NiN网络由多个NiN块构成,并且最终不使用全连接层,而是使用全局的平均池化来得到输出。NiN设计的一个优点是,它显著减少了模型所需参数的数量。然而,在实践中,这种设计有时会增加训练模型的时间。

GoogLeNet

GoogLeNet吸收了NiN中串联网络的思想,并在此基础上做了改进。在实际过程中,我们往往不确定到底选取什么样的层效果更好,到底是3X3卷积层还是5X5的卷积层。而GoogLeNet认为可以同时使用不同大小的卷积核进行组合。

在GoogLeNet中,基本的卷积块被称为Inception块(Inception block),它的基本结构如下:

  • Inception块由4条并行的路径组成,4条路径都使用合适的padding来使得输入和输出的高宽一致,最终将每条路径上的输出在channel维度上进行连接,得到Inception块的输出
  • 在Inception块中,通常进行调整的是不同路径的channel数

GoogLeNet一共使用9个Inception块和全局平均汇聚层的堆叠来生成其估计值。Inception块之间的最大汇聚层可降低维度。

ResNet

在上面的网络设计过程中,我们可以通过新添加层来提升网络的性能,但是很多时候,我们并不能保证新增层一定能够带来性能的提升。

考虑某种网络结构,它能够表征一系列函数,我们称为一个函数集。在模型优化的过程中,我们就是从中找到一个最优的函数,来逼近实际的函数。如果我们新添加层,那么就相当于得到了一个新的函数集。但是我们并不能保证新的函数集一定包含旧的函数集,因此在新函数集中找到的最优函数,不一定会优于之前的函数。

基于此思想,ResNet采用残差连接来构造模型。考虑神经网络的某个局部,假设原始输入为\(x\),而希望学到的理想映射为\(f(x)\)

在上图中,左边是正常的块设计,它的虚线部分希望直接拟合出\(f(x)\)。而右图的残差块中,虚线部分希望拟合残差\(f(x)-x\)。残差块的设计使得如果进行堆叠,我们可以保证新函数集一定是包括旧函数集的。同时在残差块中,输入可以通过跨层的数据线路更加快速地向前传播。这也使得残差块构成的网络能够更加容易做得更深。

在ResNet中沿用了VGG完整的\(3 \times 3\)卷积层设计。残差块里首先有2个有相同输出通道数的\(3\times 3\)卷积层。每个卷积层后接一个批量规范化层和ReLU激活函数。然后我们通过跨层数据通路,跳过这2个卷积运算,将输入直接加在最后的ReLU激活函数前。经过卷积的输出和原输入必须形状相同才能相加,而如果想要改变通道数,则可以在跨层数据通路上增加一个\(1\times 1\)卷积层来将输入变换成需要的形状。

ResNet的前两层跟之前介绍的GoogLeNet中的一样:在输出通道数为64、步幅为2的\(7\times 7\)卷积层后,接步幅为2的\(3\times 3\)的最大汇聚层。不同之处在于ResNet每个卷积层后增加了批量规范化层。GoogLeNet在后面接了4个由Inception块组成的模块。ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。第一个模块的通道数同输入通道数一致。由于之前已经使用了步幅为2的最大汇聚层,所以无须减小高和宽。之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。最后,与GoogLeNet一样,在ResNet中加入全局平均汇聚层(全局平均池化),以及全连接层输出。


动手学深度学习(5)-卷积神经网络
http://example.com/2023/11/19/动手学深度学习-5-卷积神经网络/
作者
EverNorif
发布于
2023年11月19日
许可协议