把卷积、ReLU、池化、全连接拼起来,就是教材 5-10 节那个网络。训练流程和第 4 章完全一样。 实测三类图案(横线、竖线、对角线):原始像素测试准确率 0.7889, 16 个卷积核加 2×2 池化升到 0.9556,换全局最大池化再升到 0.9722。 换掉的只是「喂什么特征」,训练代码一个字没改。
前面九节把零件拆开看过,这一节组装。 第 4 章 4.8 节的 MNIST 是同一个流程,只是特征从「卷积算的」换成「原始像素」。 5.11 节会在这个网络上加 dropout,看它怎么抑制过适。
一、一个完整的卷积网络长什么样
典型结构:卷积 → ReLU → 池化,重复两三次,然后摊平 → 全连接 → softmax。 越往后通道越多、尺寸越小。8×8×1 变成 6×6×16,池化后 3×3×16,摊平 144 维。 「尺寸减半、通道翻倍」是条经验规则, 目的是让每层的计算量大致相当。每层算力和前一层的比例接近 1,整个网络才均衡。
二、教材的 Keras 写法
教材这一节的重点是「能跑起来」,所以用的是最朴素的 Sequential 堆叠。 教材原码照抄如下(本环境未跑,因为没装 TensorFlow):
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Flatten, Dense
model = Sequential()
model.add(Conv2D(16, (3, 3), activation="relu", input_shape=(28, 28, 1)))
model.add(MaxPooling2D((2, 2)))
model.add(Conv2D(32, (3, 3), activation="relu"))
model.add(MaxPooling2D((2, 2)))
model.add(Flatten())
model.add(Dense(128, activation="relu"))
model.add(Dense(10, activation="softmax"))
model.compile(loss="categorical_crossentropy",
optimizer="adam", metrics=["accuracy"])
model.fit(x_train, y_train, epochs=10, batch_size=128)
十几行就把结构说完了。 卷积层的第一个参数是核的个数,第二个是核的大小,这个顺序在 Keras 里固定。
三、这台机器上怎么跑
这台机器没有 TensorFlow、没有 Keras(第 3 章体检过)。 所以 Demo 改成:16 个随机卷积核固定不动,只训练最后的 softmax 分类器。 这不是偷懒。 「随机特征加线性分类器」本身就是一套完整的方法,在核方法盛行的年代是主流做法, 它证明的是「结构本身就在干活」。 端到端训练卷积核只会更好, 不会更差—— 因为随机核只是「训练」这个搜索空间里的一个起点。这一点在 90 年代的实验里就被反复验证过。
四、三个数字
原始像素 0.7889、卷积加 2×2 池化 0.9556、卷积加全局最大池化 0.9722。 对应的特征维数是 64、144、16。注意第三个:维度比原始像素还少。 这条曲线说明:特征的质量比数量重要得多。给人 16 个「对的特征」,比给 64 个「生的像素」有用。
五、卷积到底买到了什么
买到的是「形状」和「位置」的分离。核负责形状,池化的最大值负责抹掉位置。 这也解释了为什么池化窗口越大,这个任务准确率越高——任务本身就是「什么形状」,与「在哪」无关。 反过来,如果任务是「左上角有没有横线」,全局最大池化会把这个信息一起抹掉。 所以池化窗口是一个任务相关的选择,不是一个越大越好的旋钮。
六、动手 Demo:卷积特征比原始像素好认
造三类 8×8 的图:横线、竖线、对角线。线每次放在不同位置,加 0.3 的噪声。 同一份数据,分别用原始像素和卷积特征喂给同一个 softmax 分类器。
import numpy as np
# 三类 8×8 图:横线、竖线、对角线,位置随机、噪声 0.3
Xtr, ytr = make_images(200, rg)
Xte, yte = make_images(60, rg)
# A 组:原始像素
Ftr = Xtr.reshape(len(Xtr), 64)
W, b = softmax_fit(Ftr, ytr, 3)
print("原始像素 测试 %.4f" % acc(Fte, yte, W, b))
# B 组:16 个固定随机核,卷积 + ReLU,再池化
K = rg.normal(0, 0.6, (16, 1, 3, 3))
Gtr = feats(Xtr, K) # 2×2 池化得 144 维,全局最大池化得 16 维
W2, b2 = softmax_fit(Gtr, ytr, 3)
print("卷积加池化 测试 %.4f" % acc(feats(Xte, K), yte, W2, b2))
实跑输出:
--------------------------------------------------------------------
深层卷积神经网络的学习:卷积特征比原始像素好认
--------------------------------------------------------------------
造三类 8×8 的图:横线、竖线、对角线。线每次随机放在不同位置,再叠上标准差 0.3 的噪声。
每类 200 张训练、60 张测试。
做法 特征维数 训练 测试
------------------------------------------------------
原始像素 64 0.9167 0.7889
卷积加 2×2 池化 144 1.0000 0.9556
卷积加全局最大池化 16 0.9883 0.9722
**原始像素的测试准确率 0.7889;换成 16 个卷积核加 2×2 池化,升到 0.9556。**
再往下一步更有意思:把池化窗口从 2×2 放大到整张图(全局最大池化),
特征维数从 144 掉到 16——比原始像素还少——测试准确率却再升一点,到 0.9722。
**这说明卷积那一步做的事情,不是「算得更多」,而是「算得更对」。**
原因:横线、竖线、对角线在像素空间里是「形状 + 位置」绑在一起的两件事。
**线换了位置,64 维向量几乎全变;而卷积把形状交给核、把位置交给池化的最大值。**
窗口越大,位置越不重要,形状就越突出。
这里要交代一句:**这 16 个卷积核是随机抽的、训练中一直没动,只有最后那个 softmax 被训练了。**
教材 5-10 节讲的是用 Keras 把整个网络端到端训练,本环境装不了 TensorFlow,
所以这一步只做了「固定核 + 训练分类器」。
但结论的方向不会变:
**卷积的价值主要来自「局部连接 + 权重共享 + 池化」这三个结构,不完全来自训练。**
把核也训起来只会更好。
表里三行的特征维数是 64、144、16。注意第三行:16 维比原始像素的 64 维还少,准确率却最高。 卷积加 2×2 池化已经到 0.9556,比原始像素高 16 个点。这 16 个点全部来自「把形状和位置拆开」。 全局最大池化再升到 0.9722。它的窗口是整张图,等于彻底不管位置。 为什么这件事在这个任务上成立?因为三类图的区别只有「方向」,线放在哪一行、哪一列是随机的。 任务本身与位置无关,彻底抹掉位置反而最好。 但别把它当成通用结论。换成「左上角有没有横线」,全局最大池化会把这个信息一起抹掉,准确率会崩。 最后再强调一次:这 16 个核是随机的、没训练过。如果端到端训练它们,准确率只会更高。
30 秒实验
- 把训练集从每类 200 张降到每类 20 张,三种做法的准确率各会怎么变?
- 把池化窗口从 2×2 改成 4×4,特征维数和准确率各会怎么变?
- 如果线固定在第 4 行(位置不再随机),哪种做法的优势会消失?
常见误区
- 以为卷积网络一定比全连接好。它好在「结构匹配任务」。 位置不重要的任务上优势最大,位置就是全部信息的任务上反而吃亏。
- 以为随机卷积核没有意义。随机特征加线性分类器是经典方法, 它证明的是「结构本身在干活」,不全是训练的功劳。
- 只报一个准确率。训练和测试要一起报,差多少才是过适的信号。
自测三问(点开对答案)
1. 卷积网络的典型结构是什么? —— 卷积、ReLU、池化重复几次,再摊平、接全连接、最后 softmax。越往后通道越多、尺寸越小。 2. 为什么要「尺寸减半、通道翻倍」? —— 让每层的计算量大致相当,同时让后面的层看到更大的范围。尺寸小省算力,通道多保信息量。 3. 这个 Demo 里卷积核训练了吗? —— 没有,是随机抽的、全程没动。只训练了最后的 softmax 分类器。所以它测的是「结构」本身的价值。上面那个网络有个隐患:训练集小的时候,它会把噪声也背下来。下一节看 dropout 怎么治这个病。
- ☐ (本节完,下一节 5.11 过适与丢弃)