前言
本篇主要介紹神經網絡的基本結構、激活函數以及學習算法(BP算法)
神經網絡
主要由三個組成部分,第一個是架構(architecture)或稱爲拓撲結構(topology)
,描述神經元的層次與連接神經元的結構。第二個組成部分是神經網絡使用的激勵/激活函數
。第三個組成部分是找出最優權重值的學習算法
。
爲了能夠解決感知機人工設定權重的工作,即確定合適的、能符合預期的輸入與輸出的權重,神經網絡便出現了,神經網絡的一個重要的性質是它可以自動地從數據中學習得到合適的權重參數。
一、神經網絡的結構
- 輸入層 :通過輸入層輸入數據
- 隱藏層 :通過隱藏的中間層對輸入數據進行訓練,訓練過程中中間節點的真正數值無法通過訓練集看到
- 輸出層 :輸出模型的預測值
一般情況下,我們通常將輸入層、隱藏層、輸出層的總數減去1後的數量來表示神經網絡的名稱,上圖即可稱爲兩層神經網絡
- 上圖是一個三層神經網絡
二、激活函數
上面的激活函數 是以閥值爲界,一旦輸入超過閥值,就切換輸出。這樣的激活函數爲“階躍函數”,因此,可以說感知機中使用了階躍函數作爲激活函數,那麼感知機使用其他激活函數呢?實際上,如果將激活函數從階躍函數換成其他的,就可以進入神經網絡的世界了。
激活函數
的主要作用是提供網絡的非線性建模能力。如果沒有激活函數,那麼該網絡僅能夠表達線性映射,此時即便有再多的隱藏層,其整個網絡跟單層神經網絡也是等價的。因此也可以認爲,只有加入了激活函數之後,深度神經網絡才具備了分層的非線性映射學習能力。 激活函數的主要特性是:可微性、單調性、輸出值的範圍;
- 常見的激活函數:
Sign函數
、Sigmoid函數
、Tanh函數
、ReLU函數
、P-ReLU函數
、Leaky-ReLU函數
、ELU函數
、Maxout函數
等
1、sigmoid
函數
數學表達式爲:
python
實現:
import numpy as np
import matplotlib.pylab as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
# 畫圖
plt.plot(x,y)
plt.ylim(-0.1, 1.1) # 指定y軸的範圍
plt.show()
2、階躍函數
數學表達式:
python
實現:
# 方法一
def step_function1(x):
if x > 0:
return 1
else:
return 0
# 方法二
def step_function2(x):
return np.array(x>0, dtype=np.int)
x = np.arange(-5.0, 5.0, 0.1) # 在-0.5到0.5的範圍內,以0.1爲單位,生成NumPy數組([-5.0,-4.9,...,4.9])
y = step_function2(x)
plt.plot(x,y)
plt.ylim(-0.1, 1.1) # 指定y軸的範圍
plt.show()
3、階躍函數與sigmoid
函數的比較
- “平滑性”不同,
sigmoid
函數是一條平滑的曲線,而階躍函數在0處有突變,輸出發生急劇性變化,sigmoid
函數的平滑性對神經網絡的學習具有重要意義; - 返回值的不同,階躍函數只能返回0和1,
sigmoid
函數可以返回0到1的任意數,這和平滑性有關,也就是說,感知機中神經元之間流動的是0和1的二元信號,而神經網絡中流動的是連續的實值信號; - 二者也有共同性質,輸入較小時,輸出接近0(或爲0),隨着輸入增大,輸出向1靠近(或變成1),也就是說,當輸入信號爲重要信息時,兩者都會輸出較大的值,當輸入信號不重要的時,兩者都會輸出較小的值;
- 二者輸出值都在0到1之前
4、ReLU
函數
數學表達式:
python
實現:
def relu(x):
return np.maximum(0,x)
x = np.arange(-5.0, 5.0, 0.1)
y = relu(x)
plt.plot(x,y)
plt.ylim(-0.1, 5) # 指定y軸的範圍
plt.show()
5、softmax
函數
數學表達式爲:
其中,表示假設輸出層共有n個神經元,計算第k個神經元的輸出
分子是輸入信號的指數函數,分母是所有輸入信號的指數函數之和
python
實現:
a = np.array([0.3, 2.9, 4.0])
exp_a = np.exp(a)
print(exp_a)
輸出爲:[ 1.34985881 18.17414537 54.59815003]
sum_exp_a = np.sum(exp_a)
print(sum_exp_a)
y = exp_a / sum_exp_a
print(y)
輸出爲:74.1221542101633
[0.01821127 0.24519181 0.73659691]
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
6、神經網絡爲何要使用非線性函數
如果使用線性函數,那麼不管如何加深層數,總是存在與子等效的“無隱藏層神經網絡”
- 假如我們使用 作爲激活函數,使用三層神經網絡對應 運算,即 ,我們取 同樣可以經過一次乘法來處理(即沒有隱藏層的神經網絡),因此激活函數必須使用非線性函數。
三、三層神經網絡的簡單實現
圖中,紅色表示輸入層到第一層的信號傳遞,藍色表示第一層到第二層的信號傳遞,綠色表示第二層到輸出層的信號傳遞
- 其中表示輸入,灰色實心圓表示偏置項,
- 中a表示後一層的第a個神經元,b表示前一層的第b個神經元,c表示第c層的權重
- 中c表示第c層的第d個輸出,用於下一層的輸入
- 表示最終輸出
1、從輸入層到第一層的信號傳遞
由圖,可得到:
也按此規律運算,寫成矩陣的乘法運算可以表示爲:
其中,
下面我們通過python
的 NumPy
多維數組來實現以上步驟,這裏將輸入信號、權重、偏置設置成任意值。
X = np.array([1.0, 0.5])
W1 = np.array([
[0.1, 0.3, 0.5],
[0.2, 0.4, 0.6]
])
B1 = np.array([0.1, 0.2, 0.3])
A1 = np.dot(X, W1) + B1
Z1 = sigmoid(A1) # 使用sigmoid函數轉換
print(Z1)
輸出爲:[0.57444252 0.66818777 0.75026011]
2、從第一層到第二層的信號傳遞
W2 = np.array([
[0.1, 0.4],
[0.2, 0.5],
[0.3, 0.6]
])
B2 = np.array([0.1, 0.2])
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2) # 使用sigmoid函數轉換
print(Z2)
輸出爲:[0.62624937 0.7710107 ]
3、從第二層到輸出層的信號傳遞
W3 = np.array([
[0.1, 0.3],
[0.2, 0.4]
])
B3 = np.array([0.1, 0.2])
A3 = np.dot(Z2, W3) + B3
# 第二層到輸出層的信號傳遞使用的激活函數不是sigmoid函數,而是恆等函數
def identity_function(x):
return x
Y = identity_function(A3)
print(Y)
輸出爲:[0.31682708 0.69627909]
4、代碼整合
# 恆等函數
def identity_function(x):
return x
# 進行權重和偏置的初始化,並保存到字典network中
def init_network():
network = {}
network['W1'] = np.array([
[0.1, 0.3, 0.5],
[0.2, 0.4, 0.6]
])
network['b1'] = np.array([0.1, 0.2, 0.3])
network['W2'] = np.array([
[0.1, 0.4],
[0.2, 0.5],
[0.3, 0.6]
])
network['b2'] = np.array([0.1, 0.2])
network['W3'] = np.array([
[0.1, 0.3],
[0.2, 0.4]
])
network['b3'] = np.array([0.1, 0.2])
return network
# 前向(從輸入到輸出的傳遞處理)
def forward(network, x):
W1, W2, W3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = identity_function(a3)
return y
# 測試
network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y)
輸出爲:[0.31682708 0.69627909]
5、輸出層的設計
神經網絡可以用在分類問題
和迴歸問題
上,不過需要改變輸出層的激活函數,一般而言,迴歸問題用恆等函數
,分類問題用softmax函數
。
softmax
函數,它的分子和分母都涉及指數運算,因爲指數運算的值很容易變得非常大,這在計算機的運算上有一定的缺陷,即溢出問題。
因此可對 softmax函數
進行改進,表達式如下:
這裏的 可以使用任何值,但是一般選擇輸入信號的最大值
分子、分母都乘上這個任意常數,值不變
不改進softmax函數
a = np.array([1010,1000,990])
print(np.exp(a) / np.sum(np.exp(a)))
輸出爲:array([nan, nan, nan])
很明顯發生了溢出問題
通過減去最大值
c = np.max(a)
print(a - c)
print(np.exp(a-c) / np.sum(np.exp(a-c)))
輸出爲:array([ 0, -10, -20])
array([9.99954600e-01, 4.53978686e-05, 2.06106005e-09])
def softmax(a):
c = np.max(a)
exp_a = np.exp(a - c) # 溢出對策
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
a = np.array([0.3,2.9,4.0])
y = softmax(a)
print(y) # 輸出是0.0到1.0之間的實數
print(np.sum(y)) # 和爲1
輸出爲:[0.01821127 0.24519181 0.73659691]
1.0
四、BP
算法
BP算法
便是神經網絡的一種求解W的算法,分爲信號“正向傳播(FP)”求損失,“反向傳播(BP)”回傳誤差;根據誤差值修改每層的權重,繼續迭代。上面我們將輸入信號、權重、偏置設置成任意值,然後經過了FP過程,而神經網絡的一個重要的性質是它可以自動地從數據中學習得到合適的權重參數,這便是“反向傳播(BP)”的作用。
下面我們通過一個具體的例子來說明BP
算法的 FP
和 BP
過程:
w
和 b
的初始值:
- 輸出爲:
- 假設隱層和輸出層都使用
sigmoid
激活函數 - 學習率
1、FP
過程
同理可以得到:
同理可以得到:
輸出層誤差表示如下:
2、BP
過程
輸出層到第二層隱層,以求 爲例:
下面我們分別求上式的三個部分,其中第一部分:
又因爲:
第二分部:
因爲:
第三部分:
最終得到:
更新的值:
同理可以求出:
第二層隱層到第一層隱層,以求 爲例:
下面我們分別計算,第一部分()內的:
其中:
注意:這裏由於是反向傳播,此時要用到之前更新後的的值
同理:
第二部分:
第三部分:
最終:
於是:
同理求出:
以上是第一次迭代,經過多次迭代,最終的誤差會越來越小
上圖可以看出,當迭代1000次時,輸出爲 和原本的 以及比較接近了。
五、上例 Python
代碼實現
import numpy as np
# 初始值
w = [0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65]
# 偏置項b不進行更新
b = [0.35, 0.65]
l = [5, 10]
# sigmoid函數
def sigmoid(z):
return 1.0 / (1 + np.exp(-z))
def f1(w, b, l):
# 前向傳播,計算結果值
h1 = sigmoid(w[0] * l[0] + w[1] * l[1] + b[0])
h2 = sigmoid(w[2] * l[0] + w[3] * l[1] + b[0])
h3 = sigmoid(w[4] * l[0] + w[5] * l[1] + b[0])
o1 = sigmoid(w[6] * h1 + w[8] * h2 + w[10] * h3 + b[1])
o2 = sigmoid(w[7] * h1 + w[9] * h2 + w[11] * h3 + b[1])
# 後向傳播,更新w
# 輸出層到第二層隱層,前兩項
# 公式中的第一部分-(0.01 - o1),第二部分o1 * (l - o1)
t1 = -(0.01 - o1) * o1 * (l - o1)
# 第二層隱層到第一層隱層,前兩項
t2 = -(0.99 - o2) * o2 * (l - o2)
# t1*第三部分,即輸出層到第二層隱層的參數梯度
w[6] = w[6] - 0.5 * (t1 * h1)
w[8] = w[8] - 0.5 * (t1 * h2)
w[10] = w[10] - 0.5 * (t1 * h3)
w[7] = w[7] - 0.5 * (t2 * h1)
w[9] = w[9] - 0.5 * (t2 * h2)
w[11] = w[11] - 0.5 * (t2 * h3)
# (t1 * w[6] + t2 * w[7])對於公式()中的兩項,h1 * (1 - h1)對於第二項,l[0]對應第三項
w[0] = w[0] - 0.5 * (t1 * w[6] + t2 * w[7]) * h1 * (1 - h1) * l[0]
w[1] = w[1] - 0.5 * (t1 * w[6] + t2 * w[7]) * h1 * (1 - h1) * l[1]
w[2] = w[2] - 0.5 * (t1 * w[8] + t2 * w[9]) * h2 * (1 - h2) * l[0]
w[3] = w[3] - 0.5 * (t1 * w[6] + t2 * w[9]) * h2 * (1 - h2) * l[1]
w[4] = w[4] - 0.5 * (t1 * w[10] + t2 * w[11]) * h3 * (1 - h3) * l[0]
w[5] = w[5] - 0.5 * (t1 * w[10] + t2 * w[11]) * h3 * (1 - h3) * l[1]
return o1, o2, w
for i in range(1000):
r1, r2, w = f1(w, b, l)
print("第{}次迭代後,結果值爲:({},{}),權重更新爲:{}".format(i+1, r1, r2, w))
運行結果:
第0次迭代,結果值爲:(0.8910896614765176,0.9043299248500164),權重更新爲:[0.09453429502265628
, 0.13906859004531255, 0.1982111758493806, 0.2472699352051287, 0.29949648483800345, 0.34899296967600685, 0.3609680622498306
, 0.4533833089635062, 0.4581364640581681, 0.5536287533891512, 0.5574476639638248, 0.653688458944847]
…
第1000次迭代,結果值爲:(0.022971398121212325,0.9776750383779403),權重更新爲:[0.21489448646234574, 0.3797889729246913, 0.26021215340481807, 0.3781845403360407, 0.3231871485490716, 0.3963742970981442, -1.4890558608085964, 0.9416395064631622, -1.5030558044874467, 1.0491054496597343, -1.4269056227390988, 1.151801999870186]
結果中標記的爲第一次迭代 和 更新後的值,可見和我們上面計算的結果是一致的
六、使用BP神經網絡對公路貨運量預測
使用數據 traffic_data.csv
:
人口數,機動車數,公路面積,客運量,貨運量
20.55,0.6,0.09,5126.0,1237.0
22.44,0.75,0.11,6217.0,1379.0
25.37,0.85,0.11,7730.0,1385.0
27.13,0.9,0.14,9145.0,1399.0
29.45,1.05,0.2,10460.0,1663.0
30.1,1.35,0.23,11387.0,1714.0
30.96,1.45,0.23,12353.0,1834.0
34.06,1.6,0.32,15750.0,4322.0
36.42,1.7,0.32,18304.0,8132.0
38.09,1.85,0.34,19836.0,8936.0
39.13,2.15,0.36,21024.0,11099.0
39.99,2.2,0.36,19490.0,11203.0
41.93,2.25,0.38,20433.0,10524.0
44.59,2.35,0.49,22598.0,11115.0
47.3,2.5,0.56,25107.0,13320.0
52.89,2.6,0.59,33442.0,16762.0
55.73,2.7,0.59,36836.0,18673.0
56.76,2.85,0.67,40548.0,20724.0
59.17,2.95,0.69,42927.0,20803.0
60.63,3.1,0.79,43462.0,21804.0
代碼:
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
# 設置字符集,防止中文亂碼
mpl.rcParams['font.sans-serif'] = [u'simHei']
mpl.rcParams['axes.unicode_minus'] = False
# 1. 讀取數據
df = pd.read_csv('traffic_data.csv', encoding='utf-8')
# print(df.head())
# 2. 讀取特徵屬性X
x = df[['人口數', '機動車數', '公路面積']]
y = df[['客運量', '貨運量']]
# 3. 因爲x和y的數據取值範圍太大了,所以做一個歸一化操作(使用區間縮放法)
x_scaler = MinMaxScaler(feature_range=(-1, 1))
y_scaler = MinMaxScaler(feature_range=(-1, 1))
x = x_scaler.fit_transform(x)
y = y_scaler.fit_transform(y)
# 爲了後面和w進行矩陣的乘法操作
sample_in = x.T
sample_out = y.T
# 超參數
max_epochs = 60000
learn_rate = 0.035
mse_final = 6.5e-4
sample_number = x.shape[0]
input_number = 3
out_number = 2
hidden_unit_number = 8
# 網絡參數
# 8*3的矩陣
w1 = 0.5 * np.random.rand(hidden_unit_number, input_number) - 0.1
# 8*1的矩陣
b1 = 0.5 * np.random.rand(hidden_unit_number, 1) - 0.1
# 2*8的矩陣
w2 = 0.5 * np.random.rand(out_number, hidden_unit_number) - 0.1
# 2*1的矩陣
b2 = 0.5 * np.random.rand(out_number, 1) - 0.1
def sigmoid(z):
return 1.0 / (1 + np.exp(-z))
mse_history = []
# BP的計算
for i in range(max_epochs):
# FP過程
# 隱藏層的輸出(8*20)
hidden_out = sigmoid(np.dot(w1, sample_in) + b1)
# 輸出層的輸出(爲了簡化我們的寫法,輸出層不進行sigmoid激活)(2*20)
netword_out = np.dot(w2, hidden_out) + b2
# 錯誤
# 2*20
err = sample_out - netword_out
mse = np.average(np.square(err))
mse_history.append(mse)
if mse < mse_final:
break
# BP過程
# delta2: 2*20
delta2 = -err
delta1 = np.dot(w2.transpose(), delta2) * hidden_out * (1 - hidden_out)
dw2 = np.dot(delta2, hidden_out.transpose())
db2 = np.dot(delta2, np.ones((sample_number, 1)))
dw1 = np.dot(delta1, sample_in.transpose())
db1 = np.dot(delta1, np.ones((sample_number, 1)))
# w2: 2*8的矩陣, 那也就是要求dw2必須是2*8的一個矩陣
w2 -= learn_rate * dw2
b2 -= learn_rate * db2
w1 -= learn_rate * dw1
b1 -= learn_rate * db1
# 誤差曲線圖
mse_history10 = np.log10(mse_history)
min_mse = min(mse_history10)
plt.plot(mse_history10)
plt.plot([0, len(mse_history10)], [min_mse, min_mse])
ax = plt.gca()
ax.set_yticks([-2, -1, 0, 1, 2, min_mse])
ax.set_xlabel('iteration')
ax.set_ylabel('MSE')
ax.set_title('Log10 MSE History')
plt.show()
# 仿真輸出和實際輸出對比圖
# 隱藏層輸出
hidden_out = sigmoid((np.dot(w1, sample_in) + b1))
# 輸出層輸出
network_out = np.dot(w2, hidden_out) + b2
# 反轉獲取實際值
network_out = y_scaler.inverse_transform(network_out.T)
sample_out = y_scaler.inverse_transform(y)
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(12, 10))
line1, = axes[0].plot(network_out[:, 0], 'k', marker='o')
line2, = axes[0].plot(sample_out[:, 0], 'r', markeredgecolor='b', marker='*', markersize=9)
axes[0].legend((line1, line2), ('預測值', '實際值'), loc='upper left')
axes[0].set_title('客流模擬')
line3, = axes[1].plot(network_out[:, 1], 'k', marker='o')
line4, = axes[1].plot(sample_out[:, 1], 'r', markeredgecolor='b', marker='*', markersize=9)
axes[1].legend((line3, line4), ('預測值', '實際值'), loc='upper left')
axes[1].set_title('貨流模擬')
plt.show()
運行結果:
因爲程序中
mse_history10 = np.log10(mse_history)
對MSE取了log10對數,所以會有負數的