📗 第8章 深度学习的现在和未来
📄 一句话概括

教材这一节列了三块战场:图像识别、语音识别、自然语言处理。 三块地方的路线是同一个形状——先是人挑特征,后来换成网络自己挑。 实测:同一批 8 乘 8 的数字图,喂原始像素测试 0.9025,喂人挑好的七段码只有 0.5613。 但把平移去掉,段码立刻涨到 0.9775,跟原始像素打成平手。 它输的不是信息量,是位置。

📝 本节和前面几节的关系

前七章一直在讲「怎么做」,这一章讲「已经做成了什么」。 它是全书唯一一章没有公式、也没有代码的章节。 但这一章恰恰最该配一个 Demo——因为「应用案例」这个词,很容易被读成一串成就清单。 而清单读起来爽,不解决问题。

一、图像识别:那届比赛改写了后面十年

教材给了一张 ILSVRC 的历年成绩折线图。 2012 年那一届,一个叫 AlexNet 的卷积网络把 top-5 错误率从两成六压到一成五, 第二名还在两成六。 一成五这个数字本身不稀奇,稀奇的是它和第二名差了十一个百分点。 这个比赛办了这么多年,之前每一届的冠军都比上一届好一两个点。 忽然有一届差了十一点,那不是「又进步了」,那是「换了个办法」。 换掉的东西,现在回头看很清楚:

环节 之前 之后
特征 SIFT、HOG,人手设计 卷积核,网络自己学
分类器 SVM、随机森林 几层全连接加 softmax
流程 先提特征,再训分类器,两段 端到端,一段
注意第三行。前两行是「换了工具」,第三行是「换了分工」。
以前是人负责「看什么」,机器负责「怎么分」;现在是机器把两件事一起干了。

二、语音识别:先做过一遍的事

语音那边的故事比图像还早一点,只是没那么轰动。 在这之前,主流做法是 GMM-HMM:用高斯混合模型描述每一帧声音, 用隐马尔可夫模型描述音素怎么连成词。 换成深度学习,改动其实很小:把 GMM 换成几层神经网络,HMM 那套结构留着。 这叫 DNN-HMM 混合模型,2010 年前后开始取代 GMM-HMM。 值得留意的是「混合」这两个字。 它不是推倒重来,是把链条里最弱的一环换掉,剩下的照用。 工程上大部分进步都是这个形状——教材喜欢讲颠覆,现场多半是替换。

三、自然语言处理:把词变成向量

文本这块最难,因为输入根本不是数字。 在这之前,最常见的手段叫词袋:把一句话拆成词,数每个词出现几次,得到一个很长的稀疏向量。 词袋有两个硬伤,用一句话就能说清:

硬伤 后果
词序全丢了 「狗咬人」和「人咬狗」是同一个向量
词和词之间没有关系 「汽车」和「轿车」是两个毫不相干的维度
第一个硬伤靠循环网络补,第二个硬伤靠词向量补。
词向量这件事的思路很漂亮:让每个词变成一个几十维的稠密向量,
让「意思相近」变成「向量离得近」。
第 7 章那个 Chainer 的 Demo 里,变长序列的麻烦就是从这里来的。
文本不像图像,每句话的长短都不一样,补零补到哪算完,是个真问题。

四、三块战场,同一个套路

把三个领域摆在一起,会发现它们的路线是复制的:

领域 人工特征时代 端到端时代
图像 SIFT/HOG 加 SVM 卷积网络吃像素
语音 MFCC 加 GMM-HMM 频谱进网络
文本 词袋加手工规则 词向量加循环网络
左列全是「人先想好该看什么」,右列全是「让网络自己找」。
这条路能走通,靠的不是算法变聪明,是数据和算力便宜到了某个点。
这一节最容易读出的一个误解是:人工特征不行。
其实不是。人工特征在它那个年代是最好的选择,只是它有个天花板——它只能看见设计者想到的东西。
数据一多,这个天花板就成了瓶颈;数据不够的时候,它反而是护栏。

五、Demo 要验的正是这件事

