📗 第4章 受限玻尔兹曼机
📄 一句话概括

RBM 的梯度形式极干净:数据上的「可见乘隐层」平均,减去模型上的同一个平均。 第一项手到擒来,第二项要在模型分布里采样——这一步贵到没法接受。 对比散度只改这一处:不等链收敛,直接从数据出发走 k 步,用终点冒充「模型的世界」。 k 取 1 就够用,所以叫 CD-1。 实测 k 等于 1 与 k 等于 20 的末轮重构误差都在 0.125 到 0.127 之间, 耗时却差 8 倍——这就是 CD-1 成为默认做法的原因。

📝 本节和前面几节的关系

上一节把结构砍成了二分图,采一步的代价从「成千上万次轮流更新」降到「一次矩阵乘」。 这一节要解决的是「要采多少步」——答案是:一步就够。 这一节是全章的技术核心。 没有对比散度,RBM 只是个漂亮的概率模型;有了它,RBM 才变成能跑的东西。 2006 年深度学习重新出发的那一批工作,几乎都建立在它上面。

一、梯度长什么样

训练 RBM 的目标是让数据出现的概率最大,也就是最大化对数似然。 对它求导,得到的结果出奇地整齐:

d(对数似然) / dW = <v 乘 h>数据  -  <v 乘 h>模型

<> 表示「在那个分布下求平均」

第一项:手里就有数据,把可见层送进去算出隐层概率,两层一乘再按批平均,就完事了。 第二项:要在「模型自己认为的世界」里求平均,而那个世界只有采样才能看见。 这个形式在统计物理里有个名字,叫「正相位减负相位」,也叫 wake-sleep。 直观意思是:数据说「这两个单元经常一起亮」, 模型说「我也觉得它们经常一起亮」——两者之差就是该改的方向。 问题全在第二项。 要采到收敛,玻尔兹曼机时代要跑成千上万步,而且一次只能动一个单元,没法并行。 网络一大,这一步就把训练拖死了——这是 RBM 在 1980 年代到 2000 年代一直不温不火的直接原因。

二、对比散度的主张

2002 年 Hinton 提出的做法只有一句话:不必等链收敛,从数据出发走几步就够。

1. 取一批真数据当起点 v0
2. 用 v0 采 h0,用 h0 采 v1,用 v1 采 h1 …… 来回走 k 步,得到 vk 与 hk
3. 拿 vk 那一端当「模型的世界」,代入梯度公式
4. 更新权重,换下一批数据

k 等于 1 就是只走一步,也就是「用数据采一次隐层,再用隐层采回可见层」。 这个操作有个更朴素的名字:重构。 为什么敢这么近似? 因为训练好的模型分布本该贴着数据分布,起点已经离终点不远。 从数据出发走几步不会被拉太远,用这个终点估计「模型的世界」,偏差有限。 这个理由听起来有点赖,但实测站得住。 下面用一个小网络(可见 6、隐 3,共 512 个状态)算出真梯度,拿来和 CD 比:

做法 与真梯度的夹角余弦 相对误差
CD-1 0.9471 0.4407
CD-5 0.9694 0.3070
余弦 0.9 出头,说明方向基本对;相对误差三四成,说明幅度偏得不小。
这个组合正好够用:梯度方向对了就能学下去,幅度偏只是收敛慢些、结果糙些。
k 从 1 加到 5,余弦从 0.9471 升到 0.9694、相对误差从 0.44 降到 0.31——多走四步确实更准,
但你要付四倍的采样成本。

三、实测:k 到底取几

下面在 450 张 8 乘 8 数字图上练一个 64 到 16 的 RBM,k 分别取 1、5、20:

k 耗时 首轮重构误差 末轮重构误差
1 0.05 秒 0.3400 0.1270
5 0.12 秒 0.3428 0.1250
20 0.37 秒 0.3405 0.1271
末轮误差都在 0.125 到 0.127 之间,差别不到 2%;耗时却是 8 倍的关系。
更值得注意的是:k 等于 20 并没有比 k 等于 1 更好(0.1271 对 0.1270)。
换句话说,在这个任务上多走的那十几步纯属白花。
为什么多走反而没用?
因为每一批数据只有 16 张,链走 20 步之后早就跑离这批数据了,
采到的是「模型当前认为的世界」的一个更远的角落,和这批数据的关系反而更淡。
CD 的整个前提是「起点离终点不远」,走远了就把这个前提破坏了。
工程结论很干脆:默认用 CD-1,只有在「数据分布特别复杂、模型特别容易跑偏」时才加到 5 或 10。
这个选择不是理论推出来的,是算力账算出来的。

四、练出来的隐层是什么样

把第 j 个隐单元连到 64 个像素的权重按 8 乘 8 摆回去,就是它「盯着的图案」。 训练完之后,前三个隐单元的权重图形都像「数字 8 的骨架」——不是各管一段笔画。 这个结果和教科书上的 MNIST 示例不太一样,原因在样本: 十个数字全用同一套七段笔画画出来,隐单元想区分也区分不出来,只好先学一个公共模板。 换成真正的手写数字,隐单元才会分化出笔画探测器、局部笔锋、整字轮廓等分工。 关键点不变:没有标签、没人告诉它「这是数字」,只让它复现输入,它自己就长出了结构。 这是 RBM 作为特征提取器的核心卖点,也是下一节深度信念网络的前提。 训练完之后做去噪,效果相当可观:

任务 输入与原图的差 重构与原图的差
数字 3,打坏 10 个像素 0.1562 0.0469
数字 8,打坏 14 个像素 0.2188 0.0469
两行的输入损坏程度差了 40%,重构之后却都落到 0.0469——也就是 3 个像素没补对。
补不回的正好是「坏的位置上两种走势都说得通」的地方。
这说明它学到的是输入的统计规律,不是死记硬背每一张图。

五、动手 Demo:CD-1 练一个 RBM:三种 k、两张权重图、一次梯度对账

数据是 450 张 8 乘 8 的数字图(0 到 9 各 45 张,每张打坏 2 个像素)。 可见层 64 个、隐层 16 个,批量 16,60 轮。 注意 cd_step 里那句 V.T @ p0 - Vk.T @ ph(Vk, W, c): 减号左右两边就是「数据那一端」和「模型那一端」。

def ph(v, W, c):
    return sig(c + v @ W)          # 给定可见层,全部隐单元取 1 的概率


def pv(h, W, b):
    return sig(b + h @ W.T)


def cd_step(V, W, b, c, k=1, lr=0.1):
    u"""一次对比散度的权重更新,V 是一批可见向量。"""
    p0 = ph(V, W, c)                          # 数据那一端
    H0 = (np.random.random(p0.shape) < p0).astype(float)
    Hk, Vk = H0, V
    for _ in range(k):                        # 只走 k 步,不等收敛
        Vk = (np.random.random(pv(Hk, W, b).shape)
              < pv(Hk, W, b)).astype(float)
        Hk = (np.random.random(ph(Vk, W, c).shape)
              < ph(Vk, W, c)).astype(float)
    n = len(V)
    dW = V.T @ p0 / n - Vk.T @ ph(Vk, W, c) / n   # 数据平均 减 模型平均
    db = V.mean(0) - Vk.mean(0)
    dc = p0.mean(0) - ph(Vk, W, c).mean(0)
    return W + lr * dW, b + lr * db, c + lr * dc


def recon_err(V, W, b, c):
    u"""重构误差:原样送进去再原样出来,逐像素差的平均值。"""
    return float(np.abs(pv(ph(V, W, c), W, b) - V).mean())

实跑输出:

--------------------------------------------------------------------
对比散度:算不出的梯度,用走几步顶替
--------------------------------------------------------------------
[1] 训练 RBM 的梯度长什么样
  对数似然的梯度,形式上非常干净:数据上的 <v 乘 h> 减去 模型上的 <v 乘 h>
  <> 表示「在那个分布下求平均」。第一项好办:手里就有数据,直接算
  第二项要命:要在「模型自己认为的世界」里求平均,而模型分布要靠随机采样才能看
  玻尔兹曼机时代就是硬采——每一步都不能并行,采到收敛要成千上万步,网络一大就废了

