深度学习与CV教程(7) | 神经网络训练技巧 (下)

2022-06-01 01:34:07 浏览数 (4)

  • 作者:韩信子@ShowMeAI
  • 教程地址:http://www.showmeai.tech/tutorials/37
  • 本文地址:http://www.showmeai.tech/article-detail/266
  • 声明:版权所有,转载请联系平台与作者并注明出处
  • 收藏ShowMeAI查看更多精彩内容
Training Neural Networks; 深度学习与计算机视觉; Stanford CS231nTraining Neural Networks; 深度学习与计算机视觉; Stanford CS231n

本系列为 斯坦福CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。

引言

ShowMeAI在上一篇 深度学习与CV教程(6) | 神经网络训练技巧 (上) 介绍了激活函数选择,sigmoidtanh 都有饱和的问题;权重初始化不能太小也不能太大,最好使用Xavier初始化;数据预处理使用减去均值和归一化,线性分类中这两个操作会使分界线不那么敏感,即使稍微转动也可以,神经网络中也对权重的轻微改变没那么敏感,易于优化;也可以使用批量归一化,将输入数据变成单位高斯分布,或者缩放平移;学习过程跟踪损失、准确率;超参数体调优范围由粗到细,迭代次数逐渐增加,使用随机搜索。

本篇重点

  • 更好的优化
  • 正则化
  • 迁移学习
  • 模型集成

1.更好的优化(参数更新)

关于优化算法的详细知识也可以对比阅读ShowMeAI的 深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 神经网络优化算法 的讲解。

1.1 批梯度下降(BGD)

批梯度下降即 batch gradient descent,在训练中每一步迭代都使用训练集的所有内容 {x_1, cdots ,x_n} 以及每个样本对应的输出 y_i,用于计算损失和梯度然后使用梯度下降更新参数。

当在整个数据集上进行计算时,只要学习率足够低,总是能在损失函数上得到非负的进展。参考代码如下(其中 learning_rate 是一个超参数):

代码语言:python代码运行次数:0复制
# 普通梯度下降
while True:
    weights_grad = evaluate_gradient(loss_fun, data, weights)
    weights  = -learning_rate * weights_grad  # 参数更新

如下图为只有两个参数的损失函数,经过不断更新,如果足够幸运,函数最终收敛在红色部分是最低点:

批量梯度下降BGD; 两个参数的损失函数批量梯度下降BGD; 两个参数的损失函数
  • 优点:由于每一步都利用了训练集中的所有数据,因此当损失函数达到最小值以后,能够保证此时计算出的梯度为 0,能够收敛。因此,使用BGD时不需要逐渐减小学习率。
  • 缺点:随着数据集的增大,运行速度会越来越慢。

1.2 随机梯度下降(SGD)

这里的随机梯度下降(stochastic gradient descent)其实和之前介绍的MBGD(minibatch gradient descent)是一个意思,即每次迭代随机抽取一批样本 {x_1, cdots ,x_m}y_i,以此来反向传播计算出梯度,然后向负梯度方向更新参数。

SGD的优点是训练速度快,对于很大的数据集,也能够以较快的速度收敛。但是实际应用SGD会有很多问题:

① 如果损失函数在一个参数方向下降的快另一个方向下降的慢,这样会导致 「 之字形 」下降到最低点,高维中很普遍

随机梯度下降SGD; SGD之字形路线随机梯度下降SGD; SGD之字形路线
  • 上图是一个山沟状的区域,损失最小点沿着蓝色的线方向。考虑表面上的一个点A梯度,该点的梯度可以分解为两个分量,一个沿着方向 w_1,另一个沿着 w_2
  • 梯度在 w_1 方向上的分量要大得多,因为在 w_1 方向上每走一步都比在 w_2 方向损失值下降的多,虽然最小值在 w_2 方向上。这样实际走一步在 w_1 方向走的多, w_2 走得少,就会导致在这个沟里反复震荡,「之字形」前往最小值点。

② 如果损失函数有局部极小值和鞍点(既不是极大值也不是极小值的临界点)时,此时的梯度为0,参数更新会卡住,或在极小值附近震荡

随机梯度下降SGD; 鞍点 Saddle points随机梯度下降SGD; 鞍点 Saddle points
  • 在高维数据中,鞍点的存在是个更普遍也更大的问题,极小值每个梯度方向损失都会变大,而鞍点有的方向变大,有的减小,接近鞍点时更新缓慢。

