Implement a Deep Learning Framework in Pure Python

I: Preliminaries.

Posted by Xiaozhe Yao on July 14, 2020

预备知识

本节将包括对卷积神经网络、梯度下降法、反向传播的初步介绍。同时我们也将会详细的推导我们本节最重要的结论:矩阵线性变换的导数。由于本文最初是英文写成,可能会有一些翻译腔,请谅解。

卷积神经网络 卷积神经网络是被广泛使用的一类神经网络。在一个卷积神经网络中,通常存在卷积层(Convolution Layer),池化层(Pooling Layer)和全连接层(Fully Connected Layer)。前两者用于提取图像的特征,全连接层用于实现一个分类器。同时,卷积层和全连接层含有需要学习的权重(weight)。这个权重会在训练过程中不断被更新,从而实现提取特征和分类的效果。

梯度下降法 在训练一个神经网络的过程中,我们实际上是希望寻找到一个对我们的输入的映射 $F(x)$,使得它与真实值之间的差异最小,即 $y-F(x)$ 最小。尽管我们知道寻找一个函数的最小值可以通过求解其导数,然后使得导数等于 $0$ 来实现,这种方法也不太适用于我们的神经网络中,因为神经网络往往会成为一个非线性、非凸的函数,我们很难直接求导出它的导数并让导数等于$0$。在实际中,我们主要是通过梯度下降法来实现求解其极小值。我们假设使用 $f(x)=y-F(x)$ 来衡量预测值 $F(x)$ 和真实值 $y$ 之间的差异,之后首先随机初始化 $F(x)$(假设此时 $F$ 中的参数为 $a$),计算其函数值相对于参数 $a$ 的梯度 $\nabla_a f$。由于梯度是函数上升的最快方向,我们可以用我们的初始参数减去梯度,即 $a^{new}=a-\nabla_a f$ 来得到一个新的参数。之后我们可以重复这个过程,直到这个差异 $f$ 达到一个我们满意的较小值。在实践中,我们还会在这个梯度前面乘以一个常数 $\lambda$,用于表示梯度的下降速度。这个常数就被称作是 学习率 (Learning Rate)

反向传播 根据上文提到的梯度下降法的原理,我们需要求解损失函数相对于神经网络中每一层的参数 $w_i$ 的导数,即 $\frac{\partial l}{\partial w_i}$ 或 $\nabla_{w_i}l$。我们通过反向传播来逐层求解。对于一个神经网络来说,我们至少有下列几个已知的结论:

  • 第 $i$ 层的输出是第 $i+1$ 层的输入,即 $y_i=x_{i+1}$,或者说 $x_{i}=y_{i-1}$。根据这个结论,我们会有 $\frac{\partial l}{\partial y_{i-1}}=\frac{\partial l}{\partial x_i}$。
  • 我们在每一层中,除了求得损失函数相对于参数 $w$ 的导数外,还可以求得相对于其输入的导数。也就是说,$\frac{\partial l}{\partial x_i}$ 是已知的。
  • 根据链式法则,我们会有 $\frac{\partial l}{\partial w_i}=\frac{\partial l}{\partial y_i}\frac{\partial y_i}{\partial w_i}$

根据以上两个结论,我们只要把 $\frac{\partial l}{\partial x_i}$ 传递给上一层,之后上一层就可以利用收到的这个梯度,根据链式法则计算这一层中的 $\frac{\partial l}{\partial w_i}$。这就是反向传播的大致思路:我们把每一层中,损失函数相对于输入的梯度传递给上一层,之后逐层就可以计算损失函数相对于参数的导数,此时再使用梯度下降法来更新参数,就可以得到新的参数。

线性变换的求导 为了精简,这个小标题是不够确切的。确切来说,我们想要首先解决这样一个问题:

已知两个函数,$f(Y): \mathbb{R}^{m\times n}\to\mathbb{R}$,即我们不知道该函数的解析式,只知道它把一个 $m\times n$ 的实矩阵变换为一个实数。$g(X): \mathbb{R}^{p\times n}\to\mathbb{R}^{m\times n}, Y=g(X)=AX+B$,其中 $A\in\mathbb{R}^{m\times p}, B\in\mathbb{R}^{m\times n}$。也就是说 $g(X)$ 把一个 $m\times p$ 的实矩阵通过线性变换 $AX+B$ 变换为一个 $m\times n$ 的矩阵。我们现在需要求解 $f$ 相对于 $X$ 的导数 $\frac{\partial f}{\partial X}$。

