07-自动求导

07-链式法则与自动求导

本节目录

1. 向量链式法则

  • 1.1 标量链式法则

image-20220107231957396
  • 1.2 拓展到向量

    需要注意维数的变化

    下图三种情况分别对应:

    1. y为标量,x为向量
    2. y为标量,x为矩阵
    3. y、x为矩阵
image-20220107231931153
例1(标量对向量求导)

这里应该是用分子布局,所以是X转置

image-20220107233527373

例2(涉及到矩阵的情况)

X是mxn的矩阵,w为n维向量,y为m维向量; z对Xw-y做L2 norm,为标量; 过程与例一大体一致;

image-20220107233753066


由于在神经网络动辄几百层,手动进行链式求导是很困难的,因此我们需要借助自动求导


2. 自动求导

  • 含义:计算一个函数在指定值上的导数

  • 自动求导有别于

    • 符号求导

      image-20220107235547201
    • 数值求导

      image-20220107235611767

为了更好地理解自动求导,下面引入计算图的概念

2.1 计算图

  • 将代码分解成操作子

  • 将计算表示成一个无环图

    下图自底向上其实就类似于链式求导过程

image-20220107235918270

  • 计算图有两种构造方式

    • 显示构造

      可以理解为先定义公式再代值

      Tensorflow/Theano/MXNet

      image-20220108000740736

    • 隐式构造

      系统将所有的计算记录下来

      Pytorch/MXNet

      image-20220108001154208

2.2 自动求导的两种模式

  • 正向累积

    image-20220108001506326

  • 反向累积(反向传递back propagation)

    image-20220108001517029

反向累积计算过程

image-20220108001847175

反向累积的正向过程:自底向上,需要存储中间结果

反向累积的反向过程:自顶向下,可以去除不需要的枝(图中的x应为w)

image-20220108002228166

2.3 复杂度比较

  • 反向累积

    • 时间复杂度:O(n),n是操作子数
      • 通常正向和反向的代价类似
    • 空间复杂度:O(n)
      • 存储正向过程所有的中间结果
  • 正向累积

    每次计算一个变量的梯度时都需要将所有节点扫一遍

    • 时间复杂度:O(n)
    • 空间复杂度:O(1)

3. 代码部分

#对y = x.Tx关于列向量x求导
import torch

x = torch.arange(4.0)
x
tensor([0., 1., 2., 3.])
#存储梯度
x.requires_grad_(True)#等价于x = torch.arange(4.0,requires_grad=True)
x.grad#默认值是None
y = torch.dot(x,x)
y
#PyTorch隐式地构造计算图,grad_fn用于记录梯度计算
tensor(14., grad_fn=<DotBackward0>)

通过调用反向传播函数来自动计算y关于x每个分量的梯度

y.backward()
x.grad
tensor([0., 2., 4., 6.])
x.grad==2*x#验证
tensor([True, True, True, True])
# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()#如果没有这一步结果就会加累上之前的梯度值,变为[1,5,9,13]
y = x.sum()
y.backward()
x.grad
tensor([1., 1., 1., 1.])
x.grad.zero_()
y=x*x#哈达玛积,对应元素相乘

#在深度学习中我们一般不计算微分矩阵
#而是计算批量中每个样本单独计算的偏导数之和

y.sum().backward()#等价于y.backword(torch.ones(len(x)))
x.grad
tensor([0., 2., 4., 6.])

将某些计算移动到记录的计算图之外

# 后可用于用于将神经网络的一些参数固定住
x.grad.zero_()
y = x*x
u = y.detach()#把y当作常数
z = u*x

z.sum().backward()
x.grad == u
tensor([True, True, True, True])
x.grad.zero_()
y.sum().backward()
x.grad == 2*x
tensor([True, True, True, True])

即使构建函数的计算图需要用过Python控制流,仍然可以计算得到的变量的梯度

这也是隐式构造的优势,因为它会存储梯度计算的计算图,再次计算时执行反向过程就可以

def f(a):
    b = a * 2
    while b.norm()<1000:
        b = b * 2
    if b.sum() > 0:
        c = b
    else:
        c = 100 * b
    return c

