Understanding LLMs III: Backprop & Adam
要让 loss 修正参数,核心问题是:每个参数对 loss 有多少影响?数学上是 loss 对参数的偏导数,工程上叫梯度(gradient)。
要算梯度,框架必须把前向传播每一步都记下来,连成一张图——计算图。
每个 Value 是图上一个节点,记录三件事:
| 字段 | 含义 |
|---|---|
data | 自己当前的数值 |
_children | 自己从哪几个 Value 算出来的 |
_local_grads | 每个 child 对自己的局部影响系数 |
前向传播时,每做一次运算就生成新 Value,指回它的”父母”节点。这张图保存了完整计算历史,loss 可沿图反向找到所有影响过它的参数。
反向传播听起来神秘,本质就是高中数学的链式法则:一步步把”我对你的影响系数”算回去。
| 运算 | a 的局部梯度 | b 的局部梯度 |
|---|---|---|
| 加法 | 1 | 1 |
| 乘法 |
若 经过中间结果 依赖 :
每个节点只需知道自己的局部梯度 ,再乘以上游传来的 ,就得到 对 的总梯度。这就是反向传播能逐层计算的数学基础。
调用 loss.backward() 做两件事:
从 loss 出发,按依赖关系把图里所有节点排好,保证算梯度时每个节点的”下游”都已经算完。
loss.grad = 1 # loss 对自己的梯度
for v in topo_order_reversed: # 从 loss 往回
for child, local_grad in zip(v._children, v._local_grads):
child.grad += local_grad * v.grad # 累加!
一个 child 可能被多个 parent 用过(多条路径影响 loss)。多条路径的影响都要累加,不能赋值覆盖。
每个参数最终积累的 grad 就是 loss 对它的偏导数。这个数字告诉我们:参数往哪边动一点,loss 会变小。十几行 Python 就是反向传播的全部。
最简单的更新是梯度下降:。microgpt 用更聪明的 Adam。
| 量 | 含义 | 作用 |
|---|---|---|
| (动量) | 梯度的指数移动平均(“惯性”) | 最近往哪走就继续往那走,不被单次抖动带偏 |
| 梯度平方的指数移动平均(“波动幅度”) | 波动大的参数步子放小,波动小的走快 |
方向看 ,步长看 。
这是自适应学习率:每个参数有自己的有效学习率。Adam 通常更稳、收敛更快,今天大多数大模型训练都用 Adam 或其变种。
lr_t = learning_rate * (1 - step / num_steps)
学习率从 0.01 线性降到 0。
| 训练阶段 | 离最优解 | 学习率策略 |
|---|---|---|
| 初期 | 远 | 大胆走(大步) |
| 后期 | 接近 | 小心调(小步) |
训练后期已接近最优,再大步会冲过头(在最优解附近震荡)。线性衰减让步长越来越小,平滑收敛。
for step in range(1000):
name = random_name() # 1. 取一个名字
tokens = [BOS] + encode(name) + [BOS] # 2. 加 BOS,转 id
loss = forward(tokens) # 3. 前向传播算 loss
loss.backward() # 4. 反向传播算梯度
adam_update(params) # 5. Adam 更新参数
zero_grad(params) # 6. 梯度清零,准备下一步
随机猜的 loss 大约是 (27 个字符均匀分布的交叉熵)。降到 2 说明模型已从”瞎猜”走出来很远。整个训练没有玄学,就是这六步重复 1000 遍。
梯度用累加(B21),不清零的话上一步的梯度会累加到这一步,导致更新方向错误。所以每步开始前必须 zero_grad。
训练完从 BOS 开始,让模型预测下一个字符。输出 logits 过 softmax 变概率后,不直接取最高,而是带温度采样。
logits = logits / temperature
probs = softmax(logits)
char = random.choices(chars, weights=probs)
| 温度 | 分布 | 效果 |
|---|---|---|
| 低 | 尖锐 | 几乎只选最高概率,保守、重复 |
| 高 | 平滑 | 敢选低概率,多样但易出错 |
温度高 = 更有”创造性”(探索);温度低 = 更”确定”(保守)。
取最大(greedy)每次结果相同,且容易陷入重复循环。采样引入随机性,每次生成不同——ChatGPT 的”重新生成”按钮就是因为带采样。
采到一个字符接上去,继续预测下一个,直到模型生成 BOS(“我说完了”)或达到 block_size 上限强制停。
| microgpt | GPT-4 | |
|---|---|---|
| 参数量 | ~4000 | ~万亿 |
| 层数 | 1 | 几十 |
| 数据 | 3 万人名 | 互联网级文本 |
无论 4000 还是万亿,核心机制完全一样:
差别只在规模:更多参数记住更复杂模式、更多数据覆盖更广知识、更多层做更深的推理。看懂 microgpt 的 200 行,就抓住了 GPT-4 训练的骨架。