神经网络基础与反向传播

面向 NLP 研究生的深度教材 · 改编自 Stanford CS224N Winter 2026 Lecture 3 (Diyi Yang)
覆盖:词向量内在/外在评估 · NER 窗口分类 · Softmax/Sigmoid · 现代激活族 (ReLU/GELU/SwiGLU)
交叉熵与 SGD · 矩阵微积分 · 雅可比 · 链式法则 · 上下游梯度 · 计算图 · 自动微分 · 数值梯度检验
版本 1.0 · 2026 春 · 加入详尽推导、形状约定、PyTorch 对照与扩展讨论

引论:为什么这一讲是 CS224N 全程的核心

在 CS224N 的整个学期中,Lecture 3 是分水岭。前两讲讨论的是"词向量"——一种数据驱动但相对静态的表示;从这一讲开始,你将接触到所有现代 NLP 系统(包括 GPT、Claude、Qwen、LLaMA)依赖的两个核心机制:

  1. 神经分类器 (neural classifier):在表示与决策之间引入多层非线性变换,让模型自己学习"什么样的特征对任务有用"。
  2. 反向传播 (backpropagation):用矩阵微积分自动计算梯度,让"自己学习"在数学上可行、在工程上高效。
为什么必须自己推导一遍 backprop?
PyTorch 早就把 loss.backward() 封装到一行了——你完全可以不懂这些原理就训练 BERT。但只要你做研究,就会撞到以下问题:
  • 梯度爆炸 / 消失(RNN、深层 Transformer 的训练崩溃)
  • 自定义算子(写 CUDA kernel、Triton kernel、新型注意力)
  • 调试模型不收敛(80% 的 bug 在反向,而不是前向)
  • 论文阅读(LoRA、FlashAttention、Mixture-of-Experts 的推导都建立在本讲的符号上)
当你能够把 ∂L/∂W 写到纸上、亲手验证形状对应,你就掌握了与 Karpathy 在 "Yes you should understand backprop" 中所说"同样的 mental model"。

本讲的两条主线

本讲容易让初学者迷失:表面上 Diyi 在白板上推了几十张公式,但其实只在做两件事——

前向传播 x → h → s → J(θ) "算 loss" 反向传播 ∂J/∂θ via chain rule "算 gradient" 计算图记录中间值 梯度沿计算图反向流 SGD: θ ← θ − α ∂J/∂θ
图 0.1:神经网络训练的双轨:前向算损失,反向算梯度。中间的"计算图"是连接两者的桥梁。

本教材将循序渐进地把这两条主线讲透。你会先做一个完整的 NER 二分类例子(章节 2–4),然后把同一个例子用三种粒度求导:单变量手算 → 向量化雅可比 → 通用计算图(章节 5–8)。最后我们用 PyTorch 把所有公式落地为代码(章节 9)。

第一章:词向量评估(Lecture 2 回顾)

进入神经网络之前,先回顾上一讲的 word2vec/GloVe。你可能问:词向量有什么"评估"的?给个余弦相似度不就好了吗?事实上,整个 NLP 评估方法论可以浓缩为两条线:

1.1 评估的两种范式:Intrinsic vs Extrinsic

维度Intrinsic(内在)Extrinsic(外在)
定义子任务探针任务上测真实下游任务上测
例子词类比、相似度相关性、语义聚类NER 准确率、问答 EM、机器翻译 BLEU
计算成本秒级,可在 notebook 跑小时级,需要完整 pipeline
解释力能定位单一组件的好坏反映系统整体的好坏
风险子任务好≠下游好(相关性问题)难定位是哪个子模块拖后腿
研究生应该知道的潜规则
当你在论文中只汇报 intrinsic 指标(比如"我们的词向量在 WordSim-353 上 +2 分"),审稿人会立刻质疑:"so what for downstream?"——这就是为什么 ELMo / BERT / GPT 等里程碑论文都拼命堆砌下游榜单(GLUE, SuperGLUE, MMLU),因为只有 extrinsic 评估才说服得了人。

1.2 词类比任务与"国王-皇后"几何

词类比 (word analogy) 是 Mikolov 2013 提出的经典内在测评。给定一对"前提"a:b 与一个"提问" c:?,要求模型给出 d 使得 a:b :: c:d。例如:

man : woman :: king : ?  ⟶ 期望 queen

解法基于向量加法:把 a→b 的方向加到 c 上,然后在词表中找最近邻:

词类比的公式
$$ d^\star \;=\; \arg\max_{i\,\in\,V\,\setminus\,\{a,b,c\}} \;\frac{(x_b - x_a + x_c)^\top x_i}{\|x_b - x_a + x_c\|} $$ 注意:必须把输入词 $\{a,b,c\}$ 从候选集中剔除("discarding the input words"),否则模型会偷懒返回 $c$ 本身。
x₁ x₂ man woman king queen "性别"方向 "性别"方向(平移)
图 1.1:king − man + woman ≈ queen。性别方向在嵌入空间中是一个近似平移向量。
这种几何并非总是可靠
2019 年的 Ethayarajh 等 指出 "king − man + woman = queen" 实际上对 SGNS 模型只有约 50% 的成功率,更多归功于余弦距离的偏置而非真正的几何加法。但这一现象作为探针任务仍然有价值——它让我们看到嵌入能够承载语义结构,而这是后续所有上下文表示(ELMo/BERT)的研究起点。

1.3 WordSim-353 与相关性评估

WordSim-353 是另一类内在评估:给定 353 个词对,每对由人工打 0–10 分的相似度,然后看模型的余弦相似度与人类打分的 Spearman/Pearson 相关系数。例如:

Word 1Word 2Human (mean)余弦相似度示例
tigercat7.350.78
tigertiger10.001.00
bookpaper7.460.72
stockphone1.620.21
stockjaguar0.920.06

用 Spearman 而不是 Pearson 是因为:人类打分是序号尺度("7.35" 与 "7.46" 的差异未必比 "1.62" 与 "1.31" 的差异更小),排序相关性更鲁棒。

GloVe 论文里的关键表
Lecture 2 给出的对比表里,GloVe 42B 在 WS353/MC/RG/SCWS/RW 上几乎全面超过 SVD 与 word2vec:
  • GloVe 42B → WS353: 75.9, MC: 83.6, RG: 82.9
  • CBOW 100B → 68.4 / 79.6 / 75.4(数据更多但模型架构吃亏)
这说明训练目标(GloVe 的共现矩阵分解)有时比数据规模更重要——这是一个值得记住的反直觉结论。

1.4 GloVe 在 NER 上的下游表现

把视角切到 extrinsic 评估。在 CoNLL-2003 NER 任务上,几种词向量做特征接入相同分类器的结果:

ModelDev F1Test F1ACEMUC7
Discrete (sparse one-hot)91.085.477.473.4
SVD90.885.777.373.7
HPCA92.688.781.780.7
CBOW93.188.282.281.1
GloVe93.288.382.982.2

