大作业 · 从手写线性层到图像分类应用

Views: --

这次大作业不是只调用现成网络跑一个准确率,而是沿着一条完整路线逐步深入:先手写线性层的反向传播,再搭建 30 层以上的残差 MLP,比较训练组件,随后在 CIFAR-10 上正面对比 MLP 与 CNN,最后完成一个可交互的食品图像分类应用。

任务总览

部分数据集核心要求本次实现
Task 1MNIST自定义线性层,训练 6 层 MLP手写 forward/backward,四组超参数实验
Task 2‑1MNIST训练 30 层以上 MLP32 层残差 MLP,自定义 BatchNorm1d
Task 2‑2MNIST比较训练设计优化器、初始化、激活函数三组对照
Task 2‑3CIFAR-10深层 MLP 与 10 层以上 CNN32 层 ResMLP 对比 10 层卷积网络
Task 3自建食品集完成自选分类应用ResNet-18 迁移学习与桌面 GUI

原题硬指标与提交规则

原始题目不仅规定任务方向,还给出了分值和验收线:

  • Task 1 占 60 分:6 层 MLP 的训练、测试准确率都要超过 90%,提交自定义线性层、模型代码、超参数和训练日志;
  • Task 2 占 30 分:MNIST 上 30 层以上 MLP 的训练、测试准确率都要超过 80%;若使用 normalization,必须自己实现前向和反向,否则扣 5 分;优化器、学习率、初始化和非线性层至少选两类做实验;CIFAR-10 上的深层 MLP 与 10 层以上 CNN 都要达到训练准确率 90% 以上、测试或验证准确率 50% 以上;
  • Task 3 占 10 分:原题要求构建包含自己照片的数据集,给出训练与测试准确率曲线和具体分类案例,并通过 PPT 答辩;不答辩最多得 5 分;
  • 最多两人组队,组队必须完成 Task 3,否则额外扣 5 分;答辩同学的紧截止时间是 2025 年 12 月 31 日,宽限截止时间是 2026 年 1 月 7 日,宽限期提交额外扣 5 分。

下面既记录这些原始要求,也区分“题目要求”与“本组实际实现”,避免把二者混成同一事实。

Task 1:手写线性层与六层 MLP

前向传播

一个 mini-batch 记为 XRB×dinX\in\mathbb R^{B\times d_{\text{in}}},权重为 WRdout×dinW\in\mathbb R^{d_{\text{out}}\times d_{\text{in}}},偏置为 bRdoutb\in\mathbb R^{d_{\text{out}}}。线性层计算:

Y=XW+bY=XW^\top+b

偏置会沿 batch 维广播。反向传播必须保存 XXWW;计算偏置梯度只需知道偏置是否存在,并不依赖 bb 的数值。当前实现把 bb 一并保存,是接口实现上的方便选择。

手动反向传播

设上一层传回 G=L/YG=\partial L/\partial Y,利用矩阵微分可得:

LX=GW\frac{\partial L}{\partial X}=GW LW=GX\frac{\partial L}{\partial W}=G^\top X Lb=n=1BGn\frac{\partial L}{\partial b}=\sum_{n=1}^{B}G_n

维度检查很重要:GGB×doutB\times d_{\text{out}},所以 GWGW 恢复为输入形状 B×dinB\times d_{\text{in}},而 GXG^\top X 恢复为权重形状 dout×dind_{\text{out}}\times d_{\text{in}}

实现分为两层:torch.autograd.Function 负责保存张量和返回三个梯度,nn.Module 负责注册 weightbias 并调用该函数。这样既真正手写了 backward,又能像普通 nn.Linear 一样放进模型和优化器。

六层网络

MNIST 的 28×2828\times28 灰度图先展平成 784 维,网络宽度依次为:

784512256128643210784\to512\to256\to128\to64\to32\to10

前五个线性层后接 ReLU,最后 10 维 logits 直接送入交叉熵。若再手动加 Softmax,会与 CrossEntropyLoss 内部的 LogSoftmax 重复。

