我们来回顾一下深度学习的流程:首先定义神经网络模型的结构,以及损失函数,接下来使用梯度下降去优化模型参数,最终得到训练好的神经网络。在此之后还需要做什么事情呢?

我们应当检查现在得到的模型在训练集上有没有得到好的结果,如果没有,则针对前面流程中的三个步骤进行修改。相较于其它机器学习方法,深度学习在训练集上不容易过拟合,反而在训练的过程中,容易发生得不到好的正确率的情况。在训练集上得到好的正确率后,将最终的模型应用于测试集数据,因为我们最终关注的是模型在新数据上的表现。此时如果测试误差过大,才表示发生了过拟合。此时我们需要想办法解决过拟合,但也有可能在采取新的技术后导致训练误差变大。

上面的图片来自 Kaiming He 的论文《Deep Residual Learning for Image Recognition》中的实验结果,横轴是参数更新次数,纵轴是错误率。先看右边,我们会发现 56 层比 20 层的神经网络在测试时得到的误差率要高。有人会觉得,这是因为 56 层模型参数过多,导致了过拟合。然而观察左图发现,使用 20 层的网络在训练时就已经能得到比 56 层更好的效果。

这是因为在训练神经网络时,有许多能够影响训练结果的因素,比如局部最小值、鞍点、平原等等问题。有可能在训练时,56 层的神经网络卡在了局部最小点,这并不能算作过拟合情况。而理论上 20 层网络能做到的事情, 56 层网络一定能做到,前面 20 层结构和参数一样,后面 36 层的输入输出完全不变化。所以上面的情况也不能算欠拟合,复杂的 56 层模型是有能力得到更好的结果的,只是没有训练好。

因此在阅读深度学习文献时,要考虑针对何种情况,文中提出的方法解决了何种的问题。比如后面提到的 Dropout 方法适用于测试结果不好的情况,如果现在的问题是训练结果已经不好,使用 Dropout 可能会导致现有的训练结果更加糟糕。要做到对症下药。

训练结果不好

人们曾经在手写数字识别数据集 MNIST 上做过实验,发现当网络层数变深时,准确率可能不升反降(参考前面用局部最小点做的解释), 一个原因是梯度消失(Vanishing Gradient).

当网络层数很深时,通过反向传播算法计算梯度,会发现在靠后的层的参数对损失函数的梯度比较大,而靠近输入层的参数的梯度会很小。分析一下,参数对损失的偏微分 $\frac{\partial l}{\partial w}$, 从直觉上说是:如果 $w$ 发生变化,会对 $l$ 带来什么样的影响。如果我们在第一层神经元的参数加上 $\Delta w$, 对神经元的输出会有影响。但是这个影响是会被衰减的,假设使用 Sigmoid 函数作为激活函数,它会将 $-\infty$ 到 $\infty$ 的值压缩到 $-1$ 到 $1$ 之间。也即是说如果输入有着很大变化,通过 Sigmoid 函数后,这种变化会被衰减。而每通过一次 Sigmoid 函数,这种变化带来的影响就衰减一次,直到最后对输出的影响是非常小的,因此在靠近输入层的参数的梯度通常是很小的。

因此如果设定同样的学习率,靠近输出的地方参数更新很快,而靠近输入的地方参数更新很慢。这会导致当靠近输入的地方的参数还近乎于一种随机状态时,靠近输出的地方的参数已经收敛了。此时直观上你会发现损失函数的值下降得很慢,认为找到了一个局部最小点,便停止训练,得到一个不是很好的结果。结果很差的原因在于,你基于几乎随机的参数计算并收敛出最终的结果。

使用新的激活函数

为了减少梯度消失带来的影响,人们最初的做法是训练受限玻尔兹曼机,先训练好前一个层,再训练紧接着的下一个层,这样虽然在反向传播时前面的参数几乎没有被训练到,但我们已经做好了预训练。Xavier Glorot 在 2010 年的论文《Understanding the difficulty of training deep feedforward neural networks》提出了 Xavier 初始化,使得每一层输出的方差应该尽量相等,这里不细讲。

