神经网络的训练过程是一个不断更新权重的过程,而权重的更新要使用到反向传播,而反向传播的本质呢就是求导数。
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()
即可:
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 的导数,这个变量必须设为 True
。requires_grad
的默认值为 False
。
>>> a = torch.tensor(2.)
>>> a.requires_grad
False
>>> a
tensor(1.)
而所有基于叶子节点生成的 tenor 的 requires_grad
属性与叶子节点相同。
>>> b = a**2 1
>>> b.requires_grad
False
>>> b
tensor(5.)
如果没有在创建的时候显式声明 requires_grad=True
,也可以在用之前临时声明:
>>> a.requires_grad_(True)
>>> a.requires_grad = True # 另一种写法
>>> a
tensor(2., requires_grad=True)
而因为 b = a**2 1
,此时 b
的属性变成了
tensor(5., grad_fn=<AddBackward0>)
想对 b 求导,使用 b.backward()
即可:
>>> b.backward()
查看 a
在 a = 2
处的导数,使用 a.grad
即可:
>>> 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.])
为例:
>>> 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
的值,下面这个函数可以直接求得导数。
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。如果 是 0 维张量,grad_outputs
可以忽略;否则需要为一个与x维度相同的张量作为权重。
>>> 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=True
或 create_graph=True
。retain_graph
为保存计算图,create_graph
为创建计算图,两者的作用是相同的,都可以保存当前计算图。
>>> 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)
以上面的 a
和 b
为例,b.backward()
= torch.autograd.backward(b)
。其中 grad_tensors
与 b.backward()
中的 gradient
变量作用相同;retain_graph
和 create_graph
与 torch.augograd.grad
中的同名变量相同,不再赘述。