搭建深度學習框架(一) 線性層,激活函數,損失函數

回顧

前面我們學習(複習)了線代,概率論和優化等數學課, 還複習了基礎的機器學習算法, 邏輯迴歸 SVM和決策樹等. 現在我們進入深度學習的第一課, 就是前饋神經網絡的原理和使用. 神經網絡是一種非常強大的機器學習模型, 在今天已經隱隱有取代其他所有傳統模型的趨勢. 我們之前使用統計概率模型來做NLP和語音識別, 但今天大多數從業者都會選擇RNN; 在傳統CV領域我們使用數字圖像處理方法來做圖像分割, 目標識別等任務, 但今天我們有R-CNN和其他更高級的神經網絡架構能把這個事情做的更好. 我們甚至可以用Seq2Seq模型做神經機器翻譯, 用GAN做圖像風格轉換和圖像修復. 這一切都是那麼激動人心.

線性層

在這裏插入圖片描述
我們前面已經講到了邏輯迴歸, 它是非常簡單的線性模型, 我們用m維向量x各個維度的線性組合y=xw+by = xw+b生成標量y, 作爲二分類的判據. 前饋神經網絡的線性層做的事情就是, 我們現在用輸入x經過n個不同的w和b, 生成n個y, 一字排開變成新的n維向量. 這個過程就不是行向量xx和列向量ww相乘了, 而是m維行向量xx和m行n列的矩陣WW相乘, 再與一個n維的列向量b相加. 這時輸出是n維的列向量y. 這就是線性層, 也就是pytorch的nn中實現的Linear模塊.
forward ruley=xW+b forward\ rule\qquad y = xW+b
這個矩陣運算過程就是前向傳播, 我們後面會把多個這樣的線性層級聯起來, 形成神經網絡. 爲了在這樣的網絡中計算每一個參數的梯度, 我們在優化時不僅要計算W和b的梯度, 還要計算x的梯度. 用矩陣乘法的求導法則和鏈式法則, 我們可以計算目標損失函數關於W,b和x的導數
W backward ruleLW=xTLy W\ backward\ rule\qquad \frac{\partial L}{\partial W} = x^T\frac{\partial L}{\partial y}
b backward ruleLb=SUMROWLy b\ backward\ rule\qquad \frac{\partial L}{\partial b} = SUMROW \frac{\partial L}{\partial y}
x backward ruleLx=LyWT x\ backward\ rule\qquad \frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} W^T

激活函數

只有線性層的網絡, 其本質就是多個矩陣相乘, 化簡後得到的仍然是矩陣, 無法得到非線性. 爲了讓網絡有非線性的能力, 我們會在線性層之前穿插激活函數.也就是整個神經網絡的輸出寫做
o=(...acti((acti(xW1+b1))W2+b2)...)Wn+bn o = (...acti((acti(xW_1+b_1))W_2+b_2)...)W_n+b_n
激活函數接收m維向量, 在向量的每個元素上做非線性變換. 輸出m維向量. 邏輯迴歸時我們用到了sigmoid這種函數, 它可以作爲一種激活函數, 但它不是最好的函數. sigmoid雖然求導簡單但是存在一些問題, 因爲它的前向傳播和反向傳播計算導數的公式是
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))
可以看見, 部分導是恆小於1的. 如果用這種激活函數就會在較爲深層的網絡(比如ResNet會有十幾層的卷積層和線性層)中出現梯度的快速衰減, 以至於上層的線性層參數得不到有效更新, 進而無法有效訓練網絡. 在ResNet中我們會用一些技巧防止這個現象出現, 其中包括使用特殊的激活函數ReLU, 線性整流函數.
ReLU(v)=v if v>0 else 0 ReLU(v) = v\ if\ v>0\ else\ 0
ReLU(v)=1 if v>0 else 0 ReLU'(v) = 1\ if\ v>0\ else\ 0
在RNN中, 我們會用tanh這個函數代替sigmoid, 從而讓梯度不衰減得太厲害.
tanh(v)=exexex+ex tanh(v) = \frac{e^x-e^{-x}}{e^x+e^{-x}}
tanh(v)=1(tanh(v))2 tanh'(v) = 1-(tanh(v))^2

Softmax

