0%

[DL] PyTorch 折桂 4:torch.autograph

神经网络的训练过程其实就是一个不断更新权重的过程,而更新权重要使用反向传播,而反向传播的本质是求导数。PyTorch.autograd 应运而生,接管了神经网络中不断重复的求导数运算。

1. 计算图

一个深度学习模型是由“计算图”构成的。所谓计算图是一个有向无环图(directed acyclic graph)。数据是这个图的节点(node),运算是这个图的边(edge)。如下图所示:
计算图

这张计算图的数学表达式为 $y=(x+w)*(w+1)$。其中,$x$、$w$ 和 $b$ 是由用户定义的,称为“叶子节点”(leaf node),可在 PyTorch 中加以验证:

1
2
3
4
5
6
a = torch.tensor([1.])
b = torch.tensor([2.])
c = a.add(b)

a.is_leaf() # True
c.is_leaf() # False

计算图可以分为动态图与静态图两种。

1.1 动态图

动态图的搭建过程与执行过程可以同时进行。PyTorch 默认采用动态图机制。我们看一个例子:

1
2
3
4
5
6
7
8
9
10
import torch
first_counter = torch.Tensor([0])
second_counter = torch.Tensor([10])

while (first_counter[0] < second_counter[0]): #[0] 加不加没有影响
first_counter += 2
second_counter += 1

print(first_counter)
print(second_counter)

1.2 静态图

静态图先创建计算图,然后执行计算图。计算图一经定义,无法改变。TensorFlow 2.0 以前以静态图为主。我们看同样的例子在 TensorFlow 2.0 以前是怎么搭建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import tensorflow as tf
first_counter = tf.constant(0) # 定义变量
second_counter = tf.constant(10) # 定义变量

def cond(first_counter, second_counter, *args): # 定义条件
return first_counter < second_counter
def body(first_counter, second_counter): # 定义条件
first_counter = tf.add(first_counter, 2)
second_counter = tf.add(second_counter, 1)
return first_counter, second_counter

c1, c2 = tf.while_loop(cond, body, [first_counter, second_counter]) # 定义循环

with tf.Session() as sess: # 建立会话执行计算图
counter_1_res, counter_2_res = sess.run([c1, c2])

print(first_counter)
print(second_counter)

因为静态图在设计好以后不能改变,调试的过程中 debug 实在太痛苦了。所以 TensorFlow 2.0 开始默认使用动态图。

1.3 计算图示例

假如我们想计算上面计算图中 $y=(x+w)*(w+1)$ 在 $x=2$,$w=1$ 时的导数:

  1. 首先,我们将上式进行分解:
    $$a=x+w$$
    $$b=w+1$$
    于是我们得
    $$y=a*b$$
    对上式求导有:
    $$\frac{\partial y}{\partial w}=\frac{\partial y}{\partial a}\frac{\partial a}{\partial w}+\frac{\partial y}{\partial b}\frac{\partial b}{\partial w}$$
    根据$y=a*b$,$a=x+w$ 和 $b=w+1$ 可知:
    $$\frac{\partial y}{\partial a}=b=w+1$$
    $$\frac{\partial a}{\partial w}=1$$
    $$\frac{\partial y}{\partial b}=a=x+w$$
    $$\frac{\partial b}{\partial w}=1$$
    所以
    $$\frac{\partial y}{\partial w}=(w+1)+(x+w)=2*1+2+1=5$$
    在 PyTorch 中求导数非常简单,使用 tensor.backward()即可:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import torch

    x = torch.tensor([2.], requires_grad=True) # 开启导数追踪
    w = torch.tensor([1.], requires_grad=True) # 开启导数追踪

    a = w.add(x)
    b = w.add(1)
    y = a.mul(b)

    y.backward() # 求导
    print(w.grad)

    2. derivative(导数)的概述

    当函数 $f$ 的自变量在一点 $x_0$ 上产生一个增量 $h$ 时,函数输出值的增量与自变量增量 $h$ 的比值在 $h$ 趋于0时的极限如果存在,即为 $f$ 在 $x_0$ 处的导数,记作 $f’(x_0)$。如果函数的自变量和取值都是实数的话,那么函数在某一点的导数就是该函数所代表的曲线在这一点上的切线斜率。

    -- Wikipedia

如何求导数是中学的数学知识,这里不再过多赘述,仅仅提一点,对 $z=f(x,y)$ 求 $\frac{\partial x}{\partial z}$ 叫做 “$f$ 关于 $x$ 的偏导数”,此时 $y$ 被看成常量,在求导时消去。

3. chain rule

假如我们想对 $z=f(g(x))$ 求导,可以设 $y=g(x), z=f(y)$,则
$$\frac{\partial x}{\partial z}=\frac{\partial x}{\partial y}\cdot \frac{\partial y}{\partial z}$$

4. 张量的反向传播

张量的求导函数为:

1
tensor.backward(gradient=None, retain_graph=None, create_graph=False)

4.1 运算结果为 0 维张量的反向传播

我们自己创建的 tensor 叫做*创建变量*,通过运算生成的 tensor 叫做*结果变量*。tensor 的一个创建方法为

1
torch.tensor(data, dtype=None, device=None, requires_grad=False, pin_memory=False)

别的不说,单单说 requires_grad。如果想求这个 tensor 的导数,这个变量必须设为 True
requires_grad 的默认值为 False

