神經網絡快速入門

首先我們先導入一些好用的包,但是核心算法我們自己實現。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import MinMaxScaler
from sklearn import datasets

神經網絡

神經網絡今天已經火的一塌糊塗了,因爲深度學習的強大能力,機器學習成功地走出了數據科學領域,實現了圖像識別、語音識別和NLP自然語言處理的各種重大突破。今天的CNN、RNN、AE、GAN甚至transformer,幾乎都是基於最簡單的神經網絡NN前饋神經網絡的。前饋神經網絡使用感知機陣列搭建網絡的層,把層與層級聯起來,中間使用非線性的激活函數,就可以構造神經網絡。神經網絡有着非常好的數學優化性質,我們認爲只要網絡規模夠大,神經網絡可以擬合逼近任何函數。在此之上,如果我們使用一些其他的技巧,神經網絡也能被當成一種概率模型,來逼近條件概率分佈。
因爲NN可以感知學習我們想要的任何信息,它可以在各種領域發光發熱。以至於只要我們面臨一個連續域的問題,都可以試一試用NN去逼近它。
感知機的計算是x.w,向量與向量的乘積;對感知機陣列,將有多個x.w,對應多個輸出,這個計算過程也可以寫成向量和矩陣的乘積x.W。其梯度計算相對於感知機並沒有什麼大的改變,只不過是把向量的w提高到了矩陣的W,標量的y提高到了向量的y。但是X沒有變化,矩陣求導的公式也不變。
y=XW+b y=XW+b
LW=XTLy \frac{∂L}{∂W}=X^T\frac{∂L}{∂y}
Lb=Ly \frac{∂L}{∂b}=\frac{∂L}{∂y}
常用的非線性激活函數有tanh、sigmoid和relu等,tanh和sigmoid性質相近,因爲tanh的數值更大,在很深層的網絡裏一般使用tanh。而relu一般用於圖像識別的神經網絡。
f(v)=11+e(v) f(v)=\frac {1} {1+e^{(-v)}}
f(v)=f(v)(1f(v)) f^{'}(v)=f(v)(1-f(v))
這裏我們先按照上面說的,實現基礎的線性層和激活函數層。但是在此之前,我們還要明確"反向傳播"的概念。

反向傳播

back prop算法,用於計算神經網絡中所有參數的梯度。實際上無非就是鏈式法則,看上面的W和b的梯度公式,我們計算參數關於loss的梯度時要先知道y關於loss的梯度,這個dL/dy要從後一層獲得,因爲這個y就是後一層的輸入x,所以反向傳播就是計算dL/dx。
LX=yWT \frac{∂L}{∂X}=yW^T
這樣就能把誤差傳播到上一層,也就是反向傳播這個名字的來源。如果是CNN、RNN等網絡,還有更復雜的反向傳播公式,但這裏我們不講。

神經網絡的正則化

神經網絡的擬合能力極爲強大,但與此同時也很容易出現過擬合。前面講過的正則化技巧也同樣可以用在神經網絡上。至於實現其實沒什麼好說的,和感知機一樣做就好了。在計算梯度時,我們額外考慮一項L2懲罰
LWλW+LW \frac{∂L}{∂W} \leftarrow \lambda W+\frac{∂L}{∂W}
這就相當於我們爲每個模型裏的參數施加了一個平方懲罰,可以把無關參數壓到接近於0,能有效控制過擬合。

MSGD

帶動量的梯度下降,每次參數更新不是完全用當前的梯度,而是基於原本的下降方向作出調整。這樣可以確定一個比較穩定的下降方向,本質上是共軛梯度法的簡化版本。共軛梯度法希望通過方向修正,讓所有的下降方向儘可能正交。而如果我們把修正係數設爲常數,就是MSGD算法。
dβd+(1β)αLW d \leftarrow \beta d+(1-\beta)\alpha\frac{∂L}{∂W}

class Sigmoid:
    def __init__(self):
        self.out = None
        
    def forward(self, x):
        self.out = 1/(1+np.exp(-x))
        return self.out
    
    def backward(self, dz):
        return dz*self.out*(1-self.out)
    

class Linear:
    def __init__(self,input_sz,output_sz, LEARNING_RATE=0.01,
                 LAMBDA = 0, momentum = 0.9):
        self.W = np.random.randn(input_sz,output_sz)*0.02
        self.b  = np.random.randn(output_sz)*0.01
        
        self.dW = np.zeros_like(self.W)
        self.db = np.zeros_like(self.b)
        
        self.lr = LEARNING_RATE
        self.lamda = LAMBDA
        self.momentum = momentum
        
        self.X = None
        
    def forward(self,X):
        self.X = X
        out =  np.dot(self.X,self.W)+self.b
        return out
    
    def backward(self,dz):
        """
        dz-- 前面的導數
        基於反向傳播的dz和動量、學習率,更新W和b
        """
        n,m = self.X.shape
        
        self.dW, self.db = self.dW*self.momentum, self.db*self.momentum
        dw = np.dot(self.X.T,dz)/n
        dw += self.lamda*self.W
        self.dW += self.lr*dw*(1-self.momentum)
        
        db = np.mean(dz, axis = 0)
        self.db += self.lr*db*(1-self.momentum)
        dx = np.dot(dz,self.W.T)
        
        return dx
    
    def update(self):
        #更新W和b
        self.W = self.W-self.dW
        self.b = self.b-self.db
        return
def sequential_forward(layers, x):
    for layer in layers:
        x = layer.forward(x)
    return x

def sequential_backward(layers, dz):
    for layer in layers[::-1]:
        dz = layer.backward(dz)
    return dz

我們來搭建一個神經網絡,擬合之前用多項式和線性迴歸做過的波士頓房價預測問題

class NN:    
    def __init__(self, LEARNING_RATE=0.01,LAMBDA = 0, MOMENTUM = 0.9):
        lr = LEARNING_RATE
        lmd = LAMBDA
        mom = MOMENTUM
        # 輸入:13維特徵
        self.layers = [
            Linear(13,20,lr,lmd,mom),
            Sigmoid(),
            Linear(20,20,lr,lmd,mom),
            Sigmoid(),
            Linear(20,1,lr,lmd,mom)
        ]
        # 輸出:1維標量

    def forward(self, X):
        X = sequential_forward(self.layers, X)
        self.out = X
        return X

    def backward(self,y):
        # 使用均方誤差
        dz = self.out-y
        self.loss = np.mean((dz)**2)
        dz = sequential_backward(self.layers, dz)
        return dz

    def update(self):
        for layer in self.layers:
            if type(layer)==Linear:
                layer.update()
                
    def fit(self, X, y, epochs, batch_sz):
        n,m = X.shape
        for epoch in range(epochs):
            for i in range(0,n,batch_sz):
                inputs = X_train[i:i+batch_sz]
                labels = y_train[i:i+batch_sz]
                model.forward(inputs)
                model.backward(labels)
                model.update()
        return
X,y = datasets.load_boston(return_X_y=True)

X_train, X_test, y_train, y_test = train_test_split( X, y, test_size = 0.2, random_state = 0)

y_train = y_train.reshape(-1,1)
y_test = y_test.reshape(-1,1)

#數據標準化處理
sc_X = StandardScaler()
X_train = sc_X.fit_transform(X_train)
X_test = sc_X.transform(X_test)
model = NN()
model.fit(X_train,y_train,500,32)
# 計算訓練集和測試集的L1 loss
loss = np.mean(np.abs((model.forward(X_train)-y_train)))
print("L1 loss on training set: %.2f"%loss)
loss = np.mean(np.abs((model.forward(X_test)-y_test)))
print("L1 loss on testing set: %.2f"%loss)
L1 loss on training set: 1.53
L1 loss on testing set: 2.83
# 我們也可以嘗試不同的lambda進行正則化,防止網絡過擬合
train_loss = []
test_loss = []
lamda_list = [0.0005*2**i for i in range(10)]

for lamda in lamda_list:
    model = NN(LAMBDA = lamda)
    model.fit(X_train,y_train,500,32)
    
    #計算訓練集和測試集上的L1誤差
    loss = np.mean(np.abs((model.forward(X_train)-y_train)))
    train_loss.append(loss)
    loss = np.mean(np.abs((model.forward(X_test)-y_test)))
    test_loss.append(loss)

plt.plot(train_loss,label='training loss')
plt.plot(test_loss,label='testing loss')
plt.title('Boston house prize regression by nn')
plt.ylabel('L1 loss')
plt.xlabel('power of lambda')
plt.legend()

在這裏插入圖片描述

神經網絡的分類問題

之前我們學習線性模型時,一般會構造多個判別器來實現多分類,但是神經網絡有着更好的做法。我們知道神經網絡允許多輸出,那麼能不能讓神經網絡直接輸出每個類別的概率呢?答案是可以,爲此我們要引入一個特殊的軟化概率函數,以及BCEloss的原型,Cross Entropy Loss。
softmax是有限項離散概率分佈的梯度對數歸一化,也被稱爲歸一化指數函數。softmax的計算如下σ(z)j=ezji=1Nezi \sigma(z)_j = \frac{e^{z_j}}{\sum_{i=1}^N e^{z_i}}
函數的輸入是從K個不同的線性函數得到的結果,而樣本向量 x 屬於第 j 個分類的概率爲softmax的輸出。它相當於讓神經網絡輸出了一個離散條件概率分佈
p(zX,W) p(z\quad|X,W)
即給定輸入X和模型參數W下分類爲z的概率,而我們在分類問題中希望優化的目標是:讓訓練集的真實條件概率分佈和模型輸出的概率分佈儘可能相同。我們知道評估概率分佈差異的方法是KL散度,而最優化KL散度的過程又可以簡化爲優化Cross Entropy的過程。離散概率分佈的交叉熵寫做
L(W)=1Ni=1Nlog(p(zi)) L(W) = -\frac{1}{N}\sum_{i=1}^{N}log(p(z_i))
它是模型接收第i個訓練集後,我們預期的正確分類的概率的負對數的加和。我們假設訓練集是從p(x)的真實採樣,則此方法獲得的是真實交叉熵的蒙特卡洛估計。把softmax的輸出代入,可以計算單個樣本輸入時,L(W)的導數。
L(W)=log(ezji=1Nezi) L(W) = -log(\frac{e^{z_j}}{\sum_{i=1}^N e^{z_i}})
計算Loss關於各z_i輸入的偏導數,要分爲i=j和i!=j討論
Lzi=ezik=1Nezk(ij) \frac{\partial{L}}{\partial{z_i}} = \frac{e^{z_i}}{\sum_{k=1}^N e^{z_k}} \quad \quad (i\neq j)
Lzi=ezik=1Nezkk=1Nezk=ezik=1Nezk1(i=j) \frac{\partial{L}}{\partial{z_i}} = \frac{e^{z_i}-\sum_{k=1}^N e^{z_k}}{\sum_{k=1}^N e^{z_k}}=\frac{e^{z_i}}{\sum_{k=1}^N e^{z_k}}-1 \quad \quad (i = j)
如果把標籤寫成one hot的向量,即正確標籤爲1,錯誤標籤爲0,就能給出非常簡單的向量梯度形式
Lz=zlabels \frac{\partial{L}}{\partial{z}} = z-labels
我們實現一個softmax層,然後就可以搭建能完成多分類任務的神經網絡了。

def cross_entropy_error(labels,logits):
    return np.mean(-np.sum(labels*np.log(logits),axis=1))

def softmax(input_X):
    n,m = input_X.shape
    exp_a = np.exp(input_X)
    sum_exp_a = np.sum(exp_a,axis=1)
    sum_exp_a = np.tile(sum_exp_a,(m,1))
    sum_exp_a = sum_exp_a.T
    ret = exp_a/sum_exp_a
    return ret

class SoftMax:
    '''
    softmax,歸一化層,把輸出轉概率
    該層的輸出代表每一分類的概率
    '''
    def __init__ (self):
        self.y_hat = None
        
    def forward(self,X):
        self.X = X
        self.y_hat = softmax(X)
        return self.y_hat
    
    def backward(self,labels):
        # 使用cross entropy
        dx = (self.y_hat-labels)
        return dx

我們搭建一個神經網絡來分類sklearn提供的手寫數字數據集,不用MNIST是因爲MNSIT跑起來太慢了。

class NN:    
    def __init__(self, LEARNING_RATE=0.01,LAMBDA = 0, MOMENTUM = 0.9):
        lr = LEARNING_RATE
        lmd = LAMBDA
        mom = MOMENTUM
        # 輸入:8x8 = 64維特徵
        self.layers = [
            Linear(64,128,lr,lmd,mom),
            Sigmoid(),
            Linear(128,64,lr,lmd,mom),
            Sigmoid(),
            Linear(64,32,lr,lmd,mom),
            Sigmoid(),
            Linear(32,10,lr,lmd,mom),
            SoftMax()
        ]
        # 輸出:10維分類

    def forward(self, X):
        X = sequential_forward(self.layers, X)
        self.out = X
        return X

    def backward(self,y):
        # 使用softmax和交叉熵
        self.loss = cross_entropy_error(y,self.out)
        dz = y
        dz = sequential_backward(self.layers, dz)
        return dz

    def update(self):
        for layer in self.layers:
            if type(layer)==Linear:
                layer.update()
                
    def fit(self, X, y, epochs, batch_sz):
        n,m = X.shape
        for epoch in range(epochs):
            for i in range(0,n,batch_sz):
                inputs = X_train[i:i+batch_sz]
                labels = y_train[i:i+batch_sz]
                model.forward(inputs)
                model.backward(labels)
                model.update()
        return
def one_hot_encode(labels):
    '''
    labels:一維數組,返回獨熱編碼後的二維數組
    '''
    labels = labels.astype(np.int)
    n = len(labels)
    m = labels.max()+1
    ret = np.zeros((n,m))
    rows = np.linspace(0,n-1,n).astype(np.int)
    ret[rows,labels] = 1
    return ret
X,y = datasets.load_digits(return_X_y=True)

X_train, X_test, y_train, y_test = train_test_split( X, y, test_size = 0.2, random_state = 0)

y_train = one_hot_encode(y_train)

#處理圖片的一般預處理方法是歸一化到0-1區間
sc_MM = MinMaxScaler()
X_train = sc_MM.fit_transform(X_train)
X_test = sc_MM.transform(X_test)
model = NN(LEARNING_RATE=0.5)
losses = []
for step in range(20):
    model.fit(X_train,y_train,25,50)
    losses.append(model.loss)
plt.plot(losses)
plt.title('Digits dataset training curve')
plt.ylabel('CE loss')
plt.xlabel('epoch(25)')

在這裏插入圖片描述

def one_hot_decode(prob):
    '''
    prob:二維數組,返回標籤一維數組
    '''
    return np.argmax(prob,axis = 1)
y_test_pred = one_hot_decode(model.forward(X_test))
accuracy_score(y_test_pred,y_test)
0.9611111111111111

過擬合

神經網絡有着非常驚豔的擬合能力,上面我們已經看到了神經網絡可以逼近函數和概率分佈,實現多種有趣的任務。但是和上面的玩具數據集做出來的結果不同,神經網絡在實際問題裏非常有可能出現更爲嚴重的過擬合。而除了上面的正則化方法以外,我們還有其他方法控制過擬合。
Eealy Stop
一種相當直覺的方法,但是經常能夠湊效。我們在訓練時用一個驗證集輔助訓練,一旦訓練出現了讓測試集loss下降的趨勢就立刻停止訓練。
DropOut
從模型融合中得到啓發,做過Kaggle的人應該有經驗,在模型和算法已經完善的末期,我們進一步提升正確率的方法就是在模型中引入隨機因素,製造很多的模型形成委員會,用這個混合的模型進行任務決策。而神經網絡自己本就是一個混合模型,如果我們使用dropout的技巧,即每次訓練時,隨機臨時刪除一些 神經元,它們將不在這次運算中發揮作用,同樣也不會被更新。這樣,一個神經網絡就相當於多個小型神經網絡的混合模型,這個方法在大型任務重常常很實用。
更好的數據集
更多更好的數據是防止過擬合的最好方法,即使沒有更多的數據,我們也可以用一些技巧創造數據。這個做法在圖像和語音領域尤其適用,我們可以隨機對圖片進行旋轉,平移,加噪聲,來獲得更多更穩定的數據集。
reinforcement
強化學習是一種特殊的方法,它基於模型的對抗進行。在對弈和決策問題裏,我們可以通過模型對抗的方式讓模型自行學習,從而獲得超越數據集的成效。最好的例子就是擊敗人類的alphago。

class Dropout:
    '''
    隨機把一些神經元的輸出變爲0
    從而前向和反向傳播中,這個神經元將不再發揮作用
    '''
    def __init__ (self, prob, train = True):
        self.p = prob # dropout 的概率
        self.train = train # 訓練模式,dropout層啓用
        
    def forward(self,X):
        if not self.train: return X
        self.mask = np.random.rand(len(X))>self.p
        X[self.mask] = 0
        return X
    
    def backward(self,dz):
        if not self.train: return dz
        dz[self.mask] = 0
        return dz
class NN:    
    def __init__(self, LEARNING_RATE=0.01,LAMBDA = 0,
                 MOMENTUM = 0.9, DropOut_rate = 0.5):
        lr = LEARNING_RATE
        lmd = LAMBDA
        mom = MOMENTUM
        rate = DropOut_rate
        # 輸入:13維特徵
        self.layers = [
            Linear(13,50,lr,lmd,mom),
            Sigmoid(),
            Dropout(rate),
            Linear(50,50,lr,lmd,mom),
            Sigmoid(),
            Linear(50,10,lr,lmd,mom),
            Sigmoid(),
            Linear(10,3,lr,lmd,mom),
            SoftMax()
        ]
        # 輸出:10維分類

        
    def eval(self):
        # 計算模式,關閉所有dropout
        for layer in self.layers:
            if type(layer)==Dropout:
                layer.train = False
                
                
    def train(self):
        # 訓練模式,開啓所有dropout
        for layer in self.layers:
            if type(layer)==Dropout:
                layer.train = True
        
    def forward(self, X):
        X = sequential_forward(self.layers, X)
        self.out = X
        return X

    def backward(self,y):
        # 使用softmax和交叉熵
        self.loss = cross_entropy_error(y,self.out)
        dz = y
        dz = sequential_backward(self.layers, dz)
        return dz

    def update(self):
        for layer in self.layers:
            if type(layer)==Linear:
                layer.update()
                
    def fit(self, X, y, epochs, batch_sz):
        n,m = X.shape
        for epoch in range(epochs):
            for i in range(0,n,batch_sz):
                inputs = X_train[i:i+batch_sz]
                labels = y_train[i:i+batch_sz]
                model.forward(inputs)
                model.backward(labels)
                model.update()
        return
X,y = datasets.load_wine(return_X_y=True)

# 我們設置較高的test_size來觀察過擬合的發生
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size = 0.4, random_state = 0)

