概述
前面几篇文章已经讲过 Tensor、形状、数据类型、广播、索引和常用数学操作。到这里,我们终于接近深度学习训练的核心:梯度。
神经网络训练不是简单地“算一个输出”,而是要回答一个更关键的问题:
如果 loss 变大了,每个参数应该往哪个方向调整,才能让 loss 变小?
这个“调整方向”就是梯度。对于一个参数 www 和损失函数 LLL,梯度可以写成:
∂L∂w \frac{\partial L}{\partial w} ∂w∂L
如果没有深度学习框架,开发者需要手动推导每个参数的梯度;模型一复杂,这几乎不可维护。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_gradient 是 False。
第一个梯度示例:对平方函数求导
先从一个数学函数开始:
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 会自动计算 x 和 y 的梯度,并将它们存到 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=(w−3)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()或其他方式切断。
如果 .grad 是 None,先检查:
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())
排查顺序:
loss是否是 Paddle Tensor。loss是否是标量或合理聚合后的 Tensor。- 参数是否
stop_gradient=False。 - 参数是否参与了 loss 的计算。
backward()是否被调用。- 梯度是否被过早清理。
- 是否存在
detach()或no_grad()切断路径。 - 是否出现
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,框架负责沿计算图反向计算每个参数应该如何更新。

390

被折叠的 条评论
为什么被折叠?



