PyTorch 的自动梯度原理可以通俗地理解为:它是一个能自动帮我们计算梯度的工具,就像一个智能的小助手。
在 PyTorch 中,对于 Variable 的定义就是 可以用于求导的 Tensor。将 requires_grad 设置为 True 就可以定义一个变量了。
import torch
# 构建 变量
x = torch.tensor([2.0],requires_grad=True)
# 变量的微分计算式子,该属性有值才能参与微分计算
print(x.grad_fn)
PyTorch 中的每个张量(Tensor)都可以设置一个 requires_grad 属性。当我们把 requires_grad 设置为 True 时,PyTorch 就会自动记录对这个张量进行的所有操作,构建一个计算图。在这个计算图中,张量就像是一个个节点,而操作就像是连接这些节点的边。
当我们完成前向传播(forward propagation),也就是计算出损失函数(loss)之后,我们可以通过调用 loss.backward() 来启动反向传播(backward propagation)。在这个过程中,PyTorch 会根据之前记录的操作,按照链式法则自动计算出每个张量的梯度。这些梯度会被存储在张量的 .grad 属性中。
节点表示张量,边表示张量之间运算逻辑。
以一个简单的神经元为例:
import torch
import torch.nn.functional as F
def g(x):
return x**2
x = torch.tensor([[2.0],[3.0]],dtype=torch.float,requires_grad=True)
# 变量
w = torch.tensor([[1.0,2.0]],requires_grad=True,dtype=torch.float)
b = torch.tensor([1.0],requires_grad=True,dtype=torch.float)
# 权值计算
z = torch.mm(w,x) + b
# 激活函数
a = g(z)
print(a)
# 是否为叶子结点
print(a.is_leaf,a.grad_fn)
print(x.is_leaf,x.grad_fn)
根据上述计算流程,PyTorch 就能自动搭建一个动态计算图。

同样以神经元为例,为了方便计算,假设激活函数为:g(x)=x^2
则「前向传播」计算流程就为: z=wx+b a=z^2
import torch
import torch.nn.functional as F
def g(x):
return x**2
x = torch.tensor([[2.0],[3.0]],dtype=torch.float,requires_grad=True)
# 变量
w = torch.tensor([[1.0,2.0]],requires_grad=True,dtype=torch.float)
b = torch.tensor([1.0],requires_grad=True,dtype=torch.float)
# 权值计算
z = torch.mm(w,x) + b
# 激活函数
a = g(z)
print(a)
# 从 a 开始反向传播
a.backward()
print(w.grad)
print(b.grad)
print(z.grad)
print(x.grad)
从 a 开始执行「反向传播」

根据链式求导可知:

可以获取系数w,b的梯度。 默认情况下,PyTorch 不会保留「中间结点」的梯度,即z的梯度值为 None。
由于我们是从 a 开始进行反向传播的,因此 w.grad 对应的值就是:

其他同理。
注意: - 计算图中,当某一张量Λ进行「反向传播」,那么「叶子节点Ω」的 grand 属性就是dΛ/dΩ的值。 - 某一张量进行「反向传播」后,计算图将销毁。
运行结果:
tensor([[81.]], grad_fn=<PowBackward0>)
tensor([[36., 54.]])
tensor([18.])
None
tensor([[18.],
[36.]])
阻止计算图追踪(阻止计算梯度)
方式一:
import torch
import torch.nn.functional as F
def g(x):
return x**2
x = torch.tensor([[2.0],[3.0]],dtype=torch.float,requires_grad=True)
# 变量
w = torch.tensor([[1.0,2.0]],requires_grad=True,dtype=torch.float)
b = torch.tensor([1.0],requires_grad=True,dtype=torch.float)
# 权值计算
z = torch.mm(w,x) + b
# 后续计算被计算图屏蔽
with torch.no_grad():
# 激活函数
a = g(z)
print(a)
# 从 a 开始反向传播
a.backward()
print(w.grad)
print(b.grad)
print(z.grad)
print(x.grad)
方式二:
import torch
import torch.nn.functional as F
def g(x):
return x**2
x = torch.tensor([[2.0],[3.0]],dtype=torch.float,requires_grad=True)
# 变量
w = torch.tensor([[1.0,2.0]],requires_grad=True,dtype=torch.float)
b = torch.tensor([1.0],requires_grad=True,dtype=torch.float)
# 权值计算
z = torch.mm(w,x) + b
# 后续计算被计算图屏蔽
z2 =z.detach()
a = g(z2)
print(a)
# 从 a 开始反向传播
a.backward()
print(w.grad)
print(b.grad)
print(z.grad)
print(x.grad)
中间结点梯度保存:
import torch
import torch.nn.functional as F
def g(x):
return x**2
x = torch.tensor([[2.0],[3.0]],dtype=torch.float,requires_grad=True)
# 变量
w = torch.tensor([[1.0,2.0]],requires_grad=True,dtype=torch.float)
b = torch.tensor([1.0],requires_grad=True,dtype=torch.float)
# 权值计算
z = torch.mm(w,x) + b
#中间结点梯度保存
z.retain_grad()
# 激活函数
a = g(z)
print(a)
# 从 a 开始反向传播
a.backward()
print(w.grad)
print(b.grad)
print(z.grad)
print(x.grad)
实现: 继承torch.autograd.Function类,并重写前向传播forward与反向传播backward方法。
import torch
class line(torch.autograd.Function):
@staticmethod
def forward(ctx, w, b, x):
# 保存输入值,供反向传播的梯度计算
ctx.save_for_backward(w, b, x)
return torch.mm(w, x) + b
@staticmethod
def backward(ctx, grad_output):
print('execute backward....')
""" grad_output:为链式求导法则上一级的导数结果 """
w, b, x = ctx.saved_tensors
dw = torch.mm(grad_output, x.t())
db = grad_output
dx = torch.mm(w.t(),grad_output)
#注意:返回值其实是None,因为 backward() 会将梯度计算结果存储在对应的张量的 .grad 属性中。
return dw, db, dx
x = torch.tensor([[2], [3]], dtype=torch.float,requires_grad=True)
# 变量
w = torch.tensor([[1, 2]], requires_grad=True, dtype=torch.float)
b = torch.tensor([1], requires_grad=True, dtype=torch.float)
# 调用自定义封装
z = line.apply(w, b, x)
# 激活函数
a = z ** 2
# 反向传播
a.backward()
print(w.grad)
print(b.grad)
print(z.grad)
print(x.grad)
运行结果:
execute backward....
tensor([[36., 54.]])
tensor([18.])
None
tensor([[18.],
[36.]])
如果你想执行多次反向传播,只需要添加retain_graph=True参数。
import torch
class line(torch.autograd.Function):
@staticmethod
def forward(ctx, w, b, x):
# 保存输入值,供反向传播的梯度计算
ctx.save_for_backward(w, b, x)
return torch.mm(w, x) + b
@staticmethod
def backward(ctx, grad_output):
print('execute backward....')
""" grad_output:为链式求导法则上一级的导数结果 """
w, b, x = ctx.saved_tensors
dw = torch.mm(grad_output, x.t())
db = grad_output
dx = torch.mm(w.t(), grad_output)
return dw, db, dx
x = torch.tensor([[2], [3]], dtype=torch.float, requires_grad=True)
# 变量
w = torch.tensor([[1, 2]], requires_grad=True, dtype=torch.float)
b = torch.tensor([1], requires_grad=True, dtype=torch.float)
# 调用自定义封装
z = line.apply(w, b, x)
# z.retain_grad()
# 激活函数
a = z ** 2
# 反向传播
#第一次反向传播
a.backward(None, retain_graph=True)
print(w.grad)
print(b.grad)
print(z.grad)
print(x.grad)
#第二次反向传播
a.backward(None, retain_graph=True)
print(w.grad)
print(b.grad)
print(z.grad)
print(x.grad)
运行结果:
execute backward....
tensor([[36., 54.]])
tensor([18.])
None
tensor([[18.],
[36.]])
execute backward....
tensor([[ 72., 108.]])
tensor([36.])
None
tensor([[36.],
[72.]])
问题:求下面函数的最小值。

我们可以使用 Python 的 Matplotlib 库。来绘制这个函数的三维曲面图,如下:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# 创建网格点
x = np.linspace(-5, 5, 100) # x 的范围从 -5 到 5,共 100 个点
y = np.linspace(-5, 5, 100) # y 的范围从 -5 到 5,共 100 个点
X, Y = np.meshgrid(x, y) # 生成网格点
# 计算 z 值
Z = X**2 + Y**2 + 1
# 创建三维图形
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# 绘制曲面
surf = ax.plot_surface(X, Y, Z, cmap='viridis')
# 添加标题和坐标轴标签
ax.set_title('3D Surface of z = x² + y² + 1')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
# 添加颜色条
fig.colorbar(surf, shrink=0.5, aspect=5)
# 显示图形
plt.show()

