2.12 能量模型与联想记忆

2.12.0 能量模型(家族公共部分)

<center>2.12.0 能量模型
2.12.0 能量模型
<center>2.12.0 能量模型
2.12.0 能量模型

一句话

给每个状态定义一个「能量」,能量越低越可能出现;学习就是把训练数据所在位置的能量压低、把其他位置的能量抬高。

这就是统计物理里的玻尔兹曼分布。T 是温度:T 高时各状态概率接近,T 低时概率集中到能量最低的状态。Z 叫配分函数,要对所有可能的状态求和(连续变量时是积分,积分有限才是合法的概率密度)。状态数随维度指数增长,大规模的一般模型很难精确算出 Z;小模型可以直接枚举,RBM 等可以用退火重要性采样(AIS)估计,但有额外的计算成本和估计误差。这个家族的难点和各种技巧几乎都围绕 Z 展开。

学习规则的共同形状:取 T = 1(温度可以并进能量里;不为 1 时下式整体再乘 1/T),对数似然对参数的梯度是两项之差:

第一项(正相)压低训练数据的能量,第二项(负相)抬高模型自己生成的样本的能量。负相要算模型分布下的期望。高维时只能近似,最常用的是 MCMC(Gibbs 采样、Langevin 动力学),也可以用重要性采样等方法;这个期望算起来很贵,是这类模型训练慢的主要原因。

在现代模型里的延续:现代 Hopfield 网络的更新等价于注意力(2.12.1);连续、可微时,能量的负梯度除以温度就是 score,∇ₓ log P(x) = −∇ₓE(x)/T:Z 和 x 无关,求梯度时消掉了,score 方法正是这样绕开配分函数的。用去噪的方式学 score 再一步步采样,就是扩散模型(2.2.1)。2024 年诺贝尔物理学奖授予 Hopfield 和 Hinton,表彰的正是 Hopfield 网络和玻尔兹曼机这一支工作。

1
2
3
4
5
6
7
8
9
10
11
import itertools
import torch

# 3 个 ±1 单元、所有 8 种状态的能量与玻尔兹曼分布
W = torch.tensor([[0., 1., -1.], [1., 0., 0.5], [-1., 0.5, 0.]])
states = torch.tensor(list(itertools.product([-1., 1.], repeat=3)))
E = -0.5 * ((states @ W) * states).sum(1) # E(s) = -½ sᵀ W s
for T in [5.0, 1.0, 0.2]:
p = torch.softmax(-E / T, 0) # P(s) = exp(-E/T) / Z
best = states[p.argmax()].int().tolist()
print(f"温度 {T}:最可能的状态 {best},概率 {p.max():.2f},分布的熵 {-(p * p.log()).sum():.2f}")

2.12.1 Hopfield 网络 〔图:HN〕

<center>2.12.1 Hopfield
2.12.1 Hopfield
<center>2.12.1 Hopfield
2.12.1 Hopfield

一句话

全连接、权重对称的二值神经元网络;用 Hebb 规则把若干模式「写进」权重,之后给一个残缺或带噪的模式,如果它落在某个存储模式的吸引域里,网络会自己演化回这个模式:按内容寻址的记忆。也可能停在伪记忆上,不保证到达距离最近的存储模式。

图中 HN 是所有单元两两相连的圆,每个单元画成「回馈输入单元」:同一组单元既是输入也是输出。Hopfield 1982 年提出。

能量与更新:状态 s_i ∈ {−1, +1},权重对称且 w_ii = 0:

局部场 Σ_j w_ij s_j 恰好为 0 时要定一个规则:保持原状态,或者固定取 +1(下面代码的做法),都不会让能量上升。一次更新一个神经元(异步更新)时,能量只降不升,网络一定会停在某个能量的局部极小点。

Hebb 学习(一次算完,不需要迭代训练):

Hebb 权重的用意是让每个存储的模式 ξ 成为能量地形上的一个谷底(稳定点)。能不能做到,取决于模式的数量和它们之间的相关性:模式之间的串扰太大时,有些写进去的模式本身就不再稳定。

容量与伪记忆:经典的理论结果是 N 个神经元大约能存 0.14N 个随机模式(允许回忆结果有少量错位);要求一位不差地回忆时,容量更低,超过之后模式互相干扰、回忆迅速失败(下面代码里能看到这个断崖)。网络还会出现「伪记忆」,比如几个模式的混合、模式的反相。

