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 秒实验
- 把
main = [0, 3]改成[0, 3, 4],其余不动,再跑一遍。 回想准确率会从 1.0000 掉下去——这就是三张图开始互相干扰的直接证据。 - 把噪声从 8 个像素加到 16 个、24 个,看在哪一档开始回不来。 两张图的情况下它相当耐受,这是 Hopfield 网络真正的长处:纠错能力强。
- 把
np.fill_diagonal(W, 0.0)这一行注释掉再跑。 对角线上的自连接会让每个单元不停地「自己确认自己」,状态容易卡在原地不动。
常见误区
- 「Hopfield 网络能存很多张图。」 不能。它的容量是 0.138 乘单元数,而且这个上界还要求样本互不相关。 64 个单元在理想情况下也就 8 张出头,样本一相似,实际连 3 张都撑不住。
- 「能量下降说明结果是对的。」 能量下降只说明「它停下来了」,不说明「它停在正确的地方」。 记混的时候它同样会稳稳停下,而且那个混合状态的能量可能比任何一张原图都低。 所以「能量更低」在 Hopfield 里不等于「更好」。
- 「它和玻尔兹曼机是一回事。」 差两样东西:随机性和隐层。 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 网络的更新是「非黑即白」的:局部场为正就一定取正。 这使它只能找到低谷,却没法描述「低谷有多深、周围有多宽」。 下一节给它加上随机性——按概率而不是按符号更新,于是每个状态都有了出现的可能, 网络开始描述一个分布,而不只是记住几个点。
- ☐ (本节完,下一节 4.2 玻尔兹曼机)