两个观察:(1) 任何密集嵌入都比 one-hot 好,因为它在未见词上有泛化能力;(2) GloVe 和 CBOW 差异微小——下游任务对嵌入的具体方法不敏感,但对嵌入维度、训练语料大小很敏感。

回顾到这里,我们已经能回答 Lecture 2 留下的最后一个问题——"信息存在但不是线性的怎么办?"答案就是:用非线性的神经网络代替线性投影。下面正式进入神经分类器。

第二章:从 NER 切入神经分类器

2.1 NER 任务与子模块化

命名实体识别 (Named Entity Recognition, NER) 是 NLP 最经典的序列标注任务:给定一段文本,找出并分类其中的实体提及。例如:

Last night, Paris Hilton wowed in a sequin gown.
[Paris, Hilton] → PER (人名)

Samuel Quinn was arrested in the Hilton Hotel in Paris in April 1989.
[Samuel, Quinn] → PER, [Hilton, Hotel, Paris] → LOC, [April, 1989] → DATE

注意:同一个单词 "Paris" 在不同上下文里既可能是 PER 也可能是 LOC——这正是 word2vec 这种静态嵌入无法独立解决的问题,必须依靠上下文(即"窗口")。

NER 的用处覆盖整个 NLP 栈:

2.2 窗口分类:上下文的最小单元

Lecture 3 把 NER 简化成一个二分类问题:"中心词是否为地点 (LOC)?"。给定一句话,对每个位置 t 取一个固定大小的窗口(如左右各 2 个词),拼接成一个长向量:

the museums in Paris are amazing 窗口 (window size = 2) ↑ 中心词 x_window = [ x_museums ; x_in ; x_Paris ; x_are ; x_amazing ] ∈ ℝ^(5d) 任务:y = 1 if Paris 是 LOC else 0 每个 x_w 是 d 维 GloVe 向量,拼接后整窗维度为 5d
图 2.1:窗口分类把"序列标注"转化为"逐词分类",是最简单的上下文建模。

实际系统通常用 multi-class softmax 同时输出 {PER, LOC, ORG, DATE, O} 五类,但二分类版本让我们能把数学讲清楚。

2.3 监督学习符号体系

用一套贯穿全讲的符号:

符号约定
$$ \mathcal{D} = \{(x_i, y_i)\}_{i=1}^{N} $$
  • $x_i \in \mathbb{R}^d$:输入(可以是词索引、词向量、窗口拼接)
  • $y_i \in \{1,\dots,C\}$:标签(C 个类别)
  • $N$:样本数
  • $\theta$:模型所有可学习参数(包括词嵌入!)

统计学习里,参数 θ 是要找的对象;在深度学习里,θ 包含嵌入本身——这一点稍后会反复出现。

2.4 从 Softmax 到神经分类器

先复习经典的 softmax 分类器:

Softmax 分类器
$$ p(y \mid x) \;=\; \frac{\exp(W_{y,:}\,x)}{\sum_{c=1}^{C} \exp(W_{c,:}\,x)} $$ $W \in \mathbb{R}^{C \times d}$ 是仅有的可学习参数;输入 $x$ 是稀疏的、固定的(one-hot 或预训练嵌入)。

这种模型给出的决策边界是线性的——在原始特征空间画一条直线(高维下是一张超平面)把类别分开。如果数据像图 2.2(左)那样线性可分,softmax 就足够;但如果数据呈环形(图 2.2 右),无论怎么调 W 都无能为力。

(a) softmax 能搞定的线性可分 (b) 需要非线性边界 绿/红 = 两类样本
图 2.2:左边线性分类器够用;右边需要把数据"扭曲"到一个新的表示空间。神经网络的中间层就是干这件事。

神经网络分类器与 softmax 的差别有两点:

  1. 同时学习 W 和表示:词向量 $x = L e$(其中 $L$ 是嵌入矩阵,$e$ 是 one-hot)也被当做参数训练。
  2. 引入中间层:在 softmax 之前先做若干个 $h = f(Wx + b)$ 的非线性变换,把数据搬到一个对线性 softmax 友好的新空间。
关键洞察
深度网络看似复杂,本质上还是"在最后一层做线性分类"——只不过在那之前用 L−1 层网络把数据重塑成线性可分的形状。这就是 Manning 在课件上写的"typically, it is linear relative to the pre-final layer representation"。

2.5 二元 NER 网络的完整结构

我们用最小的两层网络完成 NER 二分类:

NER 二元神经分类器
$$ \begin{aligned} x &\in \mathbb{R}^{5d} && \text{(窗口拼接)} \\ z &= W x + b, \quad W \in \mathbb{R}^{n \times 5d},\ b \in \mathbb{R}^{n} && \text{(线性投影到隐层)} \\ h &= f(z), \quad h \in \mathbb{R}^{n} && \text{(逐元素非线性)} \\ s &= u^\top h, \quad u \in \mathbb{R}^{n} && \text{(打分)} \\ \hat p &= \sigma(s) = \frac{1}{1 + e^{-s}} && \text{(sigmoid → 概率)} \end{aligned} $$
x ∈ ℝ⁵ᵈ museums in Paris are amazing h = f(Wx+b) s = uᵀ h σ(s) → P(LOC)
图 2.3:两层网络的标准结构。x 在底部,经过 W、bias、非线性 f、点积 u、sigmoid σ 后输出概率。

这个网络有 3 组可学习参数:W(n×5d)、b(n)、u(n)。如果再把嵌入 L 也算上,整体 θ 包括 所有词向量 + 网络权重——这是 Lecture 3 反复强调的"在深度学习里 θ 包括数据表示本身"。

剩下要解决的问题只有一个:如何让这个网络的输出 $\hat p$ 接近真实标签 y?下面的章节给出答案。

第三章:激活函数大全

中间层 $h = f(z)$ 中的 $f$ 是逐元素的非线性函数。本节系统地介绍从 1980s 的 sigmoid 到 2024 的 SwiGLU 的演化,并解释为什么非线性是必需的

3.1 Sigmoid 与 Tanh:饱和家族

Sigmoid (Logistic)
$$ \sigma(z) \;=\; \frac{1}{1 + e^{-z}}, \qquad \sigma'(z) = \sigma(z)\,(1-\sigma(z)) $$
Tanh(双曲正切)
$$ \tanh(z) \;=\; \frac{e^{z} - e^{-z}}{e^{z} + e^{-z}} = 2\sigma(2z) - 1, \qquad \tanh'(z) = 1 - \tanh^2(z) $$
Sigmoid σ(z) z 1 0 Tanh 1 -1 ReLU GELU / SiLU
图 3.1:四类典型激活函数的形状对比。Sigmoid/Tanh 饱和,ReLU 分段线性,GELU/SiLU 是 ReLU 的平滑版本。

Sigmoid 与 Tanh 的两个关键缺陷(这是它们在深网络里被淘汰的原因):

  1. 梯度饱和:当 $|z|$ 大时,$\sigma'(z) \to 0$,反向传播的梯度乘到深层时几乎被压缩为零。
  2. 非零中心(仅 sigmoid):输出永远 ≥ 0,导致下一层的权重在更新时只能同正或同负,训练效率低。Tanh 解决了这个问题(输出 [-1,1] 对称),但仍有饱和。
