CNN原理精讲

本文面向刚准备学习人工智能的同学,对卷积神经网络进行一个简单的介绍,帮助大家理解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中,我们已经学习了反向传播的基本原理:损失函数对权重的梯度,需要通过链式法则一层一层地传回来。

对于卷积层,反向传播要稍微复杂一些,因为我们需要计算两部分的梯度:

  1. 损失对权重的梯度 ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L:这个梯度的意义是告诉我们如何调整卷积核的权重,才能让损失函数变小。事实上对于偏置的梯度也可以写在这里,他们的方法都和上一篇讲过的方法没什么差别。

  2. 损失对输入的梯度 ∂ 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)。

这个操作本质上就是一个卷积!不过和普通卷积有两点区别:

  1. 卷积核需要翻转 :我们不是用 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
  2. 输入输出维度关系相反 :普通卷积是"大图变小图",而这个操作是"小图变大图",所以叫转置卷积 (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))

这就是反向传播所需要了解的知识。

相关推荐
摆烂工程师1 小时前
2026年新国内如何注册 Claude 账号保姆教程(成功率95%)
人工智能·ai编程·claude
Ivanqhz1 小时前
活跃范围重写(Live Range Rewriting)
开发语言·c++·后端·算法·rust
薛不痒1 小时前
大模型(2):大模型推理文本分类
人工智能·python·深度学习·机器学习
xiaoye-duck1 小时前
《算法题讲解指南:优选算法-链表》--51.两数相加,52.两两交换链表中的节点
数据结构·算法·链表
it_czz1 小时前
AI Agent 本质秘密
人工智能·ai
Cosolar2 小时前
阿里CoPaw进阶使用手册:从新手到高手的完整指南
人工智能·后端·算法
几分醉意.2 小时前
先发制人:用 Bright Data 抢先捕捉 TikTok 爆款内容(附实战案例)
java·大数据·人工智能
小浣熊喜欢揍臭臭2 小时前
【OpenSkills使用二】自定义 Skill 的实现
人工智能·ai编程
AI生成未来2 小时前
图像生成迎来“思考-研究-创造”新范式!Mind-Brush:统一意图分析、多模态搜索和知识推理
人工智能·计算机视觉·aigc·agent·图像生成