【notes】pytorch学习笔记2-autograd部分

autograd

torch.autograd是为方便用户使用,专门开发的一套自动求导引擎,它能够根据输入和前向传播过程自动构建计算图,并执行反向传播。

计算图是现代深度学习框架的核心,它为自动求导算法——反向传播提供了理论支持。

Variable

pytorch在autograd模块中实现计算图相关功能,autograd中的核心数据结构是Variable。Variable封装了tensor,并记录对tensor的操作记录用来构建计算图。Variale的数据结构如图:

            
            graph LR;
A[autograd.Variable] --> B(data);
A[autograd.Variable] --> C(grad);
A[autograd.Variable] --> D(grad_fn);
          

Variable的构造函数需要传入tensor,同时有两个可选参数。

  • require_grad(bool):是否需要对该variable进行求导。
  • volatile(bool):意为“挥发”,设置为True,构建在该variable之上的图都不会求导,转为推理阶段设计。

Variable支持大部分tensor支持的函数,但其不支持部分inplace函数,因为这些函数会修改tensor自身,而在反向传播中,variable需要缓存原来的tensor来计算梯度。如果想要计算各个Variable的梯度,只需调用根节点variable的backward方法,autograd会自动沿着计算图反向传播,计算每一个叶子节点的梯度。

  • grad_variables:形状与variable一致,对于y.backward(),grad_variables相当于链式法则$\frac{\partial{z}}{\partial{x}}=\frac{\partial{z}}{\partial{y}}\times\frac{\partial{y}}{\partial{x}}$中的$\frac{\partial{z}}{\partial{y}}$。grad_variables也可以是tensor或序列。
  • retain_graph:反向传播需要缓存一些中间结果,反向传播之后,这些缓存就被清空,可通过指定这个参数不清空缓存,用来多次反向传播。
  • create_graph:对反向传播过程再次构建计算图,可通过backward of backward实现求高阶导数。
1
2
from torch.autograd import Variable as V
import torch as t
1
2
3
# 从tensor中创建variable,指定需要求导

a = V(t.ones(3, 4), requires_grad = True);a
tensor([[1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.]], requires_grad=True)
1
b = V(t.zeros(3, 4));b
tensor([[0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.]])
1
2
3
4
# 函数的使用和tensor一致
# 也可写成c = a + b
c = a.add(b)
c
tensor([[1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.]], grad_fn=<AddBackward0>)
1
2
d = c.sum()
d.backward() # 反向传播
1
2
3
4
# 注意二者的区别
# 前者在取data后变为tensor,从tensor计算sum得到float
# 后者计算sum后仍然是Variable
c.data.sum(), c.sum()
(tensor(12.), tensor(12., grad_fn=<SumBackward0>))
1
a.grad
tensor([[1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.]])
1
2
3
# 此处虽然没有指定c需要求导,但c依赖于a,而a需要求导
# 因此c的requires_grad属性会自动设为True
a.requires_grad, b.requires_grad, c.requires_grad
(True, False, True)
1
2
3
# c.grad是None,c不是叶子节点,他的梯度是用来计算a的梯度
# 虽然c.requires_grad = True,但其梯度计算完之后即被释放
c.grad is None
True

接下来看看autograd计算导数和我们手动推导的导数的区别。
$$y=x^2e^x$$

它的导函数是:
$$\frac{\partial{y}}{\partial{x}}=2xe^x+x^2e^x$$

1
2
3
4
5
6
def f(x):
y = x**2 * t.exp(x)
return y
def gradf(x):
dx = 2*x*t.exp(x) + x**2*t.exp(x)
return dx
1
2
3
x = V(t.randn(3, 4), requires_grad = True)
y = f(x)
y
tensor([[1.3949e-01, 2.7201e-01, 4.9848e-01, 2.2968e+00],
        [3.2033e-01, 3.3618e-01, 2.3554e-02, 1.0507e+01],
        [3.9416e+01, 3.5322e+00, 9.6847e-02, 1.2743e+01]],
       grad_fn=<MulBackward0>)
1
2
y.backward(t.ones(y.size())) # grad_variables形状与y一致
x.grad
tensor([[ 1.0154, -0.4398, -0.1755,  7.1583],
        [-0.4095,  1.7961,  0.3532, 24.3531],
        [76.1412, 10.0143, -0.4190, 28.6505]])
1
2
# autograd的计算结果与利用公式手动计算的结果一致
gradf(x)
tensor([[ 1.0154, -0.4398, -0.1755,  7.1583],
        [-0.4095,  1.7961,  0.3532, 24.3531],
        [76.1412, 10.0143, -0.4190, 28.6505]], grad_fn=<AddBackward0>)

