06-自动微分入门:用 Paddle 计算梯度

概述

前面几篇文章已经讲过 Tensor、形状、数据类型、广播、索引和常用数学操作。到这里,我们终于接近深度学习训练的核心:梯度

神经网络训练不是简单地“算一个输出”,而是要回答一个更关键的问题:

如果 loss 变大了,每个参数应该往哪个方向调整,才能让 loss 变小?

这个“调整方向”就是梯度。对于一个参数 www 和损失函数 LLL,梯度可以写成:

∂L∂w \frac{\partial L}{\partial w} wL

如果没有深度学习框架,开发者需要手动推导每个参数的梯度;模型一复杂,这几乎不可维护。PaddlePaddle 的自动微分机制会记录前向计算过程,并在调用 backward()paddle.grad() 时自动计算梯度。

本篇文章重点解决这些问题:

  • stop_gradient 到底控制什么?
  • loss.backward() 做了什么?
  • x.grad 保存的是什么?
  • 为什么梯度会累积,什么时候要清理?
  • backward()paddle.grad() 有什么区别?
  • 如何手写一个最小的梯度下降过程?
  • 常见自动微分错误应该怎么排查?

读完这篇文章,你应该能写出最小自动微分示例,理解梯度如何产生、保存、累积和清理,并能把它和后续训练循环连接起来。

背景:前向传播、损失函数和反向传播

深度学习训练可以分成三个核心阶段:

前向传播
    |
计算损失
    |
反向传播
    |
参数更新

更具体一点:

输入 x
    |
模型 f(x; w, b)
    |
预测值 y_pred
    |
损失函数 loss(y_pred, y_true)
    |
根据 loss 计算参数梯度
    |
优化器更新参数

其中:

  • 前向传播:从输入计算到预测值。
  • 损失函数:衡量预测值和真实值的差距。
  • 反向传播:从 loss 反向计算每个参数的梯度。
  • 优化器更新:根据梯度调整参数。

Paddle 自动微分的价值,就是让你只需要写前向计算和损失计算,梯度计算由框架完成。

官方文档中也强调:自动微分让开发者关注前向传播过程,飞桨框架自动完成反向传播,从繁琐的求导和求梯度过程中解放出来。

准备代码:统一打印 Tensor 和梯度

为了便于观察,先准备两个打印函数:

import paddle


def show_tensor(name, tensor):
    print(f"\n{name}")
    print("-" * 60)
    print(tensor)
    print("shape:", tensor.shape)
    print("dtype:", tensor.dtype)
    print("stop_gradient:", tensor.stop_gradient)


def show_grad(name, tensor):
    print(f"\n{name}.grad")
    print("-" * 60)
    print(tensor.grad)

本文示例都可以在 CPU 环境运行。建议创建一个文件:

paddle_autograd_intro.py

并在开头写入:

import paddle

paddle.seed(2026)

stop_gradient:决定 Tensor 是否参与梯度计算

stop_gradient 是理解 Paddle 自动微分的第一个关键属性。

官方文档说明:

  • 用户自行创建的 Tensor 默认 stop_gradient=True,也就是默认不计算梯度。
  • 模型参数的 stop_gradient 默认通常为 False,也就是需要计算梯度。
  • stop_gradient=True 时,该 Tensor 不会计算梯度,并会阻断梯度传播。
  • stop_gradient=False 时,该 Tensor 会参与梯度计算。

先看最简单的例子:

import paddle

a = paddle.to_tensor([1.0, 2.0, 3.0])
b = paddle.to_tensor([1.0, 2.0, 3.0], stop_gradient=False)

print("a.stop_gradient:", a.stop_gradient)
print("b.stop_gradient:", b.stop_gradient)

输出含义:

a.stop_gradient: True
b.stop_gradient: False

如果一个 Tensor 创建时没有设置 stop_gradient=False,它默认不参与梯度计算。

也可以创建后修改:

x = paddle.to_tensor([1.0, 2.0, 3.0])
print(x.stop_gradient)

x.stop_gradient = False
print(x.stop_gradient)

想对一个 Tensor 求梯度,先确认它的 stop_gradientFalse

