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

玻尔兹曼机在 Hopfield 的基础上加了两样东西:隐层,和随机性。 于是它不再「记住几张图」,而是用一个概率分布描述「哪些状态常见、哪些罕见」。 概率由能量决定:能量越低的状态出现概率越大,公式是 exp(负能量) 除以配分函数 Z。 实测 32 个状态里最低的那个占 19.83%,最高的只占 0.0269%。 温度是旋钮:T 等于 0.05 时最低态占 100%,熵几乎为 0; T 等于 100 时 32 个状态几乎等概率,熵 3.4657 逼近上限。

📝 本节和前面几节的关系

上一节的网络只能回答「这个状态稳不稳」,这一节要回答「这个状态多常见」。 从「稳定」到「概率」的关键一步是引入温度:既然每个状态都可能出现, 那就得有个数说清「多可能」。 这一节也是后面所有内容的地基。 受限玻尔兹曼机的自由能、对比散度的采样、深度信念网络的预训练, 全都是在处理「这个概率怎么算、怎么采」的问题。

一、多了什么

相比 Hopfield 网络,玻尔兹曼机多了两处改变,每一处都改变了它的用途。 第一,加了隐层:一部分单元看得见(可见层),一部分看不见(隐层)。 第二,更新变成随机的:不再「局部场为正就取正」,而是「以某个概率取正」。

对比项 Hopfield 玻尔兹曼机
单元取值 正 1 与负 1 1 与 0
隐层 没有 有
更新方式 确定:按局部场取符号 随机:按 sigmoid 概率采样
描述的对象 几个具体的记忆点 整个状态空间上的分布
连接 全连接、对称 全连接、对称(同层也连)
隐层带来的是「表示能力」:可见层看到的是像素,
隐层可以去表示像素背后的因素(比如「这里有一根竖笔画」)。
随机性带来的是「可描述性」:只有确定性的网络,讨论「概率」没有意义。

二、能量定概率

玻尔兹曼机的能量函数比 Hopfield 多几项,因为同层之间也连。

E(v, h) = -b.v - c.h - v.W.h - 0.5*v.U.v - 0.5*h.P.h

b, c  可见层与隐层的偏置
W     可见 与 隐 之间的连接
U, P  可见层内部、隐层内部的连接

概率的写法只有一行:

P(状态) = exp(-E(状态)) / Z
Z = 所有状态 exp(-E) 之和

Z 叫配分函数,作用是让所有概率加起来等于 1。 实测 32 个状态算出 Z 等于 136.7214,概率总和 1.000000。 这个公式和信息论里的玻尔兹曼分布是同一个东西,名字也是从这里来的。 它的含义很直白:能量每低一点,概率就按指数放大一点——低能量的状态被指数级地偏爱。 最低的那个状态是 01111,能量负 3.3,概率 0.1983,占了将近两成。 最高的状态能量负 0.1,概率只有 0.000269——差了七百多倍。 注意这不是「谁出现次数多」,而是能量决定的固定分布,只要权重定了,分布就定了。

三、温度:给能量除一个数

把能量除以温度 T,再套进同一个公式,就得到了带温度的分布。

P(状态) = exp(-E(状态) / T) / Z(T)

T 趋于 0:除以一个很小的数,能量差被放大,概率全压到最低的那个状态上。 T 很大:除以一个大数,能量差被抹平,所有状态几乎等概率。

温度 T 最低态概率 概率过 1% 的状态数 熵(奈特)
0.05 1.0000 1 / 32 0.0005
0.20 0.8735 4 / 32 0.5328
0.50 0.4422 8 / 32 1.8511
1.00 0.1983 19 / 32 2.8220
2.00 0.0933 32 / 32 3.2913
10.00 0.0400 32 / 32 3.4592
100.00 0.0320 32 / 32 3.4657
熵那一列是这张表的重点:它衡量「不确定性有多大」。
32 个状态的熵上限是 log 32 等于 3.4657,也就是完全等概率时的值。
T 等于 100 时熵是 3.4657,说明网络已经彻底在乱猜;
T 等于 0.05 时熵是 0.0005,说明它咬死了那一个状态。
中间那档是折中:既能往低处偏,又保留跳出局部坑的随机性。
它到底算不算「更好」,要看你要什么——找最低态和按分布采样是两件不同的事。

