超参数(Hyper-Parameter)是困扰神经网络训练的问题之一,由于这些参数不可经过常规方法学习得到。html
神经网络经典五大超参数:算法
学习率(Leraning Rate)、权值初始化(Weight Initialization)、网络层数(Layers)网络
单层神经元数(Units)、正则惩罚项(Regularizer|Normalization)app
这五大超参数使得神经网络更像是一门实践课,而不是理论课。框架
懂神经网络可能只要一小时,可是调神经网络可能要几天。ide
所以,后来Vapnik作SVM支持向量机的时候,经过巧妙的变换目标函数,避免传统神经网络的大部分超参数,函数
尤为是以自适应型的支持向量替代人工设置神经元,这使得SVM能够有效免于过拟合之灾。oop
传统对抗这些超参数的方法是经验规则(Rules of Thumb)。学习
这几年,随着深度学习的推动,全球神经网络研究者人数剧增,已经有大量研究组着手超参数优化问题:大数据
★深度学习先锋的RBM就利用Pre-Traning自适应调出合适的权值初始化值。
★上个世纪末的LSTM长短时间记忆网络,可视为“神经网络嵌套神经网络”,自适应动态优化层数。
★2010年Duchi et.al 则推出AdaGrad,自适应来调整学习率。
自适应调整学习率的方法,目前研究火热。一个经典之做,是 Matthew D. Zeiler 2012年在Google实习时,
提出的AdaDelta。
Matthew D. Zeiler亦是Hinton的亲传弟子之一,仍是商业天才,大二时办了一个公司卖复习旧书。
Phd毕业以后,创办了Clarifai,估值五百万刀。参考[知乎专栏]
Clarifai的杰出成就是赢得了ImageNet 2013冠军,后来公布出CNN结构的时候,Caffe、Torch之类
的框架都仿真不出他在比赛时候跑的结果,应该是用了很多未公布的黑科技的。
再看他2012年提出的AdaDelta,确定是用在的2013年的比赛当中,因此后来以普通方式才没法仿真的。
SGD(Stochastic Gradient Descent)是相对于BGD(Batch Gradient Descent)而生的。
BGD要求每次正反向传播,计算全部Examples的Error,这在大数据状况下是不现实的。
最初的使用的SGD,每次正反向传播,只计算一个Example,串行太明显,硬件利用率不高。
后续SGD衍生出Mini-Batch Gradient Descent,每次大概推动100个Example,介于BGD和SGD之间。
如今,SGD一般是指Mini-Batch方法,而不是早期单Example的方法。
一次梯度更新,可视为:
$x_{t+1}=x_{t}+\Delta x_{t} \quad where \quad \Delta x_{t}=-\eta \cdot g_{t}$
$x$为参数,$t$为时序,$\Delta$为更新量,$\eta$为学习率,$g$为梯度
二阶牛顿法替换梯度更新量:
$\Delta x_{t}=H_{t}^{-1} \cdot g_{t}$
$H$为参数的二阶导矩阵,称为Hessian矩阵。
牛顿法,用Hessian矩阵替代人工设置的学习率,在梯度降低的时候,能够完美的找出降低方向,
不会陷入局部最小值当中,是理想的方法。
可是,求逆矩阵的时间复杂度近似$O(n^{3})$,计算代价过高,不适合大数据。
早期最多见的手段之一就是模拟退火。固然这和模拟退火算法没有半毛钱关系。
引入一个超参数(常数)的退火公式:
$\eta_{t}=\frac{\eta _{0}}{1+d\times t}$
$\eta _{0}$为初始学习率,$d$为衰减常数,一般为$10^{-3}$
模拟退火基于一个梯度法优化的事实:
在优化过程当中,Weight逐渐变大,于是须要逐渐减少学习率,保证更新平稳。
中期以及如今最普及的就是引入动量因子:
$\Delta x_{t}=\rho \Delta x_{t-1}-\eta \cdot g_{t}$
$\rho$为动量因子,一般设为0.9
在更新中引入0.9这样的不平衡因子,使得:
★在降低初期,使用前一次的大比重降低方向,加速。
★在越过函数谷面时,异常的学习率,会使得两次更新方向基本相反,在原地”震荡“
此时,动量因子使得更新幅度减少,协助越过函数谷面。
★在降低中后期,函数面局部最小值所在的吸引盆数量较多,一旦陷进吸引盆当中,
$Gradient \rightarrow 0$,可是先后两次更新方向基本相同。
此时,动量因子使得更新幅度增大,协助跃出吸引盆。
AdaGrad思路基本是借鉴L2 Regularizer,不过此时调节的不是$W$,而是$Gradient$:
$\Delta x_{t}=-\frac{\eta }{\sqrt{\sum_{\tau=1}^{t}(g_{\tau})^{2}}}\cdot g_{t}$
AdaGrad过程,是一个递推过程,每次从$\tau=1$,推到$\tau=t$,把沿路的$Gradient$的平方根,做为Regularizer。
分母做为Regularizer项的工做机制以下:
★训练前期,梯度较小,使得Regularizer项很大,放大梯度。[激励阶段]
★训练后期,梯度较大,使得Regularizer项很小,缩小梯度。[惩罚阶段]
另外,因为Regularizer是专门针对Gradient的,因此有利于解决Gradient Vanish/Expoloding问题。
因此在深度神经网络中使用会很是不错。
固然,AdaGrad自己有很多缺陷:
★初始化W影响初始化梯度,初始化W过大,会致使初始梯度被惩罚得很小。
此时能够人工加大$\eta$的值,但过大的$\eta$会使得Regularizer过于敏感,调节幅度很大。
★训练到中后期,递推路径上累加的梯度平方和越打越多,迅速使得$Gradinet$被惩罚逼近0,提早结束训练。
AdaDelta基本思想是用一阶的方法,近似模拟二阶牛顿法。
1988年,[Becker&LeCun]提出一种用矩阵对角线元素来近似逆矩阵的方法:
$\Delta x_{t}=-\frac{1}{\left | diag(H_{t}) \right |+\mu }\cdot g_{t}$
$diag$指的是构造Hessian矩阵的对角矩阵,$\mu$是常数项,防止分母为0。
2012年,[Schaul&S. Zhang&LeCun]借鉴了AdaGrad的作法,提出了更精确的近似:
$\Delta x_{t}=-\frac{1}{\left | diag(H_{t}) \right |}\frac{E[g_{t}-w:t]^{2}}{E[g_{t}^{2}-w:t]}\cdot g_{t}$
$E[g_{t}-w:t]$指的是从当前t开始的前w个梯度状态的指望值。
$E[g_{t}^{2}-w:t]$指的是从当前t开始的前w个梯度状态的平方的指望值。
一样是基于Gradient的Regularizer,不过只取最近的w个状态,这样不会让梯度被惩罚至0。
计算$E[g_{t}-w:t]$,须要存储前w个状态,比较麻烦。
AdaDelta使用了相似动量因子的平均方法:
$E[g^{2}]_{t}=\rho E[g^{2}]_{t-1}+(1-\rho )g_{t}^{2}$
当$\rho=0.5$时,这个式子就变成了求梯度平方和的平均数。
若是再求根的话,就变成了RMS(均方根):
$RMS[g]_{t}=\sqrt{E[g^{2}]_{t}+\epsilon }$
再把这个RMS做为Gradient的Regularizer:
$\Delta x_{t}=-\frac{\eta}{RMS[g]_{t}}\cdot g_{t}$
其中,$\epsilon$是防止分母爆0的常数。
这样,就有了一个改进版的AdaGrad。
该方法即Tieleman&Hinton的RMSProp,因为RMSProp和AdaDelta是同年出现的,
Matthew D. Zeiler并不知道这种改进的AdaGrad被祖师爷命名了。
RMSProp利用了二阶信息作了Gradient优化,在BatchNorm以后,对其需求不是很大。
可是没有根本实现自适应的学习率,依然须要线性搜索初始学习率,而后对其逐数量级降低。
另外,RMSProp的学习率数值与MomentumSGD差异甚大,须要从新线性搜索初始值。
注:$\epsilon$的建议取值为1,出处是Inception V3,不要参考V3的初始学习率。
Zeiler用了两个反复近似的式子来讲明,一阶方法到底在哪里输给了二阶方法。
首先,考虑SGD和动量法:
$\Delta x \propto g\propto \frac{\partial f}{\partial x} \propto \frac{1}{x}$
$\Delta x$能够正比到梯度$g$问题,再正比到一阶导数。而$log$一阶导又可正比于$\frac{1}{x}$。
再考虑二阶导Hessian矩阵法:
这里为了对比观察,使用了[Becker&LeCun 1988]的近似方法,让求逆矩阵近似于求对角阵的倒数:
$\Delta x \propto H^{-1}g\propto \frac{\frac{\partial f}{\partial x}}{\frac{\partial^{2}f}{\partial x^{2}}}\propto \frac{\frac{1}{x}}{\frac{1}{x}*\frac{1}{x}}\propto x$
$\Delta x$能够正比到Hessian逆矩阵$H^{-1}\cdot g$问题,再正比到二阶导数。而$log$二阶导又可正比于$x$。
能够看到,一阶方法最终正比于$\frac{1}{x}$,即与参数逆相关:参数逐渐变大的时候,梯度反而成倍缩小。
而二阶方法最终正比于$x$,即与参数正相关:参数逐渐变大的时候,梯度不受影响。
所以,Zeiler称Hessian方法获得了Correct Units(正确的更新单元)。
基于[Becker&LeCun 1988]的近似方法,有:
$\Delta x \approx \frac{\frac{\partial f}{\partial x}}{\frac{\partial^{2}f}{\partial x^{2}}}$
进而又有:
$\frac{\frac{\partial f}{\partial x}}{\frac{\partial^{2}f}{\partial x^{2}}}=\frac{1}{\frac{\partial^{2}f}{\partial x^{2}}}\cdot \frac{\partial f}{\partial x}=\frac{1}{\frac{\partial^{2}f}{\partial x^{2}}}\cdot g_{t}$
简单收束变形一下, 而后用RMS来近似:
$\frac{1}{\frac{\partial^{2}f}{\partial x^{2}}}=\frac{\Delta x}{\frac{\partial f}{\partial x}}\approx -\frac{RMS[\Delta x]_{t-1}}{RMS[g]_{t}}$
最后,一阶完整近似式:
$\Delta x= -\frac{RMS[\Delta x]_{t-1}}{RMS[g]_{t}}\cdot g_t$
值得注意的是,使用了$RMS[\Delta x]_{t-1}$而不是$RMS[\Delta x]_{t}$,由于此时$\Delta x_{t}$还没算出来。
$\quad\quad\quad\qquad\qquad\qquad ALGORITHM:ADADELTA\\\\\\\\Require:DecayRate \,\rho \, ,Constant \,\,\epsilon \\Require:InitialParam \,\,x_{1} \\1: \quad Initialize\,\,accumulation \,\,variables \,\,E[g^{2}]_{0}=E[\Delta x^{2}]_{0=0} \\2: \quad For \,\,t=1:T \,\, do \,\, Loop \,\, all \,\,updates \\3: \quad \quad Compute \,\,Gradients:g_{t} \\4: \quad \quad Accumulate \,\, Gradient:E[g^{2}]_{t}=\rho E[g^{2}]_{t-1}+(1-\rho )g_{t}^{2} \\5: \quad \quad Compute \,\,Update:\Delta x= -\frac{RMS[\Delta x]_{t-1}}{RMS[g]_{t}}\cdot g_t \\6: \quad \quad Accumulate \,\, Updates:E[\Delta x^{2}]_{t}=\rho E[\Delta x^{2}]_{t-1}+(1-\rho )\Delta x^{2} \\7: \quad \quad Apply \,\,Update:x_{t+1}=x_{t}+\Delta x_{t} \\8: \quad End \,\,For$
论文中,给出的两个超参数的合适实验值。
$\rho=0.95 \quad\quad \epsilon=1e-6$
Theano的实如今LSTM的教学部分,我的精简了一下:
def AdaDelta(tparams,grads): p=0.95;e=1e-6 # init delta_x2=[theano.shared(p.get_value() * floatX(0.)) for k, p in tparams.iteritems()] g2 = [theano.shared(p.get_value() * floatX(0.)) for k, p in tparams.iteritems()] # first to update g2 update_g2=[(g2, p * g2 + (1-p) * (g ** 2)) for g2, g in zip(g2, grads)] fn_update_1=theano.function(inputs=[],updates=update_g2) #calc delta_x by RMS delta_x=[-T.sqrt(delta_x2_last + e) / T.sqrt(g2_now + e) * g for g, delta_x2_last, g2_now in zip(grads,delta_x2,g2)] # then to update delta_x2 and param update_delta_x2=[(delta_x2, p * delta_x2 + (1-p) * (delta_x ** 2)) for delta_x2, delta_x in zip(delta_x2, delta_x)] update_param=[(param, param + delta) for param, delta in zip(tparams.values(), delta_x)] fn_update_2=theano.function(inputs=[],updates=update_delta_x2+update_param) #return the update function of theano return fn_update_1, fn_update_2
默认代码以个人Dragon框架为准,对Caffe代码进行了重写。
// hpp文件 template <typename Dtype> class AdaDeltaSolver :public SGDSolver < Dtype > { public: AdaDeltaSolver(const SolverParameter& param) :SGDSolver<Dtype>(param) { } AdaDeltaSolver(const string& param_file) :SGDSolver<Dtype>(param_file) { } protected: virtual void computeUpdateValue(int param_id, Dtype rate); virtual void applyUpdate(); }; // cpp文件 #include "gradient_solver.hpp" template <typename Dtype> void AdaDeltaSolver<Dtype>::computeUpdateValue(int param_id, Dtype rate){ Blob<Dtype>* net_param = net->getLearnableParams()[param_id]; const Dtype lr_mult = net->getLrMults()[param_id]; Dtype eps = param.delta(); Dtype momntum = param.momentum(); // adadelta will ignore base_lr Dtype lr = lr_mult; const int count = net_param->count(); switch (Dragon::get_mode()){ case Dragon::CPU: // history store for E[g^2] // update store for E[delta^2] // history=momentum*history + (1-momentum)*(diff^2) // 1. compute diff^2 in temp dragon_powx<Dtype>(count, net_param->cpu_diff(), Dtype(2), temp[param_id]->mutable_cpu_data()); // 2. compute history dragon_cpu_axpby<Dtype>(count, Dtype(1) - momntum, temp[param_id]->cpu_data(), momntum, history[param_id]->mutable_cpu_data()); // 3. compute RMS[history] as denominator in temp dragon_set<Dtype>(count, eps, temp[param_id]->mutable_cpu_data()); dragon_axpy<Dtype>(count, Dtype(1), history[param_id]->cpu_data(),temp[param_id]->mutable_cpu_data()); dragon_powx<Dtype>(count, temp[param_id]->cpu_data(), Dtype(0.5), temp[param_id]->mutable_cpu_data()); // 4. compute diff/RMS[history] in diff dragon_div<Dtype>(count, net_param->cpu_diff(), temp[param_id]->cpu_data(), net_param->mutable_cpu_diff()); // 5. compute RMS[update] as numerator in temp dragon_set<Dtype>(count, eps, temp[param_id]->mutable_cpu_data()); dragon_axpy<Dtype>(count, Dtype(1), update[param_id]->cpu_data(), temp[param_id]->mutable_cpu_data()); dragon_powx<Dtype>(count, temp[param_id]->cpu_data(), Dtype(0.5), temp[param_id]->mutable_cpu_data()); // 6. compute diff*RMS[update] in diff dragon_mul<Dtype>(count, net_param->cpu_diff(), temp[param_id]->cpu_data(), net_param->mutable_cpu_diff()); // 7. compute final diff^2 in temp dragon_powx<Dtype>(count, net_param->cpu_diff(), Dtype(2), temp[param_id]->mutable_cpu_data()); // 8. compute update dragon_cpu_axpby<Dtype>(count, (1 - momntum), temp[param_id]->cpu_data(), momntum, update[param_id]->mutable_cpu_data()); // 9. apply learning rate dragon_scal<Dtype>(count, lr, net_param->mutable_cpu_diff()); break; case Dragon::GPU: #ifndef CPU_ONLY dragon_gpu_powx<Dtype>(count, net_param->gpu_diff(), Dtype(2), temp[param_id]->mutable_gpu_data()); // 2. compute history dragon_gpu_axpby<Dtype>(count, Dtype(1) - momntum, temp[param_id]->gpu_data(), momntum, history[param_id]->mutable_gpu_data()); // 3. compute RMS[history] as denominator in temp dragon_gpu_set<Dtype>(count, eps, temp[param_id]->mutable_gpu_data()); dragon_gpu_axpy<Dtype>(count, Dtype(1), history[param_id]->gpu_data(), temp[param_id]->mutable_gpu_data()); dragon_gpu_powx<Dtype>(count, temp[param_id]->gpu_data(), Dtype(0.5), temp[param_id]->mutable_gpu_data()); // 4. compute diff/RMS[history] in diff dragon_gpu_div<Dtype>(count, net_param->gpu_diff(), temp[param_id]->gpu_data(), net_param->mutable_gpu_diff()); // 5. compute RMS[update] as numerator in temp dragon_gpu_set<Dtype>(count, eps, temp[param_id]->mutable_gpu_data()); dragon_gpu_axpy<Dtype>(count, Dtype(1), update[param_id]->gpu_data(), temp[param_id]->mutable_gpu_data()); dragon_gpu_powx<Dtype>(count, temp[param_id]->gpu_data(), Dtype(0.5), temp[param_id]->mutable_gpu_data()); // 6. compute diff*RMS[update] in diff dragon_gpu_mul<Dtype>(count, net_param->gpu_diff(), temp[param_id]->gpu_data(), net_param->mutable_gpu_diff()); // 7. compute final diff^2 in temp dragon_gpu_powx<Dtype>(count, net_param->gpu_diff(), Dtype(2), temp[param_id]->mutable_gpu_data()); // 8. compute update dragon_gpu_axpby<Dtype>(count, Dtype(1) - momntum, temp[param_id]->gpu_data(), momntum, update[param_id]->mutable_gpu_data()); // 9. apply learning rate dragon_gpu_scal<Dtype>(count, lr, net_param->mutable_gpu_diff()); #endif break; default:LOG(FATAL) << "Unknown mode: " << Dragon::get_mode(); } } template <typename Dtype> void AdaDeltaSolver<Dtype>::applyUpdate(){ CHECK(Dragon::get_root_solver()); Dtype rate = getLearningRate(); // AdaDelta do not need base lr if (param.display() && iter%param.display() == 0) LOG(INFO) << "Iteration " << iter << ", lr = AdaDelta"; clipGradients(); vector<Blob<Dtype>*> net_params = net->getLearnableParams(); for (int i = 0; i < net_params.size(); i++){ normalize(i); regularize(i); computeUpdateValue(i, rate); net_params[i]->update(); } } INSTANTIATE_CLASS(AdaDeltaSolver);
从多个数据集状况来看,AdaDelta在训练初期和中期,具备很是不错的加速效果。
可是到训练后期,进入局部最小值雷区以后,AdaDelta就会反复在局部最小值附近抖动。
主要体如今验证集错误率上,脱离不了局部最小值吸引盆。
这时候,切换成动量SGD,若是把学习率下降一个量级,就会发现验证集正确率有2%~5%的提高,
这与常规使用动量SGD,是同样的。
以后再切换成AdaDelta,发现正确率又退回去了。
再切换成动量SGD,发现正确率又回来了。
---------------------------------------------------------------------
注:使用Batch Norm以后,这样从AdaDelta切到SGD会致使数值体系崩溃,缘由未知。
---------------------------------------------------------------------
我的猜想,人工学习率的量级下降,给训练形成一个巨大的抖动,从一个局部最小值,
抖动到了另外一个局部最小值,而AdaDelta的二阶近似计算,或者说全部二阶方法,
则不会产生这么大的抖动,因此很难从局部最小值中抖出来。
这给追求state of art的结果带来灾难,由于只要你一直用AdaDelta,确定是与state of art无缘的。
基本上state of art的结果,最后都是SGD垂死挣扎抖出来的。
这也是SGD为何至今在state of art的论文中没有废除的缘由,人家丑,可是实在。
eps的数值不是固定的。
1e-6在Caffe Cifar10上就显得太小了,1e-8比较适合。
这意味着不一样数值比例体系,精度须要人工注意。
paper里高精度反而没低精度好,说明精度也有比较大抖动。
so,究竟什么样的精度是最好的呢?
————————————————————————————————————
2016.5.19 更新:
在FCNN-AlexNet里,1e-8在epoch1以后就会产生数值问题。
缘由是sqrt(1e-8)*grad很大,这时候1e-10是比较好的。
另外,DensePrediction必定要作normalize,不然也有可能让AdaDelta的迭代步长计算出现数值问题。
该问题在FCNN-AlexNet进行到epoch5左右时候开始明显化。
caffe默认给的1e-10实际上要比paper里的1e-6要相对robust。