目标:写给未来的自己,每次回看都能 从概念→公式→实现→调参 一条龙回忆起来。

  • ✅ 公式与详细计算过程(输出尺寸、参数量、FLOPs、感受野)
  • ASCII/mermaid 图示(卷积滑窗、网络结构、残差块)
  • PyTorch 代码(可直接跑:模型、训练、评估、形状追踪)
  • ✅ 经典 CNN 架构清单(LeNet、AlexNet、VGG、ResNet)
  • ✅ 常见坑与调参 checklist

1. 卷积到底在做什么?(直觉 + 数学)

1.1 直觉

  • 卷积核(filter/kernel)像一个可学习的模板,在图像上滑动,计算局部“相似度”,输出 特征图(feature map)
  • 两个关键理念:局部连接(只看小窗口)+ 参数共享(同一核在全图复用)。

1.2 符号与公式(二维卷积)

给定输入张量 (X\in\mathbb{R}^{H\times W\times C_{in}}),卷积核 (W\in\mathbb{R}^{k\times k\times C_{in}\times C_{out}}),步幅 (S),填充 (P):

  • 输出空间尺寸(无空洞/dilation=1):

    Hout=H+2PkS+1,Wout=W+2PkS+1.H_{out}=\left\lfloor\frac{H+2P-k}{S}\right\rfloor+1,\qquad W_{out}=\left\lfloor\frac{W+2P-k}{S}\right\rfloor+1.

  • 参数量

    #params=kkCinCout+Cout  (若带偏置)\#\text{params}=k\cdot k\cdot C_{in}\cdot C_{out}+C_{out}\;\text{(若带偏置)}

  • 一次前向的乘加 FLOPs(近似)

    MACs=HoutWoutCout(kkCin)(FLOPs2×MACs)\text{MACs}=H_{out}\cdot W_{out}\cdot C_{out}\cdot (k\cdot k\cdot C_{in})\quad(\text{FLOPs} \approx 2\times\text{MACs})

空洞卷积(dilation=d)时的等效核大小:(k_{eff}=k+(k-1)(d-1))。把上式的 (k) 换成 (k_{eff})。

1.3 数值例子:5×5 输入 ⨉ 3×3 卷积核

输入(单通道)(X) 与卷积核 (K):

1
2
3
4
5
6
7
8
9
10
11
X = 5x5
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

K = 3x3 (Sobel-like)
1 0 -1
1 0 -1
1 0 -1

设 (S=1, P=0)。输出尺寸:(H_{out}=W_{out}=\lfloor(5-3)/1\rfloor+1=3)。

左上角位置 (0,0) 的输出 y[0,0] 计算

patch=[123678111213],K=[101101101]y[0,0]=11+20+3(1)+61+70+8(1)+111+120+13(1)=(13)+(68)+(1113)=2+(2)+(2)=6.\begin{aligned} \text{patch} &= \begin{bmatrix}1&2&3\\6&7&8\\11&12&13\end{bmatrix},\quad K=\begin{bmatrix}1&0&-1\\1&0&-1\\1&0&-1\end{bmatrix} \\ \Rightarrow y[0,0] &= 1\cdot1+2\cdot0+3\cdot(-1) + 6\cdot1+7\cdot0+8\cdot(-1) + 11\cdot1+12\cdot0+13\cdot(-1)\\ &= (1-3) + (6-8) + (11-13) = -2 + (-2) + (-2) = -6. \end{aligned}

同理可算出 3×3 输出:

1
2
3
4
y ≈
-6 -6 -6
-6 -6 -6
-6 -6 -6

(因为该核检测“竖直边缘”,此示例是等差矩阵,响应相同。)

多通道输入时:先对每个通道做相同核尺寸的互相关(cross-correlation),再对通道求和,再加偏置,得到一个输出通道;重复 (C_{out}) 个核得到全部输出通道。


2. 形状/参数/感受野/计算量 —— 一次搞懂

2.1 输出形状速算表

