zoukankan      html  css  js  c++  java
  • PyTorch 的 backward 为什么有一个 grad_variables 参数

    假设 x 经过一番计算得到 y,那么 y.backward(w) 求的不是 y 对 x 的导数,而是 l = torch.sum(y*w) 对 x 的导数。w 可以视为 y 的各分量的权重,也可以视为遥远的损失函数 l 对 y 的偏导数(这正是函数说明文档的含义)。特别地,若 y 为标量,w 取默认值 1.0,才是按照我们通常理解的那样,求 y 对 x 的导数。

    自从 autograd 模块被合并到 PyTorch master 分支以后,PyTorch 变得越来越得心应手,成为很多人的首选。在 PyTorch 教程 Autograd: automatic differentiation 里提到,torch.autograd.backward() 函数需要一个 grad_output 参数(此处疑为笔误,根据文档描述,torch.autograd.backward() 的参数应该是 grad_variables,函数 torch.autograd.grad() 的参数才是 grad_output)。如果是对一个标量进行反向传播,那么这个参数可以省略(缺省值为 1.0)。

    所以,这个参数到底是干嘛的呢?

     

    这个函数的原型是:

    torch.autograd.backward(variablesgrad_variables=Noneretain_graph=None,create_graph=Noneretain_variables=None)

    文档里的介绍是:

    The graph is differentiated using the chain rule. If any of variables are non-scalar (i.e. their data has more than one element) and require gradient, the function additionally requires specifying grad_variables. It should be a sequence of matching length, that contains gradient of the differentiated function w.r.t. corresponding variables (None is an acceptable value for all variables that don’t need gradient tensors).

    variables 和 grad_variables 都可以是 sequence,不过平常也不太有一串变量对另一串变量求导这种需求:如果有这种需求的话,自己写个循环就行了;像 PyTorch 的这个接口,以及 TensorFlow 里提供的求导接口,虽然可以传一堆 x 和一堆 y 进去,但是返回的都是一堆 y 的和对各个 x 的导数,这样一来这个接口的用法就显得很奇怪。反倒不如定义这一堆 y 的和为 z,然后求 z 对各个 x 的导数更加自然。

    事实上,TF 和 PyTorch 这么设计不是没有原因的。原因就是: Tensor 没法对 Tensor 求导!举一个简单的例子,如果要求一个 Tensor 对另一个 Tensor 的导数,先考虑矩阵对矩阵这种情形:假设 m*n 的矩阵 x 经过运算得到了 p*q 的矩阵 y,y 又经过运算得到了 s*t 的矩阵 z,那么:dz/dy 是啥?假设可以求导,那么得到的应该是四阶张量吧,形状是 s*t*p*q?好的,dy/dx 再算一下,得到一个四阶张量 p*q*m*n。然后怎么反向传播?dz/dx = dz/dy * dy/dx = 两个四阶张量相乘???这要怎么乘???当然,也不是说绝对不行,仔细思考一下可以把这个问题解决掉,在长度为 p 和 q 的那两个维度上求个和就行,但是想一想无穷无尽的运算组合方式,怎么写一个足够 robust 的反向传播?就算你能解决两个四维 Tensor 怎么乘的问题,Tensor 对标量 Scalar 的导数又是啥?四维和三维的 Tensor 又怎么乘?导数的导数又怎么求,搞一个六阶还是八阶张量做中间结果?这一连串的问题,感觉要疯掉……

     

    一个简单的解决方案就是:

    1、不允许 Tensor 对 Tensor 求导,只允许标量 Scalar 对张量 Tensor 求导,求导结果是和自变量同型的 Tensor。

    2、在求 dl/dx 的时候(l 是标量,x 是张量),假设有一个中间结果为张量 y,即 x->y->l,那么先求 dl/dy(结果是良定义的、和 y 同型的 Tensor),然后根据 x 和 dl/dy 想办法直接算出 dl/dx,跳过 dy/dx 是啥这种玄学问题!(这种问题在推 MLP 的反向传播时也能遇到,解决办法就是跳过它!)

     

    然后再回到 PyTorch 的设计上来, backward() 为啥还需要一个额外的参数?就是为了避免 Tensor 对 Tensor 求导结果是啥这种玄学问题!torch.autograd.backward(y, w), 或者说 y.backward(w) 的含义是:先计算 l = torch.sum(y * w),然后求 l 对(能够影响到 y 的)所有变量 x 的导数。这里,y 和 w 是同型 Tensor。也就是说,可以理解成先按照 w 对 y 的各个分量加权,加权求和之后得到真正的 loss,再计算这个 loss 对于所有相关变量的导数。

    这么设计有什么好处呢?如前所述,这样一来,所有求导操作都是求 Scalar 关于 Tensor 的导数,统一了起来,不存在 Tensor 对 Tensor 求导了。再回顾一下 PyTorch 自己的文档,它说 torch.autograd.backward 的第二个参数 grad_variables 应该是第一个参数 variables 的对应的导数。

    嗯??这是什么情况??其实我上面的解释是一致的。假设 y 和 w 是同型 Tensor,那么 l = torch.sum(y*w) 对 y 的导数 dl/dy 就是 w。所以把这里的 w 理解成 y 的各项的权重也好,或者理解成某个高高在上的虚拟 loss 对 y 的导数也好,其实是一样的。事实上,l = torch.sum(y*w) 这个形式不正好是导数的定义么?数学分析一上来就学,微分是函数增量的线性主部,而在 l = torch.sum(y*w) 这个形式里,只有线性的项,因此 w 就是 dl/dy。

    那为什么标量就不需要这个参数呢?假设 y 是标量,然后取 w=1.0,那么 l=torch.sum(y*w) 其实就是 y 本身。所以这时候,dl/dx = dy/dx,可以直接把 loss 和 y 混同,这也就是通常直接把损失函数 loss 当成 y 传进去的原因。

    假设 x 经过一番计算得到 y,那么 y.backward(w) 求的不是 y 对 x 的导数,而是 l = torch.sum(y*w) 对 x 的导数。w 可以视为 y 的各分量的权重,也可以视为遥远的损失函数 l 对 y 的偏导数。也就是说,不一定需要从计算图最后的节点 y 往前反向传播,从中间某个节点 n 开始传也可以,只要你能把损失函数 l 关于这个节点的导数 dl/dn 记录下来,n.backward(dl/dn) 照样能往前回传,正确地计算出损失函数 l 对于节点 n 之前的节点的导数。特别地,若 y 为标量,w 取默认值 1.0,才是按照我们通常理解的那样,求 y 对 x 的导数。

    转载自 https://zhuanlan.zhihu.com/p/29923090

  • 相关阅读:
    2017"百度之星"程序设计大赛
    2018省赛赛第一次训练题解和ac代码
    2018天梯赛第一次训练题解和ac代码
    rsa Round #71 (Div. 2 only)
    AtCoder Grand Contest 021
    Hello 2018
    Educational Codeforces Round 36 (Rated for Div. 2)
    Codeforces Round #462 (Div. 2)
    Codeforces Round #467 (Div. 2)
    [Offer收割]编程练习赛48
  • 原文地址:https://www.cnblogs.com/welhzh/p/13596643.html
Copyright © 2011-2022 走看看