四、吉布斯采样:一次只动一个单元

概率公式给了,但怎么从里面取样本?答案是吉布斯采样。 做法:别的单元都当已知,只算第 i 个单元取 1 的条件概率,然后掷一次骰子。

p(第 i 个单元取 1) = sigmoid(局部场 / T)
局部场 = 第 i 个单元取 1 比取 0 省下的能量

一次只动一个单元,其余保持不动,这叫单点吉布斯。 跑得足够久,访问各状态的频率就会逼近理论分布——实测跑 4 万步、扔掉前一半, 32 个状态的最大偏差只有 0.0074。 为什么必须一次只动一个? 因为「保持其余不动」是让条件概率有意义的前提:条件概率里的「条件」就是其余单元。 同时翻所有单元,等于在求一个根本不存在的联合条件概率。 能量景观只有一个深坑时,低温就已经足够:实测全程低温 0.05 走 12000 步, 30 次里 30 次都落在全局最低。 但全程高温 10 只有 1 次,全程中温 1 只有 7 次——它在状态之间不停乱跳,停不下来。 模拟退火是折中方案:先高温把该逛的地方逛遍,再逐步降温锁死。 它的本事要等能量景观里有好几个坑、还一个比一个深时才显出来。 眼前这个五单元小网络只有一个坑,所以低温就已经够用,退火看不出领先。

五、动手 Demo:五个单元的小网络:算出分布、扫温度、跑吉布斯采样

网络很小:3 个可见、2 个隐,一共 5 个单元、2 的 5 次方等于 32 个状态, 小到可以把每一个状态的能量都算出来。 参数是手工设的,刻意让两个可见单元 v0 与 v1 的层内权重为负 1(喜欢一起亮), v1 与 v2 为正 1(互相排斥)——这样能量景观里就不止一个坑。

NV, NH = 3, 2                      # 3 个可见、2 个隐
B = np.array([0.3, -0.2, 0.5])     # 可见层偏置
C = np.array([0.1, -0.4])          # 隐层偏置
W = np.array([[0.8, -0.6],
              [-0.5, 0.9],
              [0.7, 0.7]])          # 可见 乘 隐
U = np.array([[0.0, -1.0, 0.0],     # 可见层内部(同层也连)
              [-1.0, 0.0, 1.0],
              [0.0, 1.0, 0.0]])
P = np.array([[0.0, 0.5],
              [0.5, 0.0]])          # 隐层内部

ALL = np.array(list(itertools.product([0.0, 1.0], repeat=5)))   # 32 个状态


def energy(s):
    v, h = s[:NV], s[NV:]
    return float(-B @ v - C @ h - v @ W @ h - 0.5 * v @ U @ v - 0.5 * h @ P @ h)


E = np.array([energy(s) for s in ALL])
raw = np.exp(-E)
Z = float(raw.sum())               # 配分函数 136.7214
p1 = raw / Z                        # 玻尔兹曼分布,加起来 1.000000


def boltz(T):
    e = -E / T                     # 给能量除以温度
    e -= e.max()
    p = np.exp(e)
    return p / p.sum()


def gibbs(s, steps=2000, T=1.0, seed=0):
    rg = np.random.default_rng(seed)
    for t in range(steps):
        i = rg.integers(0, NV + NH)          # 随机挑一个单元
        p1_ = 1.0 / (1.0 + np.exp(-local_field(s, i, T)))
        s[i] = 1.0 if rg.random() < p1_ else 0.0
    return s

实跑输出:

--------------------------------------------------------------------
玻尔兹曼机:能量定概率,温度定随机性
--------------------------------------------------------------------
[1] 一个五单元的小网络:3 个可见、2 个隐
  状态记成 5 个 0 或 1;五个单元两两都可能相连(同一层内也连),这就是「玻尔兹曼机」
  能量写成四块:可见偏置、隐层偏置、可见乘隐的连接、层内连接
  能量函数 E = 负 b 乘 v 负 c 乘 h 负 v 乘 W 乘 h 负 半个 v 乘 U 乘 v 负 半个 h 乘 P 乘 h
  这里刻意让两个可见单元 v0 与 v1 的层内权重为 负 1(喜欢一起亮),
  v1 与 v2 为 正 1(互相排斥)——这样能量景观里就有不止一个坑
  5 个单元一共 2 的 5 次方等于 32 个状态,可以直接全部穷举

