神经网络的训练分为两步:前向传播和反向传播进行梯度更新。对于任何深度学习框架来说,自动求导是它们的核心组件。Pytorch 中的 autograd
负责进行自动求导。
进行网络训练的时候,Pytorch 会自动:
虽然我们不需要显式地调用 autograd
,但更深入地了解它能够帮助我们规避一些报错,遇到报错时也有解决的思路。
我们会好奇,autograd
是如何计算梯度的。实际上,它会把所有的数据(张量)以及它们之间进行的运算,保存在一张有向无环图(directed acyclic graph, DAG)里。这张图就叫做计算图。
在计算图中,输入的张量叫做叶子节点;输出的张量叫做根节点。
前向传播过程中,autograd
会同时做两件事情:
grad_fn
例子:
x = torch.ones(2, 2, requires_grad=True)
y = torch.sigmoid(x)
print(y)
>>> tensor([[0.7311, 0.7311],
[0.7311, 0.7311]], grad_fn=<SigmoidBackward0>)
当计算图的根节点(输出张量)调用 .backward()
方法时,反向传播开始,autograd
会:
.grad_fn
计算梯度;.grad
属性中;下面是一个简单的计算图,蓝色代表叶子节点;绿色代表根节点;灰色代表要进行的运算。箭头指示前向传播的方向。
下面用代码更好地理解计算图。
x = torch.ones(2, 2, requires_grad=True)
print("Checking gradient is set to {}. Its gradient is still {} ".format(x.requires_grad, x.grad))
Output: Checking gradient is set to True. Its gradient is still None
我们先定义了一个 tensor, 并指明需要为它计算梯度——requires_grad=True
。可以看到,此时它的梯度x.grad
还是 None,因为还没有进行反向传播。
y = x + 1
print(y)
>>> tensor([[2., 2.],
[2., 2.]], grad_fn=<AddBackward0>)
此时 y 已经有了 .grad
的信息。继续查看 y 的相关信息:
print(y.grad_fn)
print(y.grad)
>>> <AddBackward0 object at 0x7fc974d3f2b0>
>>> None
这个时候还会看到一个 warning:
UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its .grad attribute won’t be populated during autograd.backward(). If you indeed want the .grad field to be populated for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor instead. See github.com/pytorch/pytorch/pull/30531 for more informations. (Triggered internally at aten/src/ATen/core/TensorBody.h:480.)
它的意思是说,y 这个张量不是叶子节点,它的梯度在反向传播时不会被保留。所以它警告我们,在获取 y.grad
的时候要小心。另外,可以查看 .is_leaf
属性,了解某个张量是否为叶子节点;如果真的要计算非叶子节点的梯度,可以设置 y.retain_grad()
y.is_leaf
>>> False
All Tensors that have
requires_grad
which isFalse
will be leaf Tensors by convention.
For Tensors that haverequires_grad
which isTrue
, they will be leaf Tensors if they were created by the user. This means that they are not the result of an operation and sograd_fn
is None.——Understanding computational graph in Pytorch
对于计算得到的(非用户定义的)张量,无法直接更改它们的 requires_grad
属性:
y.requires_grad = False
产生错误:
RuntimeError: you can only change
requires_grad
flags of leaf variables. If you want to use a computed variable in a subgraph that doesn’t require differentiation use var_no_grad = var.detach().
对于此类张量,可以通过 .detach()
方法,返回一个新的、从计算图中被分离出来的张量。
也可以使用 .detach_()
方法,实现 in-place 操作,不会返回一个新的张量——将张量从创建它的图中分离出来,使其成为一个叶子节点。
requires_grad = False
torch.autograd tracks operations on all tensors which have their requires_grad flag set to True. For tensors that don’t require gradients, setting this attribute to False excludes it from the gradient computation DAG.
autograd
会跟踪所有 requires_grad = True
的张量上的运算。对于那些不需要梯度的张量,设置 requires_grad = False
能够节省计算资源。
另外,对模型进行微调的时候,我们不希望更新已经预训练好的网络参数,此时需要为它们设置 requires_grad = False
:
for param in model.parameters():
param.requires_grad = False
with torch.no_grad()
vs model.eval()
with torch.no_grad()
是一个上下文管理器 (context manager),它会创建一个环境,在此之内的张量运算均不会计算梯度。
model.eval()
会改变模型中某些层的前向传播行为,如 BatchNorm layer, Dropout layer.
它们实际上在做完全不一样的事情。在做模型推理时,一定要设置 model.eval()
;最好也用 with torch.no_grad
,节省计算资源。
model.eval()
with torch.no_grad():
output = model(input)
...
optimizer.zero_grad()
output = model(input)
loss = criterion(output, label)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)
optimizer.step()
上面这段代码模式,我们经常见到。我们来看看每一步都在干什么。
前面提到,反向传播时, autograd
将梯度累加到相应张量的 .grad
属性中。因此第一步要通过 optimizer.zero_grad()
,将该优化器中所有的参数梯度归零。
计算得到损失函数 loss。我们知道,当计算图的根节点(输出张量)调用 .backward()
方法时,反向传播开始。这里 loss 就是根节点。loss.backward()
开启反向传播,计算梯度。
下面一步——梯度裁剪——是可选的。clip_grad_norm_(model.parameters(), 0.5)
将参数的梯度绝对值控制在 0.5 之内。梯度裁剪被认为可以增强模型的鲁棒性。
optimizer.step()
进行参数更新。如果使用最简单的梯度下降,直接计算 w = w − l r ∗ w . g r a d w = w - lr*w.grad w=w−lr∗w.grad;如果是更复杂的优化器,比如最常用的 Adam,还要考虑历史梯度。
参考:stanford cs231n 2018 lecture08
Pytorch 采用动态的方式构建计算图——计算图的构建与计算同时进行;每次前向传播都会定义一个新的计算图。这样带来了很多的方便:允许我们在不同的迭代步采用不同的网络结构;也可以根据输入的不同,生成不同的计算图。
举个例子:RNN网络中,计算图是取决于输入序列的长度的,此时动态计算图就很有用。另一个例子是模块化网络——根据输入的不同,不同的网络模块会被调用,计算图自然也不同。
而大多数深度学习框架,如 TensorFlow, Theano, Caffe, and CNTK,都采用静态的方式构建计算图——先构建计算图,然后进行计算;一次性为网络构建计算图,使用多次。静态计算图的优势也很明显:节省了多次构建的时间;并且能够在运算之前对计算图进行优化(比如将卷积层和 ReLU 激活函数合并)。
其实静态与动态之间的分界线正在变得模糊:
eager execution
,允许动态地构建计算图;预告:下一篇就来详细讲讲 Pytorch 中的即时编译(JIT)以及 TorchScript