a = torch.randn(size=(),requires_grad=True)
d = f(a)
d.backward()

a.grad == d / a

4. 自动求导 Q&A

Q1:ppt上隐式构造和显式构造看起来为啥差不多?

显式和隐式的差别其实就是数学上求梯度和python求梯度计算上的差别,不用深究

显式构造就是我们数学上正常求导数的求法,先把所有求导的表达式选出来再代值

Q2:需要正向和反向都算一遍吗?

需要正向先算一遍,自动求导时只进行反向就可以,因为正向的结果已经存储

Q3:为什么PyTorch会默认累积梯度

便于计算大批量;方便进一步设计

Q4:为什么深度学习中一般对标量求导而不是对矩阵或向量求导

loss一般都是标量

Q5:为什么获取.grad前需要backward

相当于告诉程序需要计算梯度,因为计算梯度的代价很大,默认不计算

Q6:pytorch或mxnet框架设计上可以实现矢量的求导吗

可以

5. 练习

1.为什么计算二阶导数比一阶导数的开销要更大?

二阶导数是在一阶导数的基础上进行的,开销自然更大

2.在运行反向传播函数之后,立即再次运行它,看看会发生什么。

“RuntimeError: Trying to backward through the graph a second time (or directly access saved tensors after they have already been freed). Saved intermediate values of the graph are freed when you call .backward() or autograd.grad(). Specify retain_graph=True if you need to backward through the graph a second time or if you need to access saved tensors after calling backward.”

说明不能连续两次运行,pytorch使用的是动态计算图,反向传播函数运行一次后计算图就被释放了

只需要在函数接口将参数retain_graph设为True即可

In [51]:

def f(a):
    b = a * 2
    while b.norm()<1000:
        b = b * 2
    if b.sum() > 0:
        c = b
    else:
        c = 100 * b
    return c
a.grad.zero_()
a = torch.randn(size=(),requires_grad=True)#size=0表示a是标量
d = f(a)
#d.backward(retain_graph=True)
#a.grad
d.backward()
a.grad

Out[51]:

tensor(4096.)

3.在控制流的例子中,我们计算d关于a的导数,如果我们将变量a更改为随机向量或矩阵,会发生什么?此时,计算结果f(a)不再是标量。结果会发生什么?我们如何分析这个结果?

backward函数的机制本身不允许张量对张量求导,如果输入是向量或矩阵,需要将其在各个分量上求和,变为标量;所以还需要传入一个与输入同型的张量

In [53]:

def f(a):
    b = a * 2
    while b.norm()<1000:
        b = b * 2
    if b.sum() > 0:
        c = b
    else:
        c = 100 * b
    return c
a.grad.zero_()
a = torch.randn(10,requires_grad=True)
d = f(a)
#d.backward(retain_graph=True)
#a.grad
#d.backward()#RuntimeError: grad can be implicitly created only for scalar outputs
d.sum().backward()#需要加上.sum()否则会报错 
a.grad

Out[53]:

tensor([51200., 51200., 51200., 51200., 51200., 51200., 51200., 51200., 51200.,
        51200.])

4.重新设计一个求控制流梯度的例子。运行并分析结果。

In [56]:

def h(x):
    y = x * x
    while y.norm() < 2500:
        y = y * 2
    if y.sum() < 0:
        c = 100*y
    else:
        c = y
    return c
x.grad.zero_()
x = torch.randn(size=(),requires_grad=True)
y = h(x)
y.backward()
x.grad

Out[56]:

tensor(-3311.5398)

5.使f(x)=sin(x),绘制f(x)和df(x)/dx的图像,其中后者不使用f’(x)=\cos(x)。

In [66]:

import matplotlib.pyplot as plt
x = torch.arange(-20,20,0.1,requires_grad=True,dtype=torch.float32)
y = torch.sin(x)
y.sum().backward()
plt.plot(x.detach(),y.detach(),label='y=sinx')
plt.plot(x.detach(),x.grad,label='dy/dx')
plt.legend(loc='lower right')

Out[66]:

<matplotlib.legend.Legend at 0x1d627d00280>
img.png
上一页
下一页