本节我们主要来学习梯度的定义和梯度的求解, 然后利用Pytorch相关的函数,实现张量的梯度定义,梯度计算,梯度清空已经关闭梯度等操作
梯度的计算
在一元函数中,某点的梯度标的就说某点的导数. 在多元函数中某点的梯度表示的是由每个自变量所对应的偏导数所组成的向量.如 f(x,y,z) 的梯度向量就是
%0A#card=math&code=%28%5Cfrac%7B%5Cpartial%20f%7D%7B%5Cpartial%20x%7D%2C%5Cfrac%7B%5Cpartial%20f%7D%7B%5Cpartial%20y%7D%2C%5Cfrac%7B%5Cpartial%20f%7D%7B%5Cpartial%20z%7D%29%0A&id=p3w8I)
梯度的方向就说函数值上升最快的方向
%0A#card=math&code=z%20%3D%20%5Cfrac%7B1%7D%7Bn%7D%5Csum%203y_i%5E2%20%3D%20%5Cfrac%20%7B1%7D%7B3%7D%283y_1%5E2%20%2B%203y_2%5E2%20%2B%203y_3%5E2%29%0A&id=Xm9XQ)
我们一般可以使用torch.autograd.backward()
来自动计算变量的梯度,该函数会对指定的变量进行偏导的求取.为了辨别函数中哪些变量需要求偏导,哪些不需要求偏导,我们一般会在定义张量时,加上requires_grad=True, 表示该变量可以求偏导
import torch
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)
从结果可以看出, x被定义成可以求偏导的变量, 因此, 它所对应的变量f1就是可求导的.
接下来,我们利用f1.backward()
求取f1的梯度(即所有变量的偏导), 然后利用x.grad
获取
f1.backward()
print(x.grad) # df1/dx
当然除了上面简单的一元函数求偏导外, 我们还可以使用上面的方法来求取复合函数的偏导:
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)
根据上面的代码可知,我们定义了一个Z关于变量x的多元复合函数,如下:
我们手工计算一下z关于x的偏导数. 首先我们将z进行展开.
%0A#card=math&code=z%20%3D%20%5Cfrac%7B1%7D%7Bn%7D%5Csum_%7Bi%3D1%7D%5E%7Bn%7D3y_i%5E2%20%3D%20%5Cfrac%7B1%7D%7B3%7D%283y_1%5E2%20%2B%203y_2%5E2%20%2B%203y_3%5E2%29%0A&id=PQbnY)
特别的, 我们计算z对于x1的偏导
我们首先计算:
接着计算:
最终:
%0A#card=math&code=%5Cfrac%7B%5Cpartial%20z%7D%7B%5Cpartial%20x%7B1%7D%7D%20%3D%20%5Cfrac%7B%5Cpartial%20z%7D%7B%5Cpartial%20y%7B1%7D%7D%20%5Ccdot%20%5Cfrac%7B%5Cpartial%20y%7B1%7D%7D%7B%5Cpartial%20x%7B1%7D%7D%20%3D%202y_1%20%5Ccdot%201%20%3D%202%28x_1%20%2B%202%29%0A&id=hURu1)
我们也可以使用z.backward()求取梯度, 该张量的梯度结果会被放在所对应的变量grad属性中.下面我们比较一下通过z.backward() 求取梯度和我们上面推导的结果是否一致
z.backward()
print(x.grad)
print(2*(x + 2))
上面结果为函数z的梯度向量,即函数z分别关于x1,x2,x3的偏导数.
简单的说, torch.autograd.backward就是使用链式法则对变量的偏导进行了求解.该函数有一个参数grad_variables, 该参数相当于给原梯度进行了一个加权.
如果使用函数k.backward(p)则得到的变量x.grad的值为:
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)
停止张量的梯度计算
如果我们不需要某些张量的梯度计算, 我们就可以使用下面三种方法告诉计算机停止梯度的计算:
- x.requiresgrad(False)
- x.detach()
- with torch.no_grad()
利用x.requiresgrad(…) 就地更改现有标志
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)
利用x.detach()获取具有相同内容但不能进行梯度计算的新张量
a = torch.randn(2,2,requires_grad=True)
b = a.detach()
print(a.requires_grad)
print(b.requires_grad)
在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)
梯度的清空
在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
从上面的结果可以看出,如果我们对张量y和z分别求梯度,那么他们关于x的偏导都会被放入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)
x.grad.zero_()
z.backward()
print("第二次偏导:",x.grad)
这个性质说非常重要的,在后面我们做梯度下降的时候会用到
因为我们训练模型时需要循环求梯度,如果这时梯度一直叠加, 那么我们求出来的结果就没有意义.因此,可以使用上面的方法对张量的偏导进行清空.
除了张量中存在梯度清空函数,优化器中也存在这样的函数: zero_grad()
optimizer = torch.optim.SGD([x], lr=0.1)
optimizer.step()
optimizer.zero_grad()
print(optimizer)
这里提到的优化器,我们会在后面的学习中学到,这里先只要知道优化器也需要进行梯度清空即可
小结
本节我们首先讲解了梯度的含义,然后利用PyTorch定义了可以自动求偏导的张量, 然后对张量进行了梯度求解, 最后阐述了梯度清空的重要性和必要性.