本文讲解梯度的定义和求解方式,然后引入 PyTorch 中的相关函数,完成张量的梯度定义、梯度计算、梯度清空以及关闭梯度等操作。
梯度的求解一、梯度计算
1.requires_grad2.backward3.复合函数求导 二、停止张量的梯度计算
1.x.requires_grad_(False)2.x.detach()3. with torch.no_grad() 三、梯度的清空
1.变量梯度清零x.grad.zero_()2.优化器梯度清零optimizer.zero_grad()
import torch一、梯度计算
1.requires_grad
张量梯度的计算 在一元函数中,某点的梯度表示的就是某点的导数。在多元函数中某点的梯度表示的是,由每个自变量所对应的偏导值所组成的向量。如
f
(
x
,
y
,
z
)
f(x,y,z)
f(x,y,z) 的梯度向量就是:
(
∂
f
/
∂
x
,
∂
f
/
∂
y
,
∂
f
/
∂
z
)
(partial f/ partial x,partial f/ partial y,partial f/ partial z)
(∂f/∂x,∂f/∂y,∂f/∂z)
梯度的方向就是函数值上升最快的方向。我们一般可以使用torch.autograd.backward() 来自动计算变量的梯度,该函数会对指定的变量进行偏导的求取。为了辨别函数中哪些变量需要求偏导,哪些不需要求偏导,我们一般会在定义张量时,加上requires_grad=True,表示该变量可以求偏导。
x=torch.randn(1,requires_grad=True) y=torch.randn(1) z=torch.randn(1) f1=2*x+y f2=y+z # 查看变量是否存在求梯度函数 print(f1.grad_fn) print(f2.grad_fn)
输出结果如下:
None
从结果可以看出, x x x 被定义成可以求偏导的变量,因此,它所对应的变量 f 1 f1 f1 就是可求导的(通过 torch.grad_fn 查看)。
2.backward
接下来让我利用 f1.backward() 求取 f 1 f1 f1 的梯度(即所有变量的偏导),然后利用 x.grad 展示 ∂ f / ∂ x partial f / partial x ∂f/∂x的值。
f1.backward() print(x.grad) # df1/dx
输出结果如下:
tensor([2.])
3.复合函数求导
当然除了上面简单的一元函数求偏导外,我们还可以使用上面的方法来求取复合函数的偏导:
x=torch.randn(3,requires_grad=True)#x中存了三个变量,x1 x2 x3 y=x+2 z=y*y*3 z=z.mean() print(z) print(z.grad_fn)
tensor(11.7241, grad_fn=)
根据上面代码可知,我们定义了一个 z 关于变量 x 的多元复合函数,如下:
y
=
x
+
2
y=x+2
y=x+2
z
=
1
n
∑
i
=
1
n
3
y
i
2
z=frac {1}{n} sum_{i=1}^n3y_i^2
z=n1i=1∑n3yi2
我们动手计算以下
z
z
z关于
x
x
x的偏导数,首先将
z
z
z进行展开:
z
=
1
n
∑
i
=
1
n
=
1
3
(
3
y
1
2
+
3
y
2
2
+
3
y
3
2
)
z=frac {1}{n}sum_{i=1}^n=frac {1}{3}(3y_1^2+3y_2^2+3y_3^2)
z=n1i=1∑n=31(3y12+3y22+3y32)
特别的,我们计算
z
z
z关于
x
1
x_1
x1的偏导,
∂
z
∂
x
1
=
∂
z
∂
y
1
⋅
∂
y
1
∂
x
1
frac {partial z}{partial x_1}=frac {partial z}{partial y_1} cdot frac {partial y_1}{partial x_1}
∂x1∂z=∂y1∂z⋅∂x1∂y1。
首先计算
∂
z
∂
y
1
frac {partial z}{partial y_1}
∂y1∂z:
∂
z
∂
y
1
=
1
3
∗
3
∗
2
∗
y
1
=
2
y
1
frac {partial z}{partial y_1}=frac {1}{3}*3*2*y_1=2y_1
∂y1∂z=31∗3∗2∗y1=2y1
接着计算
∂
y
1
∂
x
1
frac {partial y_1}{partial x_1}
∂x1∂y1为:
∂
y
1
∂
x
1
=
1
frac {partial y_1}{partial x_1}=1
∂x1∂y1=1
所以最终
∂
z
∂
x
1
frac {partial z}{partial x_1}
∂x1∂z为:
∂
z
∂
x
1
=
∂
z
∂
y
1
⋅
∂
y
1
∂
x
1
=
2
y
1
=
2
(
x
1
+
2
)
frac {partial z}{partial x_1}=frac {partial z}{partial y_1} cdot frac {partial y_1}{partial x_1}=2y_1=2(x_1+2)
∂x1∂z=∂y1∂z⋅∂x1∂y1=2y1=2(x1+2)
我们也可以使用 z.backward()求取梯度,该张量的梯度结果会被放在所对应变量的 grad 属性中。下面我们比较一下通过 z.backward()求取梯度和我们上面推导出的结果是否一样。
z.backward() print(x.grad) # dz/dx print(2*(x+2)) # 比较直接计算的结果
输出结果如下:
tensor([4.7038, 3.6106, 3.4256]) tensor([4.7038, 3.6106, 3.4256], grad_fn=)
上面结果为函数
z
z
z的梯度向量,即函数
z
z
z分别关于
x
1
,
x
2
,
x
3
x_1,x_2,x_3
x1,x2,x3 的偏导数。
简单的说, torch.autograd.backward 就是使用链式法则对变量的偏导进行了求解。该函数有一个参数 grad_variables,该参数相当于给原梯度进行了一个加权。
如果使用函数 k.backward(p) 则得到的的变量 x.grad 的值为:
x
⋅
g
r
a
d
=
p
⋅
k
x
xcdot grad=pcdot frac {k}{x}
x⋅grad=p⋅xk
x = torch.randn(3, requires_grad=True)
k = x * 2
for _ in range(10):
k = k * 2
print(k)
print(k.shape)
p = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float32)
k.backward(p)
print(x.grad)
输出结果如下:
tensor([-2075.6987, 782.0361, 593.5969], grad_fn=) torch.Size([3]) tensor([2.0480e+02, 2.0480e+03, 2.0480e-01])
二、停止张量的梯度计算
如果我们不需要某些张量的梯度计算,我们就可以使用下面三种方法告诉计算机停止梯度的计算:
x.requires_grad_(False) x.detach() with torch.no_grad():
1.x.requires_grad_(False)
就地更改现有标志:
a = torch.randn(2, 2, requires_grad=True) b = ((a * 3) / (a - 1)) print(b.grad_fn) # 此时可偏导,求取梯度的函数存在 a.requires_grad_(False) b = ((a * 3) / (a - 1)) print(b.grad_fn) # 此时不可偏导了,求取梯度的函数不存在了
输出结果如下:
None
2.x.detach()
获取具有相同内容但不能进行梯度计算的新张量:
a = torch.randn(2, 2, requires_grad=True) b = a.detach() print(a.requires_grad) print(b.requires_grad)
输出结果如下:
True False
3. with torch.no_grad()
作用域中定义的都是不进行梯度计算的张量。
a = torch.randn(2, 2, requires_grad=True)
print((a ** 2).requires_grad)
with torch.no_grad(): # 该作用域下定义的都是不进行梯度计算的张量
print((a ** 2).requires_grad)
输出结果如下:
True False
三、梯度的清空
1.变量梯度清零x.grad.zero_()
在 PyTorch 中,如果我们利用 torch.autograd.backward求取张量的梯度时。但是,如果我们多次运行该函数,该函数会将计算得到的梯度累加起来,如下所示:
x=torch.ones(4,requires_grad=True)
y=(2*x+1).sum()
z=(2*x).sum()
y.backward()
print("第一次偏导:", x.grad) # dy/dx
z.backward()
print("第二次偏导:", x.grad) # dy/dx+dz/dx
输出结果如下:
第一次偏导: tensor([2., 2., 2., 2.]) 第二次偏导: tensor([4., 4., 4., 4.])
从上面的结果可以看到,如果我们对张量
y
y
y和
z
z
z分别求梯度,那么它们关于
x
x
x的偏导都会被放入
x
⋅
g
r
a
d
xcdot grad
x⋅grad中,形成累加的局面。
我们可以使用 x.grad.zero_() 清空梯度。
x = torch.ones(4, requires_grad=True)
y = (2*x+1).sum()
z = (2*x).sum()
y.backward()
print("第一次偏导:", x.grad) # dy/dx
x.grad.zero_()
z.backward()
print("第二次偏导:", x.grad) # dz/dx
输出结果如下:
第一次偏导: tensor([2., 2., 2., 2.]) 第二次偏导: tensor([2., 2., 2., 2.])
这个性质是非常重要的,特别是在后面我们将要学到的梯度下降算法之中。
因为我们训练模型时需要循环求梯度,如果这时梯度一直叠加,那么我们求出来的结果就没有意义。因此,可以使用上面方法对张量的偏导进行清空。
2.优化器梯度清零optimizer.zero_grad()
除了张量中存在梯度清空函数,优化器中也存在这样的函数:zero_grad()。
optimizer = torch.optim.SGD([x], lr=0.1) optimizer.step() optimizer.zero_grad() optimizer
输出结果如下:
SGD (
Parameter Group 0
dampening: 0
lr: 0.1
momentum: 0
nesterov: False
weight_decay: 0
)



