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

Hopfield 网络是最早的「用能量描述记忆」的网络:没有隐层、没有输出层、没有损失函数, 训练只做一次外积求和。 它把每张图当成一个能量低谷,回想就是从一个坏掉的状态一路往下滑, 滑到哪个谷底就算想起了哪张图。 容量有限,而且怕样本相似:实测存 2 张数字图 20 次全中,存 3 张直接 0 次全中; 换成伪逆规则能扛到 5 张。

📝 本节和前面几节的关系

第 2 章的网络是「喂进去一个输入,吐出来一个输出」,权重靠反向传播改。 这一章换了个完全不同的思路:网络不吐输出,它自己演化到一个稳定状态。 学习也不是改权重去减小误差,而是一次性把样本「写」进连接里。 这一节是整章的地基。 能量、吸引子、状态演化这三个概念,会一路用到玻尔兹曼机、受限玻尔兹曼机、 深度信念网络——只是后面会加上随机性和隐层。

一、它长什么样

结构简单到不像神经网络:一层单元,全连接,没有隐层,没有输出层。 每个单元的取值只有两种,正 1 或负 1(也有人用 1 和 0)。 连接权重对称:从单元 i 到 j 的权重,等于从 j 到 i 的权重。 单元不连自己——自己和自己同时亮这件事没有任何信息量。 它的运行方式也和前面的网络不同。 前馈网络是「输入进来、一层层往前算、最后出一个值」,算完就结束。 Hopfield 网络是「给一个初始状态,然后反复改,改到不再变为止」。 输出不是一个数,而是一个状态——整个网络的取值模式。

对比项 前馈网络(第 2 章) Hopfield 网络
层数 输入、隐、输出至少三层 只有一层
取值 实数 只有正 1 与负 1
训练 反向传播改权重 一次外积求和,算完就固定
推理 一次前向,立刻出结果 反复迭代,直到状态不再变
目标 输出逼近标签 状态落进某个吸引子
为什么权重必须对称?
因为只有这样,才存在一个叫能量的标量函数,它的下降方向正好等于状态演化的方向。
权重一不对称,就不存在这样的函数,整个「往低处滑」的说法就站不住脚了。

二、能量函数与吸引子

能量定义成:负的半个「状态乘权重乘状态」。

E(s) = -0.5 * s.T @ W @ s

为什么这么定义? 因为它有一个关键性质:改动一个单元,能量的变化量正好可以用「局部场」表示出来。

翻转第 i 个单元,能量变化 = -2 * 局部场 * 旧值
其中 局部场 = W[i] @ s(第 i 行权重与当前状态的内积)

局部场可以读成「别的单元都在劝它往哪边倒」。 而更新规则是「局部场为正就取正、为负就取负」。 代进上面的式子,变化量一定是负数——只要还有单元要翻,能量必然下降。 能量有下界(有限个状态、每个状态的能量是有限值),所以它不可能一直降。 降不下去的地方叫吸引子,也就是网络「记起来」的那个状态。 实测从加了 8 个像素噪声的数字 0 出发,能量从负 769 一路降到负 1521,一共翻转 8 次就停住, 和原图 64 个像素全部对上。 这里有个容易看漏的点:能量单调下降保证了「一定停下来」,但没有保证「停在原来那张图上」。 如果网络把几张图记混了,它也会稳稳地停在某个混合状态上——那就是记错了。

三、训练:一次外积,没有迭代

Hebb 规则一句话:两个单元同时兴奋,就把它们之间的连接加强。 写成矩阵就是样本矩阵转置乘自己,再除以样本数。

W = patterns.T @ patterns / len(patterns)
np.fill_diagonal(W, 0.0)

这一步没有任何迭代、没有学习率、没有轮数。 样本进来,矩阵乘一次,权重就永久定下来了——所谓「一次写入」。 64 个单元的网络,权重矩阵是 64 乘 64,去掉对角线还剩 4032 个。 这种学习方式和第 2 章的反向传播完全是两个世界。 反向传播是「试错」:先猜、再算错多少、再往反方向改一点,反复几万次。 Hebb 是「一次性登记」:把样本的统计相关性直接抄进权重。 代价是它不会从错误中学习,也无法处理「同一输入该有两个不同答案」的任务。

四、容量:存得下几张

