6.1 从全连接层到卷积层

6.1.1 图像的两个特性

图像是一种具有特殊结构的高维数据,它不同于普通的数值向量,而是在空间上具有平移不变性(translation invariance)与局部相关性(locality)两大特征。传统的全连接神经网络(Fully Connected Network, FCN)无法有效利用这两种结构特性,参数量庞大且容易过拟合。卷积神经网络(Convolutional Neural Network, CNN)正是基于这两种图像特性而提出的,使模型能在空间上高效提取局部模式,并在平移后保持特征稳定。

① 平移不变性 平移不变性意味着:当图像中的物体发生小范围平移时,其语义信息和类别应保持不变。例如,把猫的图片向右移动几个像素,模型仍应识别为“猫”。

② 局部相关性 局部性指图像中相邻像素之间存在强相关,而远距离像素相关较弱。例如,一个边缘或纹理通常由相邻像素共同决定。全连接层在计算每个输出神经元时,都与所有输入像素相连,无法体现局部特征的集中性。

一个有效的神经网络应该保证这两个特性:

平移不变性:不管检测对象出现在图中的哪个位置,神经网络的前面几层都应该对相同的图像区域具有相似的反应。

局部相关性:神经网络的前面几层应该只探索输入图像中的局部位置,而不过度在意图像中相隔比较远的区域的关系。最终,在后续的网络中,可以在整个图像的级别上集成这些局部特征用于预测。

那么神经网络如何具有这两个特性呢?假设输入的图像为二维矩阵 \(X\),隐含层也是一个矩阵,记作 \(H\)。假设 \(X\)\(H\) 具有相同的形状,且都具有空间结构。使用 \([X]_{i,j}\)\([H]_{i,j}\) 分别表示输入图像和隐含层的 \((i, j)\) 处的像素。为了使每个隐含神经元都能接受到每个输入的像素的信息,需要把输入到隐含层的权重矩阵设置为 4 阶矩阵 \(W\)。为了方便,暂不考虑偏置系数。如果输入层与隐含层采用全连接的形式,其表达公式为: \[ [H]_{i,j} = \sum_k\sum_l [W]_{i, j, k, l}[X]_{k, l} \] 这里 \([W]_{i,j,k,l}\) 是 4 阶张量,表示“从输入位置 \((k,l)\) 连到隐藏位置 \((i,j)\)”的权重;参数量为 \(H\!\times\!W\!\times\!H\!\times\!W\) ,既不体现局部性,也不共享权重。这个公式说的就是:每个输出像素都要看整张输入图像,每个位置都有一套不同的权重。所以参数非常的庞大。

把与位置 \((i,j)\) 相连的输入重写成相对位移的邻域求和(平移)。对每个固定的 \((i,j)\),令 \[ a=k-i,\qquad b=l-j\quad(\text{即 }k=i+a,\ l=j+b), \] 把和式中的 \((k,l)\) 改写为 \((a,b)\),得到: \[ [H]_{i, j} = \sum_a\sum_b[V]_{i, j, a, b}[X]_{i+a, j+b}, [V]_{i,j,a,b}=[W]_{i,j,i+a,j+b} \] 此时仅仅是做了换元的操作,并没有减少参数。但我们已经把连接写成“围绕 \((i,j)\) 的邻域累加”的形状,为局部化做准备。

下面要求“同一相对位移 \((a,b)\) 在所有空间位置 \((i,j)\) 使用同一权重”,即 \[ [V]_{i,j,a,b}\equiv [V]_{a,b}\quad\text{对所有 }(i,j). \] 于是 \[ [H]_{i,j}=\sum_{a}\sum_{b}[V]_{a,b}\,[X]_{\,i+a,\,j+b} \] 这一步是关键:当输入整体平移 \((\delta_x,\delta_y)\) 时,右端所有取样点与系数的相对关系不变,因此 \[ H[T_{\delta}X]=T_{\delta}H[X], \] 即卷积(或互相关)对平移等变;若随后做池化/全局平均或最大操作,就能把“等变”进一步转成“(近似)不变”。

最后(施加局部性约束,限制感受野):只允许有限邻域参与(大小 \((2\Delta+1)\times(2\Delta+1)\)),令 \(a,b\in[-\Delta,\Delta]\),得到 \[ [H]_{i,j}=\sum_{a=-\Delta}^{\Delta}\sum_{b=-\Delta}^{\Delta}[V]_{a,b}\,[X]_{\,i+a,\,j+b}. \] 这正是二维“互相关”的写法(若把核翻转即可得到严格的卷积定义)。参数量从 \(H\!\times\!W\!\times\!H\!\times\!W\) 大幅降到 \((2\Delta+1)^2\),既体现局部连接(有限感受野),又通过权重共享带来平移等变

此时这个新的神经网络即卷积神经网络。

ps:理论太难了看不懂也没关系。

6.1.2 卷积神经网络概述

卷积神经网络(Convolution Neural Network, CNN)的核心思想是利用卷积核(Convolution Kernel)在空间上提取特征,并通过堆叠多层结构实现从低层边缘纹理到高层语义特征的逐层抽象。其基本组成包括:卷积层、激活函数、池化层和全连接层。

① 卷积层(Convolution Layer) 卷积层使用若干可学习的卷积核在输入特征图上滑动计算局部加权和。每个卷积核提取一种模式(如边缘、角点、颜色变化),多个卷积核组合形成多通道特征图。数学表达式为: \[ H_k = \sigma(X * W_k + b_k) \] 其中“*”表示卷积操作,\(k\) 表示第 \(k\) 个卷积核。卷积层通过权重共享显著减少参数量:若输入为 \(32\times32\times3\),卷积核为 \(5\times5\),则参数仅 \(5\times5\times3=75\),远少于全连接层的数千个连接。

② 激活层(Activation Layer) 卷积操作后通常接非线性激活函数(如 ReLU),以引入非线性表达能力: \[ a_{ij} = \max(0, h_{ij}) \] ③ 池化层(Pooling Layer) 池化层用于降低特征图分辨率、减小计算量、增强平移与缩放不变性。常见方法有最大池化(Max Pooling)与平均池化(Average Pooling): \[ p_{ij} = \max_{(m,n)\in R_{ij}} a_{mn} \] 其中 \(R_{ij}\) 表示局部区域窗口。

④ 全连接层(Fully Connected Layer)与输出层 在卷积与池化层提取高层特征后,网络末端通常接全连接层以进行分类或回归预测,最终通过 Softmax 输出类别概率。

卷积神经网络通过局部连接、权重共享和层次特征抽取,完美利用了图像的平移不变性与局部相关性。浅层卷积核捕获边缘、角点等低级模式;中层卷积提取纹理和形状;深层卷积聚合为高层语义特征。整个过程可视为从像素空间到语义空间的逐级映射。

