机器学习数学基础专题【左扬精讲】—— 卷积神经网络基础详解:从卷积运算到 CNN 原理

机器学习数学基础专题【左扬精讲】—— 卷积神经网络基础详解:从卷积运算到 CNN 原理

在深度学习的入门旅程中,卷积神经网络(Convolutional Neural Network,CNN)是每一位学习者必须掌握的核心概念。相比于全连接神经网络,CNN 在处理图像、视频等高维数据时展现出卓越的性能和效率。本篇文章将带领大家从零开始,系统性地理解卷积运算的基本概念、PyTorch 中的卷积实现、池化操作以及 CNN 的整体架构设计。

本篇文章属于机器学习数学基础专题,专注于打好 CNN 的理论基础。通过本文的学习,你将理解卷积运算的数学本质、掌握 PyTorch 中卷积 API 的使用规范,并为后续的 MNIST 手写体识别实战做好充分准备。

torch.nn.Conv2d              ← 2D 卷积层(图像处理核心)
torch.nn.MaxPool2d           ← 最大值池化层
torch.nn.AvgPool2d           ← 平均值池化层
torch.nn.AdaptiveAvgPool2d   ← 自适应池化层
torch.nn.Softmax             ← Softmax 激活函数
torch.nn.Sequential          ← 容器:按顺序堆叠网络层

PyTorch卷积神经网络CNN池化层SoftmaxMNIST

学习重点

  • 必须掌握
  • 卷积运算的数学定义:滑动窗口、局部连接、权重共享
  • PyTorch 中 torch.nn.Conv2d 的参数含义与使用方式
  • 卷积输出尺寸计算公式:Output = (Input - Kernel + 2*Padding) / Stride + 1
  • 池化层的降采样作用与 MaxPool2d 的使用
  • 理解即可
  • 深度可分离卷积的设计动机(后续文章详解)
  • 空洞卷积(Dilated Convolution)的感受野扩展原理

目录


一、卷积运算的基本概念

What — 卷积运算是怎么回事?

卷积(Convolution)是卷积神经网络的核心运算,它本质上是一种滑动窗口加权求和的操作。与全连接层每个神经元都与上一层所有神经元相连不同,卷积层中的每个神经元只与上一层的一个局部区域相连,这个局部区域被称为感受野(Receptive Field)

卷积运算的核心要素包括:

  • 输入特征图(Input Feature Map):通常是三维张量,形状为 (C_in, H, W),其中 C_in 是通道数,H 和 W 分别是高度和宽度
  • 卷积核(Kernel 或 Filter):也是三维张量,形状为 (C_in, K_h, K_w),其中 K_h 和 K_w 是卷积核的高度和宽度
  • 输出特征图(Output Feature Map):二维张量,形状为 (H_out, W_out)
  • 滑动步长(Stride):卷积核在输入上滑动的间隔
  • 填充(Padding):在输入边缘填充的像素数

Why — 为什么需要卷积运算?

问题一:图像数据具有局部相关性。在图像中,相邻像素之间的关系往往比远距离像素之间的关系更重要。例如,识别一张人脸,眼睛、鼻子、嘴巴等局部特征比随机选取的像素组合更有意义。卷积操作通过局部连接正好捕捉这种局部相关性。

问题二:参数效率问题。如果使用全连接层处理一张 224x224x3 的彩色图像,第一层就需要 224×224×3 × 神经元数 个权重参数量巨大。卷积层通过权重共享机制,大大减少了需要学习的参数数量。

问题三:平移不变性。由于卷积核在整张图像上滑动,同一个特征(无论出现在图像的哪个位置)都会被同一个卷积核检测到,这就赋予了 CNN 平移不变性。

没有卷积会发生什么?

  • 图像处理的参数量会爆炸,训练成本极高
  • 无法有效捕捉局部特征,模型性能大幅下降
  • 失去平移不变性,同一物体在不同位置需要重新学习
卷积运算的数学定义与计算示例

卷积运算的数学公式定义如下:

# 二维卷积的数学定义
# y[i, j] = Σ Σ x[i+s*m, j+s*n] * k[m, n]
# 其中:
#   y[i,j] 是输出特征图在位置(i,j)的值
#   x 是输入特征图
#   k 是卷积核
#   s 是步长(stride)
#   m,n 是卷积核的索引

让我们用一个具体的数值示例来理解卷积运算:

# 示例:5x5 输入与 3x3 卷积核的卷积运算
# 输入特征图 (5行5列)
# [[1, 2, 3, 0, 0],
#  [0, 1, 2, 3, 0],
#  [0, 0, 1, 2, 3],
#  [0, 0, 0, 1, 2],
#  [0, 0, 0, 0, 1]]

# 卷积核 (3行3列)
# [[1, 0, 1],
#  [0, 1, 0],
#  [1, 0, 1]]

# 计算输出[0,0]位置(无填充,步长=1)
# y[0,0] = 1*1 + 2*0 + 3*1 + 0*0 + 1*1 + 2*0 + 0*1 + 0*0 + 1*1
#        = 1 + 0 + 3 + 0 + 1 + 0 + 0 + 0 + 1
#        = 6

在这个示例中,卷积核在输入特征图上按照从左到右、从上到下的顺序滑动,每滑动一次就计算一次加权和。需要注意的是,卷积核的数值是需要通过网络学习得到的,初始时通常是随机初始化的。

卷积运算的可视化理解

为了更直观地理解卷积运算,我们可以将其想象为"特征提取器"。卷积核就像是一个"透镜",当它在图像上滑动时,会检测是否存在某种特定的局部模式。不同的卷积核可以检测不同的特征:

  • 边缘检测卷积核:可以检测图像中的垂直或水平边缘
  • 模糊卷积核:可以对图像进行平滑处理
  • 锐化卷积核:可以增强图像的细节
# 常见的边缘检测卷积核示例(用于理解卷积作用,非实际网络参数)
import torch

# 垂直边缘检测卷积核(检测垂直方向的灰度变化)
vertical_edge_kernel = torch.tensor([
    [-1, 0, 1],
    [-2, 0, 2],
    [-1, 0, 1]
], dtype=torch.float32).unsqueeze(0)  # 添加 batch 维度

# 水平边缘检测卷积核
horizontal_edge_kernel = torch.tensor([
    [-1, -2, -1],
    [0, 0, 0],
    [1, 2, 1]
], dtype=torch.float32).unsqueeze(0)

print("垂直边缘检测核形状:", vertical_edge_kernel.shape)  # torch.Size([1, 3, 3])
print("水平边缘检测核形状:", horizontal_edge_kernel.shape)  # torch.Size([1, 3, 3])

本章小结

  • 卷积本质:滑动窗口加权求和,是局部连接和权重共享的数学表达
  • 核心要素:输入特征图、卷积核、步长、填充构成卷积运算的基本参数
  • 局部连接:每个输出神经元只与输入的局部区域相连,捕捉局部相关性
  • 权重共享:同一个卷积核在整个输入上共享权重,大大减少参数量
  • 平移不变性:特征检测与位置无关,同一特征在不同位置都能被检测

二、PyTorch 中 Conv2d 的实现详解

What — torch.nn.Conv2d 是什么?

torch.nn.Conv2d 是 PyTorch 框架中实现二维卷积运算的核心类。它封装了卷积神经网络中图像(或其他二维信号)处理所需的全部逻辑,包括前向传播计算、梯度反向传播以及参数管理。

查看 PyTorch 官方文档,Conv2d 的基本参数定义如下:

  • in_channels:输入特征图的通道数(如 RGB 图像为 3)
  • out_channels:输出特征图的通道数(即卷积核的数量)
  • kernel_size:卷积核的尺寸,可以是整数或元组
  • stride:步长,控制卷积核滑动的间隔
  • padding:填充,在输入边缘填充的像素数
  • dilation:膨胀系数,控制卷积核内部采样间隔
  • groups:分组卷积的组数
  • bias:是否添加偏置项

Why — 为什么需要这些参数?

问题一:如何控制输出尺寸? 输出特征图的尺寸取决于输入尺寸、卷积核尺寸、步长和填充。PyTorch 通过 stridepadding 参数让我们灵活控制输出尺寸。

问题二:如何提取多层次特征? 通过 out_channels,我们可以设置多个卷积核同时提取不同的特征。例如第一层可能同时提取边缘、纹理、颜色等多种特征。

问题三:如何实现更高效的卷积变体? dilationgroups 参数支持空洞卷积和分组卷积等高级变体,这些在现代 CNN 架构中广泛应用。

没有这些参数控制会发生什么?

  • 无法灵活调整输出特征图尺寸
  • 只能提取单一类型的特征
  • 无法实现深度可分离卷积等高效架构
标准写法:Conv2d 的基本使用

让我们通过完整的代码示例来理解 Conv2d 的标准用法:

import torch
import torch.nn as nn

# 创建一个 Conv2d 层:输入3通道,输出16通道,卷积核3x3
# 这是 CNN 中最常见的配置
conv_layer = nn.Conv2d(
    in_channels=3,      # 输入通道数(RGB图像为3,灰度图为1)
    out_channels=16,    # 输出通道数 = 卷积核数量 = 输出特征图通道数
    kernel_size=3,      # 卷积核尺寸,3表示3x3
    stride=1,           # 步长,默认值为1
    padding=0,          # 填充,默认值为0(valid卷积)
    bias=True           # 是否添加偏置,默认True
)

# 创建一个模拟输入:B=1张图,C=3通道,H=32,W=32
batch_size = 1
input_tensor = torch.randn(batch_size, 3, 32, 32)

# 执行前向传播
output_tensor = conv_layer(input_tensor)

# 查看输入输出形状
print("输入形状:", input_tensor.shape)    # torch.Size([1, 3, 32, 32])
print("输出形状:", output_tensor.shape)   # torch.Size([1, 16, 30, 30])
# 输出30的原因:(32 - 3 + 0) / 1 + 1 = 30

在上面的示例中,输入是 32x32 的 RGB 图像,经过 3x3 卷积后变成 30x30 的特征图,通道数从 3 变成 16。这是因为每个卷积核会产生一个通道的输出,16 个卷积核就产生 16 个通道。

关键参数详解:输出尺寸计算

卷积输出尺寸的计算是深度学习中最常用的公式之一:

# 卷积输出尺寸公式
# H_out = floor((H_in + 2*padding - dilation*(kernel_size-1) - 1) / stride + 1)
# W_out = floor((W_in + 2*padding - dilation*(kernel_size-1) - 1) / stride + 1)

# 示例1:valid卷积(无填充)
conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=0)
# 输出尺寸:(32 - 3) / 1 + 1 = 30

# 示例2:same卷积(输出尺寸等于输入)
conv2 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1)
# 输出尺寸:(32 + 2*1 - 3) / 1 + 1 = 32

# 示例3:步长为2的卷积(尺寸减半)
conv3 = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1)
# 输出尺寸:(32 + 2*1 - 3) / 2 + 1 = 16

# 示例4:空洞卷积(dilation=2)
conv4 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=0, dilation=2)
# 输出尺寸:(32 + 0 - 2*(3-1) - 1) / 1 + 1 = (32 - 4 - 1 + 1) = 28
# 感受野相当于 5x5 的卷积核但只用了 3x3 的参数量
注意事项:分组卷积与深度卷积

groups 参数实现分组卷积,是深度可分离卷积的基础:

# 普通卷积:所有输入通道与所有输出通道全连接
# 输出通道数 = in_channels (必须整除 groups)

# 示例:输入32通道,输出64通道,分为8组
# 每组:in=4, out=8, 共8组
# 总参数量 = 8 * (4 * 3 * 3 * 8) = 8组 * 每组参数量
conv_grouped = nn.Conv2d(
    in_channels=32,
    out_channels=64,
    kernel_size=3,
    groups=8  # 分成8组,每组4个输入通道、8个输出通道
)

# groups=in_channels 时就是深度卷积(Depthwise Convolution)
# 常用于 MobileNet 等轻量级网络
conv_depthwise = nn.Conv2d(
    in_channels=32,
    out_channels=32,    # 深度卷积要求 out_channels == in_channels
    kernel_size=3,
    groups=32  # 每组只有一个输入通道,分别处理每个通道
)

# 深度卷积的输出通道数等于输入通道数
# 只进行通道内的空间卷积,不混合不同通道的信息

本章小结

  • Conv2d 核心参数in_channelsout_channelskernel_sizestridepadding
  • 输出尺寸公式H_out = (H_in + 2*padding - kernel_size) / stride + 1
  • 分组卷积groups 参数实现通道分组,groups=in_channels 时为深度卷积
  • 空洞卷积dilation 参数在不增加参数量的前提下增大感受野

三、池化运算详解

What — 池化运算是什么?

池化(Pooling)是卷积神经网络中的一种下采样操作,用于减小特征图的空间尺寸,同时保留重要的特征信息。池化操作通过对输入特征图的局部区域进行汇总,生成更小尺寸的特征图。

常见的池化类型包括:

  • 最大池化(Max Pooling):取局部区域中的最大值,代表该区域最显著的特征
  • 平均池化(Average Pooling):取局部区域的平均值,更平滑但可能模糊细节
  • 自适应池化(Adaptive Pooling):输出固定尺寸的特征图,自动计算所需步长

