手写数字项目接进站点之后,我没有把实验台做成“在浏览器里重新训练完整 42000 条数据”的版本。那样会太重,也不适合手机端。现在的做法更实用:保留离线 C 项目的完整下载版本,再给实验台加一个轻量、可交互、能立刻看到预测结果的浏览器模型。
这样分层有两个好处。一是网站访问者不需要本地编译环境,也能直接理解输入、概率和预测之间的关系。二是离线项目和网页演示各自边界清楚,不会为了前端演示把原始实现改得面目全非。
一、实验台里的手写数字模块做了什么
新标签页现在支持三件事:
- 浏览样本:从训练集抽取一批真实数字样本,直接加载到 28 x 28 网格里
- 手绘输入:在浏览器里直接画数字,立即查看模型预测
- 概率面板:同时显示 0 到 9 十个类别的概率分布,而不是只给一个结果
这类界面对学习很有帮助,因为你不只是看到“答案是什么”,还能看到模型为什么更偏向某几个数字。
二、浏览器版模型和离线 C 项目有什么关系
网页里加载的不是完整训练过程,而是一份预训练好的轻量 softmax 权重文件。它和 C 项目属于同一类多分类线性模型,只是为了网页体积和响应速度,改成了预先训练后再加载。
当前浏览器演示权重来自一份较小的训练切片:
- 训练样本:10000 条
- 验证样本:2000 条
- 训练轮次:18
- 学习率:0.35
- 验证准确率:约 90.65%
这不是为了追求最强分数,而是为了让实验台在浏览器里也能保持即时反馈。真正完整的数据和源码仍然保留在下载包里。
为什么你画的数字,识别率明显低于 90%
上面那个 90.65% 是验证集上的准确率。实际在浏览器里手绘几个数字就会发现,错误率明显高于这个数——而且模型本身没有任何问题。
原因是训练数据和你的输入不是同一个分布。MNIST 的样本不是随手写出来的,它经过了一套固定的预处理:原始扫描件先被二值化,然后按数字的外接框缩放到 20×20,再放进 28×28 的画布,并且平移使得像素质心落在正中心。所有样本的笔画粗细、大小、位置都被归一化过。
而你在浏览器里用鼠标或手指画出来的数字:笔画可能很粗或很细、可能偏在画布一角、可能只占画布的三分之一。这些差异对人眼毫无影响,但对一个线性 softmax 模型是致命的——它学到的是「哪些像素位置亮起来」,输入平移几个像素,激活的就是完全不同的一组权重。
所以正确的做法不是重新训练模型,而是在推理前对输入施加与训练时相同的预处理:
// 1. 找到笔画的外接框,裁掉周围空白
// 2. 等比缩放到最长边为 20 像素
// 3. 放进 28x28 画布
// 4. 按像素质心平移到画布中心
function centerByMass(img, w, h) {
let sx = 0, sy = 0, total = 0;
for (let y = 0; y < h; y++)
for (let x = 0; x < w; x++) {
const v = img[y * w + x];
sx += x * v; sy += y * v; total += v;
}
return { dx: w / 2 - sx / total, dy: h / 2 - sy / total };
}
加上这几步之后,同一个模型在手绘输入上的表现会显著改善——提升来自对齐分布,不是来自更强的模型。
这件事有一个比手写数字本身更普遍的教训:一个模型报出来的准确率,只在它的验证集分布上成立。部署时输入分布只要变了,那个数字就不再有效,而且它不会自己失效——模型照样会给出一个自信的预测。这类偏差我在量化验证的取样偏差里踩过更严重的一次:只在模型擅长的样本上验证,IoU 报 0.98,换一类真实图片掉到 0.30。
所以在这个实验台上,看模型给出的概率分布比看它的最终答案更有信息量。如果十个类别的概率都很平均,说明这张输入落在了模型没见过的区域,此时那个「最高概率」的类别基本是随机的。
三、为什么不直接在前端重跑完整训练
原因很现实:
- 完整训练集太大,不适合每次页面加载都重新处理
- 浏览器端反复做大规模梯度更新,会显著拖慢页面交互
- 手机端更需要稳定的体验,而不是把 CPU 全部占满
所以实验台更适合承担“可视化理解”和“交互试验”的角色,而不是替代离线训练环境。
四、如何判断一次手绘预测是否可信
实验台的结果不应该只看最大概率的类别。更有价值的观察方式是比较前两名概率的差距:如果第一名只有 0.31,第二名有 0.29,那么模型其实并不确定;如果第一名超过 0.85,且其他类别都很低,说明当前输入更接近训练集中学到的形状。这个差距比单纯的类别编号更能说明模型边界。
还要注意输入分布。离线数据集中的数字通常居中、笔画粗细相对稳定,而手绘输入可能偏左、偏右、太细或断笔。softmax 线性模型没有卷积网络那样强的平移不变性,所以同一个数字稍微移动位置,概率就可能明显变化。把这些失败样本保存下来,比只挑正确样本展示更适合学习模型限制。
五、这个模块最适合怎么用
建议按这个顺序体验:
- 先加载一个真实样本,确认数字网格和预测结果是否一致
- 再手动改几笔,观察概率怎么变化
- 最后清空画板,自己画一个数字,看模型最容易在哪些数字之间犹豫
如果你画了一个 9,但模型同时给 4 和 7 很高概率,这通常不是坏事,反而说明你已经看到模型的边界了。
六、这个实验台和文章、下载页怎么配合
现在这组内容已经形成了比较完整的结构:
- 第一篇 先讲数据文件和 784 维输入格式
- 第二篇 再讲 C 语言 softmax 分类器如何训练和导出结果
- 下载页 集中放源码、压缩数据、示例提交文件和浏览器模型文件
- 实验台 提供直接可交互的轻量版本
这样安排之后,读者可以在文章里理解原理,在下载区拿到材料,在实验台里马上试。
七、下一步还能继续怎么增强
这一版已经能承担教学演示,但后面还可以继续做三类增强:
- 增加“笔刷粗细”和“自动居中”之类的输入处理
- 把错误样本单独整理成可切换的案例列表
- 把浏览器端模型和离线 C 项目的输出做更细的对照说明
对学习型网站来说,这类增强比堆更多装饰更有意义。重点不是页面看起来像 AI 产品,而是让读者真的能比较输入、模型和输出。
八、直接去哪里继续
如果你还没看源码文章,建议先回到 C 语言实现说明。如果你已经准备自己跑一次完整项目,就直接打开 下载页 拿材料;如果只是想快速试一下,就去 实验台 直接画几个数字。
九、把一次预测记录成可复查的小实验
只看一次“预测正确”并不能说明模型已经可靠。更好的做法是把同一个数字做三到五次轻微变化,然后记录模型概率如何变化。比如同样画一个 3,可以分别尝试笔画居中、偏左、上半部分开口更大、下半部分更圆。每一次都记录最高概率、第二高概率和两者差值。
| 观察项 | 应该记录什么 | 为什么有用 |
|---|---|---|
| Top-1 类别 | 概率最高的数字 | 判断模型最终输出 |
| Top-2 类别 | 第二高概率的数字 | 观察模型最容易混淆的方向 |
| 概率间隔 | Top-1 减 Top-2 | 衡量当前预测是否有足够把握 |
| 输入变化 | 偏移、粗细、断笔或旋转 | 定位模型对哪类扰动最敏感 |
这样记录之后,实验台就不只是一个“画数字玩具”,而是一个可复查的模型诊断入口。你可以明确看到:线性 softmax 对整体形状有一定识别能力,但对平移、断笔和局部变形没有卷积网络那么稳定。
十、为什么这个轻量实验仍然有教学价值
浏览器版本没有追求最高准确率,这一点需要说清楚。它的价值在于把模型输入、权重输出和概率分布放到同一个界面里,让读者能快速建立“像素数组到类别概率”的直觉。对于入门者来说,这比直接给一个复杂 CNN 更容易检查每一步。
如果后续把模型换成卷积网络,也应该保留这套观察方式:先看输入是否合理,再看概率分布是否集中,最后看错误样本是否集中在某几类数字。模型可以升级,但验证思路不能省略。