深入理解pytorch autograd

学习下,转自

https://www.cnblogs.com/hellcat/p/8449031.html

https://www.cnblogs.com/hellcat/p/8449801.html

https://www.cnblogs.com/hellcat/p/8453615.html

谢谢原作者的辛苦总结。

(上)

在PyTorch中计算图的特点可总结如下:

  • autograd根据用户对variable的操作构建其计算图。对变量的操作抽象为Function
  • 对于那些不是任何函数(Function)的输出,由用户创建的节点称为叶子节点,叶子节点的grad_fn为None。叶子节点中需要求导的variable,具有AccumulateGrad标识,因其梯度是累加的。
  • variable默认是不需要求导的,即requires_grad属性默认为False,如果某一个节点requires_grad被设置为True,那么所有依赖它的节点requires_grad都为True。
  • variable的volatile属性默认为False,如果某一个variable的volatile属性被设为True,那么所有依赖它的节点volatile属性都为True。volatile属性为True的节点不会求导,volatile的优先级比requires_grad高。
  • 多次反向传播时,梯度是累加的。反向传播的中间缓存会被清空,为进行多次反向传播需指定retain_graph=True来保存这些缓存。
  • 非叶子节点的梯度计算完之后即被清空,可以使用autograd.gradhook技术获取非叶子节点的值。
  • variable的grad与data形状一致,应避免直接修改variable.data,因为对data的直接操作无法利用autograd进行反向传播
  • 反向传播函数backward的参数grad_variables可以看成链式求导的中间结果,如果是标量,可以省略,默认为1
  • PyTorch采用动态图设计,可以很方便地查看中间层的输出,动态的设计计算图结构。

Variable类和计算图

简单的建立一个计算图,便于理解几个相关知识点:

  • requires_grad  是否要求导数,默认False,叶节点指定True后,依赖节点都被置为True

  • .backward()  根Variable的方法会反向求解叶Variable的梯度

  • .backward()方法grad_variable参数  形状与根Variable一致,非标量Variable反向传播方向指定

  • 叶节点  由用户创建的计算图Variable对象,反向传播后会保留梯度grad数值,其他Variable会清空为None

  • grad_fn  指向创建Tensor的Function,如果某一个对象由用户创建,则指向None

  •  .is_leaf  是否是叶节点

  • .grad_fn.next_functions  本节点接收的上级节点的grad_fn

  • .volatile  是否处于推理模式

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

import torch as t

from torch.autograd import Variable as V

 

= V(t.ones(3,4),requires_grad=True)

= V(t.zeros(3,4))

= a.add(b)

= c.sum()

d.backward()

 

# 虽然没有要求cd的梯度,但是cd依赖于a,所以a要求求导则cd梯度属性会被默认置为True

print(a.requires_grad, b.requires_grad, c.requires_grad,d.requires_grad)

# 叶节点(由用户创建)的grad_fn指向None

print(a.is_leaf, b.is_leaf, c.is_leaf,d.is_leaf)

# 中间节点虽然要求求梯度,但是由于不是叶节点,其梯度不会保留,所以仍然是None

print(a.grad,b.grad,c.grad,d.grad)

True False True True
True True False False
Variable containing:
 1  1  1  1
 1  1  1  1
 1  1  1  1
[torch.FloatTensor of size 3x4]
 None None None

1

print('\n',a.grad_fn,'\n',b.grad_fn,'\n',c.grad_fn,'\n',d.grad_fn)

None 
None 
 

 

模拟一个简单的反向传播:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

def f(x):

    """x^2 * e^x"""

    = x**2 * t.exp(x)

    return y

 

def gradf(x):

    """2*x*e^x + x^2*e^x"""

    dx = 2*x*t.exp(x) + x**2*t.exp(x)

    return dx

 

= V(t.randn(3,4), requires_grad=True)

= f(x)

y.backward(t.ones(y.size()))

print(x.grad)

print(gradf(x))

Variable containing:
 -0.3315   3.5068  -0.1079  -0.4308
 -0.1202  -0.4529  -0.1873   0.6514
  0.2343   0.1050   0.1223  15.9192
[torch.FloatTensor of size 3x4]

