11-卷积神经网络(CNN): 原理和示例

发布于 2025-04-08  152 次阅读


“学不完,根本学不完......”

1. 核心原理

CNN通过卷积核以滑动窗口的方式,提取输入数据的空间特征信息。通过堆叠多个卷积核,逐渐提取更高阶的特征信息,如:边缘->纹理->物体部件。

1.1 核心组件

  • 卷积层:通过滑动窗口对输入信息进行局部特征提取。
  • 池化层(汇聚层):对输入信息进行下采样,降低维度保留关键信息。
  • 全连接层:将输入信息的所有特征图“展平”,整合全局信息并输出结果。

其他重要补充

  • CNN模型常使用ReLU作为隐藏层激活函数,点这里了解ReLU
  • 卷积层的输出尺寸:output_size = (input_size + 2*padding - kernel_size) // stride + 1

1.2 数学原理:

1.2.1 卷积过程

深度学习的卷积过程是数学上的互相关运算。

a. 数学上的卷积过程

以 [3x1信号,3x1卷积核,步长为1,零填充] 为例:

$$ \begin{aligned} \textbf{定义一维信号}: f[n] = [1,\ 2,\ 3] \quad &\text{和卷积核心:} \quad h[n] = [4,\ 5,\ 6] \\ \\ \textcolor{red}{\textbf{步骤1:翻转操作}}(卷积核心)\\ \\ 将 h[n] \textbf{翻转}得到 h[-n]: \\ h[-n] &= [6,\ 5,\ 4] \quad \textcolor{red}{\leftarrow \text{翻转步骤}} \\ \\ \textcolor{green}{\textbf{步骤2:互相关操作}}(滑动点乘)\\ \\ 将翻转后的 h[-n] 与 f[n] 进行\textbf{互相关}运算: \\ (f * h)[k] &= \sum_{m} f[m] \cdot h[-(k - m)] \\ \\ \textbf{逐位移计算}(假设零填充边界):& \begin{align*} \textcolor{green}{k = -1:} \quad & ({\scriptstyle 0}) \cdot 6 + 1 \cdot 5 + 2 \cdot 4 = 0 + 5 + 8 = 13 \\ \textcolor{green}{k = 0:} \quad & 1 \cdot 6 + 2 \cdot 5 + 3 \cdot 4 = 6 + 10 + 12 = 28 \\ \textcolor{green}{k = 1:} \quad & 2 \cdot 6 + 3 \cdot 5 + ({\scriptstyle 0}) \cdot 4 = 12 + 15 + 0 = 27 \\ \textcolor{green}{k = 2:} \quad & 3 \cdot 6 + ({\scriptstyle 0}) \cdot 5 + ({\scriptstyle 0}) \cdot 4 = 18 + 0 + 0 = 18 \end{align*} \\ \\ \textbf{最终结果}:\\ (f * h)[n] &= [13,\ 28,\ 27,\ 18] \quad \textcolor{red}{\leftarrow \text{完整卷积结果}} \end{aligned} $$

对于卷积神经网络,在实践应用中发现卷积过程的翻转步骤对最终预测结果几乎没什么影响(尽管物理意义变了),于是省去了该步骤对计算开销,只使用了互相关运算步骤。

b. 卷积神经网络的卷积(数学上的互相关)过程

以[3x3信号, 3x3卷积核, 步长为1, padding为1,零填充]为例:

$$ \begin{aligned} \textbf{定义输入矩阵}: \mathbf{X} = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}, \quad \textbf{卷积核} \quad \mathbf{K} = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \\ \\ \textcolor{red}{\textbf{传统步骤1:传统核翻转}}(深度学习跳过此步骤)\\ \quad 将核\mathbf{K}进行\textbf{双重翻转}(水平+垂直):\\ \\ \mathbf{K}_{\text{翻转}} = \begin{bmatrix} 9 & 8 & 7 \\ 6 & 5 & 4 \\ 3 & 2 & 1 \end{bmatrix} \quad \textcolor{red}{\leftarrow \begin{array}{l} \text{水平翻转} + \text{垂直翻转} \\ \text{原始核索引变化:} (1,1)\to(3,3) \end{array}} \\ \\ \textcolor{blue}{\textbf{真正的步骤1:直接互相关操作}}(实际深度学习实现)\\ 用\textbf{原始未翻转核}在输入矩阵上滑动计算(padding=0, stride=1): \\ \\ \mathbf{Y} = \sum_{i=0}^{2} \sum_{j=0}^{2} X[i,j] \cdot K[i,j] \\ \\ [1, 1]处展开示例: \begin{aligned} \mathbf{Y}_{1,1} &= (1\times1)+(2\times2)+(3\times3) \\ &\quad + (4\times4)+(5\times5)+(6\times6) \\ &\quad + (7\times7)+(8\times8)+(9\times9) \\ \\ &= 1 + 4 + 9 \\ &\quad + 16 + 25 + 36 \\ &\quad + 49 + 64 + 81 \\ \\ &= 285 \end{aligned} \\ \\ \textbf{最终结果}: \mathbf{X} = \begin{bmatrix} 94 & 154 & 106 \\ 186 & 285 & 186 \\ 106 & 154 & 94 \end{bmatrix} & \\ \end{aligned} $$

