用Python寫出LSTM-RNN的程式碼!
0. 前言
本文翻譯自部落格: iamtrask.github.io ,這次翻譯已經獲得trask本人的同意與支援,在此特別感謝trask。本文屬於作者一邊學習一邊翻譯的作品,所以在用詞、理論方面難免會出現很多錯誤,假如您發現錯誤或者不合適的地方,可以給我留言,謝謝!
1. 概要
我的最佳學習法就是通過玩具程式碼,一邊除錯一邊學習理論。這篇部落格通過一個非常簡單的python玩具程式碼來講解遞迴神經網路。
那麼依舊是廢話少說,放‘碼’過來!
- 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
- 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"------------"
執行輸出:
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用來拷貝東西。
Line 15:這一行聲明瞭一個查詢表,這個表是一個實數與對應二進位制表示的對映。二進位制表示將會是我們網路的輸入與輸出,所以這個查詢表將會幫助我們將實數轉化為其二進位制表示。
Line 16:這裡設定了二進位制數的最大長度。如果一切都除錯好了,你可以把它調整為一個非常大的數。
Line 18:這裡計算了跟二進位制最大長度對應的可以表示的最大十進位制數。
Line 19:這裡生成了十進位制數轉二進位制數的查詢表,並將其複製到int2binary裡面。雖然說這一步不是必需的,但是這樣的話理解起來會更方便。
Line 26:這裡設定了學習速率。
Line 27:我們要把兩個數加起來,所以我們一次要輸入兩位字元。如此以來,我們的網路就需要兩個輸入。
Line 28:這是隱含層的大小,回來儲存“攜帶位”。需要注意的是,它的大小比原理上所需的要大。自己嘗試著調整一下這個值,然後看看它