0. 前言
本文翻譯自部落格:
iamtrask.github.io ,這次翻譯已經獲得trask本人的同意與支援,在此特別感謝trask。本文屬於作者一邊學習一邊翻譯的作品,所以在用詞、理論方面難免會出現很多錯誤,假如您發現錯誤或者不合適的地方,可以給我留言,謝謝!
1. 概要
我的最佳學習法就是通過玩具程式碼,一邊除錯一邊學習理論。這篇部落格通過一個非常簡單的python玩具程式碼來講解遞迴神經網路。
那麼依舊是廢話少說,放‘碼’過來!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
import copy, numpy as np np.random.seed(0) # compute sigmoid nonlinearity def sigmoid(x): output = 1/(1+np.exp(-x)) return output # convert output of sigmoid function to its derivative def sigmoid_output_to_derivative(output): return output*(1-output) # training dataset generation int2binary = {} binary_dim = 8 largest_number = pow(2,binary_dim) binary = np.unpackbits( np.array([range(largest_number)],dtype=np.uint8).T,axis=1) for i in range(largest_number): int2binary[i] = binary[i] # input variables alpha = 0.1 input_dim = 2 hidden_dim = 16 output_dim = 1 # initialize neural network weights synapse_0 = 2*np.random.random((input_dim,hidden_dim)) - 1 synapse_1 = 2*np.random.random((hidden_dim,output_dim)) - 1 synapse_h = 2*np.random.random((hidden_dim,hidden_dim)) - 1 synapse_0_update = np.zeros_like(synapse_0) synapse_1_update = np.zeros_like(synapse_1) synapse_h_update = np.zeros_like(synapse_h) # training logic for j in range(10000): # generate a simple addition problem (a + b = c) a_int = np.random.randint(largest_number/2) # int version a = int2binary[a_int] # binary encoding b_int = np.random.randint(largest_number/2) # int version b = int2binary[b_int] # binary encoding # true answer c_int = a_int + b_int c = int2binary[c_int] # where we'll store our best guess (binary encoded) d = np.zeros_like(c) overallError = 0 layer_2_deltas = list() layer_1_values = list() layer_1_values.append(np.zeros(hidden_dim)) # moving along the positions in the binary encoding for position in range(binary_dim): # generate input and output X = np.array([[a[binary_dim - position - 1],b[binary_dim - position - 1]]]) y = np.array([[c[binary_dim - position - 1]]]).T # hidden layer (input ~+ prev_hidden) layer_1 = sigmoid(np.dot(X,synapse_0) + np.dot(layer_1_values[-1],synapse_h)) # output layer (new binary representation) layer_2 = sigmoid(np.dot(layer_1,synapse_1)) # did we miss?... if so by how much? layer_2_error = y - layer_2 layer_2_deltas.append((layer_2_error)*sigmoid_output_to_derivative(layer_2)) overallError += np.abs(layer_2_error[0]) # decode estimate so we can print it out d[binary_dim - position - 1] = np.round(layer_2[0][0]) # store hidden layer so we can use it in the next timestep layer_1_values.append(copy.deepcopy(layer_1)) future_layer_1_delta = np.zeros(hidden_dim) for position in range(binary_dim): X = np.array([[a[position],b[position]]]) layer_1 = layer_1_values[-position-1] prev_layer_1 = layer_1_values[-position-2] # error at output layer layer_2_delta = layer_2_deltas[-position-1] # error at hidden layer layer_1_delta = (future_layer_1_delta.dot(synapse_h.T) + layer_2_delta.dot(synapse_1.T)) * sigmoid_output_to_derivative(layer_1) # let's update all our weights so we can try again synapse_1_update += np.atleast_2d(layer_1).T.dot(layer_2_delta) synapse_h_update += np.atleast_2d(prev_layer_1).T.dot(layer_1_delta) synapse_0_update += X.T.dot(layer_1_delta) future_layer_1_delta = layer_1_delta synapse_0 += synapse_0_update * alpha synapse_1 += synapse_1_update * alpha synapse_h += synapse_h_update * alpha synapse_0_update *= 0 synapse_1_update *= 0 synapse_h_update *= 0 # print out progress if(j % 1000 == 0): print "Error:" + str(overallError) print "Pred:" + str(d) print "True:" + str(c) out = 0 for index,x in enumerate(reversed(d)): out += x*pow(2,index) print str(a_int) + " + " + str(b_int) + " = " + str(out) print "------------" |
執行輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
Error:[ 3.45638663] Pred:[0 0 0 0 0 0 0 1] True:[0 1 0 0 0 1 0 1] 9 + 60 = 1 ------------ Error:[ 3.63389116] Pred:[1 1 1 1 1 1 1 1] True:[0 0 1 1 1 1 1 1] 28 + 35 = 255 ------------ Error:[ 3.91366595] Pred:[0 1 0 0 1 0 0 0] True:[1 0 1 0 0 0 0 0] 116 + 44 = 72 ------------ Error:[ 3.72191702] Pred:[1 1 0 1 1 1 1 1] True:[0 1 0 0 1 1 0 1] 4 + 73 = 223 ------------ Error:[ 3.5852713] Pred:[0 0 0 0 1 0 0 0] True:[0 1 0 1 0 0 1 0] 71 + 11 = 8 ------------ Error:[ 2.53352328] Pred:[1 0 1 0 0 0 1 0] True:[1 1 0 0 0 0 1 0] 81 + 113 = 162 ------------ Error:[ 0.57691441] Pred:[0 1 0 1 0 0 0 1] True:[0 1 0 1 0 0 0 1] 81 + 0 = 81 ------------ Error:[ 1.42589952] Pred:[1 0 0 0 0 0 0 1] True:[1 0 0 0 0 0 0 1] 4 + 125 = 129 ------------ Error:[ 0.47477457] Pred:[0 0 1 1 1 0 0 0] True:[0 0 1 1 1 0 0 0] 39 + 17 = 56 ------------ Error:[ 0.21595037] Pred:[0 0 0 0 1 1 1 0] True:[0 0 0 0 1 1 1 0] 11 + 3 = 14 ------------ |
第一部分:什麼是神經元記憶?
正向的背一邊字母表……你能做到,對吧?
倒著背一遍字母表……唔……也許有點難。
那麼試試你熟悉的一首歌詞?……為什麼正常順序回憶的時候比倒著回憶更簡單呢?你能直接跳躍到第二小節的歌詞麼?……唔唔……同樣很難,是吧?
其實這很符合邏輯……你並不像計算機那樣把字母表或者歌詞像儲存在硬碟一樣的記住,你是把它們作為一個序列去記憶的。你很擅長於一個單詞一個單詞的去回憶起它們,這是一種條件記憶。你只有在擁有了前邊部分的記憶了以後,才能想起來後邊的部分。如果你對連結串列比較熟悉的話,OK,我們的記憶就和連結串列是類似的。
然而,這並不意味著當你不唱歌時,你的記憶中就沒有這首歌。而是說,當你試圖直接記憶起某個中間的部分,你需要花費一定的時間在你的腦海中尋找(也許是在一大堆神經元裡尋找)。大腦開始在這首歌裡到處尋找你想要的中間部分,但是大腦之前並沒有這麼做過,所以它並沒有一個能夠指向中間這部分的索引。這就像住在一個附近都是岔路/死衚衕的地方,你從大路上到某人的房子很簡單,因為你經常那樣走。但是把你丟在一家人的後院裡,你卻怎麼也找不到正確的道路了。可見你的大腦並不是用“方位”去尋找,而是通過一首歌的開頭所在的神經元去尋找的。如果你想了解更多關於大腦的知識,可以訪問:http://www.human-memory.net/processes_recall.html。
就像連結串列一樣,記憶這樣去儲存是很有效的。這樣可以通過腦神經網路很好的找到相似的屬性、優勢。一些過程、難題、表示、查詢也可以通過這種短期/偽條件記憶序列儲存的方式,使其更加的高效。
去記憶一些資料是序列的事情(其實就是意味著你有些東西需要去記住!),假設有一個跳跳球,每個資料點就是你眼中跳跳球運動的一幀影象。如果你想訓練一個神經網路去預測下一幀球會在哪裡,那麼知道上一幀球在哪裡就會對你的預測很有幫助!這樣的序列資料就是我們為什麼要搭建一個遞迴神經網路。那麼,一個神經網路怎麼記住它之前的時間它看到了什麼呢?
神經網路有隱藏層,一般來講,隱藏層的狀態只跟輸入資料有關。所以一般來說一個神經網路的資訊流就會像下面所示的這樣:
input -> hidden ->output
這很明顯,確定的輸入產生確定的隱藏層,確定的隱藏層產生確定的輸出層。這是一種封閉系統。但是,記憶改變了這種模式!記憶意味著隱藏層是,當前時刻的輸入與隱藏層前一時刻的一種組合。
( input + prev_hidden ) -> hidden -> output
為什麼是隱藏層呢?其實技術上來說我們可以這樣:
( input + prev_input ) -> hidden -> output
然而,我們遺漏了一些東西。我建議你認真想想這兩個資訊流的不同。給你點提示,演繹一下它們分別是怎麼運作的。這裡呢,我們給出4步的遞迴神經網路流程看看它怎麼從之前的隱藏層得到資訊。
( input + empty_hidden ) -> hidden -> output
( input + prev_hidden ) -> hidden -> output
( input + prev_hidden ) -> hidden -> output
( input + prev_hidden ) -> hidden -> output
然後,我們再給出4步,從輸入層怎麼得到資訊。
( input + empty_input ) -> hidden -> output
( input + prev_input ) -> hidden -> output
( input + prev_input ) -> hidden -> output
( input + prev_input ) -> hidden -> output
或許,如果我把一些部分塗上顏色,一些東西就顯而易見了。那麼我們再看看這4步隱藏層的遞迴:
( input + empty_hidden ) ->hidden -> output
( input + prev_hidden ) ->hidden -> output
( input + prev_hidden ) ->hidden-> output
( input + prev_hidden ) ->hidden-> output
……以及,4步輸入層的遞迴:
( input + empty_input ) -> hidden -> output
( input + prev_input ) -> hidden -> output
( input + prev_input ) -> hidden -> output
( input + prev_input ) -> hidden -> output
看一下最後一個隱藏層(第四行)。在隱藏層遞迴中,我們可以看到所有見過的輸入的存在。但是在輸入層遞迴中,我們僅僅能發現上次與本次的輸入。這就是為什麼我們用隱藏層遞迴建模。隱藏層遞迴能學習它到底去記憶什麼,但是輸入層遞迴僅僅能記住上次的資料點。
現在我們對比一下這兩種方法,通過反向的字母表與歌詞中間部分的練習。隱藏層根據越來越多的輸入持續的改變,而且,我們到達這些隱藏狀態的唯一方式就是沿著正確的輸入序列。現在就到了很重要的一點,輸出由隱藏層決定,而且只有通過正確的輸入序列才能到達隱藏層。是不是很相似?
那麼有什麼實質的區別呢?我們考慮一下我們要預測歌詞中的下一個詞,假如碰巧在不同的地方有兩個相同的詞,“輸出層遞迴”就會使你回憶不起來下面的歌詞到底是什麼了。仔細想想,如果一首歌有一句“我愛你”,以及“我愛蘿蔔”,記憶網路現在試圖去預測下一個詞,那它怎麼知道“我愛”後邊到底是什麼?可能是“你”,也可能是“蘿蔔”。所以記憶網路必須要知道更多的資訊,去識別這到底是歌詞中的那一段。而“隱藏層遞迴”不會讓你忘記歌詞,就是通過這個原理。它巧妙地記住了它看到的所有東西(記憶更巧妙地是它能隨時間逐漸忘卻)。想看看它是怎麼運作的,猛戳這裡:http://karpathy.github.io/2015/05/21/rnn-effectiveness/
好的,現在停下來,然後確認你的腦袋是清醒的。
第二部分:RNN – 神經網路記憶
現在我們已經對這個問題有個直觀的認識了,讓我們下潛的更深一點(什麼鬼,你在逗我?)。就像在反向傳播這篇博文(http://blog.csdn.net/zzukun/article/details/49556715)裡介紹的那樣,輸入資料決定了我們神經網路的輸入層。每行輸入資料都被用來產生隱含層(通過正向傳播),然後用每個隱含層生成輸出層(假設只有一層隱含層)。就像我們剛才看到的,記憶意味著隱含層是輸入與上一次隱含層的組合。那麼怎麼組合呢?其實就像神經網路的其他傳播方法,用一個矩陣就行了,這個矩陣定義了之前隱含層與當前的關係。
從這張圖中能看出來很多東西。這裡只有三個權值矩陣,其中兩個很相似(名字也一樣)。SYNAPSE_0把輸入資料傳播到隱含層,SYNAPSE_1把隱含層資料傳播到輸出層。新的矩陣(SYNAPSE_h……要遞迴的),把隱含層(layer_1)傳播到下一個時間點的隱含層(仍舊是layer_1)。
好的,現在停下來,然後確認你的腦袋是清醒的。
上邊的GIF圖展現出遞迴神經網路的奧祕,以及一些非常、非常重要的性質。圖中描述了4個時間步數,第一個僅僅受到輸入資料的影響,第二個把第二個輸入與第一個的隱含層混合,如此繼續。有人可能會注意到,在這種方式下,第四個網路“滿了”。這樣推測的話,第五步不得不選擇一個某個節點去替代掉它。是的,這很正確。這就是記憶的“容量”概念。正如你所期望的,更多的隱含層節點能夠儲存更多的記憶,並使記憶保持更長的時間。同樣這也是網路學習去忘記無關的記憶並且記住重要的記憶。你在能從第三步中看出點什麼不?為什麼有更多的綠色節點呢?
另外需要注意的是,隱含層是輸入與輸出中間的一道柵欄。事實上,輸出已經不再是對應於輸入的一個函式。輸入只是改變了記憶中儲存的東西,而且輸出僅僅依賴於記憶!告訴你另外一個有趣的事情,如果上圖中的第2,3,4步沒有輸入,隨著時間的流逝,隱含層仍然會改變。
好的,好的,我知道你已經停下來了,不過一定要保證剛才的內容你已經差不多理解了。
第三部分:基於時間的反向傳播
那麼現在問題來了,遞迴神經網路怎麼學習的呢?看下面的圖片,黑色的是預測,誤差是亮黃色,導數是芥末色的(暗黃色)。
網路通過從1到4的全部傳播(通過任意長度的整個序列),然後從4到1反向傳播所有的導數值。你也可以認為這僅僅是正常神經網路的一個有意思的變形,除了我們在各自的地方複用了相同的權值(突觸synapses 0,1,h)。其他的地方都是很普通的反向傳播。
第四部分:我們的玩具程式碼
我們現在使用遞迴神經網路去建模二進位制加法。你看到下面的序列了麼?上邊這倆在方框裡的,有顏色的1是什麼意思呢?
框框中彩色的1表示“攜帶位”。當每個位置的和溢位時(需要進位),它們“攜帶這個‘1’”。我們就是要教神經網路學習去記住這個“攜帶位”。當“和”需要它,它需要去“攜帶這個‘1’”。
二進位制加法從右邊到左邊進行計算,我們試圖通過上邊的數字,去預測橫線下邊的數字。我們想讓神經網路遍歷這個二進位制序列並且記住它攜帶這個1與沒有攜帶這個1的時候,這樣的話網路就能進行正確的預測了。不要迷戀於這個問題本身,因為神經網路事實上也不在乎。就當作我們有兩個在每個時間步數上的輸入(1或者0加到每個數字的開頭),這兩個輸入將會傳播到隱含層,隱含層會記住是否有攜帶位。預測值會考慮所有的資訊,然後去預測每個位置(時間步數)正確的值。
下面我推薦同時開啟兩個這個頁面,這樣就可以一邊看程式碼,一邊看下面的解釋。我就是這麼寫這篇文章的。
Lines 0-2:匯入依賴包,設定隨機數生成的種子。我們只需要兩個依賴包,numpy和copy。numpy是為了矩陣計算,copy用來拷貝東西。
Lines 4-11:我們的非線性函式與其導數,更多的細節可見參考我們之前的部落格:http://blog.csdn.net/zzukun/article/details/49556715
Line 15:這一行宣告瞭一個查詢表,這個表是一個實數與對應二進位制表示的對映。二進位制表示將會是我們網路的輸入與輸出,所以這個查詢表將會幫助我們將實數轉化為其二進位制表示。
Line 16:這裡設定了二進位制數的最大長度。如果一切都除錯好了,你可以把它調整為一個非常大的數。
Line 18:這裡計算了跟二進位制最大長度對應的可以表示的最大十進位制數。
Line 19:這裡生成了十進位制數轉二進位制數的查詢表,並將其複製到int2binary裡面。雖然說這一步不是必需的,但是這樣的話理解起來會更方便。
Line 26:這裡設定了學習速率。
Line 27:我們要把兩個數加起來,所以我們一次要輸入兩位字元。如此以來,我們的網路就需要兩個輸入。
Line 28:這是隱含層的大小,回來儲存“攜帶位”。需要注意的是,它的大小比原理上所需的要大。自己嘗試著調整一下這個值,然後看看它如何影響收斂速率。更高的隱含層維度會使訓練變慢還是變快?更多或是更少的迭代次數?
Line 29:我們只是預測和的值,也就是一個數。如此,我們只需一個輸出。
Line 33:這個權值矩陣連線了輸入層與隱含層,如此它就有“imput_dim”行以及“hidden_dim”列(假如你不改引數的話就是2×16)。
Line 34:這個權值矩陣連線了隱含層與輸出層,如此它就有“hidden_dim”行以及“output_dim”列(假如你不改引數的話就是16×1)。
Line 35:這個權值矩陣連線了前一時刻的隱含層與現在時刻的隱含層。它同樣連線了當前時刻的隱含層與下一時刻的隱含層。如此以來,它就有隱含層維度大小(hidden_dim)的行與隱含層維度大小(hidden_dim)的列(假如你沒有修改引數就是16×16)。
Line 37-39:這裡儲存權值更新。在我們積累了一些權值更新以後,我們再去更新權值。這裡先放一放,稍後我們再詳細討論。
Line 42:我們迭代訓練樣例10000次。
Line 45:這裡我們要隨機生成一個在範圍內的加法問題。所以我們生成一個在0到最大值一半之間的整數。如果我們允許網路的表示超過這個範圍,那麼把兩個數加起來就有可能溢位(比如一個很大的數導致我們的位數不能表示)。所以說,我們只把加法要加的兩個數字設定在小於最大值的一半。
Line 46:我們查詢a_int對應的二進位制表示,然後把它存進a裡面。
Line 48:原理同45行。
Line 49:原理同46行。
Line 52:我們計算加法的正確結果。
Line 53:把正確結果轉化為二進位制表示。
Line 56:初始化一個空的二進位制陣列,用來儲存神經網路的預測值(便於我們最後輸出)。你也可以不這樣做,但是我覺得這樣使事情變得更符合直覺。
Line 58:重置誤差值(這是我們使用的一種記錄收斂的方式……可以參考之前關於反向傳播與梯度下降的文章)
Line 60-61:這兩個list會每個時刻不斷的記錄layer 2的導數值與layer 1的值。
Line 62:在0時刻是沒有之前的隱含層的,所以我們初始化一個全為0的。
Line 65:這個迴圈是遍歷二進位制數字。
Line 68:X跟圖片中的“layer_0”是一樣的,X陣列中的每個元素包含兩個二進位制數,其中一個來自a,一個來自b。它通過position變數從a,b中檢索,從最右邊往左檢索。所以說,當position等於0時,就檢索a最右邊的一位和b最右邊的一位。當position等於1時,就向左移一位。
Line 69:跟68行檢索的方式一樣,但是把值替代成了正確的結果(0或者1)。
Line 72:這裡就是奧妙所在!一定一定一定要保證你理解這一行!!!為了建立隱含層,我們首先做了兩件事。第一,我們從輸入層傳播到隱含層(np.dot(X,synapse_0))。然後,我們從之前的隱含層傳播到現在的隱含層(np.dot(prev_layer_1.synapse_h))。在這裡,layer_1_values[-1]就是取了最後一個存進去的隱含層,也就是之前的那個隱含層!然後我們把兩個向量加起來!!!!然後再通過sigmoid函式。
那麼,我們怎麼結合之前的隱含層資訊與現在的輸入呢?當每個都被變數矩陣傳播過以後,我們把資訊加起來。
Line 75:這行看起來很眼熟吧?這跟之前的文章類似,它從隱含層傳播到輸出層,即輸出一個預測值。
Line 78:計算一下預測誤差(預測值與真實值的差)。
Line 79:這裡我們把導數值存起來(上圖中的芥末黃),即把每個時刻的導數值都保留著。
Line 80:計算誤差的絕對值,並把它們加起來,這樣我們就得到一個誤差的標量(用來衡量傳播)。我們最後會得到所有二進位制位的誤差的總和。
Line 86:將layer_1的值拷貝到另外一個陣列裡,這樣我們就可以下一個時間使用這個值。
Line 90:我們已經完成了所有的正向傳播,並且已經計算了輸出層的導數,並將其存入在一個列表裡了。現在我們需要做的就是反向傳播,從最後一個時間點開始,反向一直到第一個。
Line 92:像之前那樣,檢索輸入資料。
Line 93:從列表中取出當前的隱含層。
Line 94:從列表中取出前一個隱含層。
Line 97:從列表中取出當前輸出層的誤差。
Line 99:這一行計算了當前隱含層的誤差。通過當前之後一個時間點的誤差和當前輸出層的誤差計算。
Line 102-104:我們已經有了反向傳播中當前時刻的導數值,那麼就可以生成權值更新的量了(但是還沒真正的更新權值)。我們會在完成所有的反向傳播以後再去真正的更新我們的權值矩陣,這是為什麼呢?因為我們要用權值矩陣去做反向傳播。如此以來,在完成所有反向傳播以前,我們不能改變權值矩陣中的值。
Line 109-115:現在我們就已經完成了反向傳播,得到了權值要更新的量,所以就趕快更新權值吧(別忘了重置update變數)!
Line 118-end:這裡僅僅是一些輸出日誌,便於我們觀察中間的計算過程與效果。
第五步分:建議與評論
如果您有什麼疑問、意見與建議可以直接留言評論,或者給我email(likun@stu.zzu.edu.cn),或直接聯絡trask本人,感謝您的支援!