基础 · 11

神经网络与反向传播

一个神经元是一条线加一个弯。把线叠起来,正方形变成八边形变成圆——这就是深度学习的全部秘密。

24 min read

线性分类器的天花板

上一篇我们看到,线性分类器——无论是 logistic 回归还是 linear SVM——只能画一条直线。对于非线性可分的数据(比如两个新月),它们的准确率卡在 50% 左右,不管你怎么调参。

核技巧可以绕过这个限制:先把数据映射到高维,再在高维画直线。但核函数的映射 ϕ(x)\phi(\mathbf{x})预先固定的——你选了 RBF 核就是 RBF,选了多项式核就是多项式。如果你选错了核,分类效果照样差。

那能不能让模型自己学一个最适合这份数据的映射?这就是神经网络的核心想法:

不预设 ϕ\phi,而是用可训练的参数搭一个映射函数,让梯度下降自动找到最好的那个。

一个神经元 = 一条线 + 一个弯

一个神经元做的事只有两步:

第一步:线性变换。 把输入向量 x\mathbf{x} 和权重 w\mathbf{w} 做点积,加上偏置 bb

z=wx+bz = \mathbf{w} \cdot \mathbf{x} + b

这跟 logistic 回归完全一样——在特征空间里画一条超平面。

第二步:非线性激活。zz 喂进一个非线性函数 σ\sigma

a=σ(z)a = \sigma(z)

最常用的激活函数是 ReLU(Rectified Linear Unit):

ReLU(z)=max(0,z)\text{ReLU}(z) = \max(0, z)

几何意义:ReLU 把超平面一侧的输出截断为零,另一侧保留原值。整个操作就是「画一条线,然后把一边折下去」。

为什么这个弯至关重要?因为线性变换叠线性变换还是线性变换——两个矩阵相乘 W2W1W_2 W_1 等于一个新矩阵 WW。没有激活函数,不管堆多少层,效果跟一层一样。正是这个「弯」让叠加有了意义。

激活函数:哪种「弯」最好

ReLU 不是唯一的选择。激活函数的设计直接决定了网络能不能训好、训多快。一个好的激活函数需要满足几件事:

  1. 非线性 —— 否则多层等于一层。
  2. 处处可微 —— 否则反向传播在某些点断裂。
  3. 梯度不消失、不爆炸 —— 导数在大部分区间内接近 1,链式乘法才不会指数衰减或指数放大。
  4. 计算便宜 —— 一个 LLM 里几十亿个激活值,每次前向都要算。

来看四种最常用的激活函数,以及它们各自的导数行为:

2012+ · AlexNet 到 ResNet 的标配max(0, z)。负侧梯度为零(dead ReLU),正侧梯度恒为 1。
zf(z)-3-2-1123-1123

ReLU 在正侧导数恒为 1,梯度能一路流回去。但负侧死区会让部分神经元「永久关闭」。

激活函数对比。点「显示导数」看梯度行为——反向传播时,导数决定了信号能不能流回去。

Sigmoid σ(z)=1/(1+ez)\sigma(z) = 1/(1+e^{-z}):输出在 (0,1)(0, 1) 之间,天然适合当概率。但两端饱和——当 zz 很大或很小时,导数几乎为零。在深层网络里,这些零导数乘起来会让梯度消失,前面的层完全学不动。所以 sigmoid 在隐藏层已经被淘汰了,但最后一层做二分类输出时仍然常见。

Tanh tanh(z)=(ezez)/(ez+ez)\tanh(z) = (e^z - e^{-z})/(e^z + e^{-z}):输出在 (1,1)(-1, 1) 之间,零均值,比 sigmoid 好一些。但两端同样饱和。RNN 里还偶尔用,大部分场景已被 ReLU 系列取代。

ReLU max(0,z)\max(0, z):正侧导数恒为 1,梯度能一路流回去;计算只需一个比较,极快。问题是负侧死区——一旦某个神经元对所有输入都输出 z<0z < 0,它就永久「死了」,梯度永远为零。经验上,约 10–30% 的 ReLU 神经元会在训练中死亡,但通常不影响最终效果。

GELU zΦ(z)z \cdot \Phi(z),其中 Φ(z)\Phi(z) 是标准正态的累积分布函数:在 z=0z=0 附近有光滑过渡,负侧保留小梯度,不会完全死掉。计算比 ReLU 贵,但现代 GPU 上差异不大。BERT、GPT、几乎所有 LLM 都用 GELU 或其变体(SwiGLU、GeGLU)。

选择激活函数有一条经验法则:隐藏层用 ReLU 或 GELU,输出层看任务——回归用恒等,二分类用 sigmoid,多分类用 softmax。

叠起来:多边形逼近任意形状