什么时候还该用它们?
  • 输出层概率:sigmoid 仍是二分类输出层的标准(产生 [0,1])。
  • 门控机制:LSTM 的输入门、遗忘门、输出门都用 sigmoid 控制 [0,1] 的"通过比例"。
  • 注意力权重:softmax 是 sigmoid 的多元推广。
所以这些"老"激活并没有消失,只是退到了特定的位置。

3.2 ReLU 与"死亡 ReLU"问题

ReLU (Rectified Linear Unit)
$$ \text{ReLU}(z) \;=\; \max(z, 0), \qquad \text{ReLU}'(z) = \begin{cases} 1 & z > 0 \\ 0 & z \le 0 \end{cases} $$

ReLU 由 Nair & Hinton (2010) 推广,是深度学习革命的"幕后英雄"之一。它的好处显而易见:

但 ReLU 有个臭名昭著的问题——死亡 ReLU (Dying ReLU):一旦某神经元落入 $z < 0$ 区域并且学习率较大,更新步会让它进一步远离零点,导致梯度永远为 0,神经元"死掉"。Leaky ReLU 与 PReLU 是直接的补丁:

Leaky ReLU / PReLU
$$ \text{LeakyReLU}(z) = \begin{cases} z & z > 0 \\ \alpha z & z \le 0 \end{cases} $$ $\alpha = 0.01$ 为 Leaky;$\alpha$ 可学习则为 Parametric ReLU (He et al. 2015)。

3.3 现代激活:GELU、SiLU、Swish

2017 年起,Transformer 时代催生了一族平滑的非线性,它们在大模型上表现更好:

GELU (Gaussian Error Linear Unit)
$$ \text{GELU}(x) = x \cdot P(X \le x), \quad X \sim \mathcal{N}(0,1) \;\;\approx\;\; x \cdot \sigma(1.702\,x) $$ 直觉:把"是否激活"看成一个随机变量,激活强度 = $x$ 被采样到的概率乘以 $x$ 自身。
SiLU / Swish
$$ \text{SiLU}(x) = x \cdot \sigma(x), \quad \text{Swish}(x) = x \cdot \sigma(\beta x) $$ Swish 是 SiLU 的可学习广义化(β 可训练)。SiLU 等价于 β=1 的 Swish。

这两个家族都满足:

这种平滑特性让 Adam 等基于二阶估计的优化器(Adam/AdamW)更稳定,所以现代大模型(BERT、GPT-2、ViT)默认用 GELU。

3.4 门控线性单元:GLU 与 SwiGLU (LLaMA/Qwen)

Dauphin et al. (2017) 在语言建模上提出门控线性单元:

GLU (Gated Linear Unit)
$$ \text{GLU}(x) = (xV + v) \otimes \sigma(xW + b) $$ $\otimes$ 是逐元素乘。直觉:左半边是"数据",右半边是"门",门值在 0–1 之间决定信息保留多少。

Shazeer 2020 的 GLU Variants Improve Transformer 把 sigmoid 换成 Swish:

SwiGLU
$$ \text{SwiGLU}(x) = (xV + c) \otimes \text{Swish}_\beta(xW + b) $$

SwiGLU 现在是 LLaMA 系列、Qwen、Mistral、DeepSeek 的标配。它有两个隐含的好处:

  1. 表达力更高:门控机制是一种"内置的二阶交互",类似于 Mixture-of-Experts 的软选择。
  2. 不增加 FLOPs(按维度归一):把 FFN 隐层维度从 4d 调整为 8d/3,参数量与计算量与 ReLU FFN 相同。
实战经验
对绝大多数 NLP 研究任务:
  • 训练 RNN/小模型 → 首选 ReLU(简单、快、稳)。
  • 训练 Transformer encoder/decoder → 首选 GELU
  • 训练 LLM-scale 模型 → SwiGLU(与现代论文对齐)。
  • 用 sigmoid/tanh 当激活会被审稿人怀疑你是不是 2014 年穿越来的。

3.5 为什么必须非线性?通用近似定理

假设网络是纯线性的:$h^{(1)} = W_1 x$,$h^{(2)} = W_2 h^{(1)}$,…,最后输出 $y = W_L h^{(L-1)}$。把代换全部展开:

$$ y = W_L W_{L-1} \cdots W_1\, x \;=\; W\, x \quad \text{where} \quad W = W_L \cdots W_1 $$ 任何无激活的多层网络,等价于一个单层线性变换。所谓的"深度"完全没有任何意义。

反之,通用近似定理 (Universal Approximation Theorem, Cybenko 1989; Hornik 1991) 保证:一个仅含一层隐藏单元的前馈网络,只要激活函数是非常数有界连续,就能以任意精度近似任何紧致集上的连续函数。这是深度学习的表达力依据

线性 (欠拟合) 2 个非线性 (恰好) 过多非线性 (过拟合)
图 3.2:从欠拟合到过拟合:非线性的"用量"决定了函数族的容量。模型容量与正则化是另一个 trade-off(CS 229 / CS 230 的主题)。
深度 vs 宽度
通用近似定理只说"宽度足够大的一层网络"可以近似任意函数,但需要的宽度可能是指数级的。Telgarsky 2016 等工作证明:深度网络可以用多项式参数表达浅网络需要指数参数才能表达的函数。这就是"为什么要深"的理论解释——而不只是"经验上效果更好"。

第四章:损失函数与优化

4.1 交叉熵的信息论起源

到此为止我们的目标是最大化正确类的概率 $p(y_i | x_i)$。但深度学习习惯最小化损失。两者等价:

$$ \max_\theta \prod_{i=1}^{N} p(y_i \mid x_i; \theta) \;\;\iff\;\; \min_\theta \;-\sum_{i=1}^{N} \log p(y_i \mid x_i; \theta) $$ 左边是极大似然,右边是负对数似然 (NLL)。这种"对数+负号"在工程上有数值稳定性优势(避免溢出)。

用交叉熵 (cross entropy) 的视角来看,假设:

那么单样本的交叉熵为:

$$ H(p, q) \;=\; -\sum_{c=1}^{C} p(c)\,\log q(c) $$ 由于 $p$ 是 one-hot,求和只剩 $-\log q(y_i)$ —— 这就是 NLL!

展开交叉熵:

$$ H(p, q) = -\sum_{c=1}^{C} p(c)\,\log q(c) = -p(y_i)\,\log q(y_i) - \sum_{c \ne y_i} 0 \cdot \log q(c) = -\log q(y_i) $$

所以 cross-entropy loss 与 NLL 在 one-hot 目标下完全一致。$\square$

信息论直觉
$H(p, q)$ 衡量"用 q 的码本去编码来自 p 的样本,平均要花多少 bits"。当 $q = p$ 时 $H(p,q) = H(p)$ 是,是理论下界。优化交叉熵=让你的模型分布 q 尽量接近真实分布 p。

