Lecture 04: 神经语言模型
核心主题:NPLM (Bengio 2003)、RNN、BPTT、梯度消失/爆炸、LSTM
1. 神经概率语言模型 (NPLM, Bengio et al. 2003)
1.1 核心公式
条件概率(softmax 输出):
未归一化 log 概率:
输入表示(拼接嵌入向量):
1.2 参数表
| 参数 | 维度 | 角色 |
|---|---|---|
| $C$ | $\mathbb{R}^{|V| \times m}$ | 词嵌入矩阵(所有词共享) |
| $H$ | $\mathbb{R}^{h \times (N-1)m}$ | 输入 → 隐藏层权重 |
| $d$ | $\mathbb{R}^h$ | 隐藏层偏置 |
| $U$ | $\mathbb{R}^{|V| \times h}$ | 隐藏 → 输出层权重 |
| $W$ | $\mathbb{R}^{|V| \times (N-1)m}$ | 直接连接权重(可选跳跃连接) |
| $b$ | $\mathbb{R}^{|V|}$ | 输出偏置 |
1.3 参数量对比
| 模型 | 参数量级 | 说明 |
|---|---|---|
| N-gram | $O(|V|^N)$ | 指数级增长,存储所有 N-gram 计数 |
| NPLM | $O(|V| \cdot N \cdot m)$ | 线性级,通过共享嵌入实现泛化 |
1.4 架构图
[输出层] ← softmax(y) → P(w_t = i | context)
↑
(Wx + b) + U·tanh(Hx + d) ← "主要计算在此"
↑ ↑
[直接连接 W] [tanh 隐藏层 H]
↑ ↑
x = [C(w_{t-n+1}), ..., C(w_{t-1})] ← 拼接嵌入
↑
[查表 C] ← 共享嵌入矩阵
↑
index for w_{t-n+1}, ..., w_{t-1}
1.5 实验结果
- Shakespeare 语料:正确学到 "the cat sat" → "on"(概率 60.25%)
- WikiText-2:使用 GPT-2 tokenizer(vocab=50257, emb=64, hidden=128)
- 验证了嵌入共享机制的泛化能力:相似上下文产生相似预测
2. 循环神经网络 (RNN)
2.1 RNN 方程
隐藏状态更新:
输出计算:
预测概率:
- $\phi_h$:激活函数(tanh 或 ReLU)
- $W_{hh}$:循环权重(记忆单元),使信息在时间步之间传递
- 时间展开:$h_0 \to h_1 \to h_2 \to \ldots \to h_T$
2.2 损失函数
交叉熵损失(对所有时间步求和):
3. 时间反向传播 (BPTT)
3.1 对 $W_{yh}$ 的梯度(简单情况)
$W_{yh}$ 只直接影响当前时间步的输出,梯度计算直接:
3.2 对 $W_{hh}$ 的梯度(关键难点)
$W_{hh}$ 影响所有未来隐藏状态,需要累加所有路径的梯度贡献:
核心项 — 隐藏状态之间的梯度传播:
3.3 Jacobian 矩阵
相邻时间步的 Jacobian:
其中 $\text{Diag}(\phi'_h)$ 是激活函数导数构成的对角矩阵。对于 tanh,$\phi'_h \in (0, 1]$。
4. 梯度消失与爆炸
4.1 范数分析
其中:
- $\gamma_h = \|\text{Diag}(\phi'_h)\| \leq 1$(tanh 导数最大为 1,sigmoid 最大为 0.25)
- $\gamma_w = \|W_{hh}\|$(权重矩阵的谱范数)
4.2 两种情况
| 条件 | 结果 | 后果 |
|---|---|---|
| $\gamma_h \cdot \gamma_w < 1$ | $(\gamma_h \gamma_w)^{t-k} \to 0$ | 梯度消失 — 无法学习长距离依赖 |
| $\gamma_h \cdot \gamma_w > 1$ | $(\gamma_h \gamma_w)^{t-k} \to \infty$ | 梯度爆炸 — 训练不稳定,loss 发散 |
4.3 解决方案
| 方案 | 解决问题 | 方法 |
|---|---|---|
| 梯度裁剪 | 梯度爆炸 | 若 $\|g\| > \text{threshold}$,则 $g \leftarrow \frac{\text{threshold}}{\|g\|} \cdot g$ |
| Leaky Integration | 梯度消失 | $h_t = \alpha h_{t-1} + (1-\alpha) f(x_t, h_{t-1})$,保留部分旧状态 |
| LSTM / GRU | 梯度消失 | 门控机制 + 加法更新,梯度可无阻碍流动 |
5. LSTM (长短期记忆网络)
5.1 四个组件
1) 遗忘门(决定丢弃什么信息):
2) 输入门(决定存储什么新信息):
3) 候选细胞值:
4) 细胞状态更新:
5) 输出门:
6) 隐藏状态输出:
5.2 为什么 LSTM 解决梯度消失
细胞状态 $C_t$ 使用加法更新(而非乘法),梯度可以沿着"恒定误差传送带"(Constant Error Carousel) 无阻碍流动:
当遗忘门 $f_t \approx 1$ 时:
梯度不衰减,信息可以跨越很长的时间步传播。这与 vanilla RNN 中 $\frac{\partial h_t}{\partial h_{t-1}} = \text{Diag}(\phi'_h) \cdot W_{hh}$(连乘导致衰减)形成鲜明对比。
5.3 实验结果 (WikiText-2)
| Epoch | Train PPL | Valid PPL |
|---|---|---|
| 1 | 1521.01 | 971.39 |
| 5 | 398.48 | 433.80 |
| 10 | 212.12 | 317.88 |
测试集 PPL = 326.15(2层 LSTM, hidden=200, 10 个 epoch)
6. PyTorch 自动微分 (Autograd)
核心概念:PyTorch 通过计算图自动追踪所有涉及 requires_grad=True 张量的操作,调用 .backward() 即可自动计算梯度。
import torch
# 创建需要梯度的张量
x = torch.ones(2, 2, requires_grad=True)
y = x + 2
z = y * y * 3
out = z.mean()
# 反向传播
out.backward()
print(x.grad) # tensor([[4.5000, 4.5000],
# [4.5000, 4.5000]])
验证:$\text{out} = \frac{3}{4}\sum(x+2)^2$,因此 $\frac{\partial \text{out}}{\partial x} = \frac{3}{2}(x+2) = \frac{3}{2} \times 3 = 4.5$ ✓
Tensor/NumPy 共享内存
import torch
a = torch.ones(5)
b = a.numpy() # 共享内存!
a.add_(1) # a 和 b 都变成 [2, 2, 2, 2, 2]
print(b) # [2. 2. 2. 2. 2.]
# 反向:numpy -> tensor 也共享
import numpy as np
c = np.ones(5)
d = torch.from_numpy(c)
np.add(c, 1, out=c) # c 和 d 都变成 [2, 2, 2, 2, 2]
.clone() 或 .copy()。
7. Micrograd(手写自动微分引擎)
Micrograd 是 Karpathy 实现的极简自动微分引擎,核心是 Value 类,演示链式法则如何在计算图中传播梯度:
class Value:
def __init__(self, data, _children=(), _op=''):
self.data = data
self.grad = 0
self._backward = lambda: None
self._prev = set(_children)
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data + other.data, (self, other), '+')
def _backward():
self.grad += out.grad # dL/d(self) = dL/d(out) * 1
other.grad += out.grad # dL/d(other) = dL/d(out) * 1
out._backward = _backward
return out
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data * other.data, (self, other), '*')
def _backward():
self.grad += other.data * out.grad # dL/d(self) = other * dL/d(out)
other.grad += self.data * out.grad # dL/d(other) = self * dL/d(out)
out._backward = _backward
return out
def backward(self):
# 拓扑排序 -> 逆序应用链式法则
topo = []
visited = set()
def build_topo(v):
if v not in visited:
visited.add(v)
for child in v._prev:
build_topo(child)
topo.append(v)
build_topo(self)
self.grad = 1
for v in reversed(topo):
v._backward()
backward() 通过拓扑排序保证每个节点在其所有消费者之后才被处理。
8. NPLM 完整实现 (BengioNPLM)
import torch
import torch.nn as nn
class BengioNPLM(nn.Module):
"""
Neural Probabilistic Language Model (Bengio et al., 2003)
y = b + Wx + U * tanh(d + Hx)
"""
def __init__(self, vocab_size, embedding_dim, context_size, hidden_dim):
super().__init__()
self.context_size = context_size
self.embedding_dim = embedding_dim
# C: 共享词嵌入矩阵 |V| x m
self.emb = nn.Embedding(vocab_size, embedding_dim)
# H: 输入 -> 隐藏层 (h x (N-1)*m)
self.hidden = nn.Linear(context_size * embedding_dim, hidden_dim)
# U: 隐藏 -> 输出层 (|V| x h)
self.hidden_to_vocab = nn.Linear(hidden_dim, vocab_size)
# b: 输出偏置 (|V|,)
self.output_bias = nn.Parameter(torch.zeros(vocab_size))
# W: 直接连接(可选跳跃连接) (|V| x (N-1)*m)
self.direct = nn.Linear(context_size * embedding_dim, vocab_size)
def forward(self, input_ids):
# input_ids: (B, context_size) — 上下文词索引
B = input_ids.size(0)
# 查表 + 拼接: (B, context_size * embedding_dim)
x = self.emb(input_ids).reshape(B, -1)
# tanh 隐藏层: tanh(Hx + d)
h = torch.tanh(self.hidden(x))
# 输出: U*h + b + W*x
logits = self.hidden_to_vocab(h) + self.output_bias
logits = logits + self.direct(x)
return logits # (B, vocab_size)
训练循环要点:
- 损失函数:
nn.CrossEntropyLoss()(内含 softmax) - 优化器:Adam 或 SGD
- 数据:滑动窗口切分上下文 + 目标词对
核心要点总结
- NPLM 比 N-gram 参数量从指数级降为线性级,通过共享嵌入矩阵 $C$ 实现语义泛化,相似词自动获得相似预测
- RNN 的 BPTT 本质上是序列化的,$\prod_{j=k}^{t-1} \frac{\partial h_{j+1}}{\partial h_j}$ 连乘无法并行化,这是后续 Transformer 要解决的核心问题
- 梯度消失/爆炸是 vanilla RNN 的核心缺陷,源于 Jacobian 矩阵 $\text{Diag}(\phi'_h) \cdot W_{hh}$ 的反复相乘
- LSTM 通过加法细胞状态更新解决梯度消失:$C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t$,当 $f_t \approx 1$ 时梯度可以无损传播
- 梯度裁剪是稳定 RNN/LSTM 训练的必备技巧,防止梯度爆炸导致参数更新过大
- 自动微分(PyTorch Autograd / Micrograd)将链式法则自动化,开发者只需定义前向计算,梯度由框架自动处理