y_train = one_hot_encode(y_train)

#數據標準化處理
sc_X = StandardScaler()
X_train = sc_X.fit_transform(X_train)
X_test = sc_X.transform(X_test)
model = NN(LEARNING_RATE=0.5,DropOut_rate=0.5)
model.train()
model.fit(X_train,y_train,1000,20)
model.eval()
y_test_pred = one_hot_decode(model.forward(X_test))
accuracy_score(y_test_pred,y_test)
0.9583333333333334

神經網絡優化遇到的問題

局部最優
我們一直避而不談的一個問題是,爲什麼神經網絡能用SGD train到全局最優?前面學習邏輯迴歸時,梯度下降能把loss優化到最優,是因爲不論是MSE誤差,還是BCE誤差,它們在邏輯迴歸的模型上都是凸優化,所以我們必然能用梯度下降找到最優解。但神經網絡不同,我們並不知道它是不是凸優化啊?
事實上也和我們的直覺相同,神經網絡不是凸優化,而且我們也很難去找到一個全局最優。一個相當反直覺的現實是,我們連局部最優都很難找到。優化理論告訴我們,一個函數上的某點如果是局部最優,那麼它的Hessian矩陣是正定的。正定的矩陣要求所有矩陣特徵值都是正的,而在一個大型網絡裏,對成千上萬的參數所在的高維空間,計算出的H矩陣將會非常巨大,特徵值也非常多。而想找到一個點讓所有特徵值都是正的極爲困難,也就是,在參數空間內,我們幾乎不可能觸摸到局部最優,這也就讓所謂被"局部最優"卡住,找不到全局最優的事情幾乎不可能發生。
鞍點
換句話來說,我們訓練神經網絡時面對的問題並不是被局部最優限制,而是被一般的0梯度點,即鞍點限制。而幸運的是,鞍點並不具備局部最小點的那種強大的吸引力,在隨機梯度下降和隨機初始化下生成的網絡,雖然會經過鞍點,但在連續的梯度下降優化下,都會跳出鞍點。而牛頓法之所以無法在神經網絡優化中取得比較好的效果,就是因爲它會落入鞍點。
高原
還有一些在使用sigmoid和tanh時我們不會遇到的問題,如果我們使用relu作爲激活函數,則loss的空間很有可能出現大片的,平坦的區域。在這些區域上梯度完全爲0,優化無法繼續進行。一種解決方案是使用leaky relu,使用更好的參數初始化策略,和更好的梯度學習算法。
梯度爆炸與消失
在深層的計算圖中,梯度很可能隨着反向傳播衰減,以至於梯度很快爲接近於0.這一點在深度的卷積網絡和RNN中經常出現,CNN解決這個問題的方法是把上層的神經元直接和幾層後的神經元連接,允許梯度跨層傳播,抑制梯度的消失。RNN中因爲計算圖要隨着序列長度變化,梯度還很容易變得陡峭,RNN解決這個問題的方法是使用更柔和的LSTM。
我們想要的
回顧一下我們上面做的事情,我們想要的不是神經網絡能完美擬合訓練集,而是具有最好的泛化能力。所以我們才用了那麼多的方法防止過擬合,這也說明了,找不到一個全局最優其實一點都不重要,全局最優一般對應着較差的泛化能力,我們真正的訓練,很多時候都是"什麼都沒有做到"就停止訓練了,但這並不重要。

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