大语言模型基础:从零到一实现之路

第3讲: 特征空间的变换2
前反向运行视角理解深度学习模型

反向传播:深度学习的关键

  • 前向传播:从输入到输出,计算预测结果
  • 反向传播:从输出到输入,计算梯度;优化器再根据梯度更新参数
  • 核心思想:通过链式法则计算复合函数的导数

center

为什么需要反向传播?

  • 目标:找到使损失函数最小的参数
  • 方法:梯度下降,需要计算损失函数对每个参数的梯度
  • 挑战:深度学习模型是复合函数,直接计算梯度困难
  • 解决:反向传播算法,高效计算所有参数的梯度

模型的训练/学习

  • 假设,构建模型,其参数为
  • 目标: 设计一种可用来度量基于的模型预测结果和真实结果差距的度量,差距越小,模型越接近需估计的函数
  • 学习方法:梯度下降,寻找合适的 (被称之训练模型)

center

模型的训练/学习

  • 目标:
  1. 猜个, 根据输入,计算
  2. 评估误差: 的误差(loss)
  3. 根据误差,更新:

center

训练模型(搜索/开发参数)

优化目标:

梯度下降法 (Gradient descent): 求偏导(partial derivative)

  • 通常以真值(groundtruth)体现,因此重点关注

    • -->
    • 通常深度学习模型为复合函数,可利用链式法则求偏导
  • 核心算法: 反向传播(backpropagation)

    • 核心步骤: 针对优化目标按层“回退”,一层一层求偏导

反向传播(backpropagation)

  • 假设深度学习模型为的复合函数

  • 优化目标的偏导的核心为

  • 链式法则展开:

  • 偏导的构建

    • 传统手工实现 v.s. 基于计算图的autograd

计算图:模型计算的DAG图

  • 节点:表示变量(input, weight等)和操作(各种运算等)
  • :表示数据的传递
  • 前向传播:沿着边的方向计算
  • 反向传播:沿着边的反方向计算梯度

center

举个例子:线性层的反向传播

线性层:
损失函数:

前向传播:计算 logits ,再计算标量损失

反向传播:损失函数先传回上游梯度

  • 输入梯度:,形状
  • 权重梯度:,形状
  • 偏置梯度:,形状

共享参数的梯度需要汇总各个样本的贡献。

自动求导:PyTorch 的核心特性

  • 普通梯度模式下,Tensor 运算在需要求导时记录计算图。
  • requires_grad=True:让张量参与梯度追踪。
  • 前向记录求导所需的关系;backward() 才执行梯度计算。
  • nn.Module 组织模型;普通 Tensor 运算也能自动求导。

前向建立反向计算所需的图,反向计算梯度,优化器更新参数。

Autograd的无感知使用

optimizer = torch.optim.SGD(net.parameters(), lr=0.01)

for epoch in range(100):
    # 前向传播
    y = net(x)
    loss = criterion(y, target)

    # 反向传播
    optimizer.zero_grad()  # 清零梯度
    loss.backward()        # 计算梯度
    optimizer.step()       # 更新参数

.grad.grad_fn:数值和计算规则

属性 回答的问题 内容
t.grad_fn 这个张量由什么运算产生,如何往回求导? 反向节点对象,或 None
t.grad 本次目标对这个张量的梯度是多少? 梯度 Tensor,或 None
  • .grad_fn 在被追踪的前向运算中建立,不是梯度数值
  • .grad 在反向时写入并累加,shape 与对应张量相同。
  • 需梯度的叶子默认保存 .grad;非叶子需先调用 retain_grad()

.grad is None 表示没有保存梯度值,不等于梯度为 0。

用同一个例子观察前向与反向

x = torch.tensor([1., 2., 3.], requires_grad=True)
y = 2 * x
loss = y.sum()
y.retain_grad()  # 为了观察,保留非叶子 y 的梯度

print(x.grad, y.grad)  # None, None:尚未反向
print(x.grad_fn)       # None:x 是直接创建的叶子
print(type(y.grad_fn).__name__)  # MulBackward0
loss.backward()
print(x.grad)  # tensor([2., 2., 2.])
print(y.grad)  # tensor([1., 1., 1.])