之前我們學習線性模型時,一般會構造多個判別器來實現多分類,但是神經網絡有着更好的做法。我們知道神經網絡允許多輸出,那麼能不能讓神經網絡直接輸出每個類別的概率呢?答案是可以,爲此我們要引入一個特殊的軟化概率函數,以及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(z X,W) p(z\ |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層,然後就可以搭建能完成多分類任務的神經網絡了。

SGD with moentum

我們優化神經網絡的一種有效方法是使用帶動量的隨機梯度下降,每次參數更新不是完全用當前的梯度,而是基於原本的下降方向作出調整。這樣可以確定一個比較穩定的下降方向,本質上是共軛梯度法的簡化版本。共軛梯度法希望通過方向修正,讓所有的下降方向儘可能正交。而如果我們把修正係數設爲常數,就是SGD with moentum算法。
dβd+(1β)αLW d \leftarrow \beta d+(1-\beta)\alpha\frac{\partial L}{\partial W}
如果不嫌麻煩, 也可以實現一下Adam優化算法, 這是一種比momentum更高效的算法.

Mni-Batch

使用批量梯度下降算法是數據量較大時一種比較快速的計算和訓練方法, 我們上面神經網絡的主要計算過程都是矩陣W和向量x的乘法運算, 如果我們把多個列向量x排成矩陣, 運算就變成矩陣乘矩陣. 矩陣和矩陣的計算比起矩陣和向量進行多次計算更爲有效率. 在對擁有k個列向量的矩陣X前向傳播計算梯度時, 我們對每個單獨的向量x對應的dW和db求和, 就能一次性更新k步.

任務

  1. 實現反向傳播算法,
  2. 不使用Pytorch的計算圖模型反向傳播. 實現梯度下降算法,
  3. 不使用torch.nn中任何輔助計算的激活函數和損失函數. 用numpy或pytorch實現Linear, ReLU和Softmax網絡層, 以及CrossEntropyLoss損失函數. 使用這些原件搭建前饋神經網絡, 並在MNIST數據集上測試, 要求達到90per以上的準確率.
  4. 除了backward和torch.nn以外, 可以自由使用其他任何來自torch和torchvision的輔助工具, 比如dataloader和矩陣乘法.
import torch
import numpy as np
import matplotlib.pyplot as plt

class ReLU:
    def __init__(self, slope = 0.1):
        self.mask = None
        self.leak = slope
        
    def forward(self, x):
        self.mask = x<0
        x[self.mask] *= self.leak
        return x
    
    def backward(self, dz):
        dz[self.mask] *= self.leak
        return dz
    
    def __call__(self, x):
        return self.forward(x)
    
class Sigmoid:
    def __init__(self):
        self.out = None
        
    def forward(self, x):
        self.out = 1/(1+torch.exp(-x))
        return self.out
    
    def backward(self, dz):
        return dz*self.out*(1-self.out)
    
    def __call__(self, x):
        return self.forward(x)
    
    
class Linear:
    def __init__(self,input_sz,output_sz, LEARNING_RATE=0.01,
                 momentum = 0.9):
        '''
        使用kaiming初始化策略, W = normal(0,2/(input_sz+output_sz))
        '''
        self.W = torch.randn(input_sz,output_sz)*(2/(input_sz+output_sz))
        self.b  = torch.randn(output_sz)*(2/output_sz)
        
        self.dW = torch.zeros_like(self.W)
        self.db = torch.zeros_like(self.b)
        
        self.lr = LEARNING_RATE
        self.momentum = momentum
        
        self.X = None
        
    def forward(self,X):
        self.X = X
        out =  torch.mm(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 = torch.mm(self.X.T,dz)#/n
        self.dW += self.lr*dw*(1-self.momentum)
        
        #db = torch.mean(dz, axis = 0)
        db = torch.sum(dz, axis = 0)
        self.db += self.lr*db*(1-self.momentum)
        
        dx = torch.mm(dz,self.W.T)
        
        return dx
    
    def update(self):
        #更新W和b
        self.W = self.W-self.dW
        self.b = self.b-self.db
        
    def zero_delta(self):
        # 清零dW和db
        self.dW = torch.zeros_like(self.W)
        self.db = torch.zeros_like(self.b)
    
    def __call__(self, X):
        return self.forward(X)


def cross_entropy_error(y_pred,labels):
    n,m = y_pred.shape
    return -torch.mean(torch.log(y_pred[range(n),labels]))
    
def softmax(X):
    n,m = X.shape
    exp_x = torch.exp(X)
    sum_exp_x = torch.sum(exp_x,axis=1)
    return (exp_x.T/sum_exp_x).T

def one_hot_encode(labels):
    '''
    labels:一維數組,返回獨熱編碼後的二維數組
    '''
    n = len(labels)
    m = 10
    ret = torch.zeros((n,m))
    ret[range(n),labels] = 1.
    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 loss
        dx = (self.y_hat-one_hot_encode(labels))
        return dx
    
    def __call__(self, x):
        return self.forward(x)
    

    
class Sequential:
    def __init__(self, module_list):
        self.layers = module_list
        
    def forward(self, x):
        for layer in self.layers:
            x = layer(x)
        return x
    
    def backward(self, dz):
        for layer in self.layers[::-1]:
            dz = layer.backward(dz)
        return dz
    
    def update(self):
        for layer in self.layers:
            if type(layer)==Linear:
                layer.update()
                
    def zero_delta(self):
        for layer in self.layers:
            if type(layer)==Linear:
                layer.zero_delta()
    
    def __call__(self, x):
        return self.forward(x)

算法收斂性測試:學習異或

使用sigmoid激活函數和線性層搭建最簡單的神經網絡, 解決邏輯迴歸分類器解決不了的異或問題.

xor_mechine = Sequential([Linear(2,2,0.1),Sigmoid(),Linear(2,1,0.1)])

input_v = torch.tensor([[0.,0.],[0.,1.],[1.,0.],[1.,1.]])

output_v = torch.tensor([[1.],[0.],[0.],[1.]])

for i in range(2000):
    y = xor_mechine(input_v)
    dz = y-output_v
    if i%100==0:
        print(torch.mean(dz**2))
    xor_mechine.backward(dz)
    xor_mechine.update()
    
xor_mechine(input_v)

tensor(0.8792)
tensor(0.2519)
tensor(0.2508)
tensor(0.2503)
tensor(0.2499)
tensor(0.2496)
tensor(0.2493)
tensor(0.2489)
tensor(0.2484)
tensor(0.2474)
tensor(0.2459)
tensor(0.2430)
tensor(0.2371)
tensor(0.2242)
tensor(0.1954)
tensor(0.1382)
tensor(0.0639)
tensor(0.0171)
tensor(0.0030)
tensor(0.0004)
tensor([[0.9908],
        [0.0082],
        [0.0080],
        [0.9953]])

全連接網絡搭建與訓練

class NN:    
    def __init__(self, LEARNING_RATE=0.01, MOMENTUM = 0.9):
        lr = LEARNING_RATE
        mom = MOMENTUM
        # 輸入:28*28 = 784維特徵
        self.nn = Sequential(
            [
            Linear(28*28,256,lr,mom),
            ReLU(),
            Linear(256,128,lr,mom),
            ReLU(),
            Linear(128,128,lr,mom),
            ReLU(),
            Linear(128,10,lr,mom),
            SoftMax()
            ]
        )
        # 輸出:10維分類

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

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

    def update(self):
        self.nn.update()
        
    def zero_delta(self):
        self.nn.zero_delta()
        
    def __call__(self, x):
        return self.forward(x)

導入數據集

import torchvision
import torchvision.transforms as transforms

batch_size = 128

# MNIST dataset 
train_dataset = torchvision.datasets.MNIST(root='../mnist', 
                                           train=True, 
                                           transform=transforms.ToTensor())

test_dataset = torchvision.datasets.MNIST(root='../mnist', 
                                          train=False, 
                                          transform=transforms.ToTensor())

# Data loader
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, 
                                           batch_size=batch_size, 
                                           shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset, 
                                          batch_size=batch_size, 
                                          shuffle=False)

訓練

num_epochs = 5
total_step = len(train_loader)
model = NN(LEARNING_RATE = 0.01)
loss_list = []

for epoch in range(num_epochs):
    running_loss = 0.
    for i, (images, labels) in enumerate(train_loader):
        images = images.reshape(-1, 28*28)
        # Forward pass
        outputs = model(images)
        
        # Backward and optimize
        model.backward(labels)
        model.update()
        
        running_loss += model.loss
        
        if (i+1) % 100 == 0:
            print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                   .format(epoch+1, num_epochs, i+1, total_step, running_loss.item()/100))
            loss_list.append(running_loss)
            running_loss = 0.

plt.plot(loss_list)
plt.title('Train loss(Cross Entropy)')

在這裏插入圖片描述

correct = 0
total = 0
for images, labels in test_loader:
    images = images.reshape(-1, 28*28)
    outputs = model(images)
    predicted = torch.argmax(outputs.data, axis = 1)
    total += labels.size(0)
    correct += (predicted == labels).sum().item()

print('Accuracy of the network on the 10000 test images: {} %'.format(100 * correct / total))

Accuracy of the network on the 10000 test images: 97.41 %

小結

本實驗涉及深度學習的最基礎知識, 包括線性層的設計, 損失函數的設計, 激活函數的設計和它們的部分偏導計算. 基於這些模塊我們可以實現能處理較大型數據的分類器, 前饋神經網絡.
後續的學習中我會介紹一些前饋神經網絡的缺點, 以及用於彌補這些缺點的其他神經網絡, 並同時實現它們.

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