python手寫神經網絡之優化器(Optimizer)SGD、Momentum、Adagrad、RMSProp、Adam實現與對比——《深度學習入門——基於Python的理論與實現(第六章)》

vanila SGD先不寫了,很簡單,主要從Momentum開始。

 

老規矩,先手寫,再對照書本:

其實這個還真難手寫出一樣的,尤其v的初始化,我就沒想到他怎麼做。

他默認了很多規則在裏邊,他的v沒在init初始化,也不能動態,二是在第一次update時定型。

其他方面,有些地方k、v對,其實用k或者v都能達到效果,就不贅述

class Momentum():
    def __init__(self,lr = 0.01,momentum = 0.9):
        self.lr = lr
        self.v = None#先不考慮初始優化問題,vanilla版本
        self.momentum = momentum

    def update(self,params,grads):
        if self.v is None:
            self.v = {}
            for k,val in grads.items():#.items用法
                # self.v[k] = grads[k]#寫錯了,初始化不是把grad賦值過去,只是做一個shape相同的0集
                #兩種寫法都行
                if 0:
                    self.v[k] = np.zeros_lize(params[k])
                else:
                    self.v[k] = np.zeros_like(val)
        for k in self.v.keys():
            # self.v[k] -= self.lr * grads[k]#寫錯了,這還是SGD,而是-=的寫法不能有動量衰減,必須用正常=
            self.v[k] = self.v[k] * self.momentum - self.lr * grads[k]
            params[k] += self.v[k]#一致性,只要v對grad都是減法,這裏就是加法

 

 

 

 

最後就是拿來和SGD對比一下,同樣的網絡結構,同樣是mnist,同樣的其他條件,(額外的,momentum有個參數momentum,或者叫摩擦係數反比,或者就叫動量係數,其他如學習率等參數一致。)

網絡見:https://blog.csdn.net/huqinweI987/article/details/103149203

本文代碼見:https://github.com/huqinwei/python_deep_learning_introduction/blob/master/chap06_optimizer_Momentum.py

結果對比:batch=256,圖一差距明顯,圖二迭代到10000次才逐漸拉小差距,但是也沒能接近

 

 

 

附:Adagrad

注意的點也就是分母防0,還有學習率不能更新到h上,具體的代碼見

https://github.com/huqinwei/python_deep_learning_introduction/blob/master/chap06_optimizer_Adagrad.py

class Adagrad():
    def __init__(self,lr = 0.01):
        self.lr = lr
        self.h = None

    def update(self,params,grads):
        if self.h is None:
            self.h = {}
            for k,val in grads.items():#.items用法
                #兩種寫法都行
                if 0:
                    self.h[k] = np.zeros_lize(grads[k])
                else:
                    self.h[k] = np.zeros_like(val)
        for k in self.h.keys():
            self.h[k] += grads[k] ** 2#分母不能加學習率
            params[k] -= self.lr  * grads[k] / (np.sqrt(self.h[k]) + 1e-7)#分母防0

 

圖一:錯誤實現版本(lr更新進了h)

圖二:正確版本對照

圖三:正確版本前2000迭代圖示

 

 

由圖二可知,Adagrad後期學習率明顯衰減,被Momentum追上。當然,Adagrad的優勢不一定在這裏體現,但是至少可以看到學習率降低的趨勢,時間關係,不展開,這裏主要是檢驗實現正確與否。Adagrad主要解決的是不同維度之間step的差異,在某些複雜情況下會比Momentum表現好。

RMSProp——“leaky Adagrad”:

因爲Adagrad的分母是單調的,這樣學習率衰減不可逆,最後會變0。這是個缺點,所以需要讓它leaky,這樣纔會有學習率,纔會有step。

class RMSProp():
    def __init__(self,lr = 0.01,decay_rate = 0.9):
        self.lr = lr
        self.h = None
        self.decay_rate = decay_rate

    def update(self,params,grads):
        if self.h is None:
            self.h = {}
            for k,val in grads.items():
                self.h[k] = np.zeros_like(val)
        for k in self.h.keys():
            self.h[k] *= self.decay_rate
            self.h[k] += (1 - self.decay_rate) * grads[k] ** 2#
            params[k] -= self.lr  * grads[k] / (np.sqrt(self.h[k]) + 1e-7)#