把多个神经元并排放在一层:每个神经元画自己的线、折自己的弯。一层 NN 个 ReLU 神经元就是 NN 条线同时作用——几何上,它们切出一个 NN 边形的区域。

数学上,一个隐藏层的前向计算:

h=ReLU(W1x+b1)\mathbf{h} = \text{ReLU}(W_1 \mathbf{x} + \mathbf{b}_1) y^=w2h+b2\hat{y} = \mathbf{w}_2^\top \mathbf{h} + b_2

W1W_1 的每一行是一个神经元的权重向量(= 一条线的法向量),b1\mathbf{b}_1 是偏置(= 线到原点的距离),w2\mathbf{w}_2 控制怎么组合这些「折叠」。

下面的数据是两个同心环——内圈紫色,外圈青色,线性分类器完全没办法。切换隐藏层宽度感受:

线性分类器acc 41%
切换隐藏层宽度。4 个 ReLU 神经元画出正方形,8 个画八边形,16 个接近圆——每个神经元贡献一条直线,合在一起逼近任意形状。

4 个神经元画出正方形(4 条边),只在四个对角方向漏了。8 个画八边形,16 个已经几乎是圆。这就是万能逼近定理(Universal Approximation Theorem)的直觉:

只要隐藏层够宽,一层神经网络就能逼近任意连续函数到任意精度。

实际上,深度网络(多层)比宽网络(一层很宽)更高效——因为叠层是变换的复合 f3f2f1f_3 \circ f_2 \circ f_1,每一层可以在前一层的特征上做进一步的「折叠」。同样表达能力,深网络需要的参数更少。这也是为什么 Transformer 要堆 96 层而不是 1 层。

反向传播与计算图

网络搭好了,怎么训练?目标跟以前一样:定义一个损失 LL,算出 LL 对每个参数的梯度,然后用梯度下降更新参数。

关键问题是:参数藏在很多层里,LL 对第一层权重的梯度要穿过后面所有层才能算出来。链式法则告诉我们,把每一段的局部导数乘起来就行:

Lw=Ly^y^zzw\frac{\partial L}{\partial w} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z} \cdot \frac{\partial z}{\partial w}

反向传播(backpropagation) 就是把这条乘法链从后往前算一遍。

计算图:把链式法则画出来

把神经网络的每一次运算都画成一个节点,数据流就是边。这样一个两层网络的计算图长这样:

x×W1+b1z1ReLUh×w2+b2y^(y^t)2L\mathbf{x} \xrightarrow{\times W_1 + b_1} \mathbf{z}_1 \xrightarrow{\text{ReLU}} \mathbf{h} \xrightarrow{\times \mathbf{w}_2 + b_2} \hat{y} \xrightarrow{(\hat{y}-t)^2} L

前向传播:从左到右,每个节点根据输入算输出。反向传播:从右到左,每个节点根据输出的梯度算输入的梯度。每个节点只需要知道自己的局部导数

节点前向反向(局部导数)
×W+b\times W + bz=Wx+b\mathbf{z} = W\mathbf{x} + \mathbf{b}zx=W\frac{\partial \mathbf{z}}{\partial \mathbf{x}} = W^\topzW=x\frac{\partial \mathbf{z}}{\partial W} = \mathbf{x}^\top
ReLUh=max(0,z)\mathbf{h} = \max(0, \mathbf{z})hizi=1[zi>0]\frac{\partial h_i}{\partial z_i} = \mathbf{1}[z_i > 0](逐元素 0/1 门控)
MSEL=(y^t)2L = (\hat{y} - t)^2Ly^=2(y^t)\frac{\partial L}{\partial \hat{y}} = 2(\hat{y} - t)

梯度回传时,每经过一个节点就乘一次局部导数。对于第二层的权重 w2\mathbf{w}_2

Lw2=2(y^t)L/y^hy^/w2\frac{\partial L}{\partial \mathbf{w}_2} = \underbrace{2(\hat{y} - t)}_{\partial L / \partial \hat{y}} \cdot \underbrace{\mathbf{h}^\top}_{\partial \hat{y} / \partial \mathbf{w}_2}

对于第一层的权重 W1W_1,链子更长:

LW1=2(y^t)L/y^w2y^/hdiag(1[z1>0])h/z1xz1/W1\frac{\partial L}{\partial W_1} = \underbrace{2(\hat{y} - t)}_{\partial L / \partial \hat{y}} \cdot \underbrace{\mathbf{w}_2^\top}_{\partial \hat{y} / \partial \mathbf{h}} \cdot \underbrace{\text{diag}(\mathbf{1}[\mathbf{z}_1 > 0])}_{\partial \mathbf{h} / \partial \mathbf{z}_1} \cdot \underbrace{\mathbf{x}^\top}_{\partial \mathbf{z}_1 / \partial W_1}