③ SGD具有随机性,我们的梯度来自小批量数据(使用全部数据计算真实梯度速度太慢了),可能会有噪声,这样梯度下降的路线会很曲折,收敛的慢

随机梯度下降SGD; 有噪声的SGD路线曲折随机梯度下降SGD; 有噪声的SGD路线曲折

下面有一些「小批量梯度下降」基础上的优化算法。

1.3 动量(Momentum)更新

带动量的更新方法在深度网络上几乎总能得到更好的收敛速度。

损失值可以理解为是山的高度(因此高度势能是 U=mgh),用随机数字初始化参数等同于在某个位置给质点设定初始速度为 0 ,这样最优化过程可以看做是参数向量(即质点)在地形上滚动的过程。

质点滚动的力来源于高度势能 F = - nabla U,即损失函数的负梯度(想象损失函数为凸函数,梯度为正时质点会向负方向滚动,对应参数减小;损失函数梯度为负时会向正方向滚动对应参数增大)。又因为 F=ma,质点的加速度和负梯度成正比,所以负梯度方向速度是逐渐增加的。

在 SGD 中,梯度直接影响质点的位置,在梯度为 0 的地方,位置就不会更新了;而在这里,梯度作为作用力影响的是速度,速度再改变位置,即使梯度为 $0$ ,但之前梯度累积下来的速度还在,一般而言,一个物体的动量指的是这个物体在它运动方向上保持运动的趋势,所以此时质点还是有动量的,位置仍然会更新,这样就可以冲出局部最小值或鞍点,继续更新参数。但是必须要给质点的速度一个衰减系数或者是摩擦系数,不然因为能量守恒,质点在谷底会不停的运动。

也就是说,参数更新的方向,不仅由当前点的梯度方向决定,而且由此前累积的梯度方向决定。

计算过程也是每次迭代随机抽取一批样本 {x_1, cdots ,x_m}y_i,计算梯度和损失,并更新速度和参数(假设质量为1,v即动量):

代码语言:python代码运行次数:0复制
v=0
while True:
    dW =  compute_gradient(W, X_train, y_train)
    v = rho * v - learning_rate * dW
    W  = v
  • rho 表示每回合速度 v 的衰减程度,每次迭代得到的梯度都是 dW 那么最后得到的 v 的稳定值为:frac{-learning_{rate} ast dw}{1-rho}
  • rho0 时表示 SGD,rho 一般取值 0.50.90.99,对应学习速度提高两倍、10倍和100倍。

动量更新可以很好的解决上述 SGD 的几个问题:

  • 由于参数的更新要累积之前的梯度,所以如果我们分别累加这些梯度的两个分量,那么 w_1 方向上的分量将互相抵消,而 w_2 方向上的分量得到了加强。 但是由于衰减系数,不可能完全抵消,但是已经可以加速通过,很大程度缓解了「之字形」收敛慢的问题。这也是减少震荡的原理。
动量更新; 缓解之字形震荡动量更新; 缓解之字形震荡
  • 局部最小值和鞍点由于还有之前的速度,会加速冲过去。
动量更新; 越过局部极小值点和鞍点动量更新; 越过局部极小值点和鞍点
  • 面对梯度变化比较大的方向,即一些噪声,由于此时质点还有比较大的速度,这时的反方向需要先将速度减小为 0 才能改变参数更新方向,由于速度是累加的,所以个别的噪声的影响不会那么大,就可以平滑快速的收敛。
动量更新; 抑制噪声的影响动量更新; 抑制噪声的影响

1.4 Nesterov动量

Nesterov动量与普通动量有些许不同,最近变得比较流行。在理论上对于凸函数它能得到更好的收敛,在实践中也确实比标准动量表现更好一些。

Nesterov 动量; 动量 V.S. Nesterov动量Nesterov 动量; 动量 V.S. Nesterov动量
  • 普通的动量更新在某一点处有一个速度,然后计算该点的梯度,实际的更新方向会根据速度方向和梯度方向做一个权衡。
  • Nesterov动量更新是既然我们知道动量将会将质点带到一个新的位置(即向前看),我们就不要在原来的位置计算梯度了,在这个「向前看」的地方计算梯度,更新参数。

这样代码变为:

代码语言:python代码运行次数:0复制
v=0
while True:
    W_ahead = W   rho * v
    dW_ahead =  compute_gradient(W_ahead, X_train, y_train)
    v = rho * v - learning_rate * dW_ahead
    W  = v

动量还是之前的动量,只是梯度变成将来的点的梯度。

而在实践中,人们更喜欢和普通SGD或普通的动量方法一样简单的表达式。通过对 W_ahead = W rho * v 使用变量变换进行改写是可以做到的,然后用 W_ahead 而不是 W 来表示上面的更新。

也就是说,实际存储的参数总是向前一步的那个版本。 代码如下:

代码语言:python代码运行次数:0复制
v=0
while True:
    pre_v = v
    dW =  compute_gradient(W, X_train, y_train)
    v = rho * v - learning_rate * dW
    W  = -rho * pre_v   (1   rho) * v

推导过程如下:

最初的 Nesterov 动量可以用下面的数学表达式代替:

v_{t 1}=rho v_t - alpha nabla f(x_t rho v_t)
x_{t 1}=x_t v_{t 1}

现在令 tilde{x}_t =x_t rho v_t,则:

v_{t 1}=rho v_t-alpha nabla f(tilde{x_t})

begin{aligned}

tilde{x}{t 1} &=x{t 1} rho v_{t 1}

&=x{t} v{t 1} rho v_{t 1}

&=tilde{x}{t}-rho v{t} v{t 1} rho v{t 1}

end{aligned}

从而有:

tilde{x}_{t 1}=tilde{x_t}-rho v_t (rho 1)v_{t 1}
  • 只更新 v_ttilde{x}_t 即可

示意图如下:

Nesterov 动量; 还原推导示意图Nesterov 动量; 还原推导示意图
Nesterov 动量; SGD V.S. 动量 V.S. Nesterov 动量Nesterov 动量; SGD V.S. 动量 V.S. Nesterov 动量

1.5 自适应梯度算法(Adagrad)

上面提到的方法对于所有参数都使用了同一个更新速率,但是同一个更新速率不一定适合所有参数。如果可以针对每个参数设置各自的学习率可能会更好,根据情况进行调整,Adagrad是一个由 Duchi等 提出的适应性学习率算法。

代码如下:

代码语言:python代码运行次数:0复制
eps = 1e-7
grad_squared = 0
while True:
    dW = compute_gradient(W)
    grad_squared  = dW * dW
    W -= learning_rate * dW / (np.sqrt(grad_squared)   eps)

AdaGrad 其实很简单,就是将每一维各自的历史梯度的平方叠加起来,然后更新的时候除以该历史梯度值即可。

变量 grad_squared 的尺寸和梯度矩阵的尺寸是一样的,用于累加每个参数的梯度的平方和。这个将用来归一化参数更新步长,归一化是逐元素进行的。eps(一般设为 1e-41e-8 之间)用于平滑,防止出现除以 0 的情况。

  • 优点:能够实现参数每一维的学习率的自动更改,如果某一维的梯度大,那么学习速率衰减的就快一些,延缓网络训练;如果某一维的梯度小,那么学习速率衰减的就慢一些,网络训练加快。
  • 缺点:如果梯度累加的很大,学习率就会变得非常小,就会陷在局部极小值点或提前停( RMSProp 算法可以很好的解决该问题)。

1.6 均方根支柱算法(RMSProp)

RMSProp优化算法也可以自动调整学习率,并且RMSProp为每个参数选定不同的学习率。

RMSProp算法在AdaGrad基础上引入了衰减因子,RMSProp在梯度累积的时候,会对「过去」与「现在」做一个平衡,通过超参数 decay_rate 调节衰减量,常用的值是 [0.9,0.99,0.999]。其他不变,只是 grad_squared 类似于动量更新的形式:

代码语言:python代码运行次数:0复制
grad_squared =  decay_rate * grad_squared   (1 - decay_rate) * dx * dx

相比于AdaGrad,这种方法很好的解决了训练过早结束的问题。和 Adagrad 不同,其更新不会让学习率单调变小。

均方根支柱算法; SGD V.S. 动量 V.S. RMSProp均方根支柱算法; SGD V.S. 动量 V.S. RMSProp

1.7 自适应-动量优化(Adam)