下面这个 Demo 就是那句「人工特征有天花板」的缩小版。 同一批 8 乘 8 的数字图,喂三种东西:原始 64 个像素、2 乘 2 池化后的 16 个数、 按七段数码管取的 7 个数。 第三种是人挑的特征,而且挑得极其内行——它直接照着「数字是怎么写的」来取。 按理说应该碾压原始像素。 实测结果反着来,而且反得很有道理。 这个 Demo 的结论不是「人工特征不行」,是「人工特征怕位置变化」:段码的掩码只有一列宽, 图往右挪一格,整段就落空了。

六、动手 Demo:三种表示,谁在什么条件下才占便宜

数据还是第 1 章那张 8 乘 8 的数字图,网络还是 64→32→16→10。 只换喂进去的东西,别的全不动。 这个 Demo 做了四层排除:先看谁赢,再排除「网络太小」,再排除「没训够」, 最后用一个对照实验把原因钉死。

# 同一批 8x8 数字图,三种喂法
def to_pool(X):                      # 64 -> 16:2x2 取最大
    img = x.reshape(8, 8)
    return np.maximum(np.maximum(img[0::2, 0::2], img[0::2, 1::2]),
                      np.maximum(img[1::2, 0::2], img[1::2, 1::2])).ravel()

MASK = {s: seg_mask(s) for s in 'abcdefg'}   # 七段的像素位置

def to_seg(X):                       # 64 -> 7:每段亮不亮
    img = x.reshape(8, 8)
    return np.array([img[MASK[s]].mean() for s in 'abcdefg'])

# 数字 1 的段码是 [0, 1, 1, 0.25, 0, 0, 0.25]
# 把这张图向右滚一格,七个位置全变成 0.00
seg_b = [np.roll(one_digit(1), 1, 1)[MASK[s]].mean() for s in 'abcdefg']

实跑输出:

--------------------------------------------------------------------
应用案例:三种表示,谁在什么条件下才占便宜
--------------------------------------------------------------------
[1] 同一份数据,三种「看」法
  第 1 章那张 8 乘 8 的数字图又回来了。同一批图,可以喂三种东西进去:

    表示                维数    怎么来的
    原始像素            64      每张图摊平,一个像素一个输入
    池化后              16      2 乘 2 取最大,8 乘 8 压成 4 乘 4
    段码                7       按七段数码管去问:a 到 g 七段各亮不亮

  第三种是「人想出来的特征」。做这行的人有个朴素的期待:
  七段码里 a 到 g 亮不亮,基本上已经把「这是几」写出来了,
  网络拿到这么干净的七个数字,应该比啃 64 个像素轻松得多。
  下面就来验一验这个期待。

[2] 每类给 20 张,三种表示各训一遍
  网络 64→32→16→10 或者 7→32→16→10,都是两千轮、学习率 0.1、同一个种子。
  训练集每类 20 张、带 ±1 像素平移;测试集每类 80 张、平移之外再翻一个像素。

    表示                输入维  参数量   训练准确率  测试准确率
    原始 64 像素        64      2778     1.0000      0.9025
    4×4 池化 16 维      16      1242     0.7500      0.5563
    7 维段码            7       954      0.7600      0.5613

  结果和期待反着来:段码垫底,原始像素第一。
  而且看「训练准确率」这一列——网络连训练集都背不下来,说明不是泛化出了问题。

[3] 先把「是不是网络太小」这条排除掉
  7 维网络只有 954 个参数,64 维的有 2778 个。参数少也可能是它吃亏的原因。
  把段码网络的隐层加宽,把参数量顶到跟 64 维差不多,再跑一遍:

    7→45→45→10  2890 个参数  训练 0.7600  测试 0.5700
    64→32→16→10  2778 个参数  训练 1.0000  测试 0.9025

  参数从 954 加到 2890,准确率纹丝不动。不是容量的事。

[4] 那是没训够吗?把轮数加到十倍
  两千轮提到两万轮,学习率也跟着调,看训练准确率能不能顶上去:

    表示            两千轮 lr0.1    八千轮 lr0.5    两万轮 lr0.3
    原始 64 像素    1.0000          1.0000          1.0000
    7 维段码        0.7600          0.7650          0.7650

  两万轮之后还是那个数。训练准确率顶到天花板下不来,说明这七个数字里
  本来就没有足够的信息把十个类别分开——不是优化器偷懒,是输入混了。

