每个参数朝哪个方向调整,不靠尝试,而是计算得出

上一篇留下的待解释环节

上一篇的训练循环中,有两行代码是作为待解释的环节使用的:loss.backward()optimizer.step()。我们当时只说明它们负责"依据误差方向调整参数",并未说明具体如何调整。

而这恰恰是整个训练过程中最值得理解清楚的部分。一个拥有数百万个参数的模型,凭什么能够知道每一个参数应当增大还是减小、调整多少。如果依靠逐一尝试,数百万个参数的取值组合是一个天文数字,无论花多长时间都无法试遍。

它并不依靠尝试,而是依靠计算。本篇就把这个环节完整地拆解开。这是整个模型训练的核心机制,值得用一整篇来讲透。

把损失理解为一片地形

先建立一个直观的图景。

把模型所有参数的当前取值,理解为你在一片地形上所处的位置。把损失——也就是"模型预测得有多差"——理解为你所在位置的海拔高度。参数的取值一旦改变,位置就随之改变,海拔(损失)也随之改变。

训练的目标是让损失变小,对应到这个图景中,就是:从当前位置出发,向低处移动,最理想的情况是抵达整片地形的最低点。

现在设想你处在这片地形的某一点,四周被浓雾笼罩,无法看见最低点在何方。你能做的只有一件事:感知脚下这一小片地面的坡度,朝着下降最陡的方向,迈出一小步;到达新位置后,再次感知坡度,再迈一步。如此持续下去,就能逐步移动到最低点。

模型训练采用的正是这一方法,它有一个正式的名称,称为梯度下降gradient descentgradient descentGradient descent is the method behind the training of almost every modern model. It pictures the loss as a landscape and the parameters as a position on it, then repeatedly takes a small step in the steepest downhill direction. It never sees the whole landscape, only the slope underfoot, yet by stepping downhill again and again it works its way toward a low point. 。整件事的关键,就落在"如何感知脚下的坡度"上。

梯度:每个参数的下降方向

对于单个参数而言,"脚下的坡度"是一个可以精确定义的量:把这个参数增大极小的一点,损失会随之变大还是变小,变化的速率有多快。

这个"损失相对于某个参数的变化率",就称为梯度gradientgradientA gradient is the rate at which the loss changes when one parameter is nudged. It carries both a sign and a size: the sign tells whether increasing the parameter raises or lowers the loss, and the size tells how strongly that parameter affects the loss. Training moves every parameter against its gradient, opposite to the direction that would increase the loss.

梯度是一个既有方向、又有大小的量。如果某个参数的梯度为正,意味着"增大这个参数会使损失变大",那么我们就应当反过来减小它;如果梯度为负,意味着增大这个参数会使损失变小,那么就应当增大它。归纳成一句话:参数应当朝着梯度的相反方向移动。梯度的大小,还告诉我们这个参数对损失的影响有多强——影响强的参数多调整一些,影响弱的参数少调整一些。

模型有数百万个参数,每个参数都有各自的梯度。把它们全部计算出来,我们就得到了一份完整的"下降指引":每个参数各自应当朝哪个方向、移动多少。optimizer.step() 所做的,就是依照这份指引,把所有参数一同移动一小步。

于是问题收窄成了一个:这数百万个梯度,究竟是如何计算出来的。

用 PyTorch 实际计算一次梯度

我们先把规模缩到最小——只有一个参数,实际计算一次,把概念落到实处。

假设模型只有一个参数 w,损失的计算方式是 loss = (w - 3) ** 2。这个损失在什么情况下最小,显然是 w 等于 3 的时候,此时损失为 0。我们来观察 PyTorch 能否自行找到 3 这个值。

import torch

# 参数初始值设为 5。requires_grad=True 告知 PyTorch:需要为这个值计算梯度
w = torch.tensor(5.0, requires_grad=True)

loss = (w - 3.0) ** 2     # 前向:计算损失
loss.backward()           # 反向:计算梯度

print(f"当前 loss = {loss.item()}")     # (5-3)^2 = 4
print(f"w 的梯度 = {w.grad.item()}")    # 4

loss.backward() 执行之后,w.grad 中就出现了梯度值 4。

