深度信念网络(DBN)这个名词,现在很多深度学习入门教程里已经不太常提了,但它仍然是理解整个深度学习演进路线上绕不过去的一站。经典教材往往会把这部分放在第12章这样的后半段位置,原因是前面得先把概率图模型、玻尔兹曼机、反向传播这些地基垫好,不然很难讲清楚它到底在做什么。不过教材的篇幅有限,读者最后经常只记住一堆能量函数和采样公式,落到自己的项目里依然不知道从哪下手。
这篇我想用做实际模型训练比较直接的口吻,把DBN拆开重新过一遍:它当年解决了什么问题、受限玻尔兹曼机是怎么当积木用的、逐层贪婪预训练怎么把网络变深、微调阶段模型性质发生了什么变化。如果你在阅读相关经典论文,或者想了解除了CNN、Transformer之外的另一条表征学习路线,这篇文章配合代码可以帮你少走一些弯路。
1. 深度信念网络出现前:深层模型卡在哪一步
在真正动手复现DBN之前,我对它的印象也停留在“很老、已经被新模型替代了”这个层面。跑通一轮之后才发现,这个判断太武断了。DBN里藏着的很多思路,今天照样活跃在自编码器、生成模型以及各类无监督预训练流程里。
1.1 梯度消失之外的另一道坎
现在一谈到深度网络训练困难,大家第一反应就是梯度消失。这个说法对,但不全面。深度信念网络在提出的年代,要面对的问题其实分两层。
第一层确实是梯度消失。当时主流的非线性激活函数还是S型函数,导数在饱和区趋近于零。一个十几层的网络,反向传播的梯度信号经过多层相乘,传回前几层时基本已经衰减到可以忽略。这直接导致网络前部学不到有效参数,整个模型只是名义上“深”,实际有效层数不多。
第二层是初始化困境。没有可靠的大规模初始化方法时,深度模型一开始就落在损失曲面比较糟糕的区域,反向传播推很久都推不出来。预训练的价值就体现在这里:它先把每一层初始化为一个还不错的特征提取器,完全不依赖标签,然后从这样更有意义的起点出发去做整体优化。
有很多资料把DBN描述成“为了解决梯度消失而生”。这个说法不够准确。它真正解决的问题,是让深度网络从一开始就站在一个有结构的数据表示上,而不是站在随机权重上硬闯。
1.2 无监督预训练为什么一时间被看作通用解法
DBN的另一个关键贡献,是“无监督预训练”这个操作本身。
对于一批图像,你完全可以不关心它的类别标签,因为数据本身有结构:手写数字的笔划粗细、方向、局部曲率都呈现重复出现的模式。用无监督方法先把这些统计规律提取出来,后续的有监督训练通常只需要在一个很小的分类头上进行。
当时一批工作反复验证过:用预训练权重做初始化再去反向传播,效果远好于从随机初始化开始硬训。预训练不是锦上添花,而是让深度结构能够真正收敛的前提条件。
后来随着更好的初始化方法、批归一化、残差结构出现,端到端训练也能收敛得很好,无监督预训练才从“必需品”退成“可选手段”。但理解它为什么曾经是必需品,仍然能帮你理清很多后续模型的动机。
2. 核心积木:受限玻尔兹曼机
深度信念网络不是随便拿几层神经网络叠在一起的纯前馈结构,它的地基是受限玻尔兹曼机(RBM)。RBM本身就是一个带随机变量的概率模型,理解它之后,DBN的堆叠逻辑才会顺起来。
2.1 能量函数:从物理中借来的语言
RBM是无向概率图模型,有一层可见单元v和一层隐藏单元h。名字里“受限”这个词很关键:可见层内部没有连接,隐藏层内部也没有连接,只有可见单元和隐藏单元之间存在权重边。
它的能量函数如下: E(v,h) = -Σ_i a_i v_i - Σ_j b_j h_j - Σ_i Σ_j v_i W_ij h_j
a_i是可见偏置,b_j是隐藏偏置,W_ij是两层之间的权重。能量取负指数后做归一化,就得到联合分布: p(v,h) = exp(-E(v,h)) / Z
这里的Z是配分函数,要对所有可能的(v,h)状态组合做指数级求和,几乎不可能精确计算。所以后面所有训练算法本质上都在想办法绕开Z。
把能量函数拆开理解会直观很多:如果一块输入图像和一组隐藏特征同时出现时能量很低,说明这对组合是“舒服”的;如果能量很高,说明模型认为这种搭配不合理。训练RBM就是调整能量参数,让真实数据所在状态能量降下去,让随机搭配的状态能量抬起来。
2.2 条件分布与吉布斯采样
因为RBM层内没有连接,给定一层时另一层的条件概率可以因式分解成很简单形式: p(h_j=1|v) = σ(b_j + Σ_i v_i W_ij) p(v_i=1|h) = σ(a_i + Σ_j h_j W_ij)
也就是说,知道所有可见单元的情况下,某个隐藏单元是否激活,只取决于自身偏置和可见单元按权重的线性加权和;反过来也一样。有了这两个条件概率,就可以用两步交替采样:
- 根据当前可见状态v,算出每个h_j的激活概率,按该概率抽样得到隐藏状态h;
- 根据当前隐藏状态h,算出每个v_i的激活概率,按该概率抽样得到新的可见状态v;
- 不断重复这个过程。
这个流程叫吉布斯采样。理论上交替足够多轮之后,采出的(v,h)会趋近模型分布p(v,h)。但真正要去跑那么多轮采样,计算开销很大,所以后面才有了对比散度算法。
2.3 对比散度:让负相估计变得可计算
训练RBM的方向是最大化数据对数似然log p(v)。对参数求导之后,梯度会分成正负两项: ∂log p(v)/∂W_ij = E_data[v_i h_j] - E_model[v_i h_j]
第一项叫正相,表示数据分布下的期望,可以用训练样本和隐藏单元后验概率直接估计;第二项叫负相,表示模型分布下的期望,严格来说要等吉布斯采样跑到稳态之后,再从模型分布里采样估计。这是训练开销最大的部分,也是稳定性最差的部分。
对比散度(CD)做了一个大胆近似:既然吉布斯采样的目标是模型分布,那就直接从真实数据出发,只跑k步吉布斯采样,用最终的重建状态来近似模型的负相样本。因为起点离真实数据不远,k=1通常就够用,计算成本一下子降了下来。
于是更新规则可以写成: ΔW = ε(⟨v^0 h^0^T⟩_data - ⟨v^k h^k^T⟩_recon) Δa = ε(⟨v^0⟩_data - ⟨v^k⟩_recon) Δb = ε(⟨h^0⟩_data - ⟨h^k⟩_recon)
上标0代表原始可见输入,k代表跑完k步吉布斯采样后的重建状态。注意这类梯度是带近似误差的,所以在实际训练里很难看到平滑下降的loss曲线,通常需要靠重建误差、重建图像质量这些信息来监控训练状态。
3. 从单体RBM到DBN:逐层贪婪预训练
单个RBM只学一层隐藏表示,已经能做一些有趣的事情,但离“深度”还有距离。深度信念网络的核心操作,是把多个RBM串起来,而且采用逐层贪婪训练:每次只训练一层,锁住前面的权重,再用前面层的输出作为下面一层的输入。
3.1 堆叠顺序与新的数据表示
具体训练过程拆成四步看会很清楚:
- 把原始数据v当作可见层,训练第一个RBM,得到第一层权重W1;
- 用W1把所有样本正向传播成隐藏表示h1。这里可以用概率σ(W1 v + b1),也可以按伯努利抽样,但下一层训练时一般用概率会稳定一些;
- 把h1当作新一层的可见输入,训练第二个RBM,得到W2;
- 重复下去,直到覆盖设定好的层数。
所以初看DBN会觉得很像普通多层网络,但它其实是一个混合概率图模型。最底下的若干层之间是有向的,数据从可见层向上传播;最上面两层之间是无向的,因为它们原本就是同一个RBM里的可见层和隐藏层。顶层这个无向连接非常关键,它让DBN在顶层保持了一种“联想记忆”机制,可以从中间表示重新生成出看起来合理的输入样本。
有个细节经常被人忽略:逐层预训练过程中,每一层RBM面对的可见层分布并不相同。第一层在原始图像上训练,第二层在第一层隐藏概率空间上训练,第三层又在第二层的隐藏概率空间上训练。每一层都只负责重新表达它看到的输入,找更紧凑的内部表征。这种一层层重新编码的过程,才是DBN能够自动学到层次化特征的根本原因。
3.2 顶层无向连接意味着什么
很多人把DBN简化理解成“很多个RBM直接叠起来”,这个说法只对了一半。关键在顶层那几个RBM并不只是作为普通隐藏层安放,它还负责生成性质。
你可以把DBN理解成一个带记忆底座的生成系统:顶层无向结构负责捕捉全局的高阶关联;之下的有向层再把这些高阶关联逐层“解释”到可见空间。所以DBN不仅能做判别任务,理论上还能从顶层状态出发反向采样出新样本。这一点让它和纯粹判别式的网络结构拉开了距离。
后续的有向生成模型,比如各种VAE变体,基本不再沿用这种顶层无向结构,因为无向层训练复杂、采样成本高。但在当时,这是少有的能同时把判别路径和生成路径都放进一个深度模型里的做法。
3.3 预训练之后,模型发生了什么
完成预训练时,分类器还没影子,但模型已经和随机初始化大不一样了。每一层RBM的重建误差在下降,说明它已经记住了大量数据中的统计结构。
这时把预训练得到的权重作为初值,再接一个输出层去做反向传播,效果会比随机初始化好不少。原因有两个:一是隐藏层已经形成了有意义的中间特征,分类头只需要在优质特征上加一层简单决策边界;二是初始点所在损失曲面区域相对理想,后续反向传播不容易陷进平坦的坏局部极小值。
在实际项目里的体感是,预训练带来的收益在数据量少、标签稀缺时尤其明显。数据量巨大时,反向传播自己也能从零学出特征,预训练优势会被稀释。但数据少的时候,DBN这套“先用无监督结构垫底、再补监督头”的做法,比拿一个小网络硬训要稳定得多。
4. 微调阶段:从生成式约束到判别式输出
预训练完成之后,DBN还不能直接做分类或回归,需要一步微调,把它变成标准前馈判别网络。
4.1 网络展开与确定性映射
“展开”这个操作听起来抽象,其实就两件事。
第一,把概率采样替换成确定性激活。预训练阶段隐藏单元的激活方式是随机伯努利采样,微调阶段直接令h_j = σ(...),去掉随机性。
第二,把层与层之间的权重组成普通前馈路径。展开之后,DBN在结构上就等价于一个多层感知机:每层是仿射变换加激活,输出层按任务换成softmax或线性输出。
到这一步,顶层原来的无向连接也无需继续保留,退化成普通权重矩阵。只需要把它接成前馈图,监督信号就能通过反向传播流向所有层。
4.2 反向传播微调和权重层级的衔接
微调训练用的是最常见的交叉熵损失加反向传播。有一个操作细节特别值得强调:微调的学习率往往要比预训练阶段更小,优化器也建议从朴素SGD换成带动量版本或自适应方法。
复现DBN翻车的原因,很多都是预训练权重已经被调到了不错的状态,结果刚进入微调阶段,梯度较大,又沿用预训练的大学习率,几步更新就把预训练结构破坏了。这像房间里的家具刚摆放好,你用一台大功率吸尘器横扫全屋,反而把整齐的摆设撞得乱七八糟。稳妥做法是先用一个比较小的学习率跑几个epoch,看验证指标是在上升还是下降,再逐步调整。
4.3 DBN真正值得保留的技能点
单论最终准确率,今天的很多任务上DBN确实不占优势。但它的微调流程里藏着一个至今依然好用的通用套路:先无监督学习结构,再小步慢调做监督对齐。这套流程后来被各种“预训练+微调”框架重复了无数次。你在自然语言处理里看到的大规模预训练之后再做下游任务适配,在图像领域看到的自监督预训练再finetune,本质上和DBN先堆RBM学特征、再反向传播对齐标签是一个思路。
所以学DBN最值得带走的,其实是这套分工逻辑:无监督阶段只管数据内在结构,监督阶段只管任务对齐。这个心智模型远比具体网络结构耐用。
5. 用自动微分框架实现一个手写数字识别DBN
理论说完了,落地最要紧。下面给出一套可运行的最小实现思路,数据集用手写数字这类公开图像数据。
这里我不绑定某个具体深度学习框架的API,只保留核心逻辑。RBM没有现成模块,所有代码基本都要自己写参数更新。
5.1 数据准备与预处理
对Bernoulli版本的RBM来说,可见单元的输入分布很重要。手写数字图通常是0到255灰度值,而毫尔伯努利可见单元假设输入是二值的。实践中最简单的方法是直接把像素归一化到[0,1],训练时按概率处理为0到1之间的浮点值;也可以显式二值化,把每个像素按阈值切成0/1。两种方式都能用,归一化方式一般效果更稳。
如果你处理的是连续型数据,比如音频特征或高维稠密向量,建议换成高斯RBM,那是另一套能量函数,原理类似但细节不同,不要直接照搬Bernoulli版本。
5.2 RBM核心类与CD步
RBM核心就三部分:权重矩阵W、可见偏置v_bias、隐藏偏置h_bias,外加一套交替采样逻辑。示意代码如下:
import math def sigmoid(x): return 1.0 / (1.0 + math.exp(-x)) class RBM: def __init__(self, n_vis, n_hid): self.W = std_randn((n_vis, n_hid)) * 0.01 self.v_bias = zeros(n_vis) self.h_bias = zeros(n_hid) def sample_h(self, v): h_prob = sigmoid(v @ self.W + self.h_bias) # 按概率采样伯努利值,同时返回概率用于后续更新 return h_prob, bernoulli_sample(h_prob) def sample_v(self, h): v_prob = sigmoid(h @ self.W.t() + self.v_bias) return v_prob, bernoulli_sample(v_prob) def cd_update(self, v0, k=1, lr=0.01): h_cur = v0 for _ in range(k): h_prob, _ = self.sample_h(h_cur) _, h_cur = self.sample_v(h_prob) h_prob0 = sigmoid(v0 @ self.W + self.h_bias) h_probk = sigmoid(h_cur @ self.W + self.h_bias) positive = v0.t() @ h_prob0 negative = h_cur.t() @ h_probk grad_w = (positive - negative) / v0.size(0) grad_v = mean(v0 - h_cur, dim=0) grad_h = mean(h_prob0 - h_probk, dim=0) self.W += lr * grad_w self.v_bias += lr * grad_v self.h_bias += lr * grad_h这里的std_randn、bernoulli_sample只是示意符号。在用自动微分框架实现时,把它们替换成对应的随机初始化和伯努利采样API即可。CD步数k设成1是最常见的起点,多数任务上效果已经不错,更大的k未必带来更快的收敛。
5.3 堆叠为DBN并微调
训练完第一层RBM后,把所有样本输入到第一层,得到隐藏概率h1。然后用h1作为下一层RBM的可见输入继续训练。
rbm1 = RBM(784, 500) # 用原始特征训练rbm1若干轮,得到h1 rbm2 = RBM(500, 200) # 用h1训练rbm2若干轮 # 微调:把两层展开成784-500-200-10的MLP # 保留rbm1.W、rbm2.W作为初始权重,末端接softmax # 使用较小学习率做交叉熵训练看起来和普通多层感知机没什么区别,关键差异在初始权重:普通MLP直接随机初始化,这里用的是经过无监督预训练的权重。微调时只随机初始化最后一层分类权重,其它层沿用预训练权重,然后用较小学习率跑交叉熵损失。
这里有一个实操建议:预训练过程中,向下一层喂数据时尽量用隐藏概率而不是伯努利样本。采样会放大随机噪声,概率版本相当于做了一次平滑,训练更平稳。
6. 实际使用中的参数权衡与DBN的现代定位
能跑通只是第一步,真正把DBN调好,还要理解几个容易翻车的参数,以及它和现代模型之间的关系。
6.1 学习率、批大小、CD步数对结果的影响
对比散度训练过程里没有平稳的loss曲线,最直观的监控指标是重建误差:把原始输入送入RBM,得到重建结果,然后计算两者差的平方和。重建误差在下降,说明模型正在捕捉数据分布。
学习率是最常见的翻车点。CD梯度本身就是近似量,如果学习率太高,近似误差会被逐轮放大,很快就出现重建误差骤升甚至数值爆炸。我通常把RBM学习率放在0.005到0.05之间,手写数字这样的小型数据取0.01比较稳。
批大小也影响很大。批次太小时正相和负相估计方差变大,CD更新方向不稳定;批次太大又会让单次更新变慢,整体收敛效率不高。图像数据上128到256是比较常见的选择。
至于CD步数,CD-1几乎总是首选。更多步数理论上让负相更接近真实模型分布,但计算量涨了,收敛速度并不会变快。除非你明确要做更好的生成采样器,否则不必刻意用更长的链。
6.2 和CNN、自动编码器怎么选
把DBN放到现在的工具箱里,必须承认它既不是最快的解法,也不是精度最高的解法。选择哪种模型,取决于你的具体任务:
- 图像分类且标签量充足,直接上CNN或完整的端到端网络。预训练优势会被大量数据稀释,没必要额外增加复杂度。
- 想提取无监督表征且数据规模不大,自动编码器比DBN更容易训练。DBN有更强的概率生成背景,但参数多、调参难度大。
- 输入是高维稀疏向量,比如行为日志、点击序列,RBM类模型曾经是经典选项,今天也可以用分解机或序列模型替代。
6.3 适合DBN的场景
如果让我给一个当前环境下是否值得投入DBN的判断,我会分三种情况:
第一类是学习和理解深度学习历史,那无论如何都值得亲手写一遍RBM和DBN,直觉训练会有很大提升。
第二类是小样本问题。只有几千条训练样本时,无监督预训练提供的初始化价值非常明显。这时候DBN或者类似的预训练加微调流程,往往能比从零开始硬训的深层网络表现更好。
第三类是明确需要概率模型和不确定性的场景。如果你不只是要特征还要可解释生成过程,DBN这类带能量建模的模型仍有参考意义。
调了这么多轮DBN,我最深的感受是:它今天被取代的只是具体结构,而不是底层哲学。预训练加微调这个范式,或者“先学统计结构,再做任务对齐”的思路,现在依然在几乎所有主流模型里延续着。如果你想在深度学习里建立更完整的坐标系,在第12章这种位置认真停留一下,绝对值回票价。