Variable containing:
 -0.3315   3.5068  -0.1079  -0.4308
 -0.1202  -0.4529  -0.1873   0.6514
  0.2343   0.1050   0.1223  15.9192
[torch.FloatTensor of size 3x4]

 结果一致。

 

.grad_fn.next_functions 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

= V(t.ones(1))

= V(t.rand(1),requires_grad=True)

= V(t.rand(1),requires_grad=True)

 

= w.mul(x)

= y.add(b)

 

print(x.is_leaf,w.is_leaf,b.is_leaf,y.is_leaf,z.is_leaf)

print(x.requires_grad,w.requires_grad,b.requires_grad,y.requires_grad,z.requires_grad)

print(x.grad_fn,w.grad_fn,b.grad_fn,y.grad_fn,z.grad_fn)

 

# grad_fn.next_functions

# grad_fn.next_functions代表了本节点的输入节点信息,grad_fn表示了本节点的输出信息

# 叶子结点grad_fn为None,没有next_functions,但是间接查询到AccumulateGrad object表示该叶子节点

# 接受梯度更新,查询到None表示不接受更新

print(y.grad_fn.next_functions,z.grad_fn.next_functions)

print(z.grad_fn.next_functions[0][0]==y.grad_fn)

print(z.grad_fn.next_functions[0][0],y.grad_fn)

.is_leaf
True True True False False

.requires_grad
False True True True True

.grad_fn
None None None  

.grad_fn.next_functions
((, 0), (None, 0)) 
((, 0), 
(, 0))
z.grad_fn.next_functions[0][0]==y.grad_fn
True 
z.grad_fn.next_functions[0][0],y.grad_fn
 


.volatile

1

2

3

4

5

6

7

8

9

# volatile

# 节省显存提高效用的参数volatile,也会作用于依赖路径全部的Variable上,且优先级高于requires_grad,

# 这样我们在实际设计网络时不必修改其他叶子结点的requires_grad属性,只要将输入叶子volatile=True即可

 

= V(t.ones(1),volatile=True)

= V(t.rand(1),requires_grad=True)

= w.mul(x)

print(x.requires_grad,w.requires_grad,y.requires_grad)

print(x.volatile,w.volatile,y.volatile)

False True False
True False True

附录、Variable类源码简介

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

