{
  "lead": "MNIST 是机器学习的「Hello World」：7 万张 28×28 的手写数字灰度图。这里用它训练了一个两层神经网络，权重压缩成 int8 后只有约 50 KB，直接编译进 .wasm。你在画板上写的每一笔都会实时送进网络，识别完全在浏览器本地完成，不需要服务器。",
  "formulas": [
    [
      "h = ReLU(W<sub>1</sub>x + b<sub>1</sub>)，p = softmax(W<sub>2</sub>h + b<sub>2</sub>)",
      "784 个像素 → 64 个隐藏神经元 → 10 个数字的概率"
    ],
    [
      "w ≈ q · s，q ∈ [−127, 127]",
      "int8 量化：每个神经元一组权重共用一个缩放系数 s"
    ]
  ],
  "terms": [
    [
      "预处理：和 MNIST 对齐",
      "MNIST 的数字都被缩放到 20×20 的框内，再按「重心」放到 28×28 画面的正中。画板上的数字位置、大小各不相同，所以先裁出笔迹的边界框，按长边缩放到 20 像素（用区域平均，自然产生灰度边缘），再把重心平移到中心。右边的小图就是网络实际看到的输入。"
    ],
    [
      "隐藏层",
      "64 个神经元各自对某种笔画特征敏感（比如左侧竖线、上方弧线）。小方格里越亮的神经元被激活得越强，写不同的数字时亮起的组合完全不同。"
    ],
    [
      "训练与数据增强",
      "用 Rust 写的训练程序（<code>examples/train.rs</code>）在 CPU 上做小批量随机梯度下降，30 轮约 70 秒。手写的数字没有 MNIST 那么规整，所以每一轮都给每张训练图随机旋转、缩放、错切、平移一点，网络学到的是「数字的形状」而不是固定的像素位置。"
    ],
    [
      "int8 量化",
      "训练出的 5 万多个权重原本是 32 位浮点数（约 200 KB）。每个神经元找出绝对值最大的权重，把整组权重线性映射到 −127~127 存成 1 个字节，推理时再乘回缩放系数。体积缩小到 1/4，准确率基本不变。"
    ]
  ],
  "flow": [
    [
      "js",
      "每画一笔，JS 读出画板像素，取一个通道作为「墨迹」数组，调用 <code>digits_preprocess(墨迹, 宽, 高)</code>。"
    ],
    [
      "rust",
      "Rust 裁剪、缩放、按重心居中，得到 784 个 0~1 的值；<code>digits_classify(输入)</code> 用 int8 权重做两层矩阵乘法，返回 10 个概率和 64 个隐藏层激活值。"
    ],
    [
      "js",
      "JS 更新概率条和预测结果，并把网络输入和隐藏层画成小图。一次预处理加推理约 0.5ms，所以可以边写边识别。"
    ]
  ],
  "perf": [
    "在 MNIST 的 1 万张测试图上，int8 量化后的准确率为 98.49%（量化前 98.48%）。自己写的数字比测试集更多样，写得太小、太偏或笔画太细时仍可能认错。",
    "训练是确定性的：同样的数据和轮数，每次得到完全相同的权重文件。重新训练需要先下载 MNIST 并解压，然后运行 <code>cargo run --release -p digits --example train -- &lt;目录&gt;</code>，会覆盖 <code>src/weights.bin</code>。",
    "这个 .wasm 约 74 KB，比其他演示大，主要是嵌入的 50 KB 权重。"
  ],
  "src": [
    [
      "crates/games/digits/src/lib.rs",
      ""
    ],
    [
      "crates/games/digits/examples/train.rs",
      ""
    ],
    [
      "www/digits/index.js",
      ""
    ]
  ]
}
