如果你在概率模型里见过“一种带指数的归一化式子”反复出现,那大概率是遇到基于能量的模型(Energy Based Model, EBM)了。这类模型的核心长这样:(p(x) \propto \exp(-E(x))),能量越低的地方概率越高,能量越高的地方概率越低。第一次接触的人通常会有两个反应:它为什么不写成标准的概率密度?那个归一化常数去哪了?这两个问题背后,是整整一门从统计力学里长出来的概率建模思想。
这篇是“概率模型的统计力学理论”系列的第一篇,聚焦EBM最核心的部分:玻尔兹曼分布和配分函数如何进入机器学习、能量函数怎么设计、极大似然训练为什么需要采样、对比散度CD到底在优化什么,还有现代扩散模型和EBM之间的联系。适合正在学概率图模型或生成模型的读者,也适合知道RBM但一直没搞懂它为什么难训练的从业者。我会把从物理到算法的完整链条拆开讲,尽量不堆公式,但关键推导会保留。
1. 从统计力学到机器学习的桥
1.1 玻尔兹曼分布:能量低的系统更常见
统计力学里最著名的一个结果就是玻尔兹曼分布:当一个系统处于热平衡时,处在某个能量状态 (E) 的概率正比于 (\exp(-E/k_B T)),其中 (k_B) 是玻尔兹曼常数,(T) 是绝对温度。机器学习里通常不写温度,直接把 (k_B T) 吸收进能量函数,于是就有了 (p(x) \propto \exp(-E(x))) 这个形式。
这个式子的物理直觉非常好理解。想象一个山丘上不断被热扰动推动的小球,它处于低势能位置的次数一定比高势能位置多。能量函数在这里扮演的就是“势能”角色,它给每个状态打分,得分越低的状态越容易被系统访问。温度则控制着这种倾向的强弱:温度超高的时候,系统几乎不在乎能量差异,所有状态都差不多;温度趋近零的时候,系统只会停留在全局能量最低的地方。这个“低能量高概率”的原则,就是EBM全部理论的起点。
从物理搬到机器学习,我们只需要做一步替换:把“系统的微观状态”换成“模型要建模的数据点”。比如图片数据,每一张图片就是一个状态,能量函数负责判断这张图片在模型眼里“自然不自然”。自然的数据能量低,不自然的数据能量高。这样,图片分布就被一个能量函数间接定义出来了。
1.2 温度与配分函数:EBM的两个灵魂参数
公式 (p(x) \propto \exp(-E(x))) 里的“正比于”三个字,意味着还有一个比例常数没写出来。要把这个式子变成真正的概率分布,需要在分母上除以所有可能状态的指数能量之和:
[ p(x) = \frac{\exp(-E(x))}{Z}, \quad Z = \sum_x \exp(-E(x)) ]
这个 (Z) 就是配分函数。对连续数据,求和要换成积分。统计力学里所有的热力学量都从配分函数导出,机器学习里EBM所有的训练困难也都从配分函数开始。
关键是,这个 (Z) 几乎从来算不出来。对于一张 (32\times32) 的三通道图片,可能的像素取值组合是一个天文数字,你不可能把所有状态的指数加一遍。高维连续空间的积分更没希望。所以EBM和普通概率模型最大的区别在于:它只定义了一个“未归一化的概率形式”,归一化常数永远留在那。正因如此,EBM又被叫做无归一化模型(unnormalized model)。这种模型虽然形式灵活,但带来的代价就是训练和评估都要绕着 (Z) 走。可以说,理解了配分函数,就理解了EBM的一半。
注意:EBM里说的“能量”并不是物理上的动能加势能,它只是在数学形式上借用了统计力学的语言。你完全可以叫它“代价函数”或“打分函数”,思想是一样的。
2. 能量函数的设计:不是随便一个函数都能当“能量”
2.1 一个最简单的例子:从能量到高斯分布
先用一维例子建立直觉。设 (E(x) = (x-\mu)^2 / (2\sigma^2)),那么 (p(x) \propto \exp\left(-(x-\mu)^2 / (2\sigma^2)\right))。调整配分函数之后,这恰好就是均值 (\mu)、方差 (\sigma^2) 的高斯分布。这个例子说明一个关键点:能量函数本身决定了分布的形状,而配分函数只是确保所有概率加起来等于1。
高斯例子里的配分函数可以手算出来,(Z = \sigma\sqrt{2\pi})。但一旦把 (x) 换成高维向量,把 (E(x)) 换成神经网络,配分函数立刻就失控了。这也解释了为什么早期机器学习社区很少用EBM:能量好定义,归一化搞不定。直到后来MCMC采样方法成熟,以及对比散度这类近似训练方法出现,EBM才真正落地。
从设计的角度看,能量函数几乎可以是任意函数,只要它能输出一个实数。但问题在于,这个函数会直接控制最终分布的性质。如果能量函数关于某些输入根本不分(比如恒为常数),那最终的分布就是均匀分布,什么也没学到。所以能量函数必须足够有表达力,去刻画数据中真正重要的结构。
2.2 能量函数的常见套路与坑
实际建模里,能量函数通常分解成几部分。最典型的模式是“单体项+交互项”,比如限制玻尔兹曼机里的能量:
[ E(v,h) = -\sum_i a_i v_i - \sum_j b_j h_j - \sum_{i,j} v_i W_{ij} h_j ]
其中 (v) 是可见变量(你的数据),(h) 是隐藏变量(模型内部的潜变量)。第一项只依赖单个可见单元,第二项只依赖单个隐藏单元,第三项是可见单元和隐藏单元之间的成对交互。这个分解的意义在于:当给定其中一层时,另一层的条件分布会变得非常简单,可以逐节点独立采样。这对后面的训练算法至关重要。
设计能量函数时有一个很实际的大坑:能量函数不能出现无界的低能区域。如果你设计的能量函数在某个方向上趋向负无穷,那模型会在那个方向上疯狂积累概率质量,配分函数直接发散,训练时会看到loss变成NaN。比如能量写成 (E(x) = (x^2-1)^2 - cx^2) 这种四次项和二次项“拔河”的形式,(c) 稍大一点就可能创造两个无界下行的方向。解决方法是让能量函数的主体部分在无穷远处保持正的增长趋势,例如二次项或带约束的参数化。
实操心得:我最早写一个自定义EBM时,能量函数用的是纯MLP输出,没有对无穷远处的行为做任何约束。结果训练到第12个epoch,loss直接变成NaN,排查了很久才发现是能量函数在某个方向上无界导致的。后来我学会了在网络的最后一层输出的基础上加一个 (L_2) 范数惩罚,相当于给远距离状态一个能量上推,训练就稳了。
3. 训练EBM:极大似然背后是一场“拔河比赛”
3.1 对数似然梯度:正相、负相、配分函数幽灵
给定训练数据 ({x_1,...,x_N}),EBM的极大似然目标是对数似然最大化:
[ \mathcal{L}(\theta) = \frac{1}{N}\sum_{n=1}^N \log p(x_n) = \frac{1}{N}\sum_{n=1}^N \left[-E(x_n) - \log Z(\theta)\right] ]
求梯度时,配分函数也会参与进来。对单个样本 (x) 有:
[ \frac{\partial \log p(x)}{\partial \theta} = -\frac{\partial E(x)}{\partial \theta} + \mathbb{E}_{p(x')}\left[\frac{\partial E(x')}{\partial \theta}\right] ]
这个式子里有两项。第一项叫正相,来自当前训练样本的贡献,作用是把观测到的数据点对应的能量往下压。第二项叫负相,来自模型自身分布 (p(x')) 的期望,作用是把模型当前认为的高概率区域的能量往上抬。一压一抬,最终模型会在数据附近形成低能量的“谷”,在其他地方形成高能量的“山”。
这里的核心问题是:负相需要从当前模型分布 (p(x')) 中采样。但 (p(x')) 正是我们要学的东西,而且它的归一化常数未知,无法直接采样。所以EBM训练绕不开一条老路——MCMC(马尔可夫链蒙特卡洛)。你必须在每个训练步里运行一个采样过程,从近似稳态的马尔可夫链里拿到负相样本。
下面这张表可以直观理解正相和负相的区别:
| 阶段 | 采样来源 | 梯度作用 | 通俗解释 |
|---|---|---|---|
| 正相 | 训练数据 | 降低数据点的能量 | 让真实例子越来越“顺眼” |
| 负相 | 模型分布(MCMC采样) | 升高模型样本的能量 | 让模型编出来的假例子越来越“别扭” |
3.2 采样方法:吉布斯、朗之万、SGLD
MCMC采样是EBM训练真正的瓶颈。最常用的两类方法各有适用场景。
第一类是块吉布斯采样(block Gibbs sampling),特别适合能量函数具有“条件分布可分解”结构的情形。比如RBM,给定可见层时,隐藏层的各个神经元条件独立,可以一次性并行采样所有隐藏单元;反过来也一样。这样交替采样几步,就能得到一个近似服从模型分布的样本。块吉布斯采样的优点是简单、稳定,不需要计算梯度;缺点是要求模型结构必须允许这种条件分解,否则一步采样要跑很长的嵌套循环,速度无法接受。
第二类是朗之万动力学(Langevin dynamics),适合连续状态空间。它的更新公式是:
[ x_{t+1} = x_t - \frac{\varepsilon}{2}\nabla_x E(x_t) + \sqrt{\varepsilon}, z_t, \quad z_t \sim \mathcal{N}(0, I) ]
这个更新有两部分:一部分是沿着能量梯度下降的方向走,减少能量;另一部分是注入高斯噪声,让采样过程能够探索状态空间。理论上,当步长 (\varepsilon) 足够小、迭代次数足够多时,(x_t) 的分布会收敛到 (p(x) \propto \exp(-E(x)))。实际使用时,人们更喜欢用随机梯度朗之万动力学(SGLD),就是只对一小批数据计算梯度来更新采样链,效率高很多,在采样过程中保持收敛性质。
经验上,朗之万动力学在连续空间的EBM训练里更常用,因为现代EBM的能量函数往往是神经网络,没有自然的条件分解结构。但朗之万动力学对步长非常敏感,步长太大链子容易发散,步长太小混合又太慢。新手最常犯的错误是把采样步长和学习率设成同一个值,两个概念差别很大:学习率决定参数更新幅度,采样步长决定模拟随机过程的离散化细粒度。
3.3 对比散度CD:RBM时代最实用的近似
理论上,负相采样需要让马尔可夫链从随机状态出发跑很久才能达到稳态。实践中根本耗不起。1999年前后,Hinton提出的对比散度算法给出一个极其大胆的近似:直接从训练数据点出发,只跑几步吉布斯采样,就用这个“半生不熟”的样本当作负相估计。
为什么这真的work?核心直觉是:训练数据已经落在真实分布的高概率区域附近,所以数据点的邻居也大概率是真实分布中的合法样本。虽然这时候链子还没完全混合,但对于估计负相梯度来说,与其从全局随机状态开始等它慢慢爬到数据附近,不如直接从数据附近开始,把最必要的那点“推开”信息拿过来。CD-k算法里的 (k) 就是采样步数,通常取1就够。训练流程可以概括为:
- 从一个训练数据点 (v^{(0)}) 出发。
- 交替吉布斯采样 (k) 步,得到模型样本 (v^{(k)})。
- 用正相统计量和负相统计量的差值近似梯度更新参数。
RBM训练时,经典的更新规则如下:
[ \Delta W_{ij} = \eta \left( \langle v_i h_j \rangle_{\text{data}} - \langle v_i h_j \rangle_{\text{recon}} \right) ]
其中角括号表示期望,第一项在给定训练数据的条件下求隐藏单元期望,第二项在重建样本下求期望。这个形式其实就是极大似然梯度中正相减负相的展开。CD算法的代价是引入了偏差,但换来的是训练速度的大幅提升。后续研究者又提出了持续对比散度(PCD,Persistent Contrastive Divergence),即维护一组始终在更新的采样链,不再从训练数据重启,而是让链子在训练过程中持续演变。PCD在训练早期更稳定,尤其在学习率较大时,能比CD得到更好的结果。
注意:CD算法虽然便宜,但不是免费的。采样步数太少时,负相估计严重偏差,模型会把训练数据周围的局部区域都当成低能区,导致生成样本模糊甚至退化成数据的小扰动。如果发现生成的样本看起来只是训练集的轻微噪声版本,试试把CD步数从1增加到5,或者换成PCD。
4. 手把手实现RBM:理论落到代码
4.1 RBM的能量与条件分布
受限玻尔兹曼机(RBM)是EBM里结构最简单、应用最广的代表。它是一个二部图模型:可见层 (v) 和隐藏层 (h),层内没有连接,层间全连接。每个变量取二值(通常为0或1)。
能量函数写成:
[ E(v,h) = -\sum_i a_i v_i - \sum_j b_j h_j - \sum_{i,j} v_i W_{ij} h_j ]
省去推导过程的细节,由这个能量函数可以得到两个关键结论。给定可见层 (v) 时,第 (j) 个隐藏单元取1的条件概率是:
[ P(h_j=1 \mid v) = \sigma\left(b_j + \sum_i v_i W_{ij}\right) ]
其中 (\sigma) 是sigmoid函数。反过来,给定隐藏层 (h) 时:
[ P(v_i=1 \mid h) = \sigma\left(a_i + \sum_j W_{ij} h_j\right) ]
这两条式子简单到让人怀疑是不是漏了什么。但它们的意义非常大:条件分布是因子化的,所以采样时可以一次性并行更新整层。这正是RBM能被高效训练的根本原因。
把隐藏层积分掉,还能导出关于可见层的自由能:
[ F(v) = -\sum_i a_i v_i - \sum_j \log\left(1 + \exp(b_j + \sum_i v_i W_{ij})\right) ]
这个自由能可以直接作为可见层“有多不自然”的打分,做异常检测时特别好用。
4.2 CD训练的核心代码骨架
下面用PyTorch写一个最简RBM训练骨架,只保留核心逻辑。
import torch import torch.nn as nn import torch.nn.functional as F class RBM(nn.Module): def __init__(self, n_vis, n_hid): super().__init__() self.W = nn.Parameter(torch.randn(n_vis, n_hid) * 0.01) self.a = nn.Parameter(torch.zeros(n_vis)) self.b = nn.Parameter(torch.zeros(n_hid)) def sample_h(self, v): p_h = torch.sigmoid(self.b + v @ self.W) return p_h # 训练时一般用概率做统计量 def sample_v(self, h): p_v = torch.sigmoid(self.a + h @ self.W.t()) return p_v def free_energy(self, v): term1 = -v @ self.a term2 = -F.softplus(self.b + v @ self.W).sum(dim=1) return term1 + term2 def cd_step(self, v0, k=1): v_k = v0 with torch.no_grad(): for _ in range(k): h_k = torch.bernoulli(self.sample_h(v_k)) v_k = torch.bernoulli(self.sample_v(h_k)) # 正相自由能 + 负相自由能 pos_energy = self.free_energy(v0).mean() neg_energy = self.free_energy(v_k).mean() return pos_energy - neg_energy训练循环里只需要做三件事:算CD损失、反向传播、用优化器更新参数。以大小为128的批次为例:
model = RBM(n_vis=784, n_hid=256) optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9) for batch in dataloader: batch = batch.view(batch.size(0), -1) loss = model.cd_step(batch, k=1) optimizer.zero_grad() loss.backward() optimizer.step()注意这里并没有显式计算正相和负相统计量再手动组合梯度,而是把自由能的差值直接当作损失。原理在第3章的推导里已经提过:对单个样本 (x),(-\log p(x) = F(x) + \log Z),CD近似把 (\log Z) 的梯度用负相自由能代替,于是 (F(v_0) - F(v_k)) 就是负对数似然梯度的代理。这样写代码最干净,也不容易出错。
4.3 训练要点与超参数选择
RBM训练看起来简单,实际中超参数却非常敏感。我按踩坑频率排序说几个重点。
第一是学习率。动量SGD情况下,0.01是一个比较安全的起点。学习率过大时,训练loss会上下剧烈震荡,模型参数在两次更新之间跳来跳去,采样链完全跟不上参数的变化,最终训练失败。如果做对比,你会发现CD训练里学习率需要的量级通常比普通分类网络小一个数量级,因为负相采样带噪声,梯度本身的方差天然就大。
第二是可见层激活函数。上面代码用的是伯努利采样,适合二值数据。如果处理灰度图像这种连续数据,建议改成正态可见单元:能量函数中的可见项换成 (\sum_i (v_i-a_i)^2 / (2\sigma_i^2)),条件分布 (P(v_i|h)) 变成均值为 (a_i + \sigma_i \sum_j W_{ij}h_j) 的高斯分布。此时还需要对输入数据做标准化,否则能量函数的尺度差异会导致训练非常不稳定。
第三是初始化和权衰。权重矩阵用均值为0、标准差0.01的正态初始化即可,不要用xavier初始化。因为RBM训练早期最怕某些隐藏单元在正相统计量里被“永远激活”,xavier初始化会让初始权重偏大,容易造成隐藏单元饱和。如果觉得重建误差收敛太慢,可以加一个非常小的 (L_2) 权重衰减,1e-4量级,配合L1稀疏惩罚效果更明显。
我建议训练时每隔几个epoch计算一次重构误差(reconstruction error),即 (v_0) 经过一次采样-重建后的平均距离。重构误差虽然不等于负对数似然,但它是一个非常廉价有效的早期停止指标。如果重构误差下降后开始反弹,通常意味着过拟合了,此时应该考虑增大训练数据量或降低模型容量。
5. 常见问题与排查技巧实录
5.1 模型崩到“什么都生成不了”
症状:训练完成后从模型采样,得到的图像要么全是噪声,要么全是同一个模糊图案。原因通常有两种。
第一种是负相样本太差,采样链没有混合。解决办法是增大CD的 (k) 步数,或者改用PCD维护持续链。第二种是能量函数表达能力不足,模型无法在数据不同模态之间都形成低能谷。解决办法是增加隐藏单元数或引入卷积结构(比如ConvRBM)。
我遇到过最迷惑的一次是:训练MNIST,看起来loss一直在降,但生成的样本却变成了十种数字的“叠加平均值”。后来发现是对输入数据做了中心化但没有压缩到0到1之间,负值输入让伯努利可见单元的sigmoid输出全部推向0.5附近,重建样本自然成了灰色斑点。数据标准化这个看似不起眼的步骤,对EBM的影响比对普通神经网络大得多。
5.2 训练loss变成NaN
几乎所有EBM新手都会遇到NaN问题。主要原因有两个,都可以提前预防。
第一是能量函数无界导致配分函数发散,前面已经提到过。第二是负相采样时链子发散,尤其使用朗之万动力学时步长过大。排查方法是先固定随机种子,把采样步长缩小10倍,看看loss是否恢复稳定。如果是,加一个梯度裁剪到1.0,很多情况下就能救回来。
需要特别提醒:EBM的loss曲线不像分类任务的交叉熵那样平滑。因为负相采样本身是随机过程,loss在训练过程中会有肉眼可见的波动,这是正常的。判定训练是否健康的标志不是loss单调下降,而是重构误差和大批量采样质量是否在逐步改善。
5.3 能量的平移和缩放:温度到底怎么调
温度在EBM里的作用是隐式的。把能量函数整体乘以一个常数等价于改变温度:(p(x) \propto \exp(-E(x)/T))。温度高则分布平坦,温度低则分布尖锐。
生产环境中很少显式调温度,但有一个常见场景必须懂:训练完成后做生成时,可以通过缩放能量来调整样本多样性。想要更清晰但多样性略低的样本,可以用 (T<1),相当于把能量整体放大,让模型更倾向于落在低能量的谷底。想要多样性能探索更多模式,用 (T>1),分布变得更平坦。这个技巧在RBM做图像生成时非常有用:训练时看不到的罕见模式,适当升温后经常能被采样出来。
实操心得:有一次我做数据增强,用RBM生成一些训练样本的变体,直接采样出来的图像模式比较单一。把采样过程的朗之万温度调到1.2之后,模式多样性肉眼可见地提升,代价是生成的个体质量稍有下降。对于数据增强这种容错率高的场景,这个取舍非常划算。
5.4 模式坍塌与多峰分布
EBM存在和GAN类似的模式坍塌问题,只是表现方式略有不同。EBM训练不足时,模型可能只在某个模式附近分配过高概率,其他模式完全没有被覆盖。检查办法是训练后用模型大量采样并统计类别分布,和训练数据的类别分布做对比。
缓解手段实践下来最有效的是三种:一是用PCD替代CD,因为持续链更容易跨越模式间的能量壁垒;二是训练数据里随机做数据增强,逼迫模型看到更多变化形态;三是显式添加一个熵正则项,鼓励隐藏层激活分布分散,间接提升采样多样性。面对多模态数据时,EBM的表达力很强,但训练动力学对模式覆盖并不友好,这是模型本身的结构特性,不是简单的调参能根治的。
6. 从EBM到扩散模型
6.1 Score Matching:绕开配分函数的另一条路
极大似然之所以痛苦,全是因为配分函数。那有没有办法完全躲开它?有,Score Matching就是其中一条非常漂亮的路。
注意到EBM的对数概率梯度是:
[ \nabla_x \log p(x) = -\nabla_x E(x) ]
这个量叫得分函数,它只依赖能量函数的梯度,完全不需要配分函数。Score Matching的训练目标就是让一个参数化的得分函数 (s(x)) 尽量接近真实数据的得分函数。可惜真实得分函数没法直接观测,但理论推导表明,可以用含噪数据构造一个可行的替代目标——去噪得分匹配(Denoising Score Matching)。
去噪得分匹配的做法是:给每个数据点加高斯噪声,得到一个噪声扰动后的分布 (q(\tilde{x}))。可以证明,训练 (s(x)) 去预测噪声扰动数据条件分布的得分,等价于学习原始数据分布的真实得分。等号右边的条件分布 (q(\tilde{x}|x)) 是高斯的,它的得分有解析形式,训练直接可解。这正是NCSN和DDPM这类模型在能量视角下的基本原理。
6.2 扩散模型:一条经过平滑处理的EBM训练路径
扩散模型(Diffusion Model)可以看作是EBM思想在现代条件下的延续。它没有直接学一个 (E(x)),而是学一族时间依赖的得分函数:
[ s_\theta(x_t, t) \approx \nabla_{x_t} \log q(x_t) ]
其中 (x_t) 是原始数据经过 (t) 步加噪后的版本。加噪过程本质上做了一个很巧妙的事情:把原来复杂无比、到处都是尖锐峰值的能量景观,通过逐步噪声扰动变得平滑。注意这和回火采样殊途同归——回火是采样时升温让链子更容易跨峰,扩散模型是把步进式的“升温降温”直接编码进训练目标里。
生成阶段,扩散模型从噪声开始,用学习到的得分函数做朗之万动力学或等价的逆过程,一步步“去噪”回到数据分布。对照第3章的框架:扩散模型训练用的就是去噪得分匹配(替代极大似然),采样过程用的就是基于梯度的朗之万采样(替代吉布斯采样)。理解了EBM的配分函数难题,再看扩散模型,你会觉得它的一切设计都像是在给一个旧问题做新解法——这层联系是这两年最值得理解的部分。
6.3 EBM的现实应用场景
虽然后续有扩散模型等更强大的生成模型,但EBM在几个特定场景下依然有不可替代的优势。
一是异常检测。EBM天然自带一个打分功能:能量低是正常样本,能量高是异常样本。训练一个EBM只需要正常数据,推理时直接算自由能或能量值就行。相比其他生成模型还要算重构误差那些间接指标,EBM的打分是直接的、理论完备的。
二是组合优化和采样问题。EBM的能量函数可以编码约束条件和目标函数,通过采样直接寻找低能构型。这在一些组合优化问题、约束满足问题上表现得非常自然。
三是作为更复杂模型的基础组件。比如能量模型可以被嵌入深度架构中,或者和VAE类模型结合,用来建模潜变量空间的先验分布。很多后续理论分析也都以线性EBM作为基石,先理解这一篇的内容,后面看变分推断、平均场论、扩散模型那一大串东西都会轻松很多。
最后分享一点个人体会。刚开始接触EBM时,我最大的困惑是:一个算不出归一化常数的模型,凭什么能当概率模型用?后来在实做里把采样、正负相位、配分函数梯度的逻辑一步步走通,这个疑问才消失。EBM确实难训,采样慢,调参玄学,但它的价值恰恰在于逼迫你把概率模型的核心难点——采样和归一化——想透一次。现在去看扩散模型那些intricate的细节,很多都能在EBM的训练困境里找到影子。后面这个系列我会继续拆解RBM与深度玻尔兹曼机的关系、变分推断与平均场方法,以及EBM与对比学习方法之间的内在联系。如果你正在学概率模型,建议这一篇里的正相/负相梯度推导和CD算法亲自动手推一遍,值得花这个时间。