这个 4 如何验证。loss = (w-3)²,对 w 求导,损失相对于 w 的变化率是 2 × (w-3),代入 w=5,得 2 × 2 = 4,与 PyTorch 计算的结果完全一致。梯度是 4,为正值,说明增大 w 会使损失变大——因此 w 应当朝相反方向、即减小的方向移动,向 3 靠近。方向是正确的。

下面把"迈步"也加入进来,让这一个参数自行训练起来:

w = torch.tensor(5.0, requires_grad=True)
lr = 0.1     # 学习率:每一步移动的幅度

for step in range(20):
    loss = (w - 3.0) ** 2     # 前向
    loss.backward()           # 反向,计算出 w.grad

    with torch.no_grad():     # 这一段不纳入梯度计算
        w -= lr * w.grad      # 朝梯度的相反方向移动一步
    w.grad.zero_()            # 清空梯度,为下一轮做准备

    print(f"step {step}: w = {w.item():.4f}, loss = {loss.item():.6f}")

运行这段代码,可以看到 w 从 5 出发,一步步逼近 3,损失也一步步逼近 0。

值得在此停下来思考:这就是一次完整的训练,区别仅在于模型只有一个参数。上一篇那个拥有数百万参数的训练循环,其骨架与这段代码完全相同——前向计算损失、反向计算梯度、朝相反方向迈步。差异只在于参数的数量。

反向传播:如何一次性求出全部梯度

对于单个参数,借助求导手工计算梯度并不困难。但真实模型有数百万个参数,而且损失的计算并非 (w-3)² 这样简单——数据要穿过层层叠叠的运算,最后才汇成损失。如何一次性把所有参数的梯度都求出来。

答案是反向传播backpropagationbackpropagationBackpropagation is the algorithm that computes the gradient of every parameter in a single sweep. Using the chain rule, it walks the chain of operations backward from the loss, passing the gradient back one layer at a time and reusing intermediate results. This is why a model with millions of parameters can still have all of its gradients found in roughly the time of one forward pass.loss.backward() 中的 back 指的就是它。

它的思路如下。模型从输入计算到损失,是一连串环环相扣的运算,如同一条链条:输入经过第一层运算,结果再经过第二层,再第三层,最终得到损失。要计算"最前面某个参数对损失的影响",可以借助数学上的链式法则:沿着这条链条,从损失一端反向回溯,逐环地把影响传递回去。

可以用一个类比来理解。设想一座工厂,原料经过许多道工序加工成产品,产品最终有一个质量评分。现在想知道"第一道工序的某个参数,对最终质量评分的影响有多大"。我们不需要重新模拟整条生产线,只需从最后一道工序开始向前追问:最后一道工序的输入变化一点,质量评分变化多少;倒数第二道工序的输入变化一点,最后一道工序的输入变化多少。如此一道道向前传递,再相乘,就得到了第一道工序那个参数的影响。

反向传播正是这样工作的:从损失出发,沿着运算链条反向回溯一遍,每经过一环就把梯度向前传递一层。回溯完成后,所有参数的梯度便全部得出。它的高效之处在于中间结果可以复用——靠后的参数在计算梯度时得到的中间量,靠前的参数可以直接沿用,无需重复计算。因此即便有数百万个参数,一次反向传播的耗时也与一次前向计算相近。

更便利的是,这些都不需要自行实现。PyTorch 提供了一套称为 autogradautogradautograd 是 PyTorch 内置的自动求梯度机制。你做前向计算时,它在后台悄悄把每一步运算记录成一张计算图;当你调用 loss.backward() 时,它便沿着这张图自动执行反向传播,把每个需要求梯度的参数的梯度都算好并填入。正因为有它,你无需亲手推导任何导数,反向传播这一步只需一行代码。 的机制:在你做前向计算时,它会在后台把整条运算链条记录下来;当你调用 loss.backward() 时,它便沿着这条链条自动执行反向传播,把每一个标记了 requires_grad=True 的参数的梯度都填好。上一篇的 Bigram 模型那张表,是用 nn.Embedding 建立的,PyTorch 默认就把它登记为需要计算梯度的参数,因此 loss.backward() 一行代码即可完成全部工作。

