Weights Update 与 Dropout
参数更新:Weights Update
在进一步学习之前,先看看我们将要覆盖的权重更新方法的效果,图片来源:
http://cs231n.github.io/neural-networks-3/
学习率是怎么来的
相信有机器学习基础的读者都很熟悉一个式子:
W
−
=
λ
d
W
,这里的
λ
就是学习率,那么这个式子是怎么来的呢?(小编不敢在众大神面前班门弄斧,只是跟大家分享下,如果有什么改进,还望提出哦,谢谢!)
在传统的机器学习方法中,我们所遇到的一般都是凸问题(可以想象这种问题的Loss Function就想一个大锅,存在最小值),因此如果我们想要达到最小的Loss,我们就需要沿着这个函数的梯度方向往下走(即梯度下降),那么梯度方向就是
d
W
|
d
W
|
,走的步长就是常数
Δ
,这时就会发现,如果用
W
−
=
Δ
d
W
|
d
W
|
在梯度较大的时候,也就是比较最优解的时候,W的更新有点太不紧不慢了,然而到了梯度较小的时候,也就是较靠近最优解的时候,W的更新竟然也保持着跟原来一样的速率,这样会导致W很容易更新过度反而远离了最优解。所以这时候就想了,既然在远离最优解的时候梯度大,在靠近最优解的时候梯度小,为何不干脆把这个性质利用起来呢?于是我我们就用
λ
|
W
|
来代替
Δ
,最后得到了我们熟悉的式子
W
=
W
−
λ
|
W
|
d
W
|
d
W
|
=
W
−
λ
d
W
。所以说这时的
λ
其实是具备了一些动态更新的特点的,别看它是个常数,但是显然,我们这里不是跟大家说这样就足够了,我们还可以权重得更好。也就是我下面要跟大家一起分享的其他几个Weights Update的方法。
随机梯度下降-Stochastic Gradient Descent
这种方法简称SGD(打这个名字好累,下面都用简称了= =)。SGD其实很简单,就是相比我们之前见到的Batch Gradient Descent方法使用全部数据做一轮迭代,SGD一轮迭代只用一条随机的数据,因此SGD的迭代次数会比BGD大很多,然而总体还是比BGD快。下面简单分析下SGD的优缺点。
优点:
- 收敛速度快(通过减少总体的迭代次数)
- 可以很快的见到效果
缺点:
- 收敛时浮动,不稳定
- 在最优解附近跳动,难以判断是否已经收敛
- 随机访问可能造成巨大的IO开销
Batch与SGD的合体?Mini-Batch Gradient Descent!
看到SGD的优缺点,我们不禁想,有没什么办法可以即保留绝大部分的优点,同时有大幅的减弱缺点呢?答案是肯定的!就是我们这里介绍的Mini-Batch Gradient Descent。道理也很简单,就是SGD太极端了,一次一条,为何不一次五十条呢?是的,这个方法就是用一次迭代多条数据的方法。并且如果Batch Size选择合理,不仅收敛速度比SGD更快,更稳定,而且在最优解附近的跳动也不会很大,甚至能够得到比Batch Gradient Descent 更好的解。这样就综合了SGD和Batch Gradient Descent 的优点,同时弱化了缺点。总之不用多想,Mini-Batch比SGD和Batch Gradient Descent都好。
再给力点?Momentum来了
上面两种都是随机的方法,因此都存在不同程度的震荡,因此,如果能够以之前下降的方向作为参考,那么将会有利于下一次下降的方向(这个是成立的,因为下降的方向的期望是指向最优解)。下面给出Momentum Update的python代码:
v = momentum*v-learning_rate*dW
W += v
这里的v通常被称为第一moment。v可以初始化为0,momentum一般取0.5或0.9或0.99。从代码可以看出,在训练过程中,v不断积累和更新下降的速率和方向。从本节开头的动图也可以看出,Momentum的确比SGD快多了,但是由于有历史的影响,Momentum会出现越过最优解的情况,但是总体还是比单纯的随机方法快很多。
考虑得再多一些-Nesterov Momentum Update
NAG(Nesterov Accelerated Gradient)不仅仅把SGD梯度下降以前的方向考虑,还将Momentum梯度变化的幅度也考虑了进来。下面给出python的代码
v_prev = v
v = momentum*v-learning_rate*dx
x+=v+momentum*(v-v_prev)
从代码也可以看出,NAG多观察了Momentum拐弯的角度,因此,当Momentum在越过最优解的时候,Momentum会尽可能的希望拐回正轨,这时候NAG就会留意到Momentum在拐弯从而加速自己走回正轨的,所以我们看到动图中,NAG偏离最优解的程度更小些。
平衡梯度的更新策略-AdaGrad
在传统机器学习中,一种加速迭代速度的方法就是将数值归一化到一个一原点为中心的空间内,因为这样可以避免梯度在某些维度上跨度很小而拖慢迭代速度。而对于随机的方法,这种数值不同维度上的域的范围如果有很大的区别可能会导致非常糟糕的情况。试想一个二维的W,如果Loss Function对第二维的变化非常敏感,那么当随机方法在执行时,很有可能会在第二维处迈出了相对来说很大的一步,而在第一微处迈出了很小的一步,那么收敛效果就会不好。AdaGrad就是考虑到这种情况。AdaGrad通过不断积累每次随机梯度不同维度的大小,使得之后的梯度在该小的维度上小,该大的维度上大。下面给出AdaGrad的python代码
cache += dx**2
x+=-learning_rate*dx/(np.sqrt(cache)+1e-7)
这里的cache也常被称为第二moment。
尽量保持梯度的RMSProp方法
细心的读者可能会留意到,AdaGrad每次迭代都累加了梯度,这会导致在迭代后期,AdaGrad的梯度会变得很小,从而减慢了速度,RMSProp是Hinton在Coursera公开课上提出的方法,它尽可能的保持学习率不会下降得太快,下面给出python代码
cache = decay_rate*cache+(1-decay_rate)*dx**2
x+=-learning_rate*dx/(np.sqrt(cache)+1e-7)
这里的decay_rate一般取0.9或0.99
AdaDelta
相信大家一定也很好奇动图中那个飞得最好的AdaDelta是什么方法吧,然而课上并没有提到,于是小编就去谷歌了下,下面给出参考链接:
可能要教育网或者有数据库账号才能看吧….
http://arxiv.org/pdf/1212.5701.pdf
下面给出Python代码
cache_dx = delta*cache_dx + (1-delta)*(dx**2)
v = -learning_rate*np.sqrt(cache_x)*dx/(np.sqrt(cache_dx)+1e-7)
x += v
cache_x = delta*cache_x + (1-delta)*(v**2)
结合Momentum与AdaGrad:Adam Update
Momentum方法和AdaGrad方法都在解决权重更新不同的问题上有很大的作用,那么现在就将这两者合并起来,形成一个更加强大的方法,Adam。由于是两者的结合,那么我就不加赘述了,直接上代码
m = beta1*m+(1-beta1)*dx # update first moment
v = beta2*v+(1-beta2)*(dx**2) # update second moment
mb = m/(1-beta1**t) # correct bias
vb = v/(1-beta2**t) # correct bias
x+= -learning_rate*mb/(np.sqrt(vb)+1e-7) # RMSProp-like
提出者Kingma et al. 建议beta1初始值为0.9,beta2初始值为0.999。留意到上面代码多了一些我们没遇到过的,就是correct bias部分,这部分代码我们可以看到它除了(1-(小于1)**t),可以知道,当t很大时,近似于除以1,因此这个代码只在前期的时候有作用,一个解释就是因为m和b初始值是置0的,加入这段代码是为了修正前期m和b。
逃不了的学习率
虽然方法越来越好,但是我们始终可以看到,learning_rate一直形影不离地跟着我们,因此,这些方法是否管用,首先是learning rate一定要合适。相信各位读者有一定的机器学习的方法的积累,这时我们自然而然地就能想到validation(或cross-validation)用来选择学习率,通常初始学习率设为0.01都会有个比较好的表现。我们希望在初期学习率大一些,权重更新的程度大些,而到了收敛阶段学习率小些,权重更新得谨慎一些。因此一般让学习率虽然迭代次数的增加而逐渐变小。
一般有这三种策略:
-
step decay
乘于decay_rate(一般设0.9或0.99)每过一定数量的迭代(在VGG中,当validation error不减时就进行decay) -
exponential decay
λ
=
λ
∗
e
−
k
t
-
1/t decay
λ
=
λ
/
(
1
+
k
t
)
如果上面的方法最终得到的权重时INF或者NAN,那么就需要考虑使用更小的初始学习率了(一般学习率都是从大往小找的)。
正则化方法:Dropout
顾名思义,Dropout方法就是在训练的过程中将使用Dropout的层中一定比例的神经元输出置零。
注意:这里我们只在训练的时候使用Dropout,在测试阶段我们需要把Dropout屏蔽
课程上给出了Dropout之所以具有正则化能力的两个解释:
- 强迫网络拥有冗余的表示
- Dropout在训练一个大规模的网络组合(ensemble)
强迫网络拥有冗余的表示:
因为在每轮迭代中,总是随机屏蔽一定比例的神经元,因此输出并不知道它正在组合哪些特征,因此,比起之前能够专注于某些个特征(比如对识别猫来说,可能有长长的尾巴会是模型专注的一个特征),现在模型不敢这么肯定了,因此它开始把专注力分散到每个特征,使得这些特征也能具备比较好的预测能力。因为将专注力分散了,那么原本具有很高权重的那些特征现在的权重就会被压缩,从而达到了正则化的功能。
Dropout在训练一个大规模的网络组合:
在每轮迭代的时候,都是随机生成一个二值的mask用来屏蔽一部分神经元。因此我们可以这样认为,相比之前使用所有的数据在一个模型里面训练,Dropout相当于是为每一个数据都提供一个小模型,然后再将这些模型组合起来的一种方法。(然而小编也没有理解为什么组合小模型就能减轻overfit了,有没可能是跟Boosting这些方法有共同之处?但是考虑到像Adaboost这些都是用一堆high bias的模型组合,这里我们可以说Dropout的小模型对单个数据来说是high bias的吗?小编觉得不行= =…读者如果有什么见解或者是认为小编get错point了,希望能够提出,共同学习,谢谢~)
模型复杂程度:
一般来说,模型越复杂,overfit的可能性越大,在神经网络中,神经元越多模型越复杂(好像在说废话= =),这时网络由于节点多,可以容量也就大(这里可以认为是网络的记性就强),那么模型就更有overfit这些数据的可能性也就大,所以通过Dropout减少模型复杂度在这个角度上看的确是有正则化的能力的。
下面给出Dropout的Python代码:
注意:由于Dropout在训练的时候是用了一部分的神经元去做训练,在在测试阶段由于我们是用整个网络去训练,因此我们需要注意在训练的时候为每个存活下来的神经元做同采样比例的放大(除以p),注意,这只在训练时候做,用以保证训练得到的权重在组合之后不会太大;另外一种选择就是把测试结果根据采样比例缩小(乘于p)。
def dropout_forward(x, dropout_param):
"""
Performs the forward pass for (inverted) dropout.
Inputs:
- x: Input data, of any shape
- dropout_param: A dictionary with the following keys:
- p: Dropout parameter. We drop each neuron output with probability p.
- mode: 'test' or 'train'. If the mode is train, then perform dropout;
if the mode is test, then just return the input.
- seed: Seed for the random number generator. Passing seed makes this
function deterministic, which is needed for gradient checking but not in
real networks.
Outputs:
- out: Array of the same shape as x.
- cache: A tuple (dropout_param, mask). In training mode, mask is the dropout
mask that was used to multiply the input; in test mode, mask is None.
"""
p, mode = dropout_param['p'], dropout_param['mode']
if 'seed' in dropout_param:
np.random.seed(dropout_param['seed'])
mask = None
out = None
if mode == 'train':
mask = (np.random.rand(*x.shape) < p)/p
out = x*mask
pass
elif mode == 'test':
out = x*(np.random.rand(*x.shape) < p)/p
pass
cache = (dropout_param, mask)
out = out.astype(x.dtype, copy=False)
return out, cache
def dropout_backward(dout, cache):
"""
Perform the backward pass for (inverted) dropout.
Inputs:
- dout: Upstream derivatives, of any shape
- cache: (dropout_param, mask) from dropout_forward.
"""
dropout_param, mask = cache
mode = dropout_param['mode']
dx = None
if mode == 'train':
dx = dout * mask
pass
elif mode == 'test':
dx = dout
return dx
后言
到了这里,可以说我们已经基本覆盖掉了深度学习里面常见方法,那么用这些方法,我们就可以自信地踏入深度学习这片神奇的领域了。在之后的章节,我将跟大家介绍深度学习的各种层,相信过不了多久就可以开始构建属于自己的深度神经网络了:),一颗赛艇。小编最近更新blog会减慢些,因为最近的课程快end了,期末项目有点多= =…希望下一周能够尽量把CS231n的精华贡献给大家之后如果有可能会开始介绍Caffe的安装以及使用等等…敬请期待:)。