把解析梯度和数值计算梯度进行比较。
使用中心化公式: $$\frac{df(x)}{dx} = \frac{f(x + h) - f(x)}{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)}$$在实践中:
但需要注意的是,网络的深度越深,相对误差就越高,因为误差一直在累积,所以如果是在对一个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)$前强制增加一个特定的随机种子,在计算解析梯度时也同样如此。
检查少量的维度:在所有不同的参数中都抽取一部分来梯度检查。
寻找特定情况的正确损失值:在使用小参数进行初始化时,确保得到的损失值与期望一致。最好先单独检查数据损失(让正则化强度为0)。
提高正则化强度时导致损失值变大
对小数据子集过拟合:在整个数据集进行训练之前,尝试在一个很小的数据集上进行训练(比如20个数据),然后确保能到达0的损失值。进行这个实验的时候,最好让正则化强度为0,不然它会阻止得到0的损失。
在下面的图表中,x轴通常都是表示周期(epochs)单位,该单位衡量了在训练中每个样本数据都被观察过次数的期望(一个周期意味着每个样本数据都被观察过了一次)。相较于迭代次数(iterations),一般更倾向跟踪周期,这是因为迭代次数与数据的批尺寸(batchsize)有关,而批尺寸的设置又可以是任意的。
下图展示的是随着损失值随时间的变化,尤其是曲线形状会给出关于学习率设置的情况:
损失值的震荡程度和批尺寸(batch size)有关,当批尺寸为1,震荡会相对较大。当批尺寸就是整个数据集时震荡就会最小,因为每个梯度更新都是单调地优化损失函数(除非学习率设置得过高)。
有时损失函数看起来很有趣:lossfunctions.tumblr.com
通过跟踪验证集和训练集的准确率可以知道模型过拟合的程度:
权重中更新值的数量和全部值的数量之间的比例。需要对每个参数集的更新比例进行单独的计算和跟踪。一个经验性的结论是这个比例应该在$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左右
一个不正确的初始化可能让学习过程变慢,甚至彻底停止。还好,这个问题可以比较简单地诊断出来。其中一个方法是输出网络中所有层的激活数据和梯度分布的柱状图。
如果数据是图像像素数据,那么把第一层特征可视化会有帮助。
普通更新(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 # 位置更新变了形式
如果学习率很高,系统的动能就过大,参数向量就会无规律地跳动,不能够稳定到损失函数更深更窄的部分去。通常,实现学习率退火有3种方式:
在实践中,我们发现随步数衰减的随机失活(dropout)更受欢迎,因为它使用的超参数(衰减系数和以周期为时间单位的步数)比k更有解释性。
在深度网络背景下,第二类常用的最优化方法是基于牛顿法的,其迭代如下:
$$\displaystyle x\leftarrow x-[Hf(x)]^{-1}\nabla f(x)$$其中$Hf(x)$是$Hessian$矩阵,它是函数的二阶偏导数的方阵。$\nabla f(x)$是梯度向量
在这个公式中是没有学习率这个超参数的,这相较于一阶方法是一个巨大的优势。
但由于计算Hessian矩阵十分耗费时空资源,许多拟牛顿法被发明出来用于近似转置Hessian矩阵,如L-BFGS,该方法使用随时间的梯度中的信息来隐式地近似(没有实际计算整个矩阵)。
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]
Adam:Reference
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方法。
神经网络最常用的设置有:
实现:更大的神经网络需要更长的时间去训练,所以调参可能需要几天甚至几周。记住这一点很重要,因为这会影响你设计代码的思路。一个具体的设计是用仆程序持续地随机设置参数然后进行最优化。在训练过程中,仆程序会对每个周期后验证集的准确率进行监控,然后向文件系统写下一个模型的记录点(记录点中有各种各样的训练统计数据,比如随着时间的损失值变化等),这个文件系统最好是可共享的。在文件名中最好包含验证集的算法表现,这样就能方便地查找和排序了。然后还有一个主程序,它可以启动或者结束计算集群中的仆程序,有时候也可能根据条件查看仆程序写下的记录点,输出它们的训练统计数据等。
比起交叉验证最好使用一个验证集
超参数的范围:在对数尺度上进行超参数搜索。比起加上或者减少某些值,思考学习率的范围是乘以或者除以某些值更加自然。但是有一些参数(比如随机失活)还是在原始尺度上进行搜索(例如:dropout=uniform(0,1))。
随机搜索优于网络搜索:Reference: Random Search for Hyper-Parameter Optimization,
小心边界上的最优值:一旦我们得到一个比较好的值,一定要确认你的值不是出于这个范围的边界上,不然可能会错过其他更好的搜索范围。
由粗到细地分阶段搜索:先进性粗略范围搜索,然后根据好的结果出现的地方缩小范围进行搜索。
进行粗搜索的时候,让模型训练_一个周期_就可以了,因为很多超参数的设定会让模型没法学习,或者突然就爆出很大的损失值。第二个阶段就是对一个更小的范围进行搜索,这时可以让模型运行5个周期,而最后一个阶段就在最终的范围内进行仔细搜索,运行很多次周期。
贝叶斯超参数优化:在不同超参数设置下查看算法性能时,要在探索和使用中进行合理的权衡。
在训练的时候训练几个独立的模型,然后在测试的时候平均它们预测结果。集成的模型数量增加,算法的结果也单调提升(但提升效果越来越少)。还有模型之间的差异度越大,提升效果可能越好。进行集成有以下几种方法: