Hang Zhengyang

训练机制:不只公式,而是“信号如何流动”(结合代码)

1. 训练到底在做什么

一句话:训练是在不断回答这个问题——“当前参数下,模型给正确 token 的概率够不够大?”
如果不够大,就沿着误差反向把参数往“更容易给对答案”的方向推一点。

在本项目里,给定序列 x1:T,只在答案位置集合 𝒮 计算监督:

ℒ=−1|𝒮|∑t∈𝒮logpθ(xt∣x<t)

这条式子不是装饰,它表达了三件事:

  1. 只关心任务相关步位(不是每个 token 都算 loss)。
  2. 概率越小,−log(·) 惩罚越大。
  3. 所有监督位置取平均,避免长短样本不公平。

2. 从 hidden 到概率:softmax 为什么不是“随便归一化”

在每个监督步位 t,先拿隐藏态做线性投影得到 logits:

zt=htWo⊤

logits 可以理解为“每个词作为下一 token 的打分”,但它们还不是概率。
softmax 把一组任意实数转成分布:

pt,i=ezt,i∑jezt,j

核心概念:

  • exp 放大相对差异:高分词会被进一步拉开。
  • 分母做全局归一化:所有词概率之和为 1。
  • 这是“竞争式”分配:一个词概率升高,会挤压其他词。

所以 softmax 不只是“归一化”,它定义了“词与词之间如何竞争概率质量”。

3. 交叉熵为什么能驱动学习

若正确标签是 yt,单步损失:

ℒt=−logpt,yt

直觉:

  • 如果模型把正确词概率给到 0.9,损失很小;
  • 如果只给 0.01,损失很大;
  • 因此优化器会优先修正“非常自信但错”的样本。

最关键是它的梯度形式很干净:

∂ℒt∂zt,i=pt,i−1[i=yt]

这表示:

  • 正确类:梯度约为 pt,yt−1(通常负值)-> 推高正确类 logit。
  • 错误类:梯度约为 pt,i(正值)-> 压低错误类 logit。

也就是说,loss 在直接告诉模型:“把概率从错词挪回正确词”。

4. train_step 过程图:每步在干什么

对应:src/train/autograd.cpp::train_step

  1. forward_train
    做前向,产出每步 hidden,并记录每层 tapes。
  2. project_logits_steps_into
    只提取监督步位的 logits,避免无关位置参与 loss。
  3. cross_entropy_steps_into
    计算 loss/accuracy,同时得到 logits 侧梯度入口。
  4. backward_output_projection
    把 logits 梯度传到 Wo 和 hidden。
  5. backward_hidden + MiniLlm::backward
    梯度从输出端回灌到每一层 attention/mlp/norm 参数。
  6. backward_embedding
    梯度回到 token embedding。
  7. clip_gradients + adamw_step
    先防爆,再更新参数。

把它看成“信号管道”更容易理解:
前向负责“产生错误”,反向负责“分摊责任”,优化器负责“落实改动”。

5. 为什么必须有 tape:反向不是魔法

自动微分框架会自动保留中间量;本项目手写反向,所以必须显式保存 BlockForwardTape。

典型缓存:

  • norm1_out, q_rope, k_rope, attn_probs
  • gate, up, hidden_mid

原因是链式法则要用到前向中间值:

∂ℒ∂x=∂ℒ∂y∂y∂x

如果前向的 y(或相关中间量)丢了,反向就不知道该怎么算 ∂y∂x。

6. 一个最小反传例子:输出投影层

设

zt=htWo⊤,ℒ=CE(zt,yt)

那么:

∂ℒ∂Wo=(∂ℒ∂zt)⊤ht
∂ℒ∂ht=∂ℒ∂ztWo

解释:

  • 第一式是“参数梯度”:这层权重该怎么改。
  • 第二式是“输入梯度”:继续往前一层传什么误差信号。

所有层的 backward 本质都在做这两类事:
算本层参数梯度 + 产出给上一层的梯度。

7. Attention 反传为什么难

attention 前向:

A=softmax(S),S=QK⊤Dh+M,Y=AV

难点不在矩阵乘法,而在 softmax:

  • softmax 的 Jacobian 是稠密的(同一行类别彼此耦合);
  • mask 会让部分位置梯度必须严格为 0;
  • 数值稳定处理(减最大值)前后要保持梯度一致语义。

所以工程实现通常不会显式构造大 Jacobian,而是使用等价的向量化公式。

8. AdamW:为什么比“裸 SGD”稳

AdamW 更新(参数 w):

mt=β1mt−1+(1−β1)gt,vt=β2vt−1+(1−β2)gt2
m^t=mt1−β1t,v^t=vt1−β2t
w←w−η(m^tv^t+ϵ+λw)

概念解释:

  • mt:一阶动量,近似“平均方向”。
  • vt:二阶动量,近似“方向不确定性/尺度”。
  • 除以 v^t:让大梯度维度自动降步长,小梯度维度相对放大。
  • +λw:权重衰减,抑制参数无限增大。

9. 梯度裁剪:不是优化器,是保险丝

先算全局梯度范数:

‖g‖2=∑igi2

若超阈值 τ,统一缩放:

g←g·τ‖g‖2

本质:保留方向,限制长度。
它不解决“方向错”,只防止“步子过大把参数炸飞”。

10. 常见误区(按“概念错位”分类)

  1. 只背公式,不看监督步位定义 -> label shift 错一位,全盘白训练。
  2. 把 softmax 当独立算子,不理解类别耦合 -> attention backward 很难 debug。
  3. 忘了 tape 的角色 -> backward 缺中间量只能瞎猜。
  4. 先 optimizer 再 clip -> 等于没保险。
  5. 只看 loss,不看梯度范数/分布 -> 很难定位是“学不会”还是“学太猛”。

11. 建议你怎么读源码(按过程,不按文件)

  1. 先读 train_step,把整条训练链路画出来。
  2. 再读 backward_output_projection,吃透最小反传单元。
  3. 接着看 MiniLlm::backward 如何把梯度逐层倒放。
  4. 最后读 optimizer*.cpp,理解“梯度 -> 参数改动”的最后一跳。

读完这四步,你会从“知道公式”升级到“知道梯度为什么这样流”。