实验统一训练 20 epochs,优化器为 Adam,比较:

  • 固定 batch size 64,学习率 0.001 与 0.005;
  • 固定学习率 0.001,batch size 64 与 128。

不同学习率下六层 MLP 的损失曲线

在这组代码和随机运行中,lr=0.001, batch_size=64 是稳定的参考配置:测试准确率长期约为 98%,第 16 轮记录为 98.35%,最终为 98.14%。lr=0.005 虽然早期也能下降,但测试损失明显振荡,最终测试准确率为 97.61%。固定学习率 0.001 的另一轮 batch 对照中,64 与 128 的最终测试准确率分别为 98.21% 和 98.23%,几乎持平;64 的整体曲线略稳,不能据此宣称它在最终分数上更高。

这只能说明当前网络、数据预处理与训练预算下的表现,不能推广为“较小 batch 或较小学习率永远更好”。

Task 2-1:32 层残差 MLP

把 MLP 加深到 30 层以上,难点不只是参数更多。连续非线性变换会让梯度传播变差,网络甚至可能比浅层模型更难优化。本实现采用残差块:

y=ReLU(x+F(x))\boldsymbol y=\operatorname{ReLU} \left(\boldsymbol x+F(\boldsymbol x)\right)

其中:

F=LinearBNReLULinearBNF=\operatorname{Linear}\to\operatorname{BN} \to\operatorname{ReLU}\to\operatorname{Linear}\to\operatorname{BN}

实际代码顺序是 Linear → BatchNorm → ReLU → Linear → BatchNorm,再与输入相加并做 ReLU。跳连提供了恒等信息通道;即使残差分支暂时没学好,梯度仍可沿加法路径向前传播。

自定义 BatchNorm1d

训练阶段对每个特征维统计 batch 均值和方差:

μ=1Bi=1Bxi,σ2=1Bi=1B(xiμ)2\mu=\frac1B\sum_{i=1}^{B}x_i, \qquad \sigma^2=\frac1B\sum_{i=1}^{B}(x_i-\mu)^2 x^i=xiμσ2+ε,yi=γx^i+β\hat x_i=\frac{x_i-\mu}{\sqrt{\sigma^2+\varepsilon}}, \qquad y_i=\gamma\hat x_i+\beta

实现手写了输入、γ\gammaβ\beta 的梯度,并注册 running_meanrunning_var 供推理阶段使用。这里有一项值得留意的实现口径:训练方差采用 unbiased=False,运行方差也直接用它更新;只要训练与推理行为经过验证即可,但它与某些框架内置 BatchNorm 对运行方差的细节不一定逐项相同。

32 层的计数为:输入线性层 1 个、15 个残差块各含 2 个线性层、输出线性层 1 个,即 1+15×2+1=321+15\times2+1=32

设置为 Adam、学习率 0.001、batch size 256、10 epochs。结果从第一轮训练/测试准确率 86.65%/94.82%,提升到最终 98.75%/97.36%;测试最高值 97.44% 出现在第 9 轮。训练显著超过测试但间隔不大,说明模型已出现轻微泛化差距,暂未发生严重崩坏。

Task 2-2:什么真正影响深层网络训练

三组实验都使用同一 32 层残差 MLP、MNIST、batch size 256 和 10 epochs,只替换一个组件。这样才能把差异更合理地归因于被比较因素。

优化器

优化器关键设置最终测试准确率
SGDlr 0.01,momentum 0.997.49%
Adamlr 0.00197.41%
RMSproplr 0.00196.87%

不同优化器的损失与准确率

Adam 和 RMSprop 前期较快,SGD 曲线更平稳;最终三者差距不到 1 个百分点。因为每种优化器使用了不同但常见的学习率,这是一组“实际配置对比”,不是严格只替换更新公式的控制实验。若要比较算法本身,还应分别搜索最佳学习率并重复多个随机种子。

权重初始化

初始化最终测试准确率现象
Kaiming normal约 94.75%能学习,当前设置收敛偏慢
Xavier normal约 96.44%稳定收敛
Normal(0,0.01)(0,0.01)约 97.64%本实验最高
全零约 11%基本停在随机猜测

