📕 第4章 类神经网络的基础
📄 一句话概括

MNIST 是 7 万张 28×28 的手写数字灰度图:6 万张训练、1 万张测试、10 个类别。 它是深度学习界的 Hello World,几乎所有新想法都先在它身上试。 教材这一节用 Keras 三行搭网络、三行训练。 这套环境装不了 TensorFlow,所以 Demo 换成 7 段码造出同样形状的十类数据,流程一步不少。 实测:7-16-10 的网络跑 3000 轮,损失 2.341239 降到 0.193713,训练准确率 0.9300、测试 0.8400。

📝 本节和前面几节的关系

上一节把梯度的算法讲完了,这一节把它接到一个完整任务上。 3.5 节已经用 1024 个点的合成图跑过一次回归,这一节是它的分类版。 第 5 章的卷积网络会用真正的图,到时可以对比一下全连接和卷积差多少。

一、MNIST 是什么

70 000 张 28×28 的手写数字,每张就是 784 个 0 到 255 的灰度值,标签是 0 到 9。 官方切分是固定的:60 000 张训练、10 000 张测试。 正因为全世界都用同一份切分,报出来的数才能横向比较。 它简单到 1998 年就被做到 0.4% 的错误率,现在最强的模型能到 0.2% 以下。 换句话说,MNIST 早就被做完了,它今天的价值是教学和调试,不是刷榜。

二、教材的做法:Keras 三行

教材这一节是标准流程:读数据、归一化、one-hot、用 Sequential 搭 784-128-10、softmax 输出、交叉熵、 fit、evaluate。 教材原码照抄如下(本环境未跑,因为没装 TensorFlow):

from keras.models import Sequential
from keras.layers import Dense, Activation

model = Sequential()
model.add(Dense(128, input_dim=784))
model.add(Activation("relu"))
model.add(Dense(10))
model.add(Activation("softmax"))
model.compile(loss="categorical_crossentropy",
              optimizer="sgd", metrics=["accuracy"])
model.fit(x_train, y_train, epochs=10, batch_size=128)
score = model.evaluate(x_test, y_test)

这套环境第 3 章体检过:没有 TensorFlow,也没有 Keras。 所以 Demo 换成 7 段码——每个数字用 7 个 0 或 1 表示哪几段亮,加高斯噪声,就得到一组 7 维、 10 类的数据。 换掉的只是「图从哪来」,流程一步没少:造数据、切分、one-hot、softmax、交叉熵、反向传播、 报准确率、看混淆。

三、训练与评估

Demo 的网络是 7-16-10:隐层 16 个 tanh,输出层 10 个 softmax。 损失用交叉熵,梯度用上一节那四步,学习率 0.5,全批量跑 3000 轮。 报两个数:训练准确率看「学没学会」,测试准确率看「会不会举一反三」。 两个数的差值就是过适的程度。差 9 个点已经不算小,说明模型把训练集里那些噪声的具体形状也记下来了。

四、错在哪:三种混淆

200 个测试样本里错了 32 个。错得最多的三对是 7→1、8→0、7→3,各 3 次。 7 和 1 只差一段:七段码里 1 是 b+c 两段,7 是 a+b+c 三段。 噪声标准差 0.35,足够把顶上那段 a 的 1 压到 0.65 以下,一翻转就成了 1。 8 和 0 差的也只有中间那段 g。8 是全亮,0 只灭 g。 这两类在 7 维空间里几乎重合,靠单段区分,天生脆弱。 这引出更重要的一点:准确率是个标量,它不会告诉你错在哪。同样的 0.84,可能是均匀地错, 也可能是三类全错、其余全对——这两种情况该做的改进完全不一样。混淆矩阵才会告诉你。

五、从 7 维到 784 维差什么

流程完全一样。 差的是三件事:维度高两个数量级、参数量大得多、以及最要紧的一条——像素之间有空间结构。 全连接层把 784 个像素当成 784 个互不相干的特征。 它不知道第 1 个像素和第 2 个像素是邻居。把图整体平移一格,对它来说是完全不同的输入。 这正是第 5 章卷积要解决的问题。 所以 MNIST 这一节最大的收获不是准确率多少, 而是「同一套流程,换一种特征表示,结果天差地别」这句话有了实测支撑。

六、动手 Demo:十类数字,从 7 段码认起

造 1000 个样本(十个数字各 100 个),切 800 训练、200 测试,搭一个 7-16-10 的网络跑 3000 轮, 最后报训练与测试准确率,并把认错的样本按「真值 → 判成」统计出来。

import numpy as np

SEG = {0: (1,1,1,1,1,1,0), 1: (0,1,1,0,0,0,0), 2: (1,1,0,1,1,0,1),
       3: (1,1,1,1,0,0,1), 4: (0,1,1,0,0,1,1), 5: (1,0,1,1,0,1,1),
       6: (1,0,1,1,1,1,1), 7: (1,1,1,0,0,0,0), 8: (1,1,1,1,1,1,1),
       9: (1,1,1,1,0,1,1)}

rg = np.random.default_rng(9)
X, y = [], []
for d, seg in SEG.items():
    for _ in range(100):
        X.append(np.array(seg, float) + rg.normal(0, 0.35, 7))
        y.append(d)
X, y = np.array(X), np.array(y)
idx = rg.permutation(len(X))
tr, te = idx[:800], idx[800:]