计算图

pytorch中autograd的底层采用了计算图,计算图是一种特殊的有向无环图(DAG),用于记录算子与变量之间的关系。一般用矩形表示算子,椭圆形表示变量。如表达式$\boldsymbol{z}=\boldsymbol{wx}+\boldsymbol{b}$可分解为$\boldsymbol{y}=\boldsymbol{wx}$和$\boldsymbol{z}=\boldsymbol{y}+\boldsymbol{b}$,其计算图如图所示,图中的MUL和ADD都是算子,$\boldsymbol{w}$、$\boldsymbol{x}$、$\boldsymbol{b}$为变量。

            
            graph LR;
A((W)) --> C[MUL];
B((X)) --> C[MUL];
C[MUL] --> D((y));
E((b)) --> F[Add];
D((y)) --> F[Add];
F[Add] --> G((z));
          

如上有向无环图,$\boldsymbol{X}$和$\boldsymbol{b}$是叶子节点,这些节点通常有用户自己创建,不依赖于其他变量。$\boldsymbol{z}$称为根节点,是计算图的最终目标。利用链式法则很容易求各个叶子节点的梯度。

$$ \frac{\partial{z}}{\partial{b}}=1,\frac{\partial{z}}{\partial{y}}=1\\frac{\partial{y}}{\partial{w}}=x,\frac{\partial{y}}{\partial{x}}=w\\frac{\partial{z}}{\partial{x}}=\frac{\partial{z}}{\partial{y}}\frac{\partial{y}}{\partial{x}}=1w\\frac{\partial{z}}{\partial{w}}=\frac{\partial{z}}{\partial{y}}\frac{\partial{y}}{\partial{w}}=1x $$

有了计算图上述链式求导可自动利用计算图的反向传播自动完成,其过程如图所示:

            
            graph LR;
A((dz)) --> B[addBackward];
B[addBackward] --> C((dy));
B[addBackward] --> D((db));
C((dy)) --> E[mulBackward];
E[mulBackward] --> F((dX));
E[mulBackward] --> G((dW));
          

图中记录了操作function,每个变量在图中的位置可通过其grad_fn属性在图中的位置推测得到。在反向传播的过程中,autograd沿着这个图从当前变量(根节点z)溯源,可以利用链式求导法则计算所有叶子节点的梯度。

每一个前向传播操作的函数都有与之对应的反向传播函数用来计算输入的各个variable的梯度,这些函数的函数名通常以Backward结尾。

1
2
3
4
5
x = V(t.ones(1))
b = V(t.rand(1), requires_grad = True)
w = V(t.rand(1), requires_grad = True)
y = w * x # 等价于y = w.mul(x)
z = y + b # 等价于z = y.add(b)
1
x.requires_grad, b.requires_grad, w.requires_grad
(False, True, True)
1
2
3
# 虽然未指定y.requires_grad为True,但由于y依赖于需要求导的w
# 故而y.requires_grad为True
y.requires_grad
True
1
x.is_leaf, w.is_leaf, b.is_leaf
(True, True, True)
1
y.is_leaf, z.is_leaf
(False, False)
1
2
3
# grad_fn可以查看这个variable的反向传播函数
# z是add函数的输出,所以它的反向传播函数是AddBackward
z.grad_fn
<AddBackward0 at 0x7f073e390e80>
1
2
3
4
#next_function保存grad_fn的输入,grad_fn的输入是一个tuple
# 第一个是y,它是乘法(mul)的输出,所以对应的反向传播函数y.grad_fn是MulBackward
# 第二个是b,它是叶子节点,由用户创建,grad_fn为None,但是有
z.grad_fn.next_functions
((<MulBackward0 at 0x7f073e390828>, 0),
 (<AccumulateGrad at 0x7f073e390f28>, 0))
1
2
# variable的grad_fn对应着图中的function
z.grad_fn.next_functions[0][0] == y.grad_fn
True
1
2
3
# 第一个是w,叶子节点,需要求导,梯度是累加的
# 第二个是x,叶子节点,不需要求导,所以为None
y.grad_fn.next_functions
((<AccumulateGrad at 0x7f073e390470>, 0), (None, 0))
1
2
# 叶子节点的grad_fn是None
w.grad_fn, x.grad_fn
(None, None)

