ARTICLE DETAIL

资讯详情

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

深度学习算法原理及应用:RBM与自动编码机实战解析

深度学习算法原理及应用:RBM与自动编码机实战解析 简介这份PDF资料面向机器学习初学者与需要梳理深度学习理论脉络的读者系统讲解深度学习算法的基本原理与典型应用帮助建立从神经网络基础结构到训练过程的完整认知。资源包内仅含1个PDF文件约496KB内容为期刊论文形式涵盖输入层、隐藏层与输出层的结构说明前向传播与反向传播的训练机制以及受限玻尔兹曼机作为单层网络基本结构的原理。文中以自动编码机为例展示其通过重建输入数据实现降维与特征表示并在手写数字识别任务上取得显著性能提升同时客观讨论了训练缓慢、参数易收敛于局部最优点等局限并展望了优化算法、新型网络结构与扩展应用领域等方向。目前已有94人学习适合作为课程学习、论文写作或算法入门阶段的参考材料。1. 从一篇 2015 年的论文说起深度学习算法的原理及应用.pdf 到底能给你什么如果你手头正缺一份能把受限波尔兹曼机、自动编码机和手写数字识别串起来讲清楚的入门材料这份《深度学习算法的原理及应用.pdf》值得放进你的资料库。它是一篇 2015 年发表的中文期刊论文作者来自西安通信学院和陕西师范大学教育学院全文只有三页却把深度学习最核心的两条线索——RBM 的单层结构和自动编码机的逐层堆叠——用公式和实验数据讲明白了。适合谁看刚接触神经网络算法、想搞懂“为什么深层网络要用逐层无监督预训练”这个问题的学生和转行工程师也适合已经会调库、但说不清 RBM 能量函数和对比散度更新规则的人拿来补理论底子。它不教你写代码但它把“为什么这样设计”讲透了这正是很多速成教程缺的那一环。2. 拆开这篇论文的技术骨架RBM 能量函数与对比散度到底在算什么2.1 为什么深层网络不能直接上 BP梯度消失与局部最优的血泪经验论文开篇就点了一个关键问题多隐层结构的网络用普通 BP 算法很难奏效。原因有三条每一条都值得展开想。第一样本数据量要求大2015 年那会儿标注数据不像现在这么容易获取MNIST 的 6 万张训练图在当时已经算“大量”了。第二训练过程缓慢BP 算法每一轮都要从输出层把误差反传到底层层数一多梯度在传递过程中不断衰减到底层时已经接近于零权值几乎不更新。第三参数容易收敛在局部最优点而不是全局最优点实用意义不强。Hinton 在 2006 年给出的方案是不要一上来就端到端训练而是先用无监督方式逐层训练每一层 RBM把每一层的权值初始化到一个比较好的位置然后再用少量标注样本做微调。这个思路在当时是反直觉的——大家习惯了直接优化最终目标突然有人说“先别管最终目标先把每一层自己的重构做好”但实验证明它有效。论文里把深度学习的三个环节拆得很清楚无监督逐层提炼特征、有监督调准分类边界、用未见过的样本测试泛化能力。这三个环节对应到工程上就是预训练、微调、验证集评估。我一般会跟刚入门的人说你可以把逐层预训练理解成“让每一层先学会自己那一小段活”底层学边缘和纹理中层学部件高层学整体形状最后微调阶段再告诉整个网络“我们要区分的是 0 到 9 这十个类别”。这样每一层都不至于从随机初始化的状态开始瞎撞。2.2 RBM 的联合概率分布与条件概率公式 (1) 到 (3) 的工程含义论文第 2 节给出了 RBM 的核心公式。假设一个 RBM 包含 n 个可视单元和 m 个隐藏单元用向量 v 和 h 分别表示可视单元和隐藏单元的状态。对于一组给定的状态 (v, h)RBM 作为一个系统的联合概率分布用能量函数表示为E(v, h) -Σ a_i v_i - Σ b_j h_j - Σ v_i h_j w_ij (1)其中 w_ij 表示可视单元 i 与隐藏单元 j 之间的连接强度a_i 表示可视单元 i 的偏置b_j 表示隐藏单元 j 的偏置。学习 RBM 的任务就是求出这些参数的值以拟合给定的训练数据。这个能量函数的形式借鉴了统计物理里的玻尔兹曼分布能量越低的状态概率越高。工程上你不需要手算这个公式但你需要理解它的含义w_ij 越大可视单元 i 和隐藏单元 j 同时激活时能量越低这个状态就越容易被系统“选中”。偏置项 a_i 和 b_j 则分别控制单个单元激活的难易程度。给定一组随机选取的训练数据由于隐单元的条件概率值只取决于显单元的分布隐单元的条件概率为P(h_j 1 | v) σ(Σ_i w_ij v_i b_j) (2)同理显单元的条件概率为P(v_i 1 | h) σ(Σ_j w_ij h_j a_i) (3)其中 σ(·) 为 sigmoid 函数定义为 σ(x) 1 / (1 exp(-x))。这两个条件概率公式是 RBM 做吉布斯采样的基础。实际训练时你不需要真的跑很多步吉布斯采样Hinton 提出的对比散度算法只用一步采样就能得到足够好的梯度估计。论文里权值更新准则写为Δw_ij ε(v_i h_j_data - v_i h_j_recon) (4)其中 ε 是学习率·_data 是输入数据的期望值·_recon 是重构数据的期望值。这个公式的含义很直白如果某个连接在输入数据里经常同时激活但在重构数据里不常同时激活就增大这个连接的权重反之则减小。最小化这个差值可以使 RBM 的隐层更好地提取显层输入的本质特征。注意公式 (4) 里的学习率 ε 在工程实现中通常取 0.01 到 0.1 之间太大容易震荡太小收敛慢。另外对比散度只跑一步吉布斯采样时重构期望的估计是有偏的但实践中效果足够好。2.3 从 RBM 到自动编码机5 层网络 784-1000-500-250-30-10 的搭建逻辑论文第 3 节给出了一个具体的实验配置用 5 层自动编码机对 MNIST 手写数字进行分类。输入是 28×28 的像素图转换成 1×784 的行向量所以显层有 784 个单元。隐层分别是 1000、500、250、30、10一共四层隐层加一个输出层。原始像素强度介于 0 到 255 之间训练前转化为 0 到 1 之间的灰度值。这个结构的设计逻辑是逐层降维784 维的原始像素先扩展到 1000 维再压缩到 500、250、30最后到 10 维对应 0 到 9 十个类别。第一层从 784 到 1000 是升维目的是让 RBM 有足够的容量去捕捉像素之间的高阶相关性后面几层逐步降维每一层都在前一层的表示基础上提取更抽象的特征。30 维那一层是整个网络的“瓶颈层”它被迫用极低的维度表示一张手写数字图这相当于一种有损压缩但压缩后的表示反而对分类更有用因为它丢掉了像素级的噪声保留了类别相关的结构信息。自动编码机的工作流程是输入数据经过多层 RBM 使高维数据变成低维数据完成编码过程产生中间的代码层然后从代码层开始逆向使用编码过程产生的各层参数重构出输入数据完成解码工作。整个网络通过最小化输入数据与重构数据的交叉熵完成对模型参数的微调使得中间代码层输出更本质的特征。我一般会提醒这个 784-1000-500-250-30-10 的结构不是唯一解论文里也说了“各层的输入输出维数以及深层网络的层数都可以根据需要自行指定没有特别约束”。但如果你要复现这个实验建议先按这个配置跑通再尝试调整。比如把第一层从 1000 改成 800 或 1200观察重构误差和分类准确率的变化这样你能直观感受到容量对表示能力的影响。3. 把论文里的公式落成代码RBM 单层训练与自动编码机堆叠的实操步骤3.1 用 NumPy 实现一个最小可跑的 RBM 单层论文没有给代码但公式 (2)(3)(4) 已经足够你写出一个最小实现。下面这个 RBM 类只依赖 NumPy适合拿来理解对比散度的每一步在做什么。import numpy as np class RBM: def __init__(self, n_visible, n_hidden, lr0.1): # 权值初始化小随机数避免所有隐藏单元学出相同特征 self.W np.random.normal(0, 0.01, (n_visible, n_hidden)) self.a np.zeros(n_visible) # 可视层偏置 self.b np.zeros(n_hidden) # 隐藏层偏置 self.lr lr def sigmoid(self, x): return 1.0 / (1.0 np.exp(-x)) def sample_h(self, v): # 公式 (2)给定可视层计算隐藏层激活概率 p_h self.sigmoid(np.dot(v, self.W) self.b) return p_h, (np.random.rand(*p_h.shape) p_h).astype(np.float32) def sample_v(self, h): # 公式 (3)给定隐藏层计算可视层激活概率 p_v self.sigmoid(np.dot(h, self.W.T) self.a) return p_v, (np.random.rand(*p_v.shape) p_v).astype(np.float32) def contrastive_divergence(self, v0): # 正向从输入数据采样隐藏层 p_h0, h0 self.sample_h(v0) # 重构从隐藏层采样可视层再采样隐藏层 p_v1, v1 self.sample_v(h0) p_h1, h1 self.sample_h(v1) # 公式 (4)用数据期望和重构期望的差值更新权值 positive np.dot(v0.T, p_h0) negative np.dot(v1.T, p_h1) self.W self.lr * (positive - negative) / v0.shape[0] self.a self.lr * np.mean(v0 - v1, axis0) self.b self.lr * np.mean(p_h0 - p_h1, axis0) return np.mean((v0 - p_v1) ** 2) # 返回重构误差这段代码里最关键的是contrastive_divergence方法。它先做一次正向采样得到p_h0和h0然后从h0重构出v1再从v1采样出p_h1和h1。权值更新用的是positive - negative其中positive是输入数据下的 v-h 相关性negative是重构数据下的 v-h 相关性。学习率lr控制每次更新的步长v0.shape[0]是 batch size除以它是为了对 batch 内的梯度取平均。参数说明n_visible和n_hidden分别对应论文里的 n 和 m。W的形状是 (n_visible, n_hidden)初始化用标准差 0.01 的正态分布这是常见做法目的是打破隐藏单元之间的对称性。偏置a和b初始化为零。重构误差用均方误差衡量训练过程中观察它是否下降如果震荡或上升先把学习率调小。3.2 逐层堆叠把多个 RBM 串成自动编码机的训练循环有了单层 RBM堆叠成自动编码机的逻辑就是第一层 RBM 训练好后把它的隐藏层激活值作为第二层 RBM 的输入依次类推。下面这个DeepAutoencoder类展示了训练循环的骨架。class DeepAutoencoder: def __init__(self, layer_sizes, lr0.1): # layer_sizes 例如 [784, 1000, 500, 250, 30, 10] self.rbms [] for i in range(len(layer_sizes) - 1): self.rbms.append(RBM(layer_sizes[i], layer_sizes[i1], lr)) def pretrain(self, X, epochs10, batch_size64): # 逐层无监督预训练 input_data X for idx, rbm in enumerate(self.rbms): print(f训练第 {idx1} 层 RBM输入维度 {input_data.shape[1]}) for epoch in range(epochs): # 每个 epoch 打乱数据 perm np.random.permutation(input_data.shape[0]) for i in range(0, input_data.shape[0], batch_size): batch input_data[perm[i:ibatch_size]] rbm.contrastive_divergence(batch) # 用当前 RBM 的隐藏层激活作为下一层的输入 input_data rbm.sigmoid(np.dot(input_data, rbm.W) rbm.b) def encode(self, X): # 编码过程逐层前向传播到瓶颈层 h X for rbm in self.rbms: h rbm.sigmoid(np.dot(h, rbm.W) rbm.b) return hpretrain方法里的关键步骤是每训练完一层 RBM就用它的隐藏层激活值替换原始输入作为下一层 RBM 的输入。注意这里用的是sigmoid后的概率值而不是二值采样结果因为概率值保留了更多信息训练更稳定。encode方法把输入逐层传递到最后一层得到的h就是瓶颈层的表示。参数说明layer_sizes按论文里的 784-1000-500-250-30-10 传入。epochs控制每层 RBM 的训练轮数论文没有给具体值常见做法是 10 到 50 之间看重构误差收敛情况。batch_size取 64 或 128 都可以太小梯度噪声大太大内存吃紧。预训练完成后你可以在瓶颈层后面接一个 softmax 分类器用标注数据做微调这就是论文里说的“调准”环节。提示预训练阶段不需要标签MNIST 的 6 万张训练图可以全部用上。微调阶段才需要标签而且可以用比预训练更小的学习率比如 0.01 或 0.001。3.3 手写数字识别的验证流程从 784 维像素到 10 类输出的完整链路论文里提到“将数据库中的手写数字图像转换为 1×784 的行向量”和“像素强度 0 到 255 转化为 0 到 1”这两步是预处理的核心。下面这段代码展示了从原始 MNIST 数据到自动编码机输入的完整流程以及如何评估分类准确率。from sklearn.datasets import fetch_openml from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score # 加载 MNIST 数据集 mnist fetch_openml(mnist_784, version1, as_frameFalse) X, y mnist.data, mnist.target.astype(int) # 预处理像素值归一化到 0 到 1 X X / 255.0 # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size10000, random_state42 ) # 逐层预训练自动编码机 ae DeepAutoencoder([784, 1000, 500, 250, 30, 10], lr0.1) ae.pretrain(X_train, epochs15, batch_size64) # 用瓶颈层表示训练分类器 train_codes ae.encode(X_train) test_codes ae.encode(X_test) clf LogisticRegression(max_iter1000) clf.fit(train_codes, y_train) y_pred clf.predict(test_codes) print(f测试集准确率: {accuracy_score(y_test, y_pred):.4f})这段代码的逻辑是先用无标签的训练数据逐层预训练自动编码机然后把训练好的编码器拿来提取瓶颈层特征最后在瓶颈层特征上训练一个逻辑回归分类器。论文里说自动编码机在手写数字识别上“取得了令人惊异的进展”这个流程就是复现那个进展的最小路径。参数说明test_size10000对应 MNIST 标准的测试集大小。epochs15是预训练轮数你可以根据重构误差曲线调整。LogisticRegression的max_iter1000是为了保证收敛因为 30 维特征的分类问题不算太简单。如果你跑出来的准确率在 95% 以上说明预训练确实学到了有用的表示如果只有 90% 左右可能是预训练不够充分或者瓶颈层维度太小。4. 避坑与排查复现这篇论文时最容易翻车的五个地方4.1 现象重构误差一直不下降RBM 学出来的隐藏层激活值几乎全为 0 或全为 1原因通常是学习率太大或者权值初始化范围不对。学习率太大时权值更新步长过大能量函数在参数空间里来回震荡无法收敛。权值初始化如果用了标准差 1.0 的正态分布初始能量就很高sigmoid 函数进入饱和区梯度接近于零。解决把学习率降到 0.01 到 0.05 之间权值初始化改用标准差 0.01 的正态分布。另外检查输入数据是否已经归一化到 0 到 1如果输入还是 0 到 255可视层偏置的更新会被大数值主导隐藏层激活值也会饱和。4.2 现象逐层预训练完成后微调阶段准确率反而比直接用原始像素训练的逻辑回归还低原因是预训练学到的表示可能过拟合了重构任务而不是分类任务。RBM 优化的是重构误差它保留的是对重构有用的信息但有些对重构有用的像素级细节对分类是噪声。如果预训练轮数太多瓶颈层可能把训练集中每个样本的独特细节都编码进去了泛化能力反而下降。解决减少预训练轮数或者增大瓶颈层维度。论文里的 30 维瓶颈层是经过实验验证的但如果你用的数据集不是 MNIST需要重新调。另一个办法是在微调阶段解冻所有层用较小的学习率端到端微调让分类损失来调整预训练学到的权值。4.3 现象训练过程中内存溢出尤其是在 784-1000 这一层原因是np.dot(v0.T, p_h0)这步如果 batch_size 太大中间矩阵会占用大量内存。784×1000 的权值矩阵本身只有 3MB 左右但 batch_size 为 1000 时v0是 1000×784p_h0是 1000×1000np.dot(v0.T, p_h0)的结果是 784×1000计算过程中的临时数组可能达到几百 MB。解决把 batch_size 降到 64 或 128。如果还是不够可以把positive和negative的计算改成循环累加避免一次性生成大矩阵。另外用 float32 而不是 float64 可以省一半内存。4.4 现象对比散度只跑一步吉布斯采样时重构误差曲线抖动很大原因是一步采样得到的重构期望估计方差很大尤其是隐藏单元数量较多时。论文里用的是对比散度算法但没写具体跑了几步。Hinton 的原始论文建议一步就够了但实践中如果抖动太大可以增加到 3 步或 5 步。解决把contrastive_divergence里的采样步数从 1 改成 3即多跑几次sample_h和sample_v。这会增加计算量但梯度估计更稳定。另一个办法是减小学习率让每次更新的影响变小曲线也会平滑一些。4.5 现象用论文里的 5 层结构跑自己的数据准确率远低于预期原因是论文的结构是针对 MNIST 的 28×28 灰度图设计的输入维度 784 和 10 类输出都是固定的。如果你的数据是彩色图、尺寸不同、或者类别数不是 10直接套用这个结构肯定不行。解决根据你的数据调整layer_sizes。输入维度改成你的特征维度输出维度改成你的类别数。中间层的降维比例可以参考论文的思路第一层升维到输入维度的 1.2 到 1.5 倍然后逐层减半直到瓶颈层。瓶颈层维度一般取类别数的 3 到 5 倍比如 10 类对应 30 到 50 维。如果数据量小层数和每层单元数都要相应减少否则过拟合会很严重。5. 从这篇论文往外走一步用重构误差做早停和层数选择的实用技巧论文本身没有讲早停和层数选择但这是复现时绕不开的问题。我自己的习惯是在预训练每一层 RBM 时把重构误差记录下来画一条曲线当连续 3 个 epoch 重构误差下降幅度小于 0.001 时就停。这样每层的训练轮数不用手动指定让数据自己说话。下面这个pretrain_with_early_stop方法展示了具体做法。def pretrain_with_early_stop(self, X, max_epochs50, batch_size64, patience3, tol0.001): input_data X for idx, rbm in enumerate(self.rbms): best_error float(inf) wait 0 for epoch in range(max_epochs): perm np.random.permutation(input_data.shape[0]) epoch_error 0.0 for i in range(0, input_data.shape[0], batch_size): batch input_data[perm[i:ibatch_size]] epoch_error rbm.contrastive_divergence(batch) epoch_error / (input_data.shape[0] // batch_size) if best_error - epoch_error tol: wait 1 if wait patience: print(f第 {idx1} 层在 epoch {epoch} 早停重构误差 {epoch_error:.6f}) break else: wait 0 best_error epoch_error input_data rbm.sigmoid(np.dot(input_data, rbm.W) rbm.b)这个早停逻辑的核心是patience和tol两个参数。patience3表示连续 3 个 epoch 没有明显改善就停tol0.001是改善的阈值。这两个值可以根据你的数据规模调整数据量大时patience可以设大一点比如 5数据量小时设 2 就够了。max_epochs50是上限防止早停条件一直不触发导致训练太久。层数选择上论文用了 5 层4 个隐层加 1 个输出层但这不是金科玉律。我一般会从 3 层开始试输入层、一个瓶颈层、输出层。如果重构误差降不下去再加一层。每加一层观察瓶颈层的表示是否让分类准确率提升。如果加了一层之后准确率反而下降说明这一层学到的表示对分类没帮助去掉它。这个过程不需要什么玄学就是控制变量法固定其他层只改层数看验证集准确率。还有一个实用技巧是观察瓶颈层的激活值分布。如果大部分激活值都接近 0 或 1说明瓶颈层饱和了表示能力不够需要增大维度。如果激活值分布比较均匀在 0.2 到 0.8 之间说明瓶颈层在有效编码信息。这个检查只需要几行代码codes ae.encode(X_test[:1000]) print(f瓶颈层激活均值: {codes.mean():.4f}, 标准差: {codes.std():.4f}) print(f接近 0 的比例: {(codes 0.1).mean():.4f}, 接近 1 的比例: {(codes 0.9).mean():.4f})如果接近 0 或 1 的比例超过 80%就说明瓶颈层太拥挤了需要加宽。这个习惯是我从一次翻车经历里养成的当时用 10 维瓶颈层跑一个 20 类的问题准确率死活上不去后来一看激活值全饱和了加到 50 维之后准确率直接涨了 8 个百分点。从那以后我每次训完自动编码机都强制走一遍这个检查再决定要不要调瓶颈层维度。希望帮到你。本文还有配套的精品资源点击获取
返回列表