ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

受限玻尔兹曼机原理与PyTorch实现:能量模型与对比散度详解

受限玻尔兹曼机原理与PyTorch实现:能量模型与对比散度详解 1. 为什么还要学Boltzmann机从能量模型看另一个世界1.1 深度学习之外的另一种思路这几年不管是做CV还是NLP主流方案基本绕不开卷积神经网络、循环神经网络、Transformer这些判别模型。大家习惯的套路是拿数据喂进去让网络学会从输入到输出的映射最后用交叉熵或者MSE当损失函数一把梭。这种范式太深入人心了以至于很多人学了几年神经网络都不知道在深度学习火起来之前还有一整套基于能量的生成模型体系。Boltzmann机就是这套体系里最有代表性的模型之一。我第一次接触Boltzmann机是在读Hinton那篇用对比散度训练受限玻尔兹曼机的经典论文时当时第一反应是这玩意儿的思路和反向传播完全不一样它不直接学输入到输出的映射而是尝试拟合数据本身的分布。你可以这样理解卷积神经网络像是在学“怎么把一张图正确分类”而Boltzmann机是在学“这张图本身长什么样像素和像素之间有什么规律”。这是一种生成式思维和判别式思维有本质的区别。写这篇笔记的动机很简单——市面上的深度学习教程对Boltzmann机的讲解往往一笔带过或者一上来就甩公式缺乏从零到一、从原理到代码的完整链路。我想用我这段时间踩坑总结的经验把Boltzmann机的原理、受限玻尔兹曼机RBM的实现细节、训练过程中的常见问题好好梳理一遍。无论你是在校学生、算法工程师还是对生成模型感兴趣的爱好者这篇笔记应该都能帮你少走一些弯路。1.2 Boltzmann机到底解决什么问题先别急着看公式。我们要搞清楚一件事情的动机才能有动力学下去。Boltzmann机本质上是想解决一个问题怎么从一堆没有标签的数据中自动学到数据内部隐藏的结构。举个例子假设你有一堆手写数字图片每张图都是一个784维的向量28x28像素。在没有标签的情况下普通神经网络不知道该干什么——没有监督信号反向传播就没法用。但Boltzmann机可以。它做的事情是把数据当成一个概率分布然后让模型去拟合这个分布。一旦拟合成功模型就有了“生成能力”可以从分布中采样出新的手写数字图片也可以把高维数据压缩成低维特征供下游任务使用。所以Boltzmann机的价值主要体现在三个方向一是无监督特征学习二是生成新样本三是解决组合优化问题比如旅行商问题这类需要寻找能量最低状态的场景。在实际业务中RBM被用在过协同过滤推荐、电影评分预测、图像特征提取等任务上后来还被Hinton用来预训练深度置信网络DBN算是深度学习的祖师爷之一。这里不得不提一下它与主流模型的关系。卷积神经网络擅长捕捉空间局部特征循环神经网络擅长处理时序依赖而Boltzmann机擅长的是无监督地刻画数据分布。GNN图神经网络虽然在图上做消息传递但如果你想把图结构数据也纳入生成式建模玻尔兹曼机那一套能量视角依然很有启发。理解了能量模型再去看现在大火的扩散模型、基于能量的生成模型你会发现它们在思想上是一脉相承的。2. 波尔兹曼机原理拆解能量、概率与状态2.1 能量函数从哪来Boltzmann机这个名字听着高大上其实它借用的核心思想来自统计物理——能量。学过物理的朋友都知道一个系统的状态分布与能量密切相关。在物理里分子在高温下运动剧烈低温下趋于有序在Boltzmann机里我们把“神经元的各种激活状态”看成系统的状态给每种状态定义一个能量值然后让模型学会往低能量的状态走。具体怎么定义能量先来个简单的版本。假设网络里有若干个神经元每个神经元的状态可以是0或1这也是Boltzmann机最初的形式——一切都是二值的不要觉得简单后面你会发现二值反而让推导清爽很多。再假设每两个神经元之间有一条连接连接有对应的权重。那么整个网络的能量可以写成E -Σ_i b_i * v_i - Σ_{ij} w_ij * v_i * v_j这里的v_i表示第i个神经元的状态0或1b_i是偏置可以理解成神经元自身的倾向w_ij是第i个和第j个神经元之间的权重。这个公式看着简单意思却很直观如果两个神经元之间的权重是正的那它们同时激活都为1的时候第二项贡献为负能量会降低。系统总是倾向于低能量的状态所以正权重意味着“这两个神经元倾向于同时亮”。负权重则相反。偏置同理正的偏置让这个神经元更容易激活。就这一句话Boltzmann机把模型的行为和物理直觉打通了学习的过程本质上是找到一组权重和偏置让真实数据对应的状态能量尽可能低而随机状态的能量尽可能高。这样一来数据分布就像是能量地形图上的“低谷”模型采样的时候自然就会往低谷跑。在完整的Boltzmann机里神经元还会分成可见单元visible units和隐藏单元hidden units。可见单元用于接收外部数据比如图像的像素值隐藏单元则是模型的内部变量用于捕捉数据背后的隐藏因素。如果你把可见单元看成“表象”那隐藏单元可以理解为“本质规律”。比如一张人脸图片可见单元是像素隐藏单元可能就是“是否微笑”“是否戴眼镜”这类抽象属性。把隐藏单元加上之后能量函数变成E(v, h) -Σ_i a_i * v_i - Σ_j b_j * h_j - Σ_{i, j} w_ij * v_i * h_j这里的v代表可见单元状态h代表隐藏单元状态a和b分别是两边的偏置w_ij是可见单元i和隐藏单元j之间的权重。这个公式在标准RBM里会一次次出现建议先把它盯住后面所有推导都围绕它展开。2.2 从能量到概率Boltzmann分布有了能量之后怎么变成概率答案是玻尔兹曼分布。这个分布原本是统计物理用来描述系统在不同能量状态上的概率分布的长这样P(x) (1/Z) * e^(-E(x))其中Z叫做配分函数是所有可能状态的能量之和Z Σ_x e^(-E(x))用来保证所有概率加起来等于1。而e^(-E)这个指数形式意味着能量越低的状态概率越高能量越高的状态概率越低。这就把“低能量优先”的物理直觉转化成了数学上可操作的采样规则。在Boltzmann机里我们关心的分布是P(v, h)也就是可见单元和隐藏单元的联合分布。但由于Z涉及到对所有可能状态求和当神经元数量上百的时候Z根本算不出来——2的n次方种状态即使n100也已经是个天文数字。这也是Boltzmann机早期训练困难的核心原因后面说的各种改进算法绝大多数都是为了绕开对Z的直接计算。还有一个值得一提的概念是温度T。如果把分布写成P(x) (1/Z) * e^(-E(x)/T)T就是温度。T越大分布越平緩系统越随机T越小分布越尖锐系统越倾向于落在能量最低点。实际训练中一般固定T1但如果你对模拟退火有了解会发现调温度在采样中是个有用的技巧。我们通常不会在基础的RBM上折腾温度不过理解它的含义有助于后续阅读更复杂的变体。3. 从全连接到受限RBM的出现3.1 标准Boltzmann机为什么难搞原始的Boltzmann机里所有神经元之间都有连接不管它是可见单元还是隐藏单元。这种全连接结构理论上是美的——它足够通用理论上可以逼近任意分布。但在工程上这是噩梦问题出在训练算法上。回忆一下训练任何概率模型核心就是求极大似然也就是让模型在训练数据上的概率最大化。对Boltzmann机做梯度推导会得到一个非常优雅但极具讽刺意味的结果参数更新量等于“数据分布下的期望”减去“模型分布下的期望”。第一项好算数据就在手边第二项要求我们从模型分布中采样——可模型分布恰恰就是那个Z算不出来的分布。你没法从一个未知分布中采样所以梯度没法算。为了解决这个问题早期研究者用马尔可夫链蒙特卡洛MCMC方法让网络在数据中初始化然后反复迭代直到达到平衡态再采样获取近似梯度。但问题是MCMC需要非常多的迭代步才能收敛每一轮参数更新都要跑一遍MCMC整个训练过程慢到让人绝望。在算力有限的年代标准Boltzmann机基本是个理论玩具。3.2 受限Boltzmann机RBM的约束Hinton在2002年左右明确提出用对比散度Contrastive DivergenceCD算法来训练RBM后面又和合作者一起完善了这套方案。所谓“受限”Restricted就是在结构上加了一条硬性约束可见单元内部不允许有连接隐藏单元内部也不允许有连接所有的边只能存在于可见单元和隐藏单元之间。这个约束带来的好处是巨大的。因为层内没有连接给定可见单元状态v时每个隐藏单元的条件概率是独立的可以并行计算P(h_j1|v) sigmoid(b_j Σ_i w_ij * v_i)反过来给定隐藏单元状态h时每个可见单元的条件概率也是独立的P(v_i1|h) sigmoid(a_i Σ_j w_ij * h_j)这两个条件概率公式是RBM所有算法的基石。它们让吉布斯采样变得极其简单——你可以一次性更新所有隐藏单元再一次性更新所有可见单元完全不需要逐神经元迭代。打个比方全连接的Boltzmann机像是在一群人里两两握过手关系错综复杂RBM则把人们分成了两组同组的人互不相识只有跨组才有联系整个社交结构清爽了许多。从另一个角度说RBM把原先无法处理的联合分布P(v, h)拆解成了两个可以高效计算的条件分布的交替。这正是它能在实际任务中跑起来的关键。不要小看这个结构改动它把“理论上优美但实践上跑不动”变成了“理论依然优美且实践可行”这本身就是工程思维的胜利。4. 实战用Python从零实现一个RBM4.1 数据准备与任务定义说了半天理论终于到了动手环节。我建议你不要急着往MNIST上冲先用一个小到能在笔记本上跑动的数据集来验证理解。我这次用的是经典的MNIST但只取了一小部分1000张二值化处理方便和RBM的0/1状态匹配。二值化这一步很关键。原始MNIST是0到255的灰度值虽然也有用实数值RBM处理的方案但对初学者来说最稳妥的做法是设定阈值比如128大于阈值的像素置1小于等于的置0。这样一来每张图就是一个0/1向量正好对应RBM可见单元的状态。用这种方法训练出来的RBM学到的特征非常直观方便调试。代码整体的结构分成三块RBM类定义、训练循环、可视化验证。我下面把每一块的思路和关键代码都贴出来解释一遍。4.2 核心代码拆解从能量到采样先看RBM类的初始化部分import numpy as np class RBM: def __init__(self, n_visible, n_hidden, lr0.01, momentum0.5): self.n_visible n_visible self.n_hidden n_hidden # 权重和偏置初始化小随机值有利于打破对称 self.W np.random.normal(0, 0.01, (n_visible, n_hidden)) self.bv np.zeros(n_visible) # 可见单元偏置 self.bh np.zeros(n_hidden) # 隐藏单元偏置 # 动量缓存记录上一次梯度更新量 self.vW np.zeros_like(self.W) self.vbv np.zeros_like(self.bv) self.vbh np.zeros_like(self.bh) self.lr lr self.momentum momentum权重的初始化我用了均值为0、标准差0.01的正态分布。不要用全零初始化否则所有隐藏单元学到的东西都会一样根本没有对称性破缺。偏置初始化为0没问题但后面你会在实验中发现偏置的更新其实也很有讲究。接下来是采样函数。RBM最核心的操作有两个给定隐藏单元概率计算可见单元以及反过来。在实现上它们是一对镜像函数def sample_h(self, v): # 计算隐藏单元激活概率然后按概率采样二值状态 h_prob 1.0 / (1.0 np.exp(-(v self.W self.bh))) h_sample (h_prob np.random.rand(h_prob.shape[0], h_prob.shape[1])).astype(np.float32) return h_prob, h_sample def sample_v(self, h): # 计算可见单元重构概率同样按概率采样 v_prob 1.0 / (1.0 np.exp(-(h self.W.T self.bv))) v_sample (v_prob np.random.rand(v_prob.shape[0], v_prob.shape[1])).astype(np.float32) return v_prob, v_sample这里有个小细节值得注意我们同时返回了概率和采样结果。对比散度算法里训练时有时候用概率有时候用采样值具体怎么选要看是哪个阶段。如果你刚开始写RBM最容易犯的错误就是全用概率或者全用采样值这会导致训练行为和你预想的不一致。下面训练部分会讲清楚每个阶段该用哪个。4.3 训练流程对比散度CD-k算法对比散度是Hinton提出的聪明办法。标准的极大似然需要从模型分布中采样来计算梯度这很慢CD算法给出的近似方案简单粗暴用数据作为起点只跑k步吉布斯采样通常k1就够用然后用采样结果近似模型分布的期望。具体流程是这样的def cd_k(self, v_input, k1): # v_input 是原始输入数据形状为 (batch_size, n_visible) v_positive v_input # 正向阶段从输入数据出发采样隐藏状态 h_prob, h_sample self.sample_h(v_positive) # 反向-重构-正向的交替采样 v_negative v_input.copy() for step in range(k): h_prob_neg, h_sample_neg self.sample_h(v_negative) v_prob_neg, v_negative self.sample_v(h_sample_neg) # 最后再采样一次隐藏状态用于计算负向统计量 h_prob_neg, h_sample_neg self.sample_h(v_negative) # 梯度计算正向期望 - 负向期望 positive_grad v_positive.T h_prob # 注意这里用的是概率 negative_grad v_negative.T h_prob_neg # 更新权重和偏置 grad_w (positive_grad - negative_grad) / v_input.shape[0] grad_bv np.mean(v_positive - v_negative, axis0) grad_bh np.mean(h_prob - h_prob_neg, axis0) # 加动量并更新 self.vW self.momentum * self.vW self.lr * grad_w self.vbv self.momentum * self.vbv self.lr * grad_bv self.vbh self.momentum * self.vbh self.lr * grad_bh self.W self.vW self.bv self.vbv self.bh self.vbh # 返回重构误差用于监控训练过程 err np.mean((v_positive - v_negative) ** 2) return err这段代码是RBM训练的核心我解释几个关键点。第一正向阶段为什么用h_prob而不是h_sample在计算正梯度时我们用数据的期望来近似。理论上应该用采样得到的二值状态但Hinton在实践中发现直接用概率期望效果更好方差更小。这也是RBM实现中最常见的“软性”细节之一。第二负向阶段我们跑了k步吉布斯采样起始点是原始数据。标准的CD-k算法里k1已经能取得不错的效果k越大越接近真实的模型分布但计算量也越大。实际训练中k1是个不错的选择我后来试过k2、k3效果提升很有限但训练时间成倍增加。第三权重更新公式用的是正负梯度的差值。这个差值的含义很直观我们希望增加数据对应的那部分概率正梯度方向减少模型自发生成的那部分概率负梯度方向。正负之间互相拉扯最终达到平衡。4.4 训练循环与全程监控主体循环写起来比较直接。我每次迭代随机取一小批数据然后用CD-k更新参数。这里有个非常关键的工程点训练过程中一定要监控重构误差也就是输入数据和经过一轮正负采样后重构出的数据之间的差异。重构误差下降说明模型在逐渐记住数据的规律如果重构误差震荡不降或者前期先降后升说明学习率或者动量设置有问题。def train_rbm(rbm, data, epochs20, batch_size64, k1): n_samples data.shape[0] history [] for epoch in range(epochs): # 每个epoch打乱数据顺序 indices np.random.permutation(n_samples) data_shuffled data[indices] epoch_err 0.0 n_batch 0 for i in range(0, n_samples, batch_size): batch data_shuffled[i:ibatch_size] err rbm.cd_k(batch, k) epoch_err err n_batch 1 avg_err epoch_err / n_batch history.append(avg_err) print(fEpoch {epoch1}/{epochs}, avg reconstruction error: {avg_err:.4f}) return history训练过程的监控信息除了重构误差我强烈建议把权重可视化出来看。因为RBM学到的权重矩阵每一列对应一个隐藏单元可以reshape成图像直接展示这个隐藏单元在“看”什么样的模式。这一步比任何指标都直观效果非常震撼。import matplotlib.pyplot as plt def visualize_weights(rbm, grid_shape(10, 10), image_shape(28, 28)): fig, axes plt.subplots(grid_shape[0], grid_shape[1], figsize(10, 10)) n_hidden rbm.W.shape[1] for i in range(n_hidden): ax axes[i // grid_shape[1], i % grid_shape[1]] w rbm.W[:, i].reshape(image_shape) ax.imshow(w, cmapgray) ax.axis(off) plt.show()我第一次跑通可视化的时候看到的是一排排类似笔画片段、边缘方向的特征图那种感觉很难用语言形容——原来没人告诉模型“数字长什么样”它就是自己从1000张二值图里提炼出了这些模式。这就是无监督特征学习的魅力也是Boltzmann机最让我着迷的地方。5. 训练过程踩过的坑从发散到收敛的排查实录5.1 学习率的选择不是越大越快学习率这个超参数在RBM里比在普通前馈神经网络里更敏感。我刚开始训练的时候图省事直接设了0.1结果重构误差不仅不降反而一路狂飙从0.2涨到0.7。后来才明白原因RBM的梯度本身就带噪声学习率太大的话参数会在最优解附近剧烈震荡甚至越震越远。通过反复实验我发现对于二值RBM和MNIST数据学习率在0.01到0.05之间比较稳。如果你用的是实值可见单元即输入是连续值学习率还要调得更小一些比如0.005。一个实用的技巧是如果你发现重构误差在早期不降反升先把学习率除以10再试如果误差降得很慢但很稳可以尝试乘以2但每次调整后都要观察几个epoch再下结论。另一个相关问题是动量momentum的设置。动量可以让梯度更新更平滑抑制震荡。我的经验是前5个epoch用0.5的动量后面提升到0.9。这样的“动量预热”策略在很多深度学习任务里都有效但在RBM里尤其重要——因为RBM的梯度噪声大高动量前期容易让参数跑偏。5.2 权重初始化与偏置的细节初始化这件事我前面提过一句但值得展开说。权重的标准差如果太大比如0.1那么隐藏单元的输入加权和会分布得很散sigmoid函数非常容易饱和。饱和意味着梯度几乎为零模型学不动。反之标准差太小如0.001会让所有单元一开始都近似对半分学习速度偏慢但比较稳。0.01是一个折中的选择实测效果不错。偏置初始化有一个经验法则可见单元偏置可以设置为训练数据中对应特征的边缘概率也就是每个像素为1的频率的对数几率即log(p/(1-p))。这样模型一开始就能重建出数据的平均状态而不是从全零状态开始挣扎。隐藏单元偏置一般初始化为0就行。我踩过这个坑——初始化全零偏置的时候前几个epoch重构误差虽然也降但速度明显比用边缘概率初始化慢很多。5.3 批量大小的取舍批量大小对RBM训练的影响也很微妙。批量太小比如1梯度噪声极大训练不稳定批量太大比如512每个epoch更新次数太少收敛偏慢而且大batch会让CD-k算法中的负采样偏差更大。我在这份笔记里用的64是一个比较平衡的选择。如果数据量很大还有一个实用建议宁可多跑几个epoch也别把batch开太大。RBM的CD算法对batch大小没有普通神经网络那么敏感但实验中我发现64到128区间表现最稳。超过256之后重构误差下降速度明显变慢原因可能在于大batch减少了参数更新的频次随机性降低模型更容易陷入局部最优。5.4 过拟合怎么判断RBM的参数数量和隐藏单元数量直接相关隐藏单元太多模型容易把训练数据的细节死记硬背下来表现出来就是在训练集上重构误差很低但拿到测试集上生成的样本质量很差。判断过拟合的一个方法训练到一半时把模型生成的样本画出来看看。如果你发现生成样本几乎就是训练样本的简单复制说明模型容量已经超出需求应该减少隐藏单元数量或者加入权重衰减项。权重衰减L2正则在RBM里的实现很简单就是在权重更新时减去一个小比例比如0.0001的权重值。但要注意RBM的权重衰减系数一般比普通神经网络小很多太大了会压制模型的学习能力。我目前用的是1e-4效果还不错。6. 常见错误与调试速查表训练RBM的过程中很多问题看起来五花八门实际上归结起来就那么几类。我把这段时间遇到过的典型问题整理成一个速查表方便你做实验时快速定位。症状可能原因排查与解决重构误差一开始就很高0.5以上而且不下降学习率过大参数震荡发散把学习率调小10倍再试确认权重初始化标准差在0.01左右重构误差缓慢下降但生成的样本模糊成一团隐藏单元数量太少模型容量不足增加隐藏单元数量或者减少批量大小以增强随机性训练集重构误差很好但采样出的新样本完全不像原始数据过拟合模型记住训练样本的分部模式增加权重衰减减少训练epoch或者增大测试集和训练集的差异训练过程正常但不同隐藏单元学到的权重完全相同权重初始化对称没有打破对称性使用随机初始化确保每个隐藏单元的初始权重不一致生成样本噪声很大像是黑白电视雪花采样步数k不够或者数据预处理没做二值化尝试CD-2或CD-3检查输入数据是否已经转换为正确的0/1格式训练时间极长每轮epoch要跑几分钟隐层单元数量过大或数据没有做批量处理减少隐藏单元数确保用了批量采样而非逐样本循环这张表只覆盖了最常见的几种。实际操作中你可能会碰到更诡异的问题我的建议是先从最基础的组件开始检查——输入数据的形状和值域、sigmoid计算有没有溢出、采样逻辑是否正确。很多所谓的问题最后查出来只是代码里一个形状不匹配或者类型错误。另外一个独家的调试技巧把训练过程可视化。我在代码里加了实时画重构误差曲线和权重图像的功能每隔几个epoch就输出一次。这样做的好处是任何异常都能很早发现而不是等训练结束才在一堆数字里找线索。机器学习这个领域可视化永远是排在第一位的调试工具。7. 下一步往哪儿走从RBM到更广阔的能量模型世界7.1 深度置信网络与深度玻尔兹曼机RBM单层能学的特征有限但它有一个非常优雅的扩展方式——堆叠。把第一个RBM的隐藏层输出当成第二个RBM的输入再叠加一层隐藏单元就构成了深度置信网络DBN。Hinton最早提出DBN的时候就是用这种逐层预训练的方式初始化一个深层神经网络然后再用反向传播微调。在当时这是解决深度网络训练困难的主要方案后来才被ReLU、BatchNorm等技巧部分替代但它依然是一个重要的历史坐标。深度玻尔兹曼机DBM则更进一步它允许多个隐藏层之间也存在连接不只是相邻层的单向连接训练方式也比DBN复杂。如果你已经理解了RBM的CD算法再去看DBM会容易很多——它就是一个多层的能量模型采样过程从单层交替变成了多层交替梯度推导的核心思想完全一致。我个人觉得DBM是理解深度生成模型的最佳跳板因为它把“层间交互”和“联合学习”的概念融合在一起和现在扩散模型中的去噪过程有异曲同工之妙。7.2 能量视角在今天的技术里还重要吗你可能会有疑问都2025年了RBM这些东西还有实际价值吗我的看法是直接在生产环境里用RBM的场景确实少了但它的思想遗产无处不在。现在大火的扩散模型本质上就是在学习一个能量景观的梯度基于能量的模型EBM也仍然是生成建模的重要流派。更实际的一点是理解RBM能帮你理解所有生成模型的底层困难——近似推断、采样、对配分函数的逃避。这些问题在RBM里以最清晰、最无修饰的形式呈现出来一旦你搞懂了RBM里的CD、吉布斯采样是怎么运作的再去看变分自编码器、生成对抗网络甚至扩散模型都会有一种“原来都是一家人”的通透感。从技能树的角度看RBM涉及的数学概率图模型、能量函数、采样方法和工程二值数据处理、动量优化、正则化都是通用的。即使你的研究方向是图神经网络或者卷积神经网络这些基础能力也不会浪费。最后分享一个小经验如果你打算深入理解RBM请一定不要只看公式或者只调别人的库亲手从零把这个模型用numpy写一遍哪怕只是处理一个简单的玩具数据集收获也会远超预期。很多模型你以为自己懂了直到自己调试才会发现各种细节上的坑远比想象得多。正是因为写这篇笔记我才真正把这些细节全部串起来。学习一个模型最好的方式就是把它彻底拆开再一步步装回去。
返回列表