最近用python寫了一個實現手寫數字識別的BP神經網絡,BP的推導到處都是,但是一動手才知道,會理論推導跟實現它是兩回事。關於BP神經網絡的實現網上有一些代碼,可惜或多或少都有各種問題,在下手寫了一份,連帶着一些關於性能的分析也寫在下面,希望對大家有所幫助。
加一些簡單的說明,算不得理論推導,嚴格的理論推導還是要去看別的博客或書。
BP神經網絡是一個有監督學習模型,是神經網絡類算法中非常重要和典型的算法,三層神經網絡的基本結構如下:
這是最簡單的BP神經網絡結構,其運行機理是,一個特征向量的各個分量按不同權重加權,再加一個常數項偏置,生成隱層各節點的值。隱層節點的值經過一個激活函數激活后,獲得隱層激活值,隱層激活值仿照輸入層到隱層的過程,加權再加偏置,獲得輸出層值,通過一個激活函數得到最后的輸出。
上面的過程稱為前向過程,是信息的流動過程。各層之間采用全連接方式,權向量初始化為隨機向量,激活函數可使用值域為(0,1)的sigmoid函數,也可使用值域為(-1,1)的tanh函數,兩個函數的求導都很方便。
BP神經網絡的核心數據是權向量,經過初始化后,需要在訓練數據的作用下一次次迭代更新權向量,直到權向量能夠正確表達輸入輸出的映射關系為止。
權向量的更新是根據預測輸出與真值的差來更新的,前面說過BP神經網絡是一個有監督學習模型,對於一個特征向量,可以通過神經網絡前向過程得到一個預測輸出,而該特征向量的label又是已知的,二者之差就能表達預測與真實的偏差情況。這就是后向過程,后向過程是誤差流動的過程。拋開具體的理論推導不談,從編程來說,權值更新的后向過程分為兩步。
第一步,為每個神經元計算偏差δ,δ是從后向前計算的,故稱之為后向算法。對輸出層來說,偏差是 act(預測值-真值),act為激活函數。對隱層而言,需要從輸出層開始,逐層通過權值計算得到。確切的說,例如對上面的單隱層神經網絡而言,隱層各神經元的δ就是輸出層δ乘以對應的權值,如果輸出層有多個神經元,則是各輸出層神經元δ按連接的權值加權生成隱層的神經元δ。
第二步,更新權值,w=w+η*δ*v 其中η是學習率,是常數。v是要更新的權值輸入端的數值,δ是要更新的權值輸出端的數值。例如更新隱層第一個神經元到輸出層的權值,則δ是第一步計算得到的輸出層數值,v是該權值輸入端的值,即隱層第一個神經元的激活值。同理如果更新輸入層第一個單元到隱層第一個單元的權值,δ就是隱層第一個單元的值,而v是輸入層第一個單元的值。
偏置可以看作“1”對各神經元加權產生的向量,因而其更新方式相當於v=1的更新,不再贅述。在編程中可以將1強行加入各層輸入向量的末尾,從而不單獨進行偏置更新,也可以不這樣做,單獨把偏置抽出來更新。下面的算法采用的是第二種方法。
OK,簡單解釋完了,下面介紹這個程序。
一、數據庫
程序使用的數據庫是mnist手寫數字數據庫,這個數據庫我有兩個版本,一個是別人做好的.mat格式,訓練數據有60000條,每條是一個784維的向量,是一張28*28圖片按從上到下從左到右向量化后的結果,60000條數據是隨機的。測試數據有10000條吧好像,記不太清了。另一個版本是圖片版的,按0~9把訓練集和測試集分為10個文件夾,兩個版本各有用處,后面會說到,本程序用的是第一個版本。第二個版本比較大,不好上傳,第一個版本在https://github.com/MoyanZitto/BPNetwork/里可以找到。
二、程序結構
程序分四個部分,第一個部分數據讀取,第二個部分是神經網絡的配置,第三部分是神經網絡的訓練,第四部分是神經網絡的測試,最后還有個神經網絡的保存,保存效果很差,大概沒有做優化的緣故,不過仔細按保存方法設置程序還是能讀出來的,只關心神經網絡的實現的同學無視就好。
三、代碼和注釋
1 # -*- coding: utf-8 -*- 2 #本程序由UESTC的BigMoyan完成,並供所有人免費參考學習,但任何對本程序的使用必須包含這條聲明 3 import math 4 import numpy as np 5 import scipy.io as sio 6 7 8 # 讀入數據 9 ################################################################################################ 10 print "輸入樣本文件名(需放在程序目錄下)" 11 filename = 'mnist_train.mat' # raw_input() # 換成raw_input()可自由輸入文件名 12 sample = sio.loadmat(filename) 13 sample = sample["mnist_train"] 14 sample /= 256.0 # 特征向量歸一化 15 16 print "輸入標簽文件名(需放在程序目錄下)" 17 filename = 'mnist_train_labels.mat' # raw_input() # 換成raw_input()可自由輸入文件名 18 label = sio.loadmat(filename) 19 label = label["mnist_train_labels"] 20 21 ################################################################################################## 22 23 24 # 神經網絡配置 25 ################################################################################################## 26 samp_num = len(sample) # 樣本總數 27 inp_num = len(sample[0]) # 輸入層節點數 28 out_num = 10 # 輸出節點數 29 hid_num = 9 # 隱層節點數(經驗公式) 30 w1 = 0.2*np.random.random((inp_num, hid_num))- 0.1 # 初始化輸入層權矩陣 31 w2 = 0.2*np.random.random((hid_num, out_num))- 0.1 # 初始化隱層權矩陣 32 hid_offset = np.zeros(hid_num) # 隱層偏置向量 33 out_offset = np.zeros(out_num) # 輸出層偏置向量 34 inp_lrate = 0.3 # 輸入層權值學習率 35 hid_lrate = 0.3 # 隱層學權值習率 36 err_th = 0.01 # 學習誤差門限 37 38 39 ################################################################################################### 40 41 # 必要函數定義 42 ################################################################################################### 43 def get_act(x): 44 act_vec = [] 45 for i in x: 46 act_vec.append(1/(1+math.exp(-i))) 47 act_vec = np.array(act_vec) 48 return act_vec 49 50 def get_err(e): 51 return 0.5*np.dot(e,e) 52 53 54 ################################################################################################### 55 56 # 訓練——可使用err_th與get_err() 配合,提前結束訓練過程 57 ################################################################################################### 58 59 for count in range(0, samp_num): 60 print count 61 t_label = np.zeros(out_num) 62 t_label[label[count]] = 1 63 #前向過程 64 hid_value = np.dot(sample[count], w1) + hid_offset # 隱層值 65 hid_act = get_act(hid_value) # 隱層激活值 66 out_value = np.dot(hid_act, w2) + out_offset # 輸出層值 67 out_act = get_act(out_value) # 輸出層激活值 68 69 #后向過程 70 e = t_label - out_act # 輸出值與真值間的誤差 71 out_delta = e * out_act * (1-out_act) # 輸出層delta計算 72 hid_delta = hid_act * (1-hid_act) * np.dot(w2, out_delta) # 隱層delta計算 73 for i in range(0, out_num): 74 w2[:,i] += hid_lrate * out_delta[i] * hid_act # 更新隱層到輸出層權向量 75 for i in range(0, hid_num): 76 w1[:,i] += inp_lrate * hid_delta[i] * sample[count] # 更新輸出層到隱層的權向量 77 78 out_offset += hid_lrate * out_delta # 輸出層偏置更新 79 hid_offset += inp_lrate * hid_delta 80 81 ################################################################################################### 82 83 # 測試網絡 84 ################################################################################################### 85 filename = 'mnist_test.mat' # raw_input() # 換成raw_input()可自由輸入文件名 86 test = sio.loadmat(filename) 87 test_s = test["mnist_test"] 88 test_s /= 256.0 89 90 filename = 'mnist_test_labels.mat' # raw_input() # 換成raw_input()可自由輸入文件名 91 testlabel = sio.loadmat(filename) 92 test_l = testlabel["mnist_test_labels"] 93 right = np.zeros(10) 94 numbers = np.zeros(10) 95 # 以上讀入測試數據 96 # 統計測試數據中各個數字的數目 97 for i in test_l: 98 numbers[i] += 1 99 100 for count in range(len(test_s)): 101 hid_value = np.dot(test_s[count], w1) + hid_offset # 隱層值 102 hid_act = get_act(hid_value) # 隱層激活值 103 out_value = np.dot(hid_act, w2) + out_offset # 輸出層值 104 out_act = get_act(out_value) # 輸出層激活值 105 if np.argmax(out_act) == test_l[count]: 106 right[test_l[count]] += 1 107 print right 108 print numbers 109 result = right/numbers 110 sum = right.sum() 111 print result 112 print sum/len(test_s) 113 ################################################################################################### 114 # 輸出網絡 115 ################################################################################################### 116 Network = open("MyNetWork", 'w') 117 Network.write(str(inp_num)) 118 Network.write('\n') 119 Network.write(str(hid_num)) 120 Network.write('\n') 121 Network.write(str(out_num)) 122 Network.write('\n') 123 for i in w1: 124 for j in i: 125 Network.write(str(j)) 126 Network.write(' ') 127 Network.write('\n') 128 Network.write('\n') 129 130 for i in w2: 131 for j in i: 132 Network.write(str(j)) 133 Network.write(' ') 134 Network.write('\n') 135 Network.close()
四、幾點分析和說明
1.基本上只要有numpy和scipy,把github上的數據拷下來和程序放在一個文件夾里,就可以運行了。為了處理其他數據,應該把數據讀入部分注釋掉的raw_input()取消注釋,程序運行時手動輸入文件名。
2.關於輸入特征向量,簡單的把像素點的值歸一化以后作為輸入的特征向量理論上是可行的,因為真正有用的信息如果是這個特征向量的一個子集的話,訓練過程中網絡會自動把這些有用的信息選出來(通過增大權重),然而事實上無用信息,也會對結果造成負面影響,因而各位可以自己做一個特征提取的程序優化特征,有助於提高判斷准確率。我懶,就這么用吧。
3.關於隱層的層數和神經元節點數,隱層的層數這里只有一層,因為理論上三層神經網絡可以完成任意多種的分類,實際上隱層的數目在1~2層為好,不要太多。隱層的神經元節點數是個神奇的存在,目前沒有任何理論能給出最佳節點數,只有經驗公式可用。這里用的經驗公式是sqrt(輸入節點數+輸出節點數)+0~9之間的常數,也可以用log2(輸入節點數),具體多少需要反復驗證,這個經驗公式還是有點靠譜的,至少數量級上沒有太大問題。如果隱層節點數太多,程序一定會過擬合,正確率一定慘不忍睹(我曾經10%,相當於瞎猜)。
4.關於訓練方法,本程序用的是在線學習,即來一個樣本更新一次,這種方式的優點是更容易收斂的好,避免收斂到局部最優點上去。
5.學習率、誤差門限和最大迭代次數,學習率決定了梯度下降時每步的步長,會影響收斂速度,同時學習率太大容易扯着蛋(划掉),容易在山谷處來回倒騰hit不到谷底,學習率太大容易掉進局部的小坑里出不來,也是一個需要仔細設置的量。誤差門限本程序雖然設置了但是沒有使用,最大迭代次數我連設置都沒設置,這兩個量的作用主要在提前結束訓練,而因為我的程序連訓練帶測試跑一次也就20s,所以懶得提前結束訓練。但在應用時還是應該設置一下的。
6.這個程序是最簡單的BP神經網絡,沒有使用所謂動量因子加速收斂,也沒有亂七八糟別的玩意,基本上就是原生的BP神經網絡算法,代碼注釋都是中文而且寫的很全,仔細閱讀應該理解上並不難。推薦使用pycharm閱讀和修改。
7.補充一下實驗結果,表格行為不同的隱層神經元數目,列為不同學習率,內容都是overall的正確率,可見最佳神經元個數在15附近,最佳學習率在0.2附近。
以上,有問題歡迎評論提問。