双向联想记忆(BAM,Kosko 1988):两层神经元 X、Y,存成对的模式 (x, y),W = Σ x yᵀ。给 x 能回忆出 y(y = sign(Wᵀx)),给 y 也能回忆出 x(x = sign(W y)),来回迭代直到稳定。局部场正好为 0 时 sign 要约定一个值,下面代码和 Hopfield 一样取 +1。

现代 Hopfield 网络(Ramsauer 2020,「Hopfield Networks is All You Need」):状态变成连续值,能量函数换成 log-sum-exp 形式,在论文给的条件下(模式彼此分得足够开、β 足够大),存储容量随维度指数增长;查询离某个存储模式足够近时,一步更新就能以很小的误差取回它(模式彼此相近时,更新会收敛到几个模式的平均附近,即亚稳态);这一步更新的公式和 Transformer 的注意力完全相同:

X 的列是存储的模式(Key 和 Value),ξ 是查询(Query)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import math
import torch

torch.manual_seed(0)
N, P = 100, 5
patterns = torch.randint(0, 2, (P, N)).float() * 2 - 1 # 5 个 ±1 模式(可以想成 10×10 的黑白图)

W = patterns.T @ patterns / N # Hebb 学习:一次写入
W.fill_diagonal_(0)
energy = lambda s: (-0.5 * s @ W @ s).item()

def recall(s, sweeps=5):
s = s.clone()
for _ in range(sweeps):
for i in torch.randperm(N): # 异步更新:一次改一个神经元
s[i] = 1.0 if W[i] @ s >= 0 else -1.0
return s

probe = patterns[0].clone()
probe[torch.randperm(N)[:25]] *= -1 # 翻转 25% 的像素
out = recall(probe)
print(f"经典 Hopfield:与原模式一致 {(out == patterns[0]).float().mean():.2f},能量 {energy(probe):.1f} → {energy(out):.1f}")

# 容量:存的模式越多,完整回忆成功的比例越低(每种情况试 40 次,每次翻转 10% 的像素)
for P_ in [5, 10, 14, 20, 30]:
ok = 0
for trial in range(40):
pats = torch.randint(0, 2, (P_, N)).float() * 2 - 1
W = pats.T @ pats / N; W.fill_diagonal_(0)
noisy = pats[0].clone(); noisy[torch.randperm(N)[:10]] *= -1
ok += torch.equal(recall(noisy), pats[0])
print(f" 存 {P_:2d} 个模式({P_ / N:.2f}N):完整回忆成功 {ok / 40:.2f}")
print(f" 对照理论:要一位不差地回忆出某个模式,容量约 N/(2 ln N) = {N / (2 * math.log(N)):.1f} 个(渐近公式,只作量级参考)")

# 现代 Hopfield:一步更新 = 注意力
X = patterns.T # (N, P):存储的模式
xi_new = X @ torch.softmax(0.5 * X.T @ probe, dim=0) # softmax(β Xᵀ ξ) 加权取回
print(f"现代 Hopfield 一步取回:与原模式一致 {(torch.sign(xi_new) == patterns[0]).float().mean():.2f}")

# 双向联想记忆 BAM:存 3 对 (x, y),给带噪的 x 回忆 y
xs = torch.randint(0, 2, (3, 40)).float() * 2 - 1
ys = torch.randint(0, 2, (3, 20)).float() * 2 - 1
Wb = xs.T @ ys # (40, 20)
x = xs[1].clone(); x[torch.randperm(40)[:6]] *= -1
for _ in range(5): # 局部场为 0 时取 +1,和上面的 Hopfield 一样(torch.sign(0) 会得到 0,不在 ±1 里)
y = torch.where(Wb.T @ x >= 0, 1.0, -1.0) # X → Y
x = torch.where(Wb @ y >= 0, 1.0, -1.0) # Y → X
print("BAM 回忆出的 y 正确:", torch.equal(y, ys[1]))
<center>Hopfield联想记忆
Hopfield联想记忆
<center>Hopfield联想记忆
Hopfield联想记忆

这张图用字母图案单独画(正文代码用的是随机模式)。上排只存 T、X、O 三个图案,翻转 30% 像素的 T 一轮更新后就回到了 T。下排多存一个和 T、O 很像的 L,同样的输入落进了一个伪记忆:几个存储图案混在一起的状态。

常见说法与实测