[5] 为什么会混:拿数字 1 出来看
  数字 1 在七段码里只亮 b、c 两段,而这两段在图上都是「第五列那一竖」。
  段码掩码是按位置硬框定的:b 段取第 2 到 4 行第 5 列,c 段取第 5 到 7 行第 5 列。
  现在把这张图往右平移一格:

    段码    原图    右移一格后
    a       0.00    0.00
    b       1.00    0.00
    c       1.00    0.00
    d       0.25    0.00
    e       0.00    0.00
    f       0.00    0.00
    g       0.25    0.00

  原图七个数字是 [0.00 1.00 1.00 0.25 0.00 0.00 0.25]。
  b 和 c 是整整 1.00,d 和 g 是 0.25——那点零头不是错,
  是竖段的末端压在了横段的格子上。七段数码管本来就允许段与段在角落相交。
  往右挪一格之后变成 [0.00 0.00 0.00 0.00 0.00 0.00 0.00]。
  七个位置全归零。图上明明还站着两条竖线,可它们已经跑到掩码外面去了,
  于是这个样本在段码表示里彻底消失,成了和「全黑的图」一模一样的东西。
  单像素宽的竖段被卡死在一列上,容错量正好是一格。

[6] 验证:不让图平移,段码还吃亏吗
  三个条件各跑三种表示。为了不让「不平移」退化成十张一模一样的图,
  不平移那一组每个样本翻 2 个随机像素当扰动。

    条件                        原始 64 像素    池化 16 维      7 维段码
    训练不平移、测试不平移      0.9775          0.8125          0.9775          
    训练平移、测试平移          0.9025          0.5563          0.5613          
    训练不平移、测试平移        0.2200          0.3625          0.2625          

  第一行里三种表示的差距一下子缩小了——段码从 0.5613 爬到 0.9775。
  和原始像素打成平手,两列都是 0.9775。
  只要图老实待在该待的位置,人想出来的七个数字确实够用——
  它平移那组输掉的 0.4163,全是位置造成的。

  第三行是把「训练没见过平移、测试全是平移」的情况摆出来,三种表示一起掉。
  有意思的是掉得最狠的反而是原始像素,0.2200,比段码的 0.2625 还低。
  原因不难猜:64 维网络把「第几行第几列亮着」当成了位置标签,
  位置一挪,所有标签同时失效;段码本来信息就少,反而没那么多错的先验可依赖。
  这也顺带说明一件事:分布偏移面前,输入维数高不是保护,是负担。

[7] 这件事跟这一章讲的应用案例有什么关系
  教科书 8.1 节列了图像、语音、文本三块战场,每一块的路线都是同一个形状:
  先有人工设计的特征,后来被端到端网络整个换掉。

    领域        深度学习之前              深度学习之后
    图像识别    SIFT/HOG 加 SVM          卷积网络直接吃像素
    语音识别    MFCC 加 GMM-HMM           频谱直接进网络
    自然语言    词袋加手工规则            词向量加循环网络

  这一节测出来的东西,解释了「换掉」为什么发生得那么彻底。
  手工特征从来不是不聪明,是太脆:它把「东西在哪」和「东西是什么」绑死了。
  SIFT 之所以搞得那么复杂——关键点检测、尺度空间、方向直方图——
  全是为了把这个绑定松开。而卷积网络从一开始就不绑:
  同一个卷积核在整张图上滑一遍,第 3 章 3.7 测过平移一格后
  卷积还有 0.5487,全连接只剩 0.3894。

  所以 8.1 那三块战场被端到端吃掉,不只是因为数据变多了,
  更因为「让网络自己找特征」这条路天生对位置不敏感。
  数据管够的时候,它把手工特征唯一的那点便宜也追平了。

