我们先简述一下深度学习(Deep Learning) 起起落落的发展史——1958 年首先有人提出了感知机算法(Perceptron), 大家逐渐对这种线性模型变得十分感兴趣;在 1969 年有学者指出了感知机算法的局限性,大家意识到这些点后希望开始破灭。1980s 年代有人提出了多层感知机(Multi-layer Perceptron), 这种方法其实和现在的深度神经网络(Deep Neural Network) 没有太大的区别。1986 年 Hinton 提出了反向传播算法(Backpropagation), 但当时的问题在于通常网络超过 3 层就训练不出好的结果。1989 年 Hornik 提出,其实一个隐藏层的感知机模型就能够拟合任何可能的函数,因此没有必要叠很多层。同一阶段支持向量机(Support Vector Machine, SVM) 算法由于惊人的表现而备受青睐,而论文中带有多层感知机或是神经网络就好像带脏话一样,一定会被人拒绝。

由于臭名远扬了,所以有人开始换用“深度学习”这一说法,2006 年 Hinton 提出的受限玻尔兹曼机(Restricted Boltzmann Machine, RBM) 参数初始化被认为是一种突破,当时也被作为深度学习与多层感知机的区别。但受限玻尔兹曼机的理论很复杂,属于图模型(Graph Model), 通过多次实验后大家发现它的效果没有那么理想,这点 Hinton 在某篇论文也承认过。现在来看,RBM 的论文最主要的贡献是将神经网络又重新带回了人们的视野,起到了抛砖引玉的效果。但是如果训练很多层的神经网络,需要大量的计算资源,时间和资金成本非常之高。

2009 年人们发现可以使用 GPU 加速运算与训练,可能原本需要一周时间的实验,可以缩短至几个小时内完成。而在 2012 年 Hinton 的学生Alex Krizhevsky 在寝室用 GPU 搭出了后来大名鼎鼎的 AlexNet 模型,一举摘下了视觉领域竞赛 ILSVRC 2012 的桂冠,在百万量级的ImageNet 数据集上,效果大幅度超过传统的方法,从传统的 70% 多提升到 80% 多。从此开始,深度学习走进了更多人的视野。

第一步:神经网络模型与结构

我们知道机器学习的三个步骤是:

  1. 定义函数集合 $\{ f_1,f_2,\ldots \}$,即“模型”;
  2. 评价函数的好坏,通常以损失的形式来衡量;
  3. 选择“最好”的函数 $f^{\ast}$, 即优化损失目标。

而深度学习相较于机器学习的变化,其实就是使用了神经网络(Neural Network) 模型——将单个的 Logistic 回归单元看作是一个神经元(Neural), 使用多个神经元前后连接就会得到神经网络。如果使用不同的方法进行连接,就会得到不同的神经网络结构(Structure). 在 Logistic 回归单元中有着自己的权重(Weight) 和偏移(Bias), 所有的这些神经元中的权重和偏置,被称为网络的参数(Parameter).

注:Bias 的叫法很多,比如偏置、偏移、偏差等等,一般都直接使用其英文原词。

全连接前馈网络

最常见的将神经网络中的神经元连接起来的方式叫作全连接前馈网络(Fully Connect Feedforward Network), 也叫作深度前馈网络或全连接神经网络,将其中的神经元排成不同的层,每个神经元的 weight 和 bias 通过训练数据学得。如果已经知道了网络中所有的参数,则可以将整个神经网络看成是一个函数,输出是一个向量,输出也是一个向量(有时候只需要标量形式), 例如上图例子中的输入输出可以用下面的函数表示:

如果不知道神经网络中的参数,而只是定好了神经元的连接方式(即网络的结构), 那么该神经网络的模型已经给定了,其中不同的参数组合会形成不同的函数。这和之前的线性回归等模型类似,给定模型后,具体的函数功能由其中的参数来决定。

通常将全连接神经网络结构画成下面这样,有 $L$ 层(Layer) 的神经元,每层神经元的数量可以很多且不用相同。相邻层之间的神经元两两连接(名称中全连接的由来), 前一层神经元的输出会作为下一层神经元的输入。由于传递的方向是由后往前,因此是前馈网络。