[2] 玻尔兹曼分布:能量越低,出现的概率越大
  配分函数 Z 是把全部 32 个状态的 exp(负能量) 加起来,这里 Z = 136.7214
  概率就是 exp(负能量) 除以 Z,加起来 1.000000(必然等于 1)

  排名  状态 v0v1v2 h0h1    能量      概率
  1     01111               -3.3000   0.1983
  2     11111               -2.8000   0.1203
  3     10111               -2.6000   0.0985
  4     01101               -2.5000   0.0891
  5     10110               -2.4000   0.0806
  最低能量的状态占了 19.83%,最高能量的状态只占 2.69e-01%
  注意概率不是「谁出现次数多」,而是能量决定的固定分布——这是它和 Hopfield 最大的区别

[3] 温度 T:给能量除一个数,概率分布就变形了
  温度 T   最低态概率    状态数(概率过 1%)   熵(奈特)
  0.05     1.0000        1 / 32                0.0005
  0.20     0.8735        4 / 32                0.5328
  0.50     0.4422        8 / 32                1.8511
  1.00     0.1983        19 / 32               2.8220
  2.00     0.0933        32 / 32               3.2913
  10.00    0.0400        32 / 32               3.4592
  100.00   0.0320        32 / 32               3.4657

  温度趋于 0,概率全压到最低态上,熵趋于 0,网络变成一台死记硬背的机器;
  温度很高,32 个状态几乎等概率,熵逼近上限 3.4657,等于在乱猜。
  中间那档是个折中:既能往低处偏,又保留跳出局部坑的随机性。
  它到底算不算「更好」,要看你要什么——找最低态和按分布采样是两件不同的事。

[4] 吉布斯采样:一次只动一个单元,按条件概率决定它翻不翻
  别的单元都当已知,第 i 个单元取 1 的概率就是 sigmoid(局部场 除 温度)
  局部场等于「它取 1 比取 0 省下多少能量」,正数说明取 1 划算
  这样一次一步地改,跑得足够久,访问各状态的频率就会逼近第 2 段算出的理论概率

  从初态 11001 出发,T 等于 1,采样 40000 步,扔掉前一半

  状态        理论概率    采样频率    差距
  01111       0.1983      0.1940      -0.0044
  11111       0.1203      0.1262      +0.0059
  10111       0.0985      0.0948      -0.0037
  01101       0.0891      0.0818      -0.0074
  全部 32 个状态的最大偏差 0.0074,吻合得相当好

[5] 温度旋钮的作用:乱逛与下钻
  温度策略              结束时正落在全局最低态(共 30 次)
  全程高温 10           1 / 30
  全程中温 1            7 / 30
  全程低温 0.05         30 / 30
  从 10 降到 0.05       30 / 30

  高温那一行最惨:它在各个状态之间不停乱跳,压根停不下来,最后停在哪儿全看运气。
  低温那一行反而最好,因为这个网络只有一个明显的深坑,只要往下滑就滑对了。
  模拟退火(最后一行)先高温把该逛的地方逛遍,再逐步降温锁死——
  它的本事要等能量景观里有好几个坑、还一个比一个深时才显出来。
  眼前这个五单元的小网络只有一个坑,低温就已经够用,所以退火看不出领先。
  这也解释了为什么玻尔兹曼机的训练那么慢:每算一次梯度都要在链上跑很多步。

[6] 后一节要砍掉的东西
  这个小网络有 10 个连接:可见层内部 3 对、隐层内部 1 对、跨层 6 对
  层内连接让它很强,也让「已知可见层时隐层各单元互相影响」——条件概率算不出来
  下一节把层内连接全部砍掉,只留可见与隐之间的连接,这就是受限玻尔兹曼机