Python实现:

# 输入矩阵和卷积核定义
input_matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]
kernel = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# 超参数设置
pad = 1
stride = 1

# 对输入矩阵进行零填充
n_rows = len(input_matrix)
n_cols = len(input_matrix[0]) if n_rows > 0 else 0
padded_input = []
# 添加上方的填充行
for _ in range(pad):
    padded_input.append([0] * (n_cols + 2 * pad))
# 添加中间行(左右填充)
for row in input_matrix:
    padded_row = [0] * pad + row + [0] * pad
    padded_input.append(padded_row)
# 添加下方的填充行
for _ in range(pad):
    padded_input.append([0] * (n_cols + 2 * pad))

# 计算输出矩阵尺寸
kernel_rows = len(kernel)
kernel_cols = len(kernel[0]) if kernel_rows > 0 else 0
output_rows = (n_rows + 2 * pad - kernel_rows) // stride + 1
output_cols = (n_cols + 2 * pad - kernel_cols) // stride + 1

# 初始化输出矩阵
output = [[0 for _ in range(output_cols)] for _ in range(output_rows)]

# 执行卷积(互相关)运算
for i in range(output_rows):
    for j in range(output_cols):
        # 计算窗口起始位置
        row_start = i * stride
        row_end = row_start + kernel_rows
        col_start = j * stride
        col_end = col_start + kernel_cols

        # 提取当前窗口
        window = []
        for r in range(row_start, row_end):
            window.append(padded_input[r][col_start:col_end])

        # 计算窗口与卷积核的点乘和
        sum_val = 0
        for x in range(kernel_rows):
            for y in range(kernel_cols):
                sum_val += window[x][y] * kernel[x][y]
        output[i][j] = sum_val

# 打印结果
print("填充后的输入矩阵:")
for row in padded_input:
    print(row)
print("\n卷积结果矩阵:")
for row in output:
    print(row)
    
    
##
# 填充后的输入矩阵:
# [0, 0, 0, 0, 0]
# [0, 1, 2, 3, 0]
# [0, 4, 5, 6, 0]
# [0, 7, 8, 9, 0]
# [0, 0, 0, 0, 0]
# 
# 卷积结果矩阵:
# [94, 154, 106]
# [186, 285, 186]
# [106, 154, 94]
# #

c. 扩展

物理意义的区别

  • 数学卷积(翻转+互相关):严格的理论工具。通过加权和表示一个线性时不变系统(如滤波器)对输入信号的响应。例如,在信号处理中,卷积描述输入信号经过系统后的输出是输入与系统冲激响应的叠加。
  • 深度学习卷积(互相关):工程简化后的实践工具。主要用于提取局部特征并构建层次化表示。

翻转的必要性

  • 数学卷积(翻转+互相关):为确保交换律的成立(即f*g = g*f),翻转是必要的。此外,卷积的翻转步骤直接反映了 线性时不变系统 的物理特性。
  • 深度学习卷积(互相关):目的是提取局部特征并构建层次化表示,所更多时候只需关系输入信号在空间上的平移不变性即可,几乎无需关心其他数学性质,翻转不是必要的

经典卷积的扩展应用

  1. 转置卷积:用于生成任务的上采样,需注意棋盘效应。
  2. 分组卷积:平衡性能与效率,支持模型并行化。
  3. 可变形卷积:增强对不规则目标的适应性,适合检测与分割。
  4. 动态卷积:输入自适应,提升复杂任务的表现力。
  5. 逐点卷积:轻量化的通道信息混合工具。
  6. 通道注意力卷积:低成本提升特征表达能力。
  7. 非局部卷积:建模全局依赖,代价是计算复杂度高。