经典结论说,N 个单元的 Hopfield 网络大约能存 0.138 乘 N 张互不相关的图。 这里是 64 个单元,上界约 8.8 张。 但实测比上界惨得多。 下面这张表是同一批数字图(0 到 9 依次多存几张),每张加 8 个像素噪声、各试 20 次, 统计「能完整回想出来」的次数:

存储张数 Hebb 规则 伪逆规则
2 20 / 20 20 / 20
3 0 / 20 20 / 20
4 4 / 20 20 / 20
5 4 / 20 20 / 20
6 3 / 20 14 / 20
8 2 / 20 9 / 20
10 2 / 20 8 / 20
Hebb 那一列在第 3 张就崩了,离 8.8 张的理论上界差得很远。
原因不在公式写错,而在样本太像:七段数码管的数字共用同一批笔画,任意两张都高度相关,
外积平均时互相干扰。
0.138 乘 N 那个上界的前提是样本互相独立,眼前这十张图明显不满足。
伪逆规则换了个思路:不再简单平均,而是先解一个线性方程,让样本之间的串扰在权重里就被抵消掉。
同样这十张图,它能扛到五张满分、十张还有 8 次。
代价是要求一次矩阵求逆,样本数一大就算不动——所以它只适合小规模记忆任务。

五、动手 Demo:存两张数字图,加噪之后让它自己走回来

下面这段代码做四件事:用七段数码管画 8 乘 8 的数字图、把 0 与 1 换成负 1 与正 1、 用外积一次算出权重、加 8 个像素噪声后反复翻转。 注意 recall 里每次只翻一个单元,这叫异步更新。 如果同时翻所有单元,能量就不保证单调下降了——这条纪律不能破。

SEG = {0: 'abcdef', 1: 'bc', 2: 'abged', 3: 'abgcd', 4: 'fgbc',
       5: 'afgcd', 6: 'afgecd', 7: 'abc', 8: 'abcdefg', 9: 'abcdfg'}


def one_digit(k, size=8):
    u"""用七段数码管的思路在 8 乘 8 上画一个数字。"""
    POS = {'a': (1, 2, 5), 'b': (2, 5, 3), 'c': (5, 5, 3), 'd': (7, 2, 4),
           'e': (5, 1, 3), 'f': (2, 1, 3), 'g': (4, 2, 4)}
    img = np.zeros((size, size))
    for sg in SEG[k]:
        r, c0, ln = POS[sg]
        if sg in ('b', 'c'):
            img[r:r + ln, c0] = 1.0
        else:
            img[r, c0:c0 + ln] = 1.0
    return img


def to_pm(img):
    u"""0 与 1 换成 负 1 与 正 1,这是 Hopfield 网络的取值。"""
    return img.ravel() * 2.0 - 1.0


def hebb(patterns):
    u"""一次外积求和:同时亮的单元连正,同时暗的连负,除以张数取平均。"""
    W = patterns.T @ patterns / len(patterns)
    np.fill_diagonal(W, 0.0)      # 单元不连自己
    return W


def energy(s, W):
    return float(-0.5 * s @ W @ s)


def recall(s, W, max_sweeps=20, seed=0):
    u"""异步更新:每次随机挑一个单元,按其它单元的影响翻转它自己。"""
    rg = np.random.default_rng(seed)
    s = s.copy()
    hist = [energy(s, W)]
    for _ in range(max_sweeps):
        changed = 0
        for i in rg.permutation(len(s)):
            local = float(W[i] @ s)
            new = 1.0 if local > 0.0 else -1.0
            if new != s[i]:
                s[i] = new
                changed += 1
                hist.append(energy(s, W))
        if changed == 0:
            break
    return s, hist


main = [0, 3]                      # 先存两个数字
patterns = np.stack([to_pm(one_digit(k)) for k in main])
W = hebb(patterns)                 # 训练到此结束

noisy = patterns[0].copy()
noisy[rng.choice(64, 8, replace=False)] *= -1.0
rec, hist = recall(noisy, W, seed=2)
print(np.mean(rec == patterns[0]))  # 1.0000
print(hist[0], hist[-1])            # -769.00 到 -1521.00

实跑输出:

--------------------------------------------------------------------
Hopfield 网络:一次外积存图,反复翻转找回
--------------------------------------------------------------------
[1] 「存」两张图:把 8 乘 8 摊平成 64 个 正负 1
  这里说的存,不是把图写进硬盘,而是把 64 乘 64 的连接矩阵算出来
  规则:两个单元在某张图里同时亮,权重就加一点;同时暗就减一点
  三行代码一行搞定:样本矩阵转置乘自己,除以张数,再把对角线清零
  对角线清零是因为单元不连自己——自己当然和自己同时亮,那一项没有信息

  存进去两张:数字 0 和数字 3;连接矩阵 64 乘 64,权重量 4032 个

[2] 加噪声,看它能不能自己走回来
  原图(数字 0):
    . . . . . . . .
    . . # # # # # .
    . # # # . # . .
    . . . . . # . .
    . . . . . # . .
    . # # # . # . .
    . . . . . # . .
    . . # # # # . .
  打乱 8 个像素(64 个里坏 8 个,12.5%):
    . . # . . . . .
    # . # # # # # .
    . # # # . # . .
    . . # . . . . .
    . . . . . # . .
    . # # # # # . .
    . . . # . # . .
    . # # # . # . .
  反复翻转直到稳定,结果:
    . . . . . . . .
    . . # # # # # .
    . # # # . # . .
    . . . . . # . .
    . . . . . # . .
    . # # # . # . .
    . . . . . # . .
    . . # # # # . .
  与原图一致的比例 1.0000;能量 -769.00 降到 -1521.00;一共翻转 8 次
  能量逐次变化:-769.0 -849.0 -933.0 -1021.0 -1113.0 -1209.0 -1309.0 -1413.0 -1521.0

[3] 能量为什么保证一路往下
  翻转第 i 个单元时,能量的变化量正好等于 负 2 乘 局部场 乘 旧值
  局部场就是第 i 行权重和当前状态的加权和,等于「别的单元都劝它往哪边倒」
  而更新规则是「局部场为正取正、为负取负」,所以这个变化量必定是负的
  于是只要还有单元要翻,能量必然下降;能量又有下界,必然停在某个状态
  停下来的状态叫吸引子,网络就把这个吸引子当成本次回想出来的图
  注意这里没有损失函数、没有梯度、没有学习率——「好」直接用能量定义

[4] 容量是有限的:存得越多,混得越厉害
  下面把 0 到 9 依次多存几张,每张图加 8 个像素噪声、各试 20 次
  两种规则对比:Hebb 是上面用的外积平均,伪逆是先解一个线性方程再求权重

  存储张数   规则   完全回想                平均最终能量
  2          Hebb   20 / 20                 -1316.00
  2          伪逆   20 / 20                 -31.00
  3          Hebb   0 / 20                  -1308.67
  3          伪逆   20 / 20                 -30.50
  4          Hebb   4 / 20                  -1334.45
  4          伪逆   20 / 20                 -30.00
  5          Hebb   4 / 20                  -1233.60
  5          伪逆   20 / 20                 -29.50
  6          Hebb   3 / 20                  -1265.67
  6          伪逆   14 / 20                 -28.81
  8          Hebb   2 / 20                  -1255.00
  8          伪逆   9 / 20                  -28.00
  10         Hebb   2 / 20                  -1371.20
  10         伪逆   8 / 20                  -28.00

  Hebb 这张表很难看:两张满分,加到三张直接掉到 0,之后一直在 2 到 4 之间徘徊。
  原因不是「容量算错」,而是这些样本太像了——七段数码管的数字共用笔画,
  0 和 8 差了 3 根笔画,任意两张之间都高度相关,外积平均就把它们搅在一起。
  换成伪逆规则(先让样本之间互相正交化,再算权重),同样的样本能扛到五张,
  六张掉到 14,十张还有 8——它把「样本相关」这个麻烦在处理权重时就解掉了。
  经典结论说容量上界约 0.138 乘单元数,这里 64 乘 0.138 约 8.8 张;
  那个上界的前提是样本互相独立,眼前这十张图明显不满足,所以实际差得远。

  还有一处口径要小心:两种规则的能量列不能横着比。
  Hebb 的权重尺度大,能量动辄上千;伪逆的权重是按样本解出来的,能量只有几十。
  能量是每种规则各自内部的「高低」,不是跨规则的分数,只能拿回想率比输赢。