计算$\boldsymbol{w}$的梯度时需要用到$\boldsymbol{x}$的数值($\frac{\partial{y}}{\partial{w}}=x$),这些数值在前向过程中会保存成buffer,在计算完梯度之后会自动清空。为了能够多次反向传播需要指定retain_graph来保留这些buffer。

1
y.grad_fn.saved_variables
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-30-284a59926bb7> in <module>
----> 1 y.grad_fn.saved_variables


AttributeError: 'MulBackward0' object has no attribute 'saved_variables'

原因确实是版本问题,PyTorch0.3 中把许多python的操作转移到了C++中,saved_variables 现在是一个c++的对象,无法通过python访问。https://github.com/chenyuntc/pytorch-book/issues/7

可以查看这里进行学习https://github.com/chenyuntc/pytorch-book/blob/0.3/chapter3-Tensor和autograd/Autograd.ipynb,省掉上面的操作

1
2
3
# 使用retain_graph保存buffer
z.backward(retain_graph = True)
w.grad
tensor([1.])
1
2
3
# 多次反向传播,梯度累加,这也就是w中AccumulateGrad标识的含义
z.backward()
w.grad
tensor([2.])

pytorch使用的是动态图,它的计算图在每次前向传播时都是从头开始构建的,所以它能够使用python的控制语句(如for、if等),根据需求创建计算图。这一点在自然语言处理领域中很有用,它意为你不需要事先构建所有可能用到的图的路径,图在运行时才构建。

1
2
3
4
5
6
7
def abs(x):
if x.data[0] > 0: return x
else: return -x
x = V(t.ones(1), requires_grad = True)
y = abs(x)
y.backward()
x.grad
tensor([1.])
1
2
3
4
x = V(-1 * t.ones(1), requires_grad = True)
y = abs(x)
y.backward()
print(x.grad)
tensor([-1.])
1
2
3
4
5
6
7
8
9
def f(x):
result = 1
for ii in x:
if ii.data > 0: result = ii * result
return result
x = V(t.arange(-2, 4).float(), requires_grad = True)
y = f(x) # y = x[3] * x[4] * x[5]
y.backward()
x.grad
tensor([0., 0., 0., 6., 3., 2.])

变量的requires_grad属性默认为False,如果某一个节点requires_grad被设置为True,那么所有依赖它的节点requires_grad都是True。

with torch.no_grad()内的variable均为不会求导,其优先级高于requires_grad。函数可以用装饰器@torch.no_grad()。可实现一定程度的速度提升,并节省约一半显存,因为其不需要分配空间保存梯度。

详细内容可见:https://pytorch.org/docs/master/autograd.html#locally-disable-grad

1
2
3
4
x = t.tensor([1.], requires_grad = True)
with t.no_grad():
y = x * 2
y.requires_grad
False
1
2
3
4
5
@t.no_grad()
def doubler(x):
return x * 2
z = doubler(x)
z.requires_grad
False

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

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

推荐使用hook方法,但在实际使用中应尽量避免修改grad的值。

1
2
3
4
5
6
x = V(t.ones(3), requires_grad = True)
w = V(t.rand(3), requires_grad = True)
y = x * w
# y依赖于w,而w.requires_grad = True
z = y.sum()
x.requires_grad, w.requires_grad, y.requires_grad
(True, True, True)
1
2
3
# 非叶子节点grad计算完之后自动清空,y.grad是None
z.backward()
x.grad, w.grad, y.grad
(tensor([0.1283, 0.8326, 0.6539]), tensor([1., 1., 1.]), None)
1
2
3
4
5
6
7
8
# 第一种方法:使用grad获取中间变量的梯度
x = V(t.ones(3), requires_grad = True)
w = V(t.rand(3), requires_grad = True)
y = x * w
# y依赖于w,而w.requires_grad = True
z = y.sum()
# z对y的梯度,隐式调用backward()
t.autograd.grad(z, y)
(tensor([1., 1., 1.]),)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 第二种方法:使用hook
# hook是一个函数,输入是梯度,不应该有返回值

def variable_hook(grad):
print('y的梯度:\r\n',grad)

x = V(t.ones(3), requires_grad = True)
w = V(t.rand(3), requires_grad = True)
y = x * w
# 注册hook
hook_handle = y.register_hook(variable_hook)
z = y.sum()
z.backward()

# 除非你每次都要用hook,否则用完之后记得移除hook
hook_handle.remove()
y的梯度:
 tensor([1., 1., 1.])