下面是一个简单的 CNN 定义:

import torch
import torch.nn as nn
import torch.nn.functional as F


class CNNNet(nn.Module):
def __init(self):
super(CNNNet, self).__init__()
self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=5, padding=1) # 输入通道数1,输出通道数16,卷积核大小5x5
self.conv2 = nn.Conv2d(in_channels=16, out_channels=36, kernel_size=3, padding=1) # 输入通道数16,输出通道数36,卷积核大小3x3
self.pool = nn.MaxPool2d(kernel_size=2, stride=2) # 最大池化层,池化窗口大小2x2,步幅2
self.fc1 = nn.Linear(36 * 6 * 6, 128) # 全连接层,输入特征数36*6*6,输出特征数128
self.fc2 = nn.Linear(128, 10) # 全连接层,输入特征数128,输出特征数10(类别数)

def forward(self, x):
x = self.pool(F.relu(self.conv1(x))) # 卷积层1 + ReLU激活 + 池化层
x = self.pool(F.relu(self.conv2(x))) # 卷积层2 + ReLU激活 + 池化层
x = x.view(-1, 36 * 6 * 6) # 展平操作,将多维张量展平成二维张量
x = F.relu(self.fc1(x)) # 全连接层1 + ReLU激活
x = F.relu(self.fc2(x)) # 全连接层2 + ReLU激活
return x

6.2 卷积层

卷积层是卷积神经网络的核心,而卷积是卷积层的核心。卷积就是两个函数的一种运算。举一个例子讲述一下卷积运算:

假设输入图像 \(X\) 是一个 \(3\times3\) 的灰度块: \[ X = \begin{bmatrix} 1 & 2 & 1 \\ 0 & 1 & 0 \\ 2 & 1 & 0 \end{bmatrix} \] 卷积核 \(V\)(filter)是一个 \(2\times2\) 的小窗口: \[ V = \begin{bmatrix} 1 & 0 \\ 0 & -1 \end{bmatrix} \] 这是一个简单的“边缘检测”滤波器:它在左上角取正、右下角取负,用来检测强度差。


第一步:取输入左上角的区域进行相乘相加

区域对应: \[ \begin{bmatrix} 1 & 2 \\ 0 & 1 \end{bmatrix} \] 相乘(注意不是点积)求和: \[ 1\times1 + 2\times0 + 0\times0 + 1\times(-1) = 1 - 1 = 0 \] 所以输出左上角的 \(H(1,1)=0\)


第二步:卷积核向右滑动一格

\(X\) 区域: \[ \begin{bmatrix} 2 & 1 \\ 1 & 0 \end{bmatrix} \] 相乘求和: \[ 2\times1 + 1\times0 + 1\times0 + 0\times(-1) = 2 \] 得到 \(H(1,2)=2\)


第三步:向下滑动

\(X\) 区域: \[ \begin{bmatrix} 0 & 1 \\ 2 & 1 \end{bmatrix} \] 相乘求和: \[ 0\times1 + 1\times0 + 2\times0 + 1\times(-1) = -1 \] 得到 \(H(2,1) = -1\)

这样计算下去,就能得到一个输出矩阵 \(H\)\[ H = \begin{bmatrix} 0 & 2 \\ -1 & 1 \end{bmatrix} \] 这就是卷积运算的结果。

这个卷积运算的过程可以用 PyTorch 实现:

# 定义卷积运算函数
def cust_conv2d(X, K):
"""
手动实现二维卷积运算
参数:
X: 输入二维矩阵,形状为(H, W)
K: 卷积核,形状为(h, w)
"""
h, w = K.shape # 获取卷积核形状
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) # 初始化输出矩阵 Y
# 计算卷积
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j + w] * K).sum() # 计算卷积结果
return Y


X = torch.tensor([[1, 2, 1], [0, 1, 0], [2, 1, 0]])
K = torch.tensor([[1, 0], [0, -1]])
Y = cust_conv2d(X, K)
print(Y) # tensor([[ 0., 2.], [-1., 1.]])

6.2.1 卷积核

  1. 卷积核的作用

卷积核是整个卷积过程的核心。比较简单的卷积核(过滤器)有垂直卷积核、水平卷积核、索贝尔卷积核等。这些卷积能够检测图像的水平边缘、垂直边缘,增强图像中心区域权重等。下面是一些例子:

(1)比如针对垂直边缘检测:

卷积核(注意卷积核一般是奇数): \[ K_v = \begin{bmatrix} 1 & 0 & -1\\ 1 & 0 & -1\\ 1 & 0 & -1 \end{bmatrix} \] 假设输入图像的一部分是这样的(数值代表亮度): \[ X = \begin{bmatrix} 10 & 10 & 10 & 0 & 0 & 0\\ 10 & 10 & 10 & 0 & 0 & 0\\ 10 & 10 & 10 & 0 & 0 & 0\\ 10 & 10 & 10 & 0 & 0 & 0\\ 10 & 10 & 10 & 0 & 0 & 0\\ 10 & 10 & 10 & 0 & 0 & 0\\ \end{bmatrix} \] (左边是亮区,右边是暗区 —— 就是一条垂直的亮暗边界)

我们让卷积核在这张图上“滑动”,取中间 3 列的一个区域(对应边界处): \[ \text{取 } X_{局部} = \begin{bmatrix} 10 & 10 & 0\\ 10 & 10 & 0\\ 10 & 10 & 0 \end{bmatrix} \] 计算卷积: \[ S = 1×(10+10+10) + 0×(10+10+10) + (-1)×(0+0+0) = 30 \] 得到一个很大的正值 → 表示左边亮右边暗的“强垂直边缘”。

计算的最终结果是: \[ \begin{bmatrix} 0 & 30 & 30 & 0 \\ 0 & 30 & 30 & 0 \\ 0 & 30 & 30 & 0 \\ 0 & 30 & 30 & 0 \\ \end{bmatrix} \] 其中数值的含义是:

  • 输出的绝对值表示“边缘强度”;

  • 输出的正负号表示“亮到暗”的方向。

(2)水平边缘检测

和垂直的类似,我们只需要指定: \[ K_h = \begin{bmatrix} 1 & 1 & 1\\ 0 & 0 & 0\\ -1 & -1 & -1\\ \end{bmatrix} \] 就可以检测出水平方向亮度的变化: \[ \begin{bmatrix} 10 & 10 & 10 & 0 & 0 & 0\\ 10 & 10 & 10 & 0 & 0 & 0\\ 10 & 10 & 10 & 0 & 0 & 0\\ 0 & 0 & 0 & 10 & 10 & 10\\ 0 & 0 & 0 & 10 & 10 & 10\\ 0 & 0 & 0 & 10 & 10 & 10\\ \end{bmatrix} * \begin{bmatrix} 1 & 1 & 1\\ 0 & 0 & 0\\ -1 & -1 & -1\\ \end{bmatrix} = \begin{bmatrix} 0 & 0 & 0 & 0 \\ 30 & 10 & -10 & -30 \\ 30 & 10 & -10 & -30 \\ 0 & 0 & 0 & 0 \\ \end{bmatrix} \] 其中 30 表示边缘的强度更大(也就是说梯度更大,这样就可以求导对卷积核参数进行优化),可以看到10 和 -10 相反,代表亮到暗的变化方向相反。

  1. 如何确定卷积核

