感知機模型
假設輸入空間\(\mathcal{X}\subseteq \textbf{R}^n\),輸出空間是\(\mathcal{Y}=\{-1,+1\}\).輸入\(\textbf{x}\in \mathcal{X}\)表示例項的特徵向量,對應於輸入空間的點;輸出\(y\in \mathcal{Y}\)表示例項的類別。有輸入空間到輸出空間的如下函式:
稱為感知機,其中\(\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\)是表示啟用函式。理想中的中的啟用函式是“階躍函式”,即
它將輸入值對映為輸出值0或1,顯示“1”對應於神經元興奮,“0”對應於神經元抑制。然而,階躍函式具有不連續、不光滑等不太好的性質(
函式不連續時無法求梯度。在Pytorch/Tensorflow中體現為梯度將一直保持初值\(0\),你可以試一試)
,因而實際常用下面這個\(\text{Sigmoid}\)函式做為啟用函式,它把可能在較大範圍內變化的輸入值擠壓到\((0,1)\)輸出值範圍內,因而有時也被稱為“擠壓函式”(squashing function)。
感知機是一種線性分類模型,屬於判別模型。感知機模型的假設空間是定義在特徵空間的所有線性分類模型(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)。