公式 备注
Conv2d(k,S,P) (H’ = \lfloor (H+2P-k)/S\rfloor+1) 宽同理;dilation 用 (k_{eff}) 替换
MaxPool2d(k,S) (H’ = \lfloor (H - k)/S\rfloor+1) 宽同理;常用 k=2,S=2
UpSample(×2) (H’ = 2H) 语义分割上采样常用

2.2 参数量 & MACs 速算

  • Conv 参数 = (k^2 C_{in} C_{out} + C_{out})。
  • Conv MACs = (H’W’ C_{out} (k^2 C_{in}))。
  • FC 参数 = 输入维度 × 输出维度 + 输出偏置。

粗略显存估算(激活占主):(\sim\sum_l B\cdot H_l W_l C_l \cdot 4) bytes(float32),双向回传约×2。

2.3 感受野(Receptive Field)递推

设第 (l) 层核大小 (k_l)、步幅 (s_l),定义:

  • 有效步距(jump)(j_l = j_{l-1}\cdot s_l),初始 (j_0=1)
  • 感受野 (r_l = r_{l-1} + (k_l-1)\cdot j_{l-1}),初始 (r_0=1)

:Conv3×3(s=1) → Pool2×2(s=2) → Conv3×3(s=1)

  • 层1:(r_1=1+(3-1)\cdot1=3),(j_1=1)
  • 层2:(r_2=3+(2-1)\cdot1=4),(j_2=1\cdot2=2)
  • 层3:(r_3=4+(3-1)\cdot2=8),(j_3=2)

解释:第3层的一个像素,看到输入图的 8×8 区域。

ASCII 感受野示意