如圖:可以看到,Adagrad學習率衰減而被Momentum追上甚至有點反超的趨勢,在RMSProp已經被逆轉了。

 

但是小孩才做選擇,成年人全都要——Adam。

聲明,本書代碼可能不是唯一甚至標準實現,所以會有些困惑,尤其參考代碼中的lr_t。和我常見的如下版本就不同,感覺這個參考版本更直接,他的bias-correction(初始化偏差修正)很直接,而本例中加入了另一個因素。當然,原版Adam可能也不止一種實現,時間關係,暫時也不展開分析了。

根據描述先用直覺實現一版(結合Adagrad(RMSProp)與momentum),但是感覺直覺上不太好解釋,因爲分子是動量,之前的Adagrad的直覺解釋是,一次導數比二次導數的簡化版,但是動量怎麼解釋?動量可能方向和量級都不同~!

代碼中有bug,看完下邊再參考)

class Adam():
    def __init__(self,lr = 0.001,beta1 = 0.9,beta2 = 0.999):
        self.lr = lr
        self.v = None
        self.m = None
        self.beta1 = beta1
        self.beta2 = beta2

    def update(self,params,grads):
        if self.v is None:
            self.v = {}
            for k,val in grads.items():
                self.v[k] = np.zeros_like(val)
        if self.m is None:
            self.m = {}
            for k,val in grads.items():
                self.m[k] = np.zeros_like(val)
        for k in self.h.keys():
            self.m[k] = (self.m[k] * self.beta1) + ((1-self.beta1)*grads[k])#todo 正負號一致性檢查
            self.v[k] += (self.v[k] * self.beta2) + ((1 - self.beta2) * grads[k] ** 2)#bug!!!!!!!
            params[k] -= self.lr  * self.m[k] / (np.sqrt(self.v[k]) + 1e-7)#

實測下來,發現效果不理想,很快就停滯了,我的直覺是,分子有個動量在,這樣的step太大了?這個vanilla版本有一個改善沒有做(初始幾次迭代的權重補償),但是感覺不是核心影響。核心問題是step太大,那麼除了自己手動改一個decay出來,看看原書參考代碼,他其實是做了學習率補償,不過不是用的簡單的decay,而是利用beta1和beta2做了一個公式,

        self.iter += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)     

直覺上怎麼解釋?分子有個根號,beta2是0.999,分子會是根號下0.001,0.002,0.003這樣變化,分母會是0.1,0.19,.027這樣變化,所以相對來說分子量級很穩定,分母在變大,但是變大的有限!(個人認爲如果目的只是學習率衰減,分子分母通用一個beta也可以,當然,他也可能考慮了初始權重補償,具體一小點差別先不糾結了,如果把兩個beta反過來寫,初期學習率其實還要更大,)

先打印看一下原始版本的學習率變化(所謂原始,對比的是如果我用其他參數組合來更新lr,放在尾部對比):

數量級方面:傳進來的lr是0.001,下圖是lr_t,後邊沒有的部分,在0.0009趨於0.001。

前10次迭代,從0.0003到0.00015,後邊開始上升。

似乎,背道而馳了(至少可以肯定,學習率不是逐步降低的,不是我想的那個用處——均衡掉動量遞增問題。事實上,因爲有leaky的存在,似乎,動量遞增也不是個問題?),問題出在哪?動量在增加,學習率在增加,而分母~~~~原來分母還寫錯了(從早期的adagrad改過來,沒改掉+=),其實這纔是錯誤原因!!!

self.v[k] += (self.v[k] * self.beta2) + ((1 - self.beta2) * grads[k] ** 2)#已經包含了self.v[k]*self.beta2的部分了,前邊還用了+=符號

這裏不要糾結,用+=和用=都行,但是要保持一致(我覺得這種複雜公式就不要用+=了,亂),雖然下邊的兩種寫法是等價的~

            #self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
            #self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])

解決了這個錯誤之後,無論那個lr更新要不要,後期的結果都變好了!!!

 

lr_t修正版代碼:

    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)
        
        self.iter += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)         
        
        for key in params.keys():
            #self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
            #self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
            
            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
            

 

下面對比一下前期用不用lr修正的效果

圖一:不使用lr_t優化,圖二:使用lr_t優化(單從這個圖看,圖二似乎,至少,沒變得更好)

 

