代码仓库: https://github.com/brandonlyg/cute-dl
(转载请注明出处!)python
在上个阶段,咱们使用固定学习率优化器训练识别MNIST手写数字模型。在后面的示例中将会看到: 若是学习习设置太大,模型将没法收敛; 若是设置学习率过小模型大几率会收敛速度会很是缓慢。所以必需要要给学习率设置一个合适的值,这个合适的值究竟是什么须要反复试验。
训练模型的本质是,在由损失函数定义的高纬超平面中尽量地找到最低点。因为高纬超平面十分复杂,找到全局最低点每每不现实,所以找到一个尽可能接近全局最低点的局部最低点也是能够的。
因为模型参数是随机初始化的,在训练的初始阶段, 可能远离最低点,也可能距最低点较较近。为了使模型可以收敛,较小的学习率比较大的学习率更有可能达到目地, 至少不会使模型发散。
理想的状态下,咱们但愿,学习率是动态的: 在远离最低点的时候有较大的学习率,在靠近最低点的时候有较小的学习率。
学习率算法在训练过程当中动态调整学习率,试图使学习率接近理想状态。常见的学习率优化算法有:git
目前没有一种理论可以给出定量的结论断言一种算法比另外一种更好,具体选用哪一种算法视具体状况而定。
接下来将会详细讨论每种算法的数学性质及实现,为了方便讨论,先给出一些统一的定义:github
其中v是动量\(v_0=0\), γ是动量的衰减率\(γ∈(0,1)\). 如今把\(v_t\)展开看一下\(g_i, i=1,2,...t\)对v_t的影响.算法
个项系数之和的极限状况:网络
令\(t=\frac{1}{1-γ}\)则有\(\frac{1}{t}=1-γ\), \(g_i\)的指数加权平均值可用下式表示:框架
若是把学习率α表示为:\(α=\frac{α}{1-γ}(1-γ)\),所以\(v_t\)能够当作是最近的\(1-γ\)次迭代梯度的指数加权平均乘以一个缩放量\(\frac{α}{1-γ}\), 这里的缩放量\(\frac{α}{1-γ}\)才是真正的学习率参数。
设\(\frac{1}{1-γ}=n\), 最近n次迭代梯度的权重占总权重的比例为:函数
当γ=0.9时, \(1 - γ^{10}≈0.651\), 就是说, 这个时候, 最近的10次迭代占总权重的比例约为65.1%, 换言之\(v_t\)的值的数量级由最近10次迭代权重值决定。
当咱们设置超参数γ,α时, 能够认为取了最近\(\frac{1}{1-γ}\)次迭代梯度的指数加权平均值为动量积累量,而后对这个积累量做\(\frac{α}{1-γ}\)倍的缩放。 例如: γ=0.9, α=0.01, 表示取最近10次的加权平均值,而后将这个值缩小到原来的0.1倍。
动量算法可以有效地缓解\(g_t \to 0\)时参数更新缓慢的问题和当\(g_t\)很大时参数更新幅度过大的问题。
比较原始的梯度降低算法和使用动量的梯度降低算法更新参数的状况:性能
\(g_t \to 0\)时, 有3种可能:学习
当\(g_t\)很大时(1)式致使参数大幅度的更新, 会大几率致使模型发散。(2)式当γ=0.9时, \(g_t\)对\(v_t\)的影响权重是0.1; 式当γ=0.99时,\(g_t\)对\(v_t\)的影响权重是0.01, 相比于\(g_{t-1}\)到\(g_t\)的增长幅度, \(v_{t-1}\)到\(v_t\)增长幅度要小的多, 参数更新也会平滑许多。优化
文件: cutedl/optimizers.py, 类名:Momentum.
def update_param(self, param): #pdb.set_trace() if not hasattr(param, 'momentum'): #为参数添加动量属性 param.momentum = np.zeros(param.value.shape) param.momentum = param.momentum * self.__dpr + param.gradient * self.__lr param.value -= param.momentum
其中\(s_t\)是梯度平方的积累量, \(ε=10^{-6}\)用来保持数值的稳定, Δw_t是参数的变化量。\(s_t\)的每一个元素都是正数, 随着迭代次数增长, 数值会愈来愈大,相应地\(\frac{α}{\sqrt{s_t} + ε}\)的值会愈来愈小。\(\frac{α}{\sqrt{s_t} + ε}\)至关于为\(g_t\)中的每一个元素计算独立的的学习率, 使\(Δw_t\)中的每一个元素位于(-1, 1)区间内,随着训练次数的增长会向0收敛。这意味着\(||Δw_t||\)会愈来愈小, 迭代次数比较大时, \(||Δw_t|| \to 0\), 参数w将不会有更新。相比于动量算法, Adagrad算法调整学习率的方向比较单一, 只会往更小的方向上调整。α不能设置较大的值, 由于在训练初期\(s_t\)的值会很小, \(\frac{α}{\sqrt{s_t} + ε}\)会放大α的值, 致使较大的学习率,从而致使模型发散。
文件: cutedl/optimizers.py, 类名:Adagrad.
def update_param(self, param): #pdb.set_trace() if not hasattr(param, 'adagrad'): #添加积累量属性 param.adagrad = np.zeros(param.value.shape) a = 1e-6 param.adagrad += param.gradient ** 2 grad = self.__lr/(np.sqrt(param.adagrad) + a) * param.gradient param.value -= grad
为了克服Adagrad积累量不断增长致使学习率会趋近于0的缺陷, RMSProp算法的设计在Adagrad的基础上引入了动量思想。
算法设计者给出的推荐参数是γ=0.99, 即\(s_t\)是最近100次迭代梯度平方的积累量, 因为计算变化量时使用的是\(\sqrt{s_t}\), 对变化量的影响只至关于最近10次的梯度积累量。
的Adagrad相似, \(s_t\)对\(g_t\)的方向影响较小, 但对\(||g_t||\)大小影响较大,会把它缩小到(-1, 1)区间内, 不一样的是不会单调地把\(||g_t||\)收敛到0, 从而克服了Adagrad的缺陷。
文件: cutedl/optimizers.py, 类名:RMSProp.
def update_param(self, param): #pdb.set_trace() if not hasattr(param, 'rmsprop_storeup'): #添加积累量属性 param.rmsprop_storeup = np.zeros(param.value.shape) a = 1e-6 param.rmsprop_storeup = param.rmsprop_storeup * self.__sdpr + (param.gradient**2) * (1-self.__sdpr) grad = self.__lr/(np.sqrt(param.rmsprop_storeup) + a) * param.gradient param.value -= grad
这个算法的最大特色是不须要全局学习率超参数, 它也引入了动量思想,使用变化量平方的积累量和梯度平方的积累量共同为\(g_t\)的每一个元素计算独立的学习率。
这个算法引入了新的量\(d_t\), 是变化量平方的积累量, 表示最近n次迭代的参数变化量平方的加权平均. \(ε=10^{-6}\). 推荐的超参数值是γ=0.99。这个算法和RMSProp相似, 只是用\(\sqrt{d_{t-1}}\)代替了学习率超参数α。
文件: cutedl/optimizers.py, 类名:Adadelta.
def update_param(self, param): #pdb.set_trace() if not hasattr(param, 'adadelta_storeup'): #添加积累量属性 param.adadelta_storeup = np.zeros(param.value.shape) if not hasattr(param, "adadelta_predelta"): #添加上步的变化量属性 param.adadelta_predelta = np.zeros(param.value.shape) a = 1e-6 param.adadelta_storeup = param.adadelta_storeup * self.__dpr + (param.gradient**2)*(1-self.__dpr) grad = (np.sqrt(param.adadelta_predelta)+a)/(np.sqrt(param.adadelta_storeup)+a) * param.gradient param.adadelta_predelta = param.adadelta_predelta * self.__dpr + (grad**2)*(1-self.__dpr) param.value -= grad
前面讨论的Adagrad, RMSProp和Adadetal算法, 他们使用的加权平均积累量对\(g_t\)的范数影响较大, 对\(g_t\)的方向影响较小, 另外它们也不能缓解\(g_t \to 0\)的状况。Adam算法同时引入梯度动量和梯度平方动量,理论上能够克服前面三种算法共有的缺陷的缺陷。
其中\(v_t\)和动量算法中的\(v_t\)含义同样,\(s_t\)和RMSProp算法的\(s_t\)含义同样, 对应的超参数也有同样的推荐值\(γ_1=0.9\), \(γ_2=0.99\)。用于稳定数值的\(ε=10^{-8}\). 比较特别的是\(\hat{v_t}\)和\(\hat{s_t}\), 他们是对\(v_t\)和\(s_t\)的一个修正。以\(\hat{v_t}\)为例, 当t比较小的时候, \(\hat{v_t}\)近似于最近\(\frac{1}{1-γ}\)次迭代梯度的加权和而不是加权平均, 当t比较大时, \(1-γ^t \to 1\), 从而使\(\hat{v_t} \to v_t\)。也就是所\(\hat{v_t}\)时对对迭代次数较少时\(v_t\)值的修正, 防止在模型训练的开始阶段产生过小的学习率。\(\hat{s_t}\)的做用和\(\hat{v_t}\)是相似的。
文件: cutedl/optimizers.py, 类名:Adam.
def update_param(self, param): #pdb.set_trace() if not hasattr(param, 'adam_momentum'): #添加动量属性 param.adam_momentum = np.zeros(param.value.shape) if not hasattr(param, 'adam_mdpr_t'): #mdpr的t次方 param.adam_mdpr_t = 1 if not hasattr(param, 'adam_storeup'): #添加积累量属性 param.adam_storeup = np.zeros(param.value.shape) if not hasattr(param, 'adam_sdpr_t'): #动量sdpr的t次方 param.adam_sdpr_t = 1 a = 1e-8 #计算动量 param.adam_momentum = param.adam_momentum * self.__mdpr + param.gradient * (1-self.__mdpr) #误差修正 param.adam_mdpr_t *= self.__mdpr momentum = param.adam_momentum/(1-param.adam_mdpr_t) #计算积累量 param.adam_storeup = param.adam_storeup * self.__sdpr + (param.gradient**2) * (1-self.__sdpr) #误差修正 param.adam_sdpr_t *= self.__sdpr storeup = param.adam_storeup/(1-param.adam_sdpr_t) grad = self.__lr * momentum/(np.sqrt(storeup)+a) param.value -= grad
接下来咱们仍然使用上个阶段的模型作为示例, 使用不一样的优化算法训练模型,对比差异。代码在examples/mlp/mnist-recognize.py中
代码中有两个结束训练的条件:
def fit0(): lr = 0.0001 print("fit1 lr:", lr) fit('0.png', optimizers.Fixed(lr))
较小的固定学习率0.0001可使模型稳定地收敛,但收敛速度很慢, 训练接近100万步, 最后因为收敛速度太慢而中止训练。
def fit1(): lr = 0.2 print("fit0 lr:", lr) fit('1.png', optimizers.Fixed(lr))
较大的固定学习率0.2, 模型在训练7万步左右的时候因发散而中止训练。模型进度开始下降: 最大验证正确率为:0.8445, 结束时的验证正确率为:0.8438.
def fit2(): lr = 0.01 print("fit2 lr:", lr) fit('2.png', optimizers.Fixed(lr))
经过屡次试验, 找到了一个合适的学习率0.01, 这时模型只需训练28000步左右便可达到指望性能。
def fit_use_momentum(): lr = 0.002 dpr = 0.9 print("fit_use_momentum lr=%f, dpr:%f"%(lr, dpr)) fit('momentum.png', optimizers.Momentum(lr, dpr))
这里的真实学习率为\(\frac{0.002}{1-0.9} = 0.02\)。模型训练23000步左右便可达到指望性能。这里的学习率稍大,证实动量算法能够适应稍大学习率的数学性质。
def fit_use_adagrad(): lr = 0.001 print("fit_use_adagrad lr=%f"%lr) fit('adagrad.png', optimizers.Adagrad(lr))
屡次试验代表,Adagrad算法的参数最很差调。因为这个算法的学习率会一直单调递减, 它只能对模型进行小幅度的优化, 故而这个算法并不适合从头开始训练模型,比较适合对预训练的模型参数进行微调。
def fit_use_rmsprop(): sdpr = 0.99 lr=0.0001 print("fit_use_rmsprop lr=%f sdpr=%f"%(lr, sdpr)) fit('rmsprop.png', optimizers.RMSProp(lr, sdpr))
这里给出的是较小的学习率0.0001。屡次试验代表, RMSProp在较大学习率下很容易发散,而在较小学习率下一般会有稳定的良好表现。
def fit_use_adadelta(): dpr = 0.99 print("fit_use_adadelta dpr=%f"%dpr) fit('adadelta.png', optimizers.Adadelta(dpr))
这个算法不须要给出学习率参数。屡次试验显示, 在这个简单模型上, Adadelta算法表现得很是稳定。
def fit_use_adam(): lr = 0.0001 mdpr = 0.9 sdpr = 0.99 print("fit_use_adam lr=%f, mdpr=%f, sdpr=%f"%(lr, mdpr, sdpr)) fit('adam.png', optimizers.Adam(lr, mdpr, sdpr))
只用这个算法在较小学习率0.0001的状况下20000步左右便可完成训练且最终达到了92.4%的验证准确率。
这个阶段为框架添加了常见的学习率优化算法,并在同一个模型上进行验证,对比。我发现即便不使用优化算法,用固定的学习率, 只要给出“合适”的学习率参数,仍然可以获得理想的训练速度, 但很难肯定怎样才算“适合”。 学习率算法给出了参数调整的大体方向,通常来讲较小的学习率都不会有问题,至少不会使模型发散,而后能够经过调整衰减率来加快训练速度,而衰减率有比较简单数学性质可让咱们在调整它的时知道这样调整意味着什么。 目前为止cute-dl框架已经实现了对简单MLP模型的全面支持,接下来将会为框架添一些层,让它可以支持卷积神经网络模型。