第二段在验证一件事:概率加起来必须等于 1。 这不是巧合,是除以 Z 的目的——Z 就是为了归一化才存在的。 第四段最有说服力。 从初态 11001 出发,用单点吉布斯跑 4 万步、扔掉前一半, 采样频率和理论概率的最大偏差只有 0.0074。 最高几个状态:理论 0.1983 对采样 0.1940、0.1203 对 0.1262、0.0985 对 0.0948。 这说明「按条件概率一步步改」确实能忠实地重现那个分布——不是近似,是收敛到它。 第五段的结果有点反直觉:低温比退火还好用。 全程低温 0.05 是 30 次全中,退火也是 30 次全中,两者打平; 而中温 1 只有 7 次、高温 10 只有 1 次。 原因很简单:这个网络的能量景观只有一个深坑,随便从哪儿出发,往下滑都滑到同一个地方。 退火的价值要靠「好几个坑」才体现得出来,这里没有那个条件。 所以别把这张表读成「退火没用」,要读成「坑少时低温就够,坑多时才需要退火」。 最后一段是这一节的落点。 这个网络有 10 对连接,其中 4 对是同层内部的。 正是这 4 对连接让「已知可见层时隐层各单元还互相影响」——条件概率算不出来,只能一个个轮流采。 下一节要把这 4 对全砍掉。

30 秒实验

  1. 把 U 里的负 1 改成 0(取消 v0 与 v1 的相互吸引),再看温度表。 能量景观变平,最低态的概率优势会缩小,同一温度下的熵会变大。
  2. 把 boltz(T) 里的 T 换成 0.01,看最低态概率变成多少。 它会变成 1.0000 的近似值——这就是「零温极限」,网络退化成一个只会背书的机器。
  3. 把吉布斯采样的步数从 40000 改成 2000,再对比采样频率与理论概率。 偏差会明显变大,这说明「跑得足够久」不是客套话,是收敛的前提。

常见误区

  1. 「能量越低说明模型越好。」 能量是状态的属性,不是模型的评分。 训练玻尔兹曼机是让「数据状态的自由能」变低,不是让某个状态的能量变低。 而且不同权重下的能量尺度不能互相比较,只能在同一组参数内部比高低。
  2. 「配分函数 Z 可以顺便算出来。」 不能。Z 要把所有状态的能量加起来,状态数是 2 的单元个数次方。 5 个单元还能穷举,64 个可见加 100 个隐就是 2 的 164 次方——宇宙里没有那么多算力。 整个对比散度算法,就是为了绕开这个 Z 才被发明出来的。
  3. 「高温能到处逛,所以高温更好。」 高温确实逛得广,但它停不下来。 实测全程高温 10 走 12000 步,30 次里只有 1 次恰好停在全局最低态——它采到的是整个分布, 不是最低点。 要最低点就得降温,要分布就得保持温度,两件事不能同时满足。
自测三问(点开对答案) 1. **温度趋于 0 和趋于无穷,分布分别变成什么?** —— **T 趋于 0:概率全压到能量最低的那个状态上,熵趋于 0。** **T 趋于无穷:所有状态等概率,每个占 1 除以状态数,熵趋向 log 状态数。** **实测 T 等于 0.05 时最低态占 1.0000、熵 0.0005;T 等于 100 时占 0.0320、熵 3.4657。** 2. **为什么吉布斯采样一次只能改一个单元?** —— **因为条件概率的「条件」是其余单元固定不变。** **同时改多个单元,就等于在求一个没有定义的联合条件概率,采出来的样本不再服从目标分布。** **这也是它慢的原因:一步只能动一个,跑几万步才能采到一个像样的样本。** 3. **配分函数算不出来,为什么还能用这个模型?** —— **因为很多用途只需要「能量」或「条件概率」,不需要 Z。** **比如给定可见层采隐层、算自由能、做梯度更新,这些都能绕开 Z。** **只有在需要「某个可见状态的绝对概率」时才非算 Z 不可,而那个用法在实践中很少。**

玻尔兹曼机有两个麻烦:一是同层连接让条件概率算不出,二是 Z 算不出。 下一节砍掉同层连接,第一个麻烦直接消失; 顺带还白捡一个「自由能」,把第二个麻烦压缩到「只跟可见层有关」。

相关笔记