动量更新在SGD基础上增加了一阶动量,AdaGrad和RMSProp在SGD基础上增加了二阶动量。把一阶动量和二阶动量结合起来,就得到了Adam优化算法:Adaptive Momentum

代码如下:

代码语言:python代码运行次数:0复制
eps = 1e-8
first_moment = 0  # 第一动量,用于累积梯度,加速训练
second_moment = 0  # 第二动量,用于累积梯度平方,自动调整学习率
while True:
    dW = compute_gradient(W)
    first_moment = beta1 * first_moment   (1 - beta1) * dW  # Momentum
    second_moment = beta2 * second_moment   (1 - beta2) * dW * dW  # AdaGrad / RMSProp
    W -= learning_rate * first_moment / (np.sqrt(second_moment)   eps)

上述参考代码看起来像是 RMSProp 的动量版,但是这个版本的 Adam 算法有个问题:第一步中 second_monent 可能会比较小,这样就可能导致学习率非常大,所以完整的 Adam 需要加入偏置。

代码如下:

代码语言:python代码运行次数:0复制
eps = 1e-8
first_moment = 0  # 第一动量,用于累积梯度,加速训练
second_moment = 0  # 第二动量,用于累积梯度平方,自动调整学习率

for t in range(1, num_iterations 1):
    dW = compute_gradient(W)
    first_moment = beta1 * first_moment   (1 - beta1) * dW  # Momentum
    second_moment = beta2 * second_moment   (1 - beta2) * dW * dW  # AdaGrad / RMSProp
    first_unbias = first_moment / (1 - beta1 ** t)  # 加入偏置,随次数减小,防止初始值过小
    second_unbias = second_moment / (1 - beta2 ** t)
    W -= learning_rate * first_unbias / (np.sqrt(second_unbias)   eps)

论文中推荐的参数值 eps=1e-8, beta1=0.9, beta2=0.999, learning_rate = 1e-35e-4,对大多数模型效果都不错。

在实际操作中,我们推荐 Adam 作为默认的算法,一般而言跑起来比 RMSProp 要好一点。

自适应动量优化; SGD V.S. 动量 V.S. RMSProp V.S. Adam自适应动量优化; SGD V.S. 动量 V.S. RMSProp V.S. Adam

1.8 学习率退火

以上的所有优化方法,都需要使用超参数学习率。

在训练深度网络的时候,让学习率随着时间衰减通常是有帮助的。可以这样理解:

  • 如果学习率很高,系统的动能就过大,参数向量就会无规律地跳动,不能够稳定到损失函数更深更窄的部分去。
  • 知道什么时候开始衰减学习率是有技巧的:慢慢减小它,可能在很长时间内只能是浪费计算资源地看着它混沌地跳动,实际进展很少。但如果快速地减少它,系统可能过快地失去能量,不能到达原本可以到达的最好位置。

通常,实现学习率衰减有3种方式:

① 随步数衰减:每进行几个周期(epoch)就根据一些因素降低学习率。典型的值是每过 5 个周期就将学习率减少一半,或者每 20 个周期减少到之前的 10%。

这些数值的设定是严重依赖具体问题和模型的选择的。在实践中可能看见这么一种经验做法:使用一个固定的学习率来进行训练的同时观察验证集错误率,每当验证集错误率停止下降,就乘以一个常数(比如0.5)来降低学习率。

② 指数衰减:数学公式是 alpha=alpha_0e^{-kt},其中 alpha_0,k 是超参数, t 是迭代次数(也可以使用周期作为单位)。

③ 1/t 衰减:数学公式是 alpha=alpha_0/(1 kt)),其中 alpha_0,k 是超参数, t 是迭代次数。

在实践中随步数衰减的随机失活(Dropout)更受欢迎,因为它使用的超参数(衰减系数和以周期为时间单位的步数)比k更有解释性。

如果你有足够的计算资源,可以让衰减更加缓慢一些,让训练时间更长些。

一般像SGD这种需要使用学习率退火,Adam等不需要。也不要一开始就使用,先不用,观察一下损失函数,然后确定什么地方需要减小学习率。

学习率退火; 损失函数学习率退火; 损失函数

1.9 二阶方法(Second-Order)

在深度网络背景下,第二类常用的最优化方法是基于牛顿方法的,其迭代如下:

x leftarrow x - [H f(x)]^{-1} nabla f(x)