常见说法:N 个神经元的 Hopfield 网络大约能存 0.138N 个模式。

实测:N = 100、每次翻转 10% 的像素再回忆:存 5 个(0.05N)全部完整回忆成功,10 个(0.10N)95%,14 个(0.14N)只有 65%,20 个 33%,30 个 0。存的若是彼此相似的图案,即使只存几个也会出错:另外试了 20 次,存 T、L、X、O 四个字母、翻转 10% 像素时,L 只有 75% 能完整回到 L;去掉 L、只存 T、X、O,翻转 30% 也是 20 次全部成功。

怎么理解:0.138N 是随机模式、N 很大时的理论结果(Amit 等 1985),允许回忆结果有约 1.6% 的位出错。要求一位不差地回忆,McEliece 等 1987 给的是渐近结果:多数模式能精确回忆时容量约 N/(2 ln N),要求每一个模式都精确回忆时约 N/(4 ln N)。N = 100 代进去约 11 个,只是量级参考;有限 N 下的结果还和「成功」怎么定义、翻转多少像素有关,实测的趋势和它大致吻合。两个结论都假设各模式彼此随机、不相关;相似的图案之间串扰大得多,伪记忆更容易出现。

典型例子

  • 联想记忆与模式补全:从残缺、带噪的图像恢复存储的原图
  • 组合优化:Hopfield 与 Tank(1985)把旅行商问题写成能量函数,用网络的演化求近似解
  • 现代 Hopfield 层:用于免疫组库分类(DeepRC)等需要在大量元素中检索的任务;理解注意力的一个视角
  • 优势:按内容寻址,给一部分线索就能恢复完整记忆;Hebb 规则一步写入,不需要反向传播;对称权重 + 异步更新保证收敛
  • 局限:经典版容量只有约 0.14N;会收敛到伪记忆;主要用来存储、检索和补全模式,学不到能迁移到其他任务的表征
  • 适合的数据:二值或 ±1 模式(黑白图、二进制编码)的存储与补全;能写成二次能量函数的组合优化;现代版适合在集合或序列里做检索

2.12.2 马尔可夫链 〔图:MC〕

<center>2.12.2 马尔可夫链
2.12.2 马尔可夫链
<center>2.12.2 马尔可夫链
2.12.2 马尔可夫链

一句话

一组状态和它们之间的转移概率;下一个状态只取决于当前状态,与更早的历史无关。

图里把 MC 画成所有「概率隐藏单元」两两相连的圆,作者也说明它严格说不算神经网络,放进来是因为玻尔兹曼机、Hopfield 网络和它长得像,而且采样这些模型要用到马尔可夫链(MCMC)。

平稳分布:满足 π = πP 的分布。有限状态、不可约、非周期的链(可数无限状态时还要求正常返),从任何初始分布出发,长时间之后都会收敛到唯一的平稳分布。π 是 Pᵀ 特征值为 1 的特征向量。

和其他模型的关系:隐马尔可夫模型(HMM)在马尔可夫链上加了「状态不可见、只能看到观测」;马尔可夫决策过程(MDP)加了动作和奖励,是强化学习的基础;MCMC 构造一条平稳分布恰好是目标分布的马尔可夫链来采样;n-gram 语言模型是「下一个词只看前 n−1 个词」的马尔可夫链,大语言模型可以看成把这个条件推到几十万个 token、并用神经网络表示转移概率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import torch

# 天气:0 = 晴,1 = 多云,2 = 雨;P[i, j] = 今天是 i、明天是 j 的概率
P = torch.tensor([[0.7, 0.2, 0.1],
[0.3, 0.4, 0.3],
[0.2, 0.3, 0.5]])
torch.manual_seed(0)
s, path = 0, []
for _ in range(20):
s = torch.multinomial(P[s], 1).item() # 下一个状态只看当前状态
path.append("晴云雨"[s])
print("模拟 20 天:", "".join(path))

evals, evecs = torch.linalg.eig(P.T) # 平稳分布:Pᵀ 的特征值 1 对应的特征向量
pi = evecs[:, evals.real.argmax()].real
print("平稳分布(长期晴/云/雨比例):", (pi / pi.sum()).numpy().round(3))
print("从「晴」出发走 50 步后的分布:", (torch.tensor([1., 0., 0.]) @ torch.linalg.matrix_power(P, 50)).numpy().round(3))