Y = np.zeros((800, 10))
Y[np.arange(800), y[tr]] = 1.0                # one-hot

W1, b1 = rg.normal(0, 0.3, (7, 16)), np.zeros(16)
W2, b2 = rg.normal(0, 0.3, (16, 10)), np.zeros(10)

for ep in range(3000):
    H = np.tanh(X[tr] @ W1 + b1)
    Z = H @ W2 + b2
    Z = Z - Z.max(1, keepdims=True)           # 减最大值,防 exp 溢出
    E = np.exp(Z)
    P = E / E.sum(1, keepdims=True)           # softmax
    loss = -np.mean(np.log(P[np.arange(800), y[tr]] + 1e-12))
    G = (P - Y) / 800                         # softmax 加交叉熵的梯度
    dW2, db2 = H.T @ G, G.sum(0)
    dH = (G @ W2.T) * (1.0 - H * H)
    W1 -= 0.5 * (X[tr].T @ dH)
    b1 -= 0.5 * dH.sum(0)
    W2 -= 0.5 * dW2
    b2 -= 0.5 * db2

def acc(sel):
    H = np.tanh(X[sel] @ W1 + b1)
    return float((np.argmax(H @ W2 + b2, 1) == y[sel]).mean())

print("末轮 loss %.6f  训练 %.4f  测试 %.4f" % (loss, acc(tr), acc(te)))

实跑输出:

--------------------------------------------------------------------
MNIST 的缩小版:十类数字,从 7 段码认起
--------------------------------------------------------------------

  十个数字各造 100 个样本,每个样本是 7 段码加标准差 0.35 的噪声。
  切 800 训练、200 测试,两层网络(7-16-10,softmax 输出),3000 轮:

    第    1 轮   loss 2.341239
    第  100 轮   loss 0.475070
    第  500 轮   loss 0.309393
    第 1000 轮   loss 0.273486
    第 3000 轮   loss 0.193713

  集合        样本数    准确率
  --------------------------------
  训练        800       0.9300
  测试        200       0.8400

  认错最多的三对(真值 → 判成):
    7 → 1  3 次
    8 → 0  3 次
    7 → 3  3 次

  这就是 MNIST 那一节的完整流程,只是把 28×28 = 784 维换成了 7 维:
  **造数据、切分、softmax 输出、交叉熵、反向传播、报准确率。**

  书里 MNIST 那一节多出来的东西只有两样:**图是真图,784 维,信息多得多。**
  还有网络更大。**流程一模一样。**

损失从 2.341239 掉到 0.193713。 起始值略大于 ln 10 = 2.302585 是正常的:十类均匀乱猜的交叉熵正好是 ln 10, 随机初始化的网络比乱猜还要差一点点。 训练 0.9300、测试 0.8400,差 9 个点。 这个差距就是过适的信号——模型把训练集里那些噪声的具体形状也当成了特征。 认错最多的三对各 3 次:7→1、8→0、7→3。 7 和 1 在七段码里只差顶上那一段,噪声 0.35 足够把它翻掉;8 和 0 只差中间那段,同理。 这个 Demo 是 MNIST 那一节的形状复制:造数据、切分、one-hot、softmax、交叉熵、反向传播、报准确率、 看混淆。除了图变成 7 个数,每一步都对得上。 换回真正的 MNIST,只需要改数据加载那几行。 这也说明为什么先在小数据上把流程跑通是有意义的——排查问题时变量少得多。

30 秒实验

  1. 把噪声从 0.35 提到 0.6,训练和测试准确率各会怎么变?两者差距会变大还是变小?
  2. 把隐层从 16 改成 4,损失还能降到 0.2 以下吗?
  3. 把训练轮数从 3000 改成 300,测试准确率大概掉多少?

常见误区

  1. 以为 MNIST 准确率高就代表模型强。它早被做到 99.8%,差 0.1 个点可能只是数据增强不同。 拿它比模型强弱没有意义,它只适合验证流程有没有写错。
  2. 只看准确率、不看混淆。如果 90% 的样本都属于同一类, 一个「全猜那一类」的模型也能有 90% 准确率,它其实什么都没学到。
  3. 以为「训练高、测试低」一定是过适。也可能是训练集和测试集分布本来就不同。 判过适要看两者随轮数的走势,不是看某个轮次上的一次差。
自测三问(点开对答案) 1. MNIST 的输入维度为什么是 784? —— 每张图 28×28 个像素,展平成一维向量就是 784 个数。 这个「展平」动作本身丢掉了空间信息,正是第 5 章要补回来的东西。 2. 为什么十类分类的损失起始值接近 ln 10? —— 十类均匀猜测时每类概率 0.1,交叉熵 -log 0.1 = ln 10 约 2.3026。 实测 2.341239 略大,说明初始化的网络比乱猜还差一点。 3. 混淆矩阵比准确率多告诉你什么? —— 错在哪两类之间、往哪个方向错。 准确率是一个标量,混淆矩阵是一张表,它直接指向「该补哪一类数据」或「该加哪个特征」。

到这里第 4 章结束。 一个全连接网络该有的零件全齐了:神经元、激励函数、损失、梯度、反向传播、评估。 但它有个解决不了的毛病——把图展平成 784 个数,空间结构就丢了。下一章用卷积把它找回来。

相关笔记