[8] 三条必须说清楚的限制
  第一,这份数据是照着七段数码管造的,段码表示等于把答案抄进了输入,
  它输不是因为笨,是因为平移。换成真实手写数字,它连抄都没得抄。
  第二,十倍轮数只测了两个表示,中间那档池化的天花板没单独验,
  只能说它那 0.5563 上限的原因和段码类似——池化把细线结构糊掉了。
  第三,这里的结论只适用于「小样本加配准敏感」这一个角落。
  第 3 章那张表是反着测的:手工特征加线性只有七成六,
  原始像素加卷积是九成九。两边不矛盾——一边在说「什么时候人工特征划算」,
  一边在说「数据够多时谁赢」。

第一层结论就跟预期相反:段码垫底(0.5613),原始像素第一(0.9025)。 而且看训练准确率那一列,段码只有 0.7600——网络连训练集都背不下来。 这说明问题不在泛化,在输入本身。 第二层排除:把段码网络的隐层从 32 加到 45,参数量从 954 顶到 2890, 跟原始像素那台的 2778 差不多。准确率纹丝不动(0.5700)。 不是容量的事。 第三层排除:两千轮加到两万轮,学习率从 0.1 试到 0.5,训练准确率卡在 0.7650 不动。 两万轮还上不去,那就不是优化器偷懒,是输入里没这个信息。 第四层,找到真凶:数字 1 的段码是 [0, 1, 1, 0.25, 0, 0, 0.25],向右滚一格之后, 七个位置全变成 0.00。 图上明明还站着两条竖线,可它们已经跑到掩码外面去了。 这个样本在段码表示里,变成了一张全黑的图。 最后的对照实验最干净:不让图平移,三种表示的训练测试同分布,段码立刻涨到 0.9775, 跟原始像素(0.9775)打成平手。 它输掉的那 0.4163,全是位置造成的。 还有一个反直觉的结果:训练没见过平移、测试全是平移的时候, 掉得最狠的反而是原始像素(0.2200),比段码(0.2625)还低。 64 维网络把「第几行第几列亮着」当成了位置标签,位置一挪全失效。 输入维数高不是保护,是负担。

30 秒实验

  1. 把段码的掩码从一列宽改成两列宽。 平移一格之后就不会全归零了,准确率应该能爬回来一截。 这就是 SIFT 里「尺度空间」要解决的那件事的迷你版。
  2. 把数字 1 单独拿出来,向右滚 0 到 3 格,打印每一步的段码向量。 你会看到它一格一格地「消失」——这是人工特征脆性的最直观演示。
  3. 把三种表示都加上数据增强(随机平移),再跑一次平移测试。 三种都会涨,但涨的幅度不一样——原始像素涨得最多,因为它有东西可学。

常见误区

  1. 「深度学习赢了,所以人工特征没用了。」 这一节的对照实验说明相反:不平移的那一组里,人工特征和原始像素打平。 人工特征不是没用,是它的有效期取决于「世界有多规整」。
  2. 「卷积网络比全连接强,因为它参数少。」 参数少只是省显存。 第 3 章 3.7 和这一节都指向同一件事:卷积真正强的地方是它不管位置,权重在整张图上滑。
  3. 「AlexNet 那一年是一次突变。」 算法层面没那么突变——卷积、池化、ReLU、Dropout 在它之前都有。 突变的是「两个 GPU 加一百二十万张标注图」这个组合,凑齐了才跑得动。
自测三问(点开对答案) 1. **为什么七段码这个人挑的特征,反而输给了原始像素?** —— **因为它的掩码按位置硬框定,一像素宽的竖段容错量正好是一格。** **图一平移,掩码里就空了。去掉平移之后它立刻追平原始像素。** **所以它输的不是信息量,是配准。** 2. **语音识别那次替换,为什么叫「混合」而不是「端到端」?** —— **因为它只把链条里的 GMM 换成神经网络,HMM 那套结构留着了。** **真正端到端的语音识别要更晚,要等序列模型成熟。** **工程上的进步多半是这个形状:换一环,不是推倒重来。** 3. **词袋的两个硬伤分别靠什么补?** —— **词序丢失靠循环网络补,词与词之间没有关系靠词向量补。** **这两个东西合起来,才有了后来的文本模型。**

应用讲完了,接下来是「还能走多远」。 教材 8.2 节列了三个瓶颈:数据、算力、黑箱。 下一节把这三个都量一遍——能量出来的量,量不出来的说清楚为什么。

相关笔记