整个网络需要一组输入 $x$, 对于第 $1$ 层来说,每个神经元就是输入特征的每一个维度,被叫作输入层(Input Layer). 而最后一层的神经元没有其它输出,因此它的输出就是整个神经网络的输出结果,被叫作输出层(Output Layer). 严格来说输入层并不是由神经元组成的层,但是我们将它看成是一层以方便理解。除了输入层和输出层,中间的层叫作隐藏层(Hidden Layer), 深度前馈网络即指有很多的隐藏层。

矩阵运算

继续使用前面的例子,在使用神经网络进行计算的时候,我们通常将里面的数值写成矩阵(Matirx) 形式. 第一层的两个神经元的权重分别是 $1$, $-2$, $-1$ 和 $1$, 可以将其排成一个权重矩阵

当输入 $1$ 和 $-1$ 参与运算时,其实就是计算 $1 \times 1 + -1 \times -2 = 3 $ 和 $1 \times -1 + -1 \times 1 = -2$. 我们可以将输入写成列向量的形式,整个计算变成了矩阵与向量相乘

接着加上对应的 bias 得到完整的线性计算部分

将线性计算的输出通过 Sigmoid 函数可以得到当前这个神经元的输出,这一步中类似 Sigmoid 函数的部分在神经网络中被称作激活函数(Activation Function), 事实上不一定要用 Sigmoid 函数,只是由于人们是从 Logistic 回归的思想过渡到神经网络模型,因此在一开始都使用 Sigmoid 函数作为激活函数。

你会发现用矩阵运算计算一层神经元的输入输出十分方便,假设我们将第 $1$ 层的 weight 集合起来当作一个矩阵 $W^1$, 将 bias 集合起来当作一个向量 $b^1$; 同理表示后面所有层的这些参数。我们将输入层的 $x_1,x_2,\ldots ,x_N$ 接起来用一个向量 $x$ 表示,输出层的 $y_1,y_2,\ldots ,y_M$ 用向量 $y$ 表示,中间得到的输出使用 $a_1,a_2, \ldots$ 向量表示,则整个计算过程如下:

只考虑整个神经网络,则可以看成一个函数 $y=f(x)$, 内部是一连串的矩阵运算,

使用 GPU 做矩阵运算比 CPU 快很多,因此可以起到加速的效果。

在输出层之前的这些计算,我们可以看作是对特征的提取,代替人类手工的特征工程或特征转化等行为。输出层将它的前一层的输入看作是原始输入特征 $x$ 经过一些复杂变换和处理后得到的新特征,而输出层起到了一个多分类器的作用。也即是说经过如此多隐藏层的转化,最开始的特征可能被转化成很好的特征,它们可以用一个简单的多分类器进行分类。也正是因为我们将输出层看成是要给多分类器,因此最后需要加上一个 Soft-max 函数,方便对输出进行表示。

手写数字举例

输入一张图片,输出其代表的数字。对机器来说,一张位图就是由多个像素组成的,假设每个像素的颜色可以用一个数值表示,那么图像则可以表示成像素矩阵,再将其拉伸排列成一维向量,作为神经网络的输入 $x$. 一个 $16 \times 16$ 的图片对机器来说就是一个 $256$ 维的向量,每一个像素对应一个维度。如果使用 Soft-max 函数作为输出,则代表着一个概率分布,假设输出是 $10$ 维的,则可以对应分类到数字 $0$ 至 $9$ 的概率。

要解决手写数字识别问题,我们需要找到一个函数映射输入到输出之间的关系,中间的这个函数模型就是神经网络——输入层 $256$ 维,输出层 $10$ 维,神经网络的结构定义了对应的函数集合,其中有些函数处理手写数字识别任务的效果比较好,有些比较差。接下来我们需要对神经网络模型进行一些设计,比如由几个隐藏层,每层有多少个神经元等等(除了层数和神经元个数,还有其他的形式,这个后面会提).

神经网络模型的设计会决定存在的所有函数的集合,如果结构设计不合理,可能找不到一个好的函数来解决最终的问题。这就好像用多项式模型来拟合数据点,如果模型设计错误会产生很大的偏差或者发生过拟合,离理想的函数会过于遥远。