这就是梯度消失的数学根源:链式乘法中如果每一项都小于 1(比如 sigmoid 导数峰值只有 0.25),乘几次就趋近零了。ReLU 的正侧导数恒为 1,所以梯度不会在正侧衰减——这是它取代 sigmoid 的根本原因。

来看一个具体例子——单个神经元,输入 x=2x = 2,权重 w=0.5w = 0.5,偏置 b=0.3b = -0.3,目标 t=1t = 1

w=0.5, b=−0.3target=1.0输入线性 wx+bReLU损失 (ŷ−t)²

点「前向传播」看数据怎么流过网络,再点「反向传播」看梯度怎么流回去。

一个单神经元的前向 / 反向传播。真实网络有几十亿参数,但每个参数的梯度都是这条链的一环。

前向传播算出预测 y^=0.70\hat{y} = 0.70,损失 L=0.09L = 0.09。反向传播从 LL 出发,逐层乘回去:L/w=1.2\partial L / \partial w = -1.2。这告诉优化器:ww 应该增大(梯度为负,沿负梯度方向走就是增大),从而把预测往目标推近。

真实网络有几十亿参数,但每个参数的梯度都是同样的链式乘法。PyTorch 的 autograd 和 JAX 的 grad 做的事情就是自动地把这条链算出来——你只写前向,它们帮你做反向。

损失函数:交叉熵还是 MSE

损失函数定义「什么叫好的预测」。神经网络常用的损失有两大类,分别对应回归和分类:

均方误差(MSE)——回归任务的默认选择:

LMSE=1ni=1n(y^iyi)2\mathcal{L}_{\text{MSE}} = \frac{1}{n}\sum_{i=1}^{n}(\hat{y}_i - y_i)^2

MSE 对大误差惩罚很重(平方),对异常值敏感。概率上等价于假设数据有高斯噪声时的极大似然估计。

交叉熵(Cross-Entropy)——分类任务的标配:

LCE=1ni=1nk=1Kyiklogp^ik\mathcal{L}_{\text{CE}} = -\frac{1}{n}\sum_{i=1}^{n}\sum_{k=1}^{K} y_{ik} \log \hat{p}_{ik}

其中 yiky_{ik} 是 one-hot 标签(正确类别为 1,其余为 0),p^ik\hat{p}_{ik} 是 softmax 输出的概率。交叉熵衡量的是模型预测的分布和真实分布之间的距离

为什么分类不用 MSE?回忆 logistic 回归里讲过的:sigmoid/softmax 把输出压到 (0,1)(0,1) 后,MSE 的梯度在「远端」几乎为零——预测错得很离谱时梯度反而小,训练会卡住。交叉熵配合 softmax 则给出恒定的梯度规模L/zk=p^kyk\partial L / \partial z_k = \hat{p}_k - y_k,干净利落。

任务输出层损失概率解释
回归恒等MSE高斯噪声 MLE
二分类sigmoidBinary CEBernoulli MLE
多分类softmaxCross-EntropyMultinomial MLE
语言模型softmax(词表维度)Cross-Entropy下一个 token 的 MLE

LLM 的训练目标就是一个超大规模的交叉熵:给定前面的 token,预测下一个 token 在 50000+ 维词表上的分布。每训练一个 batch 就是在做几十万次 softmax + 交叉熵。

权重初始化:起点决定速度

梯度下降需要起点——参数的初始值。随便初始化行不行?不行。

假设一层有 512 个神经元,权重从 N(0,1)\mathcal{N}(0, 1) 采样。输入 xx 的方差是 1,那么 z=i=1512wixiz = \sum_{i=1}^{512} w_i x_i 的方差就是 512——zz 的绝对值会非常大。经过 ReLU 后,大部分值落在正侧的线性区,网络的有效容量被浪费了;如果用 sigmoid/tanh,zz 极大导致输出饱和在 ±1\pm 1,导数为零,还没开始训梯度就消失了

解决方案:控制每层输出的方差

Xavier 初始化(Glorot & Bengio, 2010)——适合 sigmoid/tanh:

WN(0,2nin+nout)U[6nin+nout,6nin+nout]W \sim \mathcal{N}\left(0, \frac{2}{n_{\text{in}} + n_{\text{out}}}\right) \quad \text{或} \quad U\left[-\sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}, \sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}\right]

直觉:让前向传播的方差和反向传播的方差都保持一致——nin+noutn_{\text{in}} + n_{\text{out}} 是两层维度之和。

He 初始化(He et al., 2015)——适合 ReLU:

WN(0,2nin)W \sim \mathcal{N}\left(0, \frac{2}{n_{\text{in}}}\right)

因为 ReLU 会把一半输出截为零,方差直接砍半,所以分母用 ninn_{\text{in}} 而不是 (nin+nout)/2(n_{\text{in}} + n_{\text{out}})/2,把初始化方差放大一倍补偿回来。