class Variable(_C._VariableBase):

 

    """

    Attributes:

        data: 任意类型的封装好的张量。

        grad: 保存与data类型和位置相匹配的梯度,此属性难以分配并且不能重新分配。

        requires_grad: 标记变量是否已经由一个需要调用到此变量的子图创建的bool值。只能在叶子变量上进行修改。

        volatile: 标记变量是否能在推理模式下应用(如不保存历史记录)的bool值。只能在叶变量上更改。

        is_leaf: 标记变量是否是图叶子(如由用户创建的变量)的bool值.

        grad_fn: Gradient function graph trace.

 

    Parameters:

        data (any tensor class): 要包装的张量.

        requires_grad (bool): bool型的标记值. **Keyword only.**

        volatile (bool): bool型的标记值. **Keyword only.**

    """

 

    def backward(self, gradient=None, retain_graph=None, create_graph=None, retain_variables=None):

        """计算关于当前图叶子变量的梯度,图使用链式法则导致分化

        如果Variable是一个标量(例如它包含一个单元素数据),你无需对backward()指定任何参数

        如果变量不是标量(包含多个元素数据的矢量)且需要梯度,函数需要额外的梯度;

        需要指定一个和tensor的形状匹配的grad_output参数(y在指定方向投影对x的导数);

        可以是一个类型和位置相匹配且包含与自身相关的不同函数梯度的张量。

        函数在叶子上累积梯度,调用前需要对该叶子进行清零。

 

        Arguments:

            grad_variables (Tensor, Variable or None):

                           变量的梯度,如果是一个张量,除非“create_graph”是True,否则会自动转换成volatile型的变量。

                           可以为标量变量或不需要grad的值指定None值。如果None值可接受,则此参数可选。

            retain_graph (bool, optional): 如果为False,用来计算梯度的图将被释放。

                                           在几乎所有情况下,将此选项设置为True不是必需的,通常可以以更有效的方式解决。

                                           默认值为create_graph的值。

            create_graph (bool, optional): 为True时,会构造一个导数的图,用来计算出更高阶导数结果。

                                           默认为False,除非``gradient``是一个volatile变量。

        """

        torch.autograd.backward(self, gradient, retain_graph, create_graph, retain_variables)

 

 

    def register_hook(self, hook):

        """Registers a backward hook.

 

        每当与variable相关的梯度被计算时调用hook,hook的申明:hook(grad)->Variable or None

        不能对hook的参数进行修改,但可以选择性地返回一个新的梯度以用在`grad`的相应位置。

 

        函数返回一个handle,其``handle.remove()``方法用于将hook从模块中移除。

 

        Example:

            >>> v = Variable(torch.Tensor([0, 0, 0]), requires_grad=True)

            >>> h = v.register_hook(lambda grad: grad * 2)  # double the gradient

            >>> v.backward(torch.Tensor([1, 1, 1]))

            >>> v.grad.data

             2

             2

             2

            [torch.FloatTensor of size 3]

            >>> h.remove()  # removes the hook

        """

        if self.volatile:

            raise RuntimeError("cannot register a hook on a volatile variable")

        if not self.requires_grad:

            raise RuntimeError("cannot register a hook on a variable that "

                               "doesn't require gradient")

        if self._backward_hooks is None:

            self._backward_hooks = OrderedDict()

            if self.grad_fn is not None:

                self.grad_fn._register_hook_dict(self)

        handle = hooks.RemovableHandle(self._backward_hooks)

        self._backward_hooks[handle.id= hook

        return handle

 

    def reinforce(self, reward):

        """Registers a reward obtained as a result of a stochastic process.

        区分随机节点需要为他们提供reward值。如果图表中包含任何的随机操作,都应该在其输出上调用此函数,否则会出现错误。

        Parameters:

            reward(Tensor): 带有每个元素奖赏的张量,必须与Variable数据的设备位置和形状相匹配。

        """

        if not isinstance(self.grad_fn, StochasticFunction):

            raise RuntimeError("reinforce() can be only called on outputs "

                               "of stochastic functions")

        self.grad_fn._reinforce(reward)

 

    def detach(self):

        """返回一个从当前图分离出来的心变量。

        结果不需要梯度,如果输入是volatile,则输出也是volatile。

 

        .. 注意::

          返回变量使用与原始变量相同的数据张量,并且可以看到其中任何一个的就地修改,并且可能会触发正确性检查中的错误。

        """

        result = NoGrad()(self)  # this is needed, because it merges version counters

        result._grad_fn = None

        return result

 

    def detach_(self):

        """从创建它的图中分离出变量并作为该图的一个叶子"""

        self._grad_fn = None

        self.requires_grad = False

 

    def retain_grad(self):

        """Enables .grad attribute for non-leaf Variables."""

        if self.grad_fn is None:  # no-op for leaves

            return

        if not self.requires_grad:

            raise RuntimeError("can't retain_grad on Variable that has requires_grad=False")

        if hasattr(self'retains_grad'):

            return

        weak_self = weakref.ref(self)

 

        def retain_grad_hook(grad):

            var = weak_self()

            if var is None:

                return

            if var._grad is None:

                var._grad = grad.clone()

            else:

                var._grad = var._grad + grad

 

        self.register_hook(retain_grad_hook)

        self.retains_grad = True

 

(中)

查看非叶节点梯度的两种方法

在反向传播过程中非叶子节点的导数计算完之后即被清空。若想查看这些变量的梯度,有两种方法:

  • 使用autograd.grad函数
  • 使用hook

autograd.gradhook方法都是很强大的工具,更详细的用法参考官方api文档,这里举例说明基础的使用。推荐使用hook方法,但是在实际使用中应尽量避免修改grad的值。

求z对y的导数

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

= V(t.ones(3))

= V(t.rand(3),requires_grad=True)

= w.mul(x)

= y.sum()

 

# hook

# hook没有返回值,参数是函数,函数的参数是梯度值

def variable_hook(grad):

    print("hook梯度输出:\r\n",grad)

 

hook_handle = y.register_hook(variable_hook)         # 注册hook

z.backward(retain_graph=True)                        # 内置输出上面的hook

hook_handle.remove()                                 # 释放

 

print("autograd.grad输出:\r\n",t.autograd.grad(z,y)) # t.autograd.grad方法

hook梯度输出:
 Variable containing:
 1
 1
 1
[torch.FloatTensor of size 3]

autograd.grad输出:
 (Variable containing:
 1
 1
 1
[torch.FloatTensor of size 3]
,)

 

多次反向传播试验

实际就是使用retain_graph参数,

1

2

3

4

5

6

7

8

9

10

# 构件图

= V(t.ones(3))

= V(t.rand(3),requires_grad=True)

= w.mul(x)

= y.sum()

 

z.backward(retain_graph=True)

print(w.grad)

z.backward()

print(w.grad)

Variable containing:
 1
 1
 1
[torch.FloatTensor of size 3]

Variable containing:
 2
 2
 2
[torch.FloatTensor of size 3]

 

如果不使用retain_graph参数,

实际上效果是一样的,AccumulateGrad object仍然会积累梯度

1

2

3

4

5

6

7

8

9

10

11

12

# 构件图

= V(t.ones(3))

= V(t.rand(3),requires_grad=True)

= w.mul(x)

= y.sum()

 

z.backward()

print(w.grad)

= w.mul(x)  # <-----

= y.sum()  # <-----

z.backward()

print(w.grad)

Variable containing:
 1
 1
 1
[torch.FloatTensor of size 3]

Variable containing:
 2
 2
 2
[torch.FloatTensor of size 3]

 

分析:

这里的重新建立高级节点意义在这里:实际上高级节点在创建时,会缓存用于输入的低级节点的信息(值,用于梯度计算),但是这些buffer在backward之后会被清空(推测是节省内存),而这个buffer实际也体现了上面说的动态图的"动态"过程,之后的反向传播需要的数据被清空,则会报错,这样我们上面过程就分别从:保留数据不被删除&重建数据两个角度实现了多次backward过程。

实际上第二次的z.backward()已经不是第一次的z所在的图了,体现了动态图的技术,静态图初始化之后会留在内存中等待feed数据,但是动态图不会,动态图更类似我们自己实现的机器学习框架实践,相较于静态逻辑简单一点,只是PyTorch的静态图和我们的比会在反向传播后清空存下的数据:下次要么完全重建,要么反向传播之后指定不舍弃图z.backward(retain_graph=True)。

总之图上的节点是依赖buffer记录来完成反向传播,TensorFlow中会一直存留,PyTorch中就会backward后直接舍弃(默认时)。

 

(下)

一、封装新的PyTorch函数

继承Function类

forward:输入Variable->中间计算Tensor->输出Variable

backward:均使用Variable

线性映射

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

from torch.autograd import Function

 

class MultiplyAdd(Function):                       # <----- 类需要继承Function类

                                                             

    @staticmethod                                  # <-----forward和backward都是静态方法

    def forward(ctx, w, x, b):                     # <-----ctx作为内部参数在前向反向传播中协调

        print('type in forward',type(x))

        ctx.save_for_backward(w,x)                 # <-----ctx保存参数

        output = * + b

        return output                              # <-----forward输入参数和backward输出参数必须一一对应

         

    @staticmethod                                  # <-----forward和backward都是静态方法

    def backward(ctx, grad_output):                # <-----ctx作为内部参数在前向反向传播中协调

        w,x = ctx.saved_variables                  # <-----ctx读取参数

        print('type in backward',type(x))

        grad_w = grad_output * x

        grad_x = grad_output * w

        grad_b = grad_output * 1

        return grad_w, grad_x, grad_b              # <-----backward输入参数和forward输出参数必须一一对应

 

调用方法一

类名.apply(参数)

输出变量.backward()

1

2

3

4

5

6

7

8

9

10

11

12

13

import torch as t

from torch.autograd import Variable as V

 

= V(t.ones(1))

= V(t.rand(1), requires_grad = True)

= V(t.rand(1), requires_grad = True)

print('开始前向传播')

z=MultiplyAdd.apply(w, x, b)                       # <-----forward

print('开始反向传播')

z.backward() # 等效                                 # <-----backward

 

# x不需要求导,中间过程还是会计算它的导数,但随后被清空

print(x.grad, w.grad, b.grad)

开始前向传播
type in forward 
开始反向传播
type in backward 
(None, 
 Variable containing:
   1
  [torch.FloatTensor of size 1], 
 Variable containing:
   1
  [torch.FloatTensor of size 1])

 

调用方法二

类名.apply(参数)

输出变量.grad_fn.apply()

1

2

3

4

5

6

7

8

9

10

= V(t.ones(1))

= V(t.rand(1), requires_grad = True)

= V(t.rand(1), requires_grad = True)

print('开始前向传播')

z=MultiplyAdd.apply(w,x,b)                         # <-----forward

print('开始反向传播')

 

# 调用MultiplyAdd.backward

# 会自动输出grad_w, grad_x, grad_b

z.grad_fn.apply(V(t.ones(1)))                      # <-----backward,在计算中间输出,buffer并未清空,所以x的梯度不是None

开始前向传播
type in forward 
开始反向传播
type in backward 
(Variable containing:
  1
 [torch.FloatTensor of size 1], Variable containing:
  0.7655
 [torch.FloatTensor of size 1], Variable containing:
  1
 [torch.FloatTensor of size 1])

 

之所以forward函数的输入是tensor,而backward函数的输入是variable,是为了实现高阶求导。backward函数的输入输出虽然是variable,但在实际使用时autograd.Function会将输入variable提取为tensor,并将计算结果的tensor封装成variable返回。在backward函数中,之所以也要对variable进行操作,是为了能够计算梯度的梯度(backward of backward)。下面举例说明,有关torch.autograd.grad的更详细使用请参照文档。

二、高阶导数

grad_x =t.autograd.grad(y, x, create_graph=True)

grad_grad_x = t.autograd.grad(grad_x[0],x)

1

2

3

4

5

6

7

8

= V(t.Tensor([5]), requires_grad=True)

= ** 2

 

grad_x = t.autograd.grad(y, x, create_graph=True)

print(grad_x) # dy/dx = 2 * x

 

grad_grad_x = t.autograd.grad(grad_x[0],x)

print(grad_grad_x) # 二阶导数 d(2x)/dx = 2

(Variable containing:
  10
 [torch.FloatTensor of size 1],)
(Variable containing:
  2
 [torch.FloatTensor of size 1],)

三、梯度检查

t.autograd.gradcheck(Sigmoid.apply, (test_input,), eps=1e-3)

此外在实现了自己的Function之后,还可以使用gradcheck函数来检测实现是否正确。gradcheck通过数值逼近来计算梯度,可能具有一定的误差,通过控制eps的大小可以控制容忍的误差。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

class Sigmoid(Function):

                                                              

    @staticmethod

    def forward(ctx, x):

        output = 1 / (1 + t.exp(-x))

        ctx.save_for_backward(output)

        return output

         

    @staticmethod

    def backward(ctx, grad_output):

        output,  = ctx.saved_variables

        grad_x = output * (1 - output) * grad_output

        return grad_x                           

 

# 采用数值逼近方式检验计算梯度的公式对不对

test_input = V(t.randn(3,4), requires_grad=True)

t.autograd.gradcheck(Sigmoid.apply, (test_input,), eps=1e-3)

True

 

测试效率,

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

def f_sigmoid(x):

    = Sigmoid.apply(x)

    y.backward(t.ones(x.size()))

     

def f_naive(x):

    =  1/(1 + t.exp(-x))

    y.backward(t.ones(x.size()))

     

def f_th(x):

    = t.sigmoid(x)

    y.backward(t.ones(x.size()))

     

x=V(t.randn(100100), requires_grad=True)

%timeit -100 f_sigmoid(x)

%timeit -100 f_naive(x)

%timeit -100 f_th(x)

 实际测试结果,

245 µs ± 70.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
211 µs ± 23.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
219 µs ± 36.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

书中说的结果,

100 loops, best of 3: 320 µs per loop
100 loops, best of 3: 588 µs per loop
100 loops, best of 3: 271 µs per loop

很奇怪,我的结果竟然是:简单堆砌<官方封装<自己封装……不过还是引用一下书中的结论吧:

显然f_sigmoid要比单纯利用autograd加减和乘方操作实现的函数快不少,因为f_sigmoid的backward优化了反向传播的过程。另外可以看出系统实现的buildin接口(t.sigmoid)更快。

你可能感兴趣的:(深入理解pytorch autograd)