训练机制:不只公式,而是“信号如何流动”(结合代码)
1. 训练到底在做什么
一句话:训练是在不断回答这个问题——“当前参数下,模型给正确 token 的概率够不够大?”
如果不够大,就沿着误差反向把参数往“更容易给对答案”的方向推一点。
在本项目里,给定序列 ,只在答案位置集合 计算监督:
这条式子不是装饰,它表达了三件事:
- 只关心任务相关步位(不是每个 token 都算 loss)。
- 概率越小, 惩罚越大。
- 所有监督位置取平均,避免长短样本不公平。
2. 从 hidden 到概率:softmax 为什么不是“随便归一化”
在每个监督步位 ,先拿隐藏态做线性投影得到 logits:
logits 可以理解为“每个词作为下一 token 的打分”,但它们还不是概率。
softmax 把一组任意实数转成分布:
核心概念:
exp放大相对差异:高分词会被进一步拉开。- 分母做全局归一化:所有词概率之和为 1。
- 这是“竞争式”分配:一个词概率升高,会挤压其他词。
所以 softmax 不只是“归一化”,它定义了“词与词之间如何竞争概率质量”。
3. 交叉熵为什么能驱动学习
若正确标签是 ,单步损失:
直觉:
- 如果模型把正确词概率给到 0.9,损失很小;
- 如果只给 0.01,损失很大;
- 因此优化器会优先修正“非常自信但错”的样本。
最关键是它的梯度形式很干净:
这表示:
- 正确类:梯度约为 (通常负值)-> 推高正确类 logit。
- 错误类:梯度约为 (正值)-> 压低错误类 logit。
也就是说,loss 在直接告诉模型:“把概率从错词挪回正确词”。
4. train_step 过程图:每步在干什么
对应:src/train/autograd.cpp::train_step
forward_train
做前向,产出每步 hidden,并记录每层tapes。project_logits_steps_into
只提取监督步位的 logits,避免无关位置参与 loss。cross_entropy_steps_into
计算 loss/accuracy,同时得到 logits 侧梯度入口。backward_output_projection
把 logits 梯度传到 和 hidden。backward_hidden+MiniLlm::backward
梯度从输出端回灌到每一层 attention/mlp/norm 参数。backward_embedding
梯度回到 token embedding。clip_gradients+adamw_step
先防爆,再更新参数。
把它看成“信号管道”更容易理解:
前向负责“产生错误”,反向负责“分摊责任”,优化器负责“落实改动”。
5. 为什么必须有 tape:反向不是魔法
自动微分框架会自动保留中间量;本项目手写反向,所以必须显式保存 BlockForwardTape。
典型缓存:
norm1_out,q_rope,k_rope,attn_probsgate,up,hidden_mid
原因是链式法则要用到前向中间值:
如果前向的 (或相关中间量)丢了,反向就不知道该怎么算 。
6. 一个最小反传例子:输出投影层
设
那么:
解释:
- 第一式是“参数梯度”:这层权重该怎么改。
- 第二式是“输入梯度”:继续往前一层传什么误差信号。
所有层的 backward 本质都在做这两类事:
算本层参数梯度 + 产出给上一层的梯度。
7. Attention 反传为什么难
attention 前向:
难点不在矩阵乘法,而在 softmax:
- softmax 的 Jacobian 是稠密的(同一行类别彼此耦合);
- mask 会让部分位置梯度必须严格为 0;
- 数值稳定处理(减最大值)前后要保持梯度一致语义。
所以工程实现通常不会显式构造大 Jacobian,而是使用等价的向量化公式。
8. AdamW:为什么比“裸 SGD”稳
AdamW 更新(参数 ):
概念解释:
- :一阶动量,近似“平均方向”。
- :二阶动量,近似“方向不确定性/尺度”。
- 除以 :让大梯度维度自动降步长,小梯度维度相对放大。
- :权重衰减,抑制参数无限增大。
9. 梯度裁剪:不是优化器,是保险丝
先算全局梯度范数:
若超阈值 ,统一缩放:
本质:保留方向,限制长度。
它不解决“方向错”,只防止“步子过大把参数炸飞”。
10. 常见误区(按“概念错位”分类)
- 只背公式,不看监督步位定义 -> label shift 错一位,全盘白训练。
- 把 softmax 当独立算子,不理解类别耦合 -> attention backward 很难 debug。
- 忘了 tape 的角色 -> backward 缺中间量只能瞎猜。
- 先 optimizer 再 clip -> 等于没保险。
- 只看 loss,不看梯度范数/分布 -> 很难定位是“学不会”还是“学太猛”。
11. 建议你怎么读源码(按过程,不按文件)
- 先读
train_step,把整条训练链路画出来。 - 再读
backward_output_projection,吃透最小反传单元。 - 接着看
MiniLlm::backward如何把梯度逐层倒放。 - 最后读
optimizer*.cpp,理解“梯度 -> 参数改动”的最后一跳。
读完这四步,你会从“知道公式”升级到“知道梯度为什么这样流”。