1
2
3
4
5
>>> a = torch.tensor(2.)
>>> a.requires_grad
False
>>> a
tensor(1.)

而所有基于叶子节点生成的 tenor 的 requires_grad 属性与叶子节点相同。

1
2
3
4
5
>>> b = a**2 + 1
>>> b.requires_grad
False
>>> b
tensor(5.)

如果没有在创建的时候显式声明 requires_grad=True,也可以在用之前临时声明:

1
2
3
4
>>> a.requires_grad_(True)
>>> a.requires_grad = True # 另一种写法
>>> a
tensor(2., requires_grad=True)

而因为 b = a + 1,此时 b 的属性变成了

1
tensor(5., grad_fn=<AddBackward0>)

想对 b 求导,使用 b.backward() 即可:

1
>>> b.backward()

查看 aa = 2 处的导数,使用 a.grad 即可:

1
2
>>> a.grad
tensor(4.)

这个很好理解,$\frac{\partial a}{\partial b} = (a^2)’ = 2 * a = 2 * 2 = 4$。

4.2 运算结果为 1 维以上张量的反向传播

如果结果为1 维以上张量,直接求导会出错:

1
2
3
4
5
6
7
8
9
10
11
>>> a = torch.tensor([1., 2.], requires_grad=True)
>>> b = a**2 + 1
>>> b
tensor([2., 3.], grad_fn=<AddBackward0>)
>>> b.backward()
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-391-a721975e1357> in <module>
----> 1 b.backward()
...
RuntimeError: grad can be implicitly created only for scalar outputs

这是因为 [2., 3.] 没法求导。这时候就必须指定 backward() 中的 gradient 变量为一个与创建变量维度相同的变量作为权重,这里以 torch.tensor([1., 1.]) 为例:

1
2
3
4
>>> b.backward(gradient=torch.tensor([1., 1.]))
>>> b.backward(gradient=torch.ones_like([1., 1.])) # 创建一个与 a 维度相同的全 1 张量
>>> a.grad
tensor([2., 4.])

关于 gradient 的详细讨论可以参考PyTorch 的 backward 为什么有一个 grad_variables 参数?Autograd:PyTorch中的梯度计算 两篇文章。

5. 张量的显式求导 torch.augograd.grad

虽然我们可以通过 b.backward() 来计算 a.grad 的值,下面这个函数可以直接求得导数。

1
torch.autograd.grad(outputs, inputs, grad_outputs=None, retain_graph=None, create_graph=False, only_inputs=True, allow_unused=False)

以 $y=f(x)$ 为例,inputs 是 $x$,outputs 是 $y$。如果 $y$ 是 0 维张量,grad_outputs 可以忽略;否则需要为一个与 $x$ 维度相同的张量作为权重。

1
2
3
4
5
6
7
>>> x=torch.tensor([[1.,2.,3.],[4.,5.,6.]],requires_grad=True)
>>> y=x+2
>>> z=y*y*3
>>> dzdx = torch.autograd.grad(inputs=x, outputs=z, grad_outputs=torch.ones_like(x))
>>> print(dzdx)
(tensor([[18., 24., 30.],
[36., 42., 48.]])

假如我们对上面的 $z$ 求 $\frac{\partial x}{\partial z}$,结果为 $\frac{\partial x}{\partial z}=\frac{\partial x}{\partial y}\cdot\frac{\partial y}{\partial z}=1\cdot 2\cdot 3\cdot(x+2)$。假如我们想求 $\frac{\partial\partial x}{\partial\partial z}$ 即二阶偏导呢?会报错:

1
2
3
4
5
6
7
>>> dzdx = torch.autograd.grad(inputs=x, outputs=z, grad_outputs=torch.ones_like(x))
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-440-7a6333e01d6f> in <module>
----> 1 dzdx = torch.autograd.grad(inputs=x, outputs=z, grad_outputs=torch.ones_like(x))
...
RuntimeError: Trying to backward through the graph a second time, but the buffers have already been freed. Specify retain_graph=True when calling backward the first time.

这是因为动态计算图的特点是使用完毕后会被释放,当我们对 b 求导的话,对 b 求导的计算图在使用完毕后就被释放了。如果我们想求二阶导数,需要设置 retain_graph=Truecreate_graph=Trueretain_graph 为保存计算图,create_graph 为创建计算图,两者的作用是相同的,都可以保存当前计算图。

1
2
3
4
5
>>> dzdx = torch.autograd.grad(inputs=x, outputs=z, grad_outputs=torch.ones_like(x),create_graph=True)
>>> dz2dx2 = torch.autograd.grad(inputs=x, outputs=dzdx, grad_outputs=torch.ones_like(x))
>>> print(dz2dx2)
(tensor([[6., 6., 6.],
[6., 6., 6.]]),)

结果也很好理解,$\frac{\partial\partial x}{\partial\partial z}=1\cdot 2\cdot 3=6$。

6. 张量的显式反向传播计算torch.autograd.backward

1
torch.autograd.backward(tensors, grad_tensors=None, retain_graph=None, create_graph=False)

以上面的 ab 为例,b.backward() = torch.autograd.backward(b)。其中 grad_tensorsb.backward() 中的 gradient 变量作用相同;retain_graphcreate_graphtorch.augograd.grad 中的同名变量相同,不再赘述。

欢迎关注我的其它发布渠道