1
2
3
4
5
6
7
8
9
Input (· 为像素)
· · · · · · · ·
· · · · · · · ·
· · [ [ [ [ ] ] ] · · ← 第3层中间元素对应的输入覆盖区域(8x8)
· · [ [ [ [ ] ] ] · ·
· · [ [ [ [ ] ] ] · ·
· · [ [ [ [ ] ] ] · ·
· · · · · · · ·
· · · · · · · ·

3. 反向传播(轻量推导备忘)

3.1 卷积层梯度

  • 设损失对输出梯度为 (\delta=\partial L/\partial Y)。
  • 对权重:(\partial L/\partial W = X \star \delta)(与前向相同“互相关”形式,对 batch 与空间求和)。
  • 对输入:(\partial L/\partial X = \delta * \text{rot180}(W))(* 为卷积,核旋转180°,并考虑 stride/pad 对齐)。

3.2 ReLU / Pooling

  • ReLU:(\frac{\partial L}{\partial x}=\mathbf{1}[x>0]\cdot\frac{\partial L}{\partial y})。
  • MaxPool:把前向位置的 argmax mask 传回,非最大位置梯度为0;AvgPool 把梯度 均分 回窗口内每个元素。

3.3 BatchNorm(2d)(推导要点)

  • 以通道维做均值/方差:(\hat{x}=(x-\mu)/\sqrt{\sigma^2+\epsilon}),(y=\gamma\hat{x}+\beta)。
  • 梯度:(\partial L/\partial \gamma=\sum(\delta\cdot\hat{x})),(\partial L/\partial \beta=\sum\delta)。对 (x) 的梯度需链式法则展开(略)。

3.4 Softmax + CrossEntropy(数值稳定)

  • (p_i=\frac{e^{z_i}}{\sum_j e^{z_j}}),(L= -\sum_i y_i\log p_i)。
  • 经典结果:(\partial L/\partial z = p - y)(避免手写 Softmax + Log 的梯度细节,框架已实现)。

4. 经典网络结构与图示

4.1 结构速览(Mermaid 可选,Hexo 需装 mermaid 插件)

graph LR
  I[Input HxWxC] --> C1[Conv3x3 + ReLU]
  C1 --> P1[MaxPool2x2]
  P1 --> C2[Conv3x3 + ReLU]
  C2 --> P2[MaxPool2x2]
  P2 --> F[Flatten]
  F --> FC1[FC 128 + ReLU]
  FC1 --> FC2[FC num_classes]
  FC2 --> SM[Softmax]

4.2 残差块(ResNet Basic Block)

graph LR
  x((x)) --> A[Conv3x3 + BN + ReLU]
  A --> B[Conv3x3 + BN]
  x -->|identity/1x1| add((+))
  B --> add
  add --> relu[ReLU]

4.3 ASCII 版(不依赖 mermaid)

1
2
3
4
5
Input -> [Conv3x3]->[ReLU]->[Pool2x2]->[Conv3x3]->[ReLU]->[Pool2x2]->[Flatten]->[FC]->[Softmax]

Residual Block:
x ──▶ Conv3x3 ─▶ BN ─▶ ReLU ─▶ Conv3x3 ─▶ BN ──▶ (+) ─▶ ReLU
╰──────────────────────── identity / 1x1 conv ─────╯

4.4 LeNet / AlexNet / VGG / ResNet(形状追踪示例)

VGG-16 为例(输入 224×224×3):

  • Block1: Conv3×3@64 → Conv3×3@64 → MaxPool2×2 → (112×112×64)
  • Block2: Conv3×3@128 → Conv3×3@128 → MaxPool2×2 → (56×56×128)
  • Block3: Conv3×3@256 ×3 → MaxPool2×2 → (28×28×256)
  • Block4: Conv3×3@512 ×3 → MaxPool2×2 → (14×14×512)
  • Block5: Conv3×3@512 ×3 → MaxPool2×2 → (7×7×512)
  • Flatten → FC4096 → FC4096 → FC1000 → Softmax

形状变化靠 输出公式 一步步核对即可;感受野可用第 2.3 节递推。


5. PyTorch 实战:可直接运行的最小 CNN(含形状追踪)

环境:Python 3.8+,PyTorch,torchvision。若无 GPU 也可在 CPU 跑 MNIST。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(64 * 7 * 7, 128)
self.fc2 = nn.Linear(128, num_classes)

def forward(self, x):
x = F.relu(self.conv1(x)) # [B,32,28,28]
x = self.pool(x) # [B,32,14,14]
x = F.relu(self.conv2(x)) # [B,64,14,14]
x = self.pool(x) # [B,64,7,7]
x = torch.flatten(x, 1) # [B,64*7*7]
x = F.relu(self.fc1(x))
x = self.fc2(x) # logits
return x

# 形状追踪 hook(可选)
def trace_shapes(model, sample):
print("\n[Shape Trace]")
def hook(name):
def fn(mod, inp, out):
ishape = tuple(inp[0].shape)
oshape = tuple(out.shape)
print(f"{name:15s}: {ishape} -> {oshape}")
return fn
handles = []
for name, m in model.named_modules():
if isinstance(m, (nn.Conv2d, nn.MaxPool2d, nn.Linear)):
handles.append(m.register_forward_hook(hook(name)))
model.eval();
with torch.no_grad():
_ = model(sample)
for h in handles: h.remove()

# 输出尺寸/参数量/感受野计算器(简版)
def conv2d_out(H, W, k, s=1, p=0, d=1):
keff = k + (k-1)*(d-1)
Hout = (H + 2*p - keff)//s + 1
Wout = (W + 2*p - keff)//s + 1
return Hout, Wout

def params_conv(k, cin, cout, bias=True):
return k*k*cin*cout + (cout if bias else 0)

# Demo 训练(MNIST)
if __name__ == "__main__":
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
tfm = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,)),
])
train_ds = datasets.MNIST(root='./data', train=True, download=True, transform=tfm)
test_ds = datasets.MNIST(root='./data', train=False, download=True, transform=tfm)
train_loader = DataLoader(train_ds, batch_size=128, shuffle=True, num_workers=2)
test_loader = DataLoader(test_ds, batch_size=256, shuffle=False, num_workers=2)

model = SimpleCNN().to(device)
print(model)

# 形状追踪
sample = torch.randn(1,1,28,28, device=device)
trace_shapes(model, sample)

# 计算参数量
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total Params: {total_params} | Trainable: {trainable_params}")

# 训练配置
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
criterion = nn.CrossEntropyLoss()