Why — 为什么需要池化操作?

问题一:减少计算量和内存占用。池化可以将特征图尺寸减半,使后续层的计算量大幅减少。例如 4x4 的特征图经过 2x2 最大池化后变成 2x2,参数减少到原来的 1/4。

问题二:提供平移不变性。池化对局部区域的汇总操作使特征对小幅平移、旋转更加鲁棒。即使目标略微移动,最大池化仍能捕捉到该特征。

问题三:防止过拟合。通过减小特征图尺寸,池化减少了后续层的参数数量,降低了模型复杂度,有助于防止过拟合。

没有池化会发生什么?

  • 特征图尺寸保持不变,计算量巨大,训练速度极慢
  • 模型对输入的小幅变化过于敏感,泛化能力差
  • 参数量难以控制,容易过拟合
PyTorch 池化层的标准写法

让我们通过代码详细理解各种池化操作的使用:

import torch
import torch.nn as nn

# 创建测试输入:1张图,16通道,8x8特征图
x = torch.randn(1, 16, 8, 8)

# 最大池化:2x2窗口,步长2(默认等于窗口大小)
max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
output_max = max_pool(x)
print("最大池化输出形状:", output_max.shape)  # torch.Size([1, 16, 4, 4])
# 尺寸从 8x8 变为 4x4

# 平均池化
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
output_avg = avg_pool(x)
print("平均池化输出形状:", output_avg.shape)  # torch.Size([1, 16, 4, 4])

# 自适应池化:无论输入尺寸多大,输出固定 3x3
adaptive_pool = nn.AdaptiveAvgPool2d(output_size=(3, 3))
output_adaptive = adaptive_pool(x)
print("自适应池化输出形状:", output_adaptive.shape)  # torch.Size([1, 16, 3, 3])
池化运算的具体计算过程

通过具体的数值示例理解池化的计算方式:

# 最大池化计算示例
# 假设有一个 4x4 的输入区域
input_4x4 = torch.tensor([
    [1, 5, 2, 6],
    [3, 7, 4, 8],
    [9, 13, 10, 14],
    [11, 15, 12, 16]
], dtype=torch.float32).unsqueeze(0).unsqueeze(0)  # [1,1,4,4]

# 2x2 最大池化,步长为2
max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
output = max_pool(input_4x4)
print("最大池化结果:", output.squeeze().tolist())
# 输出: [[7.0, 8.0], [15.0, 16.0]]
# 解释:
# - 左上 2x2: max(1,5,3,7) = 7
# - 右上 2x2: max(2,6,4,8) = 8
# - 左下 2x2: max(9,13,11,15) = 15
# - 右下 2x2: max(10,14,12,16) = 16

# 平均池化计算示例
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
output_avg = avg_pool(input_4x4)
print("平均池化结果:", output_avg.squeeze().tolist())
# 输出: [[4.0, 5.0], [12.0, 13.0]]
# 解释:(1+5+3+7)/4 = 4, (2+6+4+8)/4 = 5, 以此类推

最大池化 vs 平均池化

在大多数视觉任务中,最大池化比平均池化效果更好。原因在于:最大池化保留了最显著的特征(激活值最大的神经元),这通常对应于图像中最重要的信息,如边缘、角点等。而平均池化会将所有值平均化,可能稀释这些关键信息。

当然,在某些场景下平均池化也有其优势,例如需要保留更多背景信息时。

本章小结

  • 池化类型:最大池化保留显著特征,平均池化保留全局信息
  • 下采样作用:减小特征图尺寸,降低计算量和参数量
  • 平移不变性:增强模型对输入小幅变化的鲁棒性
  • 自适应池化:无论输入尺寸多大,输出固定尺寸

四、Softmax 激活函数

What — Softmax 是什么?

Softmax 函数是深度学习中用于多分类问题的输出层激活函数。它将一个 K 维的实数向量映射为一个 K 维的概率分布,所有输出值都在 (0, 1) 区间内,且和为 1。这使得 Softmax 的输出可以直接解释为各类别的预测概率。

Softmax 函数的数学定义:

# Softmax 公式
# softmax(x_i) = exp(x_i) / Σ exp(x_j)
# 其中 j 遍历所有类别