早期的激活函数还有线性激活函数与阶跃激活函数,而传统神经网络中使用最多的是 Sigmoid 系(Logistic-Sigmoid、Tanh-Sigmoid) 激活函数。后来有人发现,使用新的激活函数来代替 Sigmoid 函数,可以取得较好的效果:

现在比较常用的激活函数是 ReLU 函数,全称 Rectified Linear Unit.

$a$ 是激活函数 $\sigma(z)$ 的输出,如果 $a \geq 0$, 则 $z=a$; 如果 $a<0$, 则 $z=0$.

选择这样的激活函数有什么样的好处呢?

  1. 相较于 Sigomid 计算更快更方便,没有指数运算
  2. 从生物学角度去解释… 《Theoretical Neuroscience》. Peter Dayan and L.F. Abbott
  3. Hinton: 等同于无穷多的 Sigomid 函数叠加后的结果…
  4. 对梯度消失的情况进行了处理

当神经元使用 ReLU 作为激活函数后,神经元线性输出 $z$ 大于 $0$ 时,$a=z$ 表明此时激活函数功能是线性的;而当 $z$ 小于 $0$ 时,神经元输出为 $0$, 不会影响到最终的输出值。因此整个网络在计算时和线性网络一样,就不会有梯度消失的问题了,而且整体上依旧是一个非线性的网络结构。由于 ReLU 的输入不可能正好为 $0$, 所以在实际计算的时候,梯度只可能为 $1$ 或者 $0$, 虽然函数不可微,但依旧可以使用梯度下降。

针对 ReLU 函数还有种种改进后的变式,比如原始输入小于 $0$ 时,输出为 $0$, 这样没有办法更新参数。所以此时 ReLU 的输出还是应当有一定的值,比如 $a=0.01 z$. 这种形式被叫作 Leaky ReLU. 根据这种思想又有人提出了 Parametric ReLU, 使输入为负时 $a= \alpha z $, 而其中 $\alpha$ 作为参数可以被训练出来,甚至每个神经元有着不同的 $\alpha$ 值。Ian J. Goodfellow 在 ICML 2013 提出了《Maxout Network》,思想是让神经网络自动去学习激活函数的形式,此时 ReLU 可以被看成是 Maxout 的一种特例。

ICLR 2016 的论文《Fast and Accurate Deep Network Learning by Exponential Linear Units (ELUs)》提出 ELU 激活函数,将 Leaky Relu 为负的部分改为了 $a=\alpha\left(e^{z}-1\right)$. 2017 年 NIPS 有一篇投稿论文《Self-Normalizing Neural Networks》,提出了 SELU 激活函数,即 Scaled ELU, 在 ELU 基础上整个公式都乘上一个 $\lambda$. 正文只有 9 页,附录却是 93 页的证明,说明 $\alpha= 1.6732632423543772848170429916717$ 和 $\lambda=1.0507009873554804934193349852946$, 而实际代码实现的时候只需短短几行。

其实直接生成 1000 万个从高斯分布中采样的数据,过一遍 ELU, 也能求出类似的 Magic Number。例如,在 Matlab 里输入:

A = randn(10000000,1);
alpha = -mean(A(A>0)) / mean(exp(A(A<0))-1)
scale = 1 / std([A(A>0); alpha * (exp(A(A<0))-1)])

会得到差不多的结果,这篇论文的特点还是附录长,当时引起了很多人关注。

对于训练过程中产生的梯度消失和梯度爆炸情况,已经存在有不同的解决办法,我们将穿插在后面的知识中讲解,并在合适的时候进行总结。

自适应学习率

我们在讲机器学习时讲过了 Adagrad, 学习率随着每个参数的更新次数变多而变得越来越小:

而实际上我们训练神经网络时,碰到的问题可能会更加复杂:

这就导致在同一个参数变化方向上,学习率也要很快地做出调整。

RMSProp

因此有了 Adagrad 的进阶版 RMSProp, 这个方法在 Hinton 的 MOOC 线上课程中被提出。

整个计算流程如下,其中 $\eta$ 是固定的学习率:

与原来的 Adagrad 相比,RMSProp 通过手动设置 $\alpha$ 调整新旧梯度各自的重要性。

Momentum

On the importance of initialization and momentum in deep learning. JMLR 2013