H f(x) 是 Hessian 矩阵,由 f(x) 的二阶偏导数组成:

mathbf{H}=left[begin{array}{cccc}

frac{partial^{2} f}{partial x{1}^{2}} & frac{partial^{2} f}{partial x{1} partial x{2}} & cdots & frac{partial^{2} f}{partial x{1} partial x_{n}}

frac{partial^{2} f}{partial x{2} partial x{1}} & frac{partial^{2} f}{partial x{2}^{2}} & cdots & frac{partial^{2} f}{partial x{2} partial x_{n}}

vdots & vdots & ddots & vdots

frac{partial^{2} f}{partial x{n} partial x{1}} & frac{partial^{2} f}{partial x{n} partial x{2}} & cdots & frac{partial^{2} f}{partial x_{n}^{2}}

end{array}right]

xn 维的向量,f(x) 是实数,所以海森矩阵是 n ast n 的。

nabla f(x)n 维梯度向量,这和反向传播一样。

这个方法收敛速度很快,可以进行更高效的参数更新。在这个公式中是没有学习率这个超参数的,这相较于一阶方法是一个巨大的优势。

然而上述更新方法很难运用到实际的深度学习应用中去,这是因为计算(以及求逆)Hessian 矩阵操作非常耗费时间和空间。这样,各种各样的拟-牛顿法就被发明出来用于近似转置 Hessian 矩阵。

在这些方法中最流行的是L-BFGS,该方法使用随时间的梯度中的信息来隐式地近似(也就是说整个矩阵是从来没有被计算的)。

然而,即使解决了存储空间的问题,L-BFGS应用的一个巨大劣势是需要对整个训练集进行计算,而整个训练集一般包含几百万的样本。和小批量随机梯度下降(mini-batch SGD)不同,让 L-BFGS 在小批量上运行起来是很需要技巧,同时也是研究热点。

1.10 实际应用

Tips:默认选择Adam;如果可以承担全批量更新,可以尝试使用L-BFGS。

2.正则化

关于正则化的详细知识也可以对比阅读ShowMeAI的深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章深度学习的实用层面中关于正则化的讲解。

2.1 正则化的动机

当我们增加神经网络隐藏层的数量和尺寸时,网络的容量会上升,即神经元可以合作表达许多复杂函数。例如,如果有一个在二维平面上的二分类问题。我们可以训练 3 个不同的神经网络,每个网络都只有一个隐藏层,但是隐藏层的神经元数目不同,结果如下:

正则化; 隐藏层神经元数目不同正则化; 隐藏层神经元数目不同

在上图中,可以看见有更多神经元的神经网络可以表达更复杂的函数。然而这既是优势也是不足:

  • 优势是可以分类更复杂的数据
  • 不足是可能造成对训练数据的过拟合。

过拟合(Overfitting) 是网络对数据中的噪声有很强的拟合能力,而没有重视数据间(假设)的潜在基本关系。比如上图中:

  • 有20个神经元隐层的网络拟合了所有的训练数据,但是其代价是把决策边界变成了许多不相连的红绿区域。
  • 有3个神经元的模型的表达能力只能用比较宽泛的方式去分类数据。它将数据看做是两个大块,并把个别在绿色区域内的红色点看做噪声。在实际中,这样可以在测试数据中获得更好的泛化(generalization)能力。

那是不是说 「如果数据不是足够复杂,则小一点的网络似乎更好,因为可以防止过拟合」?

不是的,防止神经网络的过拟合有很多方法(L2正则化,Dropout和输入噪音等)。在实践中,使用这些方法来控制过拟合比减少网络神经元数目要好得多。

不应该因为害怕出现过拟合而使用小网络。相反,应该尽可能使用大网络,然后使用正则化技巧来控制过拟合。

正则化; 改变正则化强度正则化; 改变正则化强度

上图每个神经网络都有 20 个隐藏层神经元,但是随着正则化强度增加,网络的决策边界变得更加平滑。所以,正则化强度是控制神经网络过拟合的好方法

ConvNetsJS demo 上有一个小例子大家可以练练手。

2.2 正则化方法

有不少方法是通过控制神经网络的容量来防止其过拟合的:

L2正则化:最常用的正则化,通过惩罚目标函数中所有参数的平方实现。

  • 对于网络中的每个权重 w,向目标函数中增加一个 frac{1}{2} lambda w^2,1/2为了方便求导, lambda 是正则强度。
  • L2 正则化可以直观理解为它对于大数值的权重向量进行严厉惩罚,倾向于更加分散的权重向量。使网络更倾向于使用所有输入特征,而不是严重依赖输入特征中某些小部分特征。

L1正则化:是另一个相对常用的正则化方法,对于每个 w 都向目标函数增加一个 lambda mid w mid

  • L1 正则化会让权重向量在最优化的过程中变得稀疏(即非常接近 0)。在实践中,如果不是特别关注某些明确的特征选择,一般说来 L2 正则化都会比 L1 正则化效果好。
  • L1 和 L2 正则化也可以进行组合: lambda_1 mid w mid lambda_2 w^2,称作 Elastic net regularization。

最大范式约束(Max norm constraints):要求权重向量 w 必须满足 L2 范式 Vert vec{w} Vert_2 < cc 一般是 3 或 4。这种正则化还有一个良好的性质,即使在学习率设置过高的时候,网络中也不会出现数值「爆炸」,这是因为它的参数更新始终是被限制着的。

但是在神经网络中,最常用的正则化方式叫做 Dropout,下面我们详细展开介绍一下。

2.3 随机失活(Dropout)

1) Dropout概述

Dropout 是一个简单又极其有效的正则化方法,由 Srivastava 在论文 [Dropout](http://www.cs.toronto.edu/~rsalakhu/papers/srivastava14a.pdf)[: A Simple Way to Prevent Neural Networks from Overfitting](http://www.cs.toronto.edu/~rsalakhu/papers/srivastava14a.pdf) 中提出,与 L1 正则化、L2 正则化和最大范式约束等方法互为补充。

在训练的时候,随机失活的实现方法是让神经元以超参数 p (一般是 0.5)的概率被激活或者被设置为 0 。常用在全连接层。

Dropout 随机失活; 核心思路Dropout 随机失活; 核心思路

一个三层的神经网络 Dropout 示例代码实现:

代码语言:python代码运行次数:0复制
""" 普通版随机失活"""

p = 0.5   # 神经元被激活的概率。p值越高,失活数目越少

def train_step(X):
  """ X中是输入数据 """
  # 前向传播
  H1 = np.maximum(0, np.dot(W1, X)   b1)
  U1 = np.random.rand(*H1.shape) < p # 第一个随机失活掩模
  # rand可以返回一个或一组服从“0~1”均匀分布的随机样本值
  # 矩阵中满足小于p的元素为True,不满足False
  # rand()函数的参数是两个或一个整数,不是元组,所以需要*H1.shape获取行列
  H1 *= U1 # U1中False的H1对应位置置零
  H2 = np.maximum(0, np.dot(W2, H1)   b2)
  U2 = np.random.rand(*H2.shape) < p # 第二个随机失活掩模
  H2 *= U2 # drop!
  out = np.dot(W3, H2)   b3

  # 反向传播:计算梯度... (略)
  # 进行参数更新... (略)

在上面的代码中,train_step 函数在第一个隐层和第二个隐层上进行了两次随机失活。在输入层上面进行随机失活也是可以的,为此需要为输入数据 X 创建一个二值(要么激活要么失活)的掩模。反向传播几乎保持不变,只需回传梯度乘以掩模得到 Dropout 层的梯度。

2) Dropout的理解

为什么这个想法可取呢?一个解释是防止特征间的相互适应:

  • 比如每个神经元学到了猫的一个特征比如尾巴、胡须、爪子等,将这些特征全部组合起来可以判断是一只猫。
  • 加入随机失活后就只能依赖一些零散的特征去判断不能使用所有特征,这样可以一定程度上抑制过拟合。不然训练时正确率很高,测试时却很低。
Dropout 随机失活; 防止特征间的互相适应Dropout 随机失活; 防止特征间的互相适应

另一个比较合理的解释是:

  • 在训练过程中,随机失活可以被认为是对完整的神经网络抽样出一些子集,每次基于输入数据只更新子网络的参数。
  • 每个二值掩模都是一个模型,有 n 个神经元的网络有 2n 种掩模。Dropout 相当于数量巨大的网络模型(共享参数)在同时被训练。
Dropout 随机失活; 相当于抽样出一些子集Dropout 随机失活; 相当于抽样出一些子集
3) 测试时避免随机失活