机器学习由非深度学习变为深度学习,只是将要处理的核心问题变换了一种形式。对于非深度学习,更多的时候可能是在寻找一组好的特征,再扔进一些传统机器学习模型中训练。但使用深度学习方法,对特征的关键程度可能不是那么依赖,新的问题在于该如何去设计网络结构,即问题的核心由特征工程变成了网络结构设计。所以应当根据不同的问题情景,选择采用深度学习还是传统机器学习方法。

第二步:定义函数的好坏

我们继续以手写数字识别为例子,输入是手写数字的图片像素向量,通过神经网络后输出 $10$ 维预测向量 $y$, 其中每个元素 $y_i$ 作为预测的各个类别的概率。例如一张 “1” 的图片它的真实标签值 $\hat{y}$ 应该满足 $\hat{y}_1 = 1$, 其它值为 $0$. 想要定义函数的好坏,可以先计算单个样本预测值 $y$ 与真实值 $\hat{y}$ 之间的交叉熵代价(Cost), 这一点与多分类问题是一样的。

而在整个训练数据中有着 $N$ 个样本,要定义函数在整个训练数据上的好坏,就要将所有样本数据的交叉熵全部求和,作为总体损失 $L$:

接下来就需要找出一个最好的函数 $f^{\ast}$ , 或者说最好的网络参数 $\theta^{\ast}$ 来最小化总体损失 $L$.

第三步:找到最好的参数

我们可以发现神经网络模型的损失函数是可微分的,因此可以使用梯度下降来寻找最好的参数。步骤和线性回归梯度下降没什么区别,只是神经网络计算各个参数对损失函数的偏微分时更加复杂一些,具体如下图所示:

如果要推导并计算神经网络模型每个参数具体的偏微分 $\partial L / \partial w$ 会相当花时间,因此要使用更好的方法——反向传播。

反向传播算法

Learning representations by back-propagating errors. NATURE. 1986

反向传播算法并不是和梯度下降不同的另一种参数更新算法,而是适用于梯度下降中的一种高效的计算方式与思想。我们知道,在神经网络模型中可能有着非常多的参数:

我们选定初始的参数 $\theta^{0}$, 计算对损失函数的偏微分 $\nabla \mathrm{L}\left(\theta^{0}\right)$, 梯度实际上是向量

计算出梯度后,根据学习率 $\eta$ 对参数进行更新。不断重复这个过程…

由于参数量十分大,为了高效地计算梯度,需要使用反向传播(Backpropagation) 算法。反向传播使用到的核心数学技巧是链式法则(Chain Rule), 在微积分中应该讲过相关的知识:

我们知道,假设给定了神经网络的参数 $\theta$, 将一个训练数据 $x^n$ 丢进去后会输出预测值 $y^n$, 用 $C^n$ 代表 $y^n$ 和真实值 $\hat{y}^n$ 之间的距离,作为单个样本产生的代价(Cost). 对所有训练样本的 $C^n$ 求和,会得到总体损失

如果对某个参数 $w$ 做偏微分,会得到

接下来只需要关注某一个样本的偏微分 $\frac{\partial C^{n}(\theta)}{\partial w}$ 的计算,先考虑某一个神经元:

这是个在第 $1$ 层隐藏层的神经元,输入假设只有 $x_1$ 和 $x_2$, 根据链式法则可以得到:

线性计算得到的 $z$ 将被丢进激活函数,其中计算 $\frac{\partial z}{\partial w}$ 比较简单,被称为前向传导(Forward Pass); 而计算 $\frac{\partial C}{\partial z}$ 比较复杂,被称为反向传导(Backward Pass). 进行这样的区分是有原因的,下面进行解释。

前向传导过程

先看看对于这个神经元,如何计算 $\frac{\partial z}{\partial w}$, 已知 $z=x_{1} w_{1}+x_{2} w_{2}+b$, 则

你会发现规律,这一部分的偏微分 $\frac{\partial z}{\partial w}$ 等同于前面连接的神经元的输出(即自己的输入), 因此在神经网络进行前馈计算的时候,每个神经元的 $\frac{\partial z}{\partial w}$ 都可以直接知道:

反向传导过程

