问题提出

计算导数是训练深度学习网络过程中优化参数的关键步骤,现代深度学习框架都提供了自动微分工具(autograd),来避免手动计算过程中可能会出现的问题。

当通过每个连续的函数传递数据时,框架会构建一个计算图来跟踪每个值如何依赖于其他值。为了计算导数,自动微分通过应用链式法则通过该图向后工作。 以这种方式应用链式法则的计算算法称为反向传播(backpropagation)。

理解自动微分的过程是后续学习优化算法优化参数过程的基础,因此记录以下笔记,结合简单的实例和背后的高数原理来理解微分过程。

实例

接下来的微分计算都基于torch包。

1
import torch

首先我们定义一个矢量xx,它包含4个变量,数值分别是0、1、2、3。这里定义y=x2y=x^2

1
2
x = torch.arange(4.0, requires_grad = True)
y = x * x

此时查看xxyy的对应值,得到如下结果:

1
2
3
4
>>> x
tensor([0., 1., 2., 3.], requires_grad=True)
>>> y
tensor([0., 1., 4., 9.], grad_fn=<MulBackward0>)

这一步中,我们假设张量xx中的4个元素分别为x1x_1x2x_2x3x_3x4x_4,对应的yy的4个元素分别为y1y_1y2y_2y3y_3y4y_4,那么yy的每个元素就可以表示为:

y1=x1x1y2=x2x2y3=x3x3y4=x4x4y_1=x_1\cdot x_1\\ y_2=x_2\cdot x_2\\ y_3=x_3\cdot x_3\\ y_4=x_4\cdot x_4\\

到这一步,还不能对yy直接进行反向传播操作。yy 是一个非标量张量,我们只定义了xiyix_i\rightarrow y_i的映射关系,现在yy并不是一个单个值。只有当yy是一个标量时,才能进一步执行梯度计算。

举例说明:不如假设xix_i的含义为第ii个正方形的边长,那么yiy_i就是第ii个正方形的面积,yy此时是包含了4个正方形面积值的张量。没有xyx\rightarrow y的映射定义,我们无法继续计算梯度。

1
2
3
4
5
>>> y.backward()
Traceback (most recent call last):
...
...
RuntimeError: grad can be implicitly created only for scalar outputs

下一步,我们以常用的meanmax函数为例,解释微分过程。

Mean

调用mean()方法后,才能获得最终的输出标量值yy。以刚才的假设为例,那么这里要预测的yy的意义就是求4个正方形的平均面积:

y=(y1+y2+y3+y4)/4=(x12+x22+x32+x42)/4y = (y_1+y_2+y_3+y_4)/4=(x_1^2+x_2^2+x_3^2+x_4^2)/4

现在根据链式法则,我们可以对yy相对于xx中的每一个元素求偏导:

yx1=yy1y1x1=142x1=12x1yx2=yy2y2x2=142x2=12x2yx3=yy3y3x3=142x3=12x3yx4=yy4y4x4=142x4=12x4\frac{\partial y}{\partial x_1}=\frac{\partial y}{\partial y_1}\cdot \frac{\partial y_1}{\partial x_1} = \frac{1}{4}\cdot 2x_1=\frac{1}{2}x_1\\ \frac{\partial y}{\partial x_2}=\frac{\partial y}{\partial y_2}\cdot \frac{\partial y_2}{\partial x_2} = \frac{1}{4}\cdot 2x_2=\frac{1}{2}x_2\\ \frac{\partial y}{\partial x_3}=\frac{\partial y}{\partial y_3}\cdot \frac{\partial y_3}{\partial x_3} = \frac{1}{4}\cdot 2x_3=\frac{1}{2}x_3\\ \frac{\partial y}{\partial x_4}=\frac{\partial y}{\partial y_4}\cdot \frac{\partial y_4}{\partial x_4} = \frac{1}{4}\cdot 2x_4=\frac{1}{2}x_4\\

将之前声明的xx元素值带入,就能获得对应的xx梯度。

1
2
3
>>> y.mean().backward()
>>> x.grad
tensor([0.0000, 0.5000, 1.0000, 1.5000])

Max

同理,调用max()方法后,我们获得了一个最终输出标量yy。还是以刚才的假设为例,这里要预测的yy的意义变成了求4个正方形中的面积最大值。而与刚才的mean()方法不同,并不是所有的变量都参与了最终yy的计算。由于我们要计算最大值,需要知道最大值所在的位置,因此在计算最大值的同时也会计算相应的索引。这个索引值会保留在 PyTorch 的计算图中,以便在反向传播时计算梯度。

max() 操作中由于涉及到索引操作,即找到最大值所在的位置。在反向传播时,只有最大值所在位置对应的元素会接收到非零梯度,而其他元素的梯度会为零。这个结果反映了在最大值操作中只有最大值位置的元素对损失函数有影响,而其他元素对损失函数的贡献为零。

因此,计算如下:

y=max(y1, y2, y3, y4)=y4y=max(y_1,~y_2,~y_3,~y_4)=y_4

于是偏导计算如下所示:

yx1=yy1y1x1=02x1=0yx2=yy2y2x2=02x2=0yx3=yy3y3x3=02x3=0yx4=yy4y4x4=12x4=2x4\frac{\partial y}{\partial x_1}=\frac{\partial y}{\partial y_1}\cdot \frac{\partial y_1}{\partial x_1} = 0\cdot 2x_1=0\\ \frac{\partial y}{\partial x_2}=\frac{\partial y}{\partial y_2}\cdot \frac{\partial y_2}{\partial x_2} = 0\cdot 2x_2=0\\ \frac{\partial y}{\partial x_3}=\frac{\partial y}{\partial y_3}\cdot \frac{\partial y_3}{\partial x_3} = 0\cdot 2x_3=0\\ \frac{\partial y}{\partial x_4}=\frac{\partial y}{\partial y_4}\cdot \frac{\partial y_4}{\partial x_4} = 1\cdot 2x_4=2x_4\\

最终我们获得xx梯度:

1
2
3
>>> y.max().backward()
>>> x.grad
tensor([0., 0., 0., 6.])