梯度下降优化算法综述与PyTorch实现源码剖析
現(xiàn)代的機器學(xué)習(xí)系統(tǒng)均利用大量的數(shù)據(jù),利用梯度下降算法或者相關(guān)的變體進行訓(xùn)練。傳統(tǒng)上,最早出現(xiàn)的優(yōu)化算法是SGD,之后又陸續(xù)出現(xiàn)了AdaGrad、RMSprop、ADAM等變體,那么這些算法之間又有哪些區(qū)別和聯(lián)系呢?本文試圖對比的介紹目前常用的基于一階梯度的優(yōu)化算法,并給出它們的(PyTorch)實現(xiàn)。
SGD
算法描述
隨機梯度下降法(Stochastic Gradient Descent,SGD)是對傳統(tǒng)的梯度下降算法(Gradient Descent,GD)進行的一種改進。在應(yīng)用GD時,我們需要對整個訓(xùn)練集進行一次反向傳播計算梯度后再進行參數(shù)更新,對系統(tǒng)的計算能力和內(nèi)存的需求較高,而SGD在計算梯度更新參數(shù)時剛好相反,每次只使用整個訓(xùn)練集中的一個樣本,因此具有更快地計算速度和較少的內(nèi)存占用。同時,因為每次只使用一個樣本更新參數(shù),使得參數(shù)更新更加頻繁,更新的參數(shù)間具有更高的方差,損失函數(shù)會朝不同的方向有較大的波動,這有助于發(fā)現(xiàn)新的極值點,避免優(yōu)化器陷入一個局部極值點。但是也由于這種頻繁的震蕩,出現(xiàn)了一種折中的方法,即小批量(mini-batch)梯度下降法,每次只取訓(xùn)練集中一個batch的樣本進行梯度的計算與參數(shù)更新,一般batch的大小為4的倍數(shù)。原始SGD的更新法則如下:θ=θ?η??θJ(θ)(1)(1)θ=θ?η??θJ(θ)
傳統(tǒng)SGD面臨的問題
傳統(tǒng)的SGD在訓(xùn)練的過程中主要存在以下幾個問題:
- 很難選擇一個合適的學(xué)習(xí)速率,太小的學(xué)習(xí)速率導(dǎo)致算法收斂很慢,而太大的學(xué)習(xí)速率會導(dǎo)致在極值點附近震蕩甚至錯過,因此需要經(jīng)過多次嘗試。
 - Learning rate schedules往往實現(xiàn)定義一個學(xué)習(xí)速率衰減表,比如每過多少step對學(xué)習(xí)速率進行decay,但是這些策略往往沒法按照某個數(shù)據(jù)集的具體參數(shù)特性進行定制。
 - 對于比較稀疏的數(shù)據(jù),不同的特征出現(xiàn)的頻率差別很大,如果所有的參數(shù)均使用一個相同的學(xué)習(xí)速率進行更新,這樣做是不合理的。對于出現(xiàn)頻率的特征,我們應(yīng)該使用一個較大的學(xué)習(xí)速率。
 - 深度神經(jīng)網(wǎng)絡(luò)之所以難以訓(xùn)練,并不是因為容易陷入局部最小值,而是在學(xué)習(xí)的過程中陷入到鞍點(saddle point),此時往各個方向的梯度幾乎均為0。如果以二維平面為例,y=x3y=x3中x=0處即為一個鞍點。對于傳統(tǒng)的SGD而言,一旦優(yōu)化的過程中進入鞍點,就很難再離開這一位置。
 
