與神經網絡學習相關的技巧

目錄

SGD

Momentum

AdaGrad

Adam

4種更新參數方法的總結

權重的初始值

ReLU的權重初始值

Batch Normalization

正則化

過擬合

權值衰減

Dropout

超參數


神經網絡的學習是找到使損失函數值儘可能小時,對應的權重參數。在之前的專題中,我們介紹了梯度法來更新參數,以逐漸靠近最優參數,這個過程就是隨機梯度下降法(SGD),其核心思想是參數的更新方向是沿着變化最大的方向進行,而我們都知道梯度(即斜率)就是指向變化最大的方向。因此基於SGD,權重參數能以最快的速度找到可能的最優值。

假設一位盲人要從山頂走到山谷,他應該如何走才能迅速走到山底呢?

藉助梯度法思想,一種方法是,該盲人可以藉助腳去感知地面的坡度,坡度越傾斜的地方,就是他應該走的地方,這樣纔可能以最快的速度到達山底。

SGD

 SGD數學表達式如下:

                                               W\leftarrow W-\eta\frac{\partial L}{\partial W}                            (1)

\frac{\partial L}{\partial W}是損失函數關於權重參數的梯度,\eta是學習率(比如取0.01,0.001),該表達式直觀地表示了權重參數沿着梯度的方向進行更新,而更新的強度由學習率決定,學習率越大,更新的就越快,學習率越小,更新的就越慢。可見學習率的取值也是我們重點研究對象。

SGD比價簡單,我們可以用Python實現,這裏把它實現爲一個類。

class SGD:
    def __init__(self,lr=0.01):   
        self.lr=lr                #實例變量,學習率lr
    def update(self,params,grads):
        for key in params.keys():
            params[key]-=self.lr*grads[key]   #表達式實現,參數W,b保存在字典params中,grads爲誤 
                                              #差反向傳播法求梯度的函數,可在前面專題中查找
  

雖然SGD比較直觀且容易實現,但是它比較低效,因爲梯度的方向並沒有指向最小值的方向,它可能指向局部最小值,或局部極小值,或全局極小值。因此,我們更新的效率可能會很低,得到的結果並不是最優值。圖1是SGD尋找最小值的更新路徑。可見,SGD呈“”字形移動更新,這是一個相當低效的路徑。我們非常希望更新路徑按起始位置到最小值之間的直線進行更新,最大程度降低更新路徑少走“彎路”。基於SGD的缺點,我們來介紹其他的一些梯度更新的方法。

圖1  SGD方法更新路徑

Momentum

“momentum”這個單詞的意思是動量,它是一個物理名詞。簡單來講,它的意思就是如果物體在某一方向上受力,那麼就會產生加速度,使得物體在該方向上的速度增加。從而在整體上使得物體能儘早到達目標位置,換句話說,就是可以讓我們少走彎路,高效而快速找到最小值。採用momentum來權重參數的更新的表達式如下:

                                       v\leftarrow \alpha v-\eta\frac{\partial L}{\partial W}                  (2)

                                      W\leftarrow W+v                        (3)

           該表達式多了兩個參數,\alphav ,  \alpha是一個常量,我們可以理解爲\alpha是物理上的摩擦或空氣阻力,可以取(0~1)之間的任何數值。v是一個變量,可以理解爲物理上的速度,可見公式(2)和物理上的速度表達式可以相互對應,它表示物體在梯度方向上受力,在這個力的作用下,物體的速度增加。再次強調公式中的變量一般爲包含多個相同類型參數的矩陣。我們以圖1爲例,最小值的位置在起始位置的右側,我們希望更新位置朝x軸移動的速度大於朝y軸移動的速度,實際上,物體一直都受到朝x軸正方向的力,顯然,在這個力的作用下,我們應該加速物體在x軸方向的速度。而物體在y軸方向上的受力雖然很大,但是因爲交互地受到正方向和反方向的力,所以它們會相互抵消,所以y軸方向上速度不穩定。因此,和SGD相比,momentum可以更快地朝 x軸方向靠近,減弱“之”字形的變動程度。圖2爲momentum方法的更新路徑。