如何确定卷积核呢?卷积核类似于标准神经网络中的权重矩阵 \(W\),卷积核也可以通过模型训练得到。CNN 的主要目标就是算出这些卷积核的数值。确定了这些卷积核后,CNN 的浅层网络也就实现了对图像所有边缘特征的检测。

比如我们通过 (1)针对垂直边缘检测的例子,已知 \(X\)\(Y\) 来计算卷积核 \(K_v\),计算代码如下:

# 根据 X 和 Y 计算卷积核 K
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 定义输入和输出
X = torch.tensor([[10, 10, 10, 0, 0, 0], [10, 10, 10, 0, 0, 0], [10, 10, 10, 0, 0, 0],
[10, 10, 10, 0, 0, 0], [10, 10, 10, 0, 0, 0], [10, 10, 10, 0, 0, 0]], dtype=torch.float32)
Y = torch.tensor([[0., 30., 30., 0.], [0., 30., 30., 0.], [0., 30., 30., 0.], [0., 30., 30., 0.]])
# 训练卷积核
conv2d = nn.Conv2d(1, 1, kernel_size=3, bias=False).to(device) # 定义卷积层,不使用偏置,卷积核大小为3x3,输入输出通道数均为1
# 使用 4 维输入输出格式 (批量大小,通道数,高度,宽度)
X = X.view((1, 1, 6, 6)).to(device)
Y = Y.view((1, 1, 4, 4)).to(device)
lr = 1e-3
loss_func = nn.MSELoss() # 均方误差损失函数
optimizer = torch.optim.SGD(conv2d.parameters(), lr=lr) # 随机梯度下降优化器
for epoch in range(500):
Y_pred = conv2d(X) # 前向传播
loss = loss_func(Y_pred, Y) # 计算损失
optimizer.zero_grad() # 清零梯度
loss.backward() # 反向传播
optimizer.step() # 更新参数
print("Learned kernel:\n", conv2d.weight.data.view(3, 3)) # 输出学习到的卷积核

结果:

Learned kernel:
tensor([[ 1.1635, 0.1391, -1.0324],
[ 0.9839, 0.0822, -1.0136],
[ 0.8526, -0.2212, -0.9540]], device='cuda:0')

和我们原本示例的垂直边缘检测卷积核还是很接近的。

6.2.2 步幅

在 6.2 举的卷积运算的例子中,小窗口每次就是向右(向下)移动了一小格的位置,然后继续进行卷积计算。这 “一小格” 其实就是步幅(strides),在图像中就是跳过的像素的个数。6.2 的例子中的 strides = 1。

在小窗口(卷积核)移动的过程中,卷积核的值始终保持不变。也就是说卷积核的值在整个过程中是共享的,所以又把卷积核的值称为共享变量。这种参数共享的方法大大降低了参数的数量。

strides 参数是卷积神经网络中的一个重要的参数,strides 参数格式为单个整数或者两个整数的元组(分别表示height和width维度上的值)。如果 strides 值比 1 大,那么在小窗口在移动过程中有可能移动到输入矩阵之外,这样就需要 6.2.3 中的填充(padding)。

6.2.3 填充

在卷积神经网络中,填充(Padding)是卷积运算中一个非常重要的环节,它决定了输出特征图的空间大小以及边缘信息是否会丢失。卷积本质上是让卷积核在输入图像上滑动,每次取一个局部区域相乘求和。当卷积核移动到图像边缘时,由于超出边界的区域没有像素值,因此会造成输出尺寸减小、边缘信息丢失。为了解决这个问题,就引入了“填充”操作。 (1)卷积后尺寸为什么会变小 假设输入图像大小为 \(N \times N\),卷积核大小为 \(K \times K\),步长(stride)为 1,且不使用填充。卷积核每次滑动 1 个像素,只有当卷积核完全覆盖输入区域时才计算输出。此时输出的大小为: \[ N_{out} = N - K + 1 \] 例如,若输入为 \(5\times5\),卷积核为 \(3\times3\),则输出为 \(3\times3\)。可见输出会越来越小,若叠加多层卷积,很快就缩成一个小点。同时,图像边缘像素只参与一次卷积计算,中心像素却参与多次,导致边缘特征学习不足。 (2)Padding 的基本思想 Padding 的做法是在输入图像的四周“补上若干圈像素”(通常补 0),从而让卷积核在边缘也能完整滑动。设填充厚度为 \(P\),即每边补 \(P\) 行或列,那么输出大小变为: \[ N_{out} = N - K + 2P + 1 \] 通过调整 \(P\),我们可以控制输出尺寸。例如:

  • \(P=0\):无填充(valid convolution),输出变小;
  • \(P=\frac{K-1}{2}\):输出尺寸与输入相同(same convolution);
  • \(P>K/2\):输出甚至会变大(通常不常用)。

\(5\times5\) 输入、\(3\times3\) 卷积为例:

  • 无填充 → 输出 \(3\times3\)
  • 填充 1 → 输出 \(5\times5\)
  • 填充 2 → 输出 \(7\times7\)

设步幅大小为 \(S\),那么卷积之后的大小为: \[ \frac{N+2P-K+1}{S} + 1 \]

(3)Padding 的作用与意义保持空间尺寸稳定 在深层网络中,若每层卷积都让特征图变小,信息会快速丢失。通过填充,可以在每层输出保持相同大小,方便深层堆叠,如 VGG、ResNet 等结构常用 \(P=1\) 对应 \(3\times3\) 卷积。

保留边缘信息 填充让卷积核在边缘处也能完整覆盖像素,从而学习到边界特征。若不填充,卷积核只能在图像中间区域滑动,模型更容易忽视边缘轮廓。

控制感受野增长速度 感受野(receptive field)指一个输出像素对应的输入区域范围。填充能让感受野按可控方式扩展,而不会过快缩小空间分辨率。 (4)常见填充方式 在实践中,填充不一定只能补零,还可根据任务采用不同策略:

  • Zero Padding(零填充):最常用方式,边缘补 0,不引入新信息,计算简单;
  • Reflect Padding(反射填充):边缘复制相邻像素的值,能减少突兀边缘效果,常用于图像生成;
  • Replicate Padding(重复填充):用边界像素值反复复制;
  • Circular Padding(循环填充):让图像边缘首尾相连,常用于周期性数据(如信号处理)。