第一个梯度示例:对平方函数求导

先从一个数学函数开始:

y=x2 y = x^2 y=x2

它的导数是:

dydx=2x \frac{dy}{dx} = 2x dxdy=2x

Paddle 代码:

import paddle

x = paddle.to_tensor([1.0, 2.0, 3.0], stop_gradient=False)
y = x ** 2
loss = paddle.sum(y)

loss.backward()

print("x:", x)
print("y:", y)
print("loss:", loss)
print("x.grad:", x.grad)

手动推导:

y = [1^2, 2^2, 3^2] = [1, 4, 9]
loss = sum(y) = 14

d(loss)/d(x) = [2 * 1, 2 * 2, 2 * 3]
             = [2, 4, 6]

所以 x.grad 应该是:

[2, 4, 6]

这就是自动微分的最小闭环:

创建需要梯度的 Tensor
    |
写前向计算
    |
得到标量 loss
    |
loss.backward()
    |
查看 x.grad

backward 做了什么:从 loss 反向传播

调用:

loss.backward()

可以理解为:从当前 loss 这个 Tensor 出发,沿着前向计算记录下来的计算图反向传播,计算所有需要梯度的 Tensor 的梯度,并把结果保存到它们的 .grad 属性中。

用一个稍微复杂的例子:

import paddle

x = paddle.to_tensor([1.0, 2.0, 3.0], stop_gradient=False)
y = paddle.to_tensor([4.0, 5.0, 6.0], stop_gradient=False)

z = x ** 2 + 4 * y
loss = paddle.sum(z)

loss.backward()

print("x.grad:", x.grad)
print("y.grad:", y.grad)

手动推导:

z = x^2 + 4y
loss = sum(z)

d(loss)/d(x) = 2x = [2, 4, 6]
d(loss)/d(y) = 4  = [4, 4, 4]

因此:

x.grad -> [2, 4, 6]
y.grad -> [4, 4, 4]

官方文档中也使用了类似示例说明:对 z 调用 backward() 后,Paddle 会自动计算 xy 的梯度,并将它们存到 grad 属性中。

标量 loss:为什么经常先 sum 或 mean

训练中通常对一个标量 loss 调用 backward()

例如:

x = paddle.to_tensor([1.0, 2.0, 3.0], stop_gradient=False)
y = x ** 2
loss = paddle.mean(y)
loss.backward()

这里 loss 是 0 维 Tensor。

如果 y 是一个向量,直接调用 y.backward() 在很多场景下会因为输出不是标量而不符合预期。更常见的做法是先聚合:

loss = paddle.sum(y)

或:

loss = paddle.mean(y)

二者梯度不同。

示例:

import paddle

x1 = paddle.to_tensor([1.0, 2.0, 3.0], stop_gradient=False)
loss_sum = paddle.sum(x1 ** 2)
loss_sum.backward()
print("sum grad:", x1.grad)

x2 = paddle.to_tensor([1.0, 2.0, 3.0], stop_gradient=False)
loss_mean = paddle.mean(x2 ** 2)
loss_mean.backward()
print("mean grad:", x2.grad)

推导:

sum(x^2) 的梯度: [2, 4, 6]
mean(x^2) 的梯度:[2/3, 4/3, 6/3]

这也是为什么训练时 loss 的 reduction 方式会影响梯度大小。

grad 属性:梯度保存在 Tensor 上

反向传播之后,梯度会保存在参与自动微分的 Tensor 的 .grad 属性中。

import paddle

x = paddle.to_tensor([2.0], stop_gradient=False)
y = x * x + 3 * x

y.backward()

print("x.grad:", x.grad)

手动推导:

y=x2+3x y = x^2 + 3x y=x2+3x

dydx=2x+3 \frac{dy}{dx} = 2x + 3 dxdy=2x+3

x=2x = 2x=2 时:

2 * 2 + 3 = 7

所以 x.grad 是 7。

需要注意:

  • .grad 是 Tensor,不是普通 Python 数字。
  • 没有参与梯度计算的 Tensor,.grad 通常是 None
  • 反向传播之后,.grad 会保留,除非你手动清理。

