1. 项目概述:为什么第12章值得认真读
1.1 深度学习历史中的特殊坐标
深度信念网络(Deep Belief Network,DBN)在很多教材里都被放在第十二章——这个位置挺微妙的,前面是各种基础网络结构,后面多半是更热门的卷积网络和循环网络,于是DBN经常沦为“被跳过的第十二章”。我自己最早学的时候也是这样,觉得它又啰嗦又不管用,直到后来做无监督特征提取和缺失数据补全,才发现当初跳过的部分,恰恰是理解“预训练为什么有效”的钥匙。
我先把结论放在前面:DBN是深度学习历史上第一个被证明可以用无监督方式有效预训练深层的模型。它由多个受限玻尔兹曼机(RBM)堆叠而成,训练过程分成互相关联的两个阶段——先逐层无监督预训练,再整体有监督微调。这就是后来在各种大规模模型里反复出现的“预训练-微调”范式的原型。
哪个阶段更重要?我的理解是预训练阶段。因为微调本质上还是用反向传播做梯度下降,方向大家都能想到。DBN真正的贡献在于:它证明了当网络足够深时,先用无监督学习把每一层初始化好,后面的有监督训练才走得动。在2006年之前,这是没人系统解决的难题。
如果你正在跟着教材学深度学习,这一章能帮你建立“模型要分几步训练”的全局观;如果是其他方向的工程师,读完这部分也够你判断哪些问题适合用DBN来处理。它的结构并不复杂,但背后的能量模型、采样算法这些思想,放到今天的生成模型里依然能对上号。
1.2 第12章的内容主线:预训练与微调
我习惯把这一章的思路压缩成两条线。
第一条线是逐层无监督预训练。输入数据之所以能一层一层“往上走”,是因为每一层都先训练一个RBM:可见层接收输入,隐藏层抽取特征;然后把这一层的隐藏输出当成下一层的可见输入继续训练。这个过程不需要标签,所以可以大量吃未标注数据。它的目标是让网络在见到标签之前,就已经学会数据自身的统计规律。
第二条线是有监督微调。预训练结束后,在最顶层接上适应具体任务的输出层,比如分类就加softmax,然后用带标签数据对整个网络反向传播微调。这时候的学习率和训练策略要保守很多,因为网络已经处在一个相对好的局部区域,大步修改反而容易破坏学到的特征。
为什么要拆成两步走而不是一步到位?这里有个直觉上的类比:预训练相当于先让一个新人熟悉行业里的大量案例,建立起基本功;微调相当于在有明确目标之后,针对性地调整打法。而没有预训练直接上反向传播的深层网络,当年最大的问题是梯度消失——信号传到靠近输入层的部分时已经弱到基本不更新了。DBN先用逐层无监督方式把这些层的权重放到一个合理区间,后面的反向传播才能起效。这一点我在后面会详细展开。
2. 核心原理拆解:RBM与逐层预训练
2.1 受限玻尔兹曼机:DBN的基础积木
要说DBN,绕不开受限玻尔兹曼机RBM。它本质上是一个双层无向概率图模型,一层叫可见层,一层叫隐藏层。可见层对应观测数据,隐藏层负责提取隐含特征。所谓“受限”,指的就是层内神经元之间没有连接,只有层与层之间的全连接。这个限制让计算变得可行,也是后面采样和训练的根基。
对比一下更早的玻尔兹曼机,那是一个层内也有连接的模型,训练复杂度极高,几乎没法在实际规模的数据上用。RBM把层内连接全部去掉之后,条件概率可以被分解成一系列独立的sigmoid函数,这才让高效采样和近似训练成为可能。可以说,“受限”两个字是这个模型能走出论文、进入工程实践的最关键设计。
RBM是能量模型。它不像普通神经网络那样直接定义输入到输出的映射,而是先给每一种可见层和隐藏层的组合状态定义一个能量值,再通过玻尔兹曼分布把能量值转换成概率。我用一个生活化类比帮你理解:你面前有一杯水,它可以放在桌面上无数个位置,每个位置对应一个“能量”,系统倾向于停留在能量更低、更安稳的位置。模型把所有可能的状态都算一遍能量,能量低的状态出现概率就高。
具体来说,RBM的能量函数长这样:
E(v, h) = -Σ_i a_i v_i - Σ_j b_j h_j - Σ_i Σ_j v_i W_ij h_j
其中v是可见层状态,h是隐藏层状态,a是可见层偏置,b是隐藏层偏置,W是两层之间的权重矩阵。能量越小,代表这一组可见层和隐藏层组合越“协调”。实际项目中,如果你处理的是图像这类连续输入,通常会把数据归一化到0到1区间,并且根据任务决定是否二值化。RBM最原始的形式假设神经元是二值的,处理二值化数据时能最大程度贴合模型假设,训练也最稳定。
有了能量函数,联合概率分布就定义为P(v, h) = exp(-E(v, h)) / Z,这里的Z是配分函数,等于所有状态能量指数项的求和。Z的计算量随神经元数量指数增长,直接算完全不可能,这也是后面我们要用对比散度算法而不用最大似然的直接原因。
RBM最方便的一点是条件概率的形式非常干净。固定可见层时,隐藏层每个神经元独立的激活概率是sigmoid函数:
P(h_j = 1 | v) = sigmoid(b_j + Σ_i W_ij v_i)
反过来,固定隐藏层时,可见层每个神经元的激活概率为:
P(v_i = 1 | h) = sigmoid(a_i + Σ_j W_ij h_j)
正是因为这两个条件概率都是因子化的,我们才能用吉布斯采样去生成样本:先从一个可见层状态出发,采样隐藏层;再根据隐藏层采样可见层;如此反复,就能从模型分布中采样。这一套机制构成了对比散度算法的基础。理解到这里,DBN的地基就算是打完了。
2.2 训练RBM的对比散度算法:绕开配分函数
深度学习的常规套路是用最大似然估计训练模型,让模型给训练数据的概率尽量大。RBM的问题是配分函数Z太难算,梯度里包含对模型分布的期望,这个期望也没法直接求。
解决思路是近似。训练时用对比散度(Contrastive Divergence,简称CD)算法,最常用的是CD-1,也就是只用一步吉布斯采样。做法可以拆成几步:
- 取一批真实数据v0;
- 根据P(h|v0)采样隐藏状态h0;
- 根据P(v|h0)重建一个可见层概率分布v1,再从v1采样隐藏状态h1;
- 用“数据驱动的期望”减去“重建驱动的期望”来近似参数梯度: ΔW ≈ (v0^T h0 - v1^T h1) / N
其中N是批大小。这里的v1是模型“幻想”出来的数据,可能跟真实数据差很远,但研究表明只需一步采样,方向就已经足够好,多采样几步CD-k(比如k=5)对最终效果提升很小,训练时间却成倍增加。所以默认配置基本都是CD-1,除非你专门研究模型的采样质量。
参数更新时还会加动量和权重衰减。动量项让梯度的历史方向参与当前更新,效果类似于给参数更新加了惯性,能减少震荡、加速收敛。权重衰减给大权重做惩罚,防止过拟合。这两个在预训练阶段都建议开启。
为什么CD算法有效?我理解它的本质是:真实数据是模型最容易观测到的样本,而重建数据是模型自己幻想出来的样本。我们希望模型给真实数据的高概率越来越高于幻想数据,所以让两者产生的统计量差距逐步缩小。这个过程不需要精确计算Z,只需要局部对比就可以,这是很巧妙的工程近似。我第一次独立实现RBM的时候,最大的震撼就是发现那一行简单的减号,背后居然替代了指数级的精确计算。
如果你在实现时把重建可视作一次“预测”,那RBM某种程度上跟自编码器有点像。区别在于,自编码器是在学一个确定性的重构映射,而RBM学的是一个概率分布;自编码器的目标函数是重构误差,RBM的目标是让模型分布逼近数据分布。这两种思路在后续很多工作中都有继承,但底层逻辑完全不同。把这一点区分清楚,后面看深度玻尔兹曼机、变分自编码器这些模型时,脉络会清晰很多。
2.3 为什么逐层预训练能救深层网络
这里追一下梯度消失的问题。反向传播算法里,误差从输出层往回传,每一层都要经过激活函数的导数。sigmoid函数的导数最大值只有0.25,多层连乘之后,靠近输入层的梯度会迅速接近于零。网络深了之后,前面几层的参数几乎得不到更新,训练自然就停滞。早期大家尝试训练深层网络屡屡碰壁,根本原因就在这里。
DBN的解法很巧妙。它不直接去改反向传播公式,而是先把网络的每一层都单独训练成一个RBM。由于RBM的训练是局部的、无监督的,不依赖从顶层回传的梯度,所以每一层的参数从一开始就能根据数据学到有意义的结构。等所有层都初始化好了,再统一微调时,网络已经处在一个合理的起点附近,梯度消失问题大大缓解。
后来的研究也表明,换用ReLU激活、更好的权重初始化、批归一化、残差连接等手段,同样能够稳定训练深层网络,这导致逐层预训练在大多数现代监督任务里已经不是必需品。但要注意,“预训练”这个概念本身并没有消失,它只是从“逐层RBM初始化”演变成了更通用的“在大规模数据上先学习通用表示,再适配下游任务”。所以理解DBN,等于抓住了这条演变路线的源头。比如你在做自然语言任务时,先在海量文本上学语言模型,再到具体任务上微调,这种思路和DBN的哲学是一脉相承的。
3. 实操导向:网络设计与参数配置
3.1 层数和神经元数量怎么选
设计DBN时,网络结构的确定通常从数据规模和特征复杂程度出发。经验上,2到4层隐藏层就足够了,因为每加一层RBM,预训练时间和微调不稳定性都会同步上升。一个常见结构是神经元数量逐层递减,比如输入784维(28×28图像),后面接500、200、10这样的递减结构。这种结构强迫网络用更紧凑的表示去描述数据,有点类似瓶颈自编码器的思路。
但也不是所有场景都递减。如果是把DBN当成特征提取器来用,后一层神经元数量可以比前一层多,目的是把特征映射到更高维的稀疏空间,方便后续分类。这个选择取决于你下游任务想要的分辨率——压缩还是要更高维表达。在稀疏数据上,高维稀疏表示往往比低维稠密表示更好用,因为隐藏层有机会用不同单元分别响应不同模式的输入。
隐藏层初始权重一般用零均值、小方差的正态分布,方差取0.01左右比较稳。你别把权重的初始范围设太大,RBM的sigmoid激活很容易被推向饱和区,一旦饱和梯度就会消失,预训练就没法做了。隐藏层偏置通常初始化为0,可见层偏置可以初始化为训练数据的均值,这样第一个RBM在迭代开始时就有一个相对靠谱的基线。这个细节我最早没注意,结果同样数据量下前几十轮重构误差一直偏高,后来改成均值初始化,训练曲线明显平顺。
3.2 关键超参数:我的推荐区间
我在下表列一下常用的预训练超参数区间,都是实测比较稳定的取值。
| 参数 | 推荐区间 | 说明 | | 学习率 | 0.01~0.1 | 太大的话重构误差直接发散,太小收敛慢 | | 动量 | 0.5起步,后期到0.9 | 前期用低一点,训练几十轮后增加 | | 批大小 | 10~100 | 小批量更新更稳,不建议跑全批量 | | CD步数 | 1 | 默认版本,少用k=5以上 | | 权重衰减 | 1e-4~1e-3 | 防止预训练阶段过拟合 | | 权重初始化方差 | 0.01 | 配合sigmoid激活,别贪大 |
这些参数也受数据分布影响。比如二值化数据通常收敛比连续数据快,如果输入是0~1的连续值,建议在学习率上打个7折左右。训练过程中一定要盯住重构误差曲线:它应该在渐进下降,而不是震荡上升。如果发现曲线在某个轮次后反弹得很剧烈,多半是学习率偏大或者动量过早调高,退回上一个稳定点重新跑。
微调阶段的学习率通常是预训练阶段的十分之一到五分之一,因为预训练已经把参数放到了不错的区域,微调只需要小幅修正。如果你想让微调更稳,可以配合学习率衰减策略,每若干轮除以2,直到损失曲线平稳。注意微调轮数不用很长,通常几十轮就会收敛,拖太久反而会把预训练学到的通用特征冲淡。
3.3 一个可运行的RBM实现思路
下面给一个基于数组运算的简洁RBM实现,包含了CD-1的核心训练循环。它的作用是让你直观看到:RBM训练主要就是“数据—采样隐藏—重建—再采样—更新参数”五件事。
import numpy as np class RBM: def __init__(self, n_visible, n_hidden, lr=0.01, momentum=0.5): self.W = np.random.normal(0, 0.01, (n_visible, n_hidden)) self.v_bias = np.zeros(n_visible) self.h_bias = np.zeros(n_hidden) self.lr = lr self.momentum = momentum self.vW = np.zeros_like(self.W) self.vvb = np.zeros_like(self.v_bias) self.vhb = np.zeros_like(self.h_bias) def sample_h(self, v): p_h = 1 / (1 + np.exp(-(v @ self.W + self.h_bias))) return p_h, np.random.binomial(1, p_h) def sample_v(self, h): p_v = 1 / (1 + np.exp(-(h @ self.W.T + self.v_bias))) return p_v, np.random.binomial(1, p_v) def cd_update(self, batch): v0 = batch _, h0 = self.sample_h(v0) p_v, _ = self.sample_v(h0) _, h1 = self.sample_h(p_v) positive = v0.T @ h0 negative = p_v.T @ h1 self.vW = self.momentum * self.vW + self.lr * (positive - negative) / len(batch) self.W += self.vW self.vvb = self.momentum * self.vvb + self.lr * (v0 - p_v).mean(axis=0) self.v_bias += self.vvb self.vhb = self.momentum * self.vhb + self.lr * (h0 - h1).mean(axis=0) self.h_bias += self.vhb return np.mean(np.sum((v0 - p_v) ** 2, axis=1))代码里sampling用的是二值采样。实际项目中计算p_v后,可以用概率值或采样值两种方式做重建,我建议在CD更新里用概率值计算重建项,采样值用于下一轮采样,这样能在稳定性和随机性之间取得平衡。完整的DBN训练就是依次训练第一个RBM,把它的隐藏层输出交给第二个RBM,再训练第二个,逐层往复。最顶层的RBM隐藏输出再接分类器或下游任务即可。一个完整的DBN堆叠类只需要维护一个RBM列表,训练时不断把当前输入传入forward函数取隐藏概率,下一层用这个概率值继续,逻辑上非常直接。
4. 从预训练到微调:完整应用链路
4.1 微调的两种路线
预训练完成之后,微调方式取决于你有多少带标签数据。
如果你手里的标签数据比较充足,可以把整个网络都放开,在预训练参数基础上做全网络反向传播。这相当于把预训练当成了初始化技巧。优点是可以让所有层都为了目标任务再调整,效果上限更高;缺点是数据不足时容易破坏预训练学到的通用特征。
如果标签数据很少,比如只有几千条,我建议只微调最后新加的输出层,前面几层保持预训练参数冻结。这样既利用了预训练的特征提取能力,又减少了需要学习的参数数量,过拟合风险小很多。中间还有一种折中:前几层冻结,只微调最后两三层,适合标签量介中的场景。具体选哪种,最直接的办法是拿验证集各跑一遍看效果,不要拍脑袋。
微调阶段的输出层选择标准跟普通网络完全一样:分类用softmax加交叉熵损失,回归用线性输出加均方误差损失,排序任务可以用成对损失。需要注意的是,顶层输出层的权重不要继承预训练的数值,要用随机初始化,因为预训练时的顶层隐藏单元和输出任务没有对应关系。实际操作里,我见过有人图省事直接把预训练隐藏层输出接上分类权重就开始训练,结果验证集损失迟迟下不去,就是因为新加层和预训练层的学习节奏没有协调好,分开设置独立学习率会缓解这个问题。
4.2 DBN在各场景中的适用性
DBN用在什么地方最顺手?我列举几个典型场景。
第一个是特征提取和降维。图像输入先经过DBN的预训练,得到中间某一层的隐藏层表示,再用这个表示做分类或聚类。因为预训练让隐藏层捕捉到了数据的统计结构,这种特征往往比原始像素更稳定,也更适合下游算法。尤其在标签很少的数据集上,先用DBN在无标签数据上学表示,再在小样本上训练一个简单分类器,比直接用深层网络端到端训练要稳得多。
第二个是协同过滤和推荐。RBM天然适合处理二值化的用户行为数据,比如“看过/没看过”、“点击/没点击”。把用户行为向量作为可见层,隐藏层就能编码出隐式偏好,这在推荐场景里很常见,尤其是稀疏数据下训练效果比纯矩阵分解更灵活。我见过一个模拟场景:用户对商品的评分数据缺失率很高,用DBN建模后,模型能把缺失位置重构出合理的概率分布,直接作为推荐依据,效果比简单均值填充好不少。
第三个是缺失数据补全和生成样例。因为DBN模型本身建模了P(v, h),所以它能回答“某些可见节点未知时,其他节点的合理分布是什么”,也能通过吉布斯采样生成新的样本。这在数据有大量缺失的业务场景里很实用,算是一种早期生成式模型的实际落地方式。相比用自编码器补全数据,DBN补全的结果天然带有概率分布信息,而不只是一个确定性的重构值,这在后续需要评估不确定性的任务里很有价值。
需要坦白的是,在标准图像分类、目标检测这类任务上,DBN已经被卷积网络以及各种现代结构全面超越。你不用指望它能刷榜。它的价值在于无监督表征、稀疏数据建模,以及帮你理解生成式模型的底层机制。如果你要做的是高性能端到端识别,直接走向监督训练的现代结构效率更高。
5. 常见问题与排查技巧实录
5.1 预训练损失发散,甚至出现NaN
这个问题我遇到得最多,原因通常有三个:学习率太大、输入数据没有归一化、权重初始范围过大。
排查方式很简单。先看数据,确认输入在0到1之间且保持合理的稀疏度;再把学习率降到0.001级别试跑几十轮;最后翻一下权重初始化的标准差,不要超过0.05。如果你已经跑到一半才发现损失飞到天上,那就直接把参数重置从头开始,别指望损失自己降回来。
还有一个细节容易被忽略:可见层的偏置如果初始化为0,而数据的均值明显偏离0,第一个RBM可能迟迟找不到合理的激活基线。解决办法是把可见层偏置初始化为训练数据的按特征均值,这会让预训练起点明显更稳。我最早跑连续值数据时吃过这个亏,换了均值初始化之后,同样的学习率下重构误差第一次迭代就降了将近三分之一。如果你用的是框架内置的自动初始化,务必手动覆盖这个偏置项。
5.2 预训练跑完了,精度反而变差了
如果预训练后的模型在监督任务上的表现还不如不用预训练,先别急着怀疑模型,重点检查两件事。
第一是预训练的数据分布和微调的数据分布是否一致。比如你拿大量无标签购物数据预训练,却拿少量图像数据微调,那预训练学到的特征对任务毫无帮助,甚至起反作用。预训练和微调的数据应该来自同一个领域,这是基本前提。实际中不少人栽在这个地方,因为“无标签数据好找”不等于“无标签数据有用”。
第二是微调策略是否过于激进。有标签数据很少时,全网络微调很容易把预训练好不容易学到的通用特征忘掉,效果还不如只微调输出层。建议先用“只训练输出层”做基线,再逐步放开更多层对比,找到最合适的放开程度。我常用的做法是做一个简单实验矩阵:固定微调层数从0到全部,每两个层级取一个点,画出验证集损失曲线,选择拐点位置作为最终方案。这样做看起来费功夫,但能省下后面反复重训的时间。
5.3 网络越深效果越差,是不是DBN不行
这通常是层数选址的锅。DBN的预训练不是叠得越多越好,层数过多时,前面的RBM堆叠误差会逐层累积,后面的预训练本身就不稳定。经验上三到四层足够捕捉大多数数据规律,再多就要考虑增加数据和训练轮次,否则收益会被噪声淹没。
另外也要检查是不是隐藏层单元数设得太宽或太窄。太宽容易过拟合训练集,太窄则表达力不足。一个平衡做法是做一个简单的主成分分析对比:DBN中间层的输出,和PCA降维后的表示做一次简单聚类对比,如果DBN特征甚至不如PCA,大概率是预训练没有训练充分。这种情况可以把预训练轮数翻倍,或者减少层数,重新观察重构误差是否还能继续下降。如果重构误差已经收敛但特征依然不好,那就是结构设计问题,该调神经元数量而不是继续加层。
5.4 DBN与常见模型的横向定位
这里做一张快速对照表,方便你理解DBN和其他技术的差异。
| 模型 | 训练模式 | 核心目标 | 典型用途 | | DBN | 逐层预训练+微调 | 建模数据分布 | 特征提取、生成、协同过滤 | | 自编码器 | 单步或逐层预训练 | 重构输入 | 降维、异常检测 | | 卷积网络 | 端到端监督训练 | 最小化任务损失 | 图像分类检测 | | Transformer | 大规模预训练+微调 | 建模序列分布 | 语言理解生成 |
这张表能解释一个现象:为什么DBN看起来过时,但很多现代模型的架构里依然能看到它的影子。它开创的“先学分布、再做任务”的思路,本质上和现代大规模预训练模型是同一个哲学,只是实现工具和规模都大幅升级了。
DBN后面的发展也有细节值得留意。比如深度玻尔兹曼机(DBM)尝试把整个结构都变成无向图模型,训练更对称但难度也更高;基于RBM的协同过滤还在某些稀疏评分场景里使用,说明这类模型并没有被完全淘汰。如果你学完这一章之后想去探索相关方向,从DBM入手会比较顺。两者的关键区别在于DBN的顶层是一个无向连接的RBM,而其余层之间的连接方向是从输入向更高层传递的;DBM则让所有层之间都保持对称连接。这个结构差异听起来不大,但直接改变了训练时的采样方式和梯度估计方式,DBM通常需要使用平均场推断来近似条件概率,工程复杂度高出一截。
我最后讲一个实操习惯:训练DBN时,把重构误差、验证集损失和采样重建图三个指标放在一起盯。重构误差管着预训练是否在收敛,验证集损失管着微调是否有效,采样重建图可以帮你直观判断模型有没有学到数据的粗糙结构。如果三者方向一致,基本可以判断流程没问题;如果出现矛盾,优先信验证集损失,再回头调预训练。比如某个配置下重构误差很低,但验证集损失一直下不来,这往往说明预训练阶段过拟合了训练集,需要增加权重衰减或减少隐藏单元数量,而不是继续加大训练轮数。
这一章确实不算时髦,但它是理解整个深度学习范式的关键拼图。我把这套笔记整理出来,也是希望后来的人少走一点我当年绕过的弯路——至少下次再看到“预训练”这三个字时,你能想起一个叫深度信念网络的模型,是如何用一堆受限玻尔兹曼机,把这件事第一次做成的。