(5)示例:5×5 输入,3×3 卷积核,P=1

输入: \[ X = \begin{bmatrix} 1 & 2 & 3 & 4 & 5\\ 6 & 7 & 8 & 9 & 10\\ 11 & 12 & 13 & 14 & 15\\ 16 & 17 & 18 & 19 & 20\\ 21 & 22 & 23 & 24 & 25 \end{bmatrix} \] 填充后(四周补 0): \[ X_{pad} = \begin{bmatrix} 0 & 0 & 0 & 0 & 0 & 0 & 0\\ 0 & 1 & 2 & 3 & 4 & 5 & 0\\ 0 & 6 & 7 & 8 & 9 & 10 & 0\\ 0 & 11 & 12 & 13 & 14 & 15 & 0\\ 0 & 16 & 17 & 18 & 19 & 20 & 0\\ 0 & 21 & 22 & 23 & 24 & 25 & 0\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 \end{bmatrix} \] 此时卷积核可以完整地滑过整个图像边缘,输出仍为 \(5\times5\)

(6)总结 Padding 的引入,使卷积网络在保持空间分辨率的同时,能够充分提取边缘特征,避免信息损失。它的三个核心作用是:

  • 防止特征图尺寸逐层缩小;
  • 保留图像边缘信息;
  • 增强卷积特征提取的全面性。

因此,在现代卷积神经网络中,Padding 几乎是卷积操作的“标准配置”,尤其在深层模型中几乎总是采用“same”模式(例如 \(3\times3\) 卷积配 \(P=1\)),保证特征图在多层传播时保持稳定的空间结构。

6.2.4 多通道上的卷积

在真实的卷积神经网络中,图像往往并不是单通道的灰度输入,而是由多个通道(如 RGB 三个颜色分量)组成的三维张量。卷积层的计算也随之从二维扩展到三维甚至更高维,从而能同时融合多个特征通道的信息。多通道卷积主要包含三种核心概念:多输入通道、多输出通道以及 \(1\times1\) 卷积核(1)多输入通道(multi-input channels) 当输入有多个通道时,卷积核不仅在空间上滑动,还会在“通道维度”上进行加权求和。 设输入特征图为 \(X \in \mathbb{R}^{C_{in} \times H \times W}\),其中 \(C_{in}\) 表示输入通道数(如 RGB 图像中 \(C_{in}=3\)),每个通道记作 \(X^{(c)}\)。 若卷积核大小为 \(K\times K\),并且每个卷积核对每个输入通道都有一组独立权重 \(V^{(c)}\),那么卷积计算式为: \[ [H]_{i,j} = \sum_{c=1}^{C_{in}}\sum_{a=-\Delta}^{\Delta}\sum_{b=-\Delta}^{\Delta} [V^{(c)}]_{a,b}\,[X^{(c)}]_{i+a,j+b} \] 这里的关键是通道求和:卷积核会对每个输入通道单独进行局部卷积,然后将这些结果加起来形成一个综合的响应。这样,网络能够在不同颜色或特征层之间进行信息融合。

例如,对于一张三通道 RGB 图像:

  • 通道 1:R(红色分量);
  • 通道 2:G(绿色分量);
  • 通道 3:B(蓝色分量); 卷积核中也包含 3 组权重矩阵 \((V^{(R)}, V^{(G)}, V^{(B)})\),它们分别在三个通道上滑动卷积,最后把结果相加得到输出特征图的一层。这一过程让模型能够理解颜色之间的组合关系,如红色与绿色的对比、蓝色与亮度的变化等。

下面用 PyTorch 实现多输入通道卷积运算过程:

# 定义多输入通道卷积运算函数
def corr2d_multi_in(X, K):
"""
手动实现多输入通道的二维卷积运算
参数:
X: 输入多通道矩阵,形状为(C_in, H, W)
K: 卷积核,形状为(C_in, h, w)
"""
h, w = K.shape[1], K.shape[2] # 获取卷积核形状
Y = torch.zeros((X.shape[1] - h + 1, X.shape[2] - w + 1)) # 初始化输出矩阵 Y
# 计算卷积
for x, k in zip(X, K):
Y += cust_conv2d(x, k) # 对每个通道进行卷积并累加
return Y

(2)多输出通道(multi-output channels) 为了让网络能同时学习多种特征(如边缘、纹理、颜色模式等),通常不会只用一个卷积核,而是使用多个卷积核并行计算。假设卷积层中共有 \(C_{out}\) 个卷积核,每个卷积核都会独立扫描输入并输出一个特征图(feature map)。于是输出结果为: \[ H = [H^{(1)}, H^{(2)}, \dots, H^{(C_{out})}] \in \mathbb{R}^{C_{out} \times H' \times W'} \] 其中每个 \(H^{(k)}\) 对应一个卷积核的响应。 每个卷积核都有与输入通道数相同的深度,即 \(V^{(k)} \in \mathbb{R}^{C_{in} \times K \times K}\),因此每个卷积核会综合所有输入通道的信息。 换句话说:

  • 输入有 \(C_{in}\) 个通道;
  • 每个卷积核同时在所有输入通道上进行卷积并求和;
  • 网络中共有 \(C_{out}\) 个这样的卷积核;
  • 最终输出 \(C_{out}\) 个特征通道。

例如,若输入为 RGB 三通道图像 (\(C_{in}=3\)),我们用 16 个卷积核 (\(C_{out}=16\)),则输出特征图为 \(16\times H'\times W'\)。这些通道可能分别捕捉到不同层次的特征,如:第一个卷积核检测垂直边缘,第二个检测水平纹理,第三个检测颜色过渡,依此类推。

用矩阵表述一下,就是:输入的数据形状是 \((C, H, W)\) ,每个卷积核的形状是 \((C, FH, FW)\),因为有 \(FN\) 个卷积核,所以卷积的形状是 \((FN,C,FH,FW)\),然后输入的数据和一个卷积核运算后的形状是 \((1, OH, OW)\)\(FN\) 个卷积核那么最终输出的形状就是 \(FN,OH,OW\)

(3)\(1\times1\) 卷积核(pointwise convolution) 在常规卷积中,卷积核的空间尺寸(如 \(3\times3\)\(5\times5\))用于提取局部空间特征。而 \(1\times1\) 卷积核则只在同一位置上进行“通道融合”,即每个输出像素只对输入通道做线性组合,没有空间范围的滑动影响。 数学表达式为: \[ [H]_{i,j}^{(k)} = \sum_{c=1}^{C_{in}} V_{k,c}\,[X]_{i,j}^{(c)} \] 它的本质是:在不改变空间分辨率的情况下,对通道进行线性变换。