Momentum
針對以上提到的第四點問題,可以通過增加動量(Momentum)的SGD進行緩解,加速優(yōu)化函數(shù)的收斂。vt=γvt?1?η??θJ(θ)θ=θ+vt(2)(2)vt=γvt?1?η??θJ(θ)θ=θ+vt所謂的添加動量項,即在一定程度上保留上一次梯度更新的方向,γ,ηγ,η分別用來控制上次梯度方向和本次梯度方向?qū)ψ罱K更新方向的貢獻程度,其中γ∈(0,1]γ∈(0,1]在開始階段常常被設(shè)置為0.5,當(dāng)學(xué)習(xí)趨向穩(wěn)定后,逐漸增加到0.9甚至更高。 可以把待優(yōu)化的目標(biāo)函數(shù)想象成一座山,在山頂將一個小球推下,小球在山坡上滾動的位置即系統(tǒng)的loss值,在往下滾動的過程中小球的動量不斷增加,由于動量的存在,當(dāng)小球滾動到山坡中較為平坦的地帶時,小球?qū)⒏菀自竭^這片地帶繼續(xù)往下滾而不是陷在這一區(qū)域停滯不前,并最終到達山谷。
圖1 左:原始SGD 右:SGD+Momentum
?
Nesterov Accelerated Gradient
Its better to correct a mistake after you have made it!
目前我們有了一個帶有動量的小球,但是這個小球在滾動的過程中總是隨著山勢的變化滾動,因此其行進的路徑極不穩(wěn)定。因此我們希望有一個更加“聰明”的小球,它不但擁有動量,而且能夠知道自己將要去哪,這樣當(dāng)前面出現(xiàn)上坡小球能夠進行減速。比如說,當(dāng)接近坡底時,小球應(yīng)該提前減速避免錯過坡底。vt=γvt?1?η?θJ(θ+γvt?1)θ=θ+vt(3)(3)vt=γvt?1?η?θJ(θ+γvt?1)θ=θ+vt具體的實現(xiàn)也非常的直接,就是將傳統(tǒng)的Momentum方法對θθ計算梯度變?yōu)閷Ζ?#43;γvt?1θ+γvt?1求梯度,這一項可以看做對小球下一步將會往哪運動的一個粗略估計。也就是說,我們的小球有了一定的對未來的“預(yù)測”能力。就像本節(jié)開頭說的,如果我們知道了小球之后會犯什么錯誤,那么是否更容易更正錯誤呢?下圖上半部分是傳統(tǒng)Momentum求下一次梯度更新方向,下半部分則是使用NAG求下一次更新方向的方法。
圖2 Momentum與NAG更新的區(qū)別
當(dāng)然,在具體實現(xiàn)時,直接計算θ+γvt?1θ+γvt?1項的梯度比較麻煩,希望更新參數(shù)時計算能和傳統(tǒng)的SGD或者Momentum方法類似,因此需要對上式的計算步驟做一些改進。
?
v_prev = v  #備份vt-1項
v = mu*v - lr * g  #這一步和傳統(tǒng)的Momentum計算一樣
p += -mu*v_prev + (1+mu)*v  #更新時真實的p應(yīng)該為p-mu*v_prev,更新后為p-mu*v_prev+v,但是為了方便計算加上上次動量項的梯度,這里的p直接保存為p-mu*v_prev+v+mu*v,也就是p(小球)的“未來位置”。
 PyTorch實現(xiàn)