前向得到 y=[2,4,6]loss=12;反向得到的是它们的梯度。
节点名称的数字后缀属于实现细节。

梯度如何经过反向节点?

沿用上一页:

loss.grad_fn       y.grad_fn                  x 的梯度累加节点
SumBackward0  →    MulBackward0       →       AccumulateGrad
   1 → [1,1,1]      [1,1,1] × 2 → [2,2,2]    写入 x.grad
张量 本例中的角色 .grad_fn 反向后的 .grad
x 直接创建的叶子 None [2,2,2],默认保存
y 运算产生的非叶子 MulBackward0 [1,1,1],因调用了 retain_grad()

不调用 y.retain_grad(),梯度仍经过 y 传回 x,只是不会保存在 y.grad

常见反向节点名称对照(PyTorch)

上游梯度 :后续计算传回本层的梯度,与输出 同形,均为

  • AddmmBackward: 对应 addmm 的反向(矩阵乘 + 偏置相加),是 nn.Linear/F.linear 的核心反向
    • nn.Linear 的权重
    • 可能显示为 AddmmBackward0 等后缀变体
  • TBackward: 对应 transpose 的反向,是线性层实现里常见的辅助节点(例如 W.t())。反传中将梯度再转回原始维度
  • AccumulateGrad: 不是算子反向,而是“叶子张量梯度累加”节点。把传来的梯度写入叶子张量(如 Linear.weight/biasrequires_grad=True 的输入)的 .grad 中,并按步累加

AccumulateGrad:把梯度累加到叶子

Autograd 引擎按依赖关系调度反向节点:局部反向规则计算梯度,
AccumulateGrad 将收到的梯度累加到叶子张量的 .grad

# 接上例:第一次 backward 后,x.grad 已经是 [2,2,2]
(2 * x).sum().backward()  # 新的前向与反向
print(x.grad)            # [4,4,4]:旧梯度 + 本次梯度
x.grad = None           # 清除已保存的梯度
(2 * x).sum().backward()
print(x.grad)            # [2,2,2]
  • x.grad_fn 仍为 None:叶子 x 不是由这个累加节点生成的。
  • backward() 不更新参数;训练时用 optimizer.zero_grad() 清零,optimizer.step() 更新参数。
  • retain_grad() 保存中间梯度;retain_graph=True 保留反向所需的图数据。

工程视角看Autograd:你需要知道的

  • 动态计算图:前向即时构图;非叶子张量有 grad_fn,叶子张量的梯度写入 .grad
  • 基于向量-雅可比积(VJP):backward() 等价逐层执行 的向量-雅可比积;工程上无需显式构造雅可比矩阵即可完成训练。

工程视角看Autograd:你需要知道的

  • 标量 backward:对标量输出使用 y.backward()
    • 反向传播需要一个起点,对标量来说,就是1
    • 例如y=model(x),假设y.shape=[batch,dim],直接调用y.backward()会报错,因为torch内部不知道到底该对哪个方向做反向传播
  • 非标量 backward:对非标量输出使用 y.backward(gradient=v)vy 同形(shape)表示上游grad权重。

工程视角看Autograd:你需要知道的

  • 线性层的backward:设上游梯度
    • 计算图常见节点:AddmmBackward(Linear/F.linear 反向)、TBackward(转置辅助)、AccumulateGrad(叶子梯度累积)。
  • 训练循环要点:zero_grad() 防梯度累积;参数更新放优化器或 no_grad();避免对中间结果原地修改。

工程视角看Autograd:你需要知道的

  • 高阶/多次反向:按需使用 create_graph=Trueretain_graph=True;默认一次反向后释放图。
  • 性能与调试:AMP、梯度检查点、torch.compile;结合 hooks 与 notebook 的图打印定位梯度流。

注:理论上可把 Autograd 看成“雅可比链式法则”的高效实现,理解这一点即可,不必掌握雅可比的形式化定义再上手工程实践。

Hook 示例

h = []
def log_grad(grad):
    h.append((grad.mean().item(), grad.norm().item()))

out = net(x)
out.register_hook(log_grad)    # 观察上游梯度
loss = criterion(out, y)
loss.backward()

梯度累积与清零

  • .grad 默认累加;训练循环应先清零再 backward()
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()