最后再来看看variable中grad属性和backward函数grad_variables参数的含义。

  • variables $\boldsymbol{x}$ 的梯度是目标函数$f(x)$对$\boldsymbol{x}$的梯度,形状与$\boldsymbol{x}$一致。
  • y.backward(grad_variables)中grad_variables相当于链式法则中的$\frac{\partial{z}}{\partial{x}}=\frac{\partial{z}}{\partial{y}}\frac{\partial{y}}{\partial{x}}$中的$\frac{\partial{z}}{\partial{y}}$。z是目标函数,一般是个标量,故而$\frac{\partial{z}}{\partial{y}}$的形状与$\boldsymbol{y}$的形状一致。z.backward()等价于y.backward(grad_y)。而z.backward()省略了grad_variables参数,是因为z是个标量,而$\frac{\partial{z}}{\partial{z}}=1$
1
2
3
4
5
x = V(t.arange(0, 3).float(), requires_grad = True)
y = x**2 + x*2
z = y.sum()
z.backward()
x.grad
tensor([2., 4., 6.])
1
2
3
4
5
6
x = V(t.arange(0, 3).float(), requires_grad = True)
y = x**2 + x*2
z = y.sum()
y_grad_variables = V(t.Tensor([1, 1, 1]))
y.backward(y_grad_variables)
x.grad
tensor([2., 4., 6.])

值得注意的是,只有对variable的操作才能使用autograd,如果variable的data直接进行操作,将无法使用反向传播。除了参数初始化,一般我们不会直接修改variable.data的值。

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

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

扩展autograd

目前绝大多数函数都可以使用autograd实现反向求导,但如果需要自己写一个复杂的函数,不支持自动反向求导怎么办?答案是写一个Function,实现它的前向传播和反向传播代码,Function对应于计算图中的矩形,它接受参数,计算并返回结果。下面给出了一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from torch.autograd import Function
class MultiplyAdd(Function):

@staticmethod
def forward(ctx, w, x, b):
print('type in forward', type(x))
ctx.save_for_backward(w, x)#存储用来反向传播的参数
output = w*x +b
return output

@staticmethod
def backward(ctx, grad_output):
w, x = ctx.saved_tensors # 老版本是saved_variables
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

分析如下:

  • 自定义的Function需要继承autograd.Function,没有构造函数init,forward和backward函数都是静态方法
  • forward函数的输入和输出都是Tensor,backward函数的输入和输出都是Variable
  • backward函数的输出和forward函数的输入一一对应,backward函数的输入和forward函数的输出一一对应
  • backward函数的grad_output参数即t.autograd.backward中的grad_variables
  • 如果某一个输入不需要求导,直接返回None,如forward中的输入参数x_requires_grad显然无法对它求导,直接返回None即可
  • 反向传播可能需要利用前向传播的某些中间结果,需要进行保存,否则前向传播结束后这些对象即被释放
1
2
3
4
5
6
7
8
9
10
x = V(t.ones(1))
w = V(t.rand(1),requires_grad=True)
b = V(t.rand(1),requires_grad=True)
print('开始前向传播')
z = MultiplyAdd.apply(w, x, b)
print('开始反向传播')
z.backward()

# x不需要求导,中间过程还是会计算它的导数,但随后被清空
x.grad, w.grad, b.grad
开始前向传播
type in forward <class 'torch.Tensor'>
开始反向传播
type in backward <class 'torch.Tensor'>





(None, tensor([1.]), tensor([1.]))
1
2
3
4
5
6
7
8
9
10
x = V(t.ones(1))
w = V(t.rand(1),requires_grad=True)
b = V(t.rand(1),requires_grad=True)
print('开始前向传播')
z = MultiplyAdd.apply(w, x, b)
print('开始反向传播')

# 调用MultiplyAdd.backward
# 会自动输出grad_w, grad_x, grad_b
z.grad_fn.apply(V(t.ones(1)))
开始前向传播
type in forward <class 'torch.Tensor'>
开始反向传播
type in backward <class 'torch.Tensor'>





(tensor([1.]), tensor([0.5986], grad_fn=<MulBackward0>), tensor([1.]))

在backward函数里之所以也要对variable进行操作是为了能计算梯度的梯度。

1
2
3
4
x = V(t.Tensor([5]), requires_grad = True)
y = x ** 2
grad_x = t.autograd.grad(y, x, create_graph = True)
grad_x
(tensor([10.], grad_fn=<MulBackward0>),)
1
grad_grad_x = t.autograd.grad(grad_x[0],x);grad_grad_x
(tensor([2.]),)

———————————————感谢阅读———————————————

欢迎收藏访问我的博客 知乎 掘金 简书 知乎

贰三 wechat
欢迎扫描二维码订阅我的公众号!