圖2 momentum 方法更新路徑

momentum方法用Python實現的代碼如下:

# coding: utf-8
import numpy as np

class Momentum:

    """Momentum SGD"""

    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None
        
    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():                                
                self.v[key] = np.zeros_like(val)
                
        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key] 
            params[key] += self.v[key]

 代碼中,實例變量v會保存物體的速度。初始化時,v不保存任何值,第一次調用update()時,v會以字典變量的形式保存與參數結構相同的數據。

AdaGrad

在前面,我們提及過學習率的值對參數尋優相當重要,學習率過小,會導致學習花費過多時間;反過來,學習率過大,則會導致學習發散而不能正確進行。有關學習率的典型方法中,有一種被稱爲學習率衰減法,即隨着學習的進行,學習率逐漸減小,一開始多學,然後逐漸“少”學。基於這種方法,誕生了AdaGrad方法,由這個名字可知,它是adaptive和gradient的縮寫,意思是“適當的、梯度”。其核心思想是爲參數的每個元素適當地調整學習率,AdaGrad的表達式如下:

                                                              h\leftarrow h+\frac{\partial L}{\partial W}\bigodot \frac{\partial L}{\partial W}                                          (4)

                                                             W\leftarrow W-\eta \frac{1 }{\sqrt[]{h}} \frac{\partial L}{\partial W}                                              (5)

變量h保存了以前所有梯度值的平方和,\bigodot表示矩陣元素的乘法。可知,在更新參數時,通過乘以\frac{1 }{\sqrt[]{h}}就可以調整學習的尺度,即參數的元素中變動大(更新幅度大)的元素的學習率將變小。也就是說,可以按參數的元素進行學習率衰減,使變動大的參數的學習率逐漸減小。基於這樣的思想,我們再來分析圖1,物體在y軸方向上的梯度較大(變化大),所以AdaGrad方法會逐漸減少在y軸方向上的更新步伐,以減弱"之“字形,而在x軸方向上的更新的梯度本身就小,所以AdaGrad方法會加快在x軸方向上的更新,就像一條直線一樣,直逼最小值。圖3是AdaGrad方法的更新路徑。

圖3 AdaGrad方法更新路徑

由圖可見,由於y軸方向上的梯度較大,因此剛開始變動較大,但後面會根據這個較大的變動按比例進行調整,減少更新的步伐,因此在y軸方向上的更新程度被減弱,“之”字形的變動程度被衰減,在x軸方向上的更新基本上在平穩進行。AdaGrad方法的Python實現的代碼如下:

# coding: utf-8
import numpy as np

class AdaGrad:

    """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 key, val in params.items():
                self.h[key] = np.zeros_like(val)
            
        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

關於代碼有一個地方需要強調,最後一行代碼加上了微小值1e-7。這是爲了防止當self.h[key]中有0時,將0作爲除數的情況,在很多深度學習的框架中,都有這個參數值。

Adam

Adam方法就是將momentum方法和AdaGrad方法融合在一起,即它考慮了兩種情況:物體在受力方向上的速度會增加;梯度變動大的參數的學習率將被逐漸減小。有關Adam方法的數學原理請參見原作者的論文(http://arxiv.org/abs/1412.6980v8)圖4爲Adam方法的更新路徑。

圖4 Adam方法的更新路徑

從圖4可看出,Adam和momentum有類似的更新路徑,但Adam的移動的波動程度有所減輕,這得益於學習的更新程度被適當的調整了。Adam方法的Python實現如下:

# coding: utf-8
import numpy as np

class Adam:

    """Adam (http://arxiv.org/abs/1412.6980v8)"""

    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    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)
            
            #unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
            #unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
            #params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)

