AI 学习课程 / LESSON 08

第八节:卷积神经网络

从一个 2×2 小核在图像上滑动开始,逐格算出结果,再扩展到通道、池化和多层 CNN。每个形状都能对上真实的 PyTorch。

卷积逐格演算NCHW 形状追踪CPU 小实验离线 HTML

01 / 四个轴各表示什么

一批图像是 NCHW

PyTorch 常用 (N,C,H,W):N 张图、C 个通道、每个通道 H 行 W 列。RGB 的红绿蓝是三张对齐的二维网格;灰度图一般只有一个通道。单张灰度图常为 (1,H,W),增加批次轴后为 (N,1,H,W)。

N:样本数

8 张图可同时送入模型。

C:通道数

RGB 是 3,灰度是 1。

H/W:空间尺寸

行和列,不把通道当行。

X.shape=(8,3,32,32) X[0,1,5,7]:第 0 张图、通道 1、第 5 行第 7 列。

02 / 同一组权重到处复用

局部点积与参数共享

4×4 输入、2×2 核、无填充、步长 1,核有 3×3 个落点。每个落点对局部 4 个数逐项相乘,再求和、加偏置。同一组核权重在所有位置重复使用。

Y[r,c]=ΣᵢΣⱼ X[r+i,c+j]K[i,j]+b K=[[1,0],[0,−1]],b=0 Y[0,0]=1×1+2×0+5×0+6×(−1)=−5。

实验 A:点击输出,观察输入窗口

输入 X(4×4)
核 K(2×2)
输出 Y(3×3)

PyTorch Conv2d 使用互相关约定:核不翻转,虽然习惯上仍叫卷积。非对称核 [[1,2],[3,4]] 放在左上角得到 1×1+2×2+5×3+6×4=44;代码也核对了这个数。

Conv2d 官方定义、形状和权重布局。

03 / 数核能落在哪里

输出高宽公式

步长 s 决定移动间隔;padding p 在每侧补 p 格;dilation d 拉开核内部采样点。有效跨度为 d(k−1)+1。实验使用正方形核,H、W 分别计算。

H_out=floor((H+2p−d(k−1)−1)/s+1) W_out=floor((W+2p−d(k−1)−1)/s+1) 输出:(N,C_out,H_out,W_out)。

实验 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 个输出

输入通道 0
核:全 1
输入通道 1
核:全 −1
输入通道 2
核:主对角线为 1

若 C_out=2,则完整结果形状是 (1,2,1,1);这里只展开第 0 个输出的 −11.5。第 1 个输出通道有另一套权重。不同空间位置会复用对应输出通道的同一套权重。若设 groups,连接分组,权重第二轴变为 C_in/groups。

05 / 缩小空间网格

每个池化窗口留下最大值

MaxPool2d(2,stride=2) 把一个通道中的每个 2×2 窗口变成一个数。它不增加可训练参数,也不会减少通道数。

实验 D:点击一个池化输出

输入 4×4
输出 2×2

空间位置略有变化时,局部最大值有时不变;但池化会丢失非最大值和精确位置,不能保证对任意平移完全不变。本例固定无 padding、无 dilation 且 ceil_mode=False。MaxPool2d 官方说明。

06 / 更深一格能“看”哪里

逐层计算理论感受野

某层一个输出格可能受到原图哪些位置影响,称为感受野。忽略边界,令 R 为理论跨度,J 为相邻输出格在原图上的中心步距,输入层从 R=1、J=1 开始。

R_new=R_old+(k−1)·d·J_old J_new=J_old·s padding 会影响对齐和边界,但不增加内部理论跨度。

实验 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 张,样本由不同种子生成。验证损失选轮次;测试只在选好权重后运行一次。

逐轮训练/验证交叉熵

绿色:训练蓝色:验证

这只是简单几何线条的合成任务;高准确率证明这里的代码能学会这类输入,不能代表真实照片分类能力。第九节再做完整项目和错误分析。

逐轮 CSV · 实际结果 JSON · 最佳模型权重

09 / 什么改变仍保留答案

数据增强与迁移学习

训练时可以改变线条位置、加入适量噪声,但必须检查标签是否仍成立。此任务中竖线左右翻转仍是竖线;旋转 90° 会变横线,若标签不改就是错误监督。

实验 F:变换后原标签还能用吗?

真实任务里镜像也不总保留标签(例如左右文字)。验证/测试采用一致、确定的处理;输入标准化统计量仅从训练集拟合。迁移学习可保留合适的预训练视觉网络并训练新分类头,但要核对权重要求的预处理、尺寸、通道与类别。当前项目只安装 CPU PyTorch,本节不下载预训练权重,不声称做过迁移学习;第九节再比较。

10 / 动手核对

本地 PyTorch 练习

配套 lesson-08-cnn.py 使用 uv 管理的 Python 和已有 CPU PyTorch;没有新增依赖或下载数据。

uv run python lessons/lesson-08-cnn.py
  1. 点选一个卷积输出,手算输入窗口的四个乘积,再和 PyTorch 核对。
  2. 实验 B 设置 H=W=8、k=3、s=1、p=0、d=2;先计算有效核跨度和输出尺寸。
  3. 解释多通道第 0 个输出为什么是 −11.5,偏置加了几次。
  4. 逐层写出 N、C、H、W,并解释展平后为何是 512。
  5. 独立尝试改一个因素,调整分类头对应维度,保留验证结果;不要用测试集挑设置。

11 / 检查理解

五道自测

1. (8,3,32,32) 经 Conv2d(3,6,3,padding=1) 得到?

2. 默认 groups=1 的 Conv2d(3,6,3) 权重形状?

3. PyTorch Conv2d 计算前会翻转核吗?

4. (B,4,8,8) 经 MaxPool2d(2,2) 得到?

5. 竖线旋转 90° 后能不改标签吗?

下一节:图像分类项目。打开第九节互动项目,从这个小模型出发,做可靠对照与错误分析。