查看全课大纲(第 10 / 40 节)
- 1.从规则到学习:深度学习解决什么问题
- 2.表示学习与端到端训练
- 3.张量、计算图与自动微分
- 4.训练三要素:数据、模型、目标函数
- 5.从感知机到多层感知机
- 6.反向传播:手推一个两层网络
- 7.激活函数与初始化
- 8.优化器:SGD → Momentum → Adam
- 9.正则化:权重衰减、Dropout、BN/LN
- 10.卷积的直觉与参数共享
- 11.池化、感受野与下采样
- 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
- 13.残差连接与归一化为什么有效
- 14.迁移学习与特征可视化
- 15.RNN 与 BPTT
- 16.LSTM/GRU:门控如何缓解梯度问题
- 17.Seq2Seq 与编码器-解码器
- 18.注意力前史:Bahdanau 加性注意力
- 19.Self-Attention:QKV 与缩放点积
- 20.多头注意力:并行的多种关系
- 21.位置编码:正弦与可学习
- 22.残差、LayerNorm 与前馈层
- 23.从零实现一个最小 Transformer block
- 24.训练稳定性、复杂度与常见误区
- 25.自监督与语言建模目标
- 26.BERT:掩码语言模型与双向编码
- 27.GPT:自回归生成与缩放定律
- 28.指令微调与对齐:SFT → RLHF/DPO
- 29.参数高效微调:LoRA 与适配器
- 30.自编码器与 VAE
- 31.GAN:对抗训练与模式崩塌
- 32.扩散模型:前向加噪与反向去噪
- 33.扩散模型的应用与采样加速
- 34.对比学习与 CLIP
- 35.ViT:把图像当序列
- 36.多模态架构:对齐、融合与生成
- 37.能力边界、幻觉与评测
- 38.PyTorch 2.x 训练循环与混合精度
- 39.实验管理、评测与复现
- 40.推理部署与成本:量化/蒸馏概览
卷积的直觉与参数共享
小象实战讲义 · 深度学习基础:从神经网络到 Transformer
在上一节,我们掌握了用全连接网络处理向量数据。但面对图像这类高维、结构化的数据,全连接网络会因参数爆炸而失效。本节将带你理解卷积神经网络(CNN)的核心思想:局部连接与权值共享。学完后,你将能回答:为什么卷积能极大减少参数?一个卷积层在代码里如何实现?以及,为什么说卷积是“特征学习器”?
先修:神经网络基础、矩阵乘法、激活函数。 后续:本节是卷积网络的基石,下一节将深入卷积层的细节(步长、填充、池化)。
💡 核心导读
- 从全连接到局部连接:图像像素的强相关性仅存在于局部邻域,无需每个神经元连接全局图像。
- 权值共享的威力:同一个卷积核(滤波器)滑过整张图像,学习一种局部特征模式,参数数量与图像大小无关。
- 卷积即特征提取:每个卷积核的输出是一张“特征图”,记录了该特征在图像各处的响应强度。
- 从2D到3D卷积:处理多通道(如RGB)图像时,卷积核也变为3D,对每个通道分别卷积后求和。
- 参数数量对比:通过一个具体的例子,量化感受卷积相比全连接在参数效率上的巨大优势。
🧭 前置知识
- 矩阵乘法与张量形状:理解 PyTorch 的
(batch, channels, height, width)表示(注意通道维在前)。 - 二维数组索引:熟悉
[i, j]访问像素。 - 线性加权求和:卷积的基本计算单元。
- 链式法则与反向传播:知道网络参数可通过梯度下降优化。
1. 全连接网络的困境与图像的特性
假设我们有一张 200x200 的灰度图像,将其拉平为一个 40000 维的向量。如果第一层隐藏层有 1000 个神经元,那么仅这一层就需要 40000 * 1000 = 40,000,000 个权重参数。这会导致:
- 计算量巨大,训练缓慢。
- 极易过拟合,因为参数远多于样本能提供的信息。
- 忽略了图像的空间结构:将像素视为独立变量。
(上面的 200×200 只是纸面算术;本课所有代码都用 32×32 的小张量演示,参数结论与尺寸无关。)
然而,图像具有两个关键特性:
- 局部相关性:一个像素与其邻近像素(如上下左右)的关系最紧密,与远处像素关系微弱。
- 平移不变性:一只猫的耳朵特征,无论出现在图像的左上角还是右下角,都应该由相同的“模式检测器”来识别。
卷积神经网络正是利用了这两个特性。
2. 卷积的核心思想:局部连接与权值共享
2.1 局部连接 (Local Connectivity)
我们不要求隐藏层的每个神经元都连接到全部 40000 个输入像素。相反,每个神经元只“看”输入图像的一个小区域(例如 5x5),这个区域称为它的感受野。
import torch
import torch.nn as nn
torch.manual_seed(0)
# 模拟一张 32x32 的灰度图像 (1个通道);真实图像可以是 200x200,结论不受影响
batch_size, channels, height, width = 1, 1, 32, 32
input_image = torch.randn(batch_size, channels, height, width)
# 定义一个卷积层:1个输入通道,4个输出通道,5x5卷积核,步长1,填充0
conv_layer_local = nn.Conv2d(in_channels=channels, out_channels=4, kernel_size=5, stride=1, padding=0)
# 前向传播:每个输出神经元只与输入的一个5x5局部区域相连
output_feature_map = conv_layer_local(input_image)
print("输入图像形状:", tuple(input_image.shape))
print("卷积层权重形状 (out_channels, in_channels, kH, kW):", tuple(conv_layer_local.weight.shape))
print("输出特征图形状 (batch, out_channels, out_H, out_W):", tuple(output_feature_map.shape))输入图像形状: (1, 1, 32, 32)
卷积层权重形状 (out_channels, in_channels, kH, kW): (4, 1, 5, 5)
输出特征图形状 (batch, out_channels, out_H, out_W): (1, 4, 28, 28)形状解读:
- 输入:
(1, 1, 32, 32)表示(批大小,通道数,高,宽)。 - 权重:
(4, 1, 5, 5)表示有4个不同的卷积核,每个核作用于1个输入通道,大小为5x5。 - 输出:
(1, 4, 28, 28)。输出高/宽从 32 变为 28,因为5x5卷积核在没有填充的情况下,每边会“吃掉”2 个像素(32 - 5 + 1 = 28)。我们得到了 4 张28x28的特征图。
此时,该层的参数数量仅为 4 * 1 * 5 * 5 + 4(偏置) = 104 个。如果改用全连接层把 32×32 的输入拉平、接 1000 个神经元,需要 1024 * 1000 + 1000 = 1,025,000 个参数——卷积少了四个数量级。
2.2 权值共享 (Weight Sharing)
局部连接减少了连接数,但每个位置都有一个独立的 5x5 权重块,参数仍然很多。权值共享更进一步:对于同一张特征图,我们在图像所有位置使用完全相同的卷积核(权重)。
这意味着,我们不是为输出特征图的每个位置学习一组独立的权重,而是整个特征图共享同一组 5x5 的权重。这个共享的权重块就是“卷积核”或“滤波器”。它像一个模板,滑过整张图像,检查每个位置是否存在与该模板匹配的模式。
在代码中,nn.Conv2d 的 weight 张量形状 (4, 1, 5, 5) 已经体现了这一点:4个输出通道对应4个不同的滤波器,每个滤波器在所有空间位置上共享。
为什么有效? 这利用了图像的“平移不变性”。一个用于检测垂直边缘的滤波器,应该在图像的任何位置都执行相同的操作。共享权重迫使网络学习这种通用的、位置无关的特征检测器。
3. 卷积操作的实现与形状变化
让我们手动实现一个简单的 3x3 卷积,来理解其计算过程。
import torch
import torch.nn.functional as F
torch.manual_seed(0)
# 1. 构造一个简单的 6x6 单通道图像和 3x3 卷积核
input_2d = torch.arange(36, dtype=torch.float32).view(1, 1, 6, 6) # (1,1,6,6)
kernel = torch.tensor([[[[1., 2., 1.],
[0., 0., 0.],
[-1., -2., -1.]]]]) # 一个Sobel-like边缘检测核 (1,1,3,3)
print("输入图像 (6x6):")
for i in range(6):
print([int(x) for x in input_2d[0,0,i,:].tolist()])
print("\n卷积核 (3x3):")
for i in range(3):
print([f"{x:.1f}" for x in kernel[0,0,i,:].tolist()])
# 2. 使用PyTorch的卷积函数(步长=1,填充=0)
output_conv2d = F.conv2d(input_2d, kernel, stride=1, padding=0)
print(f"\n卷积输出形状: {tuple(output_conv2d.shape)}")
print("输出特征图 (4x4):")
for i in range(4):
print([f"{x:.1f}" for x in output_conv2d[0,0,i,:].tolist()])输入图像 (6x6):
[0, 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, 26, 27, 28, 29]
[30, 31, 32, 33, 34, 35]
卷积核 (3x3):
['1.0', '2.0', '1.0']
['0.0', '0.0', '0.0']
['-1.0', '-2.0', '-1.0']
卷积输出形状: (1, 1, 4, 4)
输出特征图 (4x4):
['-48.0', '-48.0', '-48.0', '-48.0']
['-48.0', '-48.0', '-48.0', '-48.0']
['-48.0', '-48.0', '-48.0', '-48.0']
['-48.0', '-48.0', '-48.0', '-48.0']这个输出看起来「什么都没检测到」——全是同一个数 -48.0。原因有两点:这个核的权重和恰好为 0(1+2+1−1−2−1=0),而输入在垂直方向是等差递增的(每行比上一行大 6),两者相乘后正负完全抵消,只剩一个与位置无关的常数 −48。换句话说,卷积核对「线性渐变」不敏感,它只对与自身模板匹配的局部变化(比如真实的边缘)有响应。把输入换成一个有明显明暗交界的小矩阵,输出的数值就会随位置变化。
形状计算公式(无填充): 输出高度 = (输入高度 - 卷积核高度) / 步长 + 1 输出宽度 = (输入宽度 - 卷积核宽度) / 步长 + 1
上例中:(6 - 3) / 1 + 1 = 4。
4. 处理多通道输入:从2D到3D卷积
现实中的图像通常是RGB三通道。此时,卷积核也需要变成3D:其深度(in_channels)必须与输入通道数匹配。
import torch
import torch.nn as nn
torch.manual_seed(0)
# 模拟一张 5x5 的RGB图像 (3个通道)
input_rgb = torch.randn(1, 3, 5, 5) # (batch, channels, height, width)
# 定义一个卷积层:3个输入通道,2个输出通道,3x3卷积核
conv_layer_3d = nn.Conv2d(in_channels=3, out_channels=2, kernel_size=3, bias=False)
print("卷积层权重形状 (out_channels, in_channels, kH, kW):", tuple(conv_layer_3d.weight.shape))
# 手动模拟计算一个输出位置的值 (位于输出特征图(0,0)的点)
# 假设我们计算输出特征图第0个通道,位置(0,0)的值
kernel = conv_layer_3d.weight # shape: (2, 3, 3, 3)
input_patch = input_rgb[0, :, 0:3, 0:3] # 取输入图像左上角 3x3x3 的块
# 计算:对应通道的核与输入块逐元素相乘,然后所有元素求和
output_value_manual = (kernel[0] * input_patch).sum()
print(f"手动计算输出[0,0,0,0]的值: {output_value_manual.item():.4f}")
# 用PyTorch卷积验证
output_rgb = conv_layer_3d(input_rgb)
print(f"PyTorch卷积输出[0,0,0,0]的值: {output_rgb[0,0,0,0].item():.4f}")
print(f"两者是否接近: {torch.allclose(output_value_manual, output_rgb[0,0,0,0], atol=1e-5)}")卷积层权重形状 (out_channels, in_channels, kH, kW): (2, 3, 3, 3)
手动计算输出[0,0,0,0]的值: -0.5178
PyTorch卷积输出[0,0,0,0]的值: -0.5178
两者是否接近: True关键理解:
- 对于多通道输入,每个卷积核都是一个3D体积
(in_channels, kH, kW)。 - 计算时,卷积核在每个通道上与输入图像的对应通道进行2D卷积,然后将所有通道的卷积结果相加,得到一个标量,作为输出特征图在该位置的值。
- 因此,一个卷积核整合了所有输入通道的信息,输出一张单通道的特征图。
out_channels定义了有多少个这样的滤波器,从而产生多少张输出特征图。
5. 参数数量对比:全连接 vs. 卷积
让我们用一个具体的网络层对比来结束本节。
假设网络结构如下:
- 输入:
200x200灰度图 (1通道)。 - 第一隐藏层:我们想得到
196x196大小的输出,并希望学习4种特征。
import torch.nn as nn
import math
# 场景设定
in_channels = 1
out_channels = 4
input_hw = 200
kernel_size = 5
output_hw = input_hw - kernel_size + 1 # 196
assert output_hw == 196
# 1. 全连接层的参数数量 (将输入拉平)
# 输入神经元数: 200*200 = 40000
# 输出神经元数: 196*196*4 = 153664 (假设我们想要同样多的输出单元,这很不合理,但用于对比)
# 参数数量 = 输入数 * 输出数 + 输出数 (偏置)
fc_params = (input_hw * input_hw) * (output_hw * output_hw * out_channels) + (output_hw * output_hw * out_channels)
print(f"全连接层参数数量(不合理的对比): {fc_params:,}")
print(f" 即: {fc_params / 1e8:.1f} 亿参数")
# 2. 卷积层的参数数量
# 权重: out_channels * in_channels * kernel_size * kernel_size
# 偏置: out_channels (每个输出通道一个偏置)
conv_params = out_channels * in_channels * kernel_size * kernel_size + out_channels
print(f"\n卷积层参数数量: {conv_params:,}")
print(f" 即: {conv_params} 个参数")
# 3. 更合理的对比:卷积后接一个全连接分类头
# 假设卷积后我们得到 4个 196x196 的特征图,然后将其拉平送入一个1000维的全连接层。
flattened_size = out_channels * output_hw * output_hw # 4 * 196 * 196 = 153664
fc_head_params = flattened_size * 1000 + 1000 # 接一个1000神经元的全连接层
print(f"\n卷积层 + 全连接分类头参数数量:")
print(f" 卷积层: {conv_params:,}")
print(f" 全连接头: {fc_head_params:,}")
print(f" 总计: {conv_params + fc_head_params:,} (约 { (conv_params+fc_head_params)/1e6:.1f} 百万)")
print(f" 相比纯全连接,参数减少了约 {fc_params/(conv_params+fc_head_params):.0f} 倍")全连接层参数数量(不合理的对比): 6,146,713,664
即: 61.5 亿参数
卷积层参数数量: 104
即: 104 个参数
卷积层 + 全连接分类头参数数量:
卷积层: 104
全连接头: 153,665,000
总计: 153,665,104 (约 153.7 百万)
相比纯全连接,参数减少了约 40 倍这个对比清楚地展示了卷积的参数效率:同样是「输出 4 张 196×196 的特征图」,全连接层要 61.5 亿参数,卷积层只要 104 个;即便卷积后面必须接一个 1000 类的全连接分类头(1.54 亿),总参数仍比前者小 40 倍。参数的大头被压在「特征提取」这一段,而不是「分类」这一段——这正是 CNN 能在有限数据上训练出可用图像模型的原因。
📝 动手练一练
问题:假设输入图像大小为 (1, 3, 32, 32),经过一个 nn.Conv2d(in_channels=3, out_channels=8, kernel_size=3, stride=2, padding=1) 层后,输出特征图的形状是什么?请先手动计算,再用代码验证。
👉 点击查看参考答案
思路:使用形状计算公式,考虑步长(stride)和填充(padding)。
- 输出高度 =
(输入高度 + 2*padding - 卷积核高度) / 步长 + 1 - 输出宽度同理。
padding=1表示在图像上下左右各补一圈0(若为padding_mode='zeros')。
import torch
import torch.nn as nn
torch.manual_seed(0)
# 定义输入和卷积层
input_tensor = torch.randn(1, 3, 32, 32)
conv_layer = nn.Conv2d(in_channels=3, out_channels=8, kernel_size=3, stride=2, padding=1)
# 前向传播
output = conv_layer(input_tensor)
print("输出特征图形状:", tuple(output.shape))
# 手动计算验证
H_in, W_in = 32, 32
padding, kernel_size, stride = 1, 3, 2
H_out = (H_in + 2*padding - kernel_size) // stride + 1
W_out = (W_in + 2*padding - kernel_size) // stride + 1
print(f"手动计算形状: (batch=1, out_channels=8, H={H_out}, W={W_out})")输出特征图形状: (1, 8, 16, 16)
手动计算形状: (batch=1, out_channels=8, H=16, W=16)常见错误
| 错误理解或写法 | 现象或报错 | 原因 | 正确做法 |
|---|---|---|---|
混淆 in_channels 与输入图像的高/宽 | RuntimeError: Given groups=1, weight of size [out_c, in_c, kH, kW]..., expected input...[batch, **, H, W] | nn.Conv2d 的第一个参数是通道数,不是高度。输入张量形状应为 (batch, channels, height, width)。 | 确保输入张量的第二个维度(索引1)等于卷积层的 in_channels。使用 .permute() 或 .view() 调整维度顺序。 |
忘记设置 padding 导致输出尺寸意外缩小 | 网络越深,特征图变得越小(如从224x224迅速缩小到个位数),可能丢失空间信息。 | 连续的卷积(无填充)会不断减小特征图尺寸。 | 根据需求使用 padding(通常为 kernel_size // 2 以保持尺寸),或使用池化层进行有计划的下采样。 |
| 误以为卷积核权重是2D的 | 处理多通道输入时,手动实现卷积错误地只在单个通道上运算。 | 对于 in_channels > 1,卷积核是3D的,计算需要对所有通道求和。 | 理解并实现跨通道的求和操作,或直接使用 nn.Conv2d。 |
本章小结
- 局部连接使每个神经元只处理输入的一个小区域,大幅减少连接数,契合图像的局部相关性。
- 权值共享让同一个卷积核滑过整个图像,用一组参数学习一种通用特征模式,契合图像的平移不变性,是参数效率的关键。
- 卷积层的核心参数是:
in_channels(输入通道数)、out_channels(滤波器数/输出通道数)、kernel_size、stride、padding。 - 多通道卷积时,每个卷积核是3D的,计算时先按通道卷积再求和,输出一张特征图。
- 卷积的输出形状由输入形状、卷积核大小、步长和填充共同决定,可通过公式精确计算。
📋 行动清单
- 运行本节所有代码块,观察输入、卷积核、输出形状之间的关系。
- 修改“动手练一练”中的
stride或padding值,重新计算并验证输出形状。 - 尝试手动初始化一个
3x3的卷积核(例如边缘检测核),应用于一个简单图像张量,观察输出特征图的变化。
延伸阅读
本段 PyTorch 官方文档/教程条目的框架出处:PyTorch: An Imperative Style, High-Performance Deep Learning Library(Paszke et al., 2019,arXiv:1912.01703);页面内容以 PyTorch 2.14.1 为准。
Gradient-Based Learning Applied to Document Recognition(LeCun et al., 1998, Proceedings of the IEEE 86(11))—— LeNet 原始论文,卷积网络用于图像识别的起点 - https://doi.org/10.1109/5.726791ImageNet Classification with Deep Convolutional Neural Networks(Krizhevsky, Sutskever & Hinton, 2012, NIPS)—— AlexNet 论文,让 CNN 在大规模图像识别上被广泛采用 - https://papers.nips.cc/paper_files/paper/2012/hash/c399862d3b9d6b76c8436e924a68c45b-Abstract.html- PyTorch 官方文档
torch.nn.Conv2d(2.14.1,含输出尺寸公式与参数形状) - https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html
—— 小象教研组