举例(部分):

名称核心思想实现方式特点主要作用
转置卷积(Transposed Convolution)反向操作常规卷积,用于上采样特征图(如生成高分辨率输出)。通过填充(padding)和步长(stride)调整,将低维特征图映射到高维空间。- 参数与常规卷积类似,但计算方向相反。
- 可能引入“棋盘效应”(不均匀重叠)。
图像分割、生成对抗网络(GAN)中的上采样。
分组卷积(Grouped Convolution)将输入通道分为多组,每组独立卷积后拼接结果,减少计算量。输入通道分为 GG 组,每组单独卷积,输出通道数为 G×每组输出通道数G×每组输出通道数。- 参数量减少为常规卷积的 1/G1/G
- 促进模型并行化(如ResNeXt)。
轻量化模型设计(如MobileNet、ResNeXt)。
可变形卷积(Deformable Convolution)通过可学习的偏移增强卷积核的空间适应能力,捕捉不规则目标。添加偏移参数,动态调整卷积核的采样位置(需双线性插值)。- 提升对形变目标的建模能力。
- 增加少量参数和计算量。
目标检测(如DCN)、医学图像分割。
动态卷积(Dynamic Convolution)根据输入动态调整卷积核权重,增强模型表达能力。使用多个卷积核,通过输入特征生成权重,加权融合多个核的结果。- 参数和计算量显著增加。
- 对复杂数据分布建模更强。
视频理解、多模态任务中的动态特征提取。
逐点卷积(Pointwise Convolution)使用1×1卷积核,仅混合通道信息而不改变空间尺寸。1×1卷积核作用于所有输入通道,生成新的通道组合。- 参数量少(Cin×CoutCin​×Cout​)。
- 常与深度卷积结合。
通道降维/升维(如MobileNet中的深度可分离卷积)。
通道注意力卷积(SE Block)通过通道注意力机制动态调整各通道权重,增强重要通道的响应。全局平均池化→全连接层生成通道权重→与原特征逐通道相乘。- 轻量级模块(如Squeeze-and-Excitation)。
- 显著提升模型性能。
图像分类、目标检测中的特征增强(如ResNet+SE)。
非局部卷积(Non-local Networks)捕捉长程依赖关系,模拟全图或全局上下文信息。通过相似性计算(如点积)建立像素间关系,加权融合全局信息。- 计算复杂度高(O(N2)O(N2))。
- 适合视频或大尺寸图像。
视频动作识别、语义分割中的长程依赖建模。

1.2.2 池化(汇聚)

池化是观察局部感受野信号并汇聚成一个信号

a. 常见池化(汇聚)方式

  • 平局池化:在局部感受野内计算所有值的平均值作为输出。
  • 最大池化:在局部感受野(如2×2的窗口)内直接取最大值作为输出。

若池化的感受野覆盖整个特征图,也叫全局(平均/最大)池化

b. 区别

特性最大池化平均池化全局池化
输出值局部区域最大值局部区域平均值整个特征图的统计值
抗噪声能力强(忽略非最大值噪声)弱(噪声会影响均值)取决于具体类型
保留信息突出显著特征(如边缘、纹理)保留整体分布信息高度抽象,丢弃空间信息
典型应用场景图像分类(如VGG、AlexNet)图像分割(需保留更多细节)模型轻量化(如MobileNet)

c. 主要意义

  1. 降维与计算效率
    • 减少特征图的空间尺寸(如从224×224→112×112),大幅降低后续层的参数量和计算量。
  2. 平移不变性(Translation Invariance)
    • 池化使网络对输入的小幅平移不敏感。例如,无论目标在图像中轻微移动,最大池化仍可能捕捉到相同的特征。
  3. 防止过拟合
    • 通过压缩特征图的维度,减少模型复杂度,增强泛化能力。
  4. 特征抽象能力
    • 逐步提取更高层次的特征(从边缘→纹理→物体部件→整体)。
  5. 减少内存占用
    • 池化后的特征图更小,节省显存和存储资源。

d. 扩展

  • 信息丢失:池化会丢弃部分细节信息,可能影响小目标检测或精细任务。
  • 替代方案:现代网络(如ResNet)有时用步长卷积(Strided Convolution)替代池化,通过增大卷积步长实现下采样。