在正式求解之前,我们先大致思考一下这个问题对我们的重要性:在这里的 $f(Y)$ 负责把我们的矩阵变换为一个实数,这基本上就是我们损失函数的特征。而 $g(X)$ 则是矩阵的相乘,可以视作是神经网络中间层的变换。例如,全连接层就可以被视作是一个矩阵的相乘。实际上,卷积层也可以通过矩阵相乘来实现(下一节会提供例子说明)。因此,解决了这个问题,我们就可以轻松地把这个结论应用在每一层,避免重复劳动。熟悉矩阵求导的同学可以跳过以下的说明,对具体推导不感兴趣的同学也可以跳过以下证明,直达最后一点的结论。

  • 在 $x$点,假设我们有两个中间变量 $u=\phi(x)$ 和 $v=\psi(x)$。这两个变量均有相对于 $x$ 的导数定义,即 $\frac{\partial u}{\partial x}$,$\frac{\partial v}{\partial x}$ 均存在。另外还有一个函数 $f(u,v)$ 是由 $u, v$ 共同定义的。此时如果我们想要求解 $\frac{\partial y}{\partial x}$,即需要同时考虑这两个中间变量。我们有 $\frac{\partial y}{\partial x}=\frac{\partial y}{\partial u}\frac{\partial u}{\partial x}+\frac{\partial y}{\partial v}\frac{\partial v}{\partial x}$。在这个矩阵的问题中,如果我们打算求解实值函数 $f$ 相对于 $x_{ij}$ 的导数,即$\frac{\partial f}{\partial x_{ij}}$,我们的中间矩阵 $Y$ 中就有可能有很多个这样的中间变量 $y_{kl}$,因此我们有 $\frac{\partial f}{\partial x_{ij}}=\sum_{kl}\frac{\partial f}{\partial y_{kl}}\frac{\partial y_{kl}}{\partial x_{ij}}$。
  • 我们用 $a_{ij}$ 和 $b_{ij}$ 来表示 $A$ 和 $B$ 中的元素,那么我们不难得到 $y_{kl}=\sum_{s}a_{ks}x_{sl}+b_{kl}$,因此我们就有 $\frac{\partial y_{kl}}{\partial x_{ij}}=\frac{\partial \sum_{s}a_{ks}x_{sl}}{\partial x_{ij}}=\frac{\partial a_{ki}x_{il}}{\partial x_{ij}}$。此时我们发现,只有当 $l=j$时,这个导数的值才不为 $0$。我们用一个特殊记号 $\delta_{lj}$ 来表示:若$l=j$,则$\delta_{lj}=1$,否则 $\delta_{lj}=0$。(这个记号也叫做 Kronecker Delta)那么我们求出的结果就可以写作:$\frac{\partial y_{kl}}{\partial x_{ij}}=\frac{\partial a_{ki}x_{il}}{\partial x_{ij}}\delta_{lj}=a_{ki}\delta_{lj}$。
  • 把第二个结果带入第一点,我们可以得到 $\frac{\partial f}{\partial x_{ij}}=\sum_{kl}\frac{\partial f}{\partial y_{kl}}\frac{\partial y_{kl}}{\partial x_{ij}}=\sum_{kl}\frac{\partial f}{\partial y_{kl}}a_{ki}\delta_{lj}$。这个时候,我们发现,只有 $y_{kj}$ 被留下来,其他的都会是 $0$,所以我们可以继续简化得到 $\frac{\partial f}{\partial x_{ij}}=\sum_{kl}\frac{\partial f}{\partial y_{kl}}a_{ki}\delta_{lj}=\sum_{k}\frac{\partial f}{\partial y_{kj}}a_{ki}$。在这个式子中,我们知道 $a_{ki}$ 是位于矩阵 $A$ 的第 $i$ 列,或者说 $A^T$的第 $i$ 行。同时,$\frac{\partial f}{\partial y_{kj}}$ 是位于矩阵$\frac{\partial f}{\partial Y}$的第 $j$ 列。此时,我们会发现,这是一个矩阵相乘的操作,也就是说 $\frac{\partial f}{\partial X}=A^T\frac{\partial f}{\partial Y}=A^T\nabla_Y f$。
  • 类似地,我们也可以推导出右乘的情形,即 $Y=g(X)=XC+D$。我们有 $Y^T=(XC+D)^T=C^TX^T+D^T$。之后利用上面得到的结论,就会有 $\nabla_{X^T}f=(C^T)^T\nabla_{Y^T}f=C\nabla_{Y^T}f$。因此,$\nabla_{X}f=(\nabla_{X^T}f)^T=(C\nabla_{Y^T}f)^T=(\nabla_{Y}f)C^T$。
  • 总结,我们可以得到两个结论:
    • 若$g(X)=AX+B$(左乘),则 $\frac{\partial f}{\partial X}=A^T\nabla_Y f$。
    • 若$g(X)=XA+B$(右乘),则 $\frac{\partial f}{\partial X}=(\nabla_Y f)A^T$