反向传播与神经网络前向传播完整推导

分类: 神经网络基础 · 难度: 中级 · 关联讲座: L03

反向传播与神经网络前向传播完整推导

本文以 NER 二分类网络为载体,完整推导神经网络从输入到输出的前向传播过程、交叉熵损失函数的来源(从最大似然到公式),以及反向传播算法如何沿计算图逐层回传梯度。三个部分环环相扣:前向传播定义了计算图,交叉熵定义了优化目标,反向传播则高效地求出每个参数的梯度。

神经网络前向传播

📐 神经网络分类器完整前向传播推导

变量定义

  • xR5dx \in \mathbb{R}^{5d} = 窗口拼接词向量(输入层)
  • W(1)Rm×5dW^{(1)} \in \mathbb{R}^{m \times 5d} = 第一层权重矩阵(mm 为隐藏层维度)
  • b(1)Rmb^{(1)} \in \mathbb{R}^m = 第一层偏置
  • f()f(\cdot) = 逐元素非线性激活函数(如 tanh\tanh
  • hRmh \in \mathbb{R}^m = 隐藏层输出
  • uRmu \in \mathbb{R}^m = 输出权重向量
  • sRs \in \mathbb{R} = 标量评分

推导过程

第 1 步:线性变换。将输入投影到隐藏空间: z(1)=W(1)x+b(1)Rmz^{(1)} = W^{(1)} x + b^{(1)} \in \mathbb{R}^m

展开矩阵乘法的第 ii 个分量:zi(1)=j=15dWij(1)xj+bi(1)z^{(1)}_i = \sum_{j=1}^{5d} W^{(1)}_{ij} x_j + b^{(1)}_i

第 2 步:逐元素非线性激活: h=f(z(1))Rm,hi=f(zi(1))h = f(z^{(1)}) \in \mathbb{R}^m, \quad h_i = f(z^{(1)}_i)

第 3 步:输出评分(标量)。用权重向量 uu 做内积: s=uTh=i=1muihiRs = u^T h = \sum_{i=1}^{m} u_i h_i \in \mathbb{R}

第 4 步:转化为概率(二分类用 sigmoid): p=σ(s)=11+es(0,1)p = \sigma(s) = \frac{1}{1 + e^{-s}} \in (0, 1)

为什么需要非线性:若去掉 ff,则 s=uT(W(1)x+b(1))=(uTW(1))x+uTb(1)s = u^T (W^{(1)} x + b^{(1)}) = (u^T W^{(1)}) x + u^T b^{(1)},等价于单层线性模型 s=wTx+cs = w^T x + c,无论堆多少层都如此。非线性是神经网络表达能力的根本来源。

🔢 数值计算示例

设定d=2d = 2,窗口大小 3(5d5d \to 此例简化为窗口大小 3,即输入维度 =3d=6= 3d = 6),隐藏层 m=4m = 4,激活函数 tanh\tanh

W(1)=[0.50.20.10.40.30.20.10.30.60.20.10.50.20.40.30.10.60.10.30.10.20.50.40.3]R4×6W^{(1)} = \begin{bmatrix} 0.5 & -0.2 & 0.1 & 0.4 & -0.3 & 0.2 \\ -0.1 & 0.3 & 0.6 & -0.2 & 0.1 & 0.5 \\ 0.2 & 0.4 & -0.3 & 0.1 & 0.6 & -0.1 \\ 0.3 & -0.1 & 0.2 & 0.5 & -0.4 & 0.3 \end{bmatrix} \in \mathbb{R}^{4 \times 6}

x=[0.2,0.1,0.5,0.8,0.9,0.3]T,b(1)=[0,0,0,0]T,u=[1,1,1,1]Tx = [0.2, 0.1, 0.5, 0.8, 0.9, 0.3]^T,\quad b^{(1)} = [0, 0, 0, 0]^T,\quad u = [1, -1, 1, -1]^T

计算

  1. z1(1)=0.5(0.2)0.2(0.1)+0.1(0.5)+0.4(0.8)0.3(0.9)+0.2(0.3)=0.10.02+0.05+0.320.27+0.06=0.24z^{(1)}_1 = 0.5(0.2) - 0.2(0.1) + 0.1(0.5) + 0.4(0.8) - 0.3(0.9) + 0.2(0.3) = 0.1 - 0.02 + 0.05 + 0.32 - 0.27 + 0.06 = 0.24
  2. 类似计算其余分量(略),得 z(1)[0.24,0.61,0.14,0.02]Tz^{(1)} \approx [0.24, 0.61, 0.14, 0.02]^T
  3. 激活:h=tanh(z(1))[0.235,0.545,0.140,0.020]Th = \tanh(z^{(1)}) \approx [0.235, 0.545, 0.140, 0.020]^T
  4. 评分:s=uTh=0.2350.545+0.1400.020=0.190s = u^T h = 0.235 - 0.545 + 0.140 - 0.020 = -0.190
  5. 概率:p=σ(0.190)0.453p = \sigma(-0.190) \approx 0.453(约 45% 概率为正类)

💡 为什么这样做?

神经网络的本质是特征变换器。第一层把原始词向量组合成”更抽象的特征”,激活函数引入非线性,使得决策边界可以弯曲——比线性分类器(只能画直线分割)强大得多。

类比:你想区分猫和狗,仅靠”耳朵长度”(一维)不够,但”耳朵长度 + 毛发颜色组合”(非线性特征)就能做到。神经网络自动学习哪些特征组合有用。

⚠️ 常见误区

  1. 误区s=uThs = u^T h 输出的是概率 → 正确ss 是原始评分(可正可负,无界),经过 sigmoid 才变成 (0,1)(0,1) 区间的概率
  2. 误区:二分类用 sigmoid,多分类也用 sigmoid → 正确:多分类用 softmax(对所有类别归一化),二分类才用 sigmoid(等价于 2 类 softmax)
  3. 误区:偏置 bb 不重要 → 正确:偏置允许决策边界不过原点,大幅提升模型灵活性

交叉熵损失

📐 交叉熵损失从 MLE 到公式完整推导

变量定义

  • θ\theta = 模型参数
  • ptrue(c)p_{true}(c) = 真实分布(one-hot,正确类别为 1,其余为 0)
  • qθ(cx)q_\theta(c|x) = 模型预测的概率分布(softmax 输出)
  • H(p,q)H(p, q) = 交叉熵(p 和 q 的交叉熵)
  • NN = 训练样本数

推导过程

第 1 步:最大似然估计(MLE)。给定数据集 {(xi,yi)}i=1N\{(x_i, y_i)\}_{i=1}^N,最大化所有样本的联合对数似然: maxθi=1Nlogpθ(yixi)\max_\theta \sum_{i=1}^N \log p_\theta(y_i | x_i)

第 2 步:等价于最小化负对数似然(NLL): minθi=1Nlogpθ(yixi)=minθi=1NNLLi\min_\theta -\sum_{i=1}^N \log p_\theta(y_i | x_i) = \min_\theta \sum_{i=1}^N \text{NLL}_i

第 3 步:与交叉熵的联系。交叉熵的定义: H(ptrue,qθ)=c=1Cptrue(c)logqθ(cx)H(p_{true}, q_\theta) = -\sum_{c=1}^C p_{true}(c) \log q_\theta(c|x)

ptruep_{true} 为 one-hot(ptrue(y)=1p_{true}(y) = 1,其余为 0),所有 cyc \ne y 的项乘以 0 消失: H(ptrue,qθ)=1logqθ(yx)=logpθ(yx)H(p_{true}, q_\theta) = -1 \cdot \log q_\theta(y|x) = -\log p_\theta(y|x)

因此:最小化交叉熵 = 最大化正确类别的 log 概率 = MLE

第 4 步:整个数据集的平均损失(除以 NN 以便不同大小数据集可比): J(θ)=1Ni=1Nlogpθ(yixi)=1Ni=1NH(ptrue(i),qθ(xi))J(\theta) = -\frac{1}{N}\sum_{i=1}^N \log p_\theta(y_i | x_i) = \frac{1}{N}\sum_{i=1}^N H(p_{true}^{(i)}, q_\theta(\cdot|x_i))

第 5 步:softmax + cross-entropy 合并简化。若 pθ(cx)=softmax(sc)=escjesjp_\theta(c|x) = \text{softmax}(s_c) = \frac{e^{s_c}}{\sum_j e^{s_j}},则: logpθ(yx)=sy+logjesj-\log p_\theta(y|x) = -s_y + \log\sum_j e^{s_j}

这正是 PyTorch 的 F.cross_entropy(内部做 log-softmax + NLL)。

🔢 数值计算示例

设定:3 类分类(类别 0/1/2),正确标签为类别 1

变量
one-hot label pp[0,1,0][0, 1, 0]
模型 softmax 输出 qq[0.3,0.5,0.2][0.3, 0.5, 0.2]

计算

  1. 展开:H(p,q)=0log(0.3)1log(0.5)0log(0.2)H(p, q) = -0 \cdot \log(0.3) - 1 \cdot \log(0.5) - 0 \cdot \log(0.2)
  2. 化简:=log(0.5)=log20.693= -\log(0.5) = \log 2 \approx 0.693

对比(若预测更准 q=[0.1,0.8,0.1]q = [0.1, 0.8, 0.1]):H=log(0.8)0.223H = -\log(0.8) \approx 0.223,损失更低。

对比(若预测错误 q=[0.1,0.1,0.8]q = [0.1, 0.1, 0.8]):H=log(0.1)2.303H = -\log(0.1) \approx 2.303,损失很高。

结论:正确类别预测概率越高,交叉熵越低,梯度信号越弱(已经”学好了”)。

💡 为什么用交叉熵而不是 MSE?

信息论视角:交叉熵 H(p,q)H(p,q) 度量的是”用 qq 编码 pp 事件所需的平均比特数”。ppqq 越接近,H(p,q)H(p,q) 越小(趋近于 H(p,p)=H(p,p) = 熵)。最小化交叉熵就是让模型预测分布尽量接近真实分布。

梯度优势:对 softmax 输出用 MSE,梯度中会出现 σ(z)\sigma'(z)(sigmoid 导数),在饱和区几乎为零;而交叉熵的梯度是 y^y\hat{y} - y(预测值减真实值),简洁且无饱和问题。

⚠️ 常见误区

  1. 误区:交叉熵 = 信息熵 → 正确:信息熵 H(p)=plogpH(p) = -\sum p \log p 只取决于真实分布;交叉熵 H(p,q)=plogqH(p,q) = -\sum p \log q 还取决于模型预测;两者相差一个 KL 散度:H(p,q)=H(p)+DKL(pq)H(p,q) = H(p) + D_{KL}(p \| q)
  2. 误区:多标签(multi-label)分类用 softmax + cross-entropy → 正确:多标签(一个样本可属于多个类别)应用 binary cross-entropy(每个标签独立的 sigmoid);softmax 强制概率之和为 1,不适合多标签
  3. 误区log\log 底数是 10 → 正确:机器学习中统一用自然对数(底数 ee),单位是 nats(比特用 log2\log_2,但实践中混用,因为只相差常数倍,不影响优化)

反向传播

📐 反向传播完整推导:NER 二分类网络

网络定义

z(1)=Wx+b,h=tanh(z(1)),s=uTh,J=logσ(s)(y=1 的二元交叉熵)z^{(1)} = Wx + b, \quad h = \tanh(z^{(1)}), \quad s = u^T h, \quad J = -\log \sigma(s) \quad (y=1 \text{ 的二元交叉熵})

参数:θ={W,b,u,x}\theta = \{W, b, u, x\}(此处也对输入 xx 求梯度,以便更新词向量)


第 1 步Js\frac{\partial J}{\partial s}(损失对评分的梯度)

J=logσ(s)J = -\log \sigma(s)σ(s)=11+es\sigma(s) = \frac{1}{1+e^{-s}}

Js=1σ(s)σ(s)=1σ(s)σ(s)(1σ(s))=σ(s)1\frac{\partial J}{\partial s} = -\frac{1}{\sigma(s)} \cdot \sigma'(s) = -\frac{1}{\sigma(s)} \cdot \sigma(s)(1-\sigma(s)) = \sigma(s) - 1

直觉σ(s)(0,1)\sigma(s) \in (0,1),所以 Js=σ(s)1(1,0)\frac{\partial J}{\partial s} = \sigma(s)-1 \in (-1, 0)——评分越高,梯度越小(越接近 0),因为已经预测得很好了。


第 2 步Ju\frac{\partial J}{\partial u}(损失对输出权重 uu 的梯度)

s=uThs = u^T h,故 su=h\frac{\partial s}{\partial u} = h(列向量),链式法则:

Ju=Jssu=(σ(s)1)hRm\frac{\partial J}{\partial u} = \frac{\partial J}{\partial s} \cdot \frac{\partial s}{\partial u} = (\sigma(s)-1) \cdot h \in \mathbb{R}^m


第 3 步Jz(1)\frac{\partial J}{\partial z^{(1)}}(误差信号 δ\delta,关键中间量)

sh=uT\frac{\partial s}{\partial h} = u^Thz(1)=diag(1h2)\frac{\partial h}{\partial z^{(1)}} = \text{diag}(1 - h^2)(tanh 导数)

设上游梯度(从损失到 hh)为: Jh=Jssh=(σ(s)1)uTR1×m\frac{\partial J}{\partial h} = \frac{\partial J}{\partial s} \cdot \frac{\partial s}{\partial h} = (\sigma(s)-1) \cdot u^T \in \mathbb{R}^{1 \times m}

传过 tanh(逐元素乘,\odot 表示 Hadamard 积): δJz(1)=(σ(s)1)u(1h2)Rm\delta \equiv \frac{\partial J}{\partial z^{(1)}} = (\sigma(s)-1) \cdot u \odot (1 - h^2) \in \mathbb{R}^m

(将行向量转为列向量后与 1h21-h^2 逐元素相乘)


第 4 步:各参数梯度

利用 z(1)=Wx+bz^{(1)} = Wx + b 及矩阵微积分规则:

JW=δxTRm×n(外积,形状与 W 相同)\frac{\partial J}{\partial W} = \delta \cdot x^T \in \mathbb{R}^{m \times n} \quad \text{(外积,形状与 } W \text{ 相同)}

Jb=δRm(形状与 b 相同)\frac{\partial J}{\partial b} = \delta \in \mathbb{R}^m \quad \text{(形状与 } b \text{ 相同)}

Jx=WTδRn(传给词向量,用于联合训练)\frac{\partial J}{\partial x} = W^T \delta \in \mathbb{R}^n \quad \text{(传给词向量,用于联合训练)}


计算图(ASCII 示意)

x ──→ [z=Wx+b] ──→ [h=tanh(z)] ──→ [s=uᵀh] ──→ [J=-log σ(s)]
       ↑W,b              ↑                ↑u
  前向:→→→→→→→→→→→→→→→→→→→→→→→→→→→→→
  反向:←←←←←←←←←←←←←←←←←←←←←←←←←←←
       ∂J/∂W=δxᵀ    ∂J/∂z=δ        ∂J/∂s=σ(s)-1

每个节点只需记录自己的局部导数,不需要知道网络其他部分的结构。

🔢 数值计算示例(完整前向 + 反向)

设定d=2d=2m=2m=2W=I2W = I_2(单位矩阵),b=[0,0]Tb = [0,0]^Tu=[1,1]Tu = [1,1]^Tx=[0.6,0.4]Tx = [0.6, -0.4]^T,标签 y=1y=1

前向传播

步骤计算结果
z(1)=Wx+bz^{(1)} = Wx + b=[0.6,0.4]T= [0.6, -0.4]^T[0.6, 0.4]T[0.6,\ -0.4]^T
h=tanh(z(1))h = \tanh(z^{(1)})[tanh(0.6), tanh(0.4)][\tanh(0.6),\ \tanh(-0.4)][0.537, 0.380]T[0.537,\ -0.380]^T
s=uThs = u^T h1(0.537)+1(0.380)1(0.537) + 1(-0.380)0.1570.157
σ(s)\sigma(s)σ(0.157)\sigma(0.157)0.5390.539
J=logσ(s)J = -\log\sigma(s)log(0.539)-\log(0.539)0.6180.618

反向传播

步骤计算结果
Js=σ(s)1\frac{\partial J}{\partial s} = \sigma(s)-10.53910.539 - 10.461-0.461
Ju=(σ(s)1)h\frac{\partial J}{\partial u} = (\sigma(s)-1) h0.461×[0.537,0.380]T-0.461 \times [0.537, -0.380]^T[0.248, 0.175]T[-0.248,\ 0.175]^T
1h21 - h^2[10.5372, 10.3802][1-0.537^2,\ 1-0.380^2][0.712, 0.856]T[0.712,\ 0.856]^T
δ=(σ(s)1)u(1h2)\delta = (\sigma(s)-1) u \odot (1-h^2)0.461×[1,1]T[0.712,0.856]T-0.461 \times [1,1]^T \odot [0.712, 0.856]^T[0.328, 0.395]T[-0.328,\ -0.395]^T
JW=δxT\frac{\partial J}{\partial W} = \delta x^T[0.328,0.395]T[0.6,0.4][-0.328, -0.395]^T \cdot [0.6, -0.4][0.1970.1310.2370.158]\begin{bmatrix}-0.197 & 0.131 \\ -0.237 & 0.158\end{bmatrix}
Jb=δ\frac{\partial J}{\partial b} = \delta[0.328, 0.395]T[-0.328,\ -0.395]^T
Jx=WTδ\frac{\partial J}{\partial x} = W^T \deltaI[0.328,0.395]TI \cdot [-0.328, -0.395]^T[0.328, 0.395]T[-0.328,\ -0.395]^T

验证JW\frac{\partial J}{\partial W} 形状 2×2=2 \times 2 = WW 的形状 ✓,Jx\frac{\partial J}{\partial x} 形状 2=2 = xx 的形状 ✓

💡 反向传播的本质是什么?

反向传播的天才之处在于重用中间计算结果。如果你手动展开损失对每个参数的偏导数(拆掉所有复合函数),会得到大量重复子表达式。反向传播通过计算图把这些子表达式只计算一次,存起来复用

类比:计算 f(x)=((x+1)2+(x+1)3)×5f(x) = ((x+1)^2 + (x+1)^3) \times 5,先算 u=x+1u = x+1,再算 u2u^2u3u^3,比展开后各自对 xx 求导快得多。

时间复杂度是 O(n)O(n)nn 为参数数量)——每个参数的梯度计算量与前向传播等量级。这是深度学习可扩展到数十亿参数的根本原因。

⚠️ 常见误区

  1. 误区:反向传播 = 梯度下降 → 正确反向传播只负责计算梯度θJ\nabla_\theta J),梯度下降(或 Adam 等优化器)才负责用梯度更新参数θθηJ\theta \leftarrow \theta - \eta \nabla J)。两者是完全不同的操作。
  2. 梯度累加陷阱:若某节点(如词向量)被多个地方引用(同一个词在窗口中出现多次),其梯度必须累加所有路径传来的梯度,而不是取平均或覆盖。PyTorch 的 .grad 属性在多次 backward 时会自动累加(这也是为什么要 optimizer.zero_grad())。
  3. tanh 导数记忆tanh(z)=1tanh2(z)\tanh'(z) = 1 - \tanh^2(z),不要写成 1/cosh2(z)1/\cosh^2(z)(虽然等价,但前者可直接用前向已算出的 h=tanh(z)h = \tanh(z),后者还需重新算 cosh\cosh)。
  4. 误区:只有参数需要梯度 → 正确:NLP 中词向量也是参数,Jx\frac{\partial J}{\partial x} 非零,用于联合训练(fine-tuning embedding)。若想固定词向量,需要显式 requires_grad=False