1. 程式人生 > >用Python寫出LSTM-RNN的程式碼!

用Python寫出LSTM-RNN的程式碼!

0. 前言

本文翻譯自部落格: iamtrask.github.io ,這次翻譯已經獲得trask本人的同意與支援,在此特別感謝trask。本文屬於作者一邊學習一邊翻譯的作品,所以在用詞、理論方面難免會出現很多錯誤,假如您發現錯誤或者不合適的地方,可以給我留言,謝謝!

1. 概要

我的最佳學習法就是通過玩具程式碼,一邊除錯一邊學習理論。這篇部落格通過一個非常簡單的python玩具程式碼來講解遞迴神經網路。

那麼依舊是廢話少說,放‘碼’過來!

  1. import copy, numpy as np  
  2. np.random.seed(0)  
  3. # compute sigmoid nonlinearity
  4. def sigmoid(x):  
  5.     output = 1/(1+np.exp(-x))  
  6.     return output  
  7. # convert output of sigmoid function to its derivative
  8. def sigmoid_output_to_derivative(output):  
  9.     return output*(1-output)  
  10. # training dataset generation
  11. int2binary = {}  
  12. binary_dim = 8
  13. largest_number = pow(2,binary_dim)  
  14. binary = np.unpackbits(  
  15.     np.array([range(largest_number)],dtype=np.uint8).T,axis=1)  
  16. for i in range(largest_number):  
  17.     int2binary[i] = binary[i]  
  18. # input variables
  19. alpha = 0.1
  20. input_dim = 2
  21. hidden_dim = 16
  22. output_dim = 1
  23. # initialize neural network weights
  24. synapse_0 = 2*np.random.random((input_dim,hidden_dim)) - 1
  25. synapse_1 = 2
    *np.random.random((hidden_dim,output_dim)) - 1
  26. synapse_h = 2*np.random.random((hidden_dim,hidden_dim)) - 1
  27. synapse_0_update = np.zeros_like(synapse_0)  
  28. synapse_1_update = np.zeros_like(synapse_1)  
  29. synapse_h_update = np.zeros_like(synapse_h)  
  30. # training logic
  31. for j in range(10000):  
  32.     # generate a simple addition problem (a + b = c)
  33.     a_int = np.random.randint(largest_number/2# int version
  34.     a = int2binary[a_int] # binary encoding
  35.     b_int = np.random.randint(largest_number/2# int version
  36.     b = int2binary[b_int] # binary encoding
  37.     # true answer
  38.     c_int = a_int + b_int  
  39.     c = int2binary[c_int]  
  40.     # where we'll store our best guess (binary encoded)
  41.     d = np.zeros_like(c)  
  42.     overallError = 0
  43.     layer_2_deltas = list()  
  44.     layer_1_values = list()  
  45.     layer_1_values.append(np.zeros(hidden_dim))  
  46.     # moving along the positions in the binary encoding
  47.     for position in range(binary_dim):  
  48.         # generate input and output
  49.         X = np.array([[a[binary_dim - position - 1],b[binary_dim - position - 1]]])  
  50.         y = np.array([[c[binary_dim - position - 1]]]).T  
  51.         # hidden layer (input ~+ prev_hidden)
  52.         layer_1 = sigmoid(np.dot(X,synapse_0) + np.dot(layer_1_values[-1],synapse_h))  
  53.         # output layer (new binary representation)
  54.         layer_2 = sigmoid(np.dot(layer_1,synapse_1))  
  55.         # did we miss?... if so by how much?
  56.         layer_2_error = y - layer_2  
  57.         layer_2_deltas.append((layer_2_error)*sigmoid_output_to_derivative(layer_2))  
  58.         overallError += np.abs(layer_2_error[0])  
  59.         # decode estimate so we can print it out
  60.         d[binary_dim - position - 1] = np.round(layer_2[0][0])  
  61.         # store hidden layer so we can use it in the next timestep
  62.         layer_1_values.append(copy.deepcopy(layer_1))  
  63.     future_layer_1_delta = np.zeros(hidden_dim)  
  64.     for position in range(binary_dim):  
  65.         X = np.array([[a[position],b[position]]])  
  66.         layer_1 = layer_1_values[-position-1]  
  67.         prev_layer_1 = layer_1_values[-position-2]  
  68.         # error at output layer
  69.         layer_2_delta = layer_2_deltas[-position-1]  
  70.         # error at hidden layer
  71.         layer_1_delta = (future_layer_1_delta.dot(synapse_h.T) + \  
  72.             layer_2_delta.dot(synapse_1.T)) * sigmoid_output_to_derivative(layer_1)  
  73.         # let's update all our weights so we can try again
  74.         synapse_1_update += np.atleast_2d(layer_1).T.dot(layer_2_delta)  
  75.         synapse_h_update += np.atleast_2d(prev_layer_1).T.dot(layer_1_delta)  
  76.         synapse_0_update += X.T.dot(layer_1_delta)  
  77.         future_layer_1_delta = layer_1_delta  
  78.     synapse_0 += synapse_0_update * alpha  
  79.     synapse_1 += synapse_1_update * alpha  
  80.     synapse_h += synapse_h_update * alpha      
  81.     synapse_0_update *= 0
  82.     synapse_1_update *= 0
  83.     synapse_h_update *= 0
  84.     # print out progress
  85.     if(j % 1000 == 0):  
  86.         print"Error:" + str(overallError)  
  87.         print"Pred:" + str(d)  
  88.         print"True:" + str(c)  
  89.         out = 0
  90.         for index,x in enumerate(reversed(d)):  
  91.             out += x*pow(2,index)  
  92.         print str(a_int) + " + " + str(b_int) + " = " + str(out)  
  93.         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:這是隱含層的大小,回來儲存“攜帶位”。需要注意的是,它的大小比原理上所需的要大。自己嘗試著調整一下這個值,然後看看它