前言
在神经网络中,一个重要的内容就是进行参数学习,而参数学习离不开求导,那么pytorch是如何进行求导的呢?
现在大部分深度学习架构都有自动求导的功能,pytorch也不例外,torch.autograd包就是用来自动求导的。Autograd包为张量上的所有操作提供了自动求导功能,torch.Tensor 和torch.Function为Autograd的两个核心类,它们相互连接并生成一个有向非循环图。
自动求导要点
为了实现对Tensor自动求导,需考虑如下事项:
- 创建叶子节点的Tensor,使用requires_grad参数指定是否记录对其的操作,以便之后利用backward()方法进行梯度求解。requires_grad参数的缺省值为False,如果要对其求导需设置为true,然后与之有依赖关系的节点会自动变为True。
- 可利用requires_grad()方法修改Tensor的requires_grad属性。
可以调用.detach()或者with torch.no_grad:,将不再计算张量的梯度、跟踪张量的历史记录。这点在评估模型、测试模型阶段中常常用到。 - 通过运算创建的Tensor(即非叶子节点),会被自动赋予grad_fn属性,该属性表示梯度函数。叶子节点的grad_fn为None。
- 最后得到的Tensor执行backward(),此时自动计算各变量的梯度,并将结果保存的grad属性中。计算完成后,非叶子节点的梯度自动释放。
- backward()函数接收参数,该参数应和调用backward()函数的Tensor的维度相同,或者是可broadcast的维度。如果求导的Tensor为标量(即是一个数字),backward中的参数可省略。
- 反向传播的中间缓存会被清空,如果需要多次反向传播,需要指定backward中的参数retain_graph=True。多次反向传播,梯度是累加的。
- 非叶子节点的梯度backward调用后即被清空
- 可以通过 torch.no_grad()包裹代码块的形式,来阻止autograd去跟踪那些标记为.requires_grad=True的张量的历史记录。这步在测试阶段经常使用。
在整个过程中,pytorch采用计算图的形式进行组织,该计算图为动态图,且在每次前向传播时将重新构建。其他深度学习框架,如tensorflow,keras一般为静态图。
计算图
计算图是一种有向无环图像,用图形方式来表示算子与变量之间的关系,直观高效。如图所示,圆形表示变量,矩形表示算子。
如表达式:
z
=
w
x
+
b
{\rm{z}} = wx + b
z=wx+b,可写成两个表达式:
y
=
w
x
y = wx
y=wx,则
z
=
y
+
b
z = y + b
z=y+b,其中x、w、b为变量,是用户创建的变量,不依赖与其他变量,故又称为叶子节点。为了计算各叶子节点的梯度,需要把对应的张量参数requires_grad属性设置为True,这样就可以自动跟踪其历史记录。y、z是计算得到的变量,非叶子节点,z为根节点。mul和add是算子(或操作或函数)。由这些变量及算子,就构成一个完整的计算过程或前向传播过程。
我们的目标是更新各叶子节点的梯度,根据符合函数链式求导法则,不难算出各叶子节点的梯度。
∂
z
∂
x
=
∂
z
∂
y
∂
y
∂
x
=
w
\frac{{\partial z}}{{\partial x}} = \frac{{\partial z}}{{\partial y}}\frac{{\partial y}}{{\partial x}} = w
∂x∂z=∂y∂z∂x∂y=w
∂
z
∂
w
=
∂
z
∂
y
∂
y
∂
w
=
x
\frac{{\partial z}}{{\partial w}} = \frac{{\partial z}}{{\partial y}}\frac{{\partial y}}{{\partial w}} = x
∂w∂z=∂y∂z∂w∂y=x
∂
z
∂
b
=
1
\frac{{\partial z}}{{\partial b}} = 1
∂b∂z=1
pyorch调用 backward()方法,将自动计算个节点的梯度,这是一个反向传播过程,从当前根节点z反向溯源,利用倒数链式法则,计算所有叶子节点的梯度,其梯度值将累加到grad属性中。对非叶子节点的操作或function记录在grad_fn属性中,叶子节点的grad_fn为None。
标量反向传播
假设x、w、b都是标量, z = w x + b {\rm{z}} = wx + b z=wx+b,对标量z调用backward()方法,此时无需对backward()传入参数。以下是实现自动求导的主要步骤:
- 定义叶子节点及算子节点
import torch
# 定义输入张量
x = torch.Tensor([2])
# 初始化权重参数w 偏移量b 并设置require_grad的属性为True,为自动求导
w = torch.randn(1,requires_grad=True)
b = torch.randn(1,requires_grad=True)
# 实现前向传播
y = torch.mul(w,x)
z = torch.add(y,b)
# 查看x,w,b叶子节点的requires_grad属性
print("x,w,b的requires_grad属性分别为:{},{},{}".format(x.requires_grad,w.requires_grad,b.requires_grad))
# 运行结果:x,w,b的requires_grad属性分别为:False,True,True
- 查看叶子节点、非叶子节点的其他属性
# 查看叶子节点、非叶子节点的其他属性
print("y,z的requires_grad属性分别为:{},{}".format(y.requires_grad,z.requires_grad))
# 查看各节点是否为叶子节点
print("x,w,b,y,z的是否为叶子节点{},{},{},{},{}".format(x.is_leaf,w.is_leaf,b.is_leaf,y.is_leaf,z.is_leaf))
# 查看叶子节点的grad_fn属性
print("x,w,b的grad_fn属性:{},{},{}".format(x.grad_fn,w.grad_fn,b.grad_fn))
# 查看非叶子节点的grad_fn属性
print("y,z的grad_fn属性:{},{}".format(y.grad_fn,z.grad_fn))
- 自动求导,实现梯度反向传播
z.backward()
# 查看叶子节点的梯度,x是叶子节点,但他无需求导,所以其梯度为none
print("w,b的梯度分别为{},{},{}".format(w.grad,b.grad,x.grad))
# 查看非叶子节点的梯度,执行backward后,会自动清空
print("y,z的梯度分别为{},{}".format(y.grad,z.grad))
# 输出为
#x,w,b,y,z的是否为叶子节点True,True,True,False,False
#x,w,b的grad_fn属性:None,None,None
#y,z的grad_fn属性:<MulBackward0 object at 0x000002545C745BE0>,<AddBackward0 object at 0x000002545C745C88>
#w,b的梯度分别为tensor([2.]),tensor([1.]),None
#y,z的梯度分别为None,None