# 示例:3个类别的 logits
logits = torch.tensor([2.0, 1.0, 0.1])
# 计算 Softmax
probabilities = torch.softmax(logits, dim=0)
print("概率分布:", probabilities)
# 输出: tensor([0.6590, 0.2424, 0.0986])
# 验证:0.6590 + 0.2424 + 0.0986 ≈ 1.0

Why — 为什么分类问题需要 Softmax?

问题一:如何将实数输出转换为概率? 神经网络的最后一层输出是任意实数(logits),但我们需要一个概率分布来解释预测结果。Softmax 通过指数函数的非线性变换,将 logits 转换为正数,再通过归一化得到概率分布。

问题二:为什么用指数函数? 指数函数有两个重要特性:1) 总是产生正数;2) 对较大值的放大效果更显著。这意味着 Softmax 会更"自信"地给最高 logit 分配更高的概率。

问题三:如何计算分类损失? Softmax 通常与交叉熵损失(CrossEntropyLoss)配合使用,后者天然地基于 Softmax 的概率输出计算损失。

没有 Softmax 会发生什么?

  • 输出无法解释为概率,模型无法进行分类决策
  • 无法计算有意义的分类损失函数
  • 多分类问题无法端到端解决
PyTorch 中 Softmax 的使用

在 PyTorch 中,Softmax 有两种使用方式:

import torch
import torch.nn as nn

# 方法1:使用 nn.Softmax 模块
softmax = nn.Softmax(dim=1)  # 在通道维度上计算 Softmax
logits = torch.randn(10, 5)  # 10个样本,5个类别
probabilities = softmax(logits)
print("各样本概率和:", probabilities.sum(dim=1))  # tensor([1.0000, 1.0000, ...])

# 方法2:使用 F.softmax 函数(更常用)
from torch.nn import functional as F
probabilities = F.softmax(logits, dim=1)

# 方法3:使用 CrossEntropyLoss(推荐,直接内置 Softmax + 交叉熵)
# 注意:CrossEntropyLoss 输入的是原始 logits,不需要预先 Softmax
criterion = nn.CrossEntropyLoss()
# 假设有10个样本的真实标签
labels = torch.randint(0, 5, (10,))  # 随机生成0-4的标签
loss = criterion(logits, labels)
print("交叉熵损失:", loss.item())

Softmax 与 Sigmoid 的区别

Softmax 用于多分类问题,所有类别的概率之和为 1,一个样本只能属于一个类别。而 Sigmoid 用于多标签问题,每个类别的概率独立,概率之和不一定为 1,一个样本可以同时属于多个类别。

例如,对于一张包含猫和狗的图像,多标签分类会用两个 Sigmoid 分别输出 P(猫) 和 P(狗);多分类则用 Softmax 输出 P(猫) 和 P(狗),但两者概率和为 1,表示"最可能是猫还是狗"。

本章小结

  • Softmax 作用:将 logits 转换为概率分布,用于多分类问题
  • 公式softmax(x_i) = exp(x_i) / Σexp(x_j)
  • dim 参数:指定在哪个维度上计算 Softmax,通常是最后一维
  • 交叉熵损失:通常与 nn.CrossEntropyLoss 配合使用

五、卷积神经网络的原理

What — CNN 的整体架构是怎样的?

卷积神经网络(CNN)是一类专门用于处理具有网格结构数据(如图像)的深度神经网络。一个典型的 CNN 由多种不同类型的层堆叠而成,形成一个端到端的学习系统。

CNN 的核心架构组件包括:

  • 卷积层(Convolutional Layer):提取局部特征
  • 池化层(Pooling Layer):减小尺寸,增强鲁棒性
  • 激活函数层(Activation Layer):引入非线性
  • 全连接层(Fully Connected Layer):整合特征,进行分类或回归
  • 批归一化层(Batch Normalization):稳定训练过程

Why — 为什么 CNN 能有效处理图像?

问题一:层次化特征学习。CNN 通过层层堆叠,自动学习从低级到高级的特征表示。底层网络学习边缘、纹理等简单特征,中层学习局部模式,高层学习整体语义。这种层次化结构与人类视觉皮层的组织方式相似。

问题二:空间层次结构的保持。与将图像展平为向量输入全连接层不同,CNN 保持了图像的二维空间结构,使网络能够学习空间位置相关的特征。

问题三:参数效率与泛化能力。权重共享机制使 CNN 用很少的参数就能处理任意尺寸的图像,且对输入的空间变化具有天然的鲁棒性。

