神經元(neuron)模型是神經網絡的基本組成部分,它參考了生物神經元的工做原理:經過多個樹突接收輸入,在神經元進行處理後,若是電平信號超過某個闕值(threshold),那麼該神經元就會被激活並經過一個軸突向其餘神經元發送信號。對上述流程進行數學抽象,即可以獲得以下的M-P神經元模型:python
神經元模型將接收到的總輸入和與神經元的闕值進行比較,而後經過激勵函數(activation function)處理以產生神經元的輸出。算法
常見的激勵函數一般有如下幾種:緩存
激勵函數 | 相對優點 | 相對劣勢 |
---|---|---|
階躍函數 | 當輸入大於闕值返回1,小於闕值返回0,符合理想狀態的神經元模型 | 曲線不光滑,不連續 |
Sigmoid | 曲線光滑;可以用於表示正例的機率 | 可能形成梯度消失;中心點爲0.5 |
Tanh | 曲線光滑;中心點爲0;收斂較logistic快 | 可能形成梯度消失 |
ReLU | 不會形成梯度消失;收斂更快 | 當訓練迭代必定次數後可能致使權重沒法繼續更新 |
羅森布拉特感知器(Perceptron)是最先最基礎的神經元模型,它所採用的激勵函數是單位階躍函數。因爲階躍函數曲線不連續光滑,且可導區域導數爲0,因此其有一套獨特的學習規則:網絡
如何理解這個學習規則?看下面的例子:
1)分類正確:數據結構
再也不進行更新。
2)分類錯誤:app
可見,在類標分類錯誤的狀況下,感知器會讓權值向正確的標記方向移動。dom
自適應線性神經元是普通的感知器的改進。Adaline以線性函數\(h(x)=x\)爲激勵函數,提出了代價函數的概念,而且使用了梯度降低法來最小化代價函數。其採用均方偏差來做爲代價函數:函數
那麼對參數\(w\)的求解則等價於求解:\(w=\arg\min_wJ(x_i,w)\)。使\(J\)對\(w\)求偏導,易得:性能
那麼則有:學習
考慮如下問題:如何讓計算機學得異或的計算能力?
經過繪製決策邊界不難發現,對於如下數據集:
沒法經過一個線性超平面畫出該數據集的決策邊界:
即,異或問題是一個線性不可分問題。
單個神經元模型只能經過劃分線性超平面來進行分類,那麼想要解決非線性可分問題,則能夠考慮使用性能更強大的多層神經網絡。
將多個神經元模型按照必定的次序進行組合即可以生成一個性能強大的神經網絡(neural network,NN)。神經網絡模型有不少種類,這裏介紹最多見的多層前饋神經網絡。
上圖是一個具備一個輸入層、一個隱藏層和一個輸出層的三層前饋型神經網絡。每一層分別有\(d,q,l\)個神經元,其中,只有隱藏層和輸出層的神經元是功能神經元(包含激勵函數)。假設神經網絡的輸入爲\(x=(x_1,...,x_i,...,x_d)\),輸入層神經元\(i\)到隱藏層神經元\(h\)的權重表示爲\(w^0_{ih}\),隱藏層神經元\(h\)到輸出層神經元\(j\)的權重表示爲\(w^1_{hj}\)。那麼即可以求得:
1)第\(h\)個隱藏層神經元的輸入和輸出爲:
2)第\(l\)個輸出層神經元的輸入和輸出爲:
以上即是多層前饋神經網絡模型的前向傳播(forward propagation)過程。而前向傳播須要的權值參數,則須要經過學習獲得。
神經網絡的學習過程比神經元模型複雜的多,可是也能夠經過偏差逆傳播算法(Error BackPropagation,BP)較爲輕鬆地實現。
下面先用通俗的概念闡述一下什麼是偏差逆傳播算法。偏差逆傳播算法整體看來能夠分爲三個步驟,即:前向傳播、反向傳播,以及權值更新。
1)前向傳播:從輸入層到輸出層逐層計算出每一個功能神經元的激勵函數輸出,並緩存;
2)反向傳播:從輸出層到輸入層逐層計算出每一個功能神經元的計算偏差,從而計算出梯度\(\nabla f(w)\),這一過程須要使用在前向傳播中緩存的激勵函數輸出值;
3)權值更新:按照\(w^*=w-\eta\nabla f(w)\)的更新規則更新權重。
下面用數學公式推導如何進行上述步驟。首先定義一些數學符號:使用下標\(i\)表示第\(i\)層,從0開始計數;\(v_i\)表示在前向傳播中緩存的第\(i\)層的值,其中\(v_0\)表示的是輸入層的輸入;\(w_i\)表示第\(i\)層和第\(i+1\)層之間的權值矩陣;激勵函數爲\(h(v_iw_i)\)。
1)前向傳播 :參考神經元模型的計算方法,後一層的值由前一層的值和權值計算獲得:
2)反向傳播:以均方偏差爲神經網絡的代價函數,對於樣本\(k\),假設輸出層爲第\(i+1\)層,則有:
求輸出層梯度:
求最後一層隱藏層梯度:
從上述的數學公式不難總結獲得通常推導公式,對於第\(i\)層神經元,能夠計算梯度:
這裏的\(\delta_i\)被定義爲當前層的偏差。從前面的數學推導能夠獲得:
(1)輸出層的偏差\(\delta_i=v_i-y\),即激活函數輸出值和真實標記的差;
(2)隱藏層的偏差\(\delta_i=g_{i+1}w_i\),即\(g_{i+1}\)與\(w_i\)的線性組合,係數爲權值\(w_i\)。
3)權值更新:對於矩陣\(w_i\),其更新規則以下:
這裏嘗試編寫一個高自由度可定製的多層BP神經網絡。既然是高自由度,那麼先考慮可定製的參數:
1)網絡規模:特徵數(輸入層神經元數)、隱藏層神經元數、類標數(輸出層神經元數),深度(權值矩陣個數,層數-1);
2)網絡學習速率:學習率、最大迭代次數;
3)激勵函數:因爲是分類器,那麼輸出層的激勵函數固定爲logistic較爲合適,而隱藏層的激勵函數則應當能夠變更。
綜上,能夠獲得如下參數:
def __init__(self, feature_n, hidden=None, label_n=2, eta=0.1, max_iter=100, activate_func="tanh"): # hidden表示隱藏層規模,即層數與每層神經元個數 pass
因爲要求模型可以完成多分類任務,因此須要有一個數據預處理器來對多分類數據集類標進行獨熱編碼。這裏可使用sklearn庫中的OneHotEncoder,而我是本身編寫了一個編碼器:
def _encoder(self, y): y_new = [] for yi in y: yi_new = np.zeros(self.label_n) yi_new[yi] = 1 y_new.append(yi_new) return y_new
在構造函數中初始化參數時,須要注意一下幾點:
首先是隱藏層規模,隱藏層規模默認爲None,在構造函數中,須要對hidden進行處理,防止使用者在未輸入hidden參數時模型接收到的隱藏層規模爲None。處理方法以下:
if not hidden: self.hidden = [10] else: self.hidden = hidden
而後是激活函數及其導數函數的引用。定義好激活函數及其導數函數後,將其引用存儲在一個字典中,經過超參"activate_func"獲取:
# 函數字典 funcs = { "sigmoid": (self._sigmoid, self._dsigmoid), "tanh": (self._tanh, self._dtanh), "relu": (self._relu, self._drelu) } # 獲取激活函數及其導數 self.activate_func, self.dacticate_func = funcs[activate_func]
下一步須要定義神經網絡前向傳播和反向傳播過程當中的重要數據結構:
# 擬合緩存 self.W = [] # 權重 self.g = list(range(self.deep)) # 梯度 self.v = [] # 神經元輸出值
最後初始化權重矩陣:
for d in range(self.deep): if d == 0: self.W.append(np.random.random([self.hidden[d], feature_n])) elif d == self.deep - 1: self.W.append(np.random.random([label_n, self.hidden[d - 1]])) else: self.W.append(np.random.random([self.hidden[d], self.hidden[d - 1]]))
先實現BP算法的第一部分:前向傳播。
def _forward_propagation(self, x): # 前向傳播 self.v.clear() value = None for d in range(self.deep): if d == 0: value = self.activate_func(self._linear_input(x, d)) elif d == self.deep - 1: value = self._sigmoid(self._linear_input(self.v[d - 1], d)) else: value = self.activate_func(self._linear_input(self.v[d - 1], d)) self.v.append(value) return value
前向傳播實現後須要實現反向傳播,徹底按照數學推導的公式編寫便可:
def _back_propagation(self, y): # 反向傳播 for d in range(self.deep - 1, -1, -1): if d == self.deep - 1: self.g[d] = (y - self.v[d]) * self._dsigmoid(self.v[d]) else: self.g[d] = self.g[d + 1] @ self.W[d + 1] * self.dacticate_func(self.v[d])
最後即可以實現完整的BP算法和訓練算法:
def _bp(self, X, y): for i in range(self.max_iter): for x, yi in zip(X, y): self._forward_propagation(x) # 前向傳播 self._back_propagation(yi) # 反向傳播 # 更新權重 for d in range(self.deep): if d == 0: self.W[d] += self.g[d].reshape(-1, 1) @ x.reshape(1, -1) * self.eta else: self.W[d] += self.g[d].reshape(-1, 1) @ self.v[d - 1].reshape(1, -1) * self.eta def fit(self, X, y): y = self._encoder(y) self._bp(X, y) return self
這一過程實現很簡單,代碼以下:
def _predict(self, x): y_c = self._forward_propagation(x) return np.argmax(y_c) def predict(self, X): y = [] for x in X: y.append(self._predict(x)) return np.array(y)
下面用上面編寫的神經網絡模型求解異或問題:
from model.model_demo import simple_data from model.model_demo import demo xor = simple_data("xor") demo(MLPClassifier(2, label_n=2, activate_func="tanh", max_iter=500), xor, split=False, scaler=False)
如下是結果:
能夠看見,分類效果仍是很不錯的。
這裏導入鳶尾花數據集來測試模型進行多分類任務的性能:
# 求解多分類問題 from model.model_demo import iris_data from model.model_demo import demo iris = iris_data(3) demo(MLPClassifier(4, hidden=[10, 10], label_n=3, activate_func="relu", max_iter=2000), iris)
結果以下:
效果還行。