偏置 bb 通常初始化为零。没有随机性——对称性由权重的随机初始化打破。如果所有权重也初始化为相同的值,同层的所有神经元会做完全一样的事,训练过程中永远不会分化。

经验法则:用 ReLU 就用 He,用 sigmoid/tanh 就用 Xavier,用 GELU 用 Xavier 或 PyTorch 默认的 Kaiming uniform 都行。大多数框架已经帮你做好了默认初始化,但自定义架构时仍然需要注意。

Batch Normalization 与 Dropout

网络训起来之后,有两个问题绕不开:训练不稳定过拟合。Batch Normalization 解决前者,Dropout 解决后者。

Batch Normalization:让中间层老实一点

训练过程中,每一层的输入分布在不断变化——前一层的参数在更新,它输出的均值和方差就在漂移。后一层不得不持续适应新的输入范围,训练效率低且不稳定。这种内部协变量偏移(Internal Covariate Shift) 是深度网络难训的核心原因之一。

Batch Normalization(Ioffe & Szegedy, 2015)的做法很直接:在每个 mini-batch 内,把每层的输入拉到均值为零、方差为一,再用两个可学习参数 γ\gammaβ\beta 恢复表达能力:

x^i=xiμBσB2+ϵ,yi=γx^i+β\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}, \quad y_i = \gamma \hat{x}_i + \beta

其中 μB\mu_BσB2\sigma_B^2 是当前 mini-batch 的均值和方差,ϵ\epsilon 防止除以零。γ\gammaβ\beta 是可学习参数——如果网络觉得标准化不好,它可以学回原来的分布。

Batch Norm 的好处:

  • 允许更大的学习率——标准化让损失面更平滑。
  • 减少对初始化的敏感——不管初始值怎么飘,BN 都能拉回来。
  • 轻微的正则化效果——mini-batch 的统计量带有噪声,相当于注入了随机性。

但 BN 有一个硬伤:依赖 batch size。当 batch size 很小(比如 1 或 2)时,μB\mu_BσB2\sigma_B^2 的估计非常差,效果崩溃。Transformer 训练序列很长、batch size 很小时,BN 就不好用了——这就是为什么 Transformer 选择了 Layer Normalization(对单条样本的所有特征做标准化,而非对 batch 维度)。

Dropout:随机关掉一半神经元

Dropout(Srivastava et al., 2014)是另一种正则化手段:训练时,以概率 pp(通常 0.1–0.5)随机把一些神经元的输出设为零。推理时不用 dropout,但把所有权重乘以 (1p)(1-p) 缩放。

h~=hm,miBernoulli(1p)\tilde{\mathbf{h}} = \mathbf{h} \odot \mathbf{m}, \quad m_i \sim \text{Bernoulli}(1-p)

直觉上,dropout 强迫网络不能依赖任何单个神经元——每个神经元都要在没有同伴的情况下也能工作。效果等价于训练了指数级多个子网络(每个 dropout mask 对应一个子网络),推理时是它们的集成。

Dropout 在 CNN 和传统全连接网络里非常有效。但在 Transformer 里用得比较克制(通常只在 FFN 后加 p=0.1p=0.1 的 dropout),因为 Layer Norm + 残差连接 + 大数据量本身就提供了足够的正则化。

让训练跑起来的关键

有了反向传播、好的激活函数、合理的初始化和归一化,理论上可以训练任意深的网络。但实践中还有几个工程上绕不开的问题:

批量梯度下降(mini-batch SGD)——不拿全部数据算一次梯度(太慢),也不只拿一个样本(太噪),而是每次取一个小批量(batch size 64–4096)。噪声反而帮助跳出局部最小值。

学习率调度——开头用小学习率(warm-up),让参数在初始化附近稳定住;中间放大,大步搜索;后期衰减(cosine decay),精细收敛。这套 warm-up + cosine 几乎是 Transformer 训练的标配。

Adam 优化器——不是裸用 L-\nabla L,而是给每个参数维护梯度均值梯度方差的滑动平均,自适应调步长。LLM 训练几乎全用 AdamW(Adam 加权重衰减)。

残差连接——即使有了好的初始化和 BN,超深网络仍然需要 skip connection y=f(x)+x\mathbf{y} = f(\mathbf{x}) + \mathbf{x},让梯度沿着 +x+\mathbf{x} 那条路直接流到底。Transformer 的每个子层都有残差连接。

梯度裁剪——RNN 和 LLM 训练时梯度偶尔会爆炸(loss spike),把超过阈值的梯度范数裁掉就能救回来。简单粗暴但救命。

这些技巧不是装饰——没有它们,GPT 级别的模型根本训不动。

这个想法在前沿里