所以求函数:
最小值,就是求这个曲面的最低点问题。
要理解梯度下降算法为什么可以求解这个曲面的最低点,需要先理解梯度的数学原理和性质。
我们知道:梯度(gradient),是多元函数全部偏导数所构成的向量。f(x,y) = x^2 + y^2 +1 的梯度计算过程如下:

为了快速找到这个曲面的最高点(最低点),我们就必须研究从P点出发沿着哪个方向运行能使得f(x,y)增大或者减少最快呢?

梯度的数学性质是:在函数上的某一点。 - 如果沿着函数梯度的方向运动,函数增加的最快。 - 如果沿着函数梯度的反方向运动,函数减小的最快。
为了证明沿着函数梯度的反方向运动,函数减小的最快,举例如下:

明白了这个道理,我们可以用 PyTorch 实现梯度下降算法来求解函数
的最小值。这个函数是一个简单的凸函数,其最小值在 x=0 和 y=0 处,最小值为 1。我们可以通过梯度下降算法来验证这一点。
代码实现如下:
import torch
# 定义初始参数
x = torch.tensor([2.0], requires_grad=True) # 初始 x 值
y = torch.tensor([3.0], requires_grad=True) # 初始 y 值
# 定义学习率和迭代次数
learning_rate = 0.1
epochs = 100
# 梯度下降循环
for epoch in range(epochs):
# 计算函数值
z = x ** 2 + y ** 2 + 1
# 反向传播计算梯度
z.backward()
# 更新参数
with torch.no_grad():
x -= learning_rate * x.grad
y -= learning_rate * y.grad
# 清空梯度
x.grad.zero_()
y.grad.zero_()
# 打印每步的结果
if (epoch + 1) % 10 == 0:
print(f'Epoch {epoch + 1}/{epochs}, x: {x.item():.4f}, y: {y.item():.4f}, z: {z.item():.4f}')
# 输出最终结果
print(f'\nMinimum value found at x: {x.item():.4f}, y: {y.item():.4f}, z: {z.item():.4f}')
运行结果:
Epoch 10/100, x: 0.2147, y: 0.3221, z: 1.2342
Epoch 20/100, x: 0.0231, y: 0.0346, z: 1.0027
Epoch 30/100, x: 0.0025, y: 0.0037, z: 1.0000
Epoch 40/100, x: 0.0003, y: 0.0004, z: 1.0000
Epoch 50/100, x: 0.0000, y: 0.0000, z: 1.0000
Epoch 60/100, x: 0.0000, y: 0.0000, z: 1.0000
Epoch 70/100, x: 0.0000, y: 0.0000, z: 1.0000
Epoch 80/100, x: 0.0000, y: 0.0000, z: 1.0000
Epoch 90/100, x: 0.0000, y: 0.0000, z: 1.0000
Epoch 100/100, x: 0.0000, y: 0.0000, z: 1.0000
Minimum value found at x: 0.0000, y: 0.0000, z: 1.0000
代码解释
初始化参数: - 我们初始化 x 和 y 为张量,并设置 requires_grad=True,以便 PyTorch 能够自动计算梯度。 - 初始值设为 2.0 和 3.0,这只是任意选择的起点。
学习率和迭代次数: - 学习率(learning_rate)决定了每次更新的步长大小。 - 迭代次数(epochs)决定了我们进行梯度下降的次数。
梯度下降循环:
- 在每次迭代中,我们计算函数z=x^2 + y^2 +1 的值。
- 调用 z.backward() 计算梯度。
- 使用梯度更新 x 和 y 的值,公式为:
,同理更新 y。
- 清空梯度,避免在下一次迭代中累积。
打印结果: - 每 10 次迭代打印一次当前的 x、y 和 z 值,观察收敛过程。 最终结果: - 输出最终找到的最小值点和对应的 z 值。
注意事项 - 学习率的选择很重要,如果学习率太大,可能导致不收敛;如果学习率太小,收敛速度会很慢。 - 初始值的选择也会影响收敛速度,但在这个问题中,由于函数是凸的,无论初始值如何,最终都会收敛到全局最小值。
需要注意的是:梯度下降算法最终倾向找到平坦的最低点。为什么会倾向于找到平坦的最低点呢?梯度下降的基本原理是沿着损失函数的负梯度方向更新参数,逐步逼近最小值点。那平坦的最低点通常对应着损失函数的一个最小值,可能是一个全局最小值或者局部最小值,但为什么梯度下降更倾向于平坦的呢?主要可以归结为以下几个原因:
总之:梯度下降在平坦区域因梯度小、更新稳定、噪声鲁棒性强及吸引域宽广而更易收敛。这一特性在非凸优化(如深度学习)中尤为显著,使得平坦最小值成为更可能的收敛目标。