[2] 对比散度的主张:不必等链收敛,从数据出发走几步就够
  取一批真数据当起点,跑 k 步吉布斯,把终点当成「模型的世界」的代表
  k 等于 1 就是走一步。用这个近似值代替第二项,然后照常做梯度上升
  为什么敢这么做?因为训练好的模型分布本该贴着数据分布,起点已经离终点不远

  数据:450 张 8 乘 8 的数字 0 到 9(每类 45 张),可见层 64 个,隐层 16 个
  k     耗时      首轮重构误差    末轮重构误差
  1     0.05 秒   0.3400          0.1270
  5     0.11 秒   0.3428          0.1250
  20    0.38 秒   0.3405          0.1271

  k 等于 1 的重构误差逐轮:0.340 0.156 0.142 0.132 0.127 0.132 0.130 0.130 0.124 0.123 0.126 0.127 0.125 0.130 0.127
  末轮误差都在 0.125 到 0.127 之间,差别不到 2%;耗时却是 8 倍的关系。
  而且 k 等于 20 并没有比 k 等于 1 更好(0.1271 对 0.1270),说明在这个任务上
  多走的那十几步纯属白花。这就是 CD-1 成为默认做法的原因。

[3] 练出来的隐层长什么样
  把第 j 个隐单元连到 64 个像素的权重按 8 乘 8 摆回去,就是它「盯着的图案」

  第 0 个隐单元的权重(# 为正,. 为负):
    . . . . . . . .
    . . # # # # # .
    . # # # . # . .
    . . . . . # . .
    . . # # # # . .
    . - - . . # . .
    . . . . . # . .
    . . # # # # . .

  第 1 个隐单元的权重(# 为正,. 为负):
    . . . . . . . .
    . . # # # # # .
    . # # # . # . .
    . . . . . # . .
    . . # # # # . .
    . - - . . # . .
    . . . . . # . .
    . . # # # # . .

  第 2 个隐单元的权重(# 为正,. 为负):
    . . . . . . . .
    . . # # # # # .
    . # # # . # . .
    . . . . . # . .
    . . # # # # . .
    . - - - . # . .
    . . . . . # . .
    . . # # # # . .

  这三个都像「数字 8 的骨架」,不是各管一段笔画。原因是样本太同质:
  十个数字全用同一套七段笔画,隐单元想区分也区分不出来,只好先学一个公共模板。
  真跑 MNIST 那种手写字时,隐单元才会分化出笔画探测器、局部笔锋、整字轮廓等分工。
  关键点不变:没有标签、没人告诉它「这是数字」,只让它复现输入,它自己就长出了结构。

[4] 训练完之后,能不能把坏掉的图补回来
  数字 3,打坏 10 个像素:输入与原图差 0.1562,重构与原图差 0.0469
  数字 8,打坏 14 个像素:输入与原图差 0.2188,重构与原图差 0.0469
  重构能补回大部分,但补不回的正好是「坏的位置上两种走势都说得通」的地方。
  这提示 RBM 学到的是输入的统计规律,不是死记硬背每一张图。

[5] 检查一下 CD 的梯度到底偏多少
  用一个小网络(可见 6、隐 3,共 512 个状态)就能算出真梯度,拿来和 CD 比
  做法        与真梯度的夹角余弦    相对误差
  CD-1        0.9471                0.4407
  CD-5        0.9694                0.3070

  余弦 0.9 出头,说明方向基本对;相对误差三四成,说明幅度偏得不小。
  这和训练时的观察对得上:方向对了就能学下去,幅度偏只是收敛慢些、结果糙些。
  k 从 1 加到 5,余弦从 0.9471 升到 0.9694、相对误差从 0.44 降到 0.31——
  多走四步确实更准,但你得付四倍的采样成本,这笔账在实践中通常不划算。

[6] 一句话回顾
  梯度=数据平均 减 模型平均;模型平均要靠采样,采样太慢,于是用「数据出发走 k 步」顶替
  k 等于 1 就够用,这叫 CD-1;这一步近似让 RBM 从「理论上漂亮」变成「实际上能跑」

耗时那一列要连着机器说:0.05 秒对 0.37 秒,绝对值都很小,但比值 8 倍是稳的。 放大到百万级样本、上千个隐单元,8 倍就是「一下午」和「一周」的差别。 第五段的梯度对账是这一节最硬的一处。 它在 512 个状态的极小网络上算出真梯度,再和 CD 的近似梯度比夹角。 结果说明了两件事:方法有效(方向对),但确实粗糙(幅度偏三四成)。 很多教程只说「CD 近似得很好」,不说好到什么程度——这里给出的是具体数字。 第四段的去噪表里,两行重构差都恰好是 0.0469,这不是巧合。 0.0469 乘 64 等于 3,也就是每张图都有 3 个像素没补对。 哪 3 个? 通常是笔画交叉处那些「亮也行、暗也行」的位置——模型认为两种都合理,就按概率取了中间值。 一个容易看漏的细节:cd_step 里用的采样是「按概率掷骰子」, 而误差计算 recon_err 里用的是概率本身。 前者是训练需要随机性,后者是评估需要稳定性——两处不能混。

30 秒实验

  1. 把 epochs 从 60 改成 10,看重构误差停在哪里。 它会停在 0.15 附近,说明这套数据十来轮就基本练完了,剩下的轮数是在磨细节。
  2. 把 NH=16 改成 4,再练一遍,比较重构误差。 压缩比从 4 比 1 变成 16 比 1,误差会明显上升——隐层大小决定了「留住多少信息」。
  3. 把 lr=0.1 改成 0.5 和 0.01 各试一次。 0.5 会在前几轮抖动,0.01 会练不透——这是第 2.9 节「学习率」在 RBM 上的重演。

常见误区

  1. 「CD 是精确的梯度,只是算得快。」 不是。CD 是有偏的近似,实测相对误差 0.44。 它的价值在于「方向对、成本低」,不在于「准确」。 把它当精确梯度用,会在需要严格概率估计的任务上出错。
  2. 「k 越大越准,所以取大一点总没错。」 实测 k 等于 20 反而没有比 k 等于 1 更好(0.1271 对 0.1270)。 走得太远会离开这批数据,把「起点离终点不远」这个前提破坏掉。 CD 的正确用法是「短链、勤更新」,不是「长链、少更新」。
  3. 「重构误差低就说明模型学得好。」 重构误差低只说明「能被复现」,不说明「分布学对了」。 一个只会背样本的模型重构误差可以非常低,但它对没见过的输入毫无判断力。 评估 RBM 要看「从模型采样出来的东西像不像数据」,而不只是重构。
自测三问(点开对答案) 1. **梯度里的两项分别是什么?为什么第二项难算?** —— **数据上的「可见乘隐层」平均,和模型上的同一个平均。** **第一项有数据就能算;第二项要在模型分布里采样,而采样要跑很多步、** **还要算配分函数 Z——Z 的规模是 2 的单元个数次方,根本算不出来。** 2. **为什么 CD-1 走一步就够用?** —— **因为训练目标就是让模型分布贴近数据分布,两者的起点本来就接近。** **走一步得到的样本已经带上了「模型怎么看这批数据」的信息,够用来估方向。** **实测方向余弦 0.9471,足以让训练收敛,只是结果糙一点。** 3. **CD-1 与 CD-5 的取舍该怎么定?** —— **算力账:k 从 1 到 5,采样成本涨 5 倍,梯度余弦只从 0.9471 涨到 0.9694。** **默认用 1;只有在训练明显不稳、或者数据分布特别复杂时才加到 5 或 10。** **这个结论没有理论保证,是从实测和实践中总结出来的。**

到这儿,单层 RBM 已经能练、能用、能提特征了。 但一层只能表示一层特征,想要「笔画的组合」「整字的布局」,必须往上叠。 下一节解决叠起来之后的训练问题:下层冻结、逐层来练,最后整体微调。

相关笔记