4種更新參數方法的總結

到目前爲止,我們介紹了4種更新權重的方法,方法的不同,使得參數更新的路徑也不同。實踐證明,沒有哪一種方法是絕對最佳的,各自都有其適用場合,都有各自擅長解決的問題和不擅長解決的問題。不過,一般而言,與SGD相比,其他3種方法可以學習得更快,有時最終的識別精度也更高。

權重的初始值

在本專題前面介紹的幾種權重參數的尋優方法都是以學習率\eta爲切入點。下面我們以權重參數的初始值爲切入點,試圖改善神經網絡的學習性能。

神經網絡的學習,實質就是通過以損失函數爲目標,修改權重參數爲中間過程的一個學習任務。而權重參數的初始值一般通過正態或高斯分佈進行隨機初始化,我們有理由相信權重的初始值對神經網絡的學習至關重要,它關乎神經網絡的學習是否成功。直覺告訴我們,初始值設置的恰當可以減少神經網絡的學習時間,快速找到損失函數的最小值。

權重初始值或權重中間值到底應該設置成什麼樣的值、或者權重值應該具有什麼樣的特點才有助於神經網絡的學習呢?直觀地,我們可以簡單地分析一下:

權重的大小應該分佈的更有廣度一些,杜絕某些特徵的權重係數過大(權重過大,說明其對應的特徵的重要性很大,可能導致模型過擬合等問題),而權重過小比如爲0,則可能就丟棄了某些特徵的重要性。因此,不管怎麼樣,我們希望權重的分佈廣度儘量相同,或者說,至少在權重初始值時,應該做到這一點。回想一下,機器學習中提及的一些數據預處理方法的意義,比如數據的(0~1)縮放,即把數量級分佈大的數據集縮放到大小範圍在0~1之間。這麼做的意義是什麼呢?不難理解,這樣做的目的是減少數據大小分佈過大而帶來的影響極限化,比如有的特徵項的數據是1000,有的特徵項的數據是1,顯然就會顯性地得出這些特徵項的重要性的差異。而通過(0~1)縮放後,就會降低或平衡這些特徵項的重要性,就好比不帶有感情色彩去對待某個人一樣,儘量做到公平公正。

我們以神經網絡爲例來做一個簡單的試驗,通過對權重參數的隨機初始化(服從高斯分佈),並分別對初始化後的權重乘以1和0.01(及標準差分別爲1和0.01的高斯隨機分佈),然後在這兩種情況下,觀察權重初始值對隱藏層的激活函數的輸出(簡稱激活值)的分佈的影響。輸入數據爲隨機產生的1000個數據,特徵有100個;隱藏層設爲5層;各隱藏層的神經元100個,激活函數使用sigmoid函數。我們用直方圖繪製各層激活值的數據分佈。標準差爲1的高斯分佈隨機初始化作爲權重初始值的情況下,Python代碼如下:

# coding: utf-8
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
   
input_data = np.random.randn(1000, 100)  # 1000個數據
node_num = 100  # 各隱藏層的節點(神經元)數
hidden_layer_size = 5  # 隱藏層有5層
activations = {}  # 激活值的結果保存在這裏
x = input_data
for i in range(hidden_layer_size):
    if i != 0:
        x = activations[i-1]

    # 改變初始值進行實驗!
    w = np.random.randn(node_num, node_num) * 1 #標準差爲1的高斯隨機分佈作爲權重初始值
    a = np.dot(x, w)
    z = sigmoid(a)
    activations[i] = z
# 繪製直方圖
for i, a in activations.items():
    plt.subplot(1, len(activations), i+1)
    plt.title(str(i+1) + "-layer")
    if i != 0: plt.yticks([], [])
    plt.hist(a.flatten(), 30, range=(0,1))
plt.show()

得到的直方圖如圖5所示:

圖5  標準差爲1的高斯分佈隨機初始化作爲權重初始值時的各層激活函數值的分佈

標準差爲0.01的高斯分佈隨機初始化作爲權重初始值的情況下,Python代碼和上面的一樣,只需把設定權重初始值的地方換成下面的代碼即可。

#w = np.random.randn(node_num, node_num) * 1
w = np.random.randn(node_num, node_num) * 0.01

得到的各層激活值的分佈如圖6所示:

圖6 標準差爲0.01的高斯分佈隨機初始化作爲權重初始值時的各層激活函數值的分佈

簡單來說,標準差從1修改爲0.01,就好比數據進行0~1縮放,主要是爲了使各層的激活值分佈更有廣度,然而從圖6來看,效果似乎比我們期望的要差些。

我們先分析圖5,從圖5可知,各層的激活值呈現偏向0和1的分佈。這裏我們使用激活函數是sigmoid函數,在前面的專題中,我們講過sigmoid函數的的反向傳播,如圖7所示:

圖 7 sigmoid函數的反向傳播

可知,sigmoid函數的反向傳播只需根據正向傳播的輸出就能計算出來。由於圖5展示了sigmoid函數正向傳播的輸出y偏向0和1,所以其反向傳播接近0,即梯度的值不斷變小,最後消失。這就是我們常說的梯度消失。這樣會對神經網絡的學習帶來嚴重的後果。

從圖6可知,使用標準差爲0.01的高斯隨機分佈後,各層的激活值的分佈主要集中在0.5附近。這樣的結果比前一種好些,不會發生梯度消失的問題。但是,仔細現象,激活值集中在0.5附近也是有問題的。激活值的分佈一旦有所偏向,說明神經元的表現力就不夠好。打個比方,如果100個神經元的輸出值幾乎相同,那麼不就可以由1個神經元來表達基本相同的事情了。因此激活值在分佈上有所偏向會出現“表現力受限”的問題。導致學習可能無法進行或做無用功。

綜上,我們希望激活值的分佈更有廣度,而上面介紹的標準差爲1或0.01的高斯隨機初始化權重參數的方法都存在激活值偏向的問題。現在我們使用另外一種權重初始化的方法(稱爲Xavier初始值):如果前一隱藏層的神經元(節點)數量爲n,則初始值使用標準差爲\frac{ 1}{\sqrt{n} }的分佈。其實理解起來也不難,也就是說前一層的神經元越多,則目標神經元的初始化的權重尺度就越小(相當於標準差越小的高斯分佈)。Python實現,只需將設定權重初始值的地方換成如下內容即可:

node_num=100 #前一層的神經元數量
w = np.random.randn(node_num, node_num)/np.sqrt(node_num)

這種權重初始化得到的激活值的分佈如圖8所示:

圖8 Xavier初始值作爲權重初始值時的各層激活函數值的分佈

從圖8得到的結果來看,越是後面的層,圖像就變得越是沒有規則(越歪斜),但是呈現了比之前更有廣度的分佈。由於各層間傳遞的數據有適當的廣度,所以sigmoid函數的表現力不受限制,有望進行高效的學習。

ReLU的權重初始值

之前介紹的初始值方法都是針對以sigmoid函數作爲激活函數而實現的,由於激活函數的不同,我們對權重的初始化方法可能就不同。如果激活函數是ReLU函數,那麼就有專門的權重初始化方法。這裏我們介紹一種“He初始值”:當前一層的節點數爲n時,He初始值使用標準差爲\sqrt{\frac{2 }{n}}的高斯分佈。其實理解起來也不難,因爲ReLU的負值區域的值爲0,爲了使它更有廣度,所以需要2倍的係數。有關He初始值的Python實現,只需在將激活函數sigmoid的定義修改爲ReLU函數的定義、權重初始值修改爲He初始值即可,代碼如下:

def ReLU(x):
    return np.maximum(0, x)    #ReLU激活函數

