在PyTorch中,torch.Tensor类是存储和变换数据的重要工具,相比于Numpy,Tensor提供GPU计算和自动求梯度等更多功能,在深度学习中,我们经常需要对函数求梯度(gradient)。PyTorch提供的autograd包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。本篇将介绍和总结如何使用autograd包来进行自动求梯度的有关操作。
Tensor是这个pytorch的自动求导部分的核心类,如果将其属性.requires_grad=True
,它将开始追踪(track) 在该tensor上的所有操作,从而实现利用链式法则进行的梯度传播。完成计算后,可以调用.backward()
来完成所有梯度计算。此Tensor的梯度将累积到.grad属性中。
如果不想要被继续对tensor进行追踪,可以调用.detach()将其从追踪记录中分离出来,接下来的梯度就传不过去了。此外,还可以用with torch.no_grad()
将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为此时并不需要继续对梯度进行计算。
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)
'''
输出:
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
None
'''
像x这种直接创建的tensor 称为叶子节点,叶子节点对应的grad_fn是None。如果进行一次运算操作:
y = x + 1
print(y)
print(y.grad_fn)
'''
tensor([[2., 2.],
[2., 2.]], grad_fn=)
'''
而y是通过一个加法操作创建的,所以它有一个为操作的grad_fn。
尝试进行更复杂的操作:
z = y ** 2
out = z.mean()
print(z, out)
'''
tensor([[4., 4.],
[4., 4.]], grad_fn=) tensor(4., grad_fn=)
'''
上面的out是一个标量4,通常对于标量直接使用out.backward()进行求导,不需要指定求导变量,后面进行详细说明。
也可以通过.requires_grad_()改变requires_grad属性:
a = torch.randn(3, 2) # 缺失情况下默认 requires_grad = False
a = (a ** 2)
print(a.requires_grad) # False
a.requires_grad_(True) #使用in-place操作,改变属性
print(a.requires_grad) # True
b = (a * a).sum()
print(b.grad_fn)
'''
False
True
'''
torch.autograd实现梯度求导的链式法则,用来计算一些雅克比矩阵的乘积,即函数的一阶导数的乘积。
注意: grad在反向传播过程中是累加的(accumulated),每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零x.grad.data.zero_()
。
x = torch.ones(2, 2, requires_grad=True)
y = x + 1
z = y ** 2
out = z.mean()
print(z, out)
out.backward()
print(x.grad)
# 注意grad是累加的
out2 = x.sum()
out2.backward()
print(out2)
print(x.grad)
out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(out3)
print(x.grad)
'''
tensor([[4., 4.],
[4., 4.]], grad_fn=) tensor(4., grad_fn=)
tensor([[1., 1.],
[1., 1.]])
tensor(4., grad_fn=)
tensor([[2., 2.],
[2., 2.]])
tensor(4., grad_fn=)
tensor([[1., 1.],
[1., 1.]])
'''
Tensor的自动求导对于标量比如上面的out.backward()十分方便,但是当反向传播的对象不是标量时,需要在y.backward()种加入一个与out同形的Tensor,不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量同形的张量。这是为了避免向量(甚至更高维张量)对张量求导,而转换成标量对张量求导。
x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
y = 2 * x
z = y.view(2, 2)
print(z)
'''
tensor([[2., 4.],
[6., 8.]], grad_fn=)
'''
显然上面的tensor z
不是一个标量,所以在调用 z.backward()
时需要传入一个和z同形的权重向量进行加权求和得到一个标量。
c = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float)
z.backward(c)
print(x.grad)
'''
tensor([[2., 4.],
[6., 8.]], grad_fn=)
tensor([2.0000, 0.2000, 0.0200, 0.0020])
'''
我们可以使用detach()
或者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=) True
tensor(1.) False
tensor(2., grad_fn=) True
'''
我们尝试计算梯度:
y3.backward()
print(x.grad)
# y2.backward() #这句会报错,因为此时 y2.requires_grad=False,,无法调用反向传播
'''
tensor(2.)
'''
这里结果为2,是因为我们没有获得y2的梯度,仅仅是对y1做了一次反向传播,作为最后的梯度输出。
如果我们想要修改tensor的数值,但是不希望保存在autograd的记录中,require s_grad = False
, 即不影响到正在进行的反向传播,那么可以用tensor.data进行操作。但是这种操作需要注意可能会产生一些问题,比如标量为0
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)
pytorch0.4以后保留了.data() 但是官方文档建议使用.detach(),因为使用x.detach时,任何in-place变化都会使backward报错,因此.detach()是从梯度计算中排除子图的更安全方法。
如下面的例子:
torch.tensor([1,2,3.], requires_grad = True)
out = a.sigmoid()
c = out.detach()
c.zero_() # in-place为0 ,tensor([ 0., 0., 0.])
print(out) # modified by c.zero_() !! tensor([ 0., 0., 0.])
out.sum().backward() # Requires the original value of out, but that was overwritten by c.zero_()
'''
RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation
'''
a = torch.tensor([1,2,3.], requires_grad = True)
out = a.sigmoid()
c = out.data
c.zero_() # tensor([ 0., 0., 0.])
print(out) # out was modified by c.zero_() tensor([ 0., 0., 0.])
out.sum().backward()
a.grad # 这么做不会报错,但是a已经被改变,最后计算的梯度实际是错误的
'''
tensor([ 0., 0., 0.])
'''
参考:
PyTorch官方文档
github issues