4.2 等价的极大似然视角

把 N 个样本累计起来,得到训练目标:

完整训练目标
$$ J(\theta) \;=\; \frac{1}{N}\sum_{i=1}^{N} -\log p_\theta(y_i \mid x_i) \;\;(+\; \lambda\,\Omega(\theta)\;\text{正则项可选}) $$

对二分类 NER,可以写成 sigmoid 的形式:

$$ J(\theta) = -\sum_{i=1}^{N} \big[ y_i \log \sigma(s_i) + (1 - y_i)\log(1 - \sigma(s_i)) \big] $$ 这就是logistic loss,是二分类时 NLL 的简化形式。

4.3 随机梯度下降 (SGD)

有了损失函数 $J(\theta)$,我们要找最小化它的 θ。基本算法就是梯度下降

梯度下降更新规则
$$ \theta^{\text{new}} \;=\; \theta^{\text{old}} - \alpha\,\nabla_\theta J(\theta) $$ $\alpha$ 是学习率 (learning rate / step size)。逐分量写法: $$ \theta_j^{\text{new}} = \theta_j^{\text{old}} - \alpha \frac{\partial J(\theta)}{\partial \theta_j^{\text{old}}} $$

实际中我们用 SGD:每次只用一个 mini-batch近似真实梯度。这在 NLP 中尤其重要——训练语料动辄 10 亿 token,全数据集梯度根本算不起。

SGD vs full-batch GD
  • full-batch:梯度精确,但单步太慢,且容易卡在 saddle point。
  • SGD:梯度是有噪声的估计,但每步便宜;噪声反而帮助逃出 saddle。
  • 实际选择:mini-batch SGD(batch size = 32~4096)+ Adam/AdamW 优化器。

4.4 词向量是不是参数?

这是 Lecture 3 的一个隐藏问题。答案:看你怎么用。三种主流策略:

策略描述适用场景
冻结 (frozen)用 GloVe 加载嵌入,不更新训练数据少(< 10k 样本),怕过拟合到训练词
微调 (fine-tune)嵌入和网络一起更新训练数据多(> 100k 样本),追求最佳性能
从零训练 (from scratch)嵌入用随机初始化,从头训数据量极大(如 LLM 预训练),或词表与预训练完全不重合
微调的危险
当训练集很小、词表很大时,微调会让训练集中出现过的词朝任务目标移动,而未出现过的词停在原始位置——这导致测试时两者距离变远,模型反而泛化变差。Manning 在 Lecture 2 用一张图演示了这个现象。

核心信息:在深度学习里,$\theta$ 不只是 W 和 b,还可以包含输入表示 $x = L e$。这是接下来推导梯度时要特别注意的。

第五章:矩阵微积分速成

要计算 $\nabla_\theta J(\theta)$,必须把"一元微积分"扩展到向量和矩阵。这一章给出全部需要的工具,并解释 Lecture 3 中令很多人困惑的"形状约定 vs 雅可比形式"之争。

5.1 标量函数的梯度

单输入单输出 $f(x) = x^3 \Rightarrow \dfrac{df}{dx} = 3x^2$。直觉:"输入变 ε,输出变多少?" 在 $x=1$ 处,$f'(1) = 3$,意味着 $f(1.01) \approx 1 + 3 \cdot 0.01 = 1.03$(实际 $1.01^3 \approx 1.0303$)。
多输入单输出 $f(\mathbf{x}) = f(x_1, x_2, \dots, x_n) \in \mathbb{R}$ 的梯度: $$ \frac{\partial f}{\partial \mathbf{x}} = \left[ \frac{\partial f}{\partial x_1},\;\frac{\partial f}{\partial x_2},\;\dots,\;\frac{\partial f}{\partial x_n} \right] $$ 这是一个 $1 \times n$ 的行向量(按雅可比约定)。

5.2 向量函数的雅可比矩阵

把"多输入"再扩展到"多输出",就是雅可比 (Jacobian):

雅可比矩阵
$$ f(\mathbf{x}) = [f_1(x_1,\dots,x_n),\dots,f_m(x_1,\dots,x_n)] $$ $$ \frac{\partial \mathbf{f}}{\partial \mathbf{x}} = \begin{bmatrix} \frac{\partial f_1}{\partial x_1} & \cdots & \frac{\partial f_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial f_m}{\partial x_1} & \cdots & \frac{\partial f_m}{\partial x_n} \end{bmatrix} \in \mathbb{R}^{m \times n} $$ 元素:$\left(\dfrac{\partial \mathbf{f}}{\partial \mathbf{x}}\right)_{ij} = \dfrac{\partial f_i}{\partial x_j}$

关键约定:雅可比的形状是 m × n(输出维度 × 输入维度)。换句话说:

5.3 链式法则的矩阵化

单变量复合函数的链式法则:

$$ z = 3y,\; y = x^2 \;\Rightarrow\; \frac{dz}{dx} = \frac{dz}{dy}\frac{dy}{dx} = 3 \cdot 2x = 6x $$ "导数相乘"。

多变量复合函数:

$$ \mathbf{h} = f(\mathbf{z}),\; \mathbf{z} = W\mathbf{x} + b \;\Rightarrow\; \frac{\partial \mathbf{h}}{\partial \mathbf{x}} = \frac{\partial \mathbf{h}}{\partial \mathbf{z}} \cdot \frac{\partial \mathbf{z}}{\partial \mathbf{x}} $$ "雅可比相乘"。这是 backprop 的唯一数学武器。
链式法则为什么这么"配合"?

形状对应得天衣无缝:若 $\mathbf{h} \in \mathbb{R}^a, \mathbf{z} \in \mathbb{R}^b, \mathbf{x} \in \mathbb{R}^c$,则:

  • $\partial \mathbf{h}/\partial \mathbf{z}$ 是 a × b
  • $\partial \mathbf{z}/\partial \mathbf{x}$ 是 b × c
  • 它们的乘积是 a × c,正是 $\partial \mathbf{h}/\partial \mathbf{x}$ 该有的形状 ✓

这就是为什么 Manning 反复说"multivariable calculus is just like single-variable calculus if you use matrices"。

5.4 三大基本雅可比

NER 网络只用到 3 类操作,对应 3 个基本雅可比。把它们背下来——以后做 Transformer、RNN、注意力的推导都建立在这之上。

(A) 逐元素激活函数