# 字符级二元语法:最朴素的语言模型,下一个字符只看前一个字符
text = "the cat sat on the mat. the dog sat on the log. the cat saw the dog. "
chars = sorted(set(text)); idx = {c: i for i, c in enumerate(chars)}
counts = torch.ones(len(chars), len(chars)) # 加一平滑
for a, b in zip(text, text[1:]):
counts[idx[a], idx[b]] += 1
T = counts / counts.sum(1, keepdim=True)
c, out = "t", "t"
for _ in range(50):
c = chars[torch.multinomial(T[idx[c]], 1).item()]
out += c
print("二元语法生成:", out)

典型例子

  • PageRank:网页排名就是「随机冲浪者」马尔可夫链的平稳分布
  • n-gram 语言模型、输入法联想
  • 语音识别:深度学习之前的主流是 HMM + 高斯混合模型
  • MCMC:贝叶斯推断、玻尔兹曼机的采样
  • 强化学习的 MDP、排队论、金融里的信用评级转移
  • 优势:极简、可解释;平稳分布、首达时间等性质可以解析计算;采样便宜
  • 局限:只记得当前状态,长程依赖要把历史编进状态,状态数指数增长;没有学到的特征表示
  • 适合的数据:离散状态序列:天气、用户行为序列、文本、网页链接图;作为 HMM、MDP 等更大模型的骨架

2.12.3 RBM 受限玻尔兹曼机 〔图:RBM〕

<center>2.12.3 RBM
2.12.3 RBM
<center>2.12.3 RBM
2.12.3 RBM

一句话

只有「可见层—隐藏层」之间有连接、层内没有连接的玻尔兹曼机;这一限制让每层可以整层并行采样,再配上对比散度(CD)训练,就从不可用变得可用。

图中 RBM 左边一列是可见单元(回馈输入单元),右边一列是隐藏单元(概率隐藏单元),只有两列之间有连线。Smolensky 1986 年以 Harmonium 之名提出,Hinton 2002 年提出 CD 训练后流行起来。

条件独立:给定可见层,隐藏单元之间相互独立(反过来也一样):

所以可以「整层采样隐藏层 → 整层采样可见层」交替进行(块 Gibbs 采样)。

对比散度 CD-k:负相本该从模型分布里采样(要 Gibbs 链跑到平衡),CD 改成从训练样本出发只跑 k 步(常用 k = 1):

这是一个有偏的近似,但实践中效果足够好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import torch

def bars_and_stripes(n):
"""4×4 二值图:要么若干整行亮(横条),要么若干整列亮(竖条)"""
bits = torch.randint(0, 2, (n, 4)).float()
rows = bits[:, :, None].expand(n, 4, 4)
vertical = torch.rand(n) < 0.5
return torch.where(vertical[:, None, None], rows.transpose(1, 2), rows).reshape(n, 16)

class RBM:
def __init__(self, n_vis, n_hid):
self.W = 0.01 * torch.randn(n_vis, n_hid)
self.a, self.b = torch.zeros(n_vis), torch.zeros(n_hid) # 可见层、隐藏层偏置

def p_h(self, v): return torch.sigmoid(v @ self.W + self.b) # 层内无连接 → 整层并行
def p_v(self, h): return torch.sigmoid(h @ self.W.T + self.a)

def cd_step(self, v0, lr=0.05, k=1):
ph0 = self.p_h(v0)
h = torch.bernoulli(ph0)
for _ in range(k): # 从数据出发,交替 Gibbs 采样 k 步
v = torch.bernoulli(self.p_v(h))
ph = self.p_h(v)
h = torch.bernoulli(ph)
n = v0.size(0)
self.W += lr * (v0.T @ ph0 - v.T @ ph) / n # 正相 - 负相
self.a += lr * (v0 - v).mean(0)
self.b += lr * (ph0 - ph).mean(0)

torch.manual_seed(0)
rbm = RBM(16, 64)
for step in range(10000):
rbm.cd_step(bars_and_stripes(64))

# 补全:每张图随机遮住 4 个像素,用 Gibbs 采样把它们补出来
v_true = bars_and_stripes(500)
mask = torch.zeros_like(v_true)
mask[torch.arange(500)[:, None], torch.rand(500, 16).argsort(1)[:, :4]] = 1
v = torch.where(mask.bool(), torch.bernoulli(torch.full_like(v_true, 0.5)), v_true)
for _ in range(50):
v_new = torch.bernoulli(rbm.p_v(torch.bernoulli(rbm.p_h(v))))
v = torch.where(mask.bool(), v_new, v_true) # 只更新被遮住的像素
print(f"被遮住像素的补全准确率 {(v[mask.bool()] == v_true[mask.bool()]).float().mean():.3f}(瞎猜 0.5)")