stop_gradient=True 会阻断梯度

看一个例子:

import paddle

a = paddle.to_tensor(2.0, stop_gradient=False)
b = paddle.to_tensor(5.0, stop_gradient=True)

c = a * b
c.backward()

print("a.grad:", a.grad)
print("b.grad:", b.grad)

数学上:

c = a * b
dc/da = b = 5
dc/db = a = 2

但是因为 b.stop_gradient=True,Paddle 不会计算 b 的梯度,所以:

a.grad -> 5
b.grad -> None

这个例子很重要。它说明 stop_gradient 不是“这个 Tensor 不能参与前向计算”,而是“它是否参与反向求梯度”。

stop_gradient=True 的 Tensor 可以参与前向计算,但不会得到自己的梯度,也会阻断对应路径的梯度传播。

梯度会累积:为什么需要 clear_grad

Paddle 中梯度默认会累积。也就是说,如果对同一个 Tensor 连续多次调用 backward(),梯度会加到已有 .grad 上,而不是自动覆盖。

示例:

import paddle

x = paddle.to_tensor([1.0, 2.0, 3.0], stop_gradient=False)

loss1 = paddle.sum(x ** 2)
loss1.backward()
print("after first backward:", x.grad)

loss2 = paddle.sum(x ** 2)
loss2.backward()
print("after second backward:", x.grad)

第一次梯度:

[2, 4, 6]

第二次再次累积:

[4, 8, 12]

所以训练循环中通常需要清理梯度。对于单个 Tensor,可以:

x.clear_gradient()

也可以在很多版本和对象上看到类似 clear_grad() 的用法。对于模型训练,更常见的是通过优化器清理所有参数梯度:

optimizer.clear_grad()

训练循环里通常是:

前向计算
loss.backward()
optimizer.step()
optimizer.clear_grad()

有些人习惯在下一轮前先清梯度:

optimizer.clear_grad()
前向计算
loss.backward()
optimizer.step()

手写梯度下降:不用优化器先理解原理

为了理解梯度如何驱动参数更新,我们先不用优化器,手写一个最小梯度下降。

目标函数:

loss=(w−3)2 loss = (w - 3)^2 loss=(w3)2

w=3w = 3w=3 时,loss 最小。

代码:

import paddle

w = paddle.to_tensor([0.0], stop_gradient=False)
learning_rate = 0.1

for step in range(10):
    loss = (w - 3.0) ** 2
    loss.backward()

    with paddle.no_grad():
        w -= learning_rate * w.grad

    print("step:", step, "w:", w.numpy(), "loss:", loss.numpy(), "grad:", w.grad.numpy())

    w.clear_gradient()

这里有几个关键点:

  • w.stop_gradient=False:表示 w 需要梯度。
  • loss.backward():计算 d(loss)/d(w)
  • with paddle.no_grad():参数更新本身不应该被记录进计算图。
  • w -= learning_rate * w.grad:沿梯度反方向更新参数。
  • w.clear_gradient():清理本轮梯度,避免下一轮累积。

梯度方向解释:

loss = (w - 3)^2
grad = 2(w - 3)

当 w = 0:
grad = -6

w = w - lr * grad
  = 0 - 0.1 * (-6)
  = 0.6

w 会逐步靠近 3。

训练模型的本质,就是反复计算梯度,然后沿着让 loss 下降的方向更新参数。

使用优化器:更接近真实训练流程

真实模型训练一般不手动更新参数,而是使用优化器。

下面用 paddle.create_parameter() 创建一个可训练参数:

import paddle

w = paddle.create_parameter(
    shape=[1],
    dtype="float32",
    default_initializer=paddle.nn.initializer.Constant(0.0),
)

optimizer = paddle.optimizer.SGD(
    learning_rate=0.1,
    parameters=[w],
)

for step in range(10):
    loss = (w - 3.0) ** 2

    loss.backward()
    optimizer.step()
    optimizer.clear_grad()

    print("step:", step, "w:", w.numpy(), "loss:", loss.numpy())

