01 / 四个轴各表示什么
一批图像是 NCHW
PyTorch 常用 (N,C,H,W):N 张图、C 个通道、每个通道 H 行 W 列。RGB 的红绿蓝是三张对齐的二维网格;灰度图一般只有一个通道。单张灰度图常为 (1,H,W),增加批次轴后为 (N,1,H,W)。
8 张图可同时送入模型。
RGB 是 3,灰度是 1。
行和列,不把通道当行。
02 / 同一组权重到处复用
局部点积与参数共享
4×4 输入、2×2 核、无填充、步长 1,核有 3×3 个落点。每个落点对局部 4 个数逐项相乘,再求和、加偏置。同一组核权重在所有位置重复使用。
实验 A:点击输出,观察输入窗口
PyTorch Conv2d 使用互相关约定:核不翻转,虽然习惯上仍叫卷积。非对称核 [[1,2],[3,4]] 放在左上角得到 1×1+2×2+5×3+6×4=44;代码也核对了这个数。
03 / 数核能落在哪里
输出高宽公式
步长 s 决定移动间隔;padding p 在每侧补 p 格;dilation d 拉开核内部采样点。有效跨度为 d(k−1)+1。实验使用正方形核,H、W 分别计算。
实验 B:改变尺寸和核参数
例如 (4,3,7,9) 经 Conv2d(3,6,3,padding=1) 得到 (4,6,7,9)。默认 groups=1、含偏置时,参数个数 6×3×3×3+6=168;改变批次 N 或输入高宽不会改变参数数。padding=1 只有在合适核、步长和 dilation 下才保留空间尺寸。
04 / RGB 不先平均
每个输出通道跨输入通道求和
默认 groups=1 时,一个输出通道有 C_in 张小核;分别计算,再把通道贡献加起来,只加一次该输出通道的偏置。权重形状 (C_out,C_in,kH,kW)。
实验 C:三通道 → 第 0 个输出
若 C_out=2,则完整结果形状是 (1,2,1,1);这里只展开第 0 个输出的 −11.5。第 1 个输出通道有另一套权重。不同空间位置会复用对应输出通道的同一套权重。若设 groups,连接分组,权重第二轴变为 C_in/groups。
05 / 缩小空间网格
每个池化窗口留下最大值
MaxPool2d(2,stride=2) 把一个通道中的每个 2×2 窗口变成一个数。它不增加可训练参数,也不会减少通道数。
实验 D:点击一个池化输出
空间位置略有变化时,局部最大值有时不变;但池化会丢失非最大值和精确位置,不能保证对任意平移完全不变。本例固定无 padding、无 dilation 且 ceil_mode=False。MaxPool2d 官方说明。
06 / 更深一格能“看”哪里
逐层计算理论感受野
某层一个输出格可能受到原图哪些位置影响,称为感受野。忽略边界,令 R 为理论跨度,J 为相邻输出格在原图上的中心步距,输入层从 R=1、J=1 开始。
实验 E:Conv3 → Pool2 → Conv3
三层后内部一个输出的理论感受野为 8×8,步距 J=2。代码使用梯度的非零范围核对:为了确保池化窗口每一位置都传梯度,核对时用同尺寸的平均池化;最大池化一次具体反向通常只沿选中的最大值传梯度。理论感受野是可能依赖的范围,不是各点实际贡献大小。
07 / 接成小网络
逐层看 N、C、H、W
| 操作 | B=2 的输出形状 | 推导 |
|---|---|---|
| 输入灰度图 | (2,1,16,16) | 2 张,1 通道 |
| Conv2d(1,4,3,p=1) → ReLU | (2,4,16,16) | 空间保留,4 输出通道 |
| MaxPool2d(2,2) | (2,4,8,8) | 高宽减半 |
| Conv2d(4,8,3,p=1) → ReLU | (2,8,8,8) | 8 输出通道 |
| Flatten(start_dim=1) | (2,512) | 8×8×8,保留批次 |
| Linear(512,2) | (2,2) | 每图两个原始 logits |
class SmallCNN(torch.nn.Module):
def __init__(self):
super().__init__()
self.conv1 = torch.nn.Conv2d(1, 4, 3, padding=1)
self.pool = torch.nn.MaxPool2d(2, stride=2)
self.conv2 = torch.nn.Conv2d(4, 8, 3, padding=1)
self.classifier = torch.nn.Linear(8 * 8 * 8, 2)
def forward(self, x):
x = torch.relu(self.conv1(x))
x = self.pool(x)
x = torch.relu(self.conv2(x))
return self.classifier(torch.flatten(x, start_dim=1))CrossEntropyLoss 接收形状 (B,2) 的原始 logits 和形状 (B,) 的整数类别编号 0/1;不要先 softmax。本例与上一节单 logit 的 BCEWithLogitsLoss 是两种不同但都可行的二分类写法,标签形状也不同。官方图像分类练习。
08 / 从机制走到训练
实际 CPU 分类实验
代码生成 16×16 灰度图,类别是随机位置的竖线或横线,并加独立噪声。训练 160 张、验证 64 张、测试 64 张,样本由不同种子生成。验证损失选轮次;测试只在选好权重后运行一次。
逐轮训练/验证交叉熵
这只是简单几何线条的合成任务;高准确率证明这里的代码能学会这类输入,不能代表真实照片分类能力。第九节再做完整项目和错误分析。
09 / 什么改变仍保留答案
数据增强与迁移学习
训练时可以改变线条位置、加入适量噪声,但必须检查标签是否仍成立。此任务中竖线左右翻转仍是竖线;旋转 90° 会变横线,若标签不改就是错误监督。
实验 F:变换后原标签还能用吗?
真实任务里镜像也不总保留标签(例如左右文字)。验证/测试采用一致、确定的处理;输入标准化统计量仅从训练集拟合。迁移学习可保留合适的预训练视觉网络并训练新分类头,但要核对权重要求的预处理、尺寸、通道与类别。当前项目只安装 CPU PyTorch,本节不下载预训练权重,不声称做过迁移学习;第九节再比较。
10 / 动手核对
本地 PyTorch 练习
配套 lesson-08-cnn.py 使用 uv 管理的 Python 和已有 CPU PyTorch;没有新增依赖或下载数据。
uv run python lessons/lesson-08-cnn.py- 点选一个卷积输出,手算输入窗口的四个乘积,再和 PyTorch 核对。
- 实验 B 设置 H=W=8、k=3、s=1、p=0、d=2;先计算有效核跨度和输出尺寸。
- 解释多通道第 0 个输出为什么是 −11.5,偏置加了几次。
- 逐层写出 N、C、H、W,并解释展平后为何是 512。
- 独立尝试改一个因素,调整分类头对应维度,保留验证结果;不要用测试集挑设置。
11 / 检查理解