不同初始化的损失与准确率

全零失败的根本原因是对称性:同一层所有神经元获得相同输入与梯度,更新后仍然相同,网络无法学出多样特征。小方差正态在这次运行中最好,不代表它普遍优于 Kaiming;初始化效果还取决于激活函数、fan-in/fan-out 模式、归一化、深度和训练轮数。

激活函数

激活函数最终测试准确率
ReLU97.41%
Tanh96.08%
Sigmoid95.40%

不同激活函数的损失与准确率

在 32 层网络中,Sigmoid 和 Tanh 的饱和区会压小梯度;ReLU 正半轴梯度恒为 1,当前设置下收敛更快。但 ReLU 也可能产生长期为负输入的“死亡神经元”,并非没有代价。

Task 2-3:CIFAR-10 上的 MLP 与 CNN

MNIST 的背景简单、目标居中,即使把像素展平,MLP 也容易取得高分。CIFAR-10 包含彩色自然图像,更能检验模型是否利用了二维空间结构。

公平与不完全公平的地方

两种模型都使用交叉熵、SGD、训练 batch size 256、测试 batch size 1000,并配合余弦学习率退火。SGD 的 momentum 为 0.9、weight decay 为 10410^{-4};训练图像先做 RandomCrop(32, padding=4) 与随机水平翻转,再按 CIFAR-10 通道统计量标准化。ResMLP 还把每个残差分支末端 BatchNorm 的缩放参数初始化为 0,使网络初始更接近恒等映射。但为使各自能训练,预算并不完全一致:

模型结构epochs初始学习率
ResMLP32 层,隐藏维 512800.1
CNN10 个卷积层,通道 32→64→128→256500.01

因此这部分回答的是“各自当前配置能做到什么”,不是严格的同算力架构基准。

CIFAR-10 上 MLP 与 CNN 的准确率对比

最终曲线大致表现为:

  • CNN:训练准确率接近 97%,测试准确率约 90%;
  • ResMLP:训练准确率约 96%,测试准确率约 62%。

两者都能记住训练集,但 ResMLP 的泛化差距远大于 CNN。原因在于卷积自带适合图像的归纳偏置:局部连接让网络先看邻域,权重共享让同一个边缘检测器能在不同位置复用,逐层下采样又逐渐扩大感受野。MLP 展平图像后把相邻像素与远距离像素一视同仁,需要仅靠数据重新学出这些规律。

结论应限定为:在本实验的 CIFAR-10 数据、模型规模与训练设置下,CNN 明显优于深层 MLP。它不是“MLP 永远不能做视觉”,更不能外推到使用大规模预训练的现代视觉 MLP。

Task 3:三类食品图像分类

原题写的是“构建包含自己照片的数据集”;本组实际提交把开放题具体化为汉堡、馅饼和寿司三类食品图像分类。也就是说,它完成了自建分类数据与应用链路,但报告没有证明数据满足“包含自己照片”这一字面要求。数据按 ImageFolder 组织为 train/<class>val/<class>,预处理脚本完成:

  • 非 RGB 图片统一转为 RGB;
  • 图片缩放到 224×224224\times224
  • 删除损坏或无法读取的文件。

训练时加入随机水平翻转与 ±15\pm 15^\circ 随机旋转,验证阶段不做随机增强;随后用 ImageNet 均值和方差标准化。

迁移学习设置

模型使用 ImageNet 预训练 ResNet-18,把最后的全连接层替换为 3 类输出。训练设置为:

项目设置
损失交叉熵
优化器SGD,momentum 0.9
学习率0.001
batch size32
epochs10

这里没有冻结 backbone,所有参数共同微调。预训练特征提供了边缘、纹理和物体部件等通用表示,因此小规模自建数据集也能较快收敛。

食品分类训练与验证准确率

食品分类训练与验证损失

源报告把这两张图的 Loss/Accuracy 图注写反了;这里按图内坐标轴纠正:第一张是准确率,第二张是损失。

训练准确率从 60.67% 上升到 98.33%,验证准确率从 85.33% 上升到最终 93.00%,峰值 93.33% 出现在第 8 轮。训练损失持续降到 0.0603,验证损失降到约 0.20 后趋于平缓。