这种操作有以下作用: ① 通道融合与特征组合 \(1\times1\) 卷积可以把多个通道的信息混合,例如从 64 个输入通道压缩到 16 个输出通道,相当于在通道维度上做特征重组。

降维与升维 通过调节输出通道数 \(C_{out}\),可以降低计算量(降维)或增强特征表示能力(升维)。例如在 GoogLeNet 的 Inception 模块中,\(1\times1\) 卷积被广泛用来先降维再卷积,以减少参数量。

增加非线性表达能力\(1\times1\) 卷积后接非线性激活函数(如 ReLU),相当于在每个像素点上构造了一个小的全连接网络,增强了特征组合能力。

例如:输入为 \(32\times32\times64\),使用 32 个 \(1\times1\) 卷积核后,输出为 \(32\times32\times32\)。 每个输出位置 \((i,j)\) 的 32 维向量,都是由输入的 64 维向量线性变换而来,相当于“压缩”了通道维度。

用PyTorch实现:

def corr2d_multi_in_out(X, K):
"""
手动实现多输入通道和多输出通道的二维卷积运算
参数:
X: 输入多通道矩阵,形状为(C_in, H, W)
K: 卷积核,形状为(C_out, C_in, h, w)
"""
return torch.stack([corr2d_multi_in(X, k) for k in K], 0) # 对每个输出通道进行卷积

测试:

X = torch.tensor([[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
[[1, 1, 1], [1, 1, 1], [1, 1, 1]],
[[2, 2, 2], [2, 2, 2], [2, 2, 2]]])
K = torch.tensor([[[[1]], [[2]], [[3]]],
[[[4]], [[1]], [[1]]],
[[[5]], [[3]], [[3]]]])
print(f'X shape: {X.shape}, K shape: {K.shape}, Y shape: {corr2d_multi_in_out(X, K).shape}')
print(f'Y = \n{corr2d_multi_in_out(X, K)}')

结果:

X shape: torch.Size([3, 3, 3]), K shape: torch.Size([3, 3, 1, 1]), Y shape: torch.Size([3, 3, 3])
Y =
tensor([[[ 9., 10., 11.],
[12., 13., 14.],
[15., 16., 17.]],

[[ 7., 11., 15.],
[19., 23., 27.],
[31., 35., 39.]],

[[14., 19., 24.],
[29., 34., 39.],
[44., 49., 54.]]])

(4)总结

  • 多输入通道:在卷积时对每个输入通道分别计算并求和,实现跨通道特征融合;
  • 多输出通道:使用多个卷积核并行提取不同特征,构成输出的多个通道;
  • \(1\times1\) 卷积:不提取空间特征,而在通道维度上进行线性组合,用于特征压缩与融合。

多通道卷积使 CNN 能够从低层的颜色、纹理到高层的语义逐级整合信息,从而具备强大的特征表达能力。

6.2.5 激活函数

卷积神经网络和标准的神经网络类似,为了保证非线性,也需要使用激活函数,即在卷积运算之后把输出值另加偏移量输入激活函数,作为下一层的输入:

\[ a_{i,j} = f(h_{i,j}) = f\left(\sum_{a,b} W_{a,b}X_{i+a,j+b} + b\right) \] 其中 \(f(\cdot)\) 就是激活函数。

6.2.6 卷积函数

通常 PyTorch 中的卷积函数使用 nn.Conv2d 来实现。

nn.Conv2d 函数的定义如下:

nn.Conv2d(
in_channels, # 输入通道数
out_channels, # 输出通道数
kernel_size: Union[int, Tuple[int, int]], # 卷积核大小,可以是单个整数或(高度, 宽度)的元组
stride: Union[int, Tuple[int, int]] = 1, # 步幅,可以是单个整数或(高度, 宽度)的元组
padding: Union[int, Tuple[int, int]] = 0, # 填充,可以是单个整数或(高度, 宽度)的元组
dilation: Union[int, Tuple[int, int]] = 1, # 卷积核元素之间的间距,可以是单个整数或(高度, 宽度)的元组
groups=1, # 控制输入和输出通道的连接方式,当 group=1 时输出是所有输入的卷积,当 group=2 时,相当于有并排的两个卷积层,每个卷积层计算一半的输入通道,随后将它们的输出连接在一起
bias=True, # 是否使用偏置,默认使用。其中 bias 是一个形状为 (out_channels,) 的一维张量
padding_mode='zeros' # 有 4 种填充模式: 'zeros', 'reflect', 'replicate', 'circular',默认零填充
)

注意 in_channels / groups 必须是整数,否则报错; Conv2d 的输入输出均为 4 维张量,形状为 (批量大小, 通道数, 高度, 宽度)。

卷积函数 nn.Conv2d 参数中输出形状的计算公式如下:

  • \(Input: (N, C_{in}, H_{in}, W_{in})\)

  • \(Output: (N, C_{out}, H_{out}, W_{out})\)

\[ H_{out} = \frac{H_{in} + 2\times padding[0] - dilation[0]\times(kernel\_size[0] - 1) - 1}{stride[0]} + 1 \]

\[ W_{out} = \frac{W_{in} + 2\times padding[1] - dilation[1]\times(kernel\_size[1] - 1) - 1}{stride[1]} + 1 \]

  • \(weight(out\_channels, \frac{in\_channels}{groups}, kernel\_size[0], kernel\_size[1])\)

6.2.7 特征图与感受野

在卷积神经网络(CNN)中,特征图(Feature Map)感受野(Receptive Field)是两个最核心的概念,它们共同决定了网络“看见什么”和“怎么看”的能力。特征图是卷积层输出的结果,表示模型在不同位置上对某种特征的响应;感受野描述的是每个输出神经元在输入空间中“能看到多大范围”的区域。这两个概念一起揭示了卷积网络从像素到语义的逐层抽象过程。 (1)特征图(Feature Map) 特征图是卷积操作的直接产物,它记录了卷积核在整张图像上滑动时的响应值。对于输入图像 \(X\) 和卷积核 \(W\),在位置 \((i,j)\) 上的卷积输出可写为: \[ H_{i,j} = \sigma\!\left(\sum_{a=-\Delta}^{\Delta}\sum_{b=-\Delta}^{\Delta} W_{a,b}\, X_{i+a, j+b} + b \right) \] 所有的 \(H_{i,j}\) 组成了一个二维矩阵,这个矩阵就是卷积核在整张图像上检测到的“特征强度分布”——即特征图。 一层卷积往往包含多个卷积核,因此会产生多个特征图: \[ H = [H^{(1)}, H^{(2)}, \dots, H^{(C_{out})}] \] 其中每个特征图 \(H^{(k)}\) 对应一种特征(例如边缘、角点、颜色对比、纹理方向等)。浅层卷积核通常捕捉局部几何形状,如垂直或水平边缘;中层卷积核能识别更复杂的纹理模式;深层卷积核则提取高层语义,如物体轮廓或局部结构。 从直观角度看,特征图可以理解为“神经网络的眼睛看到图像后激活的模式热力图”,其亮度或数值越高,表示该区域越符合卷积核所关注的特征。 (2)感受野(Receptive Field) 感受野表示网络中某个神经元在输入图像中能“看到”的范围大小。换句话说,它是输入空间中能影响该神经元输出值的像素区域。 以第一层卷积为例,若卷积核大小为 \(3\times3\),则每个输出像素仅受输入的 \(3\times3\) 区域影响,因此该神经元的感受野为 \(3\times3\)。 但是随着网络层数增加,感受野会不断扩大。因为第二层的每个神经元不仅取决于第一层的一个位置,还间接取决于第一层对应的卷积窗口所覆盖的输入区域。这样逐层传递后,高层神经元的感受野就对应输入图像中的更大范围。

假设网络中卷积核大小为 \(k_l\),步长为 \(s_l\),则第 \(L\) 层神经元的感受野大小可递推计算为: \[ R_L = R_{L-1} + (k_L - 1)\prod_{i=1}^{L-1}s_i \] 其中 \(R_1 = k_1\)。 例如:

  • 若有两层卷积,均为 \(3\times3\) 卷积核、步长 1,则 \[ R_1 = 3, \quad R_2 = 3 + (3-1)\times1 = 5 \] 即第二层神经元的感受野为输入图像的 \(5\times5\)

  • 若卷积层后接一个 \(2\times2\) 池化层(步长 2),则感受野扩大到 \(R_3 = 5 + (2-1)\times1\times1 = 6\),覆盖区域更大。

感受野越大,神经元捕获的上下文信息越丰富,能理解更整体的结构;感受野太小,则只能感知局部纹理,难以理解全局语义。 (3)特征图与感受野的关系 特征图告诉我们“卷积核在图像中检测到了什么”,而感受野告诉我们“它是从多大的图像区域中学到这些信息的”。 两者的关系可以概括为:

  • 特征图是响应结果,展示卷积核对不同区域的激活强度;
  • 感受野是输入范围,描述一个特征响应是由输入图像中哪一块区域决定的。 随着网络层数加深:
  • 特征图的空间分辨率逐渐变小(例如从 \(224\times224\) 逐层降到 \(7\times7\)),
  • 但每个位置的感受野逐渐变大(例如从 \(3\times3\) 扩大到覆盖整个输入图像)。 这意味着网络逐步从“局部纹理”感知过渡到“全局语义”理解。

6.2.8 全卷积神经网络

全卷积神经网络(Fully Convolutional Network, FCN)*是一种特殊形式的卷积神经网络,它去掉了传统 CNN 最后阶段的*全连接层(Fully Connected Layer),而是完全由卷积层、激活层、池化层和上采样层构成,因此得名“全卷积”。它最早被提出用于语义分割任务(Semantic Segmentation),能让网络输出与输入图像大小一致的、逐像素预测结果。

(1)传统卷积神经网络的局限 普通 CNN(例如用于分类的 AlexNet、VGG、ResNet)在前面几层使用卷积和池化层提取特征,在最后一层使用全连接层输出固定长度的分类向量。 这种结构的特点是:

  • 输入尺寸固定(例如 \(224\times224\));
  • 输出只有一个整体标签(如“狗”、“猫”);
  • 空间位置信息在全连接层中被压缩丢失。 但在图像分割或目标检测任务中,我们需要对每个像素每个区域进行预测,这时全连接层就无法保留空间结构。

(2)全卷积神经网络的核心思想 FCN 将全连接层视为一种特殊的卷积层。 例如,一个全连接层可以看作是卷积核大小等于输入特征图大小的卷积层: \[ \text{Fully Connected: } y = W\cdot x + b \quad \Rightarrow \quad \text{Equivalent Conv: } y = W * X + b \] 这样一来,网络就不再需要固定输入尺寸,而能对任意大小的图像进行卷积操作,输出对应空间尺寸的特征图。 换句话说:FCN 保留了空间坐标信息,使每个输出像素都对应输入图像的某个区域,实现“从分类到逐像素预测”的转变。

6.3 池化层

池化(Pooling)又称为下采样,通过卷积层获取图像的特征后,理论上可以直接使用这些特征训练分类器(比如softmax),但是这样做面临巨大的参数挑战,而且容易产生过拟合的现象。为了进一步降低网络训练参数及模型的过拟合程度,可以对卷积层进行池化处理。常见的池化方式有3种:

  • 最大池化(Max Pooling):选择 Pooling 窗口中的最大值作为采样值;
  • 平均池化(Mean Pooling):将 Pooling 窗口中的所有值相加取平均,以平均值作为采样值;
  • 全局最大(平均)池化:全局池化是对整个特征图的池化,而不是在移动窗口范围内的池化。

池化层在 CNN 中可以用来减小尺寸,提高运算速度及减小噪声的影响,让各特征更加健壮。池化的作用体现在下采样:保留显著特征、降低特征维度、增大感受野。深度网络越往后越能捕捉到物体的语义信息,这种语义信息建立在较大的感受野基础上。

6.3.1 局部池化

在 PyTorch 中,池化层通常使用 nn.MaxPool2d 和 nn.AvgPool2d 来实现。以最大池化为例,nn.MaxPool2d 的定义如下:


nn.MaxPool2d(
kernel_size: Union[int, Tuple[int, int]], # 池化窗口大小
stride: Optional[Union[int, Tuple[int, int]]] = None, # 步幅,默认与 kernel_size 相同
padding: Union[int, Tuple[int, int]] = 0, # 填充
dilation: Union[int, Tuple[int, int]] = 1, # 池化窗口元素之间的间距
return_indices: bool = False, # 是否返回池化窗口内最大值的索引
ceil_mode: bool = False # 是否使用向上取整来计算输出形状
)

假设输入 input 的形状为:\((N, C, H_{in}, W_{in})\),输出 output 的形状为 \((N, C, H_{out}, W_{out})\) ,则: \[ H_{out} =\left[ \frac{H_{in} + 2\times padding[0] - dilation[0]\times(kernel\_size[0] - 1) - 1}{stride[0]} + 1\right] \]

\[ W_{out} = \left[\frac{W_{in} + 2\times padding[1] - dilation[1]\times(kernel\_size[1] - 1) - 1}{stride[1]} + 1\right] \]

下面是一个具体的实例代码:

# 池化窗口为 3*3,步幅为 2
m1 = nn.MaxPool2d(3, stride=2)
# 池化窗口为 3*2,步幅为 2*1
m2 = nn.MaxPool2d((3, 2), stride=(2, 1))
input = torch.randn(20, 16, 50, 32) # 输入为 (批量大小, 通道数, 高度, 宽度)
output1 = m1(input)
output2 = m2(input)
print(f'input shape: {input.shape}, output1 shape: {output1.shape}, output2 shape: {output2.shape}')

结果为:

input shape: torch.Size([20, 16, 50, 32]), 
output1 shape: torch.Size([20, 16, 24, 15]), output2 shape: torch.Size([20, 16, 24, 31])

6.3.2 全局池化

全局池化没有卷积核大小的限制,它针对的是整张特征图。比如全局平均池化(Global Average Pooling, GAP)不以窗口的形式取平均值,而是以特征图为单位进行均值化,即一个特征图输出一个值。

理解全局池化(Global Pooling),可以和全连接层(Fully Connected Layer)作对比。两者都处在卷积神经网络的最后阶段,用于把卷积得到的特征图转换为最终的输出(比如类别概率),但它们的思路完全不同(1)从结构上理解 假设最后一层卷积输出特征图大小为 \(C\times H\times W\)

  • 全连接层会把它展平成一个长向量(长度 \(C\times H\times W\)),然后乘以一个巨大的权重矩阵,输出固定维度的分类结果;
  • 全局池化则不使用权重,而是在空间维度上进行统计,例如求平均或取最大,使每个通道只保留一个值,得到 \(C\times1\times1\) 的特征向量。

对比如下:

项目 全连接层 (FC) 全局池化 (Global Pooling)
输入 \(C\times H\times W\) \(C\times H\times W\)
处理方式 展平 + 乘权重矩阵 对每个通道求平均或最大值
参数量 有大量可学习参数 无参数
输出 固定长度向量 每通道一个值 (\(C\times1\times1\))
是否依赖输入尺寸 是(输入大小必须固定) 否(可处理任意尺寸)
是否保持语义 混合所有空间信息 保留通道语义(语义统计)

(2)从计算方式理解

全连接层: \[ y_j = \sum_{i=1}^{C\times H\times W} W_{ji} x_i + b_j \] 每个输出节点 \(y_j\) 都与输入的所有像素相连。它通过权重 \(W_{ji}\) 来“学习”哪些位置、哪些特征重要。

全局平均池化(GAP): \[ y_c = \frac{1}{H\times W}\sum_{i=1}^{H}\sum_{j=1}^{W} x_{c,i,j} \] 它没有参数,直接取每个通道的平均值,作为该通道特征的整体强度。 全局最大池化(GMP)则取每个通道的最大响应: \[ y_c = \max_{i,j} x_{c,i,j} \] 两者的结果都是一个 \(C\)-维向量,代表每个特征通道的全局响应。 (3)

可以这样理解这两种结构的思维方式:

  • 全连接层相当于“让网络自己决定关注哪里”,它通过大量参数学习哪些像素位置重要;
  • 全局池化相当于“让每个特征自己说话”,它不再关心位置,而是统计每个特征在整张图中的强度。

例如,一个卷积核可能检测到“狗的耳朵特征”,另一个检测“毛发纹理”。全局平均池化会计算这些特征在整张图片中出现的整体强度,输出一个语义层面的描述向量,用于分类器判断“这是狗”。 (4)从优缺点看差异

项目 全连接层 全局池化
优点 强大的表达能力,可学习复杂特征组合 无参数、计算高效、避免过拟合
缺点 参数多,易过拟合,输入尺寸固定 舍弃部分空间位置信息
典型应用 早期分类网络(AlexNet、VGG) 现代高效网络(GoogLeNet、ResNet、MobileNet)

在 VGG16 中,最后一层卷积后有三层全连接层,参数超过 1 亿;而在 GoogLeNet、ResNet 中,全连接层被全局平均池化替代,参数骤减到几百万,大幅提升泛化性能。

在 PyTorch 中,可以借助自适应池化层(AdaptiveMaxPool2d(1)或AdaptiveAvgPool2d(1))来实现。

# 自适应池化层
# nn.AdaptiveAvgPool2d 可以将输入的高和宽变换到指定的输出大小
m = nn.AdaptiveAvgPool2d((5, 7)) # 输出大小为 (5, 7)
input = torch.randn(1, 64, 8, 9) # 输入
output = m(input)
print(f'input shape: {input.shape}, output shape: {output.shape}')
# input shape: torch.Size([1, 64, 8, 9]), output shape: torch.Size([1, 64, 5, 7])
# 输出大小为正方形 7*7
m = nn.AdaptiveAvgPool2d(7)
output = m(input)
print(f'input shape: {input.shape}, output shape: {output.shape}')
# input shape: torch.Size([1, 64, 8, 9]), output shape: torch.Size([1, 64, 7, 7])
# 输出大小为 1*1
m = nn.AdaptiveAvgPool2d(1)
output = m(input)
print(f'input shape: {input.shape}, output shape: {output.shape}')
# input shape: torch.Size([1, 64, 8, 9]), output shape: torch.Size([1, 64, 1, 1])

6.4 现代经典卷积神经网络

现代经典卷积神经网络(Modern Classical CNN Architectures)是指在深度学习发展过程中,一系列具有里程碑意义的卷积网络结构。这些网络不仅在图像分类、检测、分割等视觉任务中取得了突破性成果,也奠定了深度神经网络设计的基本思想。以下按照时间顺序介绍几种具有代表性的现代卷积神经网络结构。 (1)LeNet-5(1998)——卷积网络的起点 LeNet-5 是 Yann LeCun 提出的早期 CNN,用于手写数字识别(MNIST 数据集)。它首次展示了卷积层、池化层和全连接层结合的完整结构。 结构特点:

  • 由 2 个卷积层 + 2 个平均池化层 + 3 个全连接层组成;
  • 激活函数使用 Sigmoid 或 Tanh;
  • 参数量极少(约 6 万个),可在当时的计算机上训练;
  • 提出了局部感受野与权重共享的概念。 意义:LeNet-5 奠定了现代卷积网络的基础,是深度视觉识别的开端。

(2)AlexNet(2012)——深度学习复兴的标志 AlexNet 在 ImageNet 图像分类竞赛中以巨大优势获胜,使深度学习重新成为主流。 主要创新:

  • 网络更深(8 层:5 个卷积层 + 3 个全连接层);
  • 引入 ReLU 激活函数,加快收敛速度;
  • 使用 Dropout 正则化 防止过拟合;
  • 利用 GPU 并行计算 实现大规模训练;
  • 使用 数据增强(翻转、平移、裁剪)提高泛化能力。 意义:AlexNet 是第一代深层 CNN,开启了“深度学习时代”。

(3)VGGNet(2014)——网络结构的统一与简洁 VGG 网络由牛津大学 Visual Geometry Group 提出(VGG16、VGG19 最常见)。 主要思想:

  • 使用 多个小卷积核(3×3) 代替大卷积核;
  • 通过堆叠多个卷积层来增加感受野;
  • 结构统一:卷积层堆叠 → 池化层降采样 → 全连接层分类。 典型结构(VGG16):13 个卷积层 + 3 个全连接层。 意义:VGG 展示了“深度带来性能提升”的规律,并以简洁规则的结构成为后续网络设计模板。

(4)GoogLeNet / Inception(2014)——多尺度特征融合 Google 团队提出的 Inception 网络(又称 GoogLeNet)在同年 ImageNet 夺冠。 主要创新:

  • 引入 Inception 模块,在同一层中并行使用不同尺寸卷积核(1×1、3×3、5×5)来捕捉多尺度特征;
  • 使用 1×1 卷积核进行降维,减少参数量;
  • 移除全连接层,使用 全局平均池化 代替分类层。 结构:22 层深,参数仅 500 万(远少于 VGG 的 1.38 亿)。 意义:GoogLeNet 提出了模块化思想,实现了高效的特征融合和计算节省。

(5)ResNet(2015)——残差连接解决深层退化问题 微软研究院提出的 ResNet(Residual Network)在 ImageNet 2015 冠军中使用了 152 层网络,性能远超前人。 主要创新:

  • 引入 残差连接(Residual Connection),即“捷径连接”: \[ y = F(x) + x \] 其中 \(F(x)\) 表示卷积块的映射函数,\(x\) 是输入特征。

  • 残差结构使梯度可以直接传递,避免深层网络训练困难。

  • 证明了网络可以安全扩展到上百层甚至上千层。 意义:ResNet 开启了“超深网络时代”,其残差思想成为现代网络的核心模块(DenseNet、Transformer 等均借鉴)。

(6)DenseNet(2017)——特征复用与梯度流动优化 DenseNet(Densely Connected CNN)进一步改进了残差思想:

  • 每一层都与之前所有层相连: \[ x_l = H_l([x_0, x_1, \dots, x_{l-1}]) \] 其中 \([x_0,\dots,x_{l-1}]\) 表示所有前层特征拼接。

  • 特征复用使网络更加高效,减少冗余计算;

  • 梯度流动更顺畅,训练更加稳定。 意义:DenseNet 强调信息复用和高效特征传播,是对 ResNet 的结构化强化。

(7)MobileNet / EfficientNet(2017–2019)——轻量化网络时代 移动端和嵌入式应用需要计算高效的模型,MobileNet、ShuffleNet、EfficientNet 等轻量化网络应运而生。 主要技术:

  • 深度可分离卷积(Depthwise Separable Convolution):将标准卷积分解为逐通道卷积 + 1×1 卷积,显著降低参数量;
  • 宽度、深度与分辨率联合缩放(Compound Scaling):EfficientNet 通过自动化搜索得到最优网络比例;
  • 高性能与低功耗兼顾,适用于实时检测与边缘设备。 意义:轻量化 CNN 让深度学习走向实际部署阶段。

(8)现代趋势:从卷积到混合架构 近年来,CNN 与 Transformer 模型逐渐融合,如 ConvNeXt、Swin Transformer、Vision Transformer (ViT) 等。 ConvNeXt 在保留卷积结构的同时引入 Transformer 风格的设计(LayerNorm、GELU、深度卷积等),展现出与纯 Transformer 相当的性能。 这标志着 CNN 从“手工设计模块”走向“统一架构演化”的新阶段。

6.5 使用卷积神经网络实现 CIFAR10 多分类

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torch.utils.data as Data
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

# 下载 CIFAR10 数据集并进行预处理
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]
)