1.2.3 全连接

全连接的本质是,整合全局特征信息,并输出结果

a. 原理——特征整合与高级抽象

  • 卷积层和池化层通过局部感受野提取图像的空间局部特征(如边缘、纹理等),但这些特征通常是分散的、局部的。
  • 全连接层将前一层的所有特征图“展平”(Flatten)为一维向量,并通过全连接的方式将这些特征全局关联,学习不同特征之间的复杂组合关系,形成更高层次的语义抽象(如物体部件或整体类别)。

b.  分类或回归决策

  • 在回归任务中(如目标检测中的坐标预测),全连接层直接输出数值结果。
  • 在分类任务中,最后一层全连接层的神经元数量等于类别数,输出每个类别的得分(如通过Softmax得到概率分布)
  • 全连接层通常作为分类器,将整合后的特征映射到样本的标签空间

c. 扩展

为了减少全连接参数效率问题,现代深度学习中可能会采用一些优化方案,如:

  • 全局平均池化(GAP):将最后一个卷积层的特征图取均值,直接替代全连接层(如ResNet)。
  • 瓶颈结构(Bottleneck):先用 1×11×1 卷积降维,再连接小规模全连接层(如MobileNet)。
  • 动态卷积/注意力:根据输入动态生成权重,减少固定参数。


2. CNN模型一览

2.1 里程碑CNN

模型名称诞生时间重要标志历史意义
LeNet-51998首个成功的CNN架构,手写数字识别奠定现代CNN基础结构
AlexNet2012ReLU激活、Dropout、多GPU训练开启深度学习黄金时代
VGGNet2014统一3x3小卷积堆叠策略证明网络深度的重要性
GoogLeNet2014Inception模块、1x1卷积降维首次突破"深度迷信"
ResNet2015残差连接、152层深度解决梯度消失难题
EfficientNet2019复合缩放(深度/宽度/分辨率)实现最优精度-效率平衡

现代CNN

模型名称发表年份论文地址重要创新点历史意义
ConvNeXt2022arXiv:2201.03545将Transformer设计理念移植到CNN:大卷积核(7x7)、LayerScale、GELU激活证明纯CNN架构通过现代化设计可超越Vision Transformer
MobileOne2022arXiv:2206.04040结构重参数化+动态稀疏训练,实现移动端1ms级ImageNet推理轻量化CNN的新标杆,iPhone 12上比MobileNetV3快5倍
RepVGG++2023arXiv:2303.15467多分支结构动态选择机制,训练时保留RepVGG特性,推理时自动优化分支推动重参数化技术进入动态化阶段,ImageNet Top-1达84.16%
EdgeNeXt2022arXiv:2206.10589混合局部-全局特征模块(LGEM),动态位置编码适应边缘设备首个在ARM芯片实现实时语义分割的CNN-Transformer混合架构
EfficientNetV22021arXiv:2104.00298渐进式训练策略+复合缩放改进,训练速度比V1快11倍确立NAS技术在工业级模型部署的实用价值
MaxViT2022arXiv:2204.01697多轴注意力机制(局部窗口+全局网格)与MBConv模块交替堆叠在ImageNet-1K上首次实现单一架构同时统治分类/检测/分割任务
DINOv22023arXiv:2304.07193自监督框架支持CNN特征解耦,无需标签即可学习场景深度/表面法线等物理属性推动CNN进入多模态自监督学习新时代
SLaK2023arXiv:2303.01158稀疏大核卷积(51x51),通过动态稀疏化降低计算量突破传统CNN小卷积核限制,ImageNet Top-1达84.3%
InceptionNeXt2023arXiv:2303.16900Inception模块与ConvNeXt架构融合,通道分组多尺度特征提取在ImageNet上以26M参数达到84.3% Top-1准确率
DualConv2022arXiv:2202.07481双路径卷积核设计(空间+通道路径),增强细粒度特征捕获能力在细粒度分类任务(FGVC)上超越传统CNN 2-3%
Dynamixer2023arXiv:2301.05883动态卷积核混合专家系统(MoE),根据输入动态激活不同卷积核首次将MoE机制引入CNN,ImageNet-1K达84.8% Top-1
ScalableViT2022arXiv:2203.10790可缩放窗口注意力机制,实现CNN-Transformer无缝缩放建立视觉模型参数从15M到700M的统一架构框架
ParC-Net2022arXiv:2203.03952位置感知循环卷积(ParC),通过全局位置编码增强CNN的长程建模能力在ImageNet上以ResNet-50参数量达到81.2% Top-1
WaveMix2022arXiv:2203.03689二维离散小波变换替代下采样,增强频域特征保留在图像复原任务中PSNR指标提升0.5-1.2dB
FocalNet2022arXiv:2203.11926焦点调制卷积,通过门控机制动态调整感受野范围在密集预测任务(检测/分割)上超越Swin Transformer


