一张图像为何不该先拉平?卷积的局部连接、权重共享与感受野
从全连接层丢失空间结构出发,手算二维卷积,追踪 NCHW 张量、输出尺寸与感受野,并用 NumPy 和 PyTorch 2.13 实现可调试 CNN。
上一篇用残差连接给深层网络铺了一条更直接的信息和梯度路径,其中的图像残差块却还把 Conv2d 当成了黑盒。本文只拆开这个核心算子:它怎样用局部连接接住二维邻域,用权重共享在整张图上寻找同一模式,以及多层堆叠后感受野如何扩大。
01 先拉平图像,究竟丢了什么?#
假设一张红绿蓝图像(Red-Green-Blue, RGB)形状为 [3,224,224]。把它拉平后接 64 个神经元,单层参数量就是:
更根本的问题是,拉平后的向量没有显式告诉模型“哪些像素互为邻居”。图案向右移一格,原来的每条连接都要换到另一组权重。
3×3 卷积则让每个输出只看一个局部窗口,并让同一组权重滑遍所有位置。若输入 3 通道、输出 64 通道,带偏置的参数量只有:
全连接:整张图 [3,224,224] ─ flatten ─► [150528] ─每个位置独立权重─► [64]
└─ 二维位置被隐藏
卷积: 局部 3×3×3 窗口 ─相同卷积核在 H、W 上滑动─► [64,H_out,W_out]
└─ 空间网格被保留text这两个假设是卷积神经网络(Convolutional Neural Network, CNN)的归纳偏置(Inductive Bias):近处像素更可能联合成有用模式,而同一模式可能出现在不同位置。它们适合图像,却不是所有数据的普遍真理。
02 一个输出像素究竟怎样算?#
先固定 PyTorch 的“批量—通道—高度—宽度”(Batch-Channel-Height-Width, NCHW)布局:
- 输入 ;
- 权重 ;
- 偏置 ;
- 输出 。
是批量大小, 是输入/输出通道数, 是空间高度/宽度, 是核的高度/宽度。
对输出位置 ,二维卷积层计算:
是步幅(Stride), 是空洞率(Dilation), 是补零(Padding)宽度;落在边界外的 按零处理。一个输出通道 使用一只覆盖所有 输入通道的卷积核,而不是“每个 RGB 通道各算各的”。
03 用九个数手算一次滑窗#
令单样本、单通道输入和一只 2×2 核为:
设步幅为 1、不补零、偏置为 0。同一只 在四个位置复用:
窗口 (0,0) 窗口 (0,1) 窗口 (1,0) 窗口 (1,1)
[1 2] ⊙ [ 1 0] [2 0] ⊙ [ 1 0] [0 1] ⊙ [ 1 0] [1 3] ⊙ [ 1 0]
[0 1] [-1 1] [1 3] [-1 1] [2 2] [-1 1] [2 1] [-1 1]
= 2 = 4 = 0 = 0text因此:
“局部连接”表示 只依赖 左上 2×2 邻域;“权重共享”表示四个窗口都乘同一只 。如果为每个窗口分别学一只核,它仍是局部连接层(Locally Connected Layer),却已不是标准卷积。
04 权重共享为什么带来平移等变?#
记 为把输入平移 个格子。忽略边界与下采样时,卷积满足:
这叫平移等变性(Translation Equivariance):输入中的边缘向右移,特征图中的响应也向右移。它不等于平移不变性(Translation Invariance);后者要求图案移动后最终输出不变,通常还需要全局池化、数据增强或其他聚合。
输入左移/右移 Δ 同一只核 K 特征响应同向移动 Δ
□ □ ■ ■ □ □ ─────────────► [K 在每个位置复用] ─────────────► · · ↑ ↑ · ·
边界补零改变了邻域;stride > 1 丢弃了中间位置。
所以真实 CNN 只是近似、有边界的平移等变,不是任意平移下的完美定理。text05 输出尺寸为什么总容易算错?#
有效卷积核高度为 。能放下多少个起点,决定输出高度:
使用完全对称的公式。例如 :
| 参数 | 改变了什么 | 常见误解 |
|---|---|---|
kernel_size | 单层观察窗口与参数量 | 核大就必然更好 |
stride | 滑窗间隔,通常同时下采样 | 只减少计算,不丢空间信息 |
padding | 边界处可用的上下文 | 'same' 对任意 stride 都保持尺寸 |
dilation | 核采样点的间隔,不增加权重数 | 等价于更大的稠密核 |
out_channels | 学习多少种局部模式 | 等于 RGB 等原始颜色通道 |
groups | 哪些输入通道可连到哪些输出通道 | 只改变速度,不改变函数族 |
PyTorch 2.13 官方 Conv2d ↗ 文档说明:padding='same' 保持空间尺寸,但当前不支持 stride != 1;in_channels 和 out_channels 都必须能被 groups 整除。不要用猜测替代形状断言。
06 感受野怎样从 3×3 长成 9×9?#
某层一个特征位置能依赖原图的范围,叫感受野(Receptive Field)。记第 层的感受野边长为 ,相邻特征位置在原图上的间隔为 :
从 开始,追踪三层 3×3 卷积:
原图单点 Conv 3×3,s=1 Conv 3×3,s=2 Conv 3×3,s=1
r=1, j=1 ───► r=3, j=1 ───► r=5, j=2 ───► r=9, j=2
1×1 3×3 视野 5×5 视野 9×9 视野
└─ 下采样后,相邻特征跨原图 2 格text多个小核逐层把局部边缘组成更大图案,中间还能插入非线性。但公式给出的是理论感受野;训练后的有效感受野往往只在其中心区域有较大影响,不代表框内所有像素贡献相等。
07 不依赖框架,写出卷积本体#
下面实现 groups=1、dilation=1 的 NCHW 二维互相关。输入是 [N,C_in,H,W],权重是 [C_out,C_in,K_h,K_w],输出是 [N,C_out,H_out,W_out]:
import numpy as np
def conv2d_nchw(x, weight, bias=None, stride=1, padding=0):
assert x.ndim == 4 and weight.ndim == 4
n, c_in, h_in, w_in = x.shape
c_out, c_weight, k_h, k_w = weight.shape
assert c_weight == c_in
h_out = (h_in + 2 * padding - k_h) // stride + 1
w_out = (w_in + 2 * padding - k_w) // stride + 1
assert h_out > 0 and w_out > 0
x_pad = np.pad(
x,
((0, 0), (0, 0), (padding, padding), (padding, padding)),
)
y = np.empty((n, c_out, h_out, w_out), dtype=np.result_type(x, weight))
for batch in range(n):
for out_ch in range(c_out):
for i in range(h_out):
for j in range(w_out):
row, col = i * stride, j * stride
window = x_pad[
batch, :, row:row + k_h, col:col + k_w
]
y[batch, out_ch, i, j] = np.sum(
window * weight[out_ch]
)
if bias is not None:
y[batch, out_ch] += bias[out_ch]
return y
x = np.array([[[[1, 2, 0], [0, 1, 3], [2, 2, 1]]]], dtype=float)
weight = np.array([[[[1, 0], [-1, 1]]]], dtype=float)
y = conv2d_nchw(x, weight)
np.testing.assert_allclose(y, [[[[2, 4], [0, 0]]]])
assert y.shape == (1, 1, 2, 2)python这段循环为了暴露数据流,不适合生产训练。高性能库会使用向量化、专用中央处理器(Central Processing Unit, CPU)/图形处理器(Graphics Processing Unit, GPU)内核和自动算法选择;它们改变实现方式,不改变上面的张量语义。
08 从特征图到分类结果,数据怎样流?#
下面用线性整流激活(Rectified Linear Unit, ReLU)引入非线性,再把空间特征聚合为分类向量:
images [N,3,32,32]
│
├─ Conv2d(3→8,k=3,p=1) → [N,8,32,32]
├─ ReLU → [N,8,32,32]
├─ Conv2d(8→16,k=3,s=2,p=1)
│ → [N,16,16,16]
├─ ReLU → [N,16,16,16]
├─ AdaptiveAvgPool2d(1) → [N,16,1,1]
├─ flatten(start_dim=1) → [N,16]
└─ Linear(16→4) → logits [N,4]text卷积不直接“输出类别”。它把每只核对局部模式的响应保存成一个特征图(Feature Map);非线性和后续卷积再把低层边缘组成高层结构。最后的聚合与线性头才生成分类分数(logit)。
09 用 PyTorch 2.13 写出可检查的 CNN#
import torch
from torch import nn
class TinyCNN(nn.Module):
def __init__(self, num_classes=4):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 8, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(8, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
)
self.pool = nn.AdaptiveAvgPool2d((1, 1))
self.classifier = nn.Linear(16, num_classes)
def forward(self, x):
assert x.ndim == 4 and x.shape[1] == 3
features = self.features(x) # [N,16,H_out,W_out]
pooled = self.pool(features) # [N,16,1,1]
vector = torch.flatten(pooled, 1) # [N,16]
logits = self.classifier(vector) # [N,num_classes]
return logits
torch.manual_seed(7)
model = TinyCNN(num_classes=4)
images = torch.randn(5, 3, 32, 32) # float32, NCHW
labels = torch.tensor([0, 1, 2, 3, 1]) # int64, [N]
logits = model(images)
assert logits.shape == (5, 4)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
optimizer.zero_grad(set_to_none=True)
loss = loss_fn(logits, labels) # 标量
loss.backward()
assert model.features[0].weight.grad is not None
assert torch.isfinite(model.features[0].weight.grad).all()
optimizer.step()pythonConv2d(3,8,3) 的权重形状是 [8,3,3,3],它为 8 种可学习的局部模式各保存一只覆盖 3 个输入通道的核。CrossEntropyLoss 接收未经 Softmax 的 [N,4] logits 和 [N] 整数类别索引;不要在模型中先转成 argmax。
完整训练会对每个小批量(mini-batch)重复“前向→损失→清梯度→反向→更新”;验证和推理则使用 model.eval() 与 torch.inference_mode()。卷积层自身没有训练/评估(train/eval)两套行为,但与它组合的批归一化(Batch Normalization, BatchNorm)和随机失活(Dropout)有。
10 一条最短的卷积调试路径#
- 先查输入契约。 打印
shape/dtype/device/min/max;图像 batch 应是[N,C,H,W]浮点张量,不是常见图像库的[N,H,W,C]整数数组。 - 用一个冲激点测位置。 令输入只有中央一点为 1,卷积核全为 1;响应区域可以暴露 padding、stride 和通道顺序错误。
- 逐层断言形状。 不要直到
Linear报矩阵乘法错误才回头;对照输出公式查第一个偏离层。 - 与手算本体对齐。 把同一组小数据和权重复制到 NumPy 与
Conv2d,用torch.testing.assert_close比较输出。 - 尝试过拟合一个极小 batch。 若 8 个样本都无法几乎记住,先查标签、损失、学习率、梯度与数据处理,而不是继续加层。
- 观察激活和梯度。 记录每层非零比例、均值、标准差与梯度范数,找到第一个全零、NaN 或尺度突变的层。
11 最常见的“能跑,但语义错了”#
- NHWC/NCHW 混淆。 若高度恰好等于期望通道数,错布局甚至可能不立即报错;在数据边界只转换一次并断言。
- 过早 flatten。 一旦在特征提取前拉平,后续就无法使用二维权重共享。
- 认为 padding 只改尺寸。 零填充还改变边界的输入分布;反射、复制与循环填充具有不同语义。
- 忽略
groups的连接约束。groups=C_in且C_out=K C_in时是深度卷积(Depthwise Convolution);它不自动混合通道,通常还要配1×1点卷积。 - 把理论感受野当成实际利用范围。 可用输出对输入的梯度热图检查有效感受野,而不只算层数。
- 把平移等变当成不变。 对输入做一像素平移测试,同时观察中间特征与最终 logits;两者应用不同标准。
- 忽略非确定算法。 官方文档提醒,统一计算设备架构(Compute Unified Device Architecture, CUDA)上的深度神经网络库(CUDA Deep Neural Network library, cuDNN)在某些形状下可能选择非确定内核;严格复现时要单独设置确定性策略,并接受性能代价。
12 卷积与相近操作的边界#
| 方法 | 连接范围 | 空间位置间共享参数 | 主要用途/代价 |
|---|---|---|---|
| 全连接层 | 所有输入 | 不适用 | 全局混合;忽略网格邻接,参数多 |
| 局部连接层 | 局部窗口 | 否 | 可学位置特定模式;参数随空间尺寸增长 |
| 标准卷积 | 局部窗口、全输入通道 | 是 | 空间等变特征;长距依赖需堆叠 |
| 深度可分离卷积 | 局部窗口后再混通道 | 是 | 减少计算;表达约束更强 |
| 池化 | 局部统计 | 通常无参数 | 下采样/聚合;不学习线性模式 |
| 自注意力 | 通常可全局交互 | 投影权重共享,关系动态 | 长距建模;计算和数据需求不同 |
卷积在网格不规则、空间位置语义完全不同、需要一层就建模全局交互,或关键模式会任意旋转、缩放时不再天然合适。数据增强、群等变卷积、图神经网络或注意力可以针对不同结构假设,但没有一种方法只因为更“现代”就在所有数据规模上更好。
13 工程中还要防哪些失败?#
- 下采样混叠。
stride>1直接丢弃位置;输入含高频纹理时可产生别名(Aliasing),必要时先低通滤波并做平移压力测试。 - 边界成为捷径。 固定裁剪、零填充或水印可让模型学到位置偏见;可视化错误样本的激活/梯度热图。
- 名义感受野大,中心仍过度主导。 小物体、长条结构和远距关系仍可被忽略;需要多尺度特征、空洞卷积或全局交互。
- 小数据上过拟合。 参数少于全连接层不等于不过拟合;仍要独立验证集、合法增强、权重衰减与早停。
- 只看准确率。 医学、遥感等任务还应分析群体、设备、时间和空间分布偏移,并检查模型是否利用了背景捷径。
14 今天真正需要记住什么?#
- 卷积的两个核心假设是局部连接与空间权重共享;它们保留网格结构、减少参数并带来有边界的平移等变性。
Conv2d把[N,C_in,H,W]与[C_out,C_in/groups,K_h,K_w]权重组合为[N,C_out,H_out,W_out];每个维度都有明确语义。- 输出尺寸由核、步幅、填充和空洞率共同决定;每层应用公式和运行时断言双重核对。
- 多层小核通过堆叠扩大理论感受野,但有效感受野、边界效应与下采样信息损失仍需实测。
- 最可靠的卷积调试不是盲目换架构,而是从小数据、冲激输入、逐层形状、框架对齐和小 batch 过拟合逐步缩小问题。
15 思考题与小练习#
- 对一个
[N=2,C=3,H=7,W=7]输入使用Conv2d(3,5,kernel_size=3,stride=2,padding=2,dilation=2)。手算输出形状、权重形状与含偏置的参数量,再用 PyTorch 断言。 - 把手算例的输入右移一格并在左侧补零。分别在
padding=0与padding=1下比较输出;哪些位置符合平移等变,哪些因边界失效? - 在
TinyCNN的每个卷积后保留激活梯度,对一个 logit 反向传播并画输入梯度热图。把模型改为三层后,比较理论感受野和非零/高强度梯度区域。
相关工作#
- LeCun et al. (1998), Gradient-Based Learning Applied to Document Recognition ↗:以 LeNet-5 系统展示局部感受野、权重共享与端到端视觉学习。
- Krizhevsky, Sutskever & Hinton (2012), ImageNet Classification with Deep Convolutional Neural Networks ↗:大规模 GPU 卷积网络在 ImageNet 上的标志性工作。
- Simonyan & Zisserman (2015), Very Deep Convolutional Networks for Large-Scale Image Recognition ↗:展示堆叠小
3×3核构造深层特征层次的经典设计。 - Dumoulin & Visin (2016), A Guide to Convolution Arithmetic for Deep Learning ↗:系统梳理卷积、填充、步幅、转置卷积与输出尺寸。
- Luo et al. (2016), Understanding the Effective Receptive Field in Deep Convolutional Neural Networks ↗:区分理论感受野与实际梯度影响集中的有效感受野。
16 下一篇预告#
卷积已经能在空间网格上复用局部模式,但高分辨率特征图会让计算和显存快速增长。下一篇将比较池化、带步幅卷积与抗混叠下采样,追踪它们怎样生成多尺度特征层次,又分别丢掉什么。