和手写版本相比:

  • loss.backward() 仍然负责计算梯度。
  • optimizer.step() 负责参数更新。
  • optimizer.clear_grad() 负责清理梯度。

这就是后续训练神经网络时最常见的结构。

线性回归小例子:自动微分串起完整训练

下面写一个最小线性回归:

y=xw+b y = xw + b y=xw+b

希望模型从数据中学到接近:

y=2x+1 y = 2x + 1 y=2x+1

代码:

import paddle

paddle.seed(2026)

x = paddle.to_tensor([[1.0], [2.0], [3.0], [4.0]], dtype="float32")
y_true = paddle.to_tensor([[3.0], [5.0], [7.0], [9.0]], dtype="float32")

w = paddle.create_parameter(
    shape=[1, 1],
    dtype="float32",
    default_initializer=paddle.nn.initializer.Normal(mean=0.0, std=0.1),
)
b = paddle.create_parameter(
    shape=[1],
    dtype="float32",
    default_initializer=paddle.nn.initializer.Constant(0.0),
)

optimizer = paddle.optimizer.SGD(
    learning_rate=0.01,
    parameters=[w, b],
)

for step in range(100):
    y_pred = paddle.matmul(x, w) + b
    loss = paddle.mean((y_pred - y_true) ** 2)

    loss.backward()
    optimizer.step()
    optimizer.clear_grad()

    if step % 20 == 0:
        print(
            "step:", step,
            "loss:", float(loss.numpy()),
            "w:", w.numpy().flatten(),
            "b:", b.numpy().flatten(),
        )

训练流程:

x, y_true
    |
y_pred = x @ w + b
    |
loss = mean((y_pred - y_true)^2)
    |
loss.backward()
    |
w.grad, b.grad
    |
optimizer.step()
    |
w, b 更新

这个例子很小,但已经包含深度学习训练的完整骨架。

paddle.grad:直接返回指定输入的梯度

除了 loss.backward(),Paddle 还提供了 paddle.grad()

官方 API 说明:paddle.grad(outputs, inputs, ...) 用于计算 outputs 相对于 inputs 的梯度,并以 tuple 形式返回。

最小示例:

import paddle

x = paddle.to_tensor([2.0], stop_gradient=False)
y = x * x + 3 * x

dx = paddle.grad(outputs=[y], inputs=[x])[0]

print("dx:", dx)
print("x.grad:", x.grad)

这里:

y = x^2 + 3x
dy/dx = 2x + 3 = 7

paddle.grad() 会返回梯度 Tensor。和 backward() 的常见差异是:

方式结果放在哪里常见用途
loss.backward()写入 Tensor 的 .grad标准训练流程
paddle.grad()直接返回梯度 tuple自定义梯度计算、高阶导数、科学计算

初学训练模型时,优先掌握 backward();需要更精细控制梯度时,再使用 paddle.grad()

高阶导数:create_graph 的直观理解

有些场景需要对梯度再次求导。例如:

y=x3 y = x^3 y=x3

一阶导数:

dydx=3x2 \frac{dy}{dx} = 3x^2 dxdy=3x2

二阶导数:

d2ydx2=6x \frac{d^2y}{dx^2} = 6x dx2d2y=6x

Paddle 示例:

import paddle

x = paddle.to_tensor([2.0], stop_gradient=False)
y = x ** 3

dy_dx = paddle.grad(
    outputs=[y],
    inputs=[x],
    create_graph=True,
)[0]

d2y_dx2 = paddle.grad(
    outputs=[dy_dx],
    inputs=[x],
)[0]

print("dy_dx:", dy_dx)
print("d2y_dx2:", d2y_dx2)

x = 2 时:

dy/dx = 3 * 2^2 = 12
d2y/dx2 = 6 * 2 = 12

这里的关键是 create_graph=True。它表示计算梯度时继续创建反向图,使得梯度本身也能继续参与求导。官方 paddle.grad 文档中也说明,create_graph=True 可支持计算高阶导数。

高阶导数常见于:

  • 科学计算。
  • PINN 物理信息神经网络。
  • 元学习。
  • 某些正则化方法。
  • 对梯度本身进行约束的自定义损失。

