恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
从1986年Nature论文到实战:反向传播与表征学习的核心细节
首页
资讯中心
/
从1986年Nature论文到实战:反向传播与表征学习的核心细节
从1986年Nature论文到实战:反向传播与表征学习的核心细节
发布时间:2026/10/15 6:10:39
1. 为什么三十多年前的这篇论文今天读起来依然让人后背发凉如果你正在入门深度学习或者已经调过不少模型但总觉得心里没底我建议你找个安静的下午把这篇1986年发表在《自然》上的文章翻出来读一遍。标题很朴素就叫《Learning representations by back-propagating errors》翻译过来是“通过反向传播误差来学习表征”。作者是三位在认知科学和计算机科学交叉地带深耕多年的研究者。我第一次读的时候还在用某个早期框架搭多层感知机当时只觉得反向传播就是一个链式求导的工程实现没什么稀奇。直到后来自己从零手写了一遍训练循环又回头重读这篇论文才发现里面藏着很多被现代框架封装掉的、真正决定模型能不能训起来的细节。这篇文章的核心贡献不是发明了反向传播——链式法则本身在微积分里早就有了——而是把反向传播系统地用在了多层非线性网络的权重学习上并且明确指出隐藏层可以学到有意义的内部表征。这个观点在当年是很有冲击力的。因为在那个年代很多人认为多层网络没法训练隐藏层没有明确的语义训练信号传不到中间层。这篇文章用实验说明只要误差能一层层往回传隐藏层自己就能组织出对任务有用的特征。今天你随手写一个几层的网络框架自动帮你算梯度你可能感受不到这个结论的分量。但如果你试过手动推导一个两层网络的梯度就会明白当年这个想法有多大胆。我写这篇博文不是要逐句翻译论文而是想从一个实际动手训练过模型的人的角度把这篇论文里真正值得反复琢磨的东西拆开。适合谁看如果你已经会用框架搭模型但对反向传播的理解还停留在“调用一下backward就行”或者你正准备从零实现一个小型神经网络来加深理解那这篇内容会对你有帮助。我会讲清楚论文里几个关键设计背后的逻辑补充一些论文里没写但实际训练中一定会遇到的问题以及我自己在复现类似结构时踩过的坑。2. 论文里那个“简单”的网络结构藏着多少没明说的设计决策2.1 前向传播的公式背后为什么用Sigmoid而不是阶跃函数论文里给出的前向传播形式很简洁每个单元先把输入加权求和再经过一个非线性函数得到输出。这个非线性函数用的是Sigmoid也就是1/(1exp(-x))。很多人读到这里会直接跳过觉得这就是个激活函数的选择。但如果你仔细想为什么不用当年感知机里常用的阶跃函数原因其实很实际阶跃函数的导数几乎处处为零除了在零点附近不可导这意味着梯度没法通过它往回传。Sigmoid虽然也有饱和区梯度消失的问题但至少在大部分区域是光滑可导的而且导数可以用输出值直接算出来这在当年算力有限的情况下非常关键。我在自己实现的时候试过用阶跃函数替换Sigmoid结果就是隐藏层的权重几乎不更新误差曲线平得像一条直线。后来换成Sigmoid训练立刻就能动起来。论文里没有花篇幅解释这个选择但从公式推导的角度看Sigmoid的导数形式是σ(x)(1-σ(x))这意味着你只需要保存前向传播的输出就能在反向传播时直接算梯度不需要重新计算输入。这个细节在当年内存和算力都很紧张的环境下是一个很务实的工程决策。2.2 隐藏层的存在到底解决了什么问题论文里反复强调的一个点是如果网络里没有隐藏层那么输入到输出的映射只能是线性的或者非常简单的非线性组合很多问题根本没法表示。比如经典的异或问题单层感知机就解决不了因为异或不是线性可分的。但加一个隐藏层之后网络可以先在隐藏层把输入空间扭曲一下让原本不可分的数据在新的表示空间里变得可分。我刚开始学的时候对这个说法理解不深直到自己动手做了一个小实验用单层网络去拟合一个简单的异或数据不管怎么调学习率损失都降不下去。后来加了一层隐藏层隐藏单元数设为2训练几百轮之后隐藏层的输出居然自动把四个输入点映射到了两个不同的区域。这个过程没有任何人告诉网络该怎么分它自己从误差信号里学出来的。论文里把这个叫做“学习内部表征”我觉得这个说法非常准确。隐藏层不是在直接做分类它是在学一种新的坐标系统让最后的输出层能在这个新坐标系里画一条直线就把问题解决掉。2.3 权重初始化的随机性为什么是必要的论文里提到权重一开始是随机的小值。这个描述很轻描淡写但实际动手时你会发现如果所有隐藏单元的初始权重完全一样那么它们在训练过程中会一直保持一样相当于你花了计算资源却只得到了一个隐藏单元。随机初始化打破了这种对称性让不同的隐藏单元有机会学到不同的特征。我在早期实现时偷懒把所有权重初始化为零结果训练了半天隐藏层的所有单元输出完全一致模型容量白白浪费了。至于为什么是小值而不是大值论文没有展开。我的理解是如果初始权重太大Sigmoid函数会进入饱和区梯度接近零训练一开始就卡住了。小值能让单元工作在Sigmoid的线性区域附近梯度比较健康。这个经验后来被总结成各种初始化方法比如Xavier初始化、He初始化但根源在这篇论文里已经埋下了。3. 反向传播的推导从误差到权重的完整链路3.1 输出层误差信号是怎么定义的论文里定义输出层的误差信号时用的是实际输出和期望输出之间的差再乘以激活函数的导数。这个形式看起来简单但里面有一个很容易被忽略的点误差信号不是单纯的差值而是差值经过激活函数导数缩放后的结果。为什么要乘导数因为你要把误差从输出空间映射回加权求和之前的空间。Sigmoid的输出范围是0到1但加权求和的范围是负无穷到正无穷两者之间隔着一个非线性变换。如果你直接拿差值去更新权重相当于忽略了非线性变换对误差的缩放作用更新方向会偏。我见过一些从零实现反向传播的代码在输出层直接写delta output - target然后拿这个delta去乘输入更新权重。这样写在小学习率下可能也能跑但收敛速度明显慢而且容易在饱和区震荡。正确的做法是乘上Sigmoid的导数也就是output * (1 - output)。这个细节论文里写得很清楚但现代框架自动帮你做了所以很多人读论文时会一带而过。3.2 隐藏层误差信号的递归计算这是整篇论文最核心的部分。隐藏层没有直接的期望输出它的误差信号是从上一层传回来的误差信号加权求和再乘以本层激活函数的导数。用公式写就是某一层的误差信号等于下一层所有单元的误差信号乘以对应连接权重之和再乘以本层输出的导数。我第一次推导这个公式的时候卡了很久因为直觉上会觉得隐藏层的误差应该由它自己的输出决定但实际上隐藏层的误差完全由它对最终输出的影响来决定。换句话说如果一个隐藏单元对最终误差几乎没有贡献那它的误差信号就接近零权重几乎不更新。这个机制让网络自动把学习资源集中在那些真正影响输出的单元上。论文里用了一个很形象的词叫“back-propagating”误差就像从输出层往回倒着流一样每经过一层就被该层的权重和导数重新分配一次。我在手写实现时把每一层的误差信号存在一个列表里从最后一层往前算算完一层就用它去更新上一层的权重。这个顺序不能乱因为计算第k层的误差信号需要用到第k1层的误差信号和它们之间的权重。3.3 权重更新为什么用梯度下降而不是直接解方程论文里用的是梯度下降也就是沿着误差对权重的负梯度方向小步更新。为什么不直接解一个最优权重的闭式解因为多层非线性网络的误差函数关于权重是非凸的存在大量局部极小值和鞍点根本没有闭式解。梯度下降虽然慢但它是通用的而且每一步计算量可控。我在实际训练中发现学习率的选择比想象中敏感。论文里没有给出具体的学习率数值只说了要小。我试过0.1、0.01、0.001几个量级发现对于小网络0.1左右能较快收敛但容易在后期震荡0.01比较稳但需要更多轮数。后来我养成了一个习惯先用一个中等学习率跑几百轮看损失曲线如果下降太慢就调大如果震荡就调小。这个经验论文里没有但我觉得比任何理论分析都实用。4. 论文里的实验设计放到今天看依然有参考价值4.1 那个编码任务到底在验证什么论文里有一个实验让我印象很深让网络学习一组对称的编码输入和输出是同一组模式但中间经过一个隐藏层。这个任务看起来像是自编码器的雏形但论文的目的不是做降维而是验证隐藏层能不能学到输入的有效编码。实验结果是训练之后隐藏层的激活模式确实能唯一区分不同的输入而且对输入的部分损坏有一定的容错能力。我自己复现过类似的结构用四个输入模式、两个隐藏单元、四个输出单元。训练收敛后两个隐藏单元的输出组合确实对应了四个不同的区域。有意思的是如果你把其中一个隐藏单元的输出固定住只让另一个变化网络对某些输入仍然能给出正确输出。这说明隐藏层学到的不是简单的查表而是一种分布式的表示。这个观察后来在表示学习领域被反复讨论但这篇论文在1986年就已经用实验展示了。4.2 对称性破缺在实验中的体现论文里提到一个细节如果隐藏单元的数量和任务需要的特征数刚好匹配训练往往能收敛到比较干净的表示但如果隐藏单元太多有些单元可能会“偷懒”输出恒定值对最终结果没有贡献。这个现象我在实际训练中经常遇到。比如用10个隐藏单元去学一个只需要3个特征的任务训练完之后你会发现有六七个单元的权重几乎没怎么变输出接近常数。这其实不是坏事它说明网络自动做了某种正则化把多余的容量闲置了。但如果你希望每个单元都学到东西就需要调整初始化或者加一点权重衰减。论文里没有深入讨论这个问题但从实验描述里能看出作者注意到了隐藏单元数量的影响。我的经验是隐藏单元数宁可稍微多一点让网络自己选择用哪些也不要卡得太紧导致欠拟合。4.3 训练轮数和收敛判断的实际操作论文里没有给出明确的停止条件只说训练到误差足够小。这个“足够小”在实际操作中很难把握。我自己的做法是画两条曲线一条是训练集上的平均误差一条是验证集上的误差。如果训练误差还在降但验证误差开始上升那就该停了。如果两条都在降但速度很慢可以再等等。对于这篇论文里那种小规模任务通常几千轮就能看到明显收敛。还有一个细节论文里用的是批量更新还是在线更新从描述看应该是每输入一个样本就更新一次权重也就是随机梯度下降。这种方式在当年算力有限的情况下很实用因为不需要把整个数据集加载进来。但它的缺点是更新方向噪声大损失曲线会抖。我试过把批量大小设为1和设为全部发现小批量比如8到32在稳定性和速度之间平衡得比较好。这个经验在现代深度学习里已经是常识但根源还是在这类早期工作里。5. 从论文到代码手写反向传播时最容易翻车的几个地方5.1 梯度检查数值梯度和解析梯度的对比如果你自己实现了反向传播第一件事应该是做梯度检查。方法很简单对某个权重用数值方法算一个近似梯度再和你反向传播算出来的解析梯度对比。如果两者差得很远说明你的推导或者实现有问题。我当年第一次写反向传播时隐藏层的误差信号忘了乘激活函数的导数结果梯度检查直接报错数值梯度和解析梯度符号都反了。数值梯度的计算是把权重加一个很小的epsilon算前向传播的误差再减一个epsilon算误差两者之差除以2epsilon。epsilon一般取1e-4到1e-5。如果解析梯度和数值梯度的相对误差在1e-6以下基本可以认为实现是对的。这个步骤论文里没有提但任何从零实现的人都应该做。5.2 权重更新顺序和原地操作的陷阱在更新权重的时候有一个很容易犯的错误一边更新前面的权重一边用更新后的权重去算后面层的误差信号。正确的顺序是先用旧的权重把所有层的误差信号都算完再统一更新权重。如果你在循环里更新了某一层的权重然后继续往前算那前面层用到的就是已经被修改过的权重梯度方向就错了。我在早期实现时犯过这个错表现是训练初期损失下降很快但很快就卡住不动了。后来把误差信号的计算和权重更新分成两个独立的循环问题就解决了。这个坑在论文里没有明说因为作者默认读者理解反向传播的链式法则要求所有梯度基于同一组权重计算。5.3 学习率衰减和动量项的取舍论文里用的是固定学习率没有提衰减和动量。但在实际训练中固定学习率往往在后期会让损失在最小值附近震荡。我后来加了一个简单的学习率衰减每过一定轮数学习率乘以0.9。这样前期下降快后期稳定。动量项则是另一个有用的技巧它把上一次的更新方向按一定比例加到当前更新上能加速收敛并减少震荡。不过对于这篇论文里那种小规模任务不加这些技巧也能跑出结果。我的建议是先把基础版本跑通确认梯度检查通过、损失能下降再去加这些优化。否则你分不清是反向传播实现错了还是优化技巧没调好。6. 这篇论文对今天做模型训练的人还有什么实际启发6.1 表征学习的思想比反向传播本身影响更深远很多人把这篇论文的贡献归结为反向传播但我觉得它更大的影响是提出了“学习表征”这个思路。在它之前做模式识别的人往往需要手工设计特征比如边缘检测、纹理统计之类的。这篇论文说明只要网络足够深隐藏层可以自己从原始输入里学出有用的特征不需要人告诉它该看什么。这个思路后来演变成了深度学习的基本范式端到端训练让网络自己学特征。今天你用一个预训练模型做微调本质上也是在利用它学到的表征。回头读这篇论文你会发现作者在结论部分已经隐约提到了这个方向只是当年的数据和算力还不足以展示深层网络的威力。6.2 误差信号作为学习信号的通用性论文里把误差信号定义为误差乘以激活函数导数这个形式后来被推广到了各种网络结构和损失函数。比如交叉熵损失配Softmax误差信号就变成了输出减目标非常简洁。再比如均方误差配线性输出误差信号就是差值本身。这些特例都可以从这篇论文的框架里推出来。我在实际工作中遇到新的损失函数时第一反应就是推一下它的误差信号形式。只要误差信号能写出来反向传播就能用。这个思维方式比记住某个具体公式重要得多。6.3 小网络实验对理解大模型的价值这篇论文里的实验规模很小隐藏单元只有几个到几十个训练数据也就几十个模式。但正是这种小规模让你能清楚地看到每个单元在做什么每个权重怎么变。我建议每个学深度学习的人都找时间复现一下这种小实验不要一上来就调大模型。大模型的很多现象比如梯度消失、表示坍塌、过拟合在小网络里都能观察到而且更容易调试。我自己在带新人的时候通常会让他们先用这篇论文的结构手写一个两层网络在异或数据上训到收敛再做梯度检查。这个过程走一遍比看十篇教程都管用。因为你会被迫面对每一个细节激活函数选什么、权重怎么初始化、误差信号怎么传、学习率怎么调。这些细节在大框架里都被封装了但它们是理解深度学习的基石。7. 我在复现这篇论文时积累的几个实操心得第一个心得是关于数据表示的。论文里的输入模式用的是0和1输出也是0和1。如果你用-1和1Sigmoid的饱和区会不一样训练动态也会变。我试过两种表示发现0和1在Sigmoid下更自然因为Sigmoid的输出范围就是0到1目标值落在范围边界上梯度不会太大也不会太小。如果你用-1和1输出层需要把Sigmoid的输出映射到-1到1多了一层变换反而麻烦。第二个心得是关于隐藏单元数量的。论文里没有给出明确的选择规则我的经验是对于二分类或者小规模多分类任务隐藏单元数取输入维度和输出维度之间的某个值比如输入4维输出4维隐藏层取2到4个就够。如果任务复杂可以适当增加但不要超过输入维度的两倍否则容易过拟合。这个规则当然不是绝对的但作为一个起点很实用。第三个心得是关于训练轮数的。论文里的任务通常几千轮就能收敛但如果你用的学习率很小可能需要几万轮。我一般会设一个上限比如50000轮然后看损失曲线。如果损失还在缓慢下降就再跑如果已经平了就停。不要盲目追求训练误差为零因为那往往意味着过拟合。第四个心得是关于随机种子的。论文里没有提随机种子但实际训练中不同的初始化会导致不同的收敛结果。有时候换个种子损失曲线形状完全不一样。我的做法是跑三到五个种子取平均表现而不是只看一次结果。这个习惯在调参时特别重要能避免你被单次运行的偶然性误导。最后说一个我踩过的坑在计算输出层误差信号时如果你用的是均方误差误差信号是输出减目标乘以输出导数。但如果你不小心把输出导数写成了输入导数梯度方向就错了。这个错误很隐蔽因为损失曲线可能还是会下降只是速度慢很多。梯度检查能发现这个问题所以千万别跳过那一步。