查看全课大纲(第 11 / 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)在卷积层之后通常紧跟着池化层?池化操作具体如何实现,它带来了哪些好处和代价?感受野是如何随着网络加深而增大的?理解这些机制,是掌握现代 CNN 架构(如 VGG、ResNet)的基础;后续在序列模型与 Transformer 里,你还会遇到「如何降低序列长度」的同类问题。
💡 核心导读
- 池化(Pooling)的本质:一种对局部区域进行“摘要”的下采样操作,最常用的是最大池化(取最大值)和平均池化(取平均值)。
- 池化的双重作用:降低特征图的空间尺寸(H, W),从而减少后续层的计算量和参数;同时引入一定的平移不变性,使网络对目标位置的微小变化不敏感。
- 感受野(Receptive Field)的计算:一个输出特征图上的像素点,其值由输入图像上多大区域内的像素计算而来。感受野随着卷积和池化层的堆叠而指数级增大,是网络能够“看到”全局信息的关键。
- 下采样(Downsampling)的实现方式:除了池化,带步长(stride>1)的卷积也可以直接实现下采样,这是现代网络设计中更常用的方式。
🧭 前置知识
- 二维卷积操作:理解卷积核在输入特征图上的滑动计算过程,包括填充(padding)和步长(stride)的概念。
- 张量形状:熟悉
(batch_size, channels, height, width)的四维张量表示。 - 基本的Python和PyTorch操作:包括张量创建、索引和简单的数值计算。
池化操作:从局部到摘要
池化层通常紧跟在卷积层之后,它对每个特征图独立操作。假设我们有一个大小为 2x2,步长为2的池化窗口。
最大池化(Max Pooling):取窗口内的最大值。它更关注该区域内是否存在某个特征(如边缘、纹理),对噪声有一定的鲁棒性。 平均池化(Average Pooling):取窗口内的平均值。它更平滑,能保留背景信息。
让我们通过代码直观感受池化操作及其效果。
import torch
import torch.nn as nn
torch.manual_seed(0) # 固定随机种子,保证结果可复现
# 模拟一个批大小为1,通道数为1,4x4的输入特征图
input_feature = torch.tensor([[[
[1., 2., 5., 6.],
[3., 4., 7., 8.],
[9., 10., 13., 14.],
[11., 12., 15., 16.]
]]]) # shape: (1, 1, 4, 4)
print("输入特征图 shape:", input_feature.shape)
print("输入特征图数据:\n", input_feature[0, 0])
# 定义最大池化层:核大小2x2,步长2
max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
output_max = max_pool(input_feature)
# 定义平均池化层:核大小2x2,步长2
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
output_avg = avg_pool(input_feature)
print("\n最大池化后 shape:", output_max.shape)
print("最大池化结果:\n", output_max[0, 0])
print("\n平均池化后 shape:", output_avg.shape)
print("平均池化结果:\n", output_avg[0, 0])输入特征图 shape: torch.Size([1, 1, 4, 4])
输入特征图数据:
tensor([[ 1., 2., 5., 6.],
[ 3., 4., 7., 8.],
[ 9., 10., 13., 14.],
[11., 12., 15., 16.]])
最大池化后 shape: torch.Size([1, 1, 2, 2])
最大池化结果:
tensor([[ 4., 8.],
[12., 16.]])
平均池化后 shape: torch.Size([1, 1, 2, 2])
平均池化结果:
tensor([[ 2.5000, 6.5000],
[10.5000, 14.5000]])可以看到,4x4 的输入经过 2x2 池化后,空间尺寸减半为 2x2。最大池化输出了每个 2x2 窗口内的最大值,而平均池化输出了平均值。
感受野:网络“看到”了多大区域?
感受野是理解CNN层次化特征提取的核心概念。一个特征图上的像素点的感受野,是指输入图像上所有能影响该点取值的像素区域。
- 第一层卷积:如果卷积核是
3x3,那么输出特征图上的每个点,其感受野就是输入图像上对应的3x3区域。 - 叠加池化:如果第一层卷积后接一个
2x2池化(步长2),那么第二层卷积(假设仍是3x3)的一个输出点,它对应输入图像上多大区域?- 第二层卷积的一个点,对应第一层池化后特征图上的一个
3x3区域。 - 第一层池化后特征图上的一个点,对应第一层卷积后特征图上的一个
2x2区域(因为池化)。 - 第一层卷积后特征图上的一个点,对应输入图像上的一个
3x3区域。 - 这三层叠加后,第二层卷积的一个输出点覆盖输入图像
8x8的区域——它恰好等于三个窗口尺寸直接相加(3+2+3=8),但这是本例步长组合下的巧合;换个 stride 就会算错,所以要用下面这种从后向前的递推方法。
- 第二层卷积的一个点,对应第一层池化后特征图上的一个
更通用的计算方式是考虑从后向前递推。假设第 l 层特征图上某点的感受野大小为 ,该层卷积核大小为 ,步长为 。那么,该点对应第 l-1 层特征图上的感受野大小 为: 对于池化层,可以将其视为一种特殊的“卷积”操作(核大小为池化窗口,步长为池化步长),使用相同的公式。
让我们计算一个简单网络的感受野。
import torch
import torch.nn as nn
torch.manual_seed(0)
# 定义一个简单的网络:Conv1 -> Pool1 -> Conv2
class SimpleNet(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 1, kernel_size=3, stride=1, padding=1) # 保持尺寸
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(1, 1, kernel_size=3, stride=1, padding=1)
def forward(self, x):
x = self.conv1(x)
x = self.pool1(x)
x = self.conv2(x)
return x
model = SimpleNet()
# 创建一个虚拟输入
dummy_input = torch.randn(1, 1, 8, 8)
output = model(dummy_input)
print("输入图像尺寸 (H, W):", dummy_input.shape[2:])
print("输出特征图尺寸 (H, W):", output.shape[2:])
# 手动计算感受野
# 从输出层(conv2)开始往回推
# Conv2: K=3, S=1, padding=1 (不改变尺寸,不影响感受野计算)
# 假设输出特征图上一个点,对应pool1后特征图上的一个3x3区域。
# Pool1: K=2, S=2
# pool1后特征图上的一个点,对应conv1后特征图上的一个2x2区域。
# Conv1: K=3, S=1, padding=1
# conv1后特征图上的一个点,对应输入图像上的一个3x3区域。
# 计算:从输出点开始
rf = 1 # 输出点自身的感受野(就是它自己)
# 经过conv2
rf = (rf - 1) * 1 + 3 # rf = (1-1)*1 + 3 = 3
# 经过pool1
rf = (rf - 1) * 2 + 2 # rf = (3-1)*2 + 2 = 6
# 经过conv1
rf = (rf - 1) * 1 + 3 # rf = (6-1)*1 + 3 = 8
print("\n输出特征图上一个点的感受野(相对于输入图像):", rf)
print("这意味着,输出图上的一个点,是由输入图像上 8x8 区域内的信息计算得出的。")输入图像尺寸 (H, W): torch.Size([8, 8])
输出特征图尺寸 (H, W): torch.Size([4, 4])
输出特征图上一个点的感受野(相对于输入图像): 8
这意味着,输出图上的一个点,是由输入图像上 8x8 区域内的信息计算得出的。尽管我们的网络只有两层卷积和一层池化,但输出特征图上一个点的感受野已经覆盖了整个输入图像(8x8)。这就是深度网络能够整合全局信息、理解复杂模式的基础。
下采样的现代实现:带步长的卷积
早期网络如 LeNet 用池化层做下采样;VGG 也一样——它的全部下采样都由 2×2 最大池化完成,卷积层清一色 stride=1。真正把带步长(stride > 1)的卷积当成主力下采样手段的是 ResNet 及之后的网络:ResNet 在阶段之间用 stride=2 的 3×3 卷积(配合 1×1 投影的快捷连接)一次性完成「特征提取 + 尺寸减半」,比「卷积再池化」少一层、参数也更省。
import torch
import torch.nn as nn
torch.manual_seed(0)
# 模拟输入
input_tensor = torch.randn(1, 3, 8, 8) # (batch, channels, height, width)
print("输入张量 shape:", input_tensor.shape)
# 方式1:传统方式(卷积+池化)
conv1 = nn.Conv2d(3, 6, kernel_size=3, stride=1, padding=1) # 输出 (1, 6, 8, 8)
pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # 输出 (1, 6, 4, 4)
output_traditional = pool1(conv1(input_tensor))
print("传统方式(Conv stride=1 + Pool)输出 shape:", output_traditional.shape)
# 方式2:现代方式(带步长的卷积)
conv2 = nn.Conv2d(3, 6, kernel_size=3, stride=2, padding=1) # 直接下采样
output_modern = conv2(input_tensor)
print("现代方式(Conv stride=2)输出 shape:", output_modern.shape)
# 比较参数量
print("\n参数量比较:")
print(f"Conv stride=1 参数量: {sum(p.numel() for p in conv1.parameters())}")
print(f"Conv stride=2 参数量: {sum(p.numel() for p in conv2.parameters())}")
print("两者参数量相同,因为卷积核的尺寸和输入/输出通道数相同。")输入张量 shape: torch.Size([1, 3, 8, 8])
传统方式(Conv stride=1 + Pool)输出 shape: torch.Size([1, 6, 4, 4])
现代方式(Conv stride=2)输出 shape: torch.Size([1, 6, 4, 4])
参数量比较:
Conv stride=1 参数量: 168
Conv stride=2 参数量: 168
两者参数量相同,因为卷积核的尺寸和输入/输出通道数相同。两种方式都将空间尺寸从 8x8 降到了 4x4。带步长的卷积一步到位,网络更简洁。在反向传播时,池化层(尤其是最大池化)需要记录最大值的位置,而带步长的卷积则统一用卷积的求导规则。
📝 动手练一练
题目:假设我们有一个输入图像尺寸为 224x224,依次经过以下层:
- Conv1:
kernel_size=7, stride=2, padding=3 - Pool1:
kernel_size=3, stride=2, padding=1 - Conv2:
kernel_size=3, stride=1, padding=1 - Conv3:
kernel_size=3, stride=2, padding=1
请计算经过每一层后特征图的尺寸,并计算最终输出特征图上一个点的感受野(相对于输入图像)。
👉 点击查看参考答案
思路:特征图尺寸计算公式为:,其中 是卷积核边长。感受野使用递推公式 从后向前计算。
import math
def output_size(H_in, padding, kernel_size, stride):
return math.floor((H_in + 2*padding - kernel_size) / stride) + 1
H_in = 224
layers = [
{'name':'Conv1', 'K':7, 'S':2, 'P':3},
{'name':'Pool1', 'K':3, 'S':2, 'P':1},
{'name':'Conv2', 'K':3, 'S':1, 'P':1},
{'name':'Conv3', 'K':3, 'S':2, 'P':1},
]
H = H_in
print("特征图尺寸变化:")
for layer in layers:
H = output_size(H, layer['P'], layer['K'], layer['S'])
print(f"{layer['name']} 后: {H}x{H}")
# 计算感受野
rf = 1
for layer in reversed(layers):
rf = (rf - 1) * layer['S'] + layer['K']
print(f"\n最终输出特征图上一个点的感受野: {rf}x{rf}")特征图尺寸变化:
Conv1 后: 112x112
Pool1 后: 56x56
Conv2 后: 56x56
Conv3 后: 28x28
最终输出特征图上一个点的感受野: 27x27常见错误
| 错误理解或写法 | 现象或报错 | 原因 | 正确做法 |
|---|---|---|---|
| 混淆池化与卷积的步长作用 | 设计网络时,期望下采样但忘记设置 stride>1 或添加池化层,导致特征图尺寸不降反增(如果通道数增加),计算量爆炸。 | 没有明确区分卷积的“特征提取”和池化的“降维摘要”功能,或忽略了步长的下采样能力。 | 明确每一层的目的。若需下采样,在卷积层设置 stride>1,或在其后添加池化层。规划好网络各层的输入输出尺寸。 |
| 最大池化的反向传播误解 | 自定义池化层反向传播时,梯度只回传到前向传播中被选为最大值的位置,其他位置梯度为0。若实现错误,会导致梯度消失或爆炸。 | 不理解最大池化在前向时记录了最大值位置(索引),反向时需根据索引将梯度“散射”回正确位置。 | 使用 torch.nn.MaxPool2d 等内置层,它们已正确实现。若需手写,务必使用 torch.max 返回索引,并用 torch.scatter 或索引赋值进行梯度回传。 |
| 感受野计算时把 padding 也算进公式 | 算出的感受野偏大或偏小,与实测对不上。 | 感受野大小的递推公式 与 padding 无关:padding 只改变特征图尺寸和感受野中心的位置,不改变窗口覆盖的范围。 | 直接用上面的公式从后往前推;只有在讨论「边界处的感受野被裁掉多少」时才需要单独考虑 padding。 |
本章小结
- 池化层(最大/平均) 是一种下采样操作:最大池化是非线性聚合(取窗口最大值,输出不可由输入的线性组合得到),平均池化则是线性聚合(窗口内加权求均值)。两者都降低特征图空间维度、减少计算量,并引入一定的平移不变性。
- 感受野 定量描述了网络中特征点的“视野”范围,它随着卷积和池化层的堆叠而迅速增大,是网络具备层次化特征理解能力的基础。
- 下采样 不仅可以通过池化实现,ResNet 之后的网络更常用带步长(stride>1)的卷积同时完成特征提取和尺寸缩减,使网络设计更灵活。
- 池化操作(尤其是最大池化)在反向传播时需要特殊处理,只将梯度传播到前向传播中被选中的位置。
📋 行动清单
- 修改第一个代码示例中的输入张量数值,观察最大池化和平均池化结果的不同,思考哪种情况更适合用最大池化。
- 在“感受野计算”的代码示例中,尝试去掉
pool1层,重新计算感受野,理解池化对扩大感受野的关键作用。 - 查阅PyTorch官方文档中
nn.Conv2d和nn.MaxPool2d的参数说明,重点关注stride,padding,dilation对输出形状的影响。
延伸阅读
本段 PyTorch 官方文档/教程条目的框架出处:PyTorch: An Imperative Style, High-Performance Deep Learning Library(Paszke et al., 2019,arXiv:1912.01703);页面内容以 PyTorch 2.14.1 为准。
- PyTorch 官方文档
torch.nn.MaxPool2d(2.14.1,窗口、步长与 dilation 对输出的影响) - https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html - PyTorch 官方文档
torch.nn.Conv2d(2.14.1,含输出尺寸公式) - https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html ImageNet Classification with Deep Convolutional Neural Networks(Krizhevsky, Sutskever & Hinton, 2012, NIPS)—— AlexNet 中使用了重叠最大池化(kernel_size=3, stride=2),对后续网络影响深远 - https://papers.nips.cc/paper_files/paper/2012/hash/c399862d3b9d6b76c8436e924a68c45b-Abstract.html
—— 小象教研组