高阶梯度与图保留

  • 二阶/高阶导: create_graph=True 构建可微分的反向图。
  • 多次反传: 若复用同一前向,需 retain_graph=True
g = torch.autograd.grad(loss, params, create_graph=True)
g2 = torch.autograd.grad(sum(p.sum() for p in g), params)

分离与禁用追踪

  • x.detach():把张量从当前计算图中分离,后续关于该结果的计算不会把梯度回传到被分离的分支。与原张量共享存储(谨慎原地写)。
    • 用途:
      • 截断梯度(冻结某分支、teacher 模型前向等)。
      • 缓存中间结果重复使用但不参与训练。

分离与禁用追踪

注意事项:

  • 不要在训练前向外层包 no_grad/inference_mode,否则无法计算梯度。
  • detach() 会打断梯度流,误用会让模型学不动;仅在确需截断时使用。
  • 分离张量与原张量共享存储,避免原地写引入隐性错误。
  • with torch.no_grad()::上下文内不记录计算图,不分配 grad_fn/中间量,因此这些计算不参与 loss.backward()
    • 与反向的关系:在该上下文中产生的新张量,即使参与后续损失计算,梯度也不会通过它们回传(因为没有构图)。
    • 用途:
      • 纯推理/验证(配合 model.eval())。
      • 统计/后处理(如 metrics、argmax/topk)。
      • 无梯度的参数更新(如 EMA、手写优化步骤)。

A0:前向的 shape 如何变化?

无偏置线性变换:

  • :批次;:序列位置;:输入特征;:输出特征。
  • 每个 位置:,对输入特征维 求和。
  • 两个位置维保留;所有位置使用同一个

这里 nn.Linear 保存的权重布局为

A0:Loss 如何产生上游梯度?

  • 前向 sum() 将全部元素归约成一个标量。
  • 反向为每个输出元素计算一个梯度,因此 同形。
  • 表示:每个 变化一点,Loss 如何变化。
前向:Y [B,H,E] → 平方、求和 → L []
反向:dL/dL = 1 → G = 2Y [B,H,E]

若 Loss 改为平方的均值,则 ;shape 不变。

A0:输入梯度为什么是 GWᵀ?

一个输入元素 影响同一位置的所有 个输出:

把逐元素求和组织成矩阵乘法:

  • 对输出特征维 求和,得到每个输入特征的梯度。
  • 保留,各位置分别计算; 同形。
  • 转置让矩阵乘法的求和维对齐:
dX = G @ W.T  # [B,H,E] @ [E,D] -> [B,H,D]

A0:权重梯度为什么要展平、求和?

被所有 位置共享,各位置的贡献相加:

,将两个位置维合并,不改变元素的对应关系

dW = X.reshape(-1, D).T @ G.reshape(-1, E)
# 例:B=2,H=3,D=4,E=5 → [4,6] @ [6,5] → [4,5]

矩阵乘法对 求和,恰好汇总所有位置; 同形。

https://marp.app/

![bg right:40% 100%](https://miro.medium.com/max/1024/1*G1v2WBigWmNzoMuKOYQV_g.png)

- `model.zero_grad(set_to_none=True)` 降低显存碎片与加速。

暂不展示:原第 29–30 页(inference_mode 及对比表) --- - `with torch.inference_mode()`:面向**纯推理/部署**的模式 - 与反向的关系:不构图 - 跳过版本计数与视图跟踪,进一步减少元数据与一致性检查的开销;用于无需梯度的快速前向,并非只读模式 - 适用场景:离线/在线推理、模型服务、导出前的快速验证 --- _class: compact(原页局部样式,恢复时改回 Marp 指令) | 特性 | `no_grad` | `inference_mode` | 说明 | |---|---|---|---| | 构图 | 关闭 | 关闭 | 两者都不记录计算图 | | 版本计数/视图跟踪 | 保留 | 跳过 | `inference_mode` 更省内存/检查更少 | | 内存/速度 | 省 | 更省/更快 | 大模型推理建议 `inference_mode` | | 训练期使用 | 可用于局部(如指标、EMA) | 不建议 | `inference_mode` 仅用于纯推理 | | 张量使用限制 | 可在之后的求导运算中使用 | 新建张量不能随意用于后续求导 | 推理张量在模式外原地写受限 |