





















假设你在教一个学生做数学题。流程是这样的:题目给他做(前向)、老师打分(loss)、老师指出每一步错在哪、错多少(反向求梯度)、学生根据这些反馈修改自己的解题习惯(优化器更新参数)。
神经网络的训练本质上就是这个循环,只是"学生"换成了一堆参数 W 和 b,"老师"换成了损失函数,"修改习惯"换成了沿着梯度方向调整数值。
类比
Loss = 老师给的总扣分。
梯度 = "这道题里第 3 步贡献了 5 分扣分,第 7 步贡献了 2 分"——把总误差摊回到每一个具体决定上。
优化器 = 学生根据这些细分反馈,决定下次每一步怎么改、改多少。
flowchart LR A["输入数据 x"] --> B["前向传播<br/>模型预测 ŷ"] B --> C["损失函数<br/>L = loss(ŷ, y)"] C --> D["反向传播<br/>计算 ∂L/∂W"] D --> E["优化器<br/>W ← W − η·g"] E -->|下一个 batch| A
图 1 · 一个训练 step 的闭环:前向 → 算 loss → 反向 → 更新
反向传播的前提是先有一次前向传播。模型把输入 x 一层一层往前算,每一层做的事情都很朴素:
z = W · x + b
a = σ(z)
其中 W 是权重矩阵,b 是偏置,σ 是激活函数(ReLU、GELU、Softmax 等)。一层输出的 a 又作为下一层的输入,一路传到最后输出 ŷ。
关键点:在前向传播的过程中,框架(PyTorch / JAX / TF)会偷偷把每一步的中间结果都"记账"下来,形成一张计算图。 这张图就是反向传播能够沿着原路返回的"路标"。没有这本账,后面的链式求导就无从谈起。
预测出来 ŷ 之后,需要一个标量来衡量"离正确答案 y 还差多少"。这就是损失函数(loss function)。
| 任务 | 常用 Loss | 直觉解释 |
|---|---|---|
| 回归 | MSE:(ŷ − y)² |
差得越远,扣分以平方放大 |
| 二分类 | BCE / Logistic | 预测概率离真实标签的对数距离 |
| 多分类 / LLM | Cross-Entropy | 模型给"正确答案"的概率越低,扣分越多 |
| 偏好对齐 | DPO / RLHF Loss | 让模型对"好回答"的概率高于"差回答" |
无论形式怎么变,Loss 都是一个标量——只有一个数字。这一点至关重要:因为只有标量函数才能对每一个参数求出一个明确的"导数方向"。
注意
Loss 本身不会改参数。它只是一个"评分员":给当前这套参数打了个分。真正动手改参数的是后面的反向传播和优化器。
现在我们有了一个数字 L(loss),还有几百万、几十亿个参数 W。问题来了:L 这一个数字,怎么变成对每一个 W 的"改进建议"?
反向传播的灵魂只有一行高中数学——复合函数求导:
∂L/∂W₁ = ∂L/∂a₃ · ∂a₃/∂z₃ · ∂z₃/∂a₂ · ∂a₂/∂z₂ · ∂z₂/∂a₁ · ∂a₁/∂z₁ · ∂z₁/∂W₁
看着吓人,但意思非常朴素:"L 对 W₁ 的敏感度"等于一连串"上一层对下一层的敏感度"相乘。从输出端开始,一层一层往输入端"传递责任"。
ŷ 的导数 ∂L/∂ŷ。例如 MSE 的话就是 2(ŷ − y)。∂L/∂ŷ,结合前向时记下来的中间值,就能反推出 ∂L/∂W_last。再往前一层,继续。W 都会拿到自己专属的梯度 g = ∂L/∂W,告诉它:"你应该往哪个方向、用多大力度修改"。类比
把神经网络想象成一条流水线。某个产品最终被客户投诉(Loss 大)。反向传播就是顺着流水线倒查:终检环节贡献了多少错?焊接环节贡献了多少?最上游的备料环节又贡献了多少?每个工人(参数)最后都拿到一张属于自己的"责任分摊单",那就是它的梯度。
| 维度 | 含义 |
|---|---|
| 方向(符号) | 梯度为正 → 增大该参数会让 Loss 变大;所以应该减小它。反之亦然。 |
| 大小(幅值) | 梯度越大 → 这个参数对当前错误的"贡献"越大,需要更大幅度修改。 |
图 3 · 不同层得到的梯度幅值(典型情况):越靠近输出层,梯度信号越强;越靠近输入层,梯度往往越小(这就是"梯度消失"的来源,也是 ResNet、LayerNorm 等技巧要解决的问题)。
反向传播只负责算出梯度,但"具体怎么用这些梯度去改参数"是优化器(Optimizer)的工作。最朴素的优化器只有一行公式:
SGD:W ← W − η · g
其中 η 是学习率(learning rate)。直觉上:梯度告诉你"哪边是上坡",优化器就反着走,迈一步 η 那么大。学习率太大容易冲过头,太小则训练慢。
| 优化器 | 核心思想 | 更新公式(简化) | 适合场景 |
|---|---|---|---|
| SGD | 纯粹沿当前梯度反方向走 | W ← W − η·g |
简单任务、需要正则化效果 |
| Momentum | 积累"惯性",避免在峡谷里反复横跳 | v ← β·v + g; W ← W − η·v |
损失面崎岖、需要加速收敛 |
| Adam | 给每个参数自己的学习率:常更新的小步走,少更新的大步走 | W ← W − η · m̂ / (√v̂ + ε) |
大模型预训练 / 微调的默认选择 |
| AdamW | Adam + 解耦的权重衰减(更干净的正则化) | 同 Adam,再加 − η·λ·W |
LLM / Transformer 微调几乎必选 |
Adam 在内部维护两个"记忆":
m ← β₁·m + (1−β₁)·g // 一阶动量:梯度的滑动平均
v ← β₂·v + (1−β₂)·g² // 二阶动量:梯度平方的滑动平均
W ← W − η · m̂ / (√v̂ + ε)
直觉:m 像方向感,告诉你"最近一直往哪走";v 像油门刹车,告诉你"这个参数的梯度噪声大不大"。梯度长期很大的参数 v 就大,于是被自动除小,更新更稳;长期不动的参数 v 就小,更新被自动放大。
易混点
很多人把"梯度"和"参数更新量"混为一谈。其实:梯度 g 是反向传播给的"建议";更新量 ΔW 是优化器对建议的"重新解读"——加上学习率、动量、自适应缩放、权重衰减之后才是真正写回W的数。
把前面四步合在一起,一个最小的训练 step 长这样(PyTorch 写法):
# 1. 取一个 batch 的数据
x, y = next(loader)
# 2. 前向传播:模型预测 + 计算 loss
y_hat = model(x)
loss = criterion(y_hat, y)
# 3. 清掉上一步的旧梯度(PyTorch 默认会累加)
optimizer.zero_grad()
# 4. 反向传播:自动沿计算图算出每个参数的梯度
loss.backward()
# 5. 优化器根据梯度更新参数
optimizer.step()
这五行代码就是深度学习训练的最小心跳。模型微调、LLM 预训练、扩散模型训练……本质上都是这同一个循环跑几万到几亿次。
sequenceDiagram autonumber participant D as 数据 (x, y) participant M as 模型 (W) participant L as Loss participant B as Backward participant O as Optimizer D->>M: 输入 x M->>L: 预测 ŷ L-->>L: 计算标量 L L->>B: dL/dŷ B->>M: 沿计算图回传,得到每个 W 的梯度 g M->>O: 把 (W, g) 交给优化器 O->>M: 写回新参数 W' Note over M: 进入下一个 batch
图 5 · 一次训练 step 的角色与信息流
核心机制完全相同,但哪些参数参与反向传播会有变化。这是理解微调各种"花招"的关键。
| 形态 | 哪些参数计算梯度 | 哪些参数被优化器更新 | 显存代价 |
|---|---|---|---|
| 全参微调 | 全部 W | 全部 W | 极高(梯度+优化器状态都翻倍) |
| 冻结部分层 | 只有未冻结层 | 只有未冻结层 | 中等 |
| LoRA / Adapter | 原始 W 不算梯度;只算新增的低秩矩阵 A、B | 只更新 A、B(原 W 完全冻结) | 极低(梯度仅占原模型 0.1%–1%) |
上面表格里反复提到的 A、B 不是随便起的字母,它们是 LoRA 论文里定义好的两个低秩矩阵,是真正接收梯度、被优化器更新的"小参数"。一句话概括:
核心公式
原模型某层权重W ∈ ℝ^(d×k)(冻结,不动)。
LoRA 新增旁路:ΔW = B · A,其中A ∈ ℝ^(r×k),B ∈ ℝ^(d×r),秩r ≪ min(d, k)(常见 4 / 8 / 16 / 32)。
前向时实际用的权重:W' = W + B·A(推理可合并,零额外延迟)。
| 角色 | 形状 | 参数量(举例 d=k=4096, r=8) | 是否参与训练 |
|---|---|---|---|
| 原模型 W | d × k | 4096 × 4096 ≈ 16.78 M | 冻结(不算梯度、不更新) |
| A(降维矩阵) | r × k | 8 × 4096 = 32 K | 训练(高斯随机初始化) |
| B(升维矩阵) | d × r | 4096 × 8 = 32 K | 训练(全零初始化) |
| A + B 合计 | — | ≈ 65 K(原模型的 0.39%) | — |
flowchart LR X["输入 x<br/>(k 维)"] --> W["原权重 W<br/>d×k · 冻结 ❄"] X --> A["A<br/>r×k · 训练 🔥"] A --> B["B<br/>d×r · 训练 🔥"] W --> S(("⊕ 加和")) B --> S S --> Y["输出 h = W·x + B·A·x<br/>(d 维)"]
图 6 · LoRA 的旁路结构:原 W 不动,A、B 负责"补丁"
| 维度 | 原模型 W | LoRA 的 A、B |
|---|---|---|
| 形状 | 方阵或近方阵 d×k | 瘦长矩阵 r×k 和 d×r,被强行压缩到秩 r |
| 参数量 | 约 d·k | 仅 r·(d+k),r=8 时只有原 W 的 ~0.4% |
| 初始化 | 预训练得到 | A 高斯随机,B 全零 → 第 0 步 B·A=0,等价于原模型 |
| 反向传播角色 | 只参与前向,反向经过但不分配梯度 | 真正接收 ∂L/∂A、∂L/∂B,被优化器更新 |
| 语义角色 | "通用知识库",保持不动 | A 像降维探针,B 像升维投影,组合表达任务相关增量 |
| 部署形态 | 主体权重,不可改 | 推理可合并到 W;也可保留旁路结构在多任务间快速切换 |
沿用第 04 节的链式法则。设某层输入为 x、上游传回来的梯度为 ∂L/∂h,则:
∂L/∂B = (∂L/∂h) · (A·x)ᵀ
∂L/∂A = Bᵀ · (∂L/∂h) · xᵀ
∂L/∂W = // 不计算,被冻结
反向传播照样要经过 W 那条路径取得 ∂L/∂h,但梯度只在 A、B 上"落地"。优化器只为 A、B 维护动量与方差,显存账单于是从"原模型 ×3"瞬间降到"A+B ×3"。这就是 LoRA 在 24GB 卡上微调 70B 模型的全部秘密。
类比
把原模型 W 想象成一本印好的字典——内容已经定型,不能改。LoRA 给你一张透明便利贴(B·A),贴在字典上,你只能在便利贴上写字(更新 A、B)。读这本字典的时候,会自动把便利贴的内容叠加到原文上读出。便利贴很小(低秩),但只要贴对位置,就能让字典"理解"你的新任务。
| 技巧 | 作用 |
|---|---|
| 梯度裁剪 Gradient Clipping | 把过大的梯度截断到一个阈值,防止某次"爆炸"把训练好的权重一巴掌打飞。LLM 微调几乎必开。 |
| 梯度累积 Gradient Accumulation | 显存装不下大 batch?跑几次小 batch 把梯度加起来,再 step 一次。等效于大 batch,但显存峰值低。 |
| 混合精度 fp16 / bf16 | 前向 + 反向用低精度,参数主副本用 fp32。配合 loss scaling 防止小梯度被精度抹零。 |
| 学习率调度 LR Schedule | warmup → cosine decay 是 LLM 微调的"咒语":先小心试探,再温柔降速,避免一开始就把预训练知识冲掉。 |
g = ∂L/∂W。ΔW。First Principle
记不住所有公式没关系。只要把这条链路想清楚——"打分 → 摊责任 → 改作业"——你就已经抓住了模型训练 95% 的本质。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。