深度學習:多層感知機和異或問題(Pytorch實現)

orion發表於2022-02-15

感知機模型

假設輸入空間\(\mathcal{X}\subseteq \textbf{R}^n\),輸出空間是\(\mathcal{Y}=\{-1,+1\}\).輸入\(\textbf{x}\in \mathcal{X}\)表示例項的特徵向量,對應於輸入空間的點;輸出\(y\in \mathcal{Y}\)表示例項的類別。有輸入空間到輸出空間的如下函式:

\[\begin{aligned} f(x)= g(\textbf{w}\cdot \textbf{x}+b) \end{aligned} \tag{1} \]

稱為感知機,其中\(\textbf{w}\)\(b\)為感知機模型引數,\(\textbf{w}\in \textbf{R}^n\)叫做權值(weight)或權值向量(weight vector),
\(b \in \textbf{R}\)叫做偏置(bias),\(\textbf{w}\cdot \textbf{x}\)叫做\(\textbf{w}\)\(\textbf{x}\)的內積。\(g\)是表示啟用函式。理想中的中的啟用函式是“階躍函式”,即

\[\begin{aligned} \mathrm{sgn}(x)=\left\{ \begin{aligned} +1, \quad x \geqslant 0\\ 0, \quad x<0 \end{aligned} \right . \end{aligned} \tag{2} \]

它將輸入值對映為輸出值0或1,顯示“1”對應於神經元興奮,“0”對應於神經元抑制。然而,階躍函式具有不連續、不光滑等不太好的性質(
函式不連續時無法求梯度。在Pytorch/Tensorflow中體現為梯度將一直保持初值\(0\),你可以試一試)
,因而實際常用下面這個\(\text{Sigmoid}\)函式做為啟用函式,它把可能在較大範圍內變化的輸入值擠壓到\((0,1)\)輸出值範圍內,因而有時也被稱為“擠壓函式”(squashing function)。

\[\begin{aligned} \mathrm{sigmoid}(x)=\frac{1}{1+e^{-x}} \end{aligned} \]

感知機是一種線性分類模型,屬於判別模型。感知機模型的假設空間是定義在特徵空間的所有線性分類模型(linear classification model)
或線性分類器(linear classifier),即函式集合\(\{ f|f(\textbf{x})=\textbf{w}\cdot \textbf{x}+b\}\)

異或問題

我們將感知機用於學習一個簡單的\(\text{XOR}\)函式。我們將其建模為二分類問題,這裡我們採用上一章講過的對數損失函式,並使用梯度下降法進行訓練。

import numpy as np
import random
import torch
# batch_size表示單批次用於引數估計的樣本個數
# y_pred大小為(batch_size, )
# y大小為(batch_size, ),為類別型變數
def log_loss(y_pred, y):
    return -(torch.mul(y, torch.log(y_pred)) + torch.mul(1-y, torch.log(1-y_pred))).sum()/y_pred.shape[0]

# 前向函式
def perceptron_f(X, w, b): 
    z = torch.add(torch.matmul(X, w), b)
    return 1/(1+torch.exp(-z))

# 之前實現的梯度下降法,做了一些小修改
def gradient_descent(X, w, b, y, n_iter, eta, loss_func, f):
    for i in range(1, n_iter+1):
        y_pred = f(X, w, b)
        loss_v = loss_func(y_pred, y)
        loss_v.backward() 
        with torch.no_grad(): 
            w -= eta*w.grad
            b -= eta*b.grad
        w.grad.zero_()
        b.grad.zero_()
    w_star = w.detach()
    b_star = b.detach()
    return w_star, b_star

# 本模型按照二分類架構設計
def Perceptron(X, y, n_iter=200, hidden_size=2, eta=0.001, loss_func=log_loss, optimizer=gradient_descent):
    # 初始化模型引數
    # 注意,各權重初始化不能相同
    w = torch.tensor(np.random.random((hidden_size, )), requires_grad=True)
    b = torch.tensor(np.random.random((1)), requires_grad=True)
    X, y = torch.tensor(X), torch.tensor(y)
    # 呼叫梯度下降法對函式進行優化
    # 這裡採用單次迭代對所有樣本進行估計,後面我們會介紹小批量法
    w_star, b_star = optimizer(X, w, b, y, n_iter, eta, log_loss, perceptron_f)
    return w_star, b_star

if __name__ == '__main__':
    X = np.array([
        [0, 0],
        [0, 1],
        [1, 0],
        [1, 1]
    ], dtype=np.float64)
    # 標籤向量
    y = np.array([0, 1, 1, 0], dtype=np.int64)
    # 迭代次數
    n_iter = 2000
    # 學習率
    eta = 2 #因為每輪所求的梯度太小,這裡增大學習率以補償
    w, b = Perceptron(X, y, n_iter, hidden_size, eta, log_loss, gradient_descent)
    # 代入
    print(perceptron_f(torch.tensor(X), w, b))            

我們資料代入所學的的模型,我們發現模型的輸出結果如下。我們可以發現\(4\)個樣本的預測值都是\(0.5\),不能擬合原本給定的\(4\)個樣本點。如果你有興趣可以將\(\bm{w}\)的梯度進行列印,可以發現\(\bm{w}\)的梯度來回震盪,這也就導致了權重\(\bm{w}\)無法穩定,最終導致模型最終無法收斂。

    tensor([0.5000, 0.5000, 0.5000, 0.5000], dtype=torch.float64)

看來,普通的感知機無法解決亦或問題。那這是為什麼呢?我們前面說了,感知機的模型是一個線性分類模型,只能處理線性可分問題(你可以試試讓其學習與、或、非等線性可分問題)。可以證明,若兩類模式是線性可分的,即存在一個線性超平面能將他們分開,如下圖中的\((a)-(c)\)所示,則感知機的學習過程一定會收斂(converge)而求得適當的權向量\(\bm{w}\);否則感知機學習過程將會發生振盪(fluctuation),\(\bm{w}\)難以穩定下來,不能求得合適解。亦或問題就是一種非線性可分問題。如圖\((d)\)所示,我們無法用線性超平面去將正負樣本分隔開。

電影愛好者的評分情況示意圖

人工智慧奠基人之一的Marvin Minsky與1969年出版了《感知機》一書,書中指出,單層神經網路無法解決非線性問題,而多層神經網路的訓練演算法尚看不到希望,這個論斷直接使神經網路研究進入了“冰河期”,這就是神經網路的第一次低谷。直到後來BP演算法的走紅,才掀起了神經網路的第二次高潮。

多層感知機

單層的感知機無法解決亦或問題,那多層的呢?我們接下來考慮一個多層神經網路,但相比前面的感知機多了一個隱藏層\(\bm{h}\)。設該網路第一層的權重矩陣為\(\textbf{W}\)(表示\(\bm{x}\)\(\bm{h}\)的對映),第二層的權重向量為\(\textbf{w}\)(表示\(\textbf{h}\)到中間變數\(z\))的對映,然後通過\(\text{Sigmoid}\)函式將\(z\)其對映到\(y\)這樣神經網路包括兩個巢狀在一起的函式\(\textbf{h} = f^{(1)}(\textbf{x};\textbf{W})\)\(y=f^{(2)}(\textbf{h}; \textbf{w})\)。注意:此處為了簡化起見,兩層的偏置已經合併到權重\(W\)\(w\)
中去了。這樣整個神經網路可以表示成一個複合函式\(f(\textbf{x};\textbf{W}, \textbf{w})=f^{(2)}f^{(1)}(x)\)

\(f^{(1)}\)應該採用那種函式?如果我們仍然採用線性函式,那麼前饋網路作為一個整體仍然是線性分類器。故我們要用非線性函式,而這可以通過仿射變換後加一個非線性變換實現
(不知道仿射變換的可以回顧線性代數),而這個非線性變換可以用我們在\(f^{(2)}\)中所包括的啟用函式實現。

還有一個工程上需要注意的是,如果我們有多層網路,那麼我們不能將所有網路層權重都初始化為相同的值,這樣會造成所有網路層權重梯度變化方向一樣,最終像單層感知機一樣無法學習。我們可以將所有網路層權重初始化為\([0, 1)\)之間的隨機數(注意,神經網路的輸入及權重一般初始時都是歸一化到\([0, 1)\)之間了的)。後面我們會介紹更科學的\(\text{golort}\)權重初始化法。對於偏置,初始化為隨機數或是常量(如\(0\)\(1\))不影響,我們這裡仍然採取將其初始化為\([0, 1)\)之間的隨機數。

我們在原本的網路中多加一層。

import numpy as np
import random
import torch
# batch_size表示單批次用於引數估計的樣本個數
# y_pred大小為(batch_size, )
# y大小為(batch_size, ),為類別型變數
def log_loss(y_pred, y):
    return -(torch.mul(y, torch.log(y_pred)) + torch.mul(1-y, torch.log(1-y_pred))).sum()/y_pred.shape[0]

# 前向函式
def perceptron_f(X, W, w, b1, b2): 
    z1 = torch.add(torch.matmul(X, W), b1) 
    h = 1/(1+torch.exp(-z1)) 
    z2 = torch.add(torch.matmul(h, w), b2)
    return 1/(1+torch.exp(-z2))

# 之前實現的梯度下降法,做了一些小修改
def gradient_descent(X, W, b1, w, b2, y, n_iter, eta, loss_func, f):
    for i in range(1, n_iter+1):
        y_pred = f(X, W, w, b1, b2)
        loss_v = loss_func(y_pred, y)
        loss_v.backward() 
        with torch.no_grad(): 
            W -= eta*W.grad
            w -= eta*w.grad
            b1 -= eta*b1.grad
            b2 -= eta*b2.grad
        W.grad.zero_()  
        w.grad.zero_()
        b1.grad.zero_()
        b2.grad.zero_()
    W_star = W.detach()
    w_star = w.detach()
    b1_star = b1.detach()
    b2_star = b2.detach()
    return W_star, w_star, b1_star, b2_star

# 本模型按照二分類架構設計
def Perceptron(X, y, n_iter=200, hidden_size=2, eta=0.001, loss_func=log_loss, optimizer=gradient_descent):
    # 初始化模型引數
    # 注意,各權重初始化不能相同
    W = torch.tensor(np.random.random((X.shape[1], hidden_size)), requires_grad=True)
    b1 = torch.tensor(np.random.random((1)), requires_grad=True)
    w = torch.tensor(np.random.random((hidden_size, )), requires_grad=True)
    b2 = torch.tensor(np.random.random((1)), requires_grad=True)
    X, y = torch.tensor(X), torch.tensor(y)
    # 呼叫梯度下降法對函式進行優化
    # 這裡採用單次迭代對所有樣本進行估計,後面我們會介紹小批量法
    W_star, w_star, b1_star, b2_star = optimizer(X, W, b1, w, b2, y, n_iter, eta, log_loss, perceptron_f)
    return W_star, w_star, b1_star, b2_star

if __name__ == '__main__':
    X = np.array([
        [0, 0],
        [0, 1],
        [1, 0],
        [1, 1]
    ], dtype=np.float64)
    # 標籤向量
    y = np.array([0, 1, 1, 0], dtype=np.int64)
    # 迭代次數
    n_iter = 2000
    # 學習率
    eta = 2 #因為每輪所求的梯度太小,這裡增大學習率以補償
    # 隱藏層神經元個數
    hidden_size = 2
    W, w, b1, b2 = Perceptron(X, y, n_iter, hidden_size, eta, log_loss, gradient_descent)
    # 代入
    print(perceptron_f(torch.tensor(X), W, w, b1, b2))

你可以將原始樣本點帶入學得的模型,可以發現擬合結果如下所示,總體效果不錯(因為數值精度問題,一般不會完全擬合)

    tensor([0.0036, 0.9973, 0.9973, 0.0030], dtype=torch.float64)

更一般地,常見的神經網路是多層的層級結構,每層神經元與下一層神經元全互聯,神經元之間不存在同層連線,也不存在跨層連線,這樣的神經網路結構通常稱為“多層前饋神經網路”(multi-layer feedforward neural)或多層感知機(multi-layer perceptron,MLP)。

相關文章