$$ \mathbf{h} = f(\mathbf{z}),\quad h_i = f(z_i) $$ $$ \frac{\partial \mathbf{h}}{\partial \mathbf{z}} = \begin{pmatrix} f'(z_1) & & 0 \\ & \ddots & \\ 0 & & f'(z_n) \end{pmatrix} = \text{diag}(f'(\mathbf{z})) $$
$$ \left(\frac{\partial \mathbf{h}}{\partial \mathbf{z}}\right)_{ij} = \frac{\partial h_i}{\partial z_j} = \frac{\partial}{\partial z_j} f(z_i) = \begin{cases} f'(z_i) & i = j \\ 0 & i \ne j \end{cases} $$ 当 $i \ne j$ 时,$h_i$ 只与 $z_i$ 有关,与 $z_j$ 无关,所以偏导为 0。结果是对角矩阵。 $\square$

(B) 线性变换

$$ \mathbf{z} = W\mathbf{x} + \mathbf{b} $$ $$ \boxed{ \frac{\partial \mathbf{z}}{\partial \mathbf{x}} = W \qquad \frac{\partial \mathbf{z}}{\partial \mathbf{b}} = I } $$
$z_i = \sum_k W_{ik} x_k + b_i$,所以 $\partial z_i / \partial x_j = W_{ij}$,正是 $W$ 的元素。 对偏置:$\partial z_i / \partial b_j = \delta_{ij}$,即单位矩阵。 $\square$

(C) 内积(标量输出)

$$ s = \mathbf{u}^\top \mathbf{h} = \sum_i u_i h_i $$ $$ \frac{\partial s}{\partial \mathbf{u}} = \mathbf{h}^\top \qquad \frac{\partial s}{\partial \mathbf{h}} = \mathbf{u}^\top $$ 注意输出形状:$\partial s / \partial \mathbf{u}$ 是 1 × n(标量对向量),所以是 $\mathbf{h}^\top$ 而不是 $\mathbf{h}$。
求以下雅可比,并标注形状:
  1. $\mathbf{y} = A\mathbf{x}$,求 $\partial \mathbf{y}/\partial A$。
  2. $f = \|\mathbf{x}\|_2^2 = \mathbf{x}^\top \mathbf{x}$,求 $\partial f/\partial \mathbf{x}$。
  3. $\mathbf{y} = \text{softmax}(\mathbf{z})$,求 $\partial \mathbf{y}/\partial \mathbf{z}$。
提示:第 1 题答案是一个 3 维张量;第 3 题答案是 $\text{diag}(\mathbf{y}) - \mathbf{y}\mathbf{y}^\top$。

5.5 形状约定 (Shape Convention)

讲到这里,要面对 Lecture 3 最让人头疼的地方:对矩阵参数 $W$ 求导时,结果应该长什么样?

纯数学上:若标量 $s$ 关于 $W \in \mathbb{R}^{n \times m}$ 求导,雅可比应是 1 × nm 的"行向量"(因为输出是 1 维,输入有 $nm$ 个元素)。但 SGD 更新 $W^{\text{new}} = W^{\text{old}} - \alpha \nabla_W J$ 要求 $\nabla_W J$ 与 $W$ 形状相同,即 n × m

两个约定的冲突
约定形状优势劣势
Jacobian form1 × nm(拉直)链式法则形状对应难以塞回 SGD 更新
Shape conventionn × m(同 W)SGD 可直接用需手动对齐转置

CS224N 与几乎所有深度学习实现(PyTorch、TensorFlow)选择 shape convention。规则:

梯度的 shape 与参数本身的 shape 一致。

所以:

工作流推荐:"用雅可比形式 compute,用 shape convention format"——先按数学正确推导,最后转置/重排让形状对齐参数。

第六章:手工推导神经网络梯度

本章把第五章的工具应用到第二章的 NER 网络。整个过程分 4 步:

  1. 把网络拆成原子操作(章节 6.1);
  2. 对要求导的参数应用链式法则(6.2);
  3. 把每一段写成第五章的已知雅可比(6.2 后半 + 6.3);
  4. 形状约定校验并整理(6.4–6.6)。

6.1 拆分式:把网络写成原子操作

原网络:

$$ s = u^\top h, \quad h = f(Wx + b) $$

引入中间变量 $z$,拆成 4 个原子等式(每一行都对应一个第五章的"已知雅可比"):

原子形式
$$ \begin{aligned} s &= u^\top h \\ h &= f(z) \\ z &= Wx + b \\ x &\;\text{(输入)} \end{aligned} $$

这种拆分让"复合函数"变成一串单步函数——而每一单步都正好对应第五章的基本雅可比。

6.2 链式法则三步走

我们目标:求 $\partial s / \partial b$(对偏置求导)。应用链式法则:

$$ \frac{\partial s}{\partial b} = \frac{\partial s}{\partial h} \cdot \frac{\partial h}{\partial z} \cdot \frac{\partial z}{\partial b} $$

每段单独写出来:

step 1 $s = u^\top h$ → $\dfrac{\partial s}{\partial h} = u^\top$   1 × n
step 2 $h = f(z)$ → $\dfrac{\partial h}{\partial z} = \text{diag}(f'(z))$   n × n
step 3 $z = Wx + b$ → $\dfrac{\partial z}{\partial b} = I$   n × n

把三段乘起来:

$$ \frac{\partial s}{\partial b} = u^\top \cdot \text{diag}(f'(z)) \cdot I = u^\top \odot f'(z)^\top $$ (这里用了 diag 矩阵乘向量等价于 Hadamard 积 的恒等式。)

结果是 1 × n。最后按 shape convention 转置成 n × 1

$$ \boxed{ \nabla_b s = u \odot f'(z) \in \mathbb{R}^{n} } $$

6.3 复用:上游误差信号 δ

现在考虑同时对 $W$ 求导:

$$ \frac{\partial s}{\partial W} = \frac{\partial s}{\partial h} \cdot \frac{\partial h}{\partial z} \cdot \frac{\partial z}{\partial W} $$ $$ \frac{\partial s}{\partial b} = \frac{\partial s}{\partial h} \cdot \frac{\partial h}{\partial z} \cdot \frac{\partial z}{\partial b} $$

前两段一模一样!这正是"复用计算"的来源。我们给前两段一个名字 δ(delta,"误差信号"):

上游梯度 δ
$$ \boxed{\;\delta \;:=\; \frac{\partial s}{\partial h} \cdot \frac{\partial h}{\partial z} \;=\; u^\top \odot f'(z) \in \mathbb{R}^{1 \times n}\;} $$ 解释:"loss 对中间变量 z 的灵敏度"。它累积了从输出层一路反向传到这里的所有梯度信息。

有了 δ,对任意上游参数的梯度都可以写成"δ 乘以本层的局部雅可比"——这是 backprop 的核心套路。

6.4 对矩阵参数 W 求导

用 δ 的写法:

$$ \frac{\partial s}{\partial W} = \delta \cdot \frac{\partial z}{\partial W} $$

$\partial z / \partial W$ 看上去棘手(z 是向量,W 是矩阵),但单元素地看就简单了。考虑某个具体的 $W_{ij}$:

s u₂ h₁ h₂ f(z₁)= =f(z₂) W₂₃ b₂ x₁ x₂ x₃ +1
图 6.1:W₂₃ 这条连接只贡献给 z₂(红色路径),与 z₁ 无关。因此 ∂zᵢ/∂Wⱼₖ 在 i ≠ j 时为零。

$W_{ij}$ 只出现在 $z_i = \sum_k W_{ik} x_k + b_i$ 这一行。因此:

$$ \frac{\partial z_i}{\partial W_{ij}} = \frac{\partial}{\partial W_{ij}}\sum_k W_{ik} x_k = x_j $$ 其他 $z_l$($l \ne i$)都与 $W_{ij}$ 无关,偏导为 0。

把它整合回链式法则:

$$ \frac{\partial s}{\partial W_{ij}} = \sum_l \delta_l \cdot \frac{\partial z_l}{\partial W_{ij}} = \delta_i \cdot x_j $$

这正好是外积 (outer product) $\delta^\top x^\top$(按雅可比形式)或 $\delta x^\top$(按 shape convention)的元素 $(i,j)$。所以:

对 W 的梯度 (shape convention)
$$ \boxed{\; \nabla_W s = \delta\, x^\top \in \mathbb{R}^{n \times d_x} \;} $$ 其中 δ 是 n × 1 列向量(按 shape convention),$x^\top$ 是 1 × d_x 行向量。

6.5 转置之谜:为什么是外积?

很多初学者在这里迷失:"为什么不是 $W \cdot x$ 或别的形式?"

用一个具体的小例子($n=3, d_x=4$):

$$ \delta = \begin{bmatrix} \delta_1 \\ \delta_2 \\ \delta_3 \end{bmatrix},\quad x^\top = [x_1, x_2, x_3, x_4] $$ $$ \delta x^\top = \begin{bmatrix} \delta_1 x_1 & \delta_1 x_2 & \delta_1 x_3 & \delta_1 x_4 \\ \delta_2 x_1 & \delta_2 x_2 & \delta_2 x_3 & \delta_2 x_4 \\ \delta_3 x_1 & \delta_3 x_2 & \delta_3 x_3 & \delta_3 x_4 \end{bmatrix} \in \mathbb{R}^{3 \times 4} $$ 形状对齐:$\delta$ 与 $z$ 同形(n),$x^\top$ 与 $z = Wx$ 中"哪一列乘 x"对应(即每行用所有 x 加权)。
直觉理解
$W_{ij}$ 是第 i 个隐藏单元用第 j 个输入特征的权重。它的梯度 = (上游对第 i 个隐藏单元的灵敏度 $\delta_i$) × (该位置实际输入值 $x_j$)。
"每个输入到每个输出 — 你想得到外积" —— Manning 原话。

6.6 对输入 x 求导:词向量学习的关键

如果嵌入也是可学习参数(fine-tune 模式),那需要 $\partial s / \partial x$:

$$ \frac{\partial s}{\partial x} = \delta \cdot \frac{\partial z}{\partial x} = \delta \cdot W $$ 按 shape convention: $$ \boxed{\; \nabla_x s = W^\top \delta \in \mathbb{R}^{d_x \times 1} \;} $$

这里出现的 $W^\top$ 是 backprop 的标志性现象:正向传播用 W,反向传播用 $W^\top$。对于这个 NER 例子,$x$ 是 5 个词向量的拼接,所以 $\nabla_x s$ 也要切回 5 段,分别加到对应词的嵌入上。

梯度形状速查
参数形状梯度公式 (shape conv.)
$W \in \mathbb{R}^{n \times d_x}$n × d_x$\delta\, x^\top$
$b \in \mathbb{R}^{n}$n$\delta$
$u \in \mathbb{R}^{n}$n$h$
$x \in \mathbb{R}^{d_x}$d_x$W^\top \delta$
其中 $\delta = u \odot f'(z) \in \mathbb{R}^{n}$ 是上游误差信号(按 shape convention 列向量)。

第七章:反向传播与计算图

上一章我们靠手算导出了 4 个梯度。但深网络有几十层、几百个变量,逐个手算不现实。反向传播 (backpropagation) 把"手算"升级成沿计算图自动执行的算法

7.1 计算图:神经网络的中间表示

把网络改写为有向无环图 (DAG)

· matmul + add f tanh · dot x W b u Wx z h s
图 7.1:NER 网络的计算图。每个圆是一个算子;边携带中间结果。这就是 PyTorch 在 backward() 内部维护的数据结构。

7.2 前向传播与缓存

沿 DAG 按拓扑序计算每个节点的输出值,并缓存到内存中——反向时要用到这些激活值。

# 伪代码
class Node:
    def forward(inputs):
        out = compute(inputs)
        self.cache = inputs    # 必须保留!
        return out
激活检查点 (gradient checkpointing)
缓存所有激活值会占大量显存。Chen et al. 2016 提出梯度检查点:只缓存少量节点,反向时重新前向算其余激活——以计算量换显存。这是训练大模型(如 70B LLaMA)的标配技巧。

7.3 单节点 backprop:上游 / 局部 / 下游

一个节点 $h = f(z)$ 的反向传播只做一件事

f ∂h/∂z (local) z h ∂s/∂z (下游) ∂s/∂h (上游) [下游] = [上游] × [局部]
图 7.2:单节点 backprop 的"上游 × 局部 = 下游"心法。这是反向传播的"原子操作"。

形式化地写:

$$ \underbrace{\frac{\partial s}{\partial z}}_{\text{downstream}} = \underbrace{\frac{\partial s}{\partial h}}_{\text{upstream}} \cdot \underbrace{\frac{\partial h}{\partial z}}_{\text{local}} $$

这就是链式法则的本质——节点不需要知道整张图,只需要:

  1. 从输出方向接到上游梯度 $\partial s/\partial h$;
  2. 自己缓存的输入计算局部雅可比 $\partial h/\partial z$;
  3. 把两者相乘得到下游梯度 $\partial s/\partial z$,传给前一个节点。

7.4 多输入节点:多个局部梯度

如果一个节点有多个输入(如 matmul 节点同时接收 $W$ 和 $x$),那么有多个局部梯度,每个输入收到一份下游梯度:

× ∂z/∂W, ∂z/∂x W ∂s/∂W = ∂s/∂z · ∂z/∂W x ∂s/∂x = ∂s/∂z · ∂z/∂x z ∂s/∂z (上游)
图 7.3:z = Wx 这个节点有两个输入,反向时各自得到一份下游梯度。

7.5 分支处的梯度求和

反过来,如果一个变量被多个下游节点使用("分叉/扇出"),它的总梯度是所有路径的梯度之和

分支求和规则
$$ \frac{\partial f}{\partial y} = \sum_{i=1}^{n} \frac{\partial f}{\partial y_i^{(\text{next})}} \cdot \frac{\partial y_i^{(\text{next})}}{\partial y} $$ 其中 $\{y_1^{(\text{next})}, \dots, y_n^{(\text{next})}\}$ 是 $y$ 的所有"后继"节点。

例:

$$ a = x + y,\quad b = \max(y, z),\quad f = ab $$ $y$ 同时被 $a$ 和 $b$ 用到,所以: $$ \frac{\partial f}{\partial y} = \frac{\partial f}{\partial a}\frac{\partial a}{\partial y} + \frac{\partial f}{\partial b}\frac{\partial b}{\partial y} $$
为什么是求和?
直觉上:如果 $y$ 微小扰动 $\Delta y$,那么"通过 a 走的影响" 与 "通过 b 走的影响"是独立叠加的——这正是全微分定理在多变量链式法则中的体现。

7.6 算子直觉:+ 分发 / max 路由 / × 切换

Lecture 3 后半给出了几个非常直观的"算子身份",背下来对调试 backprop 很有帮助:

算子反向行为原因
+(加法)分发 (distribute):把上游梯度原样传给所有输入$\partial(a+b)/\partial a = 1$, $\partial(a+b)/\partial b = 1$
max路由 (route):把上游梯度全部给"获胜方",其余为 0$\partial \max(a,b)/\partial a = 1$ 当 $a > b$ 时
×(乘法)切换 (switch):上游梯度乘以"另一个输入"$\partial(ab)/\partial a = b$, $\partial(ab)/\partial b = a$
copy/branch求和 (sum):所有下游梯度相加见 7.5

7.7 一次性完成整张图的反向

正确的 backprop 不应该对每个参数独立跑一遍(那样会重复计算 δ),而是一次性反向遍历整张图:

通用 backprop 算法
  1. Forward:按拓扑序计算每个节点的输出,缓存激活。
  2. Backward
    1. 把输出梯度初始化为 1($\partial s/\partial s = 1$)。
    2. 反拓扑序遍历每个节点:
      • 把所有"后继"节点回传的梯度相加(分支处求和)。
      • 用局部雅可比把梯度传给所有"前驱"。

关键性质:反向的时间复杂度与前向的相同。这是 backprop 的伟大之处——它把"指数级朴素方法"压缩到了"线性"。

对计算图中 $|E|$ 条边,前向恰好遍历每条边一次;反向也只遍历每条边一次(用本地雅可比一次性算所有梯度)。所以 $T_{\text{back}} = O(T_{\text{forward}})$。$\square$

7.8 数值实例:手工算到底

用 Lecture 3 经典例子:

$$ f(x,y,z) = (x+y) \cdot \max(y, z),\quad x=1, y=2, z=0 $$

前向

反向(输出 $f$ 的梯度初始化为 1):

+ max × x 1 grad=2 y 2 grad=2+3=5 z 0 grad=0 a=3 b=2 grad=2 grad=3 f=6 grad=1
图 7.4:完整数值反向。每个节点旁同时标注前向值(黑)和反向梯度(蓝)。注意 y 收到了两条路径的贡献并求和。

逐步推导

× $f = ab \Rightarrow \dfrac{\partial f}{\partial a} = b = 2,\;\dfrac{\partial f}{\partial b} = a = 3$("切换")。
+ $a = x + y$:把上游梯度 2 分发给 x 和 y。所以 $\dfrac{\partial f}{\partial x} = 2,\;\dfrac{\partial f}{\partial y}\bigg|_{\text{via}\,a} = 2$。
max $b = \max(y, z)$,由于 $y > z$,胜者是 y:上游梯度 3 全给 y,z 得 0。所以 $\dfrac{\partial f}{\partial y}\bigg|_{\text{via}\,b} = 3,\;\dfrac{\partial f}{\partial z} = 0$。
分支求和 $y$ 被两条路径用到,总梯度 = $2 + 3 = 5$。

验证:直接求偏导 $\partial f/\partial y$: $f = (x+y)\max(y, z) = (x+y) \cdot y$(因为 $y > z$), 所以 $\partial f/\partial y = \max(y,z) + (x+y) = 2 + 3 = 5$ ✓。算法和手算结果一致。

继续这个例子:把 $z$ 改为 3(其他不变),重做一遍 backprop。预期 $\partial f/\partial y$ 会变成多少?
提示:现在 max 的胜者切换为 z;分支求和时只剩一条路径。

第八章:自动微分与工业实现

8.1 反向模式 AutoDiff 的本质

第七章的算法用工程术语叫反向模式自动微分 (reverse-mode automatic differentiation)。它有两个孪生兄弟:

模式遍历方向代价适合
Forward-mode AD输入 → 输出O(输入数) × forward少输入、多输出
Reverse-mode AD输出 → 输入O(输出数) × forward多输入、单输出(神经网络!)
Symbolic differentiation符号级展开可能指数爆炸简单函数、解析解
Numerical (finite diff.)差分O(参数数) × forward仅用于校验

神经网络损失 $J(\theta)$ 的输入维度(θ)有亿级,但输出是 1 个标量——反向模式恰好把"O(输入)"换成"O(输出)=O(1)",所以"反向"才是深度学习的正确姿势。

8.2 PyTorch 是怎么算梯度的?

PyTorch 在每次前向时动态构建计算图(dynamic graph / define-by-run),TensorFlow 1.x 用静态图(define-then-run)。两者数学等价。简化伪代码:

class ComputationalGraph:
    def forward(inputs):
        # 1. 沿拓扑序计算
        for gate in self.graph.nodes_topologically_sorted():
            gate.forward()       # 缓存 inputs
        return loss

    def backward():
        # 2. 反拓扑序,每个节点用链式法则
        for gate in reversed(self.graph.nodes_topologically_sorted()):
            gate.backward()      # 应用本地雅可比
        return inputs_gradients

每种节点(gate)只需要实现两个方法:

这是为什么 PyTorch 自定义算子要继承 torch.autograd.Function 并实现 forwardbackward 两个静态方法。

import torch

class MyReLU(torch.autograd.Function):
    @staticmethod
    def forward(ctx, z):
        ctx.save_for_backward(z)          # 缓存
        return z.clamp(min=0)

    @staticmethod
    def backward(ctx, grad_h):
        (z,) = ctx.saved_tensors
        local = (z > 0).float()           # 局部雅可比 (对角)
        return grad_h * local             # 上游 × 局部 = 下游
这正是 Lecture 3 反复强调的
"Each node type needs to know how to compute its output and how to compute the gradient wrt its inputs given the gradient wrt its output." 你的自定义算子 = 实现 forward + 实现 [下游 = 上游 × 局部]。

8.3 数值梯度检查 (gradcheck)

你写了一个自定义 backward,怎么确认它正确?用有限差分近似真实梯度:

中心差分
$$ f'(x) \approx \frac{f(x+h) - f(x-h)}{2h}, \quad h \approx 10^{-4} $$ 误差是 $O(h^2)$,比单侧差分($O(h)$)准。

对每个参数 $\theta_i$ 单独算一次数值梯度,与 backward 输出对比。如果两者相对误差 < 1e-6,basically 一致。

def gradcheck(f, params, eps=1e-4):
    analytic = backward_grad(f, params)
    for i in range(len(params)):
        p = params.copy()
        p[i] += eps;   f_plus  = f(p)
        p[i] -= 2*eps; f_minus = f(p)
        numeric = (f_plus - f_minus) / (2*eps)
        assert abs(analytic[i] - numeric) / max(abs(numeric), 1e-8) < 1e-6

PyTorch 内置了 torch.autograd.gradcheck,做研究时强烈推荐写新算子时用一下。

为什么不直接用数值梯度训练?
对 GPT-3 这样的 175B 参数模型:
  • 每个参数需要 2 次前向 → 总开销 = 350B × forward;
  • backprop 只需 1 次反向 = 1 × forward 的代价。
差距 11 个数量级。数值方法只配做校验

8.4 backprop 不是黑盒:你必须懂的坑

Karpathy 写过一篇必读:"Yes you should understand backprop"。他列举了几个仅靠 loss.backward() 无法发现的 bug:

  1. Sigmoid 饱和:当 $z$ 很大时 $\sigma'(z) \approx 0$,梯度直接消失。如果你的输出层 sigmoid 接 cross-entropy,PyTorch 的 BCEWithLogitsLoss 用 log-sum-exp 把这俩合二为一以保数值稳定——单独用 sigmoid + log 容易爆。
  2. Dying ReLU:上文已述。
  3. 不可导的运算argmaxrounddiscrete sampling 处梯度为 0,要么用 Gumbel-Softmax,要么用 REINFORCE。
  4. 梯度爆炸:RNN 沿时间反向乘很多次 W,如果 $\|W\| > 1$ 就爆。修复:gradient clipping。
  5. 错误的求和维度:对 batch 维度求和而非平均,会让 effective learning rate 与 batch size 耦合,难以迁移。

8.5 梯度爆炸与梯度消失预告

当网络变深,链式法则会把 $L$ 个雅可比连乘起来:

$$ \frac{\partial s}{\partial \theta^{(1)}} = \frac{\partial s}{\partial h^{(L)}} \cdot \frac{\partial h^{(L)}}{\partial h^{(L-1)}} \cdots \frac{\partial h^{(2)}}{\partial h^{(1)}} \cdot \frac{\partial h^{(1)}}{\partial \theta^{(1)}} $$

每个雅可比的奇异值若集中在 1 附近,梯度能稳定传到底;若 < 1,连乘后趋近 0(梯度消失,如深 sigmoid RNN);若 > 1,连乘后发散(梯度爆炸,如未 clip 的 LSTM)。

解决方案的演化(CS224N 后续讲会详讲):

第九章:综合实战与扩展阅读

9.1 从零实现 NER 窗口分类器(PyTorch)

把全部理论落地为代码。下面这段 100 行内能跑的 PyTorch 实现,对应 Assignment 2 风格。

import torch, torch.nn as nn, torch.optim as optim

class NERWindowClassifier(nn.Module):
    def __init__(self, vocab_size, embed_dim=50, window_size=2, hidden=100):
        super().__init__()
        self.window = window_size
        self.embed  = nn.Embedding(vocab_size, embed_dim)
        # x ∈ R^((2*window+1)*embed_dim) → h ∈ R^hidden → score ∈ R
        self.W = nn.Linear((2*window_size+1)*embed_dim, hidden)
        self.u = nn.Linear(hidden, 1, bias=False)
        # 激活:现代选 GELU
        self.f = nn.GELU()

    def forward(self, token_ids):     # token_ids: [batch, 2k+1]
        emb  = self.embed(token_ids)           # [B, 2k+1, d]
        flat = emb.view(emb.size(0), -1)       # [B, (2k+1)d]
        z    = self.W(flat)                    # [B, hidden]
        h    = self.f(z)                       # [B, hidden]
        s    = self.u(h).squeeze(-1)           # [B]
        return s                                # logits (未 sigmoid)

# 训练循环
model     = NERWindowClassifier(vocab_size=10000)
optimizer = optim.AdamW(model.parameters(), lr=1e-3)
criterion = nn.BCEWithLogitsLoss()             # 内置数值稳定的 sigmoid+CE

for epoch in range(10):
    for token_ids, labels in train_loader:
        logits = model(token_ids)
        loss   = criterion(logits, labels.float())
        optimizer.zero_grad()
        loss.backward()                        # ← 整章第七章
        optimizer.step()                       # ← SGD/Adam 更新
代码与教材的对应
  • self.embed ↔ 嵌入矩阵 L(章节 2.4)
  • self.W ↔ W、b(章节 2.5)
  • self.u ↔ 输出向量 u
  • self.f = nn.GELU() ↔ 现代激活(章节 3.3)
  • BCEWithLogitsLoss ↔ 数值稳定的交叉熵(章节 4.1)
  • loss.backward() ↔ 整个第七、八章
  • optimizer.step() ↔ 章节 4.3 的 SGD 更新

9.2 与 Assignment 2 的衔接

CS224N Winter 2026 的 Assignment 2 要求你手算梯度(不能调用 backward())来实现一个依存句法分析器 (dependency parser)。需要的全部数学武器就是本讲内容:

建议工作流:

  1. 在草稿纸上把网络写成原子形式(6.1)。
  2. 对每个参数应用链式法则,标注每段的 shape。
  3. 把"前两段"抽象成 δ 复用(6.3)。
  4. 翻译成 NumPy 代码。
  5. gradcheck。误差 > 1e-6 → 回去检查转置 / 求和维度 / 形状对齐。

9.3 进一步阅读与论文

  1. Rumelhart, Hinton, Williams (1986): Learning representations by back-propagating errors. Nature. 必读·历史 — backprop 的奠基论文。
  2. Karpathy (2016): Yes you should understand backprop. Medium 博客。必读·实战
  3. Baydin et al. (2017): Automatic Differentiation in Machine Learning: a Survey. JMLR. 系统综述
  4. Goodfellow, Bengio, Courville (2016): Deep Learning, Chapter 6.5 (Back-Propagation and Other Differentiation Algorithms). deeplearningbook.org 教材级
  5. Shazeer (2020): GLU Variants Improve Transformer. arXiv:2002.05202. 现代激活
  6. Hendrycks & Gimpel (2016): Gaussian Error Linear Units (GELUs). arXiv:1606.08415. 现代激活
  7. Chen et al. (2016): Training Deep Nets with Sublinear Memory Cost. arXiv:1604.06174. 工程 — gradient checkpointing。
  8. CS224N 课程笔记: Matrix Calculus Notes 同期补充
  9. Math 51 Textbook (Stanford): 在线版 数学基础
  10. Universal Approximation: Cybenko 1989, Hornik 1991. 理论基础
  11. Telgarsky (2016): Benefits of depth in neural networks. COLT. 深度的理论

本讲总结

三句话总览
  1. 神经网络 = 多个"线性 + 非线性"层堆叠。最后一层之前的层做"特征学习",最后一层做线性分类(softmax/sigmoid)。
  2. 反向传播 = 沿计算图按反拓扑序应用链式法则。核心心法:[下游] = [上游] × [局部]
  3. 形状约定让数学回到工程。Jacobian 形式便于推导;shape convention 便于 SGD 实现。两者用转置桥接。

掌握这三点,你就拥有了阅读现代论文(注意力机制、LoRA、MoE、FlashAttention)的全部数学基础。接下来 Lecture 4 我们会把网络从前馈扩展到循环神经网络 (RNN),并在 Lecture 5 进入 Transformer——届时 backprop 会沿时间方向应用 (BPTT),会引出新的挑战。