第一次完整推导BP神经网络的数学原理时我整个人是懵的前向传播还好说到了反向传播那一堆上下标、链式法则、误差项每行公式都认识串在一起就成了天书。后来才发现BP神经网络听起来唬人拆开看就三步——前向算结果、反向算梯度、再更新参数。最难的反向传播也不过是反复套用链式法则加上一点点耐心。这篇内容就是想帮正在被BP数学原理推导折磨的初学者打通这条线也适合期末复习、面试突击或者给团队新人做讲义时直接参考。我会用一个小到能用手算的网络把每个步骤铺开讲清楚再补充几个从基础BP衍生出来的实用变体和调参经验。1. 先建立全局观BP神经网络在“学”什么1.1 从感知机到BP一个层层修正的自动迭代过程神经网络的基本单元来自感知机输入乘权重求和加偏置再经过一个激活函数得到输出。单个感知机只能解决线性可分问题一旦面对稍微复杂的数据就会被“线性”两个字卡死。于是人们开始堆多层网络用隐藏层去拟合非线性关系。网络一深问题立刻来了中间层的权重没有直接参与最终输出误差算出来之后到底该怪哪一层的哪个权重BP误差反向传播算法的价值就在这里。它用求导的链式法则把输出端的误差逐层反向传回每一个中间层算出每个参数对损失的影响程度也就是梯度。梯度算出来之后再沿负梯度方向更新参数不断循环。本质上这就像一条生产线上出了问题品控部门从最终成品往回倒查看看每道工序分别该承担多大比例的责任。理解了这一点后面看任何深度学习框架的代码都会轻松很多因为训练循环翻来覆去只有那几步。1.2 符号约定先把名字统一后面推公式不迷路我见过太多人在推导过程中卡壳不是不会求导而是被混乱的符号淹没了。不同教材对权重的上下标约定不一样一会儿 w 一会儿 W下标一会儿在前一会儿在后不晕才怪。所以我建议读完这一节后先在纸上把符号固定下来。网络层数用 L 表示输入层不计算在内输出层就是第 L 层。第 l 层的第 j 个神经元接收到的加权结果记为 z_j^(l)经过激活函数后的输出记为 a_j^(l)。权重 w_jk^(l) 表示第 l 层的第 j 个神经元连接到第 l-1 层第 k 个神经元的权重偏置记为 b_j^(l)。损失函数也常用字母 L 或 C 表示本文用 C 表示损失避免和层数 L 混淆。这个约定和我写矩阵代码时的习惯一致上一层有 K 个神经元当前层有 J 个神经元那么权重矩阵形状就是 J×K前向传播写成 A^(l) σ(W^(l) A^(l-1) b^(l))。矩阵乘法中左乘当前层权重矩阵恰好和“行是当前层神经元、列是上一层神经元”对应。提前把这件事想清楚后面推公式和写代码都不会乱。2. 前向传播数据是怎么流到输出端的2.1 加权求和与激活函数分别扮演什么角色前向传播的每一层都在做两件事线性加权求和与非线性变换。加权求和就是 z Wx b它只能表达直线、平面或者超平面。激活函数的作用是给网络注入非线性让多层网络不再是线性函数的简单堆叠。如果不加激活函数无论叠多少层数学上都能化简成一次线性变换那深度就没有意义了。激活函数里sigmoid 在BP推导中地位特殊因为它的导数极简σ(x) σ(x)(1 − σ(x))。只要记下前向输出值导数随手就能算出来所以几乎所有教材在讲BP时都用 sigmoid 做示例。实际工程中ReLU更常用能缓解梯度消失且计算极快但它的导数分段教学演示远不如 sigmoid 方便。先通过 sigmoid 把BP的骨架搞明白再迁移到ReLU是很顺畅的学习路线。2.2 损失函数拿什么衡量“差多少”损失函数是整个BP过程的起点也是真实标签唯一进入网络的入口。回归任务常用均方误差MSE分类任务常用交叉熵。初学阶段用MSE最直观直接把预测值和真实值相减再平方一眼就能看出损失大小。为了推导方便我把损失函数写成 C 1/2 Σ_j (a_j^(L) − y_j)^2。前面那个 1/2 纯粹为了让求导后消掉系数2是数学上常见的无害化简考试推导时也经常保留。 用交叉熵的话公式会有所不同但反向传播的架构完全一致。我建议初学者先把MSE的BP推会再推softmax加交叉熵的组合会发现只是输出层误差项的局部调整。2.3 一个具体到可以手算的小网络空谈原理容易飘我们先固定一个非常小的网络输入2个特征1个隐藏层含2个神经元输出层1个神经元激活函数全部用 sigmoid。这个网络小到每一步都能手算验证又拥有完整的非线性隐藏层关键步骤都会出现。具体数值我沿用经典示例方便对照。输入 x10.05x20.10真实标签 y1。初始参数隐藏层第一个神经元对两个输入的权重分别是0.15和0.20偏置0.35隐藏层第二个神经元对两个输入的权重分别是0.25和0.30偏置0.35输出层连接两个隐藏神经元的权重分别是0.40和0.45偏置0.60。学习率取0.5。先走一遍前向。隐藏层第一个神经元z1 0.05×0.15 0.10×0.20 0.35 0.3775经过 sigmoid 后 a1 ≈ 0.5933。第二个神经元z2 0.05×0.25 0.10×0.30 0.35 0.3925a2 ≈ 0.5969。输出层z_out 0.5933×0.40 0.5969×0.45 0.60 ≈ 1.1059经过 sigmoid 后 a_out ≈ 0.7514。损失 C 0.5×(0.7514 − 1)^2 ≈ 0.0309。预测离标签1还有距离接下来反向传播该上场了。3. 反向传播推导一个小网络的手算全过程3.1 链式法则推导前先打通这一关反向传播的数学核心只有链式法则。复合函数 y f(g(x))求导时先对 f 求导再乘以 g 对 x 的导数。神经网络本质上就是一个巨大无比的复合函数输入经过第一层激活输出又喂给第二层一直传到最后变成损失值。要求损失对任意一个权重求导就是把这个链上一段段局部导数的乘积全部串起来。可以想象成一条流水线上的质量检查最终问题可能由很多工序共同导致要追溯某一台机器调得有多偏不能只看机器自己还要把下游传下来的影响一路乘回去。初次学习时如果链式法则不够熟建议先拿 y sin(x^2 1)、z (y2)^3 这类函数自己手写几遍复合求导再回来推BP。磨刀不误砍柴工。3.2 输出层误差项公式怎么来的正式推导时先定义一个中间量误差项 δ_j^(l) ∂C / ∂z_j^(l)。为什么非要引入这个量因为它把“损失对任何参数的梯度”拆成了统一的格式。后面会看到只要算出 δ损失对权重和偏置的梯度就立刻到手。对输出层直接用链式法则展开δ_j^(L) ∂C/∂a_j^(L) × ∂a_j^(L)/∂z_j^(L)基于前面的MSE损失∂C/∂a_j^(L) a_j^(L) − y_j。而 a σ(z)所以 ∂a/∂z σ(z)。代入得δ_j^(L) (a_j^(L) − y_j) σ(z_j^(L))用例子验证a_out 0.7514y 1σ(z) a(1−a) ≈ 0.1868所以 δ_out ≈ (−0.2486) × 0.1868 ≈ −0.0464。 这个负号大家先别慌。预测比真实标签小要降低损失输出层的加权输入 z 应该增大所以损失对 z 的导数为负是合理的。负梯度方向恰恰指向损失下降的方向后面更新时会体现出来。3.3 隐藏层误差项BP中最关键的一步隐藏层的误差项不能直接和损失函数挂钩因为它不直接出现在损失表达式里。但隐藏层的 z_j^(l) 会先影响本层激活值 a_j^(l)而 a_j^(l) 又会影响下一层所有神经元的加权输入最终兜兜转转才到达损失函数。 因此需要按照链式法则把所有经过 a_j^(l) 传到损失函数的路径都加起来δ_j^(l) σ(z_j^(l)) × Σ_k w_kj^(l1) δ_k^(l1)其中 w_kj^(l1) 是第 l1 层第 k 个神经元连接当前层第 j 个神经元的权重。这个公式值得反复默念当前隐藏神经元的误差等于本层激活函数导数乘以连接到下一层的权重与下一层误差项的加权和。下一层误差越大连接权重越大当前神经元对最终错误承担的责任就越大。例子接着算。隐藏层第一个神经元σ(z1) a1(1−a1) ≈ 0.2413输出层只有1个神经元所以求和只有一项。δ1 ≈ 0.2413 × (0.40 × (−0.0464)) ≈ −0.00448。第二个神经元σ(z2) ≈ 0.2406δ2 ≈ 0.2406 × (0.45 × (−0.0464)) ≈ −0.00502。注意当前隐藏层误差的绝对值比输出层小了一个数量级这是多层网络里很常见的梯度衰减现象也是后面要讲的梯度消失问题的早期征兆。3.4 参数更新公式梯度算出来之后怎么用有了误差项之后损失对任意权重的梯度就是一个简单乘法∂C/∂w_jk^(l) a_k^(l-1) δ_j^(l) ∂C/∂b_j^(l) δ_j^(l)也就是说权重梯度等于“上游传导来的误差”乘以“这条线前向传播时的输入激活值”。前向传播时缓存下来的 a 和 z到反向阶段直接取用这也是几乎所有自动微分框架的通用做法。参数更新规则就是最朴素的梯度下降w_jk^(l) ← w_jk^(l) − η × a_k^(l-1) × δ_j^(l) b_j^(l) ← b_j^(l) − η × δ_j^(l)用小例子把输出层权重更新一遍。输出层连接隐藏层第一个神经元的权重初始为0.40其梯度 ∂C/∂w a1^(1) × δ_out ≈ 0.5933 × (−0.0464) ≈ −0.0275。更新后0.40 − 0.5 × (−0.0275) ≈ 0.4138。权重增大了于是输出层加权输入 z 会变大预测值向目标1靠拢逻辑完全闭环。隐藏层权重也更新一下用 δ1 和输入值算梯度。例如 w_h1_x1 初始0.15梯度 0.05 × (−0.00448) ≈ −0.000224更新后约0.1501变化很小。这正是反向传播的现象离输出越远的层梯度越小参数变化越慢。 一轮迭代后重新前向计算输出约0.7588损失约0.0291确实比初始的0.0309降了一些。学习率只有0.5变化看起来不大是正常的多迭代几百轮就会明显下降。3.5 从单样本到批量三种梯度下降方式怎么选上面例子里一次只用一条样本更新参数这是随机梯度下降SGD的思路。它每条样本都更新一次梯度噪声大但计算便宜而且噪声有时反而帮助跳出局部极小。另一种极端是批量梯度下降用全部样本算出平均梯度再更新稳定但算力开销大。工程上最常用的是小批量梯度下降Mini-batch SGD每个小批次算一次梯度噪声可控、计算量适中还能利用GPU并行加速。实际训练神经网络时很少真的只用一条样本更新。小批量大小一般取32、64、128这类2的幂方便矩阵运算发挥到极致。这个概念虽然不是BP独有的但和BP配合密切。前向传播算一批数据时是矩阵乘反向传播也是矩阵乘批量做看起来只是多了一个维度的循环但正是这一步让深度学习在工程上真正变得可行。4. 手写一遍BP训练最小可运行代码与运行观察4.1 无框架手写50行代码完成前向、反向、更新数学推导滚过一遍之后强烈建议不要急着上框架先用最朴素的 Python 把BP过程写出来。不用考虑任何库只需要数组和基础运算这样每一步都透明出错时能顺着中间量查。import math def sigmoid(x): return 1.0 / (1.0 math.exp(-x)) # 初始权重沿用第2章示例 w_h1_x1, w_h1_x2, b_h1 0.15, 0.20, 0.35 w_h2_x1, w_h2_x2, b_h2 0.25, 0.30, 0.35 w_o_h1, w_o_h2, b_o 0.40, 0.45, 0.60 x1, x2, y 0.05, 0.10, 1.0 eta 0.5 # 前向传播 z_h1 x1 * w_h1_x1 x2 * w_h1_x2 b_h1 a_h1 sigmoid(z_h1) z_h2 x1 * w_h2_x1 x2 * w_h2_x2 b_h2 a_h2 sigmoid(z_h2) z_o a_h1 * w_o_h1 a_h2 * w_o_h2 b_o a_o sigmoid(z_o) loss 0.5 * (a_o - y) ** 2 print(前向输出:, a_o, 损失:, loss) # 反向传播 delta_o (a_o - y) * a_o * (1 - a_o) delta_h1 a_h1 * (1 - a_h1) * (w_o_h1 * delta_o) delta_h2 a_h2 * (1 - a_h2) * (w_o_h2 * delta_o) # 参数更新 w_o_h1 - eta * a_h1 * delta_o w_o_h2 - eta * a_h2 * delta_o b_o - eta * delta_o w_h1_x1 - eta * x1 * delta_h1 w_h1_x2 - eta * x2 * delta_h1 b_h1 - eta * delta_h1 w_h2_x1 - eta * x1 * delta_h2 w_h2_x2 - eta * x2 * delta_h2 b_h2 - eta * delta_h2 print(更新后输出层权重:, w_o_h1, w_o_h2, b_o)这段代码把前向、反向、更新全都放在明面上没有任何优化技巧。变量名直接对应推导中的符号读者完全可以用手算结果逐行比对。跑完这一步再去看框架里的层对象会明白它们在封装什么。4.2 跑通示例数据并观察参数变化运行上面代码前三行输出大致是前向输出0.7514损失0.0309更新后输出层权重0.4138、0.4639、0.6232。把更新后的参数代入第二次前向传播输出约0.7588损失约0.0291。虽然只降了一点点但方向对了。这时候特别建议做一件“破坏性实验”把学习率改成5再跑一次。你会发现一轮之后参数剧烈变化甚至可能来回振荡发散。再改成0.01训练变得像蜗牛爬。学习率对训练行为的影响亲眼看一次比背十遍口条都管用。还可以把初始权重改成全0注意观察梯度同层两个神经元的梯度完全相同无论训练多久两个神经元都保持对称网络等于退化成只有一半容量。 这就是“对称性破缺问题的直观来源”也是权重必须随机初始化的根本原因。4.3 为什么要向量化从标量到矩阵上面代码里每个变量都单独写一行网络规模一上来这种写法会瞬间爆炸。假设每层1000个神经元手写变量完全不可行。于是必须向量化。用矩阵表示一批样本、一层权重、一层输出前向一步是 A^(l) σ(W^(l) A^(l-1) b^(l))反向一步是 δ^(l) σ(Z^(l)) ⊙ (W^(l1))^T δ^(l1)其中 ⊙ 表示逐元素乘。向量化不是纯粹的风格问题而是性能问题。矩阵乘在CPU和GPU上都有高度优化的实现一万条数据一步算完。很多初学者一上来就背向量化公式遇到维度不匹配或者梯度异常时无从下手。所以我的建议始终是先手推标量、手写标量代码再过渡到矩阵表达。标量版本理解透之后看见向量公式里的转置就知道那是把“下一层误差按连接权重分配回来”这件事集体化心里完全不会发虚。5. 基础BP的几个实用变体从朴素梯度下降到Adam5.1 动量法给梯度下降加一个惯性缓冲朴素梯度下降在损失曲面的峡谷区域特别容易振荡。一个方向上梯度很陡参数来回横跳另一个方向上梯度平缓参数挪动缓慢。动量法模仿小球滚下坡的物理直觉给参数更新增加一个速度累积项v ← αv − η∇C w ← w v其中 α 通常取0.9v 保存历史梯度的指数滑动平均。更新方向不再只看当前这一步梯度而是被历史惯性“平滑”过。峡谷中方向相反的震荡会相互抵消持续前进的方向则不断累加速度。我实际训练时动量几乎是默认开启的因为它的收敛稳定性提升立竿见影改动成本却只有几行代码。对于刚才手写的BP网络只需要为每个参数多维护一个 v更新时先更新 v 再用 v 更新权重。不需要改变前向过程和反向求导逻辑非常自然。5.2 自适应学习率RMSProp 与 Adam 到底改了什么固定学习率的问题在于所有权重共享同一个步长但不同参数的梯度量级差异可能很大。某些参数梯度大而陡另一些参数梯度小却重要。RMSProp的思路是为每个参数维护梯度平方的滑动平均再按这个统计量调整步长梯度平方均值大的参数学习率自动变小梯度平方均值小的参数学习率自动变大。这是一种二阶矩的适应策略。Adam在RMSProp基础上再加一阶动量相当于同时记住“方向惯性”和“步长调节”并且对训练初期的偏差做了修正。 现在的深度学习项目里Adam基本是默认选择它的超参数扰动容忍度比SGD大很多。新手直接上手Adam很多收敛问题都会自动消失但至少要知道它的内在逻辑而不是无脑用默认值。无论用哪个变体BP本身求梯度的核心机理没有变化变的只是“拿到梯度之后怎么跨出下一步”。5.3 防过拟合L2正则化与Dropout在BP中的角色模型参数越多越容易在训练集上死记硬背泛化能力变差。基础BP网络结构朴素对抗过拟合首推L2正则化。在损失函数上加一项 λ/2 Σ w^2梯度里会多出一个 λw等价于每次更新时把权重向零方向轻微压缩。它压低大权重的作用让网络用更分散的方式表达特征。Dropout是另一个简单粗暴但极其有效的变体训练时随机让一部分神经元置零不参与前向传播反向传播时同一掩码参与梯度计算。每次随机失活相当于训练了一个不同的子网络预测时近似集成所有子网络。放在基础BP上实现也就是前向时生成一个伯努利掩码乘到激活值上。很多人以为Dropout改动很大其实代码层面仅仅是一行乘法的差别。它的价值在于逼网络学习冗余特征这样才不会因为某个神经元失效就全面瘫痪。5.4 权重初始化变体Xavier与He初始化初始化对整个BP能否成功影响极大。全零会让同层神经元对称失效权重过大会把激活值直接推进饱和区梯度瞬间消失权重过小又会让信号传不了几层就衰减干净。早期常用均值为0、标准差0.01的高斯随机初始化对浅网络还行深度一上去就不够。Xavier初始化根据神经元连接数设置方差目标是让每层的输入输出方差尽量保持一致。He初始化针对ReLU做了修正方差略大原因是ReLU会截断一半信号需要更强一些的初始信号来补偿。 选择初始化不是美学问题而是让误差信号在网络里传得更远。 顺着这个视角想BP网络的训练实际上是在改善整个反馈控制系统的信号质量。初始化、激活函数、优化器、正则化全都在为同一件事服务。6. 常见问题排查与入门路径建议6.1 梯度消失和爆炸深网络难训练的根源BP网络一旦变深梯度消失几乎必然出现。 sigmoid 导数的最大值是0.25一小串小于1的数连乘起来传到输入层附近时梯度已经接近0那些层的权重几乎学不动。反过来如果权重绝对值大于1连乘会导致梯度指数增长参数更新巨大直接发散。应对方案是组合拳用ReLU系激活函数替换sigmoid解决信号衰减问题用合适的初始化让激活值保持在合理范围用批量归一化把每层输入重新拉回标准分布再加残差连接为梯度多开一条“捷径”。这些方案在不同时代分别提出但解决的问题根源都和梯度消失有关。初学者不需要学完所有方案但看到梯度消失四个字时应该能立刻联想到激活函数导数和权重连乘而不是只当个术语背下来。6.2 损失不下降一份拿来即用的排查清单训练过程中最头疼的就是损失不降。把常见原因和手段整理成一张表按顺序排查会效率高很多现象常见原因排查/修复手段损失完全不下降数据未归一化特征标准化到均值为0、方差为1损失上下剧烈振荡或发散学习率过大用0.001、0.01、0.1对数间隔试观察趋势训练很慢但能降学习率过小逐步调大并观察权重更新幅度分类任务收敛慢sigmoid输出层配MSE换交叉熵损失初始几轮就卡死权重初始化不当换Xavier或He初始化训练集损失低、验证集差过拟合加L2正则化、Dropout或数据增强怀疑梯度算错反向推导有误用数值梯度做一次核对数值梯度核对是当年帮了我大忙的做法对某个权重 w分别计算 C(wε) 和 C(w−ε)用有限差分近似梯度再和BP算出的梯度比对。两种方法若在同一量级说明链式推导没问题若差出几个数量级几乎一定是某个局部导数弄错了。6.3 复习时最容易绕晕的三个概念第一个是权重下标的含义。w_jk^(l) 中 j 是当前层神经元索引k 是上一层神经元索引这个约定不是物理公理只是教材习惯。不同资料可能相反。我复习时画了一张“输入-隐藏-输出”小图在每条连线旁边标上下标半小时就记得很牢。第二个是 δ 的符号与方向。永远记住规则参数更新是参数减去学习率乘以梯度。梯度为负权重就增大梯度为正权重就减小。总是沿着让损失下降的方向改变参数。如果某一步不知道梯度该正该负先用手算例子推一遍比背结论稳得多。第三个是矩阵维度的接合。写代码前先在注释里把每一层张量的形状标出来。例如 W^(l) 形状 (J, K)δ^(l) 形状 (J, 1)a^(l-1) 形状 (K, 1)那么反向公式 (W^(l1))^T δ^(l1) 的结果自然变成 (J, 1)和 δ^(l) 对齐。维度不匹配在很多情况下是代码报错的第一来源提前写注释能省非常多时间。6.4 我建议的学习路径如果让我重来一次我会按这个顺序复习BP先用半天把链式法则和误差项定义吃透再用第2、3章的小网络例子手推每一步然后无框架写代码跑通二分类数据最后用主流框架搭同样的模型把框架自动算出的权重更新量和自己的手写结果对比。整个过程一天到两天比反复刷教材效率高得多。第一遍推公式推不动时不要硬往下走回到输出层误差项和隐藏层误差项两个公式背下来并不算数关键是要能默写并解释每一项的含义。能用大白话向另一个人讲清楚“误差项为什么要乘激活函数的导数”才是真正过关了。最后再分享一个我在反复推导BP过程中体会很深的东西公式里那些上下标本质上都是为“责任分配”服务。每次看到 δ 和激活函数导数相乘都可以想成这个神经元当前有多活跃、它对最终错误出了多少力。一旦形成这种直觉哪怕以后去看注意力机制、Transformer这类复杂模型都会有一种“还是在做链式求导”的踏实感。BP是个很老的概念但把它真正吃透之后深度学习里很多看似高深的东西都会显得朴素起来。
阅读完成 · 觉得有帮助?