在训练过程中,失活是随机的,但是在测试过程中要避免这种随机性,所以不使用随机失活,要对数量巨大的子网络们做模型集成(model ensemble),以此来计算出一个预测期望。

比如只有一个神经元 a

Dropout 随机失活; 测试时避免随机失活Dropout 随机失活; 测试时避免随机失活

测试的时候由于不使用随机失活所以:

text{E}(a)=w_1x w_2y

假如训练时随机失活的概率为 0.5,那么:

text{E}(a)=frac{1}{4}(w_1x w_2y) frac{1}{2}(w_1x w_2cdot 0) frac{1}{2}(w_1xcdot 0 w_2) frac{1}{4}cdot 0=frac{1}{2}(w_1x w_2y)

所以一个不确切但是很实用的做法是在测试时承随机失活概率,这样就能保证预测时的输出和训练时的期望输出一致。所以测试代码:

代码语言:python代码运行次数:0复制
def predict(X):
  # 前向传播时模型集成
  H1 = np.maximum(0, np.dot(W1, X)   b1) * p # 注意:激活数据要乘以p
  H2 = np.maximum(0, np.dot(W2, H1)   b2) * p # 注意:激活数据要乘以p
  out = np.dot(W3, H2)   b3

上述操作不好的地方是必须在测试时对激活数据按照失活概率 p 进行数值范围调整。测试阶段性能是非常关键的,因此实际操作时更倾向使用反向随机失活(inverted dropout)

  • 在训练时就进行数值范围调整,从而让前向传播在测试时保持不变。

反向随机失活还有一个好处,无论是否在训练时使用 Dropout,预测的代码可以保持不变。参考实现代码如下:

代码语言:python代码运行次数:0复制
""" 
反向随机失活: 推荐实现方式.
在训练的时候drop和调整数值范围,测试时不做任何事.
"""
p = 0.5
def train_step(X):
  # 前向传播
  H1 = np.maximum(0, np.dot(W1, X)   b1)
  U1 = (np.random.rand(*H1.shape) < p) / p # 第一个随机失活遮罩. 注意/p!
  H1 *= U1 # drop!
  H2 = np.maximum(0, np.dot(W2, H1)   b2)
  U2 = (np.random.rand(*H2.shape) < p) / p # 第二个随机失活遮罩. 注意/p!
  H2 *= U2 # drop!
  out = np.dot(W3, H2)   b3

def predict(X):
  # 前向传播时模型集成
  H1 = np.maximum(0, np.dot(W1, X)   b1) # 不用数值范围调整了
  H2 = np.maximum(0, np.dot(W2, H1)   b2)
  out = np.dot(W3, H2)   b3

在更一般化的分类上,随机失活属于网络在前向传播中有随机行为的方法。这种在训练过程加入随机性,然后在测试过程中对这些随机性进行平均或近似的思想在很多地方都能见到:

  • 批量归一化:训练时的均值和方差来自随机的小批量;测试时使用的是整个训练过程中的经验方差和均值。
  • 数据增强(data augmentation) :比如一张猫的图片进行训练时,可以随机的裁剪翻转等操作再训练,然后测试过程再对一些固定的位置(四个角、中心及翻转)进行测试。也可以在训练的时候随机改变亮度对比度,色彩抖动PCA降维等。
  • DropConnect:另一个与 Dropout 类似的研究是 DropConnect,它在前向传播的时候,一系列权重被随机设置为 0
  • 部分最大池化(Fractional Max Pooling) :训练时随机区域池化,测试时固定区域或者取平均值。这个方法并不常用。
  • 随机深度(Stochastic Depth) :一个比较深的网络,训练时随机选取部分层去训练,测试时使用全部的层。这个研究非常前沿。

总之,这些方法都是在训练的时候增加随机噪声,测试时通过分析法(在使用随机失活的本例中就是乘以 p)或数值法(例如通过抽样出很多子网络,随机选择不同子网络进行前向传播,最后对它们取平均)将噪音边缘化。

4) 实践经验

一些常用的实践经验方法:

  • 可以通过交叉验证获得一个全局使用的 L2 正则化系数。
  • 使用 L2 正则化的同时在所有层后面使用随机失活

随机失活 p值一般默认设为 0.5,也可能在验证集上调参

3.迁移学习(Transfer Learning)