# 从模型分布采样:随机起点跑 1000 步 Gibbs,最后一步取 P(v|h) > 0.5 显示
v = torch.bernoulli(torch.full((500, 16), 0.5))
for _ in range(1000):
v = torch.bernoulli(rbm.p_v(torch.bernoulli(rbm.p_h(v))))
v = (rbm.p_v(torch.bernoulli(rbm.p_h(v))) > 0.5).float()
g = v.view(-1, 4, 4)
legal = ((g == g[:, :, :1]).all(2).all(1) | (g == g[:, :1, :]).all(1).all(1)).float().mean()
print(f"500 个样本(最后一步按概率 > 0.5 取整后)里,合法的横条/竖条图案占 {legal:.2f};前 6 个样本:")
for r in range(4):
print(" ".join("".join("█" if p else "·" for p in img.view(4, 4)[r].int().tolist()) for img in v[:6]))

典型例子

  • Netflix 大奖赛(2007–2009):Salakhutdinov、Mnih、Hinton 用 RBM 做协同过滤,是获胜方案融合的模型之一
  • 深度信念网络的逐层预训练(2.12.5)
  • 量子多体物理:Carleo 和 Troyer(2017)用 RBM 表示量子态的波函数
  • 早期的特征学习:MNIST 上学出笔画状的特征
  • 优势:二分结构让条件分布可以因式分解,整层并行采样;CD 训练比一般玻尔兹曼机快几个数量级;无监督地学特征;可以堆叠成深层模型
  • 局限:似然难以精确计算(Z 要靠 AIS 等方法估计),训练进度难监控;CD 有偏;大部分任务上已被 VAE、扩散模型、自监督方法取代
  • 适合的数据:二值或可二值化的数据:用户-物品评分矩阵、黑白图像、自旋构型

2.12.4 玻尔兹曼机 〔图:BM〕

<center>2.12.4 玻尔兹曼机
2.12.4 玻尔兹曼机
<center>2.12.4 玻尔兹曼机
2.12.4 玻尔兹曼机

一句话

随机版的 Hopfield 网络:单元以概率开关,网络整体服从玻尔兹曼分布;加入隐藏单元后可以学数据的高阶相关,但训练要靠漫长的 MCMC,几乎不可用。

图中 BM 是全连接的圆,一部分是可见单元(回馈输入单元),一部分是隐藏单元(概率隐藏单元)。Hinton 和 Sejnowski 1983–1985 年提出。

(这里 s 取 ±1;取 0/1 时条件概率里没有因子 2。)

学习规则:

两项都是「两个单元同时激活的程度」的平均:s 取 0/1 时就是两者同时为 1 的概率,取 ±1 时是两者同号的程度。一个在数据上统计,一个在模型自由运行时统计。规则本身很简洁、只用局部信息,难处在第二项:模型统计量要用 Gibbs 采样跑到平衡才能估准,而且有隐藏单元时,第一项也要先把隐藏单元采样到平衡。网络稍大就慢得不可接受。RBM(2.12.3)去掉了层内连接,给定一层时另一层可以整层并行采样,正相也能精确算出来,训练才变得可行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import itertools
import torch

torch.manual_seed(0)
n = 6
# 训练数据:6 个 ±1 单元,(0,1)、(2,3)、(4,5) 两两强相关
base = torch.randint(0, 2, (2000, 3)).float().repeat_interleave(2, dim=1)
flip = torch.rand_like(base) < 0.05
data = torch.where(flip, 1 - base, base) * 2 - 1

W, b = torch.zeros(n, n), torch.zeros(n)
all_states = torch.tensor(list(itertools.product([-1., 1.], repeat=n))) # 2⁶ = 64 个状态

def model_stats(W, b):
# 状态少才能直接枚举;真实的玻尔兹曼机算不出 Z,通常靠 Gibbs 采样近似,这就是它慢的原因
E = -0.5 * ((all_states @ W) * all_states).sum(1) - all_states @ b
p = torch.softmax(-E, dim=0) # P(s) = exp(-E) / Z
return (all_states.T * p) @ all_states, p @ all_states