w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)  #He初始值

讀者可親自嘗試觀察激活函數爲ReLU函數或sigmoid函數甚至tanh函數時,不同權重初始值(上面介紹的標準差0.01初始化、Xavier初始值、He初始值)的激活值的分佈情況,並總結各種初始值方法應用於不同激活函數時的結果。相關的代碼都已經給出了,希望讀者自己去實踐操作吧!

溫馨提示,tanh函數的Python實現如下:

def tanh(x):
    return np.tanh(x)

Batch Normalization

前面我們將了通過設定合適的權重參數初始值,則各層的激活值分佈會有適當的廣度。現在,我們引入一種新的方法----Batch Normalization,爲了讓各層擁有適當的廣度,"強制性"地調整激活值的分佈。Batch Normalization的優點主要有以下三點:

  • 可以增大學習率,進行快速學習
  • 不會過度依賴初始值
  • 抑制過擬合

Batch Normalization方法的思路是調整各層的激活值分佈使其擁有適當的廣度。爲達到這樣的效果,我們需要在網絡插入對數據進行正則化的層,這裏稱爲Batch Norm層。從名字就不難理解,Batch即以mini-batch爲單位,Norm即正則化。具體來說,數據分佈的均值爲0,方差爲1的正則化,數學表達式如公式(6)所示:

                                           \mu _{B}\leftarrow \frac{1 }{m}\sum_{i=1}^{m}x_{i}

                                          \sigma {_{B}}^{2}\leftarrow \frac{1 }{m}\sum_{i=1}^{m}(x_{i}-\mu _{B})^{2}                              (6)

                                           \breve{x}_{i}\leftarrow \frac{x_{i}-\mu _{B}}{\sqrt{\sigma {_{B}}^{2}+\epsilon }}             

                                            y_{i}\leftarrow \gamma \breve{x}_{i}+\beta                      

mini-batch的m個輸入數據的集合B=\left \{ x_{1},x_{2},x_{3}....x_{m} \right \}求平均值\mu _{B}和方差\sigma {_{B}}^{2}。然後進行均值爲0,方差爲1的正則化,將輸入數據x變爲\breve{x}_{i}\epsilon是一個微小值,以防止出現分母爲0的情況出現。最後,對正則化後的數據進行縮放\gamma(初始值爲1)和平移\beta(初始值爲0)。\gamma\beta通過學習進行調整。一般地,我們可以把Batch Norm層放在激活函數的前面或後面,減小數據分佈的偏向。流程如圖9所示。

圖9  Batch Normalization 正則化的神經網絡

 

正則化

有過機器學習背景的人,對“正則化”應該不陌生,“正則化”一般與模型過擬合/欠擬合/泛化能力等術語一起出現。這裏我們主要討論模型過擬合時,如何通過“正則化”來提高模型的泛化性能,使得模型可以對沒有包含在訓練數據裏的觀測數據也能進行高精度識別。

過擬合

簡單講,就是模型對訓練數據的識別能力強,而對測試數據的識別能力弱,產生該現象的原因主要有兩個:

  • 模型過於複雜,參數太多、表現力強
  • 訓練數據樣本量少

權值衰減

權值衰減是一種常用來抑制過擬合的方法,正如其名,模型在學習過程中,對大的權重進行懲罰(衰減),來抑制過擬合。

我們知道,神經網絡的學習目的是減少損失函數的值。如果爲損失函數加上權重W的平方範數(L2範數),就可以抑制權重變大。即L2範數的權值衰減爲\frac{1}{2}\lambda W^{2},我們將這個值加到損失函數上。\lambda是控制正則化強度的超參數,\lambda越大,則對權重施加的懲罰就越嚴重。\frac{1}{2}用於\frac{1}{2}\lambda W^{2}進行反向傳播求導後變成\lambda W設計的。因此,在求權重梯度的計算中,要爲之前的誤差反向傳播法的結果加上正則化項的導數\lambda W

