卷积神经网络
记录卷积神经网络相关知识,包括卷积层、池化层、LeNet、AlexNet、VGG、ResNet等经典网络架构
卷积和卷积层
当我们对于一个较大的图片进行计算时,使用之前的全连接层会占用大量的空间,因此我们由图像的性质引申出来两个重要的处理原则:平移不变性和局部性。
那么我们重新分析对于此时需要图像全部空间数据的矩阵(之前是将他们处理成了向量)
因为我们认为图像输入区域的改变并不会导致我们在处理图像时的操作发生变化,那么使用平移不变性,让输入的平移不影响权重$v$ 。效果是将一个4维张量压成了一个2维的张量。
同时,很多时候我们并不会特别关注完整图像的输入而是更多考虑一些局部的输入,此时我们采用局部性,将处理区域限制在一个范围之内。
这其实就是卷积。 总结:卷积就是对全连接层使用平移不变性和局部性。 卷积层的效果就是通过一个核kernel来扫过对应的输入并得到新的输出。
卷积和数学上的交叉相关本质上是一样的,两者都是对称的。 卷积计算的输入输出和过程遵循:
- 输入 $\mathbf{X}: n_h \times n_w$ ;
- 核 $\mathbf{W}: k_h \times k_w$ ;
- 输出 $\mathbf{Y}=(n_h-k_h+1)\times (n_w-k_w+1)$ 。
1
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
上述过程只是一个可行的从全连接层到卷积层的过程展示,体现他们之间的关系。
填充(Padding)和步幅(Stride) 控制了卷积层输出的大小,使用一个 $n\times n$ 的核对输入进行一次卷积之后,会将输入的大小减少 $n-1 \times n-1$ 。
但是如果输入没有那么大,或者核不够小,那么就无法进行较多层的计算。 所以可以人为地在输入的周围填充上一些元素(一般是0)来扩大我们的输入。
因此填充可以改变输出的形状,加入填充的行列得当,也就是将未填充之前减少的行和列补回就可以保持输入和输出是一样大小的,即下面这个公式中 $p_h$ 和 $p_w$ ,$p$ 为填充的高宽大小。
- 填充 $p_h$ 行和 $p_w$ 列,输出形状为\((n_h - k_h + p_h + 1) \times (n_w - k_w + p_w + 1)\)
- 通常取 $p_h = k_h - 1, \quad p_w = k_w - 1$
- 当 $k_h$ 为奇数:在上下两侧填充 $p_h / 2$
- 当 $k_h$ 为偶数:在上侧填充 $\lceil p_h / 2 \rceil$,在下侧填充 $\lfloor p_h / 2 \rfloor$
如果遇到较大的矩阵,并且不使用较大核的情况下(一般的核都是 $3\times 3或5\times 5$),计算就需要非常多步才能完成。那么此时我们可以对步幅进行调整,也就是一次上下/左右移动移动2格或者更多,这样也能减少计算的次数。 
给定高度 $s_h$ 和宽度 $s_w$ 的步幅,输出形状是 \(\lfloor (n_h - k_h + p_h + s_h) / s_h \rfloor \times \lfloor (n_w - k_w + p_w + s_w) / s_w \rfloor\)
- 如果 $p_h = k_h - 1, \quad p_w = k_w - 1$\(\lfloor (n_h + s_h - 1) / s_h \rfloor \times \lfloor (n_w + s_w - 1) / s_w \rfloor\)
- 如果输入高度和宽度可以被步幅整除 \((n_h / s_h) \times (n_w / s_w)\)
填充是通过控制输入来控制输出,步幅是通过控制计算过程中行/列随核滑动的步长大小来进行控制输出,可以成倍的减小输出的大小。
问题:卷积过程是可逆的吗,是不是可以通过顶部较小的层通过核反推回底层输入来还原? 是可逆的。
当我们对多个输入通道进行卷积操作时,每个通道都对应一个卷积核。多个卷积核组成一个三维的卷积核,即 $c_i\times k_h\times k_w$ ,$c_i$ 为通道数。且最终结果都是单输出通道。
输出通道也可以是多个的,此时需要使用到多个上面的三维卷积核。这多个三维卷积核组成一个四维的卷积核。 
多个输入和输出通道可以进行组合。
- 输出通道可以进行多个模式的识别;
- 输入通道核识别并组合输入中的多个模式;
对于 $1\times 1$ 的卷积层,其通道数和输入的通道数总是一样的,真实的形状是 $(C_{in}, 1, 1)$ 。如果有 $C_{out}$ 个这样的卷积核,总权重参数形状就是 $(C_{out}, C_{in}, 1, 1)$,这等价于一个矩阵 $W \in \mathbb{R}^{C_{out} \times C_{in}}$。对于空间上的固定位置 $(h, w)$ ,$1\times 1$ 卷积层的计算过程为:\(y_{h,w} = W \cdot x_{h,w} + b\) 这和一个全连接层 $Y = XW + b$ 是一样的!因此也可以看作是一个全连接层,本质是就是在空间维度($H$ 和 $W$)上的每一个单独的像素位置,抛弃了空间信息而只进行通道的融合,跨通道执行了一个全连接操作。
输入行,为压缩后的空间信息,列为特征通道数。
总结:
- 输出通道是当前卷积层的超参数。
- 每个输入通道有独立的二维卷积核,所有通道结果相加得到一个或多个输出通道结果。
- 每个输出通道有各自独立的卷积核。
- 卷积层可学习的参数就是 $c_o\times c_i\times k_h\times k_w$ 。
单个特征的提取:
- 对应一个输出通道,需要一个三维独立卷积核。
- 由于输入数据有 $c_i$ 个通道,这个三维核必须包含 $c_i$ 个二维卷积核。
- 这 $c_i$ 个二维卷积的结果会被相加,压缩成 1 个二维矩阵(即该输出通道的结果)。
多个特征的提取:
- 想要识别 $c_o$ 个特征,对应产生 $c_o$ 个输出通道。
- $c_o$ 个输出通道对应 $c_o$ 个不同的三维卷积核。
- 最终输出一个形状为 $c_o \times m_h \times m_w$ 的张量。
通常当我们的层越深,越靠近输出,输出的矩阵大小会越小,因此对应卷积核的感受野就会越大。最后我们可以让整个图像的信息都被卷积核看到,这也实现了特征的提取。
因此我们也可以发现,更加底层的层捕捉的都是更通用的信息,比如边缘、轮廓。而更上层则捕捉的是更加全面也更贴近真实情况的全局特征的信息,比如是一个猫还是一个狗?
如下图,从结果来看,更多更小的卷积核和更少更大的卷积核最终的感受野其实是一样的,但是更多的层能带来更多的非线性性。 
池化层(Pooling)(汇聚)
卷积对于位置信息是相当敏感的,池化层可以用于缓解卷积的敏感性。也可以解释为需要一定程度的平移不变性。一般我们通过最大池化层或者平均池化层来进行池化。
通过结果我们可以看到,池化后的卷积输出出现了一定的移位。说明实现了一定程度的平移不变性,这里的平移不变性指的是对于输入输出结果来说,而不是针对卷积核平移时的不变。
平均池化层只是将最大池化层的max操作转换为了求平均的操作。
池化层也有填充、步幅和多个通道。但是没有可学习的参数了,因为进行的操作已经被(max/average)。池化后的输出结果和卷积是一样的。
池化层通常作用在卷积层之后,并且再pyTorch的深度学习框架中,默认池化层的大小也是池化层的步幅,即池化过程中没有重叠。
cs231n卷积解释:
非常直观且便于理解的流程。一个卷积核,对3个通道进行融合,输出一个通道。
多个卷积核,输入通道维持3,但是输出通道变为6.
最终卷积输入、过程和结果可以总结为: 
这里还需要补充一维卷积的理解。
卷积层后也要接激活函数,池化层位于激活函数后,用于进一步提取特征并且使图像信息表示更小、更易处理,对每个激活后独立操作。
池化层没有可学习的参数,因此不再对图像进行变换,而是对图像信息选择性的保留。这里引入了空间不变性。 一个很形象的例子: 
LeNet 卷积神经网络
早期成功的神经网络,先通过卷积层来学习图片空间信息,然后使用全连接层来转换到类别空间。
自己跑了一下代码中的训练,gpu的占用率并不高,可能很多时间花在了cpu任务执行和线程切换而不是gpu计算上。很多参数其实还可以继续调整。 
AlexNet
AlexNet的诞生是因为更多更丰富更优质的图像数据的出现ImageNet(2010)并且AlexNet对传统图像识别的方法进行了更多改进。
传统方法一般向左边一样先进行人工特征提取,再输入给SVM进行识别。通常这些特征是专家进行选择后再让SVM进行学习。 但是AlexNet则是将特征的选择也交给了机器,特征和后面的softmax回归一起进行学习能使模型更可能学习到后面回归需要用到的特征,回归也更能学习到全部输入特征的信息。
AlexNet并没有做出很本质上的改变,架构逻辑还是和LeNet是一样的。具体改变如下:
- 采用了最大池化层