# 载入训练集和测试集
train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=False, transform=transform)
test_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=False, transform=transform)

# 创建数据加载器
train_loader = Data.DataLoader(train_set, batch_size=200, shuffle=True)
test_loader = Data.DataLoader(test_set, batch_size=512, shuffle=False)

# 定义类别
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

# 定义网络
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


class CNNNet(nn.Module):
def __init__(self):
super(CNNNet, self).__init__()
self.conv1 = nn.Conv2d(3, 16, 5, 1)
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(16, 36, 3, 1)
self.pool2 = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(36 * 6 * 6, 128)
self.fc2 = nn.Linear(128, 10)

def forward(self, x):
x = self.pool1(F.relu(self.conv1(x)))
x = self.pool2(F.relu(self.conv2(x)))
x = x.view(-1, 36 * 6 * 6)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
return x


net = CNNNet().to(device)
lr = 1e-3
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
optim = optim.SGD(net.parameters(), lr=lr, momentum=0.9) # 随机梯度下降优化器

# 训练网络
num_epochs = 30
for epoch in range(num_epochs):
running_loss = 0.0
for i, data in enumerate(train_loader, 0): # (train_loader, 0) 表示从第 0 个batch开始
# 获取训练数据
images, labels = data
images, labels = images.to(device), labels.to(device)

