本文面向刚准备学习人工智能的同学,对卷积神经网络进行一个简单的介绍,帮助大家理解CNN的基本原理。阅读本文需要了解高数和线代的基础知识,最好先阅读过多层感知机(MLP)原理和代码精讲一文,因为CNN实际上是神经网络的一种特殊形式。
为什么需要CNN
我们在MLP中已经学习了全连接层,也叫线性层。在MLP中,每一个神经元都与上一层的所有神经元相连,这意味着如果输入是一张32×32的彩色图片,有三个通道,就有32×32×3=3072个输入特征。假设隐藏层有1000个神经元,那么这一层的参数量就是3072×1000=307万个参数。这还是仅仅第一层,如果再多几层,参数数量会爆炸式增长。
更重要的是,全连接层忽略了数据的空间结构。一张图片相邻的像素之间往往有很强的相关性(比如边缘、纹理),但全连接层把每一个像素都当作独立的存在来处理,这就丢失了宝贵的局部信息。
而CNN就是专门为了图像而生的结构,他的核心思想就是两个:局部连接 和参数共享。
局部连接
所谓的局部连接,就是每个神经元不再与上一层的所有神经元相连,而是只与上一层的局部区域相连。这个局部区域,我们称之为感受野(Receptive Field)。
举个例子,假设我们有一张图片,每个神经元只与图片的一个3×3的小区域相连,那么这个神经元的感受野就是3×3。这样做的好处是,我们可以捕捉到局部的特征,比如边缘、角点、纹理等。
随后,我们把一组3×3的神经元再次相连,这次的感受野感受的内容就更大了,随着层数的增加,感受野会越来越大,最终可以捕捉到整个图片的特征。一般来说,浅层,也就是前几层CNN只能捕捉纹理这类的细节;中层就可以捕捉到局部信息,而深层就可以从整体的角度分析图片的内容。
参数共享
所谓的参数共享,就是在整个图片上使用同一个卷积核(Kernel)进行卷积操作。这意味着我们不需要为图片的每一个位置都学习一套独立的权重,而是让同一个权重在整个图片上"滑动"。
这样做的好处是大大减少了参数量。想象一下,如果我们有10万个参数要学习,现在只需要学习一套3×3的卷积核,就能检测图片中所有的3×3局部特征,参数量从10万减少到了9个。
卷积操作
卷积(Convolution)是CNN的核心操作。所谓的卷积,就是一个卷积核在输入图片上滑动,每次滑动时,将卷积核与图片对应位置的像素进行点乘,然后将所有结果相加,得到一个输出值。
这个说法还是稍微有些抽象了。具体而言,就是卷积核(是一个矩阵)和图片的左上角,一个和卷积核形状相同的矩阵,对应位置相乘相加,得到一个数字,这就是结果(我们称之为特征)矩阵的第一个数字。随后,卷积核向右滑动一次,也就是,现在不是图片的左上角了,是向右移一格之后的矩阵,再和卷积核对应位置相乘相加,以此类推。
我们用公式来表示卷积操作。假设输入为 X X X,卷积核为 W W W,输出为 Y Y Y。对于输出 Y Y Y的每一个位置 ( i , j ) (i,j) (i,j),我们有:
Y i , j = ∑ c = 0 C − 1 ∑ m = 0 F − 1 ∑ n = 0 F − 1 X c , i + m , j + n ⋅ W c , m , n + b Y_{i,j} = \sum_{c=0}^{C-1}\sum_{m=0}^{F-1}\sum_{n=0}^{F-1} X_{c, i+m, j+n} \cdot W_{c,m,n} + b Yi,j=c=0∑C−1m=0∑F−1n=0∑F−1Xc,i+m,j+n⋅Wc,m,n+b
其中, C C C是输入通道数, F F F是卷积核的大小(假设是 F × F F\times F F×F), b b b是偏置。
为什么卷积能行?
在我们讲解卷积的原理之前,我们必须说明为什么卷积能行。我们之前在讲解MLP的时候,说过我们要求出的其实就是一个权重,要求这个权重乘上输入数据,再加上一个偏置之后能够模拟对应的复杂函数关系。
在卷积里面,这个权重就是我们的卷积核。
还有就是,卷积这个名字非常抽象,也不容易理解。你可以把它理解成是一个互相关函数,毕竟互相关函数与卷积之间,差的就是第二个函数翻不翻转的问题。而第二个函数不就是我们的卷积核/权重吗?他不是学习出来的吗?
所以他不需要翻转,只需要在学习的时候直接学习需要的矩阵就可以了。
这样的话,所谓的卷积就可以理解成是互相关。这样,卷积核代表了对不同部分的重视程度,通过点乘来充当权重的作用,核心还是MLP的线性公式。
步长与填充
我们说回卷积本身。在卷积操作中,有两个重要的超参数:步长 (Stride)和填充(Padding)。
步长决定了卷积核每次滑动的距离。如果步长为1,卷积核每次移动1个像素;如果步长为2,每次移动2个像素。步长越大,输出的尺寸越小。
填充是在输入图片周围添加一圈0。填充的作用是控制输出尺寸,防止图片在卷积过程中变小太快。同时,填充也可以让卷积核能够处理图片边缘的像素。因为如果不填充的话,边缘像素永远不可能在与卷积核的中心卷积。
输出的尺寸可以通过以下公式计算:
H o u t = ⌊ H + 2 P − F S ⌋ + 1 H_{out} = \lfloor\frac{H + 2P - F}{S}\rfloor + 1 Hout=⌊SH+2P−F⌋+1
W o u t = ⌊ W + 2 P − F S ⌋ + 1 W_{out} = \lfloor\frac{W + 2P - F}{S}\rfloor + 1 Wout=⌊SW+2P−F⌋+1
其中, H H H和 W W W是输入的高和宽, P P P是填充, F F F是卷积核大小, S S S是步长。
我们来理解一下这个公式。首先,外面这个1是最开始的那个图片左上角区域和卷积核的乘积,所以原来的宽度/高度就要去掉最开始的整个卷积核的大小,随后就要除以步长,毕竟除以步长之后就是能做几次这样的卷积。而填充是四边同时填充的,对于宽/高来说就是一个填充会增加两倍的填充量。
多通道卷积
在实际应用中,输入往往有多通道(比如RGB图片有3个通道)。这个时候,对应的卷积核也有多通道,其通道数必须与输入的通道数相同。每个通道分别进行卷积操作,然后将所有通道的结果相加,再加上偏置,得到最终的输出。
这就解释了为什么卷积核的形状是 ( K , C , F , F ) (K, C, F, F) (K,C,F,F),其中 K K K是卷积核的数量(输出通道数), C C C是输入通道数, F F F是卷积核大小。
im2col:卷积的矩阵化
我们在MLP中说过,矩阵乘法具有并行计算的优越性。卷积操作本质上也是一种乘法,能否也用矩阵乘法来实现呢?答案是肯定的,这个技术就叫做im2col(Image to Column,图像到列)。
im2col的核心思想是:把输入图片的每一个局部区域(也就是卷积核滑动时覆盖的区域)都"拉平"成一列。这样,所有局部区域就构成了一个矩阵,我们可以直接用矩阵乘法来实现卷积操作。
矩阵乘法对于多层for循环有很大的时间优越性。这种优越性可以理解为,在定义、计算数值的时候,由于python不需要声明变量的类型,所以解释器运算起来就没有其它语言那么快,而numpy的矩阵运算是用c语言写的,那么这样的转变就变得有意义了。
具体来说,假设输入是 ( N , C , H , W ) (N, C, H, W) (N,C,H,W),卷积核是 ( K , C , F , F ) (K, C, F, F) (K,C,F,F),输出是 ( N , K , H o u t , W o u t ) (N, K, H_{out}, W_{out}) (N,K,Hout,Wout)。经过im2col变换后:
- 输入 X X X变成 X c o l X_{col} Xcol,形状为 ( C ⋅ F ⋅ F , N ⋅ H o u t ⋅ W o u t ) (C\cdot F\cdot F, N\cdot H_{out}\cdot W_{out}) (C⋅F⋅F,N⋅Hout⋅Wout)
- 卷积核 W W W变成 W m a t W_{mat} Wmat,形状为 ( K , C ⋅ F ⋅ F ) (K, C\cdot F\cdot F) (K,C⋅F⋅F)
然后,我们只需要做一次矩阵乘法:
Y m a t = W m a t ⋅ X c o l Y_{mat} = W_{mat} \cdot X_{col} Ymat=Wmat⋅Xcol
结果 ( K , N ⋅ H o u t ⋅ W o u t ) (K, N\cdot H_{out}\cdot W_{out}) (K,N⋅Hout⋅Wout)再经过reshape和transpose,就得到了最终的 ( N , K , H o u t , W o u t ) (N, K, H_{out}, W_{out}) (N,K,Hout,Wout)。
这个过程可能有点抽象,我们用一个具体的例子来说明。
假设输入是一张单通道的4×4图片:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
卷积核是2×2:
1 0
0 1
步长为1,没有填充。输出应该是3×3。
传统的卷积过程是:卷积核在图片上滑动,每次取一个2×2的区域,与卷积核做点乘。
im2col的过程是:把每一个2×2的区域都拉成一列。9个区域就是9列,每列4个元素(因为2×2=4)。所以 X c o l X_{col} Xcol的形状是 ( 4 , 9 ) (4, 9) (4,9):
区域1 区域2 区域3 ... 区域9
[1 2 3 ... 12 ] <- 第1行
[2 3 4 ... 13 ] <- 第2行
[5 6 7 ... 15 ] <- 第3行
[6 7 8 ... 16 ] <- 第4行
卷积核 W m a t W_{mat} Wmat的形状是 ( 1 , 4 ) (1, 4) (1,4):
[1 0 0 1]
然后做矩阵乘法 ( 1 , 4 ) ⋅ ( 4 , 9 ) = ( 1 , 9 ) (1, 4) \cdot (4, 9) = (1, 9) (1,4)⋅(4,9)=(1,9),结果reshape成 ( 3 , 3 ) (3, 3) (3,3)就是卷积的输出。
这是一次前向传播的过程,事实上卷积核是需要在训练过程中更新的。
池化操作
池化(Pooling)是CNN中另一个重要的操作。池化的作用是降低特征图的尺寸,减少计算量,同时也能提供一定的平移不变性。
最常用的池化是最大池化(Max Pooling)。最大池化将输入图片划分成若干个不重叠的区域(比如2×2),每个区域取最大值作为这个区域的代替,就是用最大的那个数代表这四个数,这样就能减小矩阵的大小。
池化操作没有需要学习的参数(没有权重),所以实现起来相对简单。
归一化
我们上次说过,kaiming初始化的目的,就是保证数据经过一个神经网络层之后,仍然保持原来的分布。事实上卷积核也是可以kaiming初始化的。
但是初始化之后,只要一调整权重的数值,这个效果就不能保证了。那如何保持效果的持续呢?归一化就可以了。
归一化首先计算出这一个小批次的均值和方差,然后归一化到标准正态分布,随后再乘一个系数加一个偏置。这是因为可能对于这个问题来说,最好的分布不是标准正态分布,会稍微偏一点,这两个系数的取值可以通过反向传播更新。
这样就带来了新的噪声,从而增强了鲁棒性。
反向传播
反向传播是CNN中最难理解的部分。在MLP中,我们已经学习了反向传播的基本原理:损失函数对权重的梯度,需要通过链式法则一层一层地传回来。
对于卷积层,反向传播要稍微复杂一些,因为我们需要计算两部分的梯度:
-
损失对权重的梯度 ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L:这个梯度的意义是告诉我们如何调整卷积核的权重,才能让损失函数变小。事实上对于偏置的梯度也可以写在这里,他们的方法都和上一篇讲过的方法没什么差别。
-
损失对输入的梯度 ∂ L ∂ X \frac{\partial L}{\partial X} ∂X∂L:这个梯度的意义是告诉我们输入的每一个像素对损失函数的影响。这个梯度会继续向前传播,传给上一层的网络。这是我们上一篇没有提到的内容,这是因为上一篇我们只有一个隐藏层,之后直接就准备输出了,我们直接拿损失函数作为奖励信号即可。如果是深层网络,就必须求出这个损失对输入的梯度,作为这一层的奖励信号。
对权重的梯度
假设我们已经知道损失对输出的梯度 ∂ L ∂ Y \frac{\partial L}{\partial Y} ∂Y∂L(这是从下一层传回来的),我们要计算损失对卷积核的梯度 ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L。
根据链式法则:
∂ L ∂ W c , m , n = ∑ i , j ∂ L ∂ Y i , j ⋅ ∂ Y i , j ∂ W c , m , n \frac{\partial L}{\partial W_{c,m,n}} = \sum_{i,j} \frac{\partial L}{\partial Y_{i,j}} \cdot \frac{\partial Y_{i,j}}{\partial W_{c,m,n}} ∂Wc,m,n∂L=i,j∑∂Yi,j∂L⋅∂Wc,m,n∂Yi,j
其中, ∂ Y i , j ∂ W c , m , n = X c , i + m , j + n \frac{\partial Y_{i,j}}{\partial W_{c,m,n}} = X_{c, i+m, j+n} ∂Wc,m,n∂Yi,j=Xc,i+m,j+n(这是卷积的前向公式)。
所以, ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L本质上也是一个卷积操作:我们用损失对输出的梯度作为"输入",用原输入 X X X作为"卷积核",做一次卷积,就得到了损失对权重的梯度。
对输入的梯度
损失对输入的梯度 ∂ L ∂ X \frac{\partial L}{\partial X} ∂X∂L的计算要稍微复杂一些。
首先,让我们回顾一下前向传播的公式。假设输入是 X X X,卷积核是 W W W,输出是 Y Y Y:
Y i , j = ∑ c = 0 C − 1 ∑ m = 0 F − 1 ∑ n = 0 F − 1 X c , i + m , j + n ⋅ W c , m , n + b Y_{i,j} = \sum_{c=0}^{C-1}\sum_{m=0}^{F-1}\sum_{n=0}^{F-1} X_{c, i+m, j+n} \cdot W_{c,m,n} + b Yi,j=c=0∑C−1m=0∑F−1n=0∑F−1Xc,i+m,j+n⋅Wc,m,n+b
其中, C C C是输入通道数, F F F是卷积核大小。
现在我们要计算损失对输入 X X X的梯度。根据链式法则:
∂ L ∂ X c , i , j = ∑ c ′ = 0 K − 1 ∑ m , n ∂ L ∂ Y c ′ , m , n ⋅ ∂ Y c ′ , m , n ∂ X c , i , j \frac{\partial L}{\partial X_{c,i,j}} = \sum_{c'=0}^{K-1}\sum_{m,n} \frac{\partial L}{\partial Y_{c',m,n}} \cdot \frac{\partial Y_{c',m,n}}{\partial X_{c,i,j}} ∂Xc,i,j∂L=c′=0∑K−1m,n∑∂Yc′,m,n∂L⋅∂Xc,i,j∂Yc′,m,n
其中, K K K是输出通道数, Y c ′ , m , n Y_{c',m,n} Yc′,m,n是输出特征图的第 c ′ c' c′个通道在位置 ( m , n ) (m,n) (m,n)的值。
现在关键是要求 ∂ Y c ′ , m , n ∂ X c , i , j \frac{\partial Y_{c',m,n}}{\partial X_{c,i,j}} ∂Xc,i,j∂Yc′,m,n。从前向传播公式可以看出,输出 Y c ′ , m , n Y_{c',m,n} Yc′,m,n是输入 X X X的一个局部区域与卷积核做点乘的结果。具体来说, Y c ′ , m , n Y_{c',m,n} Yc′,m,n依赖于输入 X c , m + n 0 , n + n 1 X_{c, m+n_0, n+n_1} Xc,m+n0,n+n1(其中 0 ≤ n 0 , n 1 < F 0 \leq n_0, n_1 < F 0≤n0,n1<F),对应的权重是 W c , n 0 , n 1 W_{c,n_0,n_1} Wc,n0,n1。
换一种更直观的方式理解:输入 X c , i , j X_{c,i,j} Xc,i,j会影响输出 Y Y Y的哪些位置?只有当卷积核滑动到某个位置时,其中心正好落在 X c , i , j X_{c,i,j} Xc,i,j处。也就是说,如果输出位置 ( m , n ) (m,n) (m,n)对应的卷积核中心是 ( i , j ) (i,j) (i,j),那么 X c , i , j X_{c,i,j} Xc,i,j就对 Y c ′ , m , n Y_{c',m,n} Yc′,m,n有贡献。
由此可以推出:
∂ Y c ′ , m , n ∂ X c , i , j = W c ′ , i − m , j − n \frac{\partial Y_{c',m,n}}{\partial X_{c,i,j}} = W_{c', i-m, j-n} ∂Xc,i,j∂Yc′,m,n=Wc′,i−m,j−n
(前提是 i − m i-m i−m和 j − n j-n j−n都在 [ 0 , F ) [0, F) [0,F)范围内,否则导数为0)
把这个结果代入上面的链式法则,就得到:
∂ L ∂ X c , i , j = ∑ c ′ = 0 K − 1 ∑ m , n ∂ L ∂ Y c ′ , m , n ⋅ W c ′ , i − m , j − n \frac{\partial L}{\partial X_{c,i,j}} = \sum_{c'=0}^{K-1}\sum_{m,n} \frac{\partial L}{\partial Y_{c',m,n}} \cdot W_{c', i-m, j-n} ∂Xc,i,j∂L=c′=0∑K−1m,n∑∂Yc′,m,n∂L⋅Wc′,i−m,j−n
仔细观察这个公式: ∂ L ∂ Y \frac{\partial L}{\partial Y} ∂Y∂L是损失对输出的梯度(形状为 K × H o u t × W o u t K \times H_{out} \times W_{out} K×Hout×Wout), W W W是卷积核(形状为 K × C × F × F K \times C \times F \times F K×C×F×F),而我们要得到的是 ∂ L ∂ X \frac{\partial L}{\partial X} ∂X∂L(形状为 C × H × W C \times H \times W C×H×W)。
这个操作本质上就是一个卷积!不过和普通卷积有两点区别:
-
卷积核需要翻转 :我们不是用 W W W直接做卷积,而是用翻转180度 后的 W W W来做卷积。也就是说,如果原来的卷积核是:
w00 w01 w02 w10 w11 w12 w20 w21 w22那么翻转后的卷积核就是:
w22 w21 w20 w12 w11 w10 w02 w01 w00 -
输入输出维度关系相反 :普通卷积是"大图变小图",而这个操作是"小图变大图",所以叫转置卷积 (Transposed Convolution),也常被称为反卷积(Deconvolution)。
所以,总结一下:损失对输入的梯度,就是用损失对输出的梯度作为输入,用翻转180度后的卷积核做一次卷积操作。
这和普通卷积的区别在于:普通卷积是"由大变小",而转置卷积是"由小变大",常用于上采样(比如在GAN网络中生成图片、在语义分割中恢复分辨率)。
最大池化的反向传播
最大池化的反向传播相对简单。因为前向传播时,我们保留了每个区域最大值的位置(argmax),反向传播时,我们只需要把梯度传到对应的位置,其他位置设0即可。
损失函数
在CNN里比较常用的损失函数包括softmax函数和交叉熵函数。
softmax函数可以讲一个逻辑值转化为概率。 softmax ( x ) = e z i ∑ j = 1 n e z j \text{softmax}(x)=\frac{e^{z_i}}{\sum^{n}_{j=1}e^{z_j}} softmax(x)=∑j=1nezjezi
假设我们有一组结果[2.0, 1.0, 0.1],首先将他们作为 e x e^x ex的自变量得到一组数据[7.39, 2.72, 1.10],再把这组数据归一化就好了。
和softmax函数经常一起搭配的是交叉熵函数。 H = − ∑ i P ( x i ) log ( Q ( x i ) ) H = -\sum_iP(x_i)\log(Q(x_i)) H=−i∑P(xi)log(Q(xi))
这就是反向传播所需要了解的知识。