data_corr, data_mean = data.T @ data / len(data), data.mean(0)
for step in range(500):
model_corr, model_mean = model_stats(W, b)
W += 0.1 * (data_corr - model_corr) # 正相 - 负相
W.fill_diagonal_(0)
b += 0.1 * (data_mean - model_mean)
print("学到的权重(成对的单元之间是大的正数):\n", W.numpy().round(1))

s = torch.ones(n)
samples = []
for t in range(20000): # Gibbs 采样:每次按条件概率重采一个单元
i = t % n
s[i] = 1.0 if torch.rand(()) < torch.sigmoid(2 * (W[i] @ s + b[i])) else -1.0
samples.append(s.clone())
S = torch.stack(samples[2000:])
print(f"采样里 s0·s1 的平均 {(S[:, 0] * S[:, 1]).mean():.3f},数据里 {data_corr[0, 1]:.3f}")
print(f"采样里 s0·s2 的平均 {(S[:, 0] * S[:, 2]).mean():.3f},数据里 {data_corr[0, 2]:.3f}")

典型例子

  • 历史意义远大于实际应用:第一批能学习隐藏表示的神经网络之一,是 RBM、DBN 和整个能量模型路线的源头
  • 约束满足与组合优化:配合模拟退火求解
  • 量子退火机(D-Wave)曾被用来对玻尔兹曼机做采样
  • 优势:有完整的概率解释,显式定义了联合分布;隐藏单元能学数据的高阶相关;学习规则局部、简洁
  • 局限:配分函数难以精确计算,训练要长时间 MCMC,规模稍大就不可用;实际用途已被 RBM 及之后的模型取代
  • 适合的数据:小规模二值数据的分布建模、组合优化;今天主要用来理解能量模型的思想

2.12.5 DBN 深度信念网络 〔图:DBN〕

<center>2.12.5 DBN
2.12.5 DBN
<center>2.12.5 DBN
2.12.5 DBN

一句话

把多个 RBM 一层层叠起来,逐层无监督预训练,再把权重拷进一个前馈网络用标签微调;2006 年这个办法让深层全连接网络重新训得动,是深度学习复兴的代表性工作。

图中 DBN 左边是输入(回馈输入单元),中间是交替的概率隐藏层和隐藏层,右边是输入输出匹配单元。Hinton、Osindero、Teh 2006 年提出。

作为生成模型的结构:严格的 DBN 是一个混合模型,最顶上两层之间是无向连接,构成一个 RBM;下面各层之间是自顶向下的有向连接(sigmoid 信念网络)。生成样本时先在顶层 RBM 里做 Gibbs 采样,再一层层往下采到可见层。它和所有层之间都是无向连接的深度玻尔兹曼机(DBM)不同。下面第 3 步拷进去的前馈网络只是借用了 DBN 学到的权重,本身不是 DBN。

训练流程

  1. 用数据训练第一个 RBM
  2. 把第一个 RBM 隐藏层的激活概率当成「数据」,训练第二个 RBM;以此类推,逐层贪心地往上叠
  3. 把各层 RBM 的权重拷进一个同结构的前馈网络,顶上加分类头,用带标签的数据整体微调

历史位置:2006 年这篇论文证明了「深层网络是可以训练的」,带起了深度学习这个名字和后来的复兴。2010–2012 年 ReLU、更好的初始化、Dropout、GPU 和大数据出现后,直接端到端训练的效果更好,逐层预训练就不再必要。但「先在大量无标签数据上预训练,再在少量有标签数据上微调」这个范式,后来在 BERT、GPT、MAE 上以新的形式回归。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import torch
import torch.nn as nn

def bars_and_stripes(n):
"""4×4 二值图;标签:0 = 横条,1 = 竖条"""
bits = torch.randint(0, 2, (n, 4)).float()
rows = bits[:, :, None].expand(n, 4, 4)
vertical = torch.rand(n) < 0.5
imgs = torch.where(vertical[:, None, None], rows.transpose(1, 2), rows).reshape(n, 16)
return imgs, vertical.long()

