Appearance
第6章 · Tensor 反向传播与训练稳定性
显式 tensor 对拍不替代完整网络的 clean-room 训练诊断。
先修:第3章的反传和第4章的交叉熵。
本章目标
- 逐 shape 推导 embedding、linear、激活、cross entropy 和 normalization。
- 比较初始化、LayerNorm、BatchNorm 的统计轴与训练/推理行为。
- 用梯度/激活分布诊断饱和、爆炸或消失,并解释 AdamW 的更新边界。
公式与 shape
线性层和 LayerNorm 的核心形式:
其中均值和方差沿最后一个 feature 轴计算。AdamW 将权重衰减与梯度更新解耦,可写成
| 对象 | shape | 约束 |
|---|---|---|
| embedding table | (V, D) | ids (B, T) → (B, T, D) |
| linear weight | (D_in, D_out) | X @ W + b 输出 (B, T, D_out) |
| LayerNorm 参数 | (D,) | 不跨 batch/time 混算统计 |
| attention 前 hidden | (B, T, D) | 后续 head reshape 保持元素顺序 |
| gradient | 与 parameter 相同 | finite、无静默广播 |
注意:上表的 embedding 只回答「是什么 token」,不回答「在第几个位置」。位置从哪来(sinusoidal 与学习式位置编码)在 第7章 专节展开,RoPE 留到第9章。
LayerNorm 反向传播(逐步推导)
前向(对单行、feature 维大小为
- 分子直达:
的分子含 ,贡献 。 - 均值路径:
依赖整行, ,合计 。 - 方差路径:
( 经 的间接依赖因 恒消去),再乘 ,合计 。
三路相加并用
直觉:第一项是上游梯度直通,第二项扣掉「整行一起平移」方向的分量,第三项扣掉沿 dx/dγ 相对误差约 4e-10/2e-10):
python
import numpy as np
def layer_norm_backward(dy, x, gamma, eps=1e-5):
"""LN 反向:dy (B, D), x (B, D), gamma (D,) -> dx, dgamma, dbeta。"""
mu = x.mean(axis=-1, keepdims=True)
s = np.sqrt(x.var(axis=-1, keepdims=True) + eps)
xhat = (x - mu) / s
h = dy * gamma
dx = (h - h.mean(axis=-1, keepdims=True)
- xhat * (h * xhat).mean(axis=-1, keepdims=True)) / s
return dx, (dy * xhat).sum(axis=0), dy.sum(axis=0)仓库现状要记账:python/llm_core/blocks.py 只有 layer_norm 前向(无 γ/β 的教学版),上面的反向函数是本页参考片段而非仓库模块;结构最接近的对照是 python/llm_core/rmsnorm.py 的 rmsnorm_backward——少了均值路径,其余同构。
数学桥接:第2章 → 第6章
第2章 §7 的 gradient_descent_step(param, grad, lr) 实现了最简参数更新:
第2章的 momentum_update(velocity, grad, lr, beta) 返回
第2章 §8 的 relu(x) = \max(0, x)$ 和 mlp_layer_forward 在本章里被张量化:X @ W + b 把第2章的标量链式法则推广到矩阵,activation逐元素作用于整个向量。LayerNorm 的反向三路(分子直达 / 均值路径 / 方差路径)就是第2章partial_derivative` 对每个输入维度的同时求值。
前端类比:AdamW 的 clamp()——对每个维度独立做"梯度压缩",陡峭方向步子自动变小,平坦方向步子相对变大。
交互观察
交互:AdamW vs SGD
损失面是个「峡谷」(y 方向比 x 方向陡 12 倍)。看两个优化器怎么下山。
SGD — 终点 loss 0.00054(60 步)
AdamW — 终点 loss 0.01160(60 步)
最优点 (0, 0)
$$m_t=\beta_1 m_{t-1}+(1-\beta_1)g_t,\quad v_t=\beta_2 v_{t-1}+(1-\beta_2)g_t^2$$ $$\theta_t=\theta_{t-1}-\eta\left(\frac{\hat{m}_t}{\sqrt{\hat{v}_t}+\epsilon}+\lambda\theta_{t-1}\right)$$
教学要点(红旗实验):把 lr 拉到 0.15 以上,SGD 会在陡峭方向剧烈震荡甚至发散, 而 AdamW 因为除以 √v̂ 自动缩小了陡峭方向的步长,依然稳定。 再把 β₂ 调到 0.5 —— AdamW 也开始抖了,这说明二阶动量的「记忆长度」才是它稳的原因。
在同一条病态二次损失「峡谷」上对比两条轨迹:SGD 在陡峭方向来回震荡,AdamW 按维度自适应缩放步长、更快对准谷底;把 weight decay 调大,观察 AdamW 的解耦衰减与 SGD 的 L2 等价写法何时分岔。浏览器演示只建立直觉,参数更新证据仍以下方 pytest 与 diagnostics 实测为准。
从零实践
对一个小矩阵手算
XW+b、tanh/ReLU 和最后一轴 normalization,写出每个中间 shape。用有限差分或 autograd 对拍关键张量反向;记录最大绝对误差和相对误差。
对比不同初始化与 learning rate,绘制 activation/gradient 的最小统计,不用一次 loss 下结论。
当前入口与基础测试:
bashPYTHONPATH=python python -m pytest python/tests/test_blocks_opt.py -q PYTHONPATH=python python -m pytest python/tests/test_transformer_contract.py -q PYTHONPATH=python python -m pytest python/tests/test_diagnostics.py -q
当前 blocks.py、optimizer.py、transformer.py 和 tensor_grad.py 已有可执行入口;linear_backward 与 CE gradient 提供显式 tensor 反传对拍。diagnostics.py 与 test_diagnostics.py 现在覆盖两类可观测训练失败:饱和/消失梯度,以及爆炸/非有限梯度。固定合成快照的实测信号分别为 saturation fraction 1.0、gradient RMS 1e-8,以及 gradient RMS 1290.9944487358057、non-finite count 1;这证明诊断分类器可复现,不把单层或合成快照冒充完整训练稳定性,clean-room shape contract 仍是独立门禁。
故障注入与预期信号
| 注入 | 预期失败信号 | 修复后证据 |
|---|---|---|
| ReLU 负区导数写成 1 | gradcheck 在负输入处失败 | 分段有限差分通过 |
| AdamW 把 weight decay 混入梯度 | 参数范数和 loss 轨迹偏离 fixture | 固定 seed 的更新轨迹对拍 |
论文与延伸
- Layer Normalization(Ba、Kiros、Hinton,2016)
- Decoupled Weight Decay Regularization(Loshchilov、Hutter,2019)
- 选读:Batch Normalization,只需读 §1–§3:训练态用 batch 统计、推理态用移动平均统计的分裂,是它不适合小 batch / 变长序列主线的核心原因——读懂这一点即可,不要求实现。
前端/Agent 迁移
shape contract 类似 TypeScript 类型契约:它应在边界处失败,而不是让错误广播到最后。训练 telemetry 类似状态机的 transition metrics;Agent 也要记录每次状态更新的输入、输出和不变量,避免只看最终文本。
口述与自测(不看资料,5–10 分钟)
- 指出 linear、cross entropy 与 LayerNorm 的输入/输出/梯度 shape;任选一处广播,解释错误轴为何会让数值看似能运行却语义错误。
- 对比饱和/消失与爆炸/非有限梯度的 telemetry 信号,并说明初始化、normalization、学习率或裁剪分别为何是不同层面的修复。
- 用第2章的
gradient_descent_step(5.0, 2.0, lr=0.1) = 4.8手算一步 SGD 更新;解释 η=0.5 时为什么可能 diverges。 - 用第2章的
momentum_update手算例子(v₀=0, grad=2, β=0.9, η=0.01)说明第一次更新为什么等于 SGD,后续更新为什么带惯性。
实验与参考
动手实验
从标量自动微分跨到张量自动反向:验证张量算子满足 shape 契约(前向 shape 与反向梯度 shape 一致)、LayerNorm 在受控张量上把每个样本的特征维归一到均值接近 0、方差接近 1,梯度范数与激活分布在合理阈值内没有爆炸或塌缩,且 optimizer 的 step 能让参数真的朝梯度方向移动。
环境准备
bash
cd <仓库根>
export PYTHONPATH="$PWD/python"命令与预期输出
bash
# 串行跑两个套件,便于对照阈值(test_tensor_grad.py / test_diagnostics.py 已在「从零实践」跑过)
python -m pytest python/tests/test_tensor_grad.py python/tests/test_diagnostics.py -qtext
shape contract: forward=向后梯度=形状一致
grad norm: 1.0xx 处于阈值 [1e-3, 1e2]
layernorm: 每行 mean≈0 var≈1,容差 1e-5
# 判定条件:
# - 所有 shape 断言通过
# - 梯度范数不接近 0 也不溢出
# - LayerNorm 后行均值绝对值 < 1e-5、方差与 1 的偏差 < 1e-5概念图
图:第6章 前向+反向+优化完整链路 — 第2章的偏导数和优化公式在本章首次组合成可训练的完整循环:前向计算激活值 → loss → 反向求梯度 → optimizer 更新参数。后续第8章把它封装为训练循环,第9章用矩阵分解降低投影成本,第13章把"参数 ← checkpoint"类比为 Agent 状态恢复。
故障注入清单
| 故障 | 表现 | 修复 |
|---|---|---|
| LayerNorm 在 batch 维而不是特征维归一化 | 不同 batch size 下行为不一致,相当于把 batch 信息泄漏进单个样本 | 明确归一化轴为特征维,并用 (B, F) 形状断言守住 |
LayerNorm 的 eps 取 0 | 输入某行方差恰好为 0 时出现 NaN | eps 设为 1e-5 量级,并对 NaN/Inf 单独做断言 |
| 权重初始化方差过大 | 前向激活饱和,反向梯度爆炸,grad norm 超阈值 | 初始化按 fan-in 缩放(如 Xavier/Kaiming),并在 diagnostics 中加告警 |
| 广播导致 shape 静默错位 | 表面测试通过,但梯度与参数 shape 不匹配,下一次 step 立即报错 | 关键算子用 assert shape == expected 显式断言,禁用隐式广播 |
| 优化器 step 后没有同步到参数对象 | loss 不下降,看起来"卡在某个值" | step 之后显式回写,并在受控数据集上验证 loss 单调下降 |
资源 / 成本 / 隐私
小矩阵和 CPU/NumPy 足够完成本章对拍,预计 gross cost 为 0;不需要 API 或外部数据。日志只保留 shape、误差和计数,不保留用户输入。
Evidence
仓库当前机器证据(只读快照)
evidence/module-manifest-v1.json 中 06.evidence 指向当前文件:evidence/06-runtime-v1.json。这是当前 checkout 的脱敏机器运行记录,只覆盖该 JSON 记录的命令、指标、产物和已知失败;它不是学习者提交,也不能推出学习者已完成本章。
学习者提交模板(待填写,不是当前机器证据)
复制下面模板并填写自己的真实运行结果。所有 <...> 都是未填写状态;actual 和 artifacts 尤其不能被当作已运行或已通过。artifacts 必须替换为本次提交中真实存在的仓库相对路径。
yaml
schema: learn-llm.evidence.v1
module: 06-tensor-stability
commit: <learner-commit-sha>
verified_at: <iso-date>
environment: <sanitized-python-device>
seed: 4
commands:
- PYTHONPATH=python python -m pytest python/tests/test_blocks_opt.py python/tests/test_tensor_grad.py -q
- PYTHONPATH=python python -m pytest python/tests/test_transformer_contract.py -q
- PYTHONPATH=python python -m pytest python/tests/test_diagnostics.py -q
metrics:
- name: gradient_relative_error
expected: <versioned-threshold>
actual: <recorded-value>
- name: non_finite_activation_count
expected: 0
actual: <recorded-value>
- name: activation_saturation_fraction
expected: 1.0 on saturation fixture
actual: <recorded-value>
- name: exploding_gradient_rms
expected: >=100 on exploding fixture
actual: <recorded-value>
artifacts:
- <learner-repo-relative-artifact-path>
cost:
gross_usd: 0
credit_usd: 0
licenses:
- source: <source>
version: <version>
license: <license>
attribution: <attribution>
redistribution: <redistribution>
known_failures:
- <sanitized-failure-or-none>只有统计轴、反向对拍、两类故障诊断和 learner clean-room shape contract 都具备独立证据时,本章才能离开 gate。