PyTorch3:计算图torch.autograph

2020-08-06 18:19:13 浏览数 (1)

神经网络的训练过程是一个不断更新权重的过程,而权重的更新要使用到反向传播,而反向传播的本质呢就是求导数

1. 计算图


一个深度学习模型是由“计算图”所构成的。

计算图是一个有向无环图。数据是图的节点,运算是图的

计算图计算图

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

代码语言:javascript复制
a = torch.tensor([1.])
b = torch.tensor([2.])
c = a.add(b)

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

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

1.1 动态图


动态图的搭建过程与执行过程可以同时进行。PyTorch 默认采用动态图机制。

我们看一个例子:

代码语言:javascript复制
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 以前是怎么搭建的:

代码语言:javascript复制
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 时的导数:

在 PyTorch 中求导数非常简单,使用 tensor.backward()即可:

代码语言:javascript复制
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(导数)的概述


如何求导数是中学的数学知识,这里不再过多赘述.

仅仅提一点,对求某某的 “偏导数”,此时仅将这一变量当作变量,其他不相关的变量被看成常量,在求导时消去。

3. chain rule 运算规则


假如我们想对z=f(g(x)) 求导,可以设 y=g(x),z=f(x)则:z对x的导数等于z对y求导乘上y对x求导

4. 张量的反向传播


张量的求导函数为:

代码语言:javascript复制
tensor.backward(gradient=None, retain_graph=None, create_graph=False)

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


我们自己创建的 tensor 叫做创建变量,通过运算生成的 tensor 叫做结果变量

tensor 的一个创建方法为

代码语言:javascript复制
torch.tensor(data, dtype=None, device=None, requires_grad=False, pin_memory=False)

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

代码语言:javascript复制
>>> a = torch.tensor(2.)
>>> a.requires_grad
False
>>> a
tensor(1.)

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

代码语言:javascript复制
>>> b = a**2   1
>>> b.requires_grad
False
>>> b
tensor(5.)

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

代码语言:javascript复制
>>> a.requires_grad_(True)
>>> a.requires_grad = True # 另一种写法
>>> a
tensor(2., requires_grad=True)

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

代码语言:javascript复制
tensor(5., grad_fn=<AddBackward0>)

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

代码语言:javascript复制
>>> b.backward()

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

代码语言:javascript复制
>>> a.grad
tensor(4.)

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


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

代码语言:javascript复制
>>> a = torch.tensor([1., 2.], requires_grad=True)
>>> b = a**2   1
>>> b
tensor([2., 5.], 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.]) 为例:

代码语言:javascript复制
>>> b.backward(gradient=torch.tensor([1., 1.]))
>>> b.backward(gradient=torch.ones_like([1., 1.])) # 创建一个与 a 维度相同的全 1 张量
>>> a.grad
tensor([2., 4.])

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


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

代码语言:javascript复制
torch.autograd.grad(outputs, inputs, grad_outputs=None, retain_graph=None, create_graph=False, only_inputs=True, allow_unused=False)

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

代码语言:javascript复制
>>> 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.]])

假如我们1⃣以上面的z对x求导 ,结果为6(x 2) 。假如我们想用z对x求二阶偏导呢?会报错:

代码语言:javascript复制
>>> 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 为创建计算图,两者的作用是相同的,都可以保存当前计算图。

代码语言:javascript复制
>>> 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.]]),)

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


代码语言:javascript复制
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 中的同名变量相同,不再赘述。

0 人点赞