接下来考虑反向传导过程中的偏微分 $\frac{\partial C}{\partial z}$, 难点在于 $z$ 在当前神经元通过激活函数 $\sigma$ 后输出 $a$ 给后面的神经元,后面又要经过很多的计算才能够得到 $C$

假设这里使用的激活函数是 Sigmoid 函数,我们使用链式法则再进行拆解:

其中 $\frac{\partial a}{\partial z}=\sigma^{\prime}(z)$, 而输出 $a$ 会参与后面的运算,假设如上图所示,根据链式法则又有

而我们会发现上式中的偏微分 $\frac{\partial z^{\prime}}{\partial a}$ 和 $\frac{\partial z^{\prime \prime}}{\partial a}$ 来自前向传导过程,在这个例子中分别是 $w_3$ 和 $w_4$, 所以得到

观察这个式子会发现,可以想象成它其实等同于做了一个与当前神经元反向的运算,输入是后面神经元的 $z$ 对 $C$ 的偏微分,参数 $w_3$ 和 $w_4$ 在模型中是已知的,输出是当前的神经元的 $z$ 对 $C$ 的偏微分。而在实际的前馈运算过程中,$\sigma^{\prime}(z)$ 其实是一个常数,因为线性部分 $z$ 的计算完全取决于前向传导过程。

偏微分的递归情况

那么又该如何计算 $\frac{\partial C}{\partial z^{\prime}}$ 和 $\frac{\partial C}{\partial z^{\prime \prime}}$ 呢?存在两种情况。

情况一:这两项已经是整个神经网络的输出 $y_1$ 和 $y_2$, 则有

其中 $\frac{\partial y_{1}}{\partial z^{\prime}}$ 就是激活函数的微分(常数), 而 $\frac{\partial C}{\partial y_{1}}$ 取决于代价函数 $C$ 的定义形式,比如交叉熵或者平方误差。

情况二:如果这两项并不是最终的输出,而是位于隐藏层的中间,这也意味着在这些神经元的后面有着其它神经元。我们考虑 $z^{\prime}$ 后面的计算,假设有两个神经元,如下图所示:

不难发现,如果能够计算 $\frac{\partial C}{\partial z_{a}}$ 和 $\frac{\partial C}{\partial z_{b}}$, 就能计算出 $\frac{\partial C}{\partial z^{\prime}}$.

而之前我们发现,如果知道了 $\frac{\partial C}{\partial z^{\prime}}$ 和 $\frac{\partial C}{\partial z^{\prime \prime}}$, 则可以计算出 $\frac{\partial C}{\partial z}$.

也即是说,如果知道了当前 $L$ 层所有的 $\frac{\partial C}{\partial z^{L}}$, 就能够计算出第 $L-1$ 层的 $\frac{\partial C}{\partial z^{L-1}}$.

顺着这种递归的思想,追本溯源,情况二的偏微分计算一定会递归到情况一。因此当我们想要计算(对于某个样本) 模型中所有的神经元激活函数的输入 $z$ 对代价的偏微分 $\partial C / \partial z$ 时,都可以从最后的输出层开始计算,并且将每一步得到的偏微分反向传导。

全局视角与整体意义

由于矩阵运算可以提升效率,我们现在不考虑单个的神经元,而是单层的神经元。

我们知道神经网络模型可以看成一个函数 $y=f(x)$, 内部是一连串的矩阵运算,

输入层的特征向量 $x$ 看作是 $a^0$, 经过每一层神经元的线性计算得到 $z=Wa$, 再通过激活函数得到 $a=\sigma (z)$, 通过多层的计算最终得到输出层向量 $y$, 这就是前向传导的过程,通过矩阵运算一层层地计算出当前层所有神经元的 $z$ 和 $a$, 也即是说层与层的计算中只需要记住 $z$ 和 $a$, 以及权重矩阵 $W$ 和 bias 向量 $b$.

我们通过对损失的定义发现,可以使用梯度下降计算偏微分 $\frac{\partial L(\theta)}{\partial W}$ 来更新参数($\frac{\partial L(\theta)}{\partial b}$ 的计算过程与之类似). 但是如果要对每层的参数 $W$ 和 $b$ 都计算偏微分,会造成巨大的计算量和运算的冗余,因为我们发现这种偏微分的具体运算存在着递归的形式。你可以理解成,某 $l$ 层神经元如果要更新自己的参数 $W^l$, 会需要根据最终的损失计算偏微分,使用链式法则完整地展开,公式会极其复杂。