- 增加了更多卷积层,网络更深

- 更大的全连接层(并且在隐藏层后加入了丢弃层来稳定数值)

- 以及
数据增强指的是将图片进行上面这样的放大缩小、亮暗变化、甚至是变色。同类型但多样的输入可以减少神经网络记住太多的细节来提高泛化能力。
VGG:使用块的神经网络
VGG考虑的很简单,能不能通过更深更大来让AlexNet更好。但是全连接层太贵了,所以采用更多的卷积层。 一个简单的想法就是将AlexNet的一部分用一个块来呈现,且这个块可以重复、可以自定义。 然后构建更大更深的深度卷积神经网络。
不同的卷积块个数和超参数可以得到不同复杂度的变种。
在这里使用VGG块使最多就是5个,因为通常输入224的图像在每个VGG块(每次都除以2)后就会变成7而无法再除下去了。
并且我们可以看到我们的处理方式通常是空间信息减半,通道信息翻倍。 VGG块的输出信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
Sequential output shape: torch.Size([1, 64, 112, 112])
Sequential output shape: torch.Size([1, 128, 56, 56])
Sequential output shape: torch.Size([1, 256, 28, 28])
Sequential output shape: torch.Size([1, 512, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
Flatten output shape: torch.Size([1, 25088])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 10])
由代码可以知道:
1
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))
有8个卷积层,3个全连接层,前两个卷积块各一层,后面各两层。 VGG每一块卷积都有比较大量的运算,但是因为使用的是更小的卷积核所以对精度的提高也比较明显。
NiN:Network in Network
卷积层后的全连接层的参数量非常巨大,而卷积层需要较少的参数。并且 $1\times 1$ 的卷积层起到一个全连接层的作用,所以看作全连接层。
多个NiN块相连最终形成了一个NiN网络,并且在最后通过一个全局的平均池化层取代了传统AlexNet和VGG中的全连接层来直接实现分类预测。输入通道数为类别数。
NiN这个比较简单的架构使得他无法去学习原本全连接层中很多的参数,也就没那么容易过拟合。并且还显著减少了权重参数的大小。
GoogLeNet
GoogLeNet将前面所有取得过比较好效果的神经网络全部集中到了一起,形成了一个Inception块。
Inception不改变输出高宽,通过maxpooling降低高宽。
白色部分的框可以理解为用于减少通道数来控制模型复杂度,蓝色部分的框用于提取重要特征。并且可能因为 $3\times 3$ 的卷积核效果比较好,所以分配给他多一点的信息(通道)。最后实现了一个参数更少计算速度更快的网络。这也是Inception块的主要优点:模型参数小,计算复杂度低。
GoogLeNet的总体实现是这样的:
前两个stage
后面主要就是Inception块,最终不断增加通道数,减小窗口大小,最终实现一个1024维的输出,也就是输出1024个特征。
这里其实能很明显的看出深度学习的可解释性极低,因为这样的网络排布基本是Google花大量的机器算力来找到的,很难解释为什么要采用这样你那样的卷积核。
后面还有更新了Inception块的版本Inception-V3,基本也是对Inception块内的卷积核进行大小或者排布上的调整。还有使用其他方法改进的,后续会提到。
批量归一化(Batch Normalization)
训练时数据都是先正向传播后再反向传播来计算梯度,但是一般顶层(较后面的层)训练较快,因为他们离损失函数较近。这些顶层一般也都是高级语义。但是越往底层(前面)就越容易出现下图的问题。
于是考虑了和数值稳定性中一样的方法:
对小批量的标准化就是进行稳定的方法,但是为了避免限制学习,再进行了额外的调整。
批量归一化层是作用在:
- 全连接层和卷积层的输出上,但在激活函数前;
- 或全连接层和卷积层的输入上。
对于全连接层,作用在特征维;对于卷积层,则作用在通道维。
BN其实是可以理解为是在每个小批量中加入噪音,也就是每个小批量对应的 $\mu_B$ 随机偏移,和 $\sigma_B$ 随机缩放。基本固定住小批量的均值和方差之后再让模型学习出合适的偏移和缩放。引入噪声的这个操作被公认为具有正则化效应,因此也不用再使用丢弃层。
总结:BN可以加速收敛,但是一般不改变模型精度。因为并没有改变模型的结构,添加的BN操作只将数据进行了在训练过程中学习到的更加适合全局特征的统计处理,并在预测过程中使用学习到的最完美的这个统计处理进行处理。BN最主要的作用还是加速了训练!
具体的代码实现中,使用了 moving_mean 和 moving_var ,主要是用来结合momentum不断逼近整个数据集真实的统计分布。
ResNet 残差网络
加更多的层并不能总是改进精度,因为更多的层串联(复合函数关系 $y = f_n(…f_1(x))$ )虽然能让机器学习到更多的信息,但是也更可能过拟合并且跑偏,左图。但是假如我们能让越深的层学习到的内容尽可能的在之前的内容当中,也就是呈现嵌套的关系($\mathcal{F}_1 \subseteq \dots \subseteq \mathcal{F}_6$),那么模型只会更好不会变差。
具体来说就是设计一个类似于复合函数的流程。提供一个快捷通道,即使当前层无法提供更好的信息,那么原本的信息,即残差(Residual)也能快速通过。
残差块具体的实现有很多种:
具体来说一个基础ResNet块的实现: 
最终ResNet的实现包含5个stage: 因此,残差块可以使得很深的网络更加容易训练并且提高了深度神经网络的精度,因为避免了不必要的方向偏差。
这里还对前面数值稳定性部分进行了填坑,我们还有一个办法是将梯度下降时的乘法变为加法。
绿色部分是ResNet实现的函数表示,蓝色和紫色部分则是前两层的表示。 重点是在对于靠近data的难训练的权重,我们使用的加法可以避免梯度一直通过链式法则不断相乘导致不稳定,而是可以保证总是有一个加法因子来保障之前优秀的训练权重作为兜底。这样能很好地避免训练方向走偏,以及过拟合。
所以ResNet的核心其实是两个:
- 保证函数的嵌套而非复合关系,确保前进方向正确,只会更好不会更差。
- 残差这个加法因子作为保底,一方面确保训练过程始终保留了之前的优秀信息,$1:1$ 无损地传回前面的层;一方面维持了数值稳定性并且为梯度提供了高速公路来加快训练。



