CS231n-2017 Lecture7训练神经网络(二)笔记
本节主要是神经网络的动态部分,也就是神经网络学习参数和搜索最优超参数的过程
梯度检查:
进行梯度检查,就是简单地把解析梯度与数值计算梯度进行比较,防止反向传播的逻辑出错,仅在调试过程中使用。有如下技巧 :
使用中心化公式:
在使用有限差值近似计算数值梯度时,常见的公式是:
h是一个很小,近似为1e-5的数,但这个公式不常用且不好用
经常使用的是下方的中心化公式:
该公式在检查每个梯度的维度的时候,要求计算两次损失函数,但梯度的近似值会准确很多,因为使用泰勒展开(这里假设原函数可以泰勒展开),则中心化公式的误差近似
使用相对误差来比较:
比较数值梯度与解析梯度
有哪些需要注意的细节,进而知道他们不匹配呢?
通常使用相对误差来进行量化,即:
当然,还必须注意两个式子的梯度都为0的情况
量化阈值如下:
相对误差>1e-2:通常意味着梯度出错
1e-2>相对误差>1e-4:较有可能出错
1e-4>相对误差:对有不可导点的目标函数可以接受,但若目标函数中没有(tanh和softmax),这个相对误差还是太高
1e-7>相对误差:认为正常
以上的浮点数运算都要使用双精度浮点数
目标函数的不可导点(kinks):
在进行梯度检查时,不可导点是导致不准确的原因之一,可能是由ReLU,SVM损失、Maxout神经元等引入,也就是x+h在不可导点的一侧,而x-h在不可导点的另一侧导致的。在计算损失的过程中,是可以知道不可导点有没有被跨过的,在具有max形式的函数中持续追踪保留梯度的变量身份,就可以实现这一点,若在计算f(x+h)和f(x-h)的时候,至少有一个变量其梯度保留情况变了,就说明不可导点被越过了,数值梯度会不准确
使用少量数据点:
解决上述不可导点问题的一个方法,因为含有不可导点的损失函数的数据点越少,在计算梯度时越过不可导点的概率就越小,所以使用少量数据点,可以使梯度检查变高效
设置合适的步长h:
数值计算梯度的h并不是越小越好,当h特别小时,很可能会遇到数值精度问题,当梯度检查出问题时,不妨将h调大一点,可能会恢复正常
梯度检查的时间:
梯度检查时在参数空间中的一个特定单独点(往往随机取)进行的,即使是在该点梯度检查成功了,也不能马上确保全局上的梯度实现都是正确的,且随机的初始化可能不是参数空间中最优的代表性的点,这可能会导致梯度表面上正确实现。比如,SVM使用小数值权重初始化,就会把一些接近于0的得分分配给所有的数据点,而梯度将会在所有的数据点中展现出某种模式,不正确实现的梯度也许仍然能够产生出这种模式,但是其无法泛化到更具代表性的操作模式,比如在一些数据点的得分比另一些要大的时候失效。因此,最好让网络学习先“预热”一小段时间,等到损失函数开始下降之后,再进行梯度检查。在第一次迭代就进行梯度检查的话,若此时处于不正确的梯度边界情况,无法被察觉,从而掩盖了梯度没有正常实现的事实
正则化盖过数据损失:
Loss通常是数据损失和正则化损失的和,在某种情况,正则化损失的梯度有可能会远大于数据损失,进而掩盖掉数据损失梯度的不正确实现。因此,推荐先关掉正则化,单独对数据损失做梯度检查,然后再对正则化损失做梯度检查
关闭Dropout和数据扩张(augmentation):
在进行梯度检查时,需要关闭具有不确定效果的操作,比如Dropout和随机数据扩展,否则会在计算数值梯度时导致巨大误差。但是,关闭这些操作,会导致无法对这些操作进行梯度检查,所以,更好的方法是在计算f(x+h)和f(x-h)之前,强制增加一个特定的随机种子,确保伪随机
检查少量的维度:
实际模型中,梯度可以有上百万的参数,这种情况下只能够检查其中的一些维度,然后假设其他维度是正确的,但是要确认在所有不同的参数中都抽取一部分来梯度检查,比如可能偏置会占掉权重矩阵的一部分,则随机有概率只随机到偏置参数
进行参数学习之前的合理性检查:
1.寻找特定情况的正确损失值
在使用小参数进行初始化时,确保得到的损失值与期望一致。最好先单独检查数据损失(令正则化损失为0),若跑出来损失值与期望不一致,那么可能在初始化中就出了问题
2.提高正则化强度时,观察损失值是否变大
3.对小数据子集过拟合:
在整个数据集进行训练之前,尝试在一个很小的数据集上进行训练,然后确保能到达0的损失值,最好令正则化强度为0。除非能通过这一个过拟合检查,否则对整个数据集进行训练时没有意义的。但是,能对小数据子集进行过拟合,不代表完全正确,仍有可能存在不正确的实现,比如,因为某些错误原因,数据点的特征是随机的,这样算法也有可能对小数据进行过拟合,但在整个数据集上训练的时候,就不会产生泛化能力。
检查整个学习过程:
在训练神经网络时,需要追踪多个重要数值
在下方的图表中,x轴通常表示周期(epochs)单位,该单位衡量了在训练中每个样本数据都被观测过的次数的期望
损失函数:
训练期间第一个要跟踪的数字就是Loss,它在前向传播时对每个独立的batch数据进行计算
左图不同曲线对应了不同学习率下loss随epoch的变化,可以看到,过低的学习率导致loss的下降是现行的,高一些的学习率会看起来呈几何指数下降。更高的学习率会使Loss下降得很快,但是接下来就会停在一个不好的损失值上(绿线)。这是因为最优化的能量太大,参数只能在混沌中随机震荡,无法最优化到一个很好的点上。
右图显示了一个经典的随时间变化的损失函数值
损失值的震荡程度和batch size有关,当batch size = 1时,震荡会相对较大,当batch size 就是整个数据集时,震荡就会最小,因为这时每次梯度更新都是在单调地优化Loss
训练集和验证集准确率:
在训练分类器的时候,需要跟踪的第二重要的数字就是验证集和训练集的准确率,这个图表能够使我们了解模型过拟合的程度
训练集准确率和验证集准确率之间的间隔指明了模型过拟合的程度,比如
蓝色的验证集正确率相较于训练集正确率低了很多,就说明模型有很强的过拟合,遇到这种情况,就应该增大正则化强度,或者收集更多的训练数据
另一种情况是验证集曲线和训练集曲线相近,这种情况说明模型参数容量不够大,需要增大参数数量
权重更新比例:
最后一个需要跟踪的量是权重中所有更新值的和与全部权重值之和的比例,一个经验的结论是,这个比例应该要在1e-3左右,如果更低,说明学习率太小,如果更高,说明学习率过高
每层的激活数据及梯度分布:
一个不正确的初始化可能使学习过程变慢,甚至停止。其中一个检查方法就是输出网络所有层的激活数据和梯度的柱状图,直观的来说,如果看到任何奇怪的分布情况,那很可能有异常。如,对使用tanh的神经元,我们应该看到激活数据的值在整个[-1,1]区间中都有分布,如果看到神经元的输出全都是0,或者全都聚集在-1/1,那就肯定有问题了
第一层可视化:
如果数据是图像像素数据,那么把第一层特征可视化会有帮助,如图:
这是将神经网络第一层权重可视化的例子,可以发现左侧的特征中充满了噪声,这暗示网络可能出现了问题:网络没有收敛、学习率设置不恰当,正则化惩罚的权重过低等
而右图的特征就不错,平滑干净且种类繁多,说明训练过程良好进行
参数更新:
在能使用反向传播计算梯度的前提下,梯度就能够被用来进行参数更新了
随机梯度下降及各种更新方法:
普通更新:
最简单的更新形式就是沿着负梯度方向改变参数(因为梯度指向的是上升方向,但我们通常希望最小化损失函数),假设有参数向量x和梯度dx,则简单的更新形式为:
x+= - learning_rate * dx
动量(Momentum)更新:
这个方法在深度网络中几乎总能得到更好的收敛速度
原理:
我们将Loss理解为山的高度(而重力势能是U=mgh,所以有U正比于h),用随机数字初始化参数等同于在某个位置给质点设置初速度为0,则最优化过程可以看作是模拟参数向量(质点)在地形上滚动的过程
因为作用于质点的力与梯度的潜在能量有关(),质点所受的力就是损失函数的负梯度,又因为
,所以负梯度与质点的加速度是成比例的。即梯度影响加速度,加速度影响质点的速度,速度再影响质点在山中的位置
更新形式:
v = mu * v - learning_rate * dx // 更新速度
x += v //更新位置
这里引入了一个初始化为0的变量v,和一个超参数mu,mu被看作动量(一般设置为0.9),其有效地抑制了速度,降低了系统的动能(即质点上次的速度方向会影响该次的速度方向)
mu通常设置为[0.5, 0.9, 0.95, 0.99]中的一个,动量随时间慢慢提升有时能略微改善最优化的结果
Nesterov动量:
与不同变量不同,其核心思想是,观察上文的动量公式,x会通过mu*v而稍微改变,而mu*v是在还没计算梯度之前就已经确定的,我们可以将未来的近似位置x+mu*v看作是“预测未来”,x+mu*v这个点一定在我们等会梯度下降更新后要停止的位置附近,因此,我们不妨计算x+mu*v的梯度,而不是x的梯度
如图所示,既然我们知道mu*v会把我们带到绿色箭头指向的点,我们就不在原地(红色点)计算梯度了,我们在绿色箭头所指的点计算梯度
#易于理解的实现版本
x_ahead = x + mu * v
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
学习率退火:
在训练深度网络的时候,让学习率随着时间退火(逐渐减小)通常是有帮助的。因为如果学习率很高,系统的动能就过大,参数向量就会无规律地跳动,不能够稳定到损失函数更深更窄的局部极值去。
实现学习率退火的方式:
1.随步数衰减:
每进行几个epoch就根据一些因素降低学习率。典型的是每过5个epoch就将学习率减少一半,或者每20个epoch将学习率减少到之前的0.1,具体数字设定严重依赖于具体问题和模型。
经验做法:使用一个固定的学习率来进行训练的同时观察验证集的错误,每当验证集错误率停止下降时,就乘一个常数(例如0.5)来降低学习率
2.指数衰减:
公式为,其中
为初始学习率,k是超参数,t是迭代次数,也可以使用epoch
3.1/t衰减
公式为
在实践中,随步数衰减的dropout更受欢迎,因为它使用的超参数可解释性比k更强,若有足够的计算资源,可以让衰减更缓慢一些,让训练时间更长
逐参数适应学习率方法:
前面讨论的方法都是对学习率进行全局的操作,且对所有参数,其学习率都是一样的,下面要介绍的方法是能够适应性地根据参数来调整其对应的学习率,从而使得每个参数的学习率可能都不一样。
Adagrad:
cache += dx**2
x += -learning_rate * dx / (np.sqrt(cache) + eps)
cache与梯度矩阵size一致,跟踪了每个参数梯度的平方和,被用来归一化参数更新步长(学习率),这样一来,高梯度值的权重的学习率被减弱,低梯度值的权重的学习率被增强。平方根的操作非常重要,加入eps噪声是为了防止除0,该算法的缺点是学习率单调变小通常过于激进,且过早停止学习
RMSprop:
是Adagrad的改进
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]的超参,cache变成了梯度平方的滑动平均,从而使学习率不会单调变小
Adam:
m = beta1 * m + (1 - beta1) * dx
v = beta2 * v + (1 - beta2) * dx ** 2
x += - learning_rate * m / (np.sqrt(v) + eps)
这里的更新方法和RMSProp很像,但其使用的是平滑版的梯度m
超参数调优:
训练一个神经网络会遇到很多的超参数设置,常用的有:
初始学习率
学习率衰减方式
正则化强度
实现:
更大的神经网络需要更长的时间去训练,所以调参可能幻几天甚至几周。一个设计代码的思路是:
使用子程序持续地随机设置参数,然后进行最优化,训练过程中,子程序会对每个周期后验证集的准确率进行监控,然后写下一个记录点日志。还有一个主程序,他可以启动或者结束计算群中的子程序,根据筛选条件查看子程序写下的记录点,输出它们的训练统计数据,所谓海选。
比起交叉验证,最好使用一个验证集:
大多数情况下,一个size合理的验证集可以使代码更简单
超参数范围:
在对数尺度上进行超参数搜索,例如,一个典型的学习率搜索应该是这样:
learning_rate = 10**uniform(-6,1)
这是因为如果只采取固定的线性步长分布的话,当学习率很小或者很大的话,步长对学习率的相对改变量差异是巨大的,所以我们采取对数尺度。但对于一些特别的参数(比如dropout),我们还是采取在原始尺度上搜索(dropout = uniform(0,1))
随机搜索优于网络搜索:
通常有部分超参数比其他超参数更重要,通过随机化搜索,而不是网格化搜索,可以更精确地发现较重要的超参数的好数值
对于边界上的最优值要谨慎:
这种情况一般发生在搜索范围不好的情况,若我们得到的超参数较优值在搜索边界上,我们就需要调整我们的搜索范围
从粗到细地分阶段搜索:
可以先进行粗略的范围搜索,然后根据最优值出现的地方,缩小范围进行搜索。进行粗搜索的时候,训练一个epoch就可以了,因为很多超参数的设定会让模型无法学习。搜索的范围越精细,训练的epoch越多