[5] 它和后两节的关系
  没有隐层、没有输出层、没有损失函数,训练只是一次矩阵乘法,推理只是反复翻转
  这套「用能量定义好坏、让状态自己往低处滑」的框架,正是受限玻尔兹曼机的起点
  下一节要给它加两样东西:随机性(不再非黑即白地翻),和隐层(能表示没观测到的因素)

跑出来的数字值得逐条看。 回想结果是 1.0000:加噪时打坏了 8 个像素(占 12.5%),最后 64 个像素全部对上了。 能量从负 769 降到负 1521,中间每一步都在降,没有一次反弹。 翻转次数正好 8 次,和打坏的像素个数一样——每个坏像素都被翻回来了,一个没多。 能量逐次变化是负 769、负 849、负 933、负 1021、负 1113、负 1209、负 1309、负 1413、负 1521。 差值越来越大,说明越到后面翻的越是关键像素——一次修正带来的能量收益更多。 第三段的容量表是最有信息量的部分。 它同时否掉了两个常见说法:一是「Hebb 规则简单所以好用」, 二是「理论上界 0.138 乘 N 就是实际能存多少」。 样本一相关,两条都不成立。工程上记住这一条:理论上界的前提条件,往往比上界本身更重要。 还有一处口径要小心:两张表的能量列不能横着比。 Hebb 的权重尺度大,能量动辄上千;伪逆的权重是按样本解出来的,能量只有几十。 能量是每种规则各自内部的「高低」,不是跨规则的分数,只能拿回想率比输赢。

30 秒实验

  1. 把 main = [0, 3] 改成 [0, 3, 4],其余不动,再跑一遍。 回想准确率会从 1.0000 掉下去——这就是三张图开始互相干扰的直接证据。
  2. 把噪声从 8 个像素加到 16 个、24 个,看在哪一档开始回不来。 两张图的情况下它相当耐受,这是 Hopfield 网络真正的长处:纠错能力强。
  3. 把 np.fill_diagonal(W, 0.0) 这一行注释掉再跑。 对角线上的自连接会让每个单元不停地「自己确认自己」,状态容易卡在原地不动。

常见误区

  1. 「Hopfield 网络能存很多张图。」 不能。它的容量是 0.138 乘单元数,而且这个上界还要求样本互不相关。 64 个单元在理想情况下也就 8 张出头,样本一相似,实际连 3 张都撑不住。
  2. 「能量下降说明结果是对的。」 能量下降只说明「它停下来了」,不说明「它停在正确的地方」。 记混的时候它同样会稳稳停下,而且那个混合状态的能量可能比任何一张原图都低。 所以「能量更低」在 Hopfield 里不等于「更好」。
  3. 「它和玻尔兹曼机是一回事。」 差两样东西:随机性和隐层。 Hopfield 的更新是确定的(局部场为正就取正),玻尔兹曼机是按概率采样的; Hopfield 没有隐层,玻尔兹曼机有。 这两样一加,网络就从「记住样本」变成「表示分布」。
自测三问(点开对答案) 1. **为什么权重必须对称?不对称会怎样?** —— **因为能量函数要靠对称性才成立:E 等于负半个 s 转置乘 W 乘 s,** **只有 W 对称时它才是同一个数,翻转单元时的能量变化才能写成「负 2 乘局部场乘旧值」。** **不对称就没有这个标量函数,「一定停下来」的保证也就没了。** 2. **64 个单元、去掉对角线,权重有多少个?** —— **64 乘 64 等于 4096,减去 64 个对角线位置,等于 4032 个。** **又因为对称,实际上只有 2016 个独立参数——W 的第 i 行第 j 列等于第 j 行第 i 列。** 3. **存 3 张就崩了,是代码写错了吗?** —— **不是。** **换伪逆规则在同样样本上能到 20 / 20,说明代码没错,是 Hebb 规则在相关样本上串扰严重。** **七段数码管的数字共用笔画,两两高度相关,外积平均把特征搅在一起了。**

Hopfield 网络的更新是「非黑即白」的:局部场为正就一定取正。 这使它只能找到低谷,却没法描述「低谷有多深、周围有多宽」。 下一节给它加上随机性——按概率而不是按符号更新,于是每个状态都有了出现的可能, 网络开始描述一个分布,而不只是记住几个点。

相关笔记