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 秒实验
- 把
epochs从 60 改成 10,看重构误差停在哪里。 它会停在 0.15 附近,说明这套数据十来轮就基本练完了,剩下的轮数是在磨细节。 - 把
NH=16改成 4,再练一遍,比较重构误差。 压缩比从 4 比 1 变成 16 比 1,误差会明显上升——隐层大小决定了「留住多少信息」。 - 把
lr=0.1改成 0.5 和 0.01 各试一次。 0.5 会在前几轮抖动,0.01 会练不透——这是第 2.9 节「学习率」在 RBM 上的重演。
常见误区
- 「CD 是精确的梯度,只是算得快。」 不是。CD 是有偏的近似,实测相对误差 0.44。 它的价值在于「方向对、成本低」,不在于「准确」。 把它当精确梯度用,会在需要严格概率估计的任务上出错。
- 「k 越大越准,所以取大一点总没错。」 实测 k 等于 20 反而没有比 k 等于 1 更好(0.1271 对 0.1270)。 走得太远会离开这批数据,把「起点离终点不远」这个前提破坏掉。 CD 的正确用法是「短链、勤更新」,不是「长链、少更新」。
- 「重构误差低就说明模型学得好。」 重构误差低只说明「能被复现」,不说明「分布学对了」。 一个只会背样本的模型重构误差可以非常低,但它对没见过的输入毫无判断力。 评估 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 已经能练、能用、能提特征了。 但一层只能表示一层特征,想要「笔画的组合」「整字的布局」,必须往上叠。 下一节解决叠起来之后的训练问题:下层冻结、逐层来练,最后整体微调。
- ☐ (本节完,下一节 4.5 深度信念网络)