没有 CNN 会发生什么?

  • 图像处理必须使用全连接层,参数量爆炸
  • 无法有效学习层次化特征,模型性能严重下降
  • 对图像的平移、旋转等变化非常敏感
典型 CNN 架构:LeNet-5

让我们通过经典的 LeNet-5 网络理解 CNN 的完整结构:

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

class LeNet5(nn.Module):
    """经典的 LeNet-5 架构,用于 MNIST 手写数字识别"""
    def __init__(self, num_classes=10):
        super(LeNet5, self).__init__()
        # 第一个卷积块:C1 卷积层
        # 输入: 1x28x28 (MNIST 图像)
        # 输出: 6x28x28 (6个通道,保持尺寸)
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5, padding=2)
        # 第一个池化层:S2
        # 输入: 6x28x28 -> 输出: 6x14x14 (减半)
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
        # 第二个卷积块:C3
        # 输入: 6x14x14 -> 输出: 16x10x10
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
        # 第二个池化层:S4
        # 输入: 16x10x10 -> 输出: 16x5x5
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
        # 全连接层:F5
        # 输入: 16*5*5=400 -> 输出: 120
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        # 全连接层:F6
        # 输入: 120 -> 输出: 84
        self.fc2 = nn.Linear(120, 84)
        # 输出层:F7
        # 输入: 84 -> 输出: 10 (类别数)
        self.fc3 = nn.Linear(84, num_classes)

    def forward(self, x):
        # 卷积 -> 激活 -> 池化
        x = self.pool1(F.relu(self.conv1(x)))
        x = self.pool2(F.relu(self.conv2(x)))
        # 展平操作:将特征图展平为一维向量
        x = x.view(-1, 16 * 5 * 5)
        # 全连接层
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

# 创建模型实例
model = LeNet5(num_classes=10)
print(model)
print("参数量:", sum(p.numel() for p in model.parameters()))
CNN 的数据流动过程

让我们追踪一张 MNIST 图像在 LeNet-5 中的处理过程:

import torch

# 创建模型和输入
model = LeNet5(num_classes=10)
x = torch.randn(1, 1, 28, 28)  # 一张 28x28 的灰度图像

print("=" * 50)
print("数据流动过程(LeNet-5)")
print("=" * 50)

# 输入层
print(f"输入形状: {x.shape}")  # torch.Size([1, 1, 28, 28])

# 第一层:卷积 + ReLU + 池化
x1 = model.conv1(x)
x1_act = torch.relu(x1)
x1_pool = model.pool1(x1_act)
print(f"Conv1 输出: {x1.shape} -> ReLU: {x1_act.shape} -> Pool1: {x1_pool.shape}")

# 第二层:卷积 + ReLU + 池化
x2 = model.conv2(x1_pool)
x2_act = torch.relu(x2)
x2_pool = model.pool2(x2_act)
print(f"Conv2 输出: {x2.shape} -> ReLU: {x2_act.shape} -> Pool2: {x2_pool.shape}")

# 展平
x_flat = x2_pool.view(-1, 16 * 5 * 5)
print(f"展平后: {x_flat.shape}")

# 全连接层
x_fc1 = torch.relu(model.fc1(x_flat))
print(f"FC1 输出: {x_fc1.shape}")

x_fc2 = torch.relu(model.fc2(x_fc1))
print(f"FC2 输出: {x_fc2.shape}")

x_fc3 = model.fc3(x_fc2)
print(f"FC3 (输出层) 输出: {x_fc3.shape}")  # torch.Size([1, 10]),10个类别的 logits

# Softmax 得到概率
probs = torch.softmax(x_fc3, dim=1)
print(f"预测概率: {probs.squeeze()}")
现代 CNN 架构的演进

从 LeNet-5 到现代架构,CNN 经历了显著的演进:

  • AlexNet (2012):更深网络,首次在 ImageNet 大赛取得突破性成绩
  • VGGNet (2014):全部使用 3x3 卷积核,证明了深度的重要性
  • GoogLeNet (2014):引入 Inception 模块,平行多尺度卷积
  • ResNet (2015):引入残差连接,解决了深层网络训练困难的问题
  • MobileNet (2017):深度可分离卷积,轻量化网络
