Python寫出LSTM-RNN(Long-Short Term Memory Recurrent Neural Networks )的代碼

通過一個簡單的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*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 "------------"

運行輸出:

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
------------

第一部分:什麼是神經元記憶?

神經網絡有隱藏層,一般來講,隱藏層的狀態只跟輸入數據有關。所以一般來說一個神經網絡的信息流就會像下面所示的這樣:

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:這裏僅僅是一些輸出日誌,便於我們觀察中間的計算過程與效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章