那麼,Adam內的lr_t是幹什麼的呢?其實可能公式重新排列組合一下,結果會更清晰,lr_t其實應該和v的更新合併在一起,

下圖,v是分母,m是分子,lr是分子要乘以的係數,m*lr是完整的分子部分,lr和v趨勢正好是一個互補,分母是逐漸變大的,分子經過互補之後如果是均勻的,那麼整體的step就是逐漸變小,並且初期可能會大一些(因爲m的上升),這,滿足初期動量補償的效果(圖中,m和v只取W1作爲參考,使用np.linalg.norm()得到矩陣範式)

其實不知道算不算巧合,我之前瞭解的bias-correction,動量前期的校正(可能是Momentum算法,Adam本來就是結核性的,所以可能有所不同),貌似只有分母部分:/ (1.0 - self.beta1**self.iter)) 。加入了分子np.sqrt(1.0 - self.beta2**self.iter)之後,(初期lr的那個下降)變得難以解釋起來,如果忽略那一小段iters,整體是好解釋的。(這個lr難解釋就難在,他是兩部分的合成,後邊我會給出說明)

此圖可以有nike贊助

 

我找來了論文的僞代碼:

犯了個錯誤,我使用v,h,其他人都使用m,v,看來是慣例,應該遵守,不同名,分析起來很繞!在確認了beta1、beta2和參數1、參數2的對應關係後,下面我打算也用m、v來描述參數1和參數2.(爲了方便閱讀,前文也都改了)

在循環中,先計算梯度,然後完成mt和vt的更新,然後,mt和vt都做了bias-corrected,看來分子分母都要做。

最後就是更新,這裏學習率清晰一些,沒和其他的公式合併。分子分母就是mt和vt對應的該有的樣子。

 

其實這裏是能對應的:這個就是mt和vt分別的修正,分子係數beta1,lr_t中beta1是分母,剛好符合論文除以1-beta1**t的操作(唯一的區別,書中參考代碼中(v相關的)beta2的補償用了根號,不知道是刻意還是失誤?因爲beta2對應的v本來就是梯度平方?因爲在總公式中v本來就被開根號?應該是後者,但是我沒論文作者的真實代碼,暫時無法驗證)所以結論就是,這個lr_t是分子分母都做了初始動量補償操作

lr_t = self.lr * np.sqrt(1.0 - self.beta2 ** self.iter) / (1.0 - self.beta1 ** self.iter)
        for k in self.h.keys():
#對參數名進行了修正,與前文不同
            self.m[k] = (self.m[k] * self.beta1) + ((1-self.beta1)*grads[k])#todo 正負號一致性檢查
            self.v[k] = (self.v[k] * self.beta2) + ((1 - self.beta2) * grads[k] ** 2)#
            params[k] -= (lr_t  * self.m[k]) / (np.sqrt(self.v[k]) + 1e-7)#

那麼lr_t拆解清了,應該重新打印跟蹤指標了

我進行拆解之後,lr_m和lr_v就一致了,都是初期高的一個補償(合併到lr會出現“nike”是因爲,他們分別是分子和分母,兩個分別是0.9和0.999,補償倍率不一致出現的波動,沒有太多意義)

 

 

 

相關代碼

https://github.com/huqinwei/python_deep_learning_introduction/chap06_optimizer_Adam.py

網絡結構也在總目錄下

https://github.com/huqinwei/python_deep_learning_introduction/

 

 

附:lr更新公式使用不同的beta組合對應的效果

如果不考慮初始的補償或者其他因素,我感覺似乎3、4更好呢,但是隻是從lr的角度,其實lr可以通過外部decay來處理,此處核心目的應該不是lr decay(最終趨近於1,也證明它無意干涉真正的lr)

默認方法:lr_t = self.lr * np.sqrt(1.0 - self.beta2 ** self.iter) / (1.0 - self.beta1 ** self.iter)

lr_t = self.lr * np.sqrt(1.0 - self.beta1 ** self.iter) / (1.0 - self.beta1 ** self.iter)

lr_t = self.lr * np.sqrt(1.0 - self.beta2 ** self.iter) / (1.0 - self.beta2 ** self.iter)

lr_t = self.lr * np.sqrt(1.0 - self.beta1 ** self.iter) / (1.0 - self.beta2 ** self.iter)

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章