optim.zero_grad() # 梯度清零
outputs = net(images) # 前向传播
loss = criterion(outputs, labels) # 计算损失
loss.backward() # 反向传播
optim.step() # 更新参数

running_loss += loss.item()
if i % 100 == 99: # 每 100个 小批量输出一次loss
print(f'[Epoch {epoch + 1}, Batch {i + 1}] loss: {running_loss / 100:.3f}')
running_loss = 0.0

if (epoch + 1) % 10 == 0: # 每 10 个 epoch 测试一次
correct = 0
total = 0
with torch.no_grad(): # 测试时不需要计算梯度
for data in test_loader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1) # 获取预测结果
total += labels.size(0)
correct += (predicted == labels).sum().item()
if (epoch + 1) % num_epochs == 0:
print('*' * 20 + '!!! CNN Final Result !!!' + '*' * 20)
print(f'Epoch {epoch + 1}, Accuracy of the network on the 10000 test images: {100 * correct / total:.2f}%')


# 计算每个类别的准确率
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
for data in test_loader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs, 1)
# outputs.shape = (batch_size, 10),torch.max返回(最大值, 索引)
# predicted.shape = (batch_size),包含每个样本的预测类别

c = (predicted == labels).squeeze() # # 比较预测和真实标签,得到布尔张量,c.shape = (batch_size)
for i in range(len(labels)): # 遍历当前批次中的每个样本
label = labels[i] # 获取第i个样本的真实标签(0-9)
class_correct[label] += c[i].item() # 如果预测正确,对应类别的正确计数+1
class_total[label] += 1 # 对应类别的总计数+1

# 输出每个类别的准确率
for i in range(10):
if class_total[i] == 0:
accuracy = 0
else:
accuracy = 100 * class_correct[i] / class_total[i]
print(f'Accuracy of {classes[i]:5s} : {accuracy:.2f}%')


# 输出模型的参数量
total_params = sum(p.numel() for p in net.parameters())
print(f'Total parameters in CNN: {total_params}')