反向传播就是神经网络的「分布式归因」(attribution)。前向传播像一次请求穿过微服务调用链,最终产生一个响应(预测)和一笔误差(loss)。反向传播则像出故障后沿调用链反向追责:算出每个服务(参数)该为最终误差负多大责任,再按责任大小修正它。它不是什么神秘魔法——本质就是对计算图做一次反向遍历。
痛点:一个网络有上亿个参数,怎么知道每个参数往哪个方向、调多少才能降低 loss?暴力法是逐个参数微调试错——上亿次前向传播,不可能。反向传播用链式法则(chain rule)一次反向遍历就把所有参数的梯度全算出来,成本仅约等于一次前向传播。
核心数学就一个式子。设一条路径 w → z → y → L,要求 loss 对权重 w 的敏感度:
直觉:∂L/∂w 读作「w 动一点点,L 会跟着动多少」。它无法直接算,但每一段局部导数都好算(z 对 w、y 对 z…都是单步运算)。链式法则把全局问题分解成「沿路径局部导数连乘」——和分布式 tracing 把端到端延迟拆成每一跳的耗时,是同一个思想。反向走的原因:先算出靠近 loss 的 ∂L/∂y,往回每一层复用上一层的结果,避免重复计算。
import numpy as np # 手写一个神经元的反向传播:y = sigmoid(w*x + b),看清「责任连乘」 x, y_true = 1.5, 1.0 w, b = 0.3, 0.0 # --- 前向:一路存下中间量,反向时要复用 --- z = w * x + b y = 1 / (1 + np.exp(-z)) # sigmoid 激活 L = 0.5 * (y - y_true) ** 2 # 均方误差 loss # --- 反向:链式法则逐段相乘 --- dL_dy = (y - y_true) # ∂L/∂y dy_dz = y * (1 - y) # sigmoid 导数 dz_dw = x # ∂z/∂w grad_w = dL_dy * dy_dz * dz_dw # 三段连乘 = ∂L/∂w w -= 0.1 * grad_w # 用梯度更新(下一概念详解) print(f"grad_w={grad_w:.4f} new_w={w:.4f}")
梯度下降 = 蒙眼下山。你站在山坡上看不见全局,但能用脚感知脚下哪个方向最陡(梯度),于是朝最陡的下坡方向迈一步,反复直到走到谷底(loss 最小)。后端类比:像一个反馈控制环(control loop)——测量误差、按误差方向调参数、再测量,迭代逼近目标值。「步子迈多大」就是学习率(learning rate)。
反向传播给了每个参数的梯度(责任方向),但梯度只告诉你往哪走,不告诉你走多远、怎么走稳。优化器解决这个。最朴素的更新规则:
梯度 ∇L 是「各方向偏导数组成的向量」,指向 loss 上升最快的方向,所以取负号往下走。三个关键演进:
import torch # 用 PyTorch 拟合 y = 2x,对比 SGD 与 Adam 的「下山」过程 x = torch.linspace(-1, 1, 64).unsqueeze(1) y = 2 * x w = torch.zeros(1, 1, requires_grad=True) opt = torch.optim.Adam([w], lr=0.1) # 换成 SGD([w], lr=0.1) 对比收敛速度 for step in range(50): pred = x @ w loss = ((pred - y) ** 2).mean() # MSE opt.zero_grad() # 清空上一轮梯度(否则会累加) loss.backward() # ← 这一步就是反向传播,自动算梯度 opt.step() # 按梯度更新 w(θ ← θ − η·∇L) print(f"learned w={w.item():.3f} (target=2.0)")
zero_grad() 让梯度跨步累加,是新手最常见的「loss 莫名其妙不对」的 bug。激活函数是夹在每层之间的非线性「门」。没有它,再深的网络也会坍缩成一层——就像一串纯转发的代理服务,无论叠多少层,整体行为还是一个线性变换。激活函数像电路里的晶体管(非线性开关):正是这点非线性,让网络能表达 if/else 式的复杂逻辑,而不只是 y = ax + b。
关键事实:线性的复合还是线性。两层不带激活的网络 W₂(W₁x) = (W₂W₁)x,两个矩阵相乘还是一个矩阵——等价于单层。叠 100 层也一样,毫无意义。激活函数在每层后插入一个非线性弯折,网络才能逼近任意复杂函数。常见三种:
import numpy as np # 直观验证:sigmoid 导数会消失,ReLU 不会 sigmoid = lambda x: 1 / (1 + np.exp(-x)) d_sigmoid = lambda x: sigmoid(x) * (1 - sigmoid(x)) relu = lambda x: np.maximum(0, x) d_relu = lambda x: (x > 0).astype(float) # 模拟 6 层网络:把每层的激活导数连乘(链式法则) xs = np.array([0.5, -0.3, 1.2, 0.8, -1.0, 0.6]) print("sigmoid 连乘:", np.prod(d_sigmoid(xs))) # → ~0.0007,几乎消失 print("relu 连乘: ", np.prod(d_relu(xs))) # → 1.0(全为正时)或 0(碰到死神经元) # 结论:深网络默认用 ReLU 系,不是品味问题,是数学必需
Dropout 就是给神经网络做 Chaos Engineering。Netflix 的 Chaos Monkey 随机杀掉生产节点,逼整个系统不依赖任何单点、学会冗余容错。Dropout 在训练时随机「宕机」一部分神经元,逼网络不依赖任何单个神经元记答案——于是它学到的是鲁棒的、分布式的特征,而不是脆弱的死记硬背。
痛点叫过拟合(overfitting):模型在训练集上近乎满分,一上真实数据就崩——它背下了训练样本(包括噪声),而没学到泛化规律。这是 Goodhart 定律的机器版:「为指标优化过头,指标就失去意义」。两个最常用的解药:
import torch.nn as nn # 同时用上两种正则:Dropout 层 + 优化器的 weight_decay(=L2) model = nn.Sequential( nn.Linear(784, 256), nn.ReLU(), nn.Dropout(p=0.3), # 训练时随机丢 30% 神经元 nn.Linear(256, 10), ) # weight_decay 就是 L2 正则的强度 λ(对大权重罚款) opt = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2) model.train() # 训练模式:Dropout 生效 # ... 训练循环 ... model.eval() # 评估模式:Dropout 关闭,用完整网络(关键!别忘)
model.eval(),导致评估时 Dropout 还在随机丢神经元,结果忽高忽低、不可复现。Dropout 训练开、推理关,是铁律。