Neural Nets 3

内容列表

  • 梯度检查
  • 合理性(Sanity)检查
  • 检查学习过程
    • 损失函数
    • 训练集与验证集准确率
    • 权重:更新比例
    • 每层的激活函数与梯度分布
    • 可视化
  • 参数更新
    • 一阶(随机梯度下降)方法,动量方法,Nesterov动量方法
    • 学习率退火
    • 二阶方法
    • 逐参数适应学习率方法(Adagrad,RMSProp)
  • 超参数调优
  • 评价
    • 模型集成
  • 拓展阅读

Learning

在前面章节中,我们讨论了神经网络的静态部分:如何创建网络的连接、数据和损失函数。本节将致力于讲解神经网络的动态部分,即神经网络学习参数和搜索最优超参数的过程。

Gradient Checks

把解析梯度和数值计算梯度进行比较。

使用中心化公式: $$\frac{df(x)}{dx} = \frac{f(x + h) - f(x-h)}{2h}$$ 而不是 $$\frac{df(x)}{dx} = \frac{f(x + h) - f(x)}{h}$$

使用相对误差来比较:假设数值梯度表示为$f_n'$,解析梯度表示为$f_a'$,使用相对误差判断两者是否匹配更加合适:

$$\frac{\mid f'_a - f'_n \mid}{\max(\mid f'_a \mid, \mid f'_n \mid)}$$

在实践中:

  • $相对误差>1e-2$:梯度可能出错
  • $1e-2>相对误差>1e-4$:不是很好
  • $1e-4>相对误差$:对于有不可导点的目标函数是OK的,但是如果目标函数中没有kink(使用tanh和softmax),则相对误差还是偏高
  • $1e-7或更小$:Good!

但需要注意的是,网络的深度越深,相对误差就越高,因为误差一直在累积,所以如果是在对一个10层网络的输入数据做梯度检查,那么可能1e-2的相对误差就OK了。

使用双精度

保持在浮点数的有效范围:建议阅读《What Every Computer Scientist Should Konw About Floating-Point Artthmetic

目标函数的不可导点(kinks):不可导点是指目标函数不可导的部分,由ReLU(max(0,x))等函数,或SVM损失,Maxout神经元等引入。

注意,在计算损失的过程中是可以知道不可导点有没有被越过的。在具有$\max(x,y)$形式的函数中持续跟踪所有“赢家”的身份,就可以实现这一点。其实就是看在前向传播时,到底$x$和$y$谁更大。如果在计算$f(x+h)$和$f(x-h)$的时候,至少有一个“赢家”的身份变了,那就说明不可导点被越过了,数值梯度会不准确。

使用少量数据点:解决上面的不可导点问题的一个办法是使用更少的数据点。因为含有不可导点的损失函数(ReLU或者边缘损失等函数)的数据点越少,不可导点就越少,所以在计算有限差值近似时越过不可导点的几率就越小。

谨慎设置步长h:有时候如果梯度检查无法进行,可以试试将$h$调到$1e-4$或者$1e-6$,然后突然梯度检查可能就恢复正常。可以参考wiki

在操作的特性模式中梯度检查:为了安全起见,最好让网络学习(“预热”)一小段时间,等到损失函数开始下降之后再进行梯度检查。在第一次迭代就进行梯度检查的危险就在于,此时可能正处在不正常的边界情况,从而掩盖了梯度没有正确实现的事实。

不要让正则化吞没数据:通常损失函数是数据损失和正则化损失的和。推荐先关掉正则化对数据损失做单独检查,然后对正则化做单独检查。对于正则化的单独检查可以是修改代码,去掉其中数据损失的部分,也可以提高正则化强度,确认其效果在梯度检查中是无法忽略的,这样不正确的实现就会被观察到了。

关闭随机失活(dropout)和数据扩张(augmentation):在进行梯度检查时,记得关闭网络中任何不确定的效果的操作。关闭这些操作不好的一点是无法对它们进行梯度检查(例如随机失活的反向传播实现可能有错误)。因此,一个更好的解决方案就是在计算$f(x+h)$和$f(x-h)$前强制增加一个特定的随机种子,在计算解析梯度时也同样如此

检查少量的维度在所有不同的参数中都抽取一部分来梯度检查

Before learning: sanity checks Tips/Tricks

寻找特定情况的正确损失值:在使用小参数进行初始化时,确保得到的损失值与期望一致。最好先单独检查数据损失(让正则化强度为0)。

提高正则化强度时导致损失值变大

对小数据子集过拟合:在整个数据集进行训练之前,尝试在一个很小的数据集上进行训练(比如20个数据),然后确保能到达0的损失值。进行这个实验的时候,最好让正则化强度为0,不然它会阻止得到0的损失。

Babysitting the learning process

