自动求导
下面的例子涉及到向量:
下面的例子涉及到矩阵:
实际上,给定一个复杂的函数,我们是可以通过链式法则加上一些基本函数的求导公式是可以对该函数进行求导的。
但是最大的问题是神经网络动不动就是几百层,几乎很难手写。因此就希望能够自动求导来解决这个问题。
自动求导如何做出来呢?这个涉及到一个叫计算图的知识点,下面介绍何为计算图:
虽然PyTorch框架已经给我们封装好了对应的知识,但是我们还是有必要了解一下计算图内部的工作原理。
实现
求导是几乎所有深度学习优化算法的关键步骤,对于简单的模型来说,你当然可以通过手工进行,但是对于复杂的模型,手工进行更新是一件很痛苦的事情,且经常容易出错。
深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。实际中,根据设计好的模型,系统会构建一个计算图(computational graph),来跟踪计算是哪些数据通过哪些操作组合起来产生输出。自动微分使系统能够随后反向传播梯度。这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。
下面还是举一个简单的例子进行说明:
假设我们想对函数 y = 2 x ⊤ x y=2\mathbf{x}^{\top}\mathbf{x} y=2x⊤x关于列向量 x \mathbf{x} x求导
首先,我们创建变量x
并为其分配一个初始值。
import torch
x = torch.arange(4.0)
x
在我们计算 y y y关于 x \mathbf{x} x的梯度之前,需要一个地方来存储梯度
重要的是,我们不会在每次对一个参数求导时都分配新的内存。因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。
需要注意的是,一个标量函数关于向量 x \mathbf{x} x的梯度是向量,并且与 x \mathbf{x} x具有相同的形状。
x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True)
# 使张量x可以记录梯度计算的历史,便于进行自动求导
x.grad # 默认值是None
# 计算出来的梯度将会存在张量的.grad属性中
下面通过调用反向传播函数来自动计算y
关于x
每个分量的梯度,打印出来看看:
y.backward() # 求导
x.grad # 之前的求导结果都是放在这里面的,因此可以通过该条语句访问导数
上述我们可以通过数学的方式进行验证,但是我们也可以通过代码的方式验证是否正确:
函数 y = 2 x ⊤ x y=2\mathbf{x}^{\top}\mathbf{x} y=2x⊤x关于 x \mathbf{x} x的梯度应为 4 x 4\mathbf{x} 4x。下面快速验证这个梯度是否计算正确:
x.grad == 4 * x # 可以使用该条语句进行求导验证
下面来计算x
的另一个函数。
默认情况下,PyTorch会累积梯度,因此在每次求解之前需要将梯度清零
# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()
# 将张量x的梯度清零,要是不清零的话,会导致梯度累积,结果是不正确的
y = x.sum() # 计算x的所有元素的和,并将结果存储在一个新的张量y中
y.backward() # 触发反向传播,计算y相对于所有需要梯度的张量的梯度
x.grad
# 求和之后,对于x[i]只有x[i]对其偏导数有贡献,其余的都没贡献
上述 y = ∑ k = 1 n x k = x m + x m + 1 + … + x n y=\sum_{k=1}^{n} x_k = x_m + x_{m+1} + \ldots + x_n y=k=1∑nxk=xm+xm+1+…+xn对每一个 x i x_i xi求导的话,结果都是1,因此自动求导正确。
非标量变量的反向传播
上述定义的 y y y 都是标量,当 y y y 不是标量的时候,向量y
关于向量x
的导数的最自然解释是一个矩阵。对于高阶和高维的 y
和 x
,求导的结果可以是一个高阶张量。
然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括[深度学习中]),但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。这里(,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。)
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
分离计算
某些情况下,我们希望将某些计算移动到记录的计算图之外。例如,假设y
是作为x
的函数计算的,而z
则是作为y
和x
的函数计算的。想象一下,我们想计算z
关于x
的梯度,但由于某种原因,希望将y
视为一个常数,并且只考虑到x
在y
被计算后发挥的作用。
这里可以分离y
来返回一个新变量u
,该变量与y
具有相同的值,但丢弃计算图中如何计算y
的任何信息。换句话说,梯度不会向后流经u
到x
。
因此,下面的反向传播函数计算z=u*x
关于x
的偏导数,同时将u
作为常数处理,而不是z=x*x*x
关于x
的偏导数。
x.grad.zero_() # 梯度清零
y = x * x
u = y.detach()
# 切断计算图,从计算图中分离出来一个张量,这样就可以将y视为一个常数了
z = u * x
z.sum().backward()
x.grad == u # 因为切断了联系,因此求导后就只剩下了u这样一个常数了
由于记录了y
的计算结果,随后在y
上调用反向传播,得到y=x*x
关于的x
的导数,即2*x
。
Python 控制流的梯度计算
使用自动微分的一个好处是:即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),仍然可以计算得到的变量的梯度。
在下面的代码中,while
循环的迭代次数和if
语句的结果都取决于输入a
的值。
def f(a):
b = a * 2
while b.norm() < 1000: # 要是b的二范数小于1000
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
下面计算梯度:
a = torch.randn(size=(), requires_grad=True) # 创建输入张量a并启用梯度计算
d = f(a)
d.backward() # 执行反向传播
我们现在可以分析上面定义的f
函数。请注意,它在其输入a
中是分段线性的。换言之,对于任何a
,存在某个常量标量k
,使得f(a)=k*a
,其中k
的值取决于输入a
,因此可以用d/a
验证梯度是否正确。
a.grad == d / a
# 看完函数f(a) 得d=f(a)=c, c为b 或者 b*100
# 且 b 为a*2 或者a*(n个2 2n=实数)
# 综上得 d 最终结果都是 此处用A代表实数 a*A
# 则 d=f(a)=Aa
# 对 d 求导得 A
# d=Aa 故d/a = 实数A a的导数 a.grad = 实数A
关于上述 s i z e ( ) size() size() 的用法,大致简单介绍如下:
思考QA
当在神经网络中求梯度的时候,是需要正向算一遍,反向算一遍的,这是必要的
反向指的是通过计算图,求出目标函数对每个中间以及输入的导
正向指的是要把他的“公式”写出来