普通分类和回归入门训练通常不需要高阶导数。

no_grad:参数更新和推理时不记录梯度

有些计算不应该记录到计算图里,例如:

  • 手动更新参数。
  • 验证集评估。
  • 推理预测。
  • 只做日志统计。

这时可以使用 paddle.no_grad()

import paddle

x = paddle.to_tensor([2.0], stop_gradient=False)

with paddle.no_grad():
    y = x * x

print("y.stop_gradient:", y.stop_gradient)

no_grad 作用域中,Paddle 不会为其中的操作构建反向传播所需的梯度信息。

训练时手动更新参数要放在 no_grad 中:

with paddle.no_grad():
    w -= learning_rate * w.grad

推理时也建议使用:

with paddle.no_grad():
    y_pred = model(x)

好处:

  • 减少内存占用。
  • 避免不必要的计算图记录。
  • 让代码语义更清晰。

detach:切断当前 Tensor 与计算图的关系

有时你希望使用某个 Tensor 的数值,但不希望梯度继续沿着它之前的计算路径传播,可以使用 detach()

import paddle

x = paddle.to_tensor([2.0], stop_gradient=False)
y = x * x
z = y.detach()

out = z * 3
out.backward()

print("x.grad:", x.grad)
print("y.stop_gradient:", y.stop_gradient)
print("z.stop_gradient:", z.stop_gradient)

z 使用了 y 的数值,但切断了和 x -> y 这段计算图的梯度关系。所以从 out 反向时,梯度不会回传到 x

常见用途:

  • 固定某些中间结果。
  • 避免某一分支回传梯度。
  • 构造 teacher-student、目标网络、缓存特征等场景。
  • 调试时确认某段计算是否影响梯度。

初学阶段不必过早大量使用 detach(),但要知道它是“用数值,断梯度”的工具。

两种方式都可以,关键是每轮参数更新前后要保证旧梯度不会错误地混入新一轮。

retain_graph:为什么同一张图不能随便 backward 两次

官方文档说明,Paddle 默认会在 backward() 后释放反向计算图,以节省内存。如果需要对同一张图继续反向,需要设置 retain_graph=True

示例:

import paddle

x = paddle.to_tensor([1.0, 2.0, 3.0], stop_gradient=False)
y = paddle.sum(x ** 2)

y.backward(retain_graph=True)
print("first grad:", x.grad)

y.backward()
print("second grad:", x.grad)

这里第二次 backward 能执行,是因为第一次保留了图。不过梯度仍然会累积,所以第二次之后 x.grad 会变成两次梯度之和。

一般建议:

  • 普通训练不要设置 retain_graph=True
  • 只有明确需要同一张图多次反向时再用。
  • 频繁保留计算图会增加内存占用。

一句话总结retain_graph=True 是特殊场景工具,不是训练循环默认配置。

梯度检查:用解析梯度和数值梯度对比

为了确认自动微分结果,可以用一个简单函数做数值梯度检查。

函数:

f(x)=x2+3x f(x) = x^2 + 3x f(x)=x2+3x

解析梯度:

f′(x)=2x+3 f'(x) = 2x + 3 f(x)=2x+3

x=2x = 2x=2

f'(2) = 7

数值梯度近似:

f(x+ϵ)−f(x−ϵ)2ϵ \frac{f(x+\epsilon)-f(x-\epsilon)}{2\epsilon} 2ϵf(x+ϵ)f(xϵ)

代码:

import paddle

eps = 1e-4

x = paddle.to_tensor([2.0], stop_gradient=False)
y = x * x + 3 * x
y.backward()

auto_grad = x.grad.numpy()[0]

x_plus = 2.0 + eps
x_minus = 2.0 - eps
f_plus = x_plus * x_plus + 3 * x_plus
f_minus = x_minus * x_minus + 3 * x_minus
numeric_grad = (f_plus - f_minus) / (2 * eps)

print("auto_grad:", auto_grad)
print("numeric_grad:", numeric_grad)

两者应该非常接近。数值梯度检查常用于:

  • 验证自定义损失是否写对。
  • 排查某个复杂公式的梯度是否符合预期。
  • 学习自动微分原理。