在下面的图表中,x轴通常都是表示周期(epochs)单位,该单位衡量了在训练中每个样本数据都被观察过次数的期望(一个周期意味着每个样本数据都被观察过了一次)。相较于迭代次数(iterations),一般更倾向跟踪周期,这是因为迭代次数与数据的批尺寸(batchsize)有关,而批尺寸的设置又可以是任意的。

Loss function

下图展示的是随着损失值随时间的变化,尤其是曲线形状会给出关于学习率设置的情况:

左图展示了不同的学习率的效果。右图显示了一个典型的随时间变化的损失函数值。

损失值的震荡程度和批尺寸(batch size)有关,当批尺寸为1,震荡会相对较大。当批尺寸就是整个数据集时震荡就会最小,因为每个梯度更新都是单调地优化损失函数(除非学习率设置得过高)。

有时损失函数看起来很有趣:lossfunctions.tumblr.com

Train/Val accuracy

通过跟踪验证集和训练集的准确率可以知道模型过拟合的程度:

在训练集准确率和验证集准确率中间的空隙指明了模型过拟合的程度。在图中,蓝色的验证集曲线显示相较于训练集,验证集的准确率低了很多,这就说明模型有很强的过拟合。遇到这种情况,就应该增大正则化强度(更强的L2权重惩罚,更多的随机失活等)或收集更多的数据。另一种可能就是验证集曲线和训练集曲线如影随形,这种情况说明你的模型容量还不够大:应该通过增加参数数量让模型容量更大些。

Ratio of weights:updates

权重中更新值的数量和全部值的数量之间的比例。需要对每个参数集的更新比例进行单独的计算和跟踪。一个经验性的结论是这个比例应该在$1e-3$左右。如果更低,说明学习率可能太小,如果更高,说明学习率可能太高。

# 假设参数向量为W,其梯度向量为dW
param_scale = np.linalg.norm(W.ravel())
update = -learning_rate*dW # 简单SGD更新
update_scale = np.linalg.norm(update.ravel())
W += update # 实际更新
print update_scale / param_scale # 要得到1e-3左右

Activation / Gradient distributions per layer

一个不正确的初始化可能让学习过程变慢,甚至彻底停止。还好,这个问题可以比较简单地诊断出来。其中一个方法是输出网络中所有层的激活数据和梯度分布的柱状图。

First-layer Visualizations

如果数据是图像像素数据,那么把第一层特征可视化会有帮助。

Parameter updates

SGD and bells and whistles

普通更新(Vanilla update):沿着负梯度方向改变参数

# 普通更新
x += -learning_rate*dx

动量更新(Momentum update)

# 动量更新
v = mu * v - learning_rate * dx # 与速度融合
x += v # 与位置融合

引入了一个初始化为0的变量v和一个超参数mu,其中mu在最优化的过程中被看做动量(一般设为0.9),但其物理意义与摩擦系数更一致。通过交叉验证,这个参数通常设为[0.5,0.9,0.95,0.99]中的一个。动量随时间变化的设置有时能略微改善最优化的效果,其中动量在学习过程的后阶段会上升。一个典型的设置是刚开始将动量设为0.5而在后面的多个周期(epoch)中慢慢提升到0.99。

通过动量更新,参数向量会在任何有持续梯度的方向上增加速度。

Nesterov动量更新:理论上对于凸函数能得到更好的收敛。

Nesterov动量的核心思路是,当参数向量位于某个位置x时,观察上面的动量更新公式可以发现,动量部分(忽视带梯度的第二个部分)会通过$mu * v$稍微改变参数向量。因此,如果要计算梯度,那么可以将未来的近似位置$x + mu * v$看做是“向前看”,这个点在我们一会儿要停止的位置附近。因此,计算$x + mu * v$的梯度而不是“旧”位置x的梯度就有意义了。 实现代码如下:

x_ahead = x + mu * v
# 计算dx_ahead(在x_ahead处的梯度,而不是在x处的梯度)
v = mu * v - learning_rate * dx_ahead
x += v

或者等价的:

v_prev = v # 存储备份
v = mu * v - learning_rate * dx # 速度更新保持不变
x += -mu * v_prev + (1 + mu) * v # 位置更新变了形式

Annealing the learning rate

如果学习率很高,系统的动能就过大,参数向量就会无规律地跳动,不能够稳定到损失函数更深更窄的部分去。通常,实现学习率退火有3种方式:

  • 随步数衰减:每进行几个周期就根据一些因素降低学习率。典型的值是每过5个周期就将学习率减少一半,或者每20个周期减少到之前的0.1。在实践中可能看见这么一种经验做法:使用一个固定的学习率来进行训练的同时观察验证集错误率,每当验证集错误率停止下降,就乘以一个常数(比如0.5)来降低学习率
  • 指数衰减:$\alpha=\alpha_0e^{-kt}$,其中$t$是迭代次数(或周期),$\alpha_0,k$是hyperparameters
  • 1/t衰减:$\alpha=\alpha_0/(1+kt)$,其中$\alpha_0,k$是hyperparameters,$t$是迭代次数

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