# ResNet 的残差连接核心思想
class ResidualBlock(nn.Module):
    """残差块:通过跳跃连接解决深层网络训练问题"""
    def __init__(self, in_channels, out_channels, stride=1):
        super(ResidualBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        # 跳跃连接(Shortcut)
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, 1, stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        out = torch.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        # 核心:F(x) + x 的残差连接
        out += self.shortcut(x)
        out = torch.relu(out)
        return out

本章小结

  • CNN 组成:卷积层 + 池化层 + 激活函数 + 全连接层的层次化堆叠
  • 特征层次:底层提取边缘纹理,高层提取语义特征
  • 权重共享:大大减少参数量,提高泛化能力
  • 现代架构:ResNet 的残差连接、MobileNet 的深度可分离卷积等创新

六、FAQ(20 组)

FAQ — 精选 20 问,深入理解卷积神经网络的基础概念

Q1. 卷积和互相关有什么区别?

在深度学习中通常说的"卷积"实际上是互相关(Cross-Correlation)。数学上卷积需要将卷积核翻转 180 度,但深度学习框架实现的是互相关(不需要翻转)。由于网络会学习卷积核的权重,翻转与否只是改变了初始值,最终学到的权重会补偿这个差异。

Q2. 为什么卷积核通常是奇数尺寸(如 3x3、5x5)?

奇数尺寸的卷积核有两个优点:1) 有中心像素,便于确定锚点;2) 可以使用 same padding 保持尺寸。偶数尺寸的卷积核没有明确的中心,在进行空间变换时会有歧义。3x3 是最常用的尺寸,因为它在参数量和感受野之间取得了良好平衡。

Q3. 卷积层和全连接层各有什么优缺点?

卷积层参数少、保持空间结构、具平移不变性;全连接层表达能力更强但参数量大。卷积层通过局部连接和权重共享大大减少参数,适合处理图像等高维数据。全连接层可以将所有特征整合,常用于网络的最后几层进行分类决策。

Q4. 如何计算 Conv2d 的参数量?

Conv2d 参数量 = (in_channels × out_channels × kernel_h × kernel_w) + out_channels(偏置)。例如 in_channels=3, out_channels=16, kernel_size=3x3,则参数量 = 3×16×3×3 + 16 = 432。分组卷积时,每组内部独立计算再求和。

Q5. 什么是感受野(Receptive Field)?

感受野是指输出特征图上的一个像素对应于输入图像的区域大小。随着网络层数增加,感受野逐渐扩大。深层神经元的感受野覆盖了更大的输入区域,可以获取更全局的信息。例如 3 个 3x3 卷积堆叠的感受野等于一个 7x7 卷积,但参数量更少。

Q6. padding="same" 和 padding="valid" 有什么区别?

padding="valid" 不填充,输出尺寸减小;padding="same" 填充使输出尺寸等于输入尺寸。PyTorch 中 padding=0 对应 valid,padding=k(k = (kernel_size-1)/2)对应 same。使用 same padding 时,卷积核在每个方向上向外扩展 k/2 个像素。

Q7. stride=2 的卷积和 2x2 的池化层有何区别?

stride=2 的卷积可以学习特征变换,池化只是固定的下采样。两者都能将尺寸减半,但卷积层有权重可以学习,而池化层没有参数。stride=2 的卷积在减小尺寸的同时还能提取特征,效果通常优于池化,这也是现代网络倾向于用 stride 卷积替代池化的原因。

Q8. 为什么 ReLU 比 Sigmoid 更常用?

ReLU 计算简单、梯度大、能缓解梯度消失问题,但可能导致"神经元死亡"。Sigmoid 输出在 (0,1),梯度在两端接近 0,反向传播时梯度很小(梯度消失),训练慢。ReLU 在正区间的梯度恒为 1,计算高效。但负区间的梯度为 0,可能导致神经元永远不激活。

Q9. 批归一化(BatchNorm)在 CNN 中起什么作用?

BatchNorm 通过规范化每个 batch 的均值和方差,稳定网络训练、加速收敛。它对每个通道在整个 batch 和空间维度上计算均值方差,然后进行标准化,再通过可学习的 scale 和 shift 恢复表达能力。推理时使用训练时统计的移动平均均值方差。

Q10. 为什么 CNN 需要非线性激活函数?

没有非线性激活,多层卷积堆叠等价于单层线性变换,无法学习复杂模式。卷积操作本质是矩阵乘法(线性变换),多层线性变换仍可化简为单层线性变换。ReLU 等非线性激活打破这种线性,使网络能学习任意复杂的非线性函数。

Q11. 1x1 卷积核有什么作用?

1x1 卷积核可以实现通道间的信息交互和维度变换,同时保持空间尺寸。它只看每个像素位置的不同通道,进行加权求和。这类似于全连接层在每个空间位置上独立作用。1x1 卷积常用于压缩通道数(如 ResNet 的 bottleneck)或增加非线性。

