Bonky
Neither beliver nor reject anything, because any other person has rejected of believed it. Heaven has given you a mind for judging truth and error, Use it.
By Thomas Jefferson

Pytorch 的自动求导机制

构建深度学习模型的基本流程就是:搭建计算图,求得损失函数,然后计算损失函数对模型参数的导数,再利用梯度下降法等方法来更新参数。

搭建计算图的过程,称为“正向传播”,这个是需要我们自己动手的,因为我们需要设计我们模型的结构。由损失函数求导的过程,称为“反向传播”,求导是件辛苦事儿,所以自动求导基本上是各种深度学习框架的基本功能和最重要的功能之一,PyTorch也不例外。

2018-11-25-123659

简介

所有的tensor都有.requires_grad属性,都可以设置成自动求导。具体方法就是在定义tensor的时候,让这个属性为True (默认为 False):

只要这样设置了之后,后面由x经过运算得到的其他tensor,都会有requires_grad=True属性,即requires_grad == True 具有传递性。

如果想改变这个属性,可以调用tensor.requires_grad_()方法。但是我们在修改 y的时候碰到了以下的问题:

为了说明这个问题,我们不得不提一下 Pytorch 的计算图。

Pytorch的计算图

转自:https://zhuanlan.zhihu.com/p/33378444

构建一个计算图

pytorch是动态图机制,所以在训练模型时候,每迭代一次都会构建一个新的计算图。而计算图其实就是代表程序中变量之间的关系。举个列子: [公式](b%2Bc)) 在这个运算过程就会建立一个如下的计算图:

img

在这个计算图中,节点就是参与运算的变量,在pytorch中是用Variable()变量来包装的,而图中的边就是变量之间的运算关系,比如:torch.mul()torch.mm()torch.div()` 等等。

注意图中的 leaf_node,叶子结点就是由用户自己创建的Variable变量,在这个图中仅有abcleaf_node。为什么要关注leaf_node?因为在网络backward时候,需要用链式求导法则求出网络最后输出的梯度,然后再对网络进行优化,如下就是网络的求导过程。

img

图的细节

pytorch构建的计算图是动态图,为了节约内存,所以每次一轮迭代完之后计算图就被在内存释放,所以当你想要多次backward时候就会报如下错:

上面这个程序是能够正常运行的,但是下面就会报错

之所以会报这个错,因为Pytorch 是动态图,每一次训练,都会销毁图并重新创建,计算图在内存中已经被释放。但是,如果你需要多次backward只需要在第一次反向传播时候添加一个标识,如下:

这样在第一次backward之后,计算图并不会被立即释放。

读到这里,可能你对计算图中的backward还是一知半解。例如上面提过backward只能是标量。那么在实际运用中,如果我们只需要求图中某一节点的梯度,而不是整个图的,又该如何做呢?下面举个例子,列子下面会给出解释。

上面有个很重要的点:要想使x支持求导,必须让x为浮点类型。所以对 x 定义的时候,我们利用了FloatTensor 这一个来定义,不如我们在定义的时候要这么写:torch.Tensor([[1., 2.]])

结果如下:

可能看到上面例子有点懵,用数学表达式形式解释一下,上面程序等价于下面的数学表达式:

[公式]

[公式]

[公式]

[公式]

[公式]

这样我们就很容易利用backward得到一个雅克比行列式:

image-20190902155022220

这就是对 pytorch的计算图和backward 一个介绍,所以我们可以知道上面之所以只有叶子结点可以requires_gradFalse是因为只有叶子结点不需要从后向前传播导数,即不需要计算导数,对计算图的后向传播没有影响。

最后还有一点很重要:==求导,只能是【标量】对标量,或者【标量】对向量/矩阵求导!==,也就是说 loss.backwardloss 必须要是常数或者可以利用backward第一个参数gradient进行限定, 否则会提示如下错误

一般来说,我是对标量求导,比如在神经网络里面,我们的loss会是一个标量,那么我们让loss对神经网络的参数w求导,直接通过loss.backward()即可。

但是,有时候我们可能会有多个输出值,比如loss=[loss1,loss2,loss3],那么我们可以让loss的各个分量分别对x求导,这个时候就采用:
loss.backward(torch.tensor([[1.0,1.0,1.0,1.0]]))

如果你想让不同的分量有不同的权重,那么就赋予gradients不一样的值即可,比如:
loss.backward(torch.tensor([[0.1,1.0,10.0,0.001]]))

这样,我们使用起来就更加灵活了,虽然也许多数时候,我们都是直接使用.backward()就完事儿了。

Tensor的自动求导的技巧

前面介绍完这么多,接下来我们继续研讨下怎么来用 Tensor 的自动求导

保存在backward时自动抛弃的中间变量梯度

首先,我们需要了解的是 backward() 函数,PyTorchbackward时会自动抛弃中间变量梯度,所以我们只有对叶子结点调用 x.grad 才会有值返回,计算图中间结点的的梯度全部为空 None

但事实上,中间梯度也非常重要,这是我们可以利用register_hook 的方法,他是利用hook机制,使得PyTorch图在进行backward的时候触发保存下中间变量的grad。在官方文档里,register_hook用法是这样的:

上面这个指令的意思就是,当每次计算梯度的时候这个 hook 会被执行一次,输入参数是当时的梯度,执行 lambda 函数。利用register_hook 我们可以保存中间梯度,方法如下:

下面三个也是常用的 hook

detach 函数的作用

主要转载并翻译自:http://www.bnikolic.co.uk/blog/pytorch-detach.html

生成的计算图如下

attached

我们可以得到正确的梯度

如果我们想要把 z 从子图中提取出来,不需要计算 z 的梯度和更新 z的参数,那么我们利用 detach 函数可以解决这个问题

生成的计算图会少了 z

detached

然后我们可以得到梯度为 y 的导数

Share

You may also like...

发表评论

电子邮件地址不会被公开。 必填项已用*标注