真实大模型不会靠这种方式逐参数检查,但它对理解梯度很有帮助。

常见错误

错误一:忘记设置 stop_gradient=False

问题代码:

x = paddle.to_tensor([1.0, 2.0, 3.0])
y = paddle.sum(x ** 2)
y.backward()
print(x.grad)

x 默认 stop_gradient=True,因此不会得到梯度。

正确写法:

x = paddle.to_tensor([1.0, 2.0, 3.0], stop_gradient=False)

或:

x.stop_gradient = False

错误二:梯度被累积但没有清理

典型现象:

  • loss 变化异常。
  • 梯度越来越大。
  • 手写训练循环结果不稳定。

排查方式:

print(parameter.grad)

训练循环中加入:

optimizer.clear_grad()

或对单个 Tensor:

x.clear_gradient()

错误三:在没有 no_grad 的情况下手动改参数

错误倾向:

w -= learning_rate * w.grad

更稳写法:

with paddle.no_grad():
    w -= learning_rate * w.grad

参数更新不是模型前向逻辑的一部分,不应该被记录到计算图里。

错误四:把 numpy 数组混进训练计算

如果中途调用 .numpy(),再用 NumPy 做计算,Paddle 的计算图就断了。

问题倾向:

y_np = y.numpy()
loss_np = (y_np ** 2).mean()

这时 loss_np 已经不是 Paddle Tensor,无法通过它反向传播。

训练中的计算尽量保持为 Paddle Tensor:

loss = paddle.mean(y ** 2)

错误五:对已经释放的计算图重复 backward

如果需要同一张图多次 backward,第一次需要:

loss.backward(retain_graph=True)

但普通训练不需要这样做。更常见的是每轮重新前向计算:

每轮训练都重新计算 y_pred 和 loss
然后 backward

错误六:以为所有 Tensor 都会有 grad

只有满足条件的 Tensor 才会有有效梯度:

  • 它参与了从 loss 到自身的计算路径。
  • 它的 stop_gradient=False
  • 该路径没有被 detach()no_grad() 或其他方式切断。

如果 .gradNone,先检查:

print(tensor.stop_gradient)

再检查它是否真的参与了 loss 计算。

调试方法:看到梯度问题先打印这些信息

建议准备一个调试函数:

def debug_autograd(name, tensor):
    print(f"{name}:")
    print("  shape:", tensor.shape)
    print("  dtype:", tensor.dtype)
    print("  stop_gradient:", tensor.stop_gradient)
    print("  grad:", tensor.grad)

训练中重点打印:

debug_autograd("w", w)
debug_autograd("b", b)
print("loss:", loss.numpy())

排查顺序:

  1. loss 是否是 Paddle Tensor。
  2. loss 是否是标量或合理聚合后的 Tensor。
  3. 参数是否 stop_gradient=False
  4. 参数是否参与了 loss 的计算。
  5. backward() 是否被调用。
  6. 梯度是否被过早清理。
  7. 是否存在 detach()no_grad() 切断路径。
  8. 是否出现 NaN/Inf

这套顺序比盲目修改学习率更可靠。

总结

这一篇围绕 Paddle 自动微分讲了几个关键点:

  • stop_gradient=False 表示 Tensor 需要参与梯度计算。
  • 用户创建的普通 Tensor 默认通常不需要梯度,模型参数通常需要梯度。
  • loss.backward() 会从 loss 反向计算梯度,并写入 .grad
  • 梯度默认会累积,所以训练循环需要清理梯度。
  • optimizer.step() 根据梯度更新参数,optimizer.clear_grad() 清理参数梯度。
  • paddle.grad() 可以直接返回指定输入的梯度,适合更精细的梯度控制。
  • create_graph=True 支持高阶导数。
  • paddle.no_grad() 用于不需要记录梯度的计算。
  • detach() 可以使用数值但切断梯度路径。

如果只能记住一句话,那就是:

Paddle 自动微分让你只写前向计算和 loss,框架负责沿计算图反向计算每个参数应该如何更新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值