Q12. 空洞卷积(Dilation Convolution)的作用是什么?

空洞卷积通过在卷积核内部插入间隔,在不增加参数量的情况下增大感受野。dilation=2 的 3x3 卷积核实际覆盖 5x5 的区域,但只使用了 9 个参数。这在语义分割等需要大感受野的任务中非常有用,可以增大特征图的感受野而不损失分辨率。

Q13. 分组卷积(Group Convolution)是什么?

分组卷积将输入通道和输出通道分成多组,每组独立进行卷积,减少参数量。当 groups=2 时,输入通道分成 2 组,每组独立卷积后拼接。groups=in_channels 时为深度卷积(Depthwise),每个输入通道独立处理。分组卷积的参数量是普通卷积的 1/groups。

Q14. 为什么深度可分离卷积(Depthwise Separable Convolution)效率高?

深度可分离卷积将空间卷积和通道卷积分开计算,大幅减少参数量和计算量。标准卷积一次性处理所有通道,深度可分离卷积先用深度卷积处理空间信息(逐通道),再用 1x1 卷积混合通道信息。MobileNet 正是基于此设计,实现了高效轻量级网络。

Q15. 卷积神经网络的平移不变性是如何实现的?

平移不变性来自卷积的滑动窗口机制和池化操作。由于卷积核对整个图像共享权重,同一特征无论出现在哪里都会被检测到。池化操作对局部区域进行汇总,进一步增强了抗平移能力。输入图像平移几个像素,特征图也会相应平移,但提取的特征不变。

Q16. 为什么 MobileNet 使用 ReLU6 而不是普通 ReLU?

ReLU6 将最大输出值限制为 6,增强了在低精度量化下的鲁棒性。在移动端部署时,权重常被量化到 8 位整数。ReLU6 = min(max(x, 0), 6) 将激活值限制在 [0, 6] 范围,使量化误差对网络性能的影响更小。这是专为移动端优化的设计。

Q17. 什么是特征图(Feature Map)?

特征图是卷积层输出的多通道二维数据,每一通道对应一个特征检测结果。输入图像的每个通道代表原始颜色信息(RGB)。经过卷积后,每个输出通道由一个卷积核产生,表示该卷积核检测到的特征在空间上的分布。深层特征图通道数多但空间尺寸小。

Q18. 为什么 ResNet 的残差连接能帮助训练深层网络?

残差连接让网络学习 F(x) = H(x) - x(残差),简化了学习目标。如果恒等映射是最优解,网络只需将 F(x) 推向零即可,而不是学习复杂的恒等变换。残差连接还缓解了梯度消失问题,使深层网络的端到端训练成为可能。

Q19. 什么是通道(Channel)和特征通道?

通道是特征图的第三维(除高度和宽度外),每个通道存储一类特征信息。输入图像的通道代表颜色(R/G/B)。卷积层输出的通道数等于使用的卷积核数量,每个卷积核检测一种特征(如边缘、纹理)。深层网络的通道数通常较多,表示提取了多种抽象特征。

Q20. 为什么 CNN 的特征图尺寸通常是 32 的倍数?

32 是 2 的 5 次方,经过多次 2 倍下采样后仍是整数,便于 GPU 高效计算。图像通常从 224x224 或 32x32 开始,经过多个 stride=2 的层后,32 的倍数能保证每个尺寸都是整数。现代 GPU 的内存对齐和并行计算也针对 32 的倍数做了优化。

FAQ 总结

  • 卷积基础:局部连接 + 权重共享 = 参数高效 + 平移不变性
  • PyTorch APIConv2d 参数(in/out_channels, kernel_size, stride, padding)
  • 池化作用:下采样 + 降计算量 + 增强鲁棒性
  • CNN 架构:卷积层 + 激活 + 池化 + 全连接层的层次化组合

七、Roadmap 预告

下篇预告:《基于卷积的 MNIST 手写体分类实战》

在下一篇实战文章中,我们将基于本文学习的卷积神经网络基础知识,从零开始构建一个 MNIST 手写数字识别系统:

  • MNIST 数据集的下载、加载与预处理
  • 基于 torch.nn.Conv2d 构建 CNN 模型
  • 训练循环与验证流程的完整实现
  • 模型评估与预测结果可视化

敬请期待!


posted @ 2026-07-15 15:16  左扬  阅读(24)  评论(0)    收藏  举报