这不是“完全没有过拟合”:后期训练继续改善,而验证集在 91%~93% 之间波动,第 9 轮还从 93.33% 降到 91.33%。更准确的判断是存在温和的泛化差距,但暂未出现验证损失持续发散。若部署,应保存验证集最佳 checkpoint,而不只是最后一轮模型。

从模型到可用界面

项目还实现了 Tkinter GUI,流程为:

  1. 选择或拖入一张图片;
  2. 使用与验证集一致的预处理;
  3. 模型输出 3 维 logits;
  4. Softmax 转成类别概率;
  5. 展示预测类别与置信度。

这一步看似只是套界面,实际验证了训练和推理的完整契约:类别顺序必须与训练时 ImageFolder.classes 一致,图像尺寸、颜色模式和标准化也必须完全相同。任何一处不一致,都可能让离线准确率很高的模型在应用中答错。

具体预测案例

三张典型类内样本都被正确识别,且置信度很高:汉堡 99.98%、寿司 98.62%、馅饼 100.00%。

典型汉堡样本被正确识别

典型寿司样本被正确识别

典型馅饼样本被正确识别

困难样本则暴露了比总体准确率更具体的问题:包含汉堡、卷饼和寿司的混合图被统一判成寿司,置信度 89.30%;与食品无关的动画人物也被判成寿司,置信度高达 98.51%;素描寿司被判成寿司且置信度 99.99%。

多种食品混合图被判为寿司

无关动画人物被高置信判为寿司

素描寿司仍被高置信判为寿司

报告据此推测,模型可能把“颜色丰富”和“大片白色区域”当成寿司线索。素描寿司答对说明它能跨越真实照片的纹理变化,但混合图和无关图片上的高置信输出也说明系统没有拒识能力。更严谨的验证应使用显著性图或遮挡实验确认模型究竟看了哪里,而不能只凭三个例子断言其内部规则。

整个项目串起来学到了什么

自动微分不是魔法

手写 MyLinearFunction.backward 后可以看到,框架只是按计算图保存必要中间量,再把上游梯度乘以局部 Jacobian。Task 1 的三个矩阵公式,就是深度网络反向传播的最小可运行样本。

深度需要可优化的路径

32 层 MLP 能训练起来,不只是“把层数写到 32”,还依赖残差连接、归一化、合理初始化和优化器。深度提供表达能力,这些组件负责让能力真正可学。

架构先验影响泛化

CIFAR-10 实验中,MLP 与 CNN 都把训练准确率推到 96% 以上,测试差距却约 28 个百分点。它清楚地区分了“拟合训练样本”和“学到适合任务的规律”。

实验结论必须带口径

一次运行中的 97.64% 不能证明 Normal 初始化普遍最佳;CNN 约 90% 也不是模型上限。数据划分、随机种子、训练预算和超参数都会影响结果。可靠结论应说明控制变量,并最好报告多次运行的均值与波动。

应用链路同样属于机器学习

数据清洗、类别映射、训练/推理预处理一致性、最佳模型保存和错误样本分析,不是“模型之外的杂活”,而是决定系统能否真实工作的组成部分。

后续改进清单

  • gradcheck 对自定义线性层和 BatchNorm 做双精度数值梯度校验;
  • 为每组对照固定种子并重复 3~5 次,报告均值和标准差;
  • 给 MLP/CNN 对比补参数量、FLOPs 与相同训练预算实验;
  • 为食品数据建立独立测试集,避免反复看验证集后产生选择偏差;
  • 输出混淆矩阵,定位汉堡、馅饼、寿司之间最常见的误判;
  • 保存第 8 轮等验证集最佳 checkpoint,并在 GUI 中使用同一份类别映射。

从一个 Y=XW+bY=XW^\top+b 的线性层,到能接受用户图片的分类界面,这份大作业把深度学习最重要的几层抽象都走了一遍:梯度怎样来、深网怎样训、架构为何重要、实验怎样解释,以及模型怎样真正落地。

评论