L2範數相當於各個元素的平方和。比如W=\left ( w_{1},w_{2},w_{3}.....w_{n}}\right ),則L2範數可以用\sqrt{w_{1}^{2}+w_{2}^{2}+.....w_{n}^{2}}}}}}}計算。此外,還有L1範數(相當於各個元素的絕對值之和)、L∞範數(相當於各個元素的絕對值中最大的那一個)。這些範數各有各的特點。

關於L2範數的Python實現,這裏就不給出了,讀者只要明白了L2範數的數學表達式,就可以對前面專題介紹的多層神經網絡的實現部分進行修改。

Dropout

L2範數抑制過擬合的方法雖然簡單,但是難以應付複雜的模型,基於此,我們使用Dropout方法。其原理如圖10所示。

圖10 Dropout隨機刪除神經元示意圖

Dropout是一種在學習過程中隨機刪除神經元的方法,被刪除的神經元不參與信號的傳遞。訓練時,每傳遞一次數據,就會隨機選擇要刪除的神經元。測試時,會傳遞所有所有神經元信號,但是各個神經元的輸出要乘上訓練時的刪除比例後再輸出。Dropout的Python簡單實現如下:

import numpy as np
class Dropout:
    def __init__(self,dropout_ratio=0.5):
        self.dropout_ratio=dropout_ratio
        self.mask=None
    def forward(self,x,train_flag=True):
        if train_flag:
            self.mask=np.random.rand(*x.shape)>self.dropout_ratio
            return x*self.mask
        else:
            return x*(1.0-self.dropout_ratio
    def backward(self,dout):
            return dout*self.mask

每次正向傳播時,self.mask以false形式保存要刪除的神經元。self.mask會隨機生成和x形狀相同的數組,並將比dropout_ratio大的元素設爲true。正向傳播時傳遞了信號的神經元,反向傳播時按原樣傳遞信號(forward和backward函數共用self.mask值);否則反向傳播時信號將停在那裏。

超參數

超參數一般是指神經元數量、batch大小、學習率、權值衰減等。實踐證明,超參數的設置對模型的性能影響很大。一般地,超參數的設置需要我們反覆地去調整或試錯,因此需要有專門的數據集來完成這一任務,這裏我們引入驗證數據來完成超參數的調整。

一般地,訓練數據完成參數(權重和偏置) 的學習,驗證數據完成超參數的調整,測試數據完成模型泛化性能的評估。

顯然,進行超參數調整前,我們需要對原始數據集進行分割(訓練數據,驗證數據,測試數據),關於數據分割的方法也是有技巧的,這裏就不多說了。其實這裏講的超參數調整方法和機器學習中我們常常聽說的格子搜素、交叉驗證等超參數調整方法類似。因此,我們簡單歸納一下超參數的調整步驟:

  • 設定超參數的範圍(以10的階乘爲尺度)
  • 從設定的超參數範圍中隨機採樣
  • 使用採樣到的超參數的值進行學習
  • 通過驗證數據評估識別精度
  • 根據識別精度的結果,縮小超參數範圍,再進行學習

這裏,我們以學習率和權值衰減強度的係數爲例,如何用Python實現學習率爲(10^{-6}~10^{-2})和權值衰減係數(10^{-8}~10^{-4})範圍的隨機選擇。

import numpy as np
weight_decay=10**np.random.uniform(-8,-4)
lr=10**np.random.uniform(-6,-2)

讀者可使用採樣到的超參數進行學習,然後通過驗證數據評估識別精度,然後再進一步縮小超參數範圍,反覆循環,尋找一個最優的超參數的值。

本專題的知識就講到這裏了,回顧一下,我們主要講了參數的更新方法,權重初始值的賦值方法,抑制過擬合的正則化方法,超參數的搜索方法等。

歡迎關注微信公衆號“Python生態智聯”,學知識,享生活!

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