:label:sec_autograd
正如 :numref:sec_calculus
中所说,求导是几乎所有深度学习优化算法的关键步骤。 虽然求导的计算很简单,只需要一些基本的微积分。 但对于复杂的模型,手工进行更新是一件很痛苦的事情(而且经常容易出错)。
深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。
实际中,根据设计好的模型,系统会构建一个计算图(computational graph), 来跟踪计算是哪些数据通过哪些操作组合起来产生输出。 自动微分使系统能够随后反向传播梯度。
这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。
一个简单的例子
作为一个演示例子,(假设我们想对函数𝑦=2𝑥⊤𝑥关于列向量𝑥求导)。
首先,我们创建变量x
并为其分配一个初始值。
[在我们计算𝑦关于𝑥的梯度之前,需要一个地方来存储梯度。]
重要的是,我们不会在每次对一个参数求导时都分配新的内存。 因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。
注意,一个标量函数关于向量𝑥的梯度是向量,并且与𝑥具有相同的形状。
现在计算𝑦
x
是一个长度为4的向量,计算x
和x
的点积,得到了我们赋值给y
的标量输出。
接下来,[通过调用反向传播函数来自动计算y
关于x
每个分量的梯度],并打印这些梯度。
函数𝑦=2𝑥⊤𝑥关于𝑥的梯度应为4𝑥。 让我们快速验证这个梯度是否计算正确。
[现在计算x
的另一个函数。]
非标量变量的反向传播
当y
不是标量时,向量y
关于向量x
的导数的最自然解释是一个矩阵。
对于高阶和高维的y
和x
,求导的结果可以是一个高阶张量。
然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括[深度学习中]), 但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。
这里,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。
分离计算
有时,我们希望[将某些计算移动到记录的计算图之外]。
例如,假设y
是作为x
的函数计算的,而z
则是作为y
和x
的函数计算的。 想象一下,我们想计算z
关于x
的梯度,但由于某种原因,希望将y
视为一个常数, 并且只考虑到x
在y
被计算后发挥的作用。
这里可以分离y
来返回一个新变量u(.detach())
,该变量与y
具有相同的值, 但丢弃计算图中如何计算y
的任何信息。 换句话说,梯度不会向后流经u
到x
。
因此,下面的反向传播函数计算z=u*x
关于x
的偏导数,同时将u
作为常数处理, 而不是z=x*x*x
关于x
的偏导数。
由于记录了y
的计算结果,我们可以随后在y
上调用反向传播, 得到y=x*x
关于的x
的导数,即2*x
。
y依然是关于x的函数
Python控制流的梯度计算
使用自动微分的一个好处是: [即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到的变量的梯度]。
在下面的代码中,while
循环的迭代次数和if
语句的结果都取决于输入a
的值。
让我们计算梯度。
我们现在可以分析上面定义的f
函数。 请注意,它在其输入a
中是分段线性的。 换言之,对于任何a
,存在某个常量标量k
,使得f(a)=k*a
,其中k
的值取决于输入a
,因此可以用d/a
验证梯度是否正确。
小结
- 深度学习框架可以自动计算导数:我们首先将梯度附加到想要对其计算偏导数的变量上,然后记录目标值的计算,执行它的反向传播函数,并访问得到的梯度。
练习
1.为什么计算二阶导数比一阶导数的开销要更大?
- 计算复杂度:二阶导数的计算需要在一阶导数的基础上再次进行求导,这意味着需要对已经求导过的函数再次应用求导规则。这个过程涉及到更多的数学运算,因此计算量更大。
- 多次函数求值:在某些数值方法中,如中心差分法,计算二阶导数可能需要在同一个点附近计算多个函数值。
- 数值稳定性:在数值计算中,二阶导数的计算可能会引入更多的舍入误差,因为涉及到更多的浮点运算。此外,当分母变得很小时,如在中心差分法中的 ( h^2 ),可能会导致数值不稳定。
- 反向传播的复杂性:在机器学习中,尤其是在使用神经网络时,计算二阶导数通常涉及到反向传播算法的二次运行。这不仅增加了计算量,还可能导致更复杂的控制流和数据依赖关系。
2.在运行反向传播函数之后,立即再次运行它,看看会发生什么。
在计算梯度时,试图对已经释放的图进行第二次反向传播。
3.在控制流的例子中,我们计算d
关于a
的导数,如果将变量a
更改为随机向量或矩阵,会发生什么?
会报错
错误原因是在计算梯度时,输出值不是一个标量。
解决方法为尝试将输出值转换为标量,例如通过取平均值或求和