class RBM: # 与 2.12.3 相同的极简 RBM
def __init__(self, n_vis, n_hid):
self.W, self.a, self.b = 0.01 * torch.randn(n_vis, n_hid), torch.zeros(n_vis), torch.zeros(n_hid)
def p_h(self, v): return torch.sigmoid(v @ self.W + self.b)
def p_v(self, h): return torch.sigmoid(h @ self.W.T + self.a)
def cd_step(self, v0, lr=0.05):
ph0 = self.p_h(v0); h = torch.bernoulli(ph0)
v = torch.bernoulli(self.p_v(h)); ph = self.p_h(v)
self.W += lr * (v0.T @ ph0 - v.T @ ph) / len(v0)
self.a += lr * (v0 - v).mean(0); self.b += lr * (ph0 - ph).mean(0)

torch.manual_seed(0)
sizes = [16, 64, 32]
unlabeled, _ = bars_and_stripes(5000)
# 1) 逐层无监督预训练:上一层 RBM 的隐藏激活当下一层的数据
rbms, x = [], unlabeled
for n_vis, n_hid in zip(sizes[:-1], sizes[1:]):
rbm = RBM(n_vis, n_hid)
for epoch in range(30):
for batch in x.split(64):
rbm.cd_step(batch)
rbms.append(rbm)
x = rbm.p_h(x)

# 2) 把 RBM 权重拷进前馈网络,加分类头,只用 20 个带标签样本微调
def build(pretrained):
torch.manual_seed(1)
layers = []
for i, (n_in, n_out) in enumerate(zip(sizes[:-1], sizes[1:])):
lin = nn.Linear(n_in, n_out)
if pretrained:
lin.weight.data, lin.bias.data = rbms[i].W.T.clone(), rbms[i].b.clone()
layers += [lin, nn.Sigmoid()]
return nn.Sequential(*layers, nn.Linear(sizes[-1], 2))

accs = {False: [], True: []}
for rep in range(5): # 20 个标签的结果波动很大,换 5 组标注样本取平均
torch.manual_seed(100 + rep)
x_lab, y_lab = bars_and_stripes(20)
x_te, y_te = bars_and_stripes(2000)
for pretrained in [False, True]:
model = build(pretrained)
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
for _ in range(300):
loss = nn.functional.cross_entropy(model(x_lab), y_lab)
opt.zero_grad(); loss.backward(); opt.step()
accs[pretrained].append((model(x_te).argmax(1) == y_te).float().mean().item())
for pretrained, name in [(False, "随机初始化直接训练"), (True, "DBN 预训练 + 微调")]:
print(f"{name}:5 组的平均测试准确率 {sum(accs[pretrained]) / 5:.3f},各组 {[round(a, 2) for a in accs[pretrained]]}")
# 4 个 bit 全 0 或全 1 时(占 2/16 = 12.5%)图是全暗或全亮,横竖分不出,这部分平均有一半判错,准确率上限是 0.9375

常见说法与实测

常见说法:先用大量无标签数据逐层预训练,再用少量标签微调,比直接从随机初始化训练好。

实测:最初一版设置(隐藏层 32、16,微调学习率 1e-2,只抽一组 20 个标签)得到相反的结论:随机初始化 0.86,预训练 0.71。同样的设置换 5 组标签取平均,变成 0.68 对 0.71。按现在的设置(隐藏层 64、32,学习率 1e-3),5 组里预训练全部更好,平均 0.61 → 0.72。

怎么理解:只有 20 个标签时,抽到哪 20 个样本对结果影响很大,单次比较可能得出相反的结论。预训练的收益在平均意义上存在,但幅度不大。2010 年以后有了更好的初始化和归一化,这点收益已经不值得两阶段训练的麻烦。

典型例子

  • 2006 年:不用任何领域知识,DBN 在 MNIST 上达到当时很好的错误率
  • 2009–2012 年语音识别:Mohamed、Dahl、Hinton 等用 DBN 预训练的深层网络做声学模型,大幅降低了错误率,微软、谷歌随后在产品中采用,这是深度学习复兴后第一个大规模工业应用(更早的卷积网络在 1990 年代已经用于支票识别)
  • 今天主要作为「无监督预训练 → 有监督微调」范式的历史原型
  • 优势:历史上解决了「深层网络训不动」;能利用大量无标签数据;本身也是生成模型
  • 局限:流程繁琐(两阶段、超参多);有了 ReLU、BatchNorm、残差之后,直接端到端训练更好,逐层预训练不再必要
  • 适合的数据:无标签多、标签少的二值或 [0, 1] 数据;今天主要用于理解预训练思想的来历