MNIST 常被当成深度学习框架的“你好世界”,但程序能运行不代表实验可信。数据是否按 NCHW 排列,输出层是否给损失函数正确的 logits,CPU 与 GPU 上的张量是否处于同一上下文,训练准确率是否混入测试信息,这些细节都会改变结果。与其照抄一长段旧代码,更好的做法是把实验拆成若干验收点,每通过一个点再进入下一步。
冻结环境后再讨论模型
历史框架、Python、NumPy 与 CUDA 之间常存在版本耦合。复现时使用独立虚拟环境或容器,记录解释器版本、MXNet 包版本、计算设备和随机种子,不要为一份教程直接降级日常环境。安装完成后的第一项测试不是训练,而是创建一个 CPU 张量、执行简单运算并打印框架版本;需要 GPU 时,再单独验证设备可见性与基础算子。
- 保存依赖清单与安装日志,确保另一台机器能够重建环境。
- 数据文件记录大小与哈希,避免同名数据包内容不同。
- 固定拆分规则,训练集用于拟合,测试集只承担最终评价。
- 给随机种子加注释,说明它控制了哪些随机过程。
先看四张图,再相信数据读取
原始图像应为 28×28 的灰度矩阵,标签是零到九的整数。随机显示若干样本,并同时打印标签、像素最小值、最大值与形状,可以快速发现通道顺序、归一化和标签错位。卷积层通常接收 (批次, 通道, 高, 宽),因此单张图需要增加通道维;像素转为 float32 后除以 255,使输入落在零到一范围。所有转换只改表示,不能改变样本对应关系。

| 检查对象 | 预期状态 | 失败时的典型现象 |
|---|---|---|
| 图像批次 | N×1×28×28 | 卷积报告维度或通道错误 |
| 类别标签 | 长度为N的整数 | 损失函数索引异常 |
| 像素范围 | float32且约在0到1 | 梯度尺度过大、收敛缓慢 |
| 设备上下文 | 输入、标签和参数一致 | CPU与GPU张量无法运算 |
| 输出张量 | N×10未归一化分数 | 类别数不符或损失语义错误 |
网络结构应从输出契约倒推
这个任务需要为十个数字产生十个类别分数。可以用两层卷积提取局部笔画,池化缩小空间尺寸,再通过全连接层得到分类输出。中间层使用 ReLU 等激活,最后一层通常不额外加 softmax 或 softrelu,因为 SoftmaxCrossEntropyLoss 期待未归一化 logits,并在内部完成稳定计算。把非负激活放到最后一层会限制分数表达,也可能与损失实现不匹配。
- 创建 Sequential 容器并依次加入卷积、激活、池化和全连接层。
- 选择初始化器,将所有参数明确放到目标 context。
- 生成一批与真实输入同形状的随机张量,完成首次前向传播。
- 核对每层输出形状和总参数量,再创建 Trainer 与损失对象。
- 只用一个小批次执行前传、反传和更新,确认参数确实发生变化。
Gluon 的五个角色不能互相替代
Dataset 保存样本与标签关系,DataLoader 负责分批和训练时打乱;Block 或 Sequential 定义前向计算;autograd.record 记录需要求导的运算;Trainer 根据梯度更新参数;context 决定数据和参数位于 CPU 还是某块 GPU。把这五个角色分开理解,遇到错误时就能判断是数据、计算图、优化器还是设备迁移的问题,而不是反复调整学习率碰运气。
训练循环的最小闭环是:清空或覆盖旧梯度,记录前向与损失,反向传播,再按实际批大小更新参数。顺序错误可能仍然运行,却得不到预期学习。
遍历批次时,先把图像与标签移动到参数所在 context。在记录块中计算 logits 和损失,退出后调用 backward,再让 Trainer 以真实批大小执行一步。最后一个批次可能不足预设大小,若仍固定传入 32,梯度归一化会发生偏差。累积轮次损失时也要按样本数加权,不能简单平均每个大小不同的批次均值。
准确率函数也需要单元测试
分类准确率通常对 logits 沿类别维取 argmax,再与整数标签逐项比较。可手工构造三条预测,让其中两条正确,确认函数返回三分之二。评估测试集时不记录梯度,不打乱也不更新参数,并使用与训练相同的像素缩放。若训练模式中含 Dropout 或批归一化,还要明确切换到评估语义;不同框架的接口细节应按对应版本确认。
历史样例曾记录一轮训练后约 95.42% 的训练准确率和约 97.10% 的测试准确率,但这个数字只能视为特定代码、随机状态与数据文件下的观测。测试准确率高于训练准确率可能来自训练阶段的随机性、统计方式或抽样波动,不应据此承诺复现结果。更有价值的是保存每轮损失、训练指标、测试指标与耗时,并检查曲线是否符合预期。
模型文件必须带着预处理一起保存
参数文件本身无法说明输入需要什么形状、像素如何缩放、类别如何映射。保存模型时,同时写出网络结构、框架版本、预处理规则、随机种子、训练数据标识和最佳检查点选择标准。加载后先在 CPU 上对一组固定样本推理,比较类别与 logits 是否接近保存前结果,再测试目标设备。这样才能区分“文件成功读取”和“模型行为成功恢复”。
还可以把若干代表样本及期望输出作为迁移夹具。当需要改用另一套框架时,先复现预处理,再对齐中间张量和最终 logits,而不是只比较总体准确率。逐层差异能更快定位权重排列、卷积 padding 或数据布局问题。
让 MNIST 承担管线验收,而不是成绩展示
完成实验后,应主动加入破坏性测试:交换两个标签确认损失恶化,关闭归一化观察训练变化,尝试空批次或错误形状确保程序给出清楚异常,保存后重载验证输出一致。MNIST 过于简单,不能证明模型能处理现实图像,却非常适合证明训练管线的基本语义。只要环境可重建、每一步形状可解释、指标可复算、参数可恢复,这份旧框架练习就完成了比“跑出一个数字”更有长期价值的任务。
本文《把一份 MNIST 旧代码变成可复现实验:Gluon 训练循环逐项验收》由 xkmchenmu 发布于 xkmchenmu Blog。 转载请保留原文链接并注明出处。
支付宝扫一扫