作者:BigMoyan
鏈接:https://zhuanlan.zhihu.com/p/26475643
來源:知乎
著作權歸作者所有,轉載請聯系作者獲得授權。
總而言之,DL不是太過困難的事情,但是也沒有容易到21天從入門到精通的地步。所以突然很想把之前寫的BP神經網絡拿出來,不是很復雜,但我固執的認為,這是每個學DL的人都應該了解的事情——會無腦調別人寫好的API,不代表就真的懂DL了。
好的,這篇文章與Keras無關,我們要介紹的是:
什么是BP網絡,以及其推導
如何用純Python手擼一個BP神經網絡
就這兩點。
本文的讀者是最低意義下的“零基礎”,想入門DL的同學可以自測一下……如果不能讀懂本文的話,基本上你的DL基礎是負數,需要先把欠的債補回來。
所以真的不要再聽信什么21天速成的鬼話了,腳踏實地才能走的高遠,想一步登天小心步子太大扯到蛋,欲速則不達喲。
BP神經網絡:DL的祖宗
人工神經網絡從來就不是這兩年才冒出來的新鮮玩意,雖然最近由于深度學習的出現變得異常火爆。人工神經網絡本來就是機器學習的一種方法的說,既然我們都開始搞BP了,不如就再說的更底層一點,從神經元說起。
最近有一個45個問題測出你的深度學習基本功,第一個問題就是關于神經元的,然后我就答錯了……可能是我見識有限,反正我知道的“神經元”是這樣的:
對于一個輸入向量[a1...an],神經元對它的響應就是按照一定的權重對它們做乘加操作,然后加一項bias,最后用某個函數f對其做映射,得到輸出,也就是這樣一個運算:
其中w是神經元的連接權,b是偏置,a是輸入信號,f通常是一個非線性的函數,稱為激活函數。
當我們用很多個神經元對輸入進行變換,由于每個神經元的參數都不一樣,得到的輸出也不一樣。n個神經元就會得到n個輸出,這n個神經元就稱為一層神經網絡。而這層神經網絡的輸出本身也是一個向量,自然可以看作下一層神經網絡的輸入信號,于是我們又搞了若干神經元構成下一層,于是我們獲得了有兩層神經元構成的神經網絡。一般而言,我們把輸入也算作一層神經網絡結點,所以此時的網絡層數是3層。
我們離深度學習只有一步了。如果再加一層,讓網絡層達到4層及以上,恭喜你,得到了深度神經網絡,也就是“深度學習”了。
超簡單,是不?
用腳丫子想都知道不可能這么簡單吧= =
實際上,從單個神經元,到兩層神經網絡,到三層神經網絡,到四層以上神經網絡,每一步都是巨大的飛躍,每一步……實際上都是神經網絡歷史上的重要事件。
單個神經元,又稱“M-P神經元模型”,在1943被提出。把神經元組成一層網絡層,加上輸入數據構成的兩層神經網絡稱為“感知機”。感知機由于無法處理線性不可分的問題,被圖靈獎獲得者Marvin Minksky一巴掌扇落塵埃,相當長時間內無人研究。兩層神經網絡進化為三層,就進化到了“多層感知機”,多層感知機由輸入層-隱層-輸出層構成,能夠處理非線性問題,再加上Rumelhart重新發明的BP算法,硬硬的把神經網絡起死回生一波。
但是多層感知機也是有問題的,最突出的問題是無法搞的太深,否則網絡非常難訓練,模型再美,訓練不出來就是shi。所以多層感知機的水平,也就是個這了。然后就是我們熟悉的故事,北風呼嘯之時所有人都關門閉戶,唯有Hinton等少數幾人踽踽獨行,最終搞出預訓練+微調的深度網絡訓練方法,挖出10+年的學術巨坑。
今天我們的故事是多層感知機和BP算法。
多層感知機的模型如下圖:
順便,文中所有圖侵刪。
對于一個特定任務,我們需要根據具體數據來確定模型的參數。對于多層感知機而言,參數就是每個神經元的連接權和偏置。
網絡參數在損失函數的指導下確定的,這里的指導指的是,損失函數告訴你,網絡的預測值跟真實值差多少,應該如何更新參數才能減小這個差距。
假設:
神經元激活函數可導的
以預測值和真值的均方差作為損失函數
BP神經網絡之所以叫BP,就是其參數更新的方式遵循“誤差反向傳播(error BackPropagation)”的方式。在給定的網絡參數的時候,一個神經網絡有兩項最基本的操作:
對輸入數據進行運算,得到預測(Predictions),這個過程是信號從網絡的輸出端到網絡的輸出端的運算,稱為前向過程。
根據預測值與真值的偏差,產生誤差信號從輸出端向輸入端傳輸,并在傳輸的過程中更新網絡的參數,這個過程稱為后向過程,或者反傳過程。
神經網絡的訓練,就是這兩個過程的輪番上陣的結果。參數更新的方式,是梯度下降法。假設對給定的樣本x,神經網絡的輸出是
,真實值為y,則網絡的損失函數由最后一層神經元跟數據的真實標簽產生的,所以損失函數其實直接是最后一層神經元的函數。比方說最后一層有
個神經元吧,那損失函數是:
其中,預測值的每個點
都是由隱層神經元的輸出值加權和再搞個激活函數得到的,所以再寫的細一點:
其中,
就是網絡輸出層輸入,是個向量。
是最后一層的第i個神經元的參數,也是向量。
是對應的偏置,是一個數字。當然,我們可以寫的更緊湊一點,用矩陣乘法來表示這個過程。如果你看不懂這一點,那代表你需要復習一下線性代數。
寫來寫去,都是一個意思。現在我們已經有誤差了,如何根據誤差一層層的反傳來修改參數呢?先考慮最后一層的參數,我們要用梯度下降法對其進行更新,即:
這個偏導數由鏈式法則求,L是輸出層預測值的直接函數,所以先求它對輸出層預測值的導數,然后根據鏈式法則求預測值對參數的導數,大概就是這樣:
那對其他層的參數呢?比方說對任意第r層的參數
,實際上也具有相同的形式:
所以要用梯度下降法更新神經網絡的參數,關鍵就是要求出損失函數對任意層輸出的導數。我們定義損失函數對第r層輸出(未經過激活函數)的導數為
。先考慮r=l,即最后一層的情況。
第一項是預測值和真值的差,我們將其記作誤差
。那么對網絡的最后一層,梯度是可以輕松算出來的。
只需要把表達式
寫出,即可輕松得到它關于
的導數。
對于非最后一層,導數計算略復雜。根據鏈式法則,損失函數對任意r
所以我們總有關系:
那么
是誰咧,下一層的輸出當然是上一層的輸出經過激活函數后再線性加權,也就是:
這個導數很好求嘛,就是跟之前一模一樣的:
所以我們就構造了一個遞推的關系,從最后一層開始,我們總有:
為啥叫反向傳播哪,就是因為第r層的梯度更新是由沿著第r+1層的網絡反傳回來的
確定的,因此叫反向傳播。這里我們只推了權值W的更新,偏置b的更新更加簡單,這里就不推了,其實只要把每層的輸入y增加一個恒為1的數,變為(y,1)就可以了,但這樣不太好在代碼里實現就是了。
以上就是BP算法的推導,也是深度學習的基礎。所以大家知道了,為什么我們要求深度學習的目標函數必須可導,為什么激活函數一定也要可導。因為一旦有一環不可導,前向運算固然沒問題,反向運算的梯度鏈條就要斷了。目標函數不可導,則從最后一層起我們就沒法更新參數。激活函數不可導,哪層不可導梯度的反傳就到哪層為止。
手擼BP
下面的代碼是我兩年多前的時候剛學Python和機器學習時手擼的,實現了一個三層的BP神經網絡,對MNIST數字進行分類,代碼寫的不怎么好,畢竟那會兒是剛學,python什么的用的也是稀里糊涂,寫出來跟C++一個味。
這里的激活函數取得是sigmoid激活函數,它的導數是它自己再乘以1減去它自己,性質比較好。
首先讀入數據,當時拿到的mnist數據是matlab存的.mat格式,所以用scipy的相關接口讀一下
importmathimportnumpyasnpimportscipy.ioassio# 讀入數據################################################################################print"輸入樣本文件名(需放在程序目錄下)"filename='mnist_train.mat'sample=sio.loadmat(filename)sample=sample["mnist_train"]sample/=256.0# 特征向量歸一化print"輸入標簽文件名(需放在程序目錄下)"filename='mnist_train_labels.mat'label=sio.loadmat(filename)label=label["mnist_train_labels"]
然后配置網絡,主要是設置學習率,還有隱層參數,并初始化一下權重。權重有兩套,輸入層到隱層的映射是一套,隱層到輸出層的映射是一套:
# 神經網絡配置################################################################################samp_num=len(sample)# 樣本總數inp_num=len(sample[0])# 輸入層節點數out_num=10# 輸出節點數hid_num=6# 隱層節點數(經驗公式)w1=0.2*np.random.random((inp_num,hid_num))-0.1# 初始化輸入層權矩陣w2=0.2*np.random.random((hid_num,out_num))-0.1# 初始化隱層權矩陣hid_offset=np.zeros(hid_num)# 隱層偏置向量out_offset=np.zeros(out_num)# 輸出層偏置向量inp_lrate=0.3# 輸入層權值學習率hid_lrate=0.3# 隱層學權值習率err_th=0.01# 學習誤差門限
你看,其實自己寫很屌的,不同層的學習率都可以設不一樣,想怎么搞怎么搞——只不過實際上用的時候是一樣就是了。我這里學習率居然設了0.3簡直可怕……唉那會兒真的不知道學習率多大算大23333。
然后定義幾個函數,一個是激活函數,一個是損失函數。深度學習框架里損失函數都是正兒八經的要從這獲得梯度的,我這損失函數就負責輸出一個輸出值,看看現在訓練效果咋樣。好吧,其實這個函數我壓根沒用著23333
# 必要函數定義################################################################################defget_act(x):#激活函數act_vec=[]foriinx:act_vec.append(1/(1+math.exp(-i)))act_vec=np.array(act_vec)returnact_vecdefget_err(e):#損失函數return0.5*np.dot(e,e)
然后下一步是訓練網絡,這里用的是隨機梯度下降法——正兒八經的隨機梯度下降,一個樣本一個樣本的來訓練。這部分就是之前推導的BP神經網絡了,結合代碼,看一下自己是否能夠讀懂?
# 訓練——可使用err_th與get_err() 配合,提前結束訓練過程################################################################################for count in range(0, samp_num):printcountt_label=np.zeros(out_num)t_label[label[count]]=1#前向過程hid_value=np.dot(sample[count],w1)+hid_offset# 隱層值hid_act=get_act(hid_value)# 隱層激活值out_value=np.dot(hid_act,w2)+out_offset# 輸出層值out_act=get_act(out_value)# 輸出層激活值#后向過程e=t_label-out_act# 輸出值與真值間的誤差out_delta=e*out_act*(1-out_act)# 輸出層delta計算hid_delta=hid_act*(1-hid_act)*np.dot(w2,out_delta)# 隱層delta計算foriinrange(0,out_num):w2[:,i]+=hid_lrate*out_delta[i]*hid_act# 更新隱層到輸出層權向量foriinrange(0,hid_num):w1[:,i]+=inp_lrate*hid_delta[i]*sample[count]# 更新輸出層到隱層的權向量out_offset+=hid_lrate*out_delta# 輸出層偏置更新hid_offset+=inp_lrate*hid_delta
最后是測試網絡,只跑前向過程,依然是一個一個樣本測試:
# 測試網絡################################################################################filename = 'mnist_test.mat' test = sio.loadmat(filename)test_s = test["mnist_test"]test_s /= 256.0filename = 'mnist_test_labels.mat' testlabel = sio.loadmat(filename)test_l = testlabel["mnist_test_labels"]right = np.zeros(10)numbers = np.zeros(10)# 以上讀入測試數據# 統計測試數據中各個數字的數目for i in test_l:? ? numbers[i] += 1for count in range(len(test_s)):? ? hid_value = np.dot(test_s[count], w1) + hid_offset? ? ? # 隱層值? ? hid_act = get_act(hid_value)? ? ? ? ? ? ? ? # 隱層激活值? ? out_value = np.dot(hid_act, w2) + out_offset? ? ? ? ? ? # 輸出層值? ? out_act = get_act(out_value)? ? ? ? ? ? ? ? # 輸出層激活值? ? if np.argmax(out_act) == test_l[count]:? ? ? ? right[test_l[count]] += 1print rightprint numbersresult = right/numberssum = right.sum()print resultprint sum/len(test_s)
其實后面還有一段保存網絡,就是把權重無腦保存成txt……可見少年時期的我已經有了“神經網絡最重要的是權重”這樣的意識了,可惜寫得太爛基本上很難recover。我就不貼了。
最后貼一個當時我調的結果,前段時間在知乎看到一個文章還是回答在吹MNIST識別,一個mlp最后結果是92%居然敢號稱效果非常好。23333吹牛之前拜托了解行情啊,我一個裸著的3層MLP,亂七八糟的代碼,神奇的學習率、參數初始化以及激活函數都搞到將近92%,你拿現代深度學習框架搭出來的網絡也92%實在是……有一種全副武裝拿著沖鋒槍跟遠古時代持矛野人打成平手的即視感……
橫軸是隱層神經元個數,縱軸是學習率(我TM居然試了0.7的學習率這是有多屌!)
最好的效果是91.4%的準確率,這幅破爛能跑成這樣可以了。
好,以上就是本期的內容。有志于深度學習的你,就從簡單的BP神經網絡開始吧!鞠躬下臺~
對了,忘了說題圖,題圖是我的二老婆五更琉璃,我們是最近剛認識的不過很快就墜入愛河了,助手跟小埋已經同意我們了所以你們不要多廢話。