pytorch自动求导
学习过程中参考动手学深度学习pytorch版
深度卷积神经网络权重参数更新的的基础实际上就是一个导数的链式法则,然后将其误差反向传播。所以对于一个深度学习框架来说最重要和最基本的操作之一就是自动求导机制,在pytorch中提供的autograd包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。本节将介绍如何使用autograd包来进行张量自动求梯度的有关操作。
基本概念
前面已经将tensor的创建和基本操作学了一遍,其实在Tensor
中还有许多核心的属性没有说到,比如要想完成自动求导,可以将.autograd
属性设置为True
,这样他就可以追踪在张量上面的所有操作来完成链式求导法则,完成计算后通过.backward()
完成反向传播实现梯度计算,同时计算得到的梯度将会累积到.grad
属性中。
在调用
y.backward()
时,如果张量y
是一个标量则不需要传入任何参数,如果不是标量则需要传入一个与y
同形的张量,这里我的理解是其实和矩阵求导一样也就是点对点求导,如果对不上那就没办法进行求导,因此需要一个同形张量来完成一个点对点的求导操作。
在模型评估中其实有的时候并不想让梯度被追踪,这个时候就应该想办法让其停止梯度的反向传播,通常可以通过调用.datach()
将其不希望被追踪的中间变量从追踪记录分离出来,这样就可以不用被追踪到。如果是希望一个块不被追踪到,则可以通过with torch.nograd():
将不希望被追踪的模块包裹起来,实现不被追踪反向传播。这种方法在评估模型的时候很常用,因为在评估模型时,我们并不需要计算可训练参数(requires_grad=True
)的梯度。
Function
是另外一个很重要的类。Tensor
和Function
互相结合就可以构建一个记录有整个计算过程的有向无环图(DAG)。每个Tensor
都有一个.grad_fn
属性,该属性即创建该Tensor
的Function
, 就是说该Tensor
是不是通过某些运算得到的,若是则grad_fn
返回一个与这些运算相关的对象,否则是None。例如:
#通过将tensor的requires_grad设置为True,可以实现自动求导
x = torch.ones(2, 2, requires_grad=True)
print(x)
print(x.grad_fn) #由于是常数,所以求导得到的没有值,返回None
y = x + 2
print(y)
print(y.grad_fn)#返回<AddBackward0 object at 0x00000203C02361D0>
#注意x是直接创建的,所以它没有
grad_fn
, 而y是通过一个加法操作创建的所以它有一个为<AddBackward>
的grad_fn
。
对于求导得到的其实也是一个张量,所以对张量的操作依然可以用在求导前后的张量中。例如,同样可以通过后缀下划线实现inplace,通过.requires_grad_()
来用inplace的方式改变requires_grad
属性:
a = torch.randn(2, 2) # 不指定的情况下默认 requires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad) # False
a.requires_grad_(True)
print(a.requires_grad) # True
b = (a * a).sum()
print(b.grad_fn)
输出:
False
True
<SumBackward0 object at 0x00000203BCE72160>
计算实例
如上所示,在通过计算得到的变量是由.grad_fn
属性的,并且这个属性是跟原来的变量(可以称为自变量吧)有关的。并且这种直接创建的张量在计算图中被认为是叶节点,而计算得到的则不是叶节点,可以通过.is_leaf
属性查看:
print(x.is_leaf, y.is_leaf)
# True False
为了体现链式求导的法则,当然也有复杂一些的操作,比如下面的先乘积在求均值的操作求导中:
z = y * y * 3
out = z.mean() #标量
print(z, out)
输出:
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward0>)
tensor(27., grad_fn=<MeanBackward0>)
这里再来一个反向传播感受一下标量的求导不需要给.backward()
传入任何参数:
#直接利用反向传播的方法
out.backward() # 等价于 out.backward(torch.tensor(1.))
print(x.grad)
输出:
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])
解释:
要计算的是out
关于x
的梯度
d
(
o
u
t
)
d
x
\frac{d(out)}{dx}
dxd(out),我们令out
为
o
o
o , 因为
o
=
1
4
∑
i
=
1
4
z
i
=
1
4
∑
i
=
1
4
3
(
x
i
+
2
)
2
o=\frac14\sum_{i=1}^4z_i=\frac14\sum_{i=1}^43(x_i+2)^2
o=41i=1∑4zi=41i=1∑43(xi+2)2
所以
∂
o
∂
x
i
∣
x
i
=
1
=
9
2
=
4.5
\frac{\partial{o}}{\partial{x_i}}\bigr\rvert_{x_i=1}=\frac{9}{2}=4.5
∂xi∂o∣∣xi=1=29=4.5
所以上面的输出没有问题。在数学上矩阵的链式求导可以用雅可比矩阵和一个向量来表示,有兴趣可以查阅原书,我的公式打得还不熟。
下面来一个更复杂些的反向传播操作:
#grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,
#梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。
#再来反向传播一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)
#先清零梯度在进行反向传播,不在累加
out3 = x.sum()
x.grad.data.zero_() #梯度清零
out3.backward()
print(x.grad)
为了避免梯度在求导过程中出现矩阵相乘或者维度不一致的问题,所以我们不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量同形的张量。所以必要时我们要把张量通过将所有张量的元素加权求和的方式转换为标量,举个例子,假设y
由自变量x
计算而来,w
是和y
同形的张量,则y.backward(w)
的含义是:先计算l = torch.sum(y * w)
,则l
是个标量,然后求l
对自变量x
的导数。例如:
#不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量同形的张量
x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
y = 2 * x
print(y)
z = y.view(2, 2)
print(z)
#现在 z 不是一个标量,所以在调用backward时需要传入一个和z同形的权重向量进行加权求和得到一个标量。
v = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float)#z同形的权重向量
z.backward(v)
print(x.grad)
输出:
tensor([2., 4., 6., 8.], grad_fn=<MulBackward0>)
tensor([[2., 4.],
[6., 8.]], grad_fn=<ViewBackward>)
tensor([2.0000, 0.2000, 0.0200, 0.0020])
梯度回传例子,用
with torch.no_grad():
包裹一个模块:
#梯度追踪
x = torch.tensor(1.0, requires_grad=True)
y1 = x ** 2
with torch.no_grad():
y2 = x ** 3
y3 = y1 + y2
print(x.requires_grad)
print(y1, y1.requires_grad) # True
print(y2, y2.requires_grad) # False
print(y3, y3.requires_grad) # True
输出:
True
tensor(1., grad_fn=<PowBackward0>) True
tensor(1.) False
tensor(2., grad_fn=<AddBackward0>) True
在这个反向传播的过程中y3是有grad_fn
的,但是y2没有这个东西,因此如果我们随y3反向传播得到一个grad_fn
会有一个什么样的结果:
y3.backward()#反向传播
print(x.grad) #y2的梯度被包裹不会前向传播,因此只有y1的导数,同时y2.backward()他是会报错的。
此外,如果我们想要修改tensor
的数值,但是又不希望被autograd
记录(即不会影响反向传播),那么我么可以对tensor.data
进行操作。
x = torch.ones(1,requires_grad=True)
print(x.data) # 还是一个tensor
print(x.data.requires_grad) # 但是已经是独立于计算图之外
y = 2 * x
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播
y.backward()
print(x) # 更改data的值也会影响tensor的值
print(x.grad)
输出:
tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])
算图,所以不会影响梯度传播
y.backward()
print(x) # 更改data的值也会影响tensor的值
print(x.grad)
输出:
tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])