for epoch in range(3):
model.train(); running = 0.0
for x, y in train_loader:
x, y = x.to(device), y.to(device)
logits = model(x)
loss = criterion(logits, y)
optimizer.zero_grad(); loss.backward(); optimizer.step()
running += loss.item()
print(f"Epoch {epoch+1} | Loss {running/len(train_loader):.4f}")

# 评估
model.eval(); correct=0; total=0
with torch.no_grad():
for x,y in test_loader:
x,y = x.to(device), y.to(device)
pred = model(x).argmax(dim=1)
total += y.size(0)
correct += (pred==y).sum().item()
print(f"Test Acc: {100*correct/total:.2f}%")

6. 典型计算题(带完整过程)

题 1:输出尺寸与参数量

已知:输入 224×224×3;Conv7×7@64,S=2,P=3;后接 MaxPool3×3,S=2。

  • Conv 输出:(H’ = \lfloor(224+2\cdot3-7)/2\rfloor+1 = 112),同理 (W’=112),通道 64 → 112×112×64
  • Conv 参数:(7\cdot7\cdot3\cdot64 + 64 = 9{,}472)。
  • Pool 输出:(H’’ = \lfloor(112-3)/2\rfloor+1 = 55)(若 P=0),→ 55×55×64

题 2:感受野

堆叠:Conv3×3(s=1) → Conv3×3(s=1) → Pool2×2(s=2) → Conv3×3(s=1)

  • 递推((r_0=1, j_0=1)):
    • L1:(r_1=1+(3-1)\cdot1=3,\ j_1=1)
    • L2:(r_2=3+(3-1)\cdot1=5,\ j_2=1)
    • L3:(r_3=5+(2-1)\cdot1=6,\ j_3=2)
    • L4:(r_4=6+(3-1)\cdot2=10,\ j_4=2)
      → 第4层每个像素看输入 10×10 区域。

题 3:FLOPs 估算

输入 56×56×64,Conv3×3@128,S=1,P=1 → 输出 56×56×128。

  • MACs = (56\cdot56\cdot128\cdot(3\cdot3\cdot64)) ≈ 231M;FLOPs ≈ 462M。

7. 常见坑与调参清单

  • 形状对不上:逐层用输出公式核对;在 PyTorch 里加 shape hook(上文提供)。
  • 数值爆炸/梯度消失:用 ReLU/LeakyReLU、BatchNorm;学习率热身 + 余弦退火;权重衰减。
  • 过拟合:数据增强(翻转、裁剪、颜色抖动)、Dropout、Weight Decay、早停。
  • 训练慢:用更大的 batch(显存允许)、混合精度(torch.cuda.amp)、少用过大的 k/通道数。
  • 内存不够:减小分辨率/批大小;检查中间激活是否被意外保存;with torch.no_grad() 包裹评估。
  • 检测/分割任务:单阶段(YOLO/SSD)vs 两阶段(Faster R-CNN);分割常用 FCN/U-Net/DeepLab(空洞卷积、ASPP)。

8. 附录:可复制图示(Markdown/ASCII)

8.1 卷积滑窗(3×3, stride=1)

1
2
3
4
5
6
7
8
┌────────────── 5x5 输入 ──────────────┐
│ 1 2 3 4 5 │
│ 6 7 8 9 10 │ ← 3x3 Kernel 覆盖区域每次向右/向下移动 1 格
│ 11 12 13 14 15 │
│ 16 17 18 19 20 │
│ 21 22 23 24 25 │
└─────────────────────┘
→ 输出 3x3:每个位置是覆盖块与核元素乘加之和

8.2 VGG Block(ASCII)

1
[Input] -> Conv3x3 -> ReLU -> Conv3x3 -> ReLU -> MaxPool2x2

8.3 ResNet 残差(ASCII)

1
2
x ── Conv3x3 ─ BN ─ ReLU ─ Conv3x3 ─ BN ──▶ (+) ─ ReLU
╰──────────────── identity / Conv1x1 ─────╯

使用建议

  1. Hexo 若需渲染公式,请启用 hexo-mathkatex 插件;
  2. Mermaid 图需安装 hexo-filter-mermaid-diagrams 或同类插件;
  3. 若不装插件,本文已提供 ASCII 备选图 与完整公式文本,不影响阅读。