人们做科学研究很多时候是从真实的世界中得到灵感,如果将一个球从很高的坡上滚下来,滚到高原或鞍点的地方,由于惯性还是会继续往前走;假设一个局部最小点的坡不是很陡,它甚至可能翻过这个小坡,滚到更低的地方。

将惯性的特性引入梯度下降,就得到了 Momentum 方法。此时每次移动的方向不仅仅取决于当前的梯度,还取决于前一个时间点移动的方向,更新规则为:

依此类推,其中 $\theta$ 是参数, $v$ 是方向,$\lambda$ 决定惯性造成多大的影响,$\eta$ 是学习率。

上图中,梯度告诉我们走红色方向,惯性告诉我们走绿色方向,综合起来就是走蓝色方向。我们也可以将某个时间点移动的方向 $v_i$ 理解成过去所有时间点梯度的总和,用上面的式子计算可以得到:

直觉上理解则如下图所示,最右边由于惯性的力量可能跳出了局部最小点:

Adam

Adam: A Method for Stochastic Optimization. Diederik P. Kingma. ICLR 2015

前面提到科研的灵感可能来自于生活,而科学研究中呢,新的思路 $C$ 也可以能来自于将 $A$ 加上 $B$, Adam 做的其实就是 $A+B=C$ 的工作,它是 RMSProp 与 Momentum 的结合体,再加上一些改进。

其中 $m$ 即 Momentum 中的惯性方向,$v$ 是 RMSProp 中的 $\sigma$, 多出来的两个公式:

即在计算了梯度的指数移动均值(Exponential Moving Average)后,超参数 $\beta_1$ 和 $\beta_2$ 控制了这些移动均值的衰减率,作为一种偏差修正(bias-corrected).

在 ICLR 2018 的一篇 Oral 论文《On the Convergence of Adam and Beyond》中提供了一个简单的凸优化问题作为案例来说明 RMSProp 和 Adam 中的指数移动均值会如何导致算法不收敛,并且提出了一个 Adam 的新变体,可能会变为更加流行的选择。

特征缩放

我们在讲机器学习基础部分时已经提到过特征缩放, 假设现在有 $R$ 个样本 $x^1,x^2, \ldots , x^r, \ldots ,x^R$, 每个样本中有一组特征,如 $x^1$ 中有 $x^1_1 , x^2_2, \ldots$. 对于每个维度 $i$ 去计算均值 $m_i$ 与标准差 $\sigma_i$, 接着对第 $r$ 个样本的第 $i$ 个元素进行如下处理:

经过这样的处理后,所有维度的均值将变为 $0$, 方差会变为 $1$.

我们在之前也解释了为什么特征缩放可以加快参数训练时的收敛速度。对于神经网络模型,隐藏层之间有许多激活后的值 $a$, 它们都可以看作是下一个隐藏层的输入特征。根据这个角度,既然输入 $x$ 需要进行特征缩放,那么隐藏层之间的这些 $a$ 也应该进行特征缩放以提升效果。

另外我们也可以从 Internal Covariate Shift(内部协方差偏移) 这篇论文的角度来解释为什么要要进行对 $a$ 的特征缩放。神经网络中的每一层都可以想象成是一根电话线,电话线需要平直地接起来,声音才能够从头传到尾。当网络还未被训练好时,就好像电话线还没有被连接好。

而在反向传播训练的过程中,第三个人可能告诉第二个人,将线提高一些;而第二个人告诉第一个人,将线降低一些。而这种变化的幅度可能是不一致的,结果可能由左高右底变成了左低右高的情况。这并不是最终我们希望的通话效果,因此又要进行反复如此的调整,而对于神经网络模型,所有的电话线是一起进行调整的,这样就变得更加麻烦。

过去的解决方法是设置较小的学习率,每次线的调整幅度很小,但这样会导致训练过慢。因此我们希望有更好的方法,在训练过程中不论隐藏层如何变化,中间输出的 $a$ 之间的统计学(均值、方差等) 特征是一致的。比较笨的方法是每次隐藏层有了输出,就对其进行归一化,但实现起来是行不通的,因为这些值在训练的过程中是不断变化的。最后经过探索,大家目前常用的是批归一化(Batch Normalization) 方法。

Batch Normalization