既然我们进行前馈运算的时候可以逐层进行计算,那为什么不将层与层之间的偏微分进行 $\frac{\partial C}{\partial z^l}$ 反向的传导,以保持这种递归关系呢?这样的话,每层参数的梯度计算就可以简化成:

其中有 $\frac{\partial z^l}{\partial W^l} = a^{l-1} $, $\frac{\partial a^l}{\partial z^l} = \sigma^{\prime} (z^l)$ 和 $\frac{\partial z^{l+1}}{\partial a^l} = W^{l+1}$, 整理可得

第二个式子不用再进一步递归展开,而 $a^{l-1}$, $\sigma ^{\prime} (z^l)$ 和 $W^{l+1}$ 在前向传导时已经得到。考虑到计算 $\frac{\partial C}{\partial y}$ 很方便,因此从最后的输出层开始计算偏微分,当前 $l$ 层要处理的任务仅仅是接受上一层传来的偏微分 $\frac{\partial C}{\partial z^{l+1}}$, 并计算出当前层的偏微分 $\frac{\partial C}{\partial z^{l}}$, 将其继续反向传导给第 $l-1$ 层进行计算。完整地进行一次反向传导后,每一层参数的偏微分都被计算出来,这时就可以进行所有参数的更新。递归式中很多已经计算过的偏微分被重复访问,这个方法可以减少许多的计算冗余,充分利用了每一步计算。

注意,每次的梯度下降前,都是经过一次前向传导计算出所有的 $a$, $z$, $W$, $b$ 和 $\sigma^{\prime} (z)$, 再通过反向传导得到所有的偏微分,利用偏微分计算出所有参数的梯度,此时再进行参数更新。而不是再计算出某一层参数的梯度后立即更新当前层的参数,再计算前一层的梯度。

这样的话,整个神经网络的参数优化就变成了多次前馈计算损失与反向传播计算梯度的迭代,直至找到最好的网络参数 $W$ 和 $b$.

如何直观解释反向传播》中有一个有趣的比喻:如果把上图中的箭头表示欠钱的关系,即 $c \rightarrow e$ 表示 $e$ 欠 $c$ 的钱。以 $a$, $b$ 为例,直接计算 $e$ 对它们俩的偏导相当于 $a$, $b$ 各自去讨薪。$a$ 向 $c$ 讨薪,$c$ 说 $e$ 欠我钱,你向他要。于是 $a$ 又跨过 $c$ 去找 $e$. $b$ 先向 $c$ 讨薪,同样又转向 $e$, $b$ 又向 $d$ 讨薪,再次转向 $e$. 可以看到,追款之路,充满艰辛,而且还有重复,即 $a$, $b$ 都从 $c$ 转向 $e$. 而反向传播算法就是主动还款, $e$ 把所欠之钱还给 $c$ 和 $d$. $c$ 和 $d$ 收到钱,乐呵地把钱转发给了 $a$ 和 $b$, 大家皆大欢喜。

梯度下降用于神经网络

在讲梯度下降时提到,实际训练中容易卡在局部最小点、鞍点或是高原区域。有一些论文对其用于深度学习领域的有效性进行了解释:

Yann LeCun 在 07 年的论文中表明不用担心局部最小点这一情况,他认为在实际的空间中并没有那么多的局部最小点。因为要成为局部最小点,它需要在每一个维度都是山谷的谷底,假设每个维度出现山谷的概率是 $p$, 如果神经网络有 $1000$ 个参数,出现局部最小点的概率会非常非常低。所以一个很大的神经网络看起来应该是很平滑的,当你在局部最小点卡住时,很有可能就位于全局最小点,或者很接近全局最小点。

我们现在知道了深度前馈网络是可以通过梯度下降训练的,然而在训练的过程中,可能会遇到一些不如意的情况,在下一篇文章中将介绍一些基础的神经网络训练技巧,这对于更复杂的网络结构几乎是通用的。