初步了解了Pytorch如何使用自动求导,以及如何编写简单的神经网络,然后通过阅读官方文档Autograd mechanics,Automatic differentiation package - torch.autograd以及网上各种博客笔记,记录一下对自动求导机制的个人理解。
计算图:Variable和Function
Variable
Variable的 API兼容了几乎所有Tensor API,大多数情况下,都可以将Tensor替换为Variable。
class torch.autograd.Variable
中,构造函数接收三个参数:
data (any tensor class)
,required_grad(bool)
,volatile(bool)
。
requires_grad
:之前博客中有提到,表示了一个操作中是否有输入设置了该变量。如果任一输入设置requires_grad=True
,则输出Variable的该属性也为True
,反之,如果所有输入变量都设置requires_grad=False
,则输出Variable的该属性也为False
。
在训练模型中,有时我们想固定一些层,而针对某一特定层单独训练,那么我们就可以设置一些层的变量参数requires_grad=False
,这样在训练中,就不会针对这些参数求梯度,从而保证了这些层不会受到训练的影响。如官方文档给出的例子:
model = torchvision.models.resnet18(pretrained=True)
for param in model.parameters():
param.requires_grad = False
# Replace the last fully-connected layer
# Parameters of newly constructed modules have requires_grad=True by default
model.fc = nn.Linear(512, 100)
# Optimize only the classifier
optimizer = optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9)
这里使用了预训练的模型,通过设置模型中的所有参数的属性requires_grad=False
,然后替换掉最后一层全连接层,从而使模型训练时只针对最后一层全连接层。
volatile
:推断模式,比设置requires_grad
更加有效,可以最大程度的节省内存。volatile=True
决定了requires_grad=False
。当一个输入设置了volatile=True
,那么输出变量的该属性也为True(意味着不需要求梯度),反之,如果所有输入变量都设置volatile=False,
则输出Variable的该属性也=才为False
。在实际修改模型时,只需将输入叶子节点设置属性volatile=True
,不必修改任何requires_grad
属性
二者的参数值传递方式相同,但是结果相反。
Variable类有以下属性:
data
:封装的Tensorgrad
:保存着变量的梯度,并且该变量只能赋值一次requires_grad
:指示了是否在创建该Variable的子图中,有指定该属性为True
的Variable。该属性只能在叶子节点中被修改。volatile
:指示了是否用于推断模式(不保留历史信息)该属性只能在叶子节点中被修改。is_leaf
:Variable是否为叶子节点(如用户创建的)grad_fn
:创建该Variable的Function(如果该 Variable 是由用户创建的,那么该属性为 None)
在对Variable求梯度时,会通过链式法则自动求出该变量对叶子节点的梯度,同时设置叶子节点的grad
属性
import torch
from torch.autograd import Variable
a = Variable(torch.Tensor([1, 2]), requires_grad=True)
b = a * a
c = b + 2
c.backward(gradient=torch.Tensor([1, 1]))
print(b.grad)
print(a.grad)
None
Variable containing:
2
4
[torch.FloatTensor of size 2]
.backward(gradient=None, retain_graph=None, create_graph=None, retain_variables=None)
方法是用来求当前变量对叶子节点的梯度并且累加。该方法有以下参数
gradient
:与梯度相关的Variable(Tensor自动转换为Variable),需要与所求梯度的维度匹配。retain_graph
:如果设置为False
,在计算完梯度后,构造的计算图就会被释放。在大多数情况下,该变量都为True
,并且可以提高效率。初始化值与create_graph
相同。create_graph
:表示计算图是否创建,从而计算高阶梯度。默认为False
。
在上面代码中,计算c对a的梯度,由于a是二维变量,在调用.backward()
时传递了参数gradient=torch.Tensor([1, 1])
当多次调用.backward()
方法时,由于没有设置create_graph
与retain_graph
,会出现RuntimeError
a = Variable(torch.Tensor([1, 2]), requires_grad=True)
b = a * a
c = b + 2
c.backward(torch.Tensor([1, 1]))
print(a.grad)
c.backward(torch.Tensor([1, 1]))
print(a.grad)
RuntimeError: Trying to backward through the graph a second time, but the buffers have already been freed. Specify retain_graph=True when calling backward the first time.
这是因为我们想在一个计算图中第二次调用backward()
方法,但是由于没有设置retain_graph
,计算图已经被释放掉,所以无法第二次调用。
在设置参数retain_graph
后,即可正常调用
a = Variable(torch.Tensor([1, 2]), requires_grad=True)
b = a * a
c = b + 2
c.backward(torch.Tensor([1, 1]), retain_graph=True)
print(a.grad)
c.backward(torch.Tensor([1, 1]), retain_graph=True)
print(a.grad)
c.backward(torch.Tensor([1, 1]), retain_graph=True)
print(a.grad)
Variable containing:
2
4
[torch.FloatTensor of size 2]
Variable containing:
4
8
[torch.FloatTensor of size 2]
Variable containing:
6
12
[torch.FloatTensor of size 2]
可以看到,每次调用backward()
方法时,梯度都会累加,所以大多数情况下,在调用backward()
前,我们都需要先将梯度清零。
Function
在计算图中,节点是由Variable构成,边则是由Function构成,保存了一项操作中的运算方式,如加减乘除,relu等。
在对Variable进行操作时,新获得的Variable中,其.grad_fn
属性就是生成该Variable的Function
import torch
from torch.autograd import Variable
import torch.nn.functional as F
a = Variable(torch.ones(2))
b = a * a
c = F.relu(a)
print(a.grad_fn)
print(b.grad_fn)
print(c.grad_fn)
None
<torch.autograd.function.MulBackward object at 0x7f0c72def228>
<torch.autograd.function.ThresholdBackward object at 0x7f0c72def318>
Function类有以下属性:
requires_grad
:指示是否.backward()
方法会被调用
Function类有两个静态方法,在自定义Function时需要重写这两个方法:
static backward(ctx, *grad_outputs)
:定义微分运算,微分顺序为拓扑排序,从根节点到叶子节点。第一个参数为一个上下文对象,之后接收的参数需要与forward()
方法的输出相对应。上下文对象用来存放变量,使变量可以在forward()
中取回static forward(ctx, *args, **kwargs)
:定义一个Function的运算。第一个参数为一个上下文对象,之后可以接收任意参数,输入参数需要与backward()
的输出相对应。上下文对象用来存放变量,使变量可以在backward()
中取回
简言之,上下文对象ctx就是在两个方法中传递变量,两个方法中,任意一个的输入都需要与另一个的输出相对应。
自定义Function
假设我们需要定义一个线性函数
,那么在forward()
中需要定义该操作,在backward()
中需要定义微分操作。
import torch
from torch.autograd import Variable
from torch.autograd import Function
class MyFunction(Function):
@staticmethod
def forward(ctx, w, x, b):
ctx.save_for_backward(w,x)
output = w * x + b
return output
@staticmethod
def backward(ctx, output):
w,x = ctx.saved_variables
grad_w = output * x
grad_x = output * w
grad_b = output * 1
return grad_w, grad_x, grad_b
之后尝试使用该Function,通过.apply()
传递参数
x = Variable(torch.rand(1), requires_grad = True)
w = Variable(torch.rand(1), requires_grad = True)
b = Variable(torch.rand(1), requires_grad = True)
y = MyFunction.apply(w, x, b)
y.backward()
print(w, x, b)
print(w.grad, x.grad, b.grad)
Variable containing:
0.2667
[torch.FloatTensor of size 1]
Variable containing:
0.2042
[torch.FloatTensor of size 1]
Variable containing:
0.2314
[torch.FloatTensor of size 1]
Variable containing:
0.2042
[torch.FloatTensor of size 1]
Variable containing:
0.2667
[torch.FloatTensor of size 1]
Variable containing:
1
[torch.FloatTensor of size 1]