我们先理解一下批(Batch) 的概念,我们在训练神经网络的时候,实际上并不是每一笔数据分别丢进去训练,而是会抽样出一批数据。真正在计算的时候,会将这些数据并在一起成为一个矩阵,与权重矩阵相乘得到对应的输出。我们在讨论线性回归的矩阵形式时讲过,矩阵运算优于循环运算,可以加快计算效率。

批归一化的思想是,对每一批数据计算得到的 $z_1,z_2,z_3$ 进行归一化,即计算均值和方差:

需要注意的是,批归一化不能用于小批次,这很好理解,抽样量小的时候不能代表整体统计学分布特征。另外批归一化操作也可以放在激活函数之后,即对 $a$ 执行。(然而在深度学习框架中内部实现批归一化时,并不是将 $\mu$ 和 $\sigma$ 看作常数参与反向传播计算,而是随着这一批数据的参数一起被更新。)

由于批归一化后的输出基本会限制在正态分布下,使网络表达能力下降,为了解决这个问题,通常还会进行尺度变换与一定的偏移:

其中 $\gamma$ 和 $\beta$ 参数是由网络自己学得的。如果令 $\gamma = \sigma$, $\beta = \mu$, 其实等于进行了逆归一化使得 $\hat{z}^{i}={z}^{i}$, 这样结合前面的步骤,就等于没有进行批归一化操作。由于 $\gamma$ 和 $\beta$ 由网络学得,可以认为网络自行决定了批归一化的程度,至少保证了不会比原来的模型效果差,因此通常初始化 $\gamma = 1$, $\beta = 0$.

下面讲讲测试的时候如何进行批归一化,明显的特点是测试的时候没有批数据,通常是逐个测试的。这个时候为了得到 $\mu$ 和 $\sigma$, 一个可能的做法是计算整个训练数据的 $\mu$ 和 $\sigma$, 作为常数用于测试情怀,这种方法在某些情况不适用。

实际上的做法是,在训练时计算 $\mu$ 和 $\sigma$ 的移动平均值。即每批数据都会得到自己的 $\mu$ 和 $\sigma$, 新的一批数据训练时,将 $\mu$ 和 $\sigma$ 平均起来,这样就可以估测出整个数据集的 $\mu$ 和 $\sigma$.

注意在训练的过程中,由于权重矩阵一直在改变,就算是同一批数据,计算出的 $\mu$ 也是一直在变化的,等到损失函数开始收敛,权重矩阵的变化慢慢不那么大,对应的 $\mu$ 才会固定下来。因此在计算移动平均值时,可能会把最开始计算得到的 $\mu$ 和 $\sigma$ 去掉,只考虑参数比较平均时的 $\mu$ 和 $\sigma$.

批归一化的优点是产生较少的协方差偏移(回想电话线的例子), 因此可以使用大一些的学习率以加快训练。另外这也可以减轻梯度消失的情况,如果激活函数依旧是 Sigmoid 函数,经过归一化后,自变量范围会在比较函数曲线比较中间的位置,对应的斜率比较大,因此批归一化通常是在通过激活函数之前进行的。另外,批归一化会降低参数初始化好坏对学习带来的影响,假设 $W$ 放大 $k$ 倍,经过批归一化后得到的 $\tilde{z}^{i}$ 还是不变的。

批归一化也有一些正则化的效果,可以应对过拟合问题,本文下面会讲到正则化。

而至于归一化(Normalization), 有着许多应对不同情况提出的方法——如果每批数据量很小,使用 Batch Renormalization. 对于以后会讲到的循环神经网络,通常使用 Layer Normalization. 对于卷积神经网络,通常使用 Instance NormalizationGroup Normalization. 对于网络的权重有 Weight Normalization, 对于生成对抗网络有 Spectrum Normalization. 目前先不深究它们分别是什么样子,等在合适的地方再进行讲解。

测试结果不好

这里的测试结果,有可能是指人为从训练集中划分出的验证数据集,用其模拟测试数据集得到的结果。

Early Stopping