3. 简单CNN代码示例

*以下代码仅为CNN运用示例,未经任何调参、优化等处理。更合适你的模型请查阅论文(如上述论文)或其他资料

Tensorflow/Keras

import tensorflow as tf
import keras

# other platform
from tensorflow.keras.datasets import mnist
# macOS may
# from keras._tf_keras.keras.datasets import mnist


# 加载数据集
(X_train, y_train), (X_test, y_test) = mnist.load_data()

# 预处理:适配 CNN 输入。(还可以加上归一化)
X_train = X_train[..., tf.newaxis]
X_test = X_test[..., tf.newaxis]

# 模型
def cnn_net():
    return keras.Sequential([
        # 输入
        keras.layers.Input(shape=(28, 28, 1)),
        # 卷积
        keras.layers.Conv2D(filters=32, kernel_size=(3, 3), padding='same', strides=1, activation='relu'),
        keras.layers.Conv2D(filters=64, kernel_size=(2, 2), padding='same', strides=1, activation='relu'),
        # 最大池化
        keras.layers.MaxPool2D(pool_size=(2, 2)),

        # 卷积
        keras.layers.Conv2D(filters=32, kernel_size=(3, 3), padding='same', strides=1, activation='relu'),
        keras.layers.Conv2D(filters=64, kernel_size=(2, 2), padding='same', strides=1, activation='relu'),
        # 最大池化
        keras.layers.MaxPool2D(pool_size=(2, 2)),

        # 全连接
        keras.layers.Flatten(),
        # 输出
        keras.layers.Dense(10, activation='softmax')
    ])


model = cnn_net()

# 损失函数
loss = keras.losses.SparseCategoricalCrossentropy() 

# 优化器
optimizer = keras.optimizers.Adam() 

# 训练批次大小
batch_size = 32

# 训练批次
epoch_num = 5

# 编译模型
model.compile(
    optimizer=optimizer,
    loss=loss,
    metrics=['accuracy']
)

# 训练
history = model.fit(
    x=X_train,
    y=y_train,
    batch_size=batch_size,
    epochs=epoch_num,
    validation_data=(X_test, y_test)
)

#保存CNN模型
model.save("mnist_cnn.keras")

##
# Epoch 1/5
# 1875/1875 ━━━━━━━━━━━━━━━━━━━━ 24s 13ms/step - accuracy: 0.9114 - loss: 0.4192 - val_accuracy: 0.9839 - val_loss: 0.0490
# Epoch 2/5
# 1875/1875 ━━━━━━━━━━━━━━━━━━━━ 24s 13ms/step - accuracy: 0.9850 - loss: 0.0468 - val_accuracy: 0.9865 - val_loss: 0.0412
# Epoch 3/5
# 1875/1875 ━━━━━━━━━━━━━━━━━━━━ 26s 14ms/step - accuracy: 0.9892 - loss: 0.0351 - val_accuracy: 0.9890 - val_loss: 0.0305
# Epoch 4/5
# 1875/1875 ━━━━━━━━━━━━━━━━━━━━ 25s 13ms/step - accuracy: 0.9914 - loss: 0.0256 - val_accuracy: 0.9824 - val_loss: 0.0534
# Epoch 5/5
# 1875/1875 ━━━━━━━━━━━━━━━━━━━━ 25s 13ms/step - accuracy: 0.9917 - loss: 0.0247 - val_accuracy: 0.9886 - val_loss: 0.0390
# #

Pytorch

import torch
import torchvision
from torch.utils.data import DataLoader
from tqdm import tqdm

# 定义设备(GPU/CPU)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 下载训练数据
train_data = torchvision.datasets.MNIST(
    root='./data',
    train=True,
    download=True,
    transform=torchvision.transforms.ToTensor(),
)

# 加载测试数据
test_data = torchvision.datasets.MNIST(
    root='./data',
    train=False,
    transform=torchvision.transforms.ToTensor(),
)