学习率:每一步调整的幅度

回到地形下降的图景。梯度告诉了你下降的方向,但每一步迈出多大,是另一个需要确定的量。这个步长,就是学习率learning ratelearning rateThe learning rate is the size of each step taken downhill. Too small and training crawls; too large and a step overshoots, sending the loss bouncing or diverging instead of settling. It is one of the most experience-driven settings in training, and a common starting point is around 1e-3. ,即上一篇代码中的 lr

学习率过小,每步只移动微小的距离,需要很久才能到达最低点,训练缓慢。学习率过大,一步迈过了头,可能直接从一侧斜坡越到对侧斜坡上,损失不降反升,甚至来回振荡、无法收敛。

你可以用上面那个单参数的例子做一个试验:把 lr 改为 1.5 再运行,w 不会收敛到 3,而是越偏越远。学习率是训练中最需要凭经验把握的参数之一,第七篇正式训练时还会再次调整它。常用的取值起点在 1e-3 这个量级。

为什么每一轮都要先清空梯度

上一篇的训练循环中有一行 optimizer.zero_grad(),单参数的例子中也有 w.grad.zero_(),现在可以解释了。

PyTorch 有一项设定:loss.backward() 计算出的梯度,是累加到 .grad 上的,而非覆盖。如果不手动清零,本轮的梯度会与上一轮的梯度叠加在一起,越积越多,迈步的方向便完全错误。

因此每一轮的标准动作是:清空梯度、前向计算、反向传播、迈步。遗漏清空这一步,是初学者最常见的错误之一——程序不会报错,但损失始终无法下降。

优化器在梯度下降之上的改进

最后说明 optimizer。在上面的单参数例子中,我们用最朴素的方式迈步:w -= lr * w.grad,朝梯度的相反方向、按固定步长移动。这种最基础的做法称为 SGD(随机梯度下降)。

上一篇所用的 AdamW 是更完善的优化器。它在 SGD 的基础上额外做了两件事。其一是引入动量:参考前几步的移动方向,如同下坡时带有一定的惯性,能够更快地穿过平缓地带,也更不容易停滞在微小的凹陷处。其二是为每个参数配置各自的步长:依据每个参数过往梯度的大小,自动为它确定一个合适的步长,而不是所有参数统一使用一个 lr

在实践中,AdamW 几乎是训练 GPT 这类模型的默认选择,使用方便,收敛较快。理解到它"在梯度下降的基础上,增加了动量与逐参数自适应的步长"这一层即可,不必自行实现。

至此,训练的核心机制就讲解完毕了。再回看那个训练循环:前向计算得出损失(你在地形上的海拔),反向传播算出每个参数的梯度(脚下的坡度),优化器依照梯度把所有参数朝下降方向移动一步。数百万个参数,就是这样被一步一步、自动地,调整到最低点附近的。

本篇要点

  • 训练可以理解为在地形上下降:损失是海拔,参数取值是所处位置,目标是抵达损失的最低点。
  • 梯度是"损失相对于某个参数的变化率",指明该参数的调整方向与影响强度;参数应朝梯度的相反方向移动。
  • 反向传播借助链式法则,从损失一端反向回溯运算链条,一次性求出所有参数的梯度,并复用中间结果。
  • PyTorch 的 autograd 自动记录运算链条,loss.backward() 一行即可完成反向传播。
  • 学习率是每一步的步长,过小则训练缓慢,过大则无法收敛,常用起点在 1e-3 量级。
  • 梯度会累加,每一轮必须先清零;AdamW 在梯度下降之上增加了动量与逐参数自适应的步长。

下一篇

至此,"如何训练"已经讲解透彻——这套机制对任何模型都是通用的。接下来几篇转入另一条线索:把模型本身从"只依据一个字"的 Bigram,升级为能够参考整段上下文的 GPT。下一篇登场的是 GPT 最核心的部件——注意力机制。

参考资料

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:反向传播:模型如何确定参数的调整方向

本文链接:https://www.sshipanoo.com/blog/ai/mini-gpt/04-反向传播模型怎么知道该往哪改/

本文最后一次更新为 天前,文章中的某些内容可能已过时!