特征图减半为何会凭空改变图案?池化、步幅卷积与抗混叠下采样
从直接隔点取样的位移敏感出发,手算频率混叠,比较最大池化、平均池化与步幅卷积,并用 PyTorch 2.13 实现可调试的抗混叠下采样。
上一篇把卷积拆成了局部连接、权重共享与感受野,但它的示例网络用了一个 stride=2 的卷积,直接把 32×32 特征图变成 16×16。空间尺寸减半能大幅节省计算与显存,却也隐藏了一个问题:当新网格不足以表示原特征的快速变化时,被丢掉的高频会伪装成错误的低频图案。
本文只讲透这一次“缩小”:池化(Pooling)、带步幅卷积(Strided Convolution)分别保留什么,为什么先低通再抽样能减少混叠(Aliasing),以及下采样如何生成卷积神经网络(Convolutional Neural Network, CNN)的多尺度特征层次。
01 为什么不一直保留高分辨率?#
设一层卷积的输入为 ,输出通道数为 ,核为 。忽略常数和内核实现,乘加量约为:
若 同时减半,下一层的空间乘加量只剩约 ,激活张量也从 [N,C,H,W] 变成 [N,C,H/2,W/2]。这使网络能在后面增加通道数和层数,并让每个新位置对应原图上更大的区域。
高分辨率阶段 下采样 低分辨率阶段
[N, 64, 56, 56] ── 低通/聚合 ──► [N, 64, 28, 28] ── 卷积 ──► [N, 128, 28, 28]
细边缘、小纹理 └─ 网格间距×2 大轮廓、组合模式
位置多、显存大 位置少、单点视野大text下采样不是免费压缩。新网格中每个位置要代表更大区域,究竟先聚合哪些信息,决定了小物体、细边缘和纹理是被保留、平滑,还是误读。
02 “隔一个取一个”为什么会创造假图案?#
下采样因子 表示每两个位置只保留一个:
考察一条交替亮暗信号与它右移一格的版本:
原信号 x: 1 0 1 0 1 0 1 0
取样位置: ↑ ↑ ↑ ↑ y = [1, 1, 1, 1]
右移 1 格 x': 0 1 0 1 0 1 0 1
取样位置: ↑ ↑ ↑ ↑ y' = [0, 0, 0, 0]text输入只移了一格,输出却从全 1 变成全 0。新网格无法表示“每格翻转一次”的变化,于是取样相位决定了看见哪个假的常量。这就是频率混叠:超过新奈奎斯特频率(Nyquist Frequency)的变化,在低分辨率网格上冒充成另一种较慢的模式。
根据采样定理(Sampling Theorem),抽样前应先用低通滤波器(Low-pass Filter)压低新网格无法表示的快速变化:
不抗混叠: x[n] ───────── 每 2 点取 1 点 ──► y[m] 高频折叠到低频
抗混叠: x[n] ──► 低通 h[n] ──► 每 2 点取 1 点 ──► y[m]
先删除新网格表示不了的成分text03 用八个数手算“先平滑再抽样”#
对上面的交替信号使用不重叠的两点平均:
x : [1,0] [1,0] [1,0] [1,0] ── 每组求平均 ──► [0.5, 0.5, 0.5, 0.5]
x': [0,1] [0,1] [0,1] [0,1] ── 每组求平均 ──► [0.5, 0.5, 0.5, 0.5]text一格位移不再把整个输出翻转。但两点均值只是最简单的盒式滤波器(Box Filter),频率截止并不锐利,且不重叠窗口仍会受分组边界影响。
一个常用的小型平滑核是二项式核(Binomial Kernel):
对二维特征图可用可分离外积 :
它先将中心与周围邻域做加权平均,再以步幅 2 保留结果。权重和为 1,因此常量区域的幅度不会平白放大或缩小。
04 最大池化与平均池化保留了什么?#
对 ,2×2,stride=2 池化不混合通道,只在每个通道的局部窗口聚合:
对窗口 ,最大池化输出 7,平均池化输出 2.5。它们对同一邻域做了不同的摘要:
| 方法 | 窗口内保留什么 | 参数 | 反传到哪里 | 主要风险 |
|---|---|---|---|---|
| 最大池化 | 最强响应的幅度 | 0 | 通常只到 argmax 位置 | 一个尖峰/噪声支配,仍可混叠 |
| 平均池化 | 局部直流与粗略能量 | 0 | 均匀分给窗口元素 | 边缘、小物体可被模糊 |
| 步幅卷积 | 从数据学得的局部组合 | 有 | 由学得的核分配 | 核不保证是低通,训练可偏爱高频 |
| 低通后抽样 | 可控带宽内的平滑信号 | 可为 0 | 按固定/可学核分配 | 滤波过强会删除任务需要的细纹理 |
最大池化对窗口内重排不敏感,但它不是标准低通;对交替强弱信号,它仍可在不同取样相位上产生大幅差异。平均池化同时完成粗糙低通和降采样,但其固定权重不会根据任务选择边缘方向。
05 带步幅卷积为何不自动抗混叠?#
带步幅卷积在一步中同时完成局部线性组合和抽样:
若 学成平滑核,它可在抽样前压低高频;但训练目标通常只要求降低任务损失,并未约束卷积核的频率响应。一只边缘检测核恰好会放大高频,然后紧接着以步幅 2 抽样,依然可产生混叠。
更清楚的设计是拆开两种职责:
可学特征变换 明确带宽限制 抽样
Conv2d(stride=1) ──► 非线性 ──► low-pass filter ──► take every 2nd point
[N,Cin,H,W] [N,Cout,H,W] [N,Cout,H,W] [N,Cout,⌈H/2⌉,⌈W/2⌉]text这不表示所有 stride=2 卷积都必须替换。它表示当模型对一像素位移过度敏感、输入含周期纹理,或训练与部署缩放链不一致时,“变换”和“抽样”应分开检查。
06 输出尺寸与奇数边长如何处理?#
对核 、步幅 、补边 、空洞率 ,最大池化和卷积的高度输出公式是:
平均池化没有 dilation 参数,令 即可。对 ,默认向下取整得 ,最后一行没有进入任何完整窗口。
PyTorch 2.13 当前官方 MaxPool2d ↗ 和 AvgPool2d ↗ 都提供 ceil_mode=True,允许从左侧补边或有效输入内开始的窗口越过右边界。这能覆盖奇数尺寸的边缘,但也会改变形状规则;与跳连接或分割标签对齐时,必须显式断言。
07 不依赖深度学习框架,先写出一维本体#
下面代码只处理 [L] 信号,用对称补边、二项式低通和步幅 2 抽样暴露数据流:
import numpy as np
def decimate(x, factor=2):
assert x.ndim == 1 and factor >= 1
return x[::factor]
def blur_then_decimate(x, factor=2):
assert x.ndim == 1 and factor == 2
kernel = np.array([1.0, 2.0, 1.0]) / 4.0
padded = np.pad(x, (1, 1), mode="reflect")
blurred = np.convolve(padded, kernel, mode="valid")
return blurred[::factor]
x = np.array([1., 0., 1., 0., 1., 0., 1., 0.])
x_shift = np.array([0., 1., 0., 1., 0., 1., 0., 1.])
np.testing.assert_array_equal(decimate(x), [1., 1., 1., 1.])
np.testing.assert_array_equal(decimate(x_shift), [0., 0., 0., 0.])
y = blur_then_decimate(x)
y_shift = blur_then_decimate(x_shift)
assert y.shape == y_shift.shape == (4,)
assert np.max(np.abs(y - y_shift)) < 1.0 # 不再出现 1 的整幅翻转python最后的不等式只是此极小例的回归检查,不是“抗混叠必然位移不变”的定理。补边、非线性、有限核长和后续网络都会影响结果。
08 用 PyTorch 2.13 拆开可学变换与降采样#
先实现一个无可学参数的逐通道模糊下采样层。输入 [N,C,H,W] 使用相同 3×3 核独立处理每个通道,再以 stride=2 抽样:
import torch
import torch.nn.functional as F
from torch import nn
class BlurDownsample2d(nn.Module):
def __init__(self, channels: int):
super().__init__()
one_d = torch.tensor([1.0, 2.0, 1.0])
kernel_2d = one_d[:, None] * one_d[None, :]
kernel_2d = kernel_2d / kernel_2d.sum()
# [C,1,3,3];groups=C 表示各通道独立滤波
weight = kernel_2d.expand(channels, 1, 3, 3).contiguous()
self.channels = channels
self.register_buffer("weight", weight)
def forward(self, x: torch.Tensor) -> torch.Tensor:
assert x.ndim == 4 and x.shape[1] == self.channels
return F.conv2d(
x,
self.weight,
stride=2,
padding=1,
groups=self.channels,
)
class AntiAliasedConvBlock(nn.Module):
def __init__(self, in_channels: int, out_channels: int):
super().__init__()
self.transform = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, stride=1, padding=1),
nn.ReLU(),
)
self.downsample = BlurDownsample2d(out_channels)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.downsample(self.transform(x))
x = torch.randn(4, 3, 32, 32, requires_grad=True)
block = AntiAliasedConvBlock(3, 16)
y = block(x)
assert y.shape == (4, 16, 16, 16)
assert dict(block.downsample.named_parameters()) == {}
assert "weight" in dict(block.downsample.named_buffers())
y.square().mean().backward()
assert block.transform[0].weight.grad is not None
assert torch.isfinite(x.grad).all()pythonregister_buffer 让固定核随模型进入 state_dict、设备和数据类型,却不会被优化器当成参数。groups=C 是逐通道卷积,它平滑空间维度,不混合通道。这里的零补边是明确取舍;若边界语义重要,可先用 F.pad(..., mode="reflect") 再做 padding=0 的卷积,并重新验证尺寸。
训练时,数据流是 images → transform → blur/downsample → 后续网络 → logits → loss;loss.backward() 会穿过固定滤波计算输入和前面可学卷积的梯度,但优化器不会更新 buffer。推理时使用 model.eval() 与 torch.inference_mode();该滤波层本身没有训练/推理两套行为,但同一模型内的批归一化和随机失活有。
09 数据缩放时如何使用官方抗混叠 API?#
对输入图像或连续特征图做明确的尺寸变换时,PyTorch 当前官方 torch.nn.functional.interpolate ↗ 提供 antialias=True。对以“批量—通道—高度—宽度”(Batch-Channel-Height-Width, NCHW)排列的四维张量,可写成:
import torch
import torch.nn.functional as F
images = torch.rand(8, 3, 224, 224)
small = F.interpolate(
images,
size=(112, 112),
mode="bilinear",
align_corners=False,
antialias=True,
)
assert small.shape == (8, 3, 112, 112)
assert small.dtype == images.dtypepython当前文档说明 antialias 支持二线性(Bilinear)、双三次(Bicubic)和 Lanczos 模式;对下采样,align_corners=False 与抗混叠的组合还有明确的图像库对齐语义。不要在没有检查模式支持时只是“加上这个参数”。
这个 API 适合尺寸重采样,不会自动学习任务特征。在 CNN 块内,你仍需决定它放在可学卷积之前还是之后,并保证训练、验证和服务端使用同一条缩放链。
10 下采样如何生成多尺度特征层次?#
记第 层感受野为 ,相邻特征位置在原图上的间距为 。沿用上一篇的递推:
一次 2×2,stride=2 池化使 翻倍。后续同样的 3×3 卷积,每次会在原图上扩张更多像素:
层 张量形状 r(原图感受野) j(原图间距)
输入 [N, 3, 32,32] 1 1
Conv 3×3,s=1 [N, 32, 32,32] 3 1
Pool 2×2,s=2 [N, 32, 16,16] 4 2
Conv 3×3,s=1 [N, 64, 16,16] 8 2
Pool 2×2,s=2 [N, 64, 8, 8] 10 4
Conv 3×3,s=1 [N,128, 8, 8] 18 4text高分辨率层保留精确位置和小边缘,低分辨率层用更少位置表示更大区域和更抽象模式。这就是多尺度特征层次(Multi-scale Feature Hierarchy)。但一旦只保留最深的 8×8 层,小物体可能在早期下采样中消失;后续的上采样无法凭空恢复它。
11 一条可执行的位移敏感调试路径#
- 先定位所有降采样点。 搜索
stride>1、池化、interpolate/resize和数据增强,画出从原图到 logits 的尺寸链。 - 用可视化的高频输入。 棋盘格、一像素细线和斜条纹能比自然图像更快暴露混叠;逐层保存特征图。
- 做一像素压力测试。 将输入上下左右各移一格,忽略补边影响的边缘,比较中间特征和最终概率,不要只比较
argmax类别。 - 只替换一个下采样点。 在相同权重、输入和
eval()模式下,比较原池化/步幅卷积与低通后抽样,找到第一个过度敏感层。 - 同时看任务指标与稳定性。 统计原图与小位移版的 logits 差、一致预测率和分组指标;抗混叠不应以丢掉关键小结构为代价。
- 确保对比可复现。 固定随机种子、输入批次和软件版本。官方文档提醒,统一计算设备架构(Compute Unified Device Architecture, CUDA)上的某些卷积与插值反向可为非确定,严格实验要单独配置确定性并记录性能代价。
一个简单的最终表示稳定性指标可写成:
是第 个样本的 维未归一化类别分数(logits), 是位移版输出, 防止分母为 0。 越小只表示对该扰动更稳定,不能替代准确率、校准度和任务安全性。
12 最常见的“能跑,但缩错了”#
- 把步幅当成纯计算优化。
stride=2改变了采样网格与可表示频率,不是与stride=1近似等价的快速实现。 - 认为最大池化天然平移不变。 窗口内的小移动可保持最大值,但峰值跨过窗口边界就会跳到另一个输出单元。
- 忽略奇数尺寸。 向下取整、
ceil_mode、不对称补边会影响哪一侧被丢掉,并在编码器—解码器或残差分支相加时引发一格错位。 - 训练和推理的 resize 不一致。 不同图像库、插值模式、
align_corners和抗混叠选项会产生系统性输入偏移。 - 用循环移位冒充真实平移。
torch.roll会把右边像素绕回左边;除非数据本来是周期的,否则应用明确补边和裁剪实现位移。 - 只检查特征图是否“更平滑”。 过强低通可让图看起来干净,却同时删掉微小病灶、细裂纹或文字笔画。
13 它们分别会在哪里失败?#
- 小目标与密集预测。 目标只有几个像素时,早期降采样可将其整体平均掉;检测、分割和关键点需保留高分辨率分支或融合浅层特征。
- 纹理本身就是信号。 材料缺陷、显微图像和遥感中的细颗粒可能是分类依据;抗混叠带宽必须用验证集和频段压力测试选择。
- 单个异常峰值。 最大池化会保留热像素、脉冲噪声或污点,并将大部分梯度集中到它。
- 边界包含语义。 医学扫描视野、地图瓦片或周期经纬网格需要不同补边;固定零补边可创造虚假边缘。
- 只追求位移稳定。 姿态估计和像素定位要求的是平移等变(Translation Equivariance),不是所有中间响应不变;终端任务决定该对齐还是该聚合。
14 与相近方法的边界#
| 方法 | 空间尺寸 | 是否可学 | 抗混叠保证 | 典型用途 |
|---|---|---|---|---|
MaxPool2d | 通常减小 | 否 | 无 | 保留最强局部响应 |
AvgPool2d | 通常减小 | 否 | 有限的盒式平滑 | 局部均值、全局平均池化 |
Conv2d(stride=2) | 减小 | 是 | 无显式保证 | 联合学习特征变换和降采样 |
| 固定低通 + 抽样 | 减小 | 低通核否 | 由滤波器带宽决定 | 降低位移敏感和高频折叠 |
interpolate(..., antialias=True) | 可大可小 | 否 | 降采样时显式抗混叠 | 图像/特征尺寸重采样 |
| 空洞卷积 | 可保持 | 是 | 不是降采样抗混叠方法 | 不减小特征图而扩大理论感受野 |
| 自适应池化 | 指定输出 | 否 | 不自动保证 | 将不同输入尺寸归约到固定网格/向量 |
自适应池化的“自适应”指它根据输入尺寸计算窗口与步幅,不是学习参数。空洞卷积虽然也“隔点读取”卷积核,但它通常保留输出网格,与把整张特征图降采样不是一件事。
15 今天真正需要记住什么?#
- 下采样将 减小一半可使后续空间计算约降为 ,并快速扩大感受野,但它必然改变可表示的空间频率。
- 直接隔点取样会让新网格表示不了的高频折叠成假低频;先低通再抽样能减少这种混叠,但不能使信息损失可逆。
- 最大池化保留最强局部响应,平均池化做固定局部平滑,步幅卷积学习局部组合;“可学”不等于“必然抗混叠”。
- 下采样逐步把精确空间位置换成更大视野与更低成本,从而构成多尺度特征层次;小目标是否在换取中消失必须实测。
- 调试时应用棋盘格、一像素平移、逐层形状和 logits 差找到第一个敏感降采样点,并与真实任务指标一起评估。
16 思考题与小练习#
- 对
[N=2,C=8,H=15,W=17]输入使用MaxPool2d(kernel_size=3,stride=2,padding=1)。分别在ceil_mode=False/True下手算输出形状,画出最后一个窗口覆盖了哪些有效位置,再用 PyTorch 断言。 - 生成
32×32棋盘格及其右移一像素版本,比较直接隔点取样、AvgPool2d(2)、MaxPool2d(2)和BlurDownsample2d。记录四种输出的均方差,解释为什么“输出更接近”不能单独证明任务更好。 - 在一个小型 CNN 中只替换第一个
stride=2层,对验证集的原图、四个一像素平移版和带细纹理子集分别统计准确率与稳定性 。哪些结果能支持使用抗混叠,哪些表明低通过强?
相关工作#
- LeCun et al. (1998), Gradient-Based Learning Applied to Document Recognition ↗:LeNet-5 以可训系数的子采样层建立早期 CNN 多尺度层次。
- Scherer, Müller & Behnke (2010), Evaluation of Pooling Operations in Convolutional Architectures for Object Recognition ↗:系统比较多种池化操作在物体识别中的表现。
- Springenberg et al. (2015), Striving for Simplicity: The All Convolutional Net ↗:展示用带步幅卷积替代显式池化的全卷积架构。
- Azulay & Weiss (2019), Why Do Deep Convolutional Networks Generalize So Poorly to Small Image Transformations? ↗:分析现代 CNN 对小平移和缩放的意外敏感性。
- Zhang (2019), Making Convolutional Networks Shift-Invariant Again ↗:将低通滤波显式放回降采样路径,检验抗混叠与位移稳定性。
17 下一篇预告#
下采样让深层特征拥有大感受野,却可把小物体和精确位置丢在浅层。下一篇将以特征金字塔网络(Feature Pyramid Network, FPN)为主线,追踪自顶向下路径与横向连接如何把深层语义送回高分辨率特征。