Second order methods

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

$$\displaystyle x\leftarrow x-[Hf(x)]^{-1}\nabla f(x)$$

其中$Hf(x)$是$Hessian$矩阵,它是函数的二阶偏导数的方阵。$\nabla f(x)$是梯度向量

在这个公式中是没有学习率这个超参数的,这相较于一阶方法是一个巨大的优势。

但由于计算Hessian矩阵十分耗费时空资源,许多拟牛顿法被发明出来用于近似转置Hessian矩阵,如L-BFGS,该方法使用随时间的梯度中的信息来隐式地近似(没有实际计算整个矩阵)。

Per-parameter adaptive learning rate methods

Adagrad:由Duchi等提出的适应性学习率算法

# 假设有梯度和参数向量x
cache += dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

变量cache将用来归一化参数更新步长,归一化是逐元素进行的。接收到高梯度值的权重更新的效果被减弱,而接收到低梯度值的权重的更新效果将会增强。用于平滑的式子eps(一般设为$1e-4$到$1e-8$之间)是防止出现除以0的情况。

RMSProp:修改了Adagrad方法,使用了一个梯度平方的滑动平均。但是和Adagrad不同,其更新不会让学习率单调变小。Reference

cache = decay_rate * cache + (1 - decay_rate) * dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

其中decay_rate常用值为[0.9,0.99,0.999]

AdamReference

m = beta1*m + (1-beta1)*dx
v = beta2*v + (1-beta2)*(dx**2)
x += - learning_rate * m / (np.sqrt(v) + eps)

论文中推荐的参数值$eps=1e-8, beta1=0.9, beta2=0.999$。

完整的Adam更新算法也包含了一个偏置(bias)矫正机制,因为m,v两个矩阵初始为0,在没有完全热身之前存在偏差,需要采取一些补偿措施。

推荐的两个更新方法是SGD+Nesterov动量方法,或者Adam方法。

Hyperparameter optimization

神经网络最常用的设置有:

  • 初始学习率。
  • 学习率衰减方式(例如一个衰减常量)。
  • 正则化强度(L2惩罚,dropout强度)。

实现:更大的神经网络需要更长的时间去训练,所以调参可能需要几天甚至几周。记住这一点很重要,因为这会影响你设计代码的思路。一个具体的设计是用仆程序持续地随机设置参数然后进行最优化。在训练过程中,仆程序会对每个周期后验证集的准确率进行监控,然后向文件系统写下一个模型的记录点(记录点中有各种各样的训练统计数据,比如随着时间的损失值变化等),这个文件系统最好是可共享的。在文件名中最好包含验证集的算法表现,这样就能方便地查找和排序了。然后还有一个主程序,它可以启动或者结束计算集群中的仆程序,有时候也可能根据条件查看仆程序写下的记录点,输出它们的训练统计数据等。

比起交叉验证最好使用一个验证集

超参数的范围:在对数尺度上进行超参数搜索。比起加上或者减少某些值,思考学习率的范围是乘以或者除以某些值更加自然。但是有一些参数(比如随机失活)还是在原始尺度上进行搜索(例如:dropout=uniform(0,1))。

随机搜索优于网络搜索:Reference: Random Search for Hyper-Parameter Optimization,

小心边界上的最优值:一旦我们得到一个比较好的值,一定要确认你的值不是出于这个范围的边界上,不然可能会错过其他更好的搜索范围。

由粗到细地分阶段搜索:先进性粗略范围搜索,然后根据好的结果出现的地方缩小范围进行搜索。

进行粗搜索的时候,让模型训练_一个周期_就可以了,因为很多超参数的设定会让模型没法学习,或者突然就爆出很大的损失值。第二个阶段就是对一个更小的范围进行搜索,这时可以让模型运行5个周期,而最后一个阶段就在最终的范围内进行仔细搜索,运行很多次周期

贝叶斯超参数优化:在不同超参数设置下查看算法性能时,要在探索和使用中进行合理的权衡。

Evaluation

Model Ensembles

在训练的时候训练几个独立的模型,然后在测试的时候平均它们预测结果。集成的模型数量增加,算法的结果也单调提升(但提升效果越来越少)。还有模型之间的差异度越大,提升效果可能越好。进行集成有以下几种方法:

  • 同一个模型,不同的初始化:使用交叉验证得到最好的hyperparameter,然后用这些参数训练不同初始化条件的模型
  • 在交叉验证中发现最好的模型
  • 一个模型设置多个记录点
  • 训练的时候跑参数的平均值:在训练过程中,如果损失值相较于前一次权重和出现指数下降时,就在内存中对网络的权重进行一个备份

Further Readings