深度学习中的导数
在深度学习的训练过程中,我们经常需要计算导数。导数是函数在某一点的切线斜率,它可以告诉我们函数在某一点的变化率。在深度学习中,我们经常需要计算损失函数对模型参数的导数,以便调整模型参数,使得模型的预测值更接近真实值。在这个notebook中,我们将总结深度学习中常用的一些导数计算规则。在此之前,我们应该具有微积分的基本知识,基本掌握简单的标量求导和链式法则。主要目标是要能够清楚求导过程中的张量形状变化。
目录
1 向量导数
在深度学习中,我们最常使用的数据结构就是张量,因此,我们需要从标量的导数去泛化,推广到向量和矩阵的导数,进而理解张量的导数。在这里,我们总结一些常用的向量导数计算规则,也作为张量导数的引入。
1.1 向量与标量间的导数
-
标量对向量求导
设 y y y是一个标量, x x x是一个向量,我们规定 y y y对 x x x的导数是一个行向量,它的第 i i i个元素是 y y y对 x i x_i xi的偏导数(具体原因到后面就可以理解了)。如图:
-
** 向量对标量求导**
设 y y y是一个向量, x x x是一个标量,我们规定 y y y对 x x x的导数是一个列向量,它的第 i i i个元素是 y i y_i yi对 x x x的偏导数。如图:
1.2 向量与向量、矩阵间的导数
- ** 向量对向量求导**
设 y y y是一个向量, x x x是一个向量,我们规定 y y y对 x x x的导数是一个矩阵,它的第 i i i行第 j j j列的元素是 y i y_i yi对 x j x_j xj的偏导数。
我们可以把向量对向量的导数先看作是标量对向量的导数,即 y y y的各个分量对 x x x求导,这样我们就先得到了一个列向量。该列向量的各个元素再对 x x x求导,由于这是标量对向量的导数,所以每一行就得到了一个行向量,把这些行向量排列起来,就得到了一个矩阵。 - 向量对矩阵求导、矩阵对矩阵求导
可以想象,我们会得到一个高阶张量。
2 向量链式法则
在深度学习中,我们经常需要计算复合函数的导数,这就需要用到链式法则。在这里,我们总结一下向量链式法则。
其实向量的链式法则和标量的链式法则是一样的,只是在求导的过程中不再是从前的标量对标量的求导,而是出现了有向量参与的求导,即链式求导法则同样适用于向量,只是需要考虑向量与标量和向量间的求导,我们可以举一个例子来说明:
设
x
,
w
x,w
x,w
∈
R
n
\in \mathbb{R}^{n}
∈Rn
y
y
y
∈
R
,
z
=
(
<
x
,
w
>
−
y
)
2
\in \mathbb{R},z=(<x,w>-y)^{2}
∈R,z=(<x,w>−y)2,我们要求
∂
z
∂
x
\frac{\partial z}{\partial x}
∂x∂z,这里
<
x
,
w
>
<x,w>
<x,w>表示
x
x
x和
w
w
w的内积。
我们可以先做一个拆解,设
a
=
<
x
,
w
>
,
b
=
a
−
y
,
z
=
b
2
a=<x,w>,b=a-y,z=b^{2}
a=<x,w>,b=a−y,z=b2,则
∂
z
∂
x
\frac{\partial z}{\partial x}
∂x∂z=
∂
z
∂
b
∂
b
∂
a
∂
a
∂
x
\frac{\partial z}{\partial b}\frac{\partial b}{\partial a}\frac{\partial a}{\partial x}
∂b∂z∂a∂b∂x∂a,我们可以逐步求解,
∂
z
∂
=
b
=
2
b
,
∂
b
∂
a
=
1
,
∂
a
∂
x
=
w
\frac{\partial z}{\partial =b}=2b,\frac{\partial b}{\partial a}=1,\frac{\partial a}{\partial x}=w
∂=b∂z=2b,∂a∂b=1,∂x∂a=w,所以
∂
z
∂
x
=
2
b
∗
w
=
2
(
<
x
,
w
>
−
y
)
w
⊤
\frac{\partial z}{\partial x}=2b*w=2(<x,w>-y)w^\top
∂x∂z=2b∗w=2(<x,w>−y)w⊤,这就是我们要求的结果。
最后, < x , w > − y <x,w>-y <x,w>−y是一个标量, w ⊤ w^\top w⊤是一个行向量,所以 2 ( < x , w > − y ) w ⊤ 2(<x,w>-y)w^\top 2(<x,w>−y)w⊤是一个行向量,即 z z z对 x x x的导数是一个行向量。这也照应了我们之前所说的求导规则
3 自动求导
3.1 计算图
在深度学习中,我们经常使用计算图来表示计算过程。计算图是一种直观的表示计算过程的方法,它将计算过程表示为一个有向无环图。在计算图中,节点表示变量或者操作,边表示变量或者操作之间的依赖关系。计算图中的每一条路径都代表了一种计算顺序,我们可以根据计算图来进行自动求导。
3.1.1 将计算过程转化为计算图
我们可以将一个计算过程转化为一个计算图。例如,我们可以将
z
=
(
x
+
y
)
∗
y
z=(x+y)*y
z=(x+y)∗y转化为如下计算图:
在此之前,如此简单的计算我们完全可以直接进行,那为什么要转化为计算图呢?这在正向传播(计算)中完全看不到优势所在!是的,它的优势体现在反向传播(求导)中,我们可以根据计算图来进行自动求导。这点我们马上就会提到,现在让我们先根据这张计算图来说明一下计算图的特点
- 计算图中的节点表示输入变量或者操作符,边表示变量或者操作之间的依赖关系。正如这幅图中,处理几个起始节点外,其余节点均记录了运算符(操作),而边则体现了变量输入、输出何计算流程。
- 计算图进行的是局部计算。对于‘+’运算符而言,他接受两个变量,进行加运算;乘法运算符也是如此。 z = 2 x + y z=2x+y z=2x+y则是2与x做乘法,然后与y做加法,同样是局部计算。这就使节点特征更为突出。
3.1.2 链式法则和利用计算图进行自动求导
在前面我们就已经提到了,我们使用计算图的最大的原因就是,可以通过它的反向传播进行快速求导,这里我们就来看一下链式法则是如何在计算图中进行的。
还是之前的例子:
z
=
(
x
+
y
)
∗
y
z=(x+y)*y
z=(x+y)∗y,我们想要求
∂
z
∂
x
\frac{\partial z}{\partial x}
∂x∂z和
∂
z
∂
y
\frac{\partial z}{\partial y}
∂y∂z。我们可以使用计算图来进行求导。如下图所示:
根据链式法则,我们可以得到
∂
z
∂
x
=
∂
z
∂
t
∂
t
∂
x
\frac{\partial z}{\partial x}=\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}
∂x∂z=∂t∂z∂x∂t,
∂
z
∂
y
=
∂
z
∂
t
∂
t
∂
y
+
∂
z
∂
y
\frac{\partial z}{\partial y}=\frac{\partial z}{\partial t}\frac{\partial t}{\partial y}+\frac{\partial z}{\partial y}
∂y∂z=∂t∂z∂y∂t+∂y∂z
我们把链式法则反映在计算图上,我们发现从最开始的输入,经过每一个节点,传出的信号会乘以相应的系数。那最后的乘法节点来说,对于传来的
p
a
r
t
i
a
l
z
∂
z
\frac{partial z}{\partial z}
∂zpartialz,要传到y路径,乘以\frac{\partial z}{\partial y}=t,传到t路径,乘以\frac{\partial z}{\partial t}=y,即乘法节点,反向传播的系数和正向传播的输入恰恰相反;而对于加法节点,反向传播的系数全为1.那么,只要我们在正向传播时存储了必要的信息,那么在反向传播时我们只需要调取就可以自动完成反向传播的过程。这就是自动求导的原理。我们可以举一个例子来说明:
首先,我们先定义乘法和加法节点的正向和反向传播:
class AddLayer:
"""
一个加法节点类
"""
def __init__(self):
#加法节点不需要存储信息即可完成反向传播
pass
def forward(self,x,y):
return x+y
def backward(self,dz):
dx=dz*1
dy=dz*1
return dx,dy
'''定义加法节点结束'''
class MulLayer:
"""
一个乘法节点类
"""
def __init__(self):
self.x=None
self.y=None
def forward(self,x,y):
#乘法节点反向传播需要存储正向传播的信息
self.x=x
self.y=y
return x*y
def backward(self,dz):
dx=dz*self.y
dy=dz*self.x
return dx,dy
'''定义乘法节点结束'''
'定义乘法节点结束'
当我们已经完成定义了加法和乘法节点的正向和反向传播之后,我们就可以针对一个简单的问题开始构建计算图,然后进行自动求导了。
有这样一个问题,一个苹果的价格为100元,数量为2,一个橘子的价格是150元,数量为3,消费税为10%,请计算最终的价格。我们可以构建如下的计算图:
利用上述所建立的节点来进行计算
apple_price=100
apple_num=2
orange_price=150
orange_num=3
tax=1.1
mul_apple_layer=MulLayer()
mul_orange_layer=MulLayer()
add_apple_orange_layer=AddLayer()
mul_tax_layer=MulLayer()
#正向传播
apple_price_total=mul_apple_layer.forward(apple_price,apple_num)
orange_price_total=mul_orange_layer.forward(orange_price,orange_num)
all_price_before_tax=add_apple_orange_layer.forward(apple_price_total,orange_price_total)
all_price=mul_tax_layer.forward(all_price_before_tax,tax)
print(all_price)
#反向传播
dall_price=1
dall_price_before_tax,dtax=mul_tax_layer.backward(dall_price)
dapple_price_total,dorange_price_total=add_apple_orange_layer.backward(dall_price_before_tax)
dapple_price,dapple_num=mul_apple_layer.backward(dapple_price_total)
dorange_price,dorange_num=mul_orange_layer.backward(dorange_price_total)
print(dapple_price,dapple_num,dorange_price,dorange_num,dtax)
上面的例子让我们明白,我们可以定义一些列的操作层,比如乘、加、乘方等,我们可以通过这些运算的反向传播系数的特点来设计层正向计算和反向传播的过程,这样我们就可以通过计算图来进行自动求导了。这也是深度学习中的自动求导的原理。
在深度学习中,求梯度主要是为了得到更小的损失和函数,来不断更新我们的权重系数,而在由权重系数计算得到损失函数的过程中,会有很多复杂的运算或函数,比如relu激活函数、sigmoid激活函数等等,对这些函数运算我们也会定义其正向,反向传播,并且大多是以张量的形式。在这里我们只是用简单的例子来说明深度学习的框架下自动求导的原理。在实际的深度学习事件中,我们可以直接调用框架的功能,来实现我们的求梯度的需求
3.2 PyTorch中的自动求导
在PyTorch中,我们可以使用autograd来进行自动求导。autograd是PyTorch中用于自动求导的一个重要模块,它能够根据输入和前向传播过程自动构建计算图,并执行反向传播。在反向传播过程中,autograd能够自动计算出梯度。在PyTorch中,我们可以通过requires_grad=True来告诉PyTorch需要对该张量进行自动求导。在张量进行运算之后,我们可以调用.backward()来进行反向传播,计算出梯度。下面我们通过一个例子来说明:
import torch
#定义一个张量,并告诉PyTorch需要对该张量进行自动求导
x=torch.arange(4.0,requires_grad=True)
#定义一个计算过程(正向传播)
y=x**2
print(y)
# y.backward()
# print(x.grad) 是错误的,因为在pytorch中自动求导机制要求反向传播的目标(即损失函数)必须是标量,而y是一个向量,所以我们无法对y进行反向传播。
z=y.sum()
#反向传播,计算梯度
z.backward()
#打印梯度
print(x.grad)
至此,我们已经了解了深度学习中的关于导数的基本知识以及自动求导机制,和利用pytorch进行自动求导