2025-04-20 李沐深度学习4 —— 自动求导
文章目录
- 1 导数拓展
- 1.1 标量导数
- 1.2 梯度:向量的导数
- 1.3 扩展到矩阵
- 1.4 链式法则
- 2 自动求导
- 2.1 计算图
- 2.2 正向模式
- 2.3 反向模式
- 3 实战:自动求导
- 3.1 简单示例
- 3.2 非标量的反向传播
- 3.3 分离计算
- 3.4 Python 控制流
硬件配置:
- Windows 11
- Intel®Core™i7-12700H
- NVIDIA GeForce RTX 3070 Ti Laptop GPU
软件环境:
- Pycharm 2025.1
- Python 3.12.9
- Pytorch 2.6.0+cu124
1 导数拓展
1.1 标量导数
基本公式
- 常数: d ( a ) / d x = 0 d(a)/dx = 0 d(a)/dx=0
- 幂函数: d ( x n ) / d x = n ⋅ x n − 1 d(x^n)/dx = n·x^{n-1} d(xn)/dx=n⋅xn−1
- 指数/对数:
- d ( e x ) / d x = e x d(e^x)/dx = e^x d(ex)/dx=ex
- d ( ln x ) / d x = 1 / x d(\ln x)/dx = 1/x d(lnx)/dx=1/x
- 三角函数:
- d ( sin x ) / d x = cos x d(\sin x)/dx = \cos x d(sinx)/dx=cosx
- d ( cos x ) / d x = − sin x d(\cos x)/dx = -\sin x d(cosx)/dx=−sinx
求导法则
d ( u + v ) d x = d u d x + d v d x d ( u v ) d x = u d v d x + v d u d x d f ( g ( x ) ) d x = f ′ ( g ( x ) ) ⋅ g ′ ( x ) \begin{aligned}&\frac{d(u+v)}{dx}=\frac{du}{dx}+\frac{dv}{dx}\\&\frac{d(uv)}{dx}=u\frac{dv}{dx}+v\frac{du}{dx}\\&\frac{df(g(x))}{dx}=f^{\prime}(g(x))\cdotp g^{\prime}(x)\end{aligned} dxd(u+v)=dxdu+dxdvdxd(uv)=udxdv+vdxdudxdf(g(x))=f′(g(x))⋅g′(x)
不可微函数的导数:亚导数
- ∣ x ∣ |x| ∣x∣ 在 x = 0 x=0 x=0 时的亚导数: [ − 1 , 1 ] [-1,1] [−1,1] 区间任意值。
- ReLU 函数:
max(0,x)在 x = 0 x=0 x=0 时导数可取 [ 0 , 1 ] [0,1] [0,1]。
1.2 梯度:向量的导数
形状匹配规则
| 函数类型 | 自变量类型 | 导数形状 | 示例 |
|---|---|---|---|
| 标量 y y y | 标量 x x x | 标量 | d y / d x = 2 x dy/dx = 2x dy/dx=2x |
| 标量 y y y | 向量 x \mathbf{x} x | 行向量 | d y / d x = [ 2 x 1 , 4 x 2 ] dy/d\mathbf{x} = [2x_1,4x_2] dy/dx=[2x1,4x2] |
| 向量 y \mathbf{y} y | 标量 x x x | 列向量 | d y / d x = [ cos x , − sin x ] T d\mathbf{y}/dx = [\cos x, -\sin x]^T dy/dx=[cosx,−sinx]T |
| 向量 y \mathbf{y} y | 向量 x \mathbf{x} x | 雅可比矩阵 | d y / d x = [ [ 1 , 0 ] , [ 0 , 1 ] ] d\mathbf{y}/d\mathbf{x} = [[1,0],[0,1]] dy/dx=[[1,0],[0,1]] |
案例 1
- y y y: x 1 2 + 2 x 2 2 x_1^2 + 2x_2^2 x12+2x22(第一个元素的平方与第二个元素平方的 2 倍之和)
- x \mathbf{x} x:向量。
d y d x = [ 2 x 1 , 4 x 2 ] \frac{dy}{d\mathbf{x}}=\begin{bmatrix}2x_1,&4x_2\end{bmatrix} dxdy=[2x1,4x2]
- 几何解释:梯度向量 [2, 4] 指向函数值增长最快方向。
-
其他情况
案例 2
- y \mathbf{y} y:向量。
- x x x:标量。
案例 3
- y \mathbf{y} y:向量。
- x \mathbf{x} x:向量。
- 其他情况
1.3 扩展到矩阵
1.4 链式法则
标量链式法则的向量化
当 y = f ( u ) , u = g ( x ) y = f(u), u = g(x) y=f(u),u=g(x) 时:
d y d x = d y d u ⋅ d u d x \frac{dy}{d\mathbf{x}}=\frac{dy}{du}\cdot\frac{du}{d\mathbf{x}} dxdy=dudy⋅dxdu
- d y / d u dy/du dy/du:标量 → 形状不变
- d u / d x du/dx du/dx:若 u u u 是向量, x x x 是向量 → 雅可比矩阵(形状 [ d i m ( u ) , d i m ( x ) ] [dim(u), dim(x)] [dim(u),dim(x)])
多变量链式法则
d z d w = d z d b ⋅ d b d a ⋅ d a d w = 2 b ⋅ 1 ⋅ x T \frac{dz}{d\mathbf{w}}=\frac{dz}{db}\cdot\frac{db}{da}\cdot\frac{da}{d\mathbf{w}}=2b\cdot1\cdot\mathbf{x}^T dwdz=dbdz⋅dadb⋅dwda=2b⋅1⋅xT
- 示例:线性回归 z = ( x T w − y ) 2 z = (x^Tw - y)^2 z=(xTw−y)2 的梯度计算
2 自动求导
自动求导计算一个函数在指定值上的导数,它有别于
- 符号求导
- 数值求导
2.1 计算图
构建原理
-
将代码分解成操作子
-
将计算表示成一个无换图
-
节点:输入变量(如 x , w , y x,w,y x,w,y)或基本操作(如 + , − , × +,-,× +,−,×)
-
边:数据流向
-
显式 vs 隐式构造
| 类型 | 代表框架 | 特点 |
|---|---|---|
| 显式 | TensorFlow,Mxnet,Theano | 先定义计算图,后喂入数据 |
| 隐式 | PyTorch,Mxnet | 动态构建图,操作即记录 |
2.2 正向模式
从输入到输出逐层计算梯度,每次计算一个输入变量对输出的梯度,通过链式法则逐层传递梯度。
以 z = ( x ⋅ w − y ) 2 z = (x \cdot w - y)^2 z=(x⋅w−y)2 为例(线性回归损失函数):
# 正向计算过程
a = x * w # a对x的梯度:∂a/∂x = w
b = a - y # b对a的梯度:∂b/∂a = 1
z = b ** 2 # z对b的梯度:∂z/∂b = 2b
-
特点:每次只能计算一个输入变量(如
x或w)的梯度,需多次计算。 -
计算复杂度:
O(n)(n为输入维度) -
内存复杂度:
O(1)(不需要存储中间结果) -
适用场景:输入维度低(如参数少)、输出维度高的函数。
2.3 反向模式
从输出到输入反向传播梯度,一次性计算所有输入变量对输出的梯度。
数学原理
-
前向计算
计算所有中间值(
a,b,z)并存储。 -
反向传播(Back Propagation,也称反向传递)
从输出
z开始,按链式法则逐层回传梯度。先计算
∂z/∂b = 2b,再计算∂b/∂a = 1,最后计算∂a/∂x = w。
同样以 z = ( x ⋅ w − y ) 2 z = (x \cdot w - y)^2 z=(x⋅w−y)2 为例:
-
前向计算
a = x * w # 存储 a b = a - y # 存储 b z = b ** 2 -
反向传播
dz_db = 2 * b # ∂z/∂b db_da = 1 # ∂b/∂a da_dx = w # ∂a/∂x dz_dx = dz_db * db_da * da_dx # 最终梯度
- 计算复杂度:
O(n)(与正向模式相同) - 内存复杂度:
O(n)(需存储所有中间变量) - 适用场景:深度学习(输入维度高,输出为标量损失函数)。
3 实战:自动求导
3.1 简单示例
以函数 y = 2 x ⊤ x y=2\mathbf{x}^{\top}\mathbf{x} y=2x⊤x 为例,关于列向量 x \mathbf{x} x 求导。
-
首先,创建变量
x并为其分配一个初始值。import torchx = torch.arange(4.0) x
-
在计算 y y y 关于 x \mathbf{x} x 的梯度之前,需要一个地方来存储梯度。
我们不会在每次对一个参数求导时都分配新的内存。
因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。
注意,一个标量函数关于向量 x \mathbf{x} x 的梯度是向量,并且与 x \mathbf{x} x 具有相同的形状。
x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True) x.grad # 默认值是None -
现在计算 y y y。
y = 2 * torch.dot(x, x) y
-
x是一个长度为 4 的向量,计算x和x的点积,得到了我们赋值给y的标量输出。
接下来,通过调用反向传播函数来自动计算y关于x每个分量的梯度,并打印这些梯度。y.backward() x.grad
-
函数 y = 2 x ⊤ x y=2\mathbf{x}^{\top}\mathbf{x} y=2x⊤x 关于 x \mathbf{x} x 的梯度应为 4 x 4\mathbf{x} 4x。让我们快速验证这个梯度是否计算正确。
x.grad == 4 * x
-
探究
x的另一个函数。# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值 x.grad.zero_() y = x.sum() y.backward() x.grad
3.2 非标量的反向传播
当y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。
对于高阶和高维的y和x,求导的结果可以是一个高阶张量。
虽然这些更奇特的对象确实出现在高级机器学习中(包括[深度学习中]),但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。
这里,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
理解
x.grad.zero_()
作用:清空
x的梯度(grad)缓存。为什么需要清零?
- PyTorch 会累积梯度(
grad),如果之前已经计算过x的梯度(比如在循环中多次backward()),新的梯度会加到旧的梯度上。- 调用
zero_()可以避免梯度累积,确保每次计算都是新的梯度。
y = x \* x
计算
y = x²(逐元素相乘)。例如:
- 如果
x = [1, 2, 3],那么y = [1, 4, 9]。
y.backward(torch.ones(len(x)))
backward()的作用:计算y对x的梯度(即dy/dx)。- 为什么需要
gradient参数?
- 如果
y是 标量(单个值),可以直接调用y.backward(),PyTorch 会自动计算dy/dx。- 但如果
y是 非标量(向量/矩阵),PyTorch 不知道如何计算梯度,必须传入一个gradient参数(形状和y相同),表示y的梯度权重。gradient=torch.ones(len(x))的含义:
- 这里
gradient是一个全 1 的张量,表示我们希望计算y的所有分量对x的 梯度之和(相当于sum(y)对x的梯度)。- 数学上:
y = [y₁, y₂, y₃] = [x₁², x₂², x₃²]sum(y) = x₁² + x₂² + x₃²d(sum(y))/dx = [2x₁, 2x₂, 2x₃](这就是x.grad的结果)
结果
x.grad
由于
y = x²,dy/dx = 2x。由于
gradient=torch.ones(len(x)),PyTorch 计算的是sum(y)的梯度:
x.grad = [2x₁, 2x₂, 2x₃](即2 * x)。例如:
- 如果
x = [1, 2, 3],那么x.grad = [2, 4, 6]。
3.3 分离计算
有时,我们希望将某些计算移动到记录的计算图之外。例如,假设y是作为x的函数计算的,而z则是作为y和x的函数计算的。
想象一下,我们想计算z关于x的梯度,但由于某种原因,希望将y视为一个常数,并且只考虑到x在y被计算后发挥的作用。这里可以分离y来返回一个新变量u,该变量与y具有相同的值,但丢弃计算图中如何计算y的任何信息?
换句话说,梯度不会向后流经u到x。因此,下面的反向传播函数计算z = u * x关于x的偏导数,同时将u作为常数处理,而不是z = x * x * x关于x的偏导数。
x.grad.zero_()
y = x * x
u = y.detach()
z = u * xz.sum().backward()
x.grad == u
由于记录了y的计算结果,我们可以随后在y上调用反向传播,得到y = x * x关于的x的导数,即2 * x。
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
3.4 Python 控制流
使用自动微分的一个好处是:即使构建函数的计算图需要通过 Python 控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到的变量的梯度。
在下面的代码中,while循环的迭代次数和if语句的结果都取决于输入a的值。
def f(a):# type: (torch.Tensor)->torch.Tensorb = a * 2while b.norm() < 1000:b = b * 2if b.sum() > 0:c = belse:c = 100 * breturn c
让我们计算梯度。
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()
我们现在可以分析上面定义的f函数。请注意,它在其输入a中是分段线性的。换言之,对于任何a,存在某个常量标量k,使得f(a)=k*a,其中k的值取决于输入a,因此可以用d/a验证梯度是否正确。
a.grad, d / a, a.grad == d / a