由于实际上训练数据和测试数据的分布是不一致的,所以有可能训练误差在下降的时候,测试误差正在上升。因此理想情况下,假设已知了它们的变化曲线,参数的更新次数应该停止在测试误差最低的地方。可实际上我们无法预知这件事情,根据训练误差人为地停止训练也不是好方法,因此需要通过划分验证集,使之发现在验证集上测试误差上升时停止训练。

正则化

正则化并不是深度学习时代才出现的技术,它在线性回归中也经常被使用。我们重新定义需要最小化的损失函数,原本的损失函数是定义在训练数据上的,可能是平方误差或交叉熵,而此时加上一个正则项,比如 $L_2$ 正则项:

其中 $\theta=\left\{w_{1}, w_{2}, \ldots\right\}$ 是一组参数,而 $\|\theta\|_{2}=\left(w_{1}\right)^{2}+\left(w_{2}\right)^{2}+\ldots$ 即将所有参数取平方并相加。之前提过,在进行正则化的时候,一般不考虑 Bias 项,它不影响模型函数的平滑程度。计算梯度:

更新参数:

你会发现在更新参数时,每次更新前都缩小了 $w$ 的值,最终会越来越接近 $0$. 由于它与后面得到的微分项进行了平衡,因此最后得到的参数不会为 $0$, 也不用担心梯度消失的情况。在深度学习中,$L_2$ 正则化能够使得权重参数的数值越来越小,因此这种技术又叫作权重衰减(Weight Decay). 其实通过 Early Stopping 减少更新次数,也可以避免参数值距离 $0$ 太远,这和正则化有着异曲同工之妙。

接下来看 $L_1$ 正则化:

V 型的绝对值该如何微分?实际上不用考虑在 $0$ 处的情况:

使用 Sign 函数,$w$ 为正,微分值为 $1$, 否则为 $-1$. 更新参数:

和 $L_2$ 正则化比较会发现,它们的目的都是让参数值变小,但 $L_1$ 正则化每次都减去固定值。如果 $w$ 很大,使用 $L_1$ 训练得到的模型中可能还是会有很大的参数。但如果 $w$ 很小,使用 $L_2$ 训练参数更新的速度就会很慢。因此使用 $L_1$ 正则化训练神经网络,通常会得到比较稀疏(Sparse) 的模型,其中有很多接近 $0$ 的值,但是也会有很多很大的值;而用 $L_2$ 训练得到的参数值是比较平均的,而且都接近于 $0$.

Dropout

Dropout 的思路是在训练的过程中,每次更新参数前,对网络中的每个神经元做一个抽样,决定当前的神经元是否要被丢掉,每个神经元被丢掉的概率为 $p$.

如果决定丢掉某个神经元,与之相连的权重也变无效,整个网络变得比较细长。而每次更新参数,都要进行一次 Dropout, 所以每次用于训练的网络结构是不一样的。而在测试的时候不加 Dropout, 另外所有的权重乘上 $1-p$. 注意 Dropout 是会影响训练时的效果的,目的是在测试时变得更好。

直观上来看,在训练的时候丢掉一些神经元,就好像练轻功的时候在脚上绑了重物。而在测试的时候,将这些重物扔掉。另一种角度是,每个神经元就是一个学生,大家要一起做结课项目,而在团队中总是会有人划水,因此其他人会担心这个点,所以自己想着要 Carry 起来,而最终发现其实大家都有认真完成项目,整体项目也变得更好。

为什么测试时需要将神经元的参数乘上 $1-p$ 呢?这是为了使得给神经元相同的输入,训练和测试得到的输出尽可能类似。如下图所示情况:

不同的文献上对于 Dropout 有着不同的正式解释,可以参考 Artificial Intelligence 期刊 2014 年中的《The dropout learning algorithm》一文。

训练技巧之外的改进

我们目前所了解的神经网络模型结构非常简单直接,在用于比较复杂任务的时候可能存在着局限性。因此很多时候我们都希望,能够从一些传统的专业领域知识中进行一定的借鉴,将其与神经网络的思想结合,从而指导进行新的神经网络结构设计。这种感觉就如同统计机器学习领域的发展,由最基础的线性回归模型出发,推广到线性分类和广义线性模型,跨领域的知识通常会起到很大的帮助。

下面的文章将介绍计算机视觉和自然语言处理等领域中,经典神经网络结构的设计思想和发展过程。