关于迁移学习的详细知识也可以对比阅读ShowMeAI的 深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 AI 应用实践策略(下) 中关于正则化的讲解。

另一个导致过拟合的原因可能是训练样本过少,这时可以使用迁移学习来解决这个问题,它允许使用很少的数据来训练 CNN。

3.1 迁移学习的思想

迁移学习; Transfer Learning迁移学习; Transfer Learning
  • 第①步:在大量的数据集上训练一个 CNN,得到模型(比如使用 ImageNet,有 1000 个分类)
  • 第②步:使用一个少量的数据集,最后需要的得到的分类也不再是 1000 而是一个较小的值 C,比如10。这时最后一个全连接层的参数矩阵变成 4096 times C,初始化这个矩阵,重新训练这个线性分类器,保持前面的所有层不变,因为前面的层已经训练好了,有了泛化能力。
  • 第③步:当得到较多的训练集后,训练的层数可以增多,比如可以训练最后三个全连接层。可以使用较低的学习率微调参数。
迁移学习; Transfer Learning迁移学习; Transfer Learning

3.2 应用

在目标检测和图像标记中都会使用迁移学习,图像处理部分都使用一个已经用 ImageNet 数据预训练好的 CNN 模型,然后根据具体的任务微调这些参数。

所以对一批数据集感兴趣但是数量不够时,可以在网上找一个数据很相似的有大量数据的训练模型,然后针对自己的问题微调或重新训练某些层。一些常用的深度学习软件包都含有已经训练好的模型,直接应用就好。

  • Caffe: https://github.com/BVLC/caffe/wiki/Model-Zoo
  • TensorFlow: https://github.com/tensorflow/models
  • PyTorch: https://github.com/pytorch/vision

4.模型集成(Model Ensembles)

在实践的时候,有一个总是能提升神经网络几个百分点准确率的办法,就是在训练的时候训练几个独立的模型,然后在测试的时候平均它们预测结果。

集成的模型数量增加,算法的结果也单调提升(但提升效果越来越少)。

模型之间的差异度越大,提升效果可能越好。

进行集成有以下几种方法:

  • 同一个模型,不同的初始化。使用交叉验证来得到最好的超参数,然后用最好的参数来训练不同初始化条件的模型。这种方法的风险在于多样性只来自于不同的初始化条件。
  • 在交叉验证中发现最好的模型。使用交叉验证来得到最好的超参数,然后取其中最好的几个(比如10个)模型来进行集成。这样就提高了集成的多样性,但风险在于可能会包含不够理想的模型。在实际操作中,这样操作起来比较简单,在交叉验证后就不需要额外的训练了。
  • 一个模型设置多个记录点 ( checkpoints ) 。如果训练非常耗时,那就在不同的训练时间对网络留下记录点(比如每个周期结束),然后用它们来进行模型集成。很显然,这样做多样性不足,但是在实践中效果还是不错的,这种方法的优势是代价比较小。
  • 在训练的时候跑参数的平均值。和上面一点相关的,还有一个也能得到1-2个百分点的提升的小代价方法,这个方法就是在训练过程中,如果损失值相较于前一次权重出现指数下降时,就在内存中对网络的权重进行一个备份。这样你就对前几次循环中的网络状态进行了平均。你会发现这个「平滑」过的版本的权重总是能得到更少的误差。直观的理解就是目标函数是一个碗状的,你的网络在这个周围跳跃,所以对它们平均一下,就更可能跳到中心去。

5.拓展学习

可以点击 B站 查看视频的【双语字幕】版本

  • 【课程学习指南】斯坦福CS231n | 深度学习与计算机视觉
  • 【字幕 资料下载】斯坦福CS231n | 深度学习与计算机视觉 (2017·全16讲)
  • 【CS231n进阶课】密歇根EECS498 | 深度学习与计算机视觉
  • 【深度学习教程】吴恩达专项课程 · 全套笔记解读
  • 【Stanford官网】CS231n: Deep Learning for Computer Vision

6.要点总结

  • 优化方式:SGD、动量更新、Nesterov动量、Adagrad、RMSProp、Adam等,一般无脑使用Adam。此外还有学习率退火和二阶方法。
  • 正则化:L2比较常用,Dropout也是一个很好的正则方法。
  • 数据较少时可以使用迁移学习。
  • 模型集成。

0 人点赞