Momentum/NAG的實現(xiàn)和原始論文中的實現(xiàn)有些許的不用,具體的,在PyTorch實現(xiàn)中按照如下的公式更新梯度,其中ηη為learning rate,gg為θθ的梯度。目前尚不清楚為什么要做出這樣的改變?vt=γvt?1+gθ=θ?η?vt(4)(4)vt=γvt?1+gθ=θ?η?vt具體代碼如下: > class torch.optim.SGD(params, lr=required, momentum=0, dampening=0, weight_decay=0, nesterov=False)
def step(self, closure=None):"""Performs a single optimization step.Arguments:closure (callable, optional): A closure that reevaluates the modeland returns the loss."""loss = Noneif closure is not None:loss = closure()for group in self.param_groups:weight_decay = group['weight_decay']momentum = group['momentum']dampening = group['dampening']nesterov = group['nesterov']for p in group['params']:if p.grad is None:continued_p = p.grad.dataif weight_decay != 0:d_p.add_(weight_decay, p.data)if momentum != 0:   #動量項添加param_state = self.state[p]if 'momentum_buffer' not in param_state:buf = param_state['momentum_buffer'] = d_p.clone()else:buf = param_state['momentum_buffer']buf.mul_(momentum).add_(1 - dampening, d_p)if nesterov:    #如果使用NAG,則為t+1步先保存可能到達的“位置”d_p = d_p.add(momentum, buf)else:d_p = bufp.data.add_(-group['lr'], d_p)return loss AdaGrad
算法描述
AdaGrad為的是解決傳統(tǒng)的SGD對所有參數(shù)使用相同的學(xué)習(xí)速率的問題(即1.2節(jié)中提到的第三點問題)。它使用參數(shù)的歷史梯度累計和去歸一化該參數(shù)對應(yīng)的學(xué)習(xí)速率。具體的,對于經(jīng)常出現(xiàn)的參數(shù),那么其梯度累積和較大,歸一化的學(xué)習(xí)速率就較小。而對于不常見的參數(shù),往往包含更多關(guān)于特征的信息,累積和較小,歸一化后的學(xué)習(xí)速率較大,也即是學(xué)習(xí)算法應(yīng)該更加關(guān)注這些罕見的特征的出現(xiàn)。Gt,ii=Gt?1,ii+g2t,iθt+1,i=θt,i?η√Gt,ii+??gt,i(5)(5)Gt,ii=Gt?1,ii+gt,i2θt+1,i=θt,i?ηGt,ii+??gt,i當(dāng)然,通過觀察式(5),我們也發(fā)現(xiàn)AdaGrad在學(xué)習(xí)速率的調(diào)整上存在過于激進的問題,隨著時間的累積,Gt,iiGt,ii這一項會越來越大,導(dǎo)致歸一化的學(xué)習(xí)速率越來越小,這有可能導(dǎo)致優(yōu)化函數(shù)在收斂之前就停止更新。
PyTorch實現(xiàn)
class torch.optim.Adagrad(params, lr=0.01, lr_decay=0, weight_decay=0)
def step(self, closure=None):"""Performs a single optimization step.Arguments:closure (callable, optional): A closure that reevaluates the modeland returns the loss."""loss = Noneif closure is not None:loss = closure()for group in self.param_groups:for p in group['params']:if p.grad is None:continuegrad = p.grad.datastate = self.state[p]state['step'] += 1if group['weight_decay'] != 0:if p.grad.data.is_sparse:raise RuntimeError("weight_decay option is not compatible with sparse gradients ")grad = grad.add(group['weight_decay'], p.data)clr = group['lr'] / (1 + (state['step'] - 1) * group['lr_decay'])if p.grad.data.is_sparse:grad = grad.coalesce()  # the update is non-linear so indices must be uniquegrad_indices = grad._indices()grad_values = grad._values()size = torch.Size([x for x in grad.size()])def make_sparse(values):constructor = type(p.grad.data)if grad_indices.dim() == 0 or values.dim() == 0:return constructor()return constructor(grad_indices, values, size)state['sum'].add_(make_sparse(grad_values.pow(2)))std = state['sum']._sparse_mask(grad)std_values = std._values().sqrt_().add_(1e-10)p.data.add_(-clr, make_sparse(grad_values / std_values))else:state['sum'].addcmul_(1, grad, grad)    #更新核心部分std = state['sum'].sqrt().add_(1e-10)p.data.addcdiv_(-clr, grad, std)return loss Adadelta
為了避免AdaGrad存在的學(xué)習(xí)過早停止的問題,Adadelta不再保存過去所有時刻的梯度和,而是采用decaying average的方法平滑過去的梯度值和參數(shù)值。
算法描述
圖3 Adadelta偽代碼描述
其中E[g2]tE[g2]t存儲的是歷史梯度平方的平滑值,此外,這里還需要對歷史的參數(shù)值的平方進行decaying average,也就是E[Δx2]t=ρE[Δx2]t?1+(1?ρ)Δx2tE[Δx2]t=ρE[Δx2]t?1+(1?ρ)Δxt2,然后分別進行開方處理得到RMS[Δx]t=√E[Δx2]t+?RMS[Δx]t=E[Δx2]t+?和RMS[g]t=√E[g2]t+?RMS[g]t=E[g2]t+?。最后按照xt
總結(jié)
以上是生活随笔為你收集整理的梯度下降优化算法综述与PyTorch实现源码剖析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 【转载】 Python动态生成变量
 - 下一篇: Pandas库常用函数和操作