# 批次大小
batch_size = 64

# 训练数据加载器
train_loader = DataLoader(
    dataset=train_data,
    batch_size=batch_size,
    shuffle=True,
    num_workers=2
)

# 测试数据加载器
test_loader = DataLoader(
    dataset=test_data,
    batch_size=batch_size,
    shuffle=False
)


# 模型
class CnnNet(torch.nn.Module):
    def __init__(self):
        super(CnnNet, self).__init__()

        # (卷积+激活)*2 + 最大池化
        self.conv1 = torch.nn.Sequential(
            torch.nn.Conv2d(in_channels=1, out_channels=32, kernel_size=(3, 3), stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.Conv2d(in_channels=32, out_channels=64, kernel_size=(3, 3), stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(kernel_size=2, stride=2),
        )

        self.conv2 = torch.nn.Sequential(
            torch.nn.Conv2d(in_channels=64, out_channels=128, kernel_size=(3, 3), stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.Conv2d(in_channels=128, out_channels=64, kernel_size=(3, 3), stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(kernel_size=2, stride=2),
        )

        self.classifier = torch.nn.Sequential(
            # 展平后输出
            torch.nn.Flatten(),
            torch.nn.Linear(in_features=64*7*7, out_features=256),
            torch.nn.ReLU(),
            torch.nn.Linear(in_features=256, out_features=128),
            torch.nn.ReLU(),
            torch.nn.Linear(in_features=128, out_features=10),
        )

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        output = self.classifier(x)
        return output


if __name__ == '__main__':

    model = CnnNet()
    print(model)
    model = model.to(device)  # 将模型移动到设备
    
    # 损失函数
    criterion = torch.nn.CrossEntropyLoss()
    # 优化器
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    # 训练轮次
    num_epoch = 5
    # 最高正确率
    best_val_acc = 0

    for epoch in range(num_epoch):
        # 训练阶段
        model.train()  # 设置为训练模式
        train_loss = 0.0
        train_correct = 0
        train_total = 0

        for inputs, labels in tqdm(train_loader, desc=f"Epoch {epoch + 1}/{num_epoch} [Train]"):
            inputs, labels = inputs.to(device), labels.to(device)

            optimizer.zero_grad()  # 清空梯度
            outputs = model(inputs)  # 前向传播
            loss = criterion(outputs, labels)  # 计算损失
            loss.backward()  # 反向传播
            optimizer.step()  # 更新参数

            # 统计训练结果
            train_loss += loss.item() * inputs.size(0)
            _, predicted = torch.max(outputs.data, 1)
            train_total += labels.size(0)
            train_correct += (predicted == labels).sum().item()

        train_loss = train_loss / len(train_loader.dataset)  # 平均训练损失
        train_acc = 100 * train_correct / train_total  # 训练准确率

        # 验证/测试阶段
        model.eval()  # 设置为评估模式
        val_loss = 0.0
        val_correct = 0
        val_total = 0

        with torch.no_grad():  # 关闭梯度计算
            for inputs, labels in tqdm(test_loader, desc=f"Epoch {epoch + 1}/{num_epoch} [Val]"):
                inputs, labels = inputs.to(device), labels.to(device)
                outputs = model(inputs)
                loss = criterion(outputs, labels)

                # 统计验证结果
                val_loss += loss.item() * inputs.size(0)
                _, predicted = torch.max(outputs.data, 1)
                val_total += labels.size(0)
                val_correct += (predicted == labels).sum().item()

        val_loss = val_loss / len(test_loader.dataset)  # 平均验证损失
        val_acc = 100 * val_correct / val_total  # 验证准确率

        # 打印结果
        print(f"Epoch {epoch + 1}/{num_epoch}: "
              f"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%, "
              f"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%")

        # 保存最佳模型
        if val_acc > best_val_acc:
            best_val_acc = val_acc
            torch.save(model.state_dict(), "best_model.pth")
            print(f"Saved best model with val acc {val_acc:.2f}%")


##
# Epoch 1/5 [Train]: 100%|██████████| 938/938 [01:35<00:00,  9.85it/s]
# Epoch 1/5 [Val]: 100%|██████████| 157/157 [00:04<00:00, 35.55it/s]
# Epoch 1/5: Train Loss: 1.6747, Train Acc: 78.58%, Val Loss: 1.5900, Val Acc: 87.09%
# Saved best model with val acc 87.09%
# Epoch 2/5 [Train]: 100%|██████████| 938/938 [01:36<00:00,  9.77it/s]
# Epoch 2/5 [Val]: 100%|██████████| 157/157 [00:04<00:00, 35.30it/s]
# Epoch 2/5: Train Loss: 1.5747, Train Acc: 88.60%, Val Loss: 1.5032, Val Acc: 95.77%
# Saved best model with val acc 95.77%
# Epoch 3/5 [Train]: 100%|██████████| 938/938 [01:36<00:00,  9.77it/s]
# Epoch 3/5 [Val]: 100%|██████████| 157/157 [00:04<00:00, 35.61it/s]
# Epoch 3/5: Train Loss: 1.5215, Train Acc: 93.95%, Val Loss: 1.5097, Val Acc: 95.14%
# Epoch 4/5 [Train]: 100%|██████████| 938/938 [01:36<00:00,  9.76it/s]
# Epoch 4/5 [Val]: 100%|██████████| 157/157 [00:04<00:00, 35.67it/s]
# Epoch 4/5: Train Loss: 1.5186, Train Acc: 94.24%, Val Loss: 1.5147, Val Acc: 94.64%
# Epoch 5/5 [Train]: 100%|██████████| 938/938 [01:35<00:00,  9.80it/s]
# Epoch 5/5 [Val]: 100%|██████████| 157/157 [00:04<00:00, 34.81it/s]
# Epoch 5/5: Train Loss: 1.5314, Train Acc: 92.97%, Val Loss: 1.5799, Val Acc: 88.11%
# #

4. Q&A

Q:卷积核的数量如何选择?

A:卷积核数量的选择需综合考虑任务复杂度、网络深度、计算资源和防止过拟合等因素:浅层网络或简单任务(如MNIST分类)通常从较少的卷积核(如32-64)开始,深层网络或复杂任务(如ImageNet分类)可逐层倍增(如64→128→256),通过递增设计逐步提取高阶特征;同时需平衡模型容量与计算开销,避免参数量过大导致显存不足或过拟合,必要时结合正则化或轻量化设计(如分组卷积)优化资源消耗。更多时候需要通过验证集调参确定最优值

Q:为什么卷积核数量(大部分)都是4的倍数?

A:卷积核数量常取4的倍数,主要源于硬件计算效率的优化需求:现代GPU的并行计算单元偏好4字节对齐的内存访问模式,通道数为4的倍数时,数据存取和矩阵分块运算(如4×4或8×8)能充分利用显存带宽与计算资源,减少冗余操作;同时,深度学习框架(如CuDNN)的底层加速算法针对此类数值设计了优化路径。这一惯例虽非强制,但遵循可提升计算吞吐量,实际设计时仍需结合硬件特性灵活调整。

Q:卷积层的padding如何选择?

A:padding通常被设置为能确保输出尺寸与输入层保持一致的值,主要避免特征图尺寸失控,防止影响后续网络层的计算。如不设置padding(不作填充),可以减少计算量,缩小特征图尺寸,但通常有其他更保险方式减少计算量(如Dropout)。

Q:卷积层的stride如何选择?

A:stride的不同设置可以适用不同场景。通常情况下,小步长(如1或2)保留更多空间细节,常用于浅层或密集预测任务;大步长(如2或更大)加速下采样,降低计算成本,但可能丢失细粒度特征,多用于分类网络深层。多数情况下,根据平衡特征保留与计算效率,来选择大小。在实践中,更合适的步长,还需通过实验比较来确定。

Q:池化层是必须的吗?

A:池化层并非必须,现代网络常通过带步长的卷积(如stride=2)实现下采样,但传统设计中池化(如最大池化)能高效压缩特征图尺寸并增强平移不变性,选择需结合任务需求和网络结构权衡。一般来说,对计算敏感或需强正则化时选池化,追求特征丰富性或端到端优化时优先步长卷积,也可混合使用(如浅层池化+深层步长卷积),实践中,哪一种方式更合适,需通过试验确定。

Q:全连接层是必须的吗?

全连接层的主要作用在于展平特征图。一般情况下,CNN模型中全连接并非必须,但在卷积池化的最后,特征图的展平是必须的。现代网络常以全局平均池化(GAP)或全卷积结构替代全连接层,同样实现展平效果,同时减少参数量并增强空间鲁棒性。但传统分类任务中仍可保留以聚合高层语义特征。