概要
我通過玩具程式碼一邊學習一邊除錯能達到最好的學習效果。本文通過一個簡單的python實現,教會你迴圈神經網路。
原文作者@iamtrask說他會在twitter上繼續釋出第二部分LSTM,敬請關注。
廢話少說, 給我看看程式碼
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 |
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 #暫時製作256以內的加法, 可以調大 ## 以下5行程式碼計算0-256的二進位制表示 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 #因為我們是做兩個數相加,每次會餵給神經網路兩個bit,所以輸入的維度是2 hidden_dim = 16 #隱藏層的神經元節點數,遠比理論值要大(譯者注:理論上而言,應該一個節點就可以記住有無進位了,但我試了發現4的時候都沒法收斂),你可以自己調整這個數,看看調大了是容易更快地收斂還是更慢 output_dim = 1 #我們的輸出是一個數,所以維度為1 # initialize neural network weights synapse_0 = 2*np.random.random((input_dim,hidden_dim)) - 1 #輸入層到隱藏層的轉化矩陣,維度為2*16, 2是輸入維度,16是隱藏層維度 synapse_1 = 2*np.random.random((hidden_dim,output_dim)) - 1 synapse_h = 2*np.random.random((hidden_dim,hidden_dim)) - 1 # 譯者注:np.random.random產生的是[0,1)的隨機數,2 * [0, 1) - 1 => [-1, 1), # 是為了有正有負更快地收斂,這涉及到如何初始化引數的問題,通常來說都是靠“經驗”或者說“啟發式規則”,說得直白一點就是“蒙的”!機器學習裡面,超引數的選擇,大部分都是這種情況,哈哈。。。 # 我自己試了一下用【0, 2)之間的隨機數,貌似不能收斂,用[0,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 # 學習10000個例子 for j in range(100000): # 下面6行程式碼,隨機產生兩個0-128的數字,並查出他們的二進位制表示。為了避免相加之和超過256,這裡選擇兩個0-128的數字 # 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)) #一開始沒有隱藏層,所以裡面都是0 # 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]]])#從右到左,每次去兩個輸入數字的一個bit位 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)) #隱藏層 * 隱藏層到輸出層的轉化矩陣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]) #記錄下每一個預測bit位 # 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 # 我們已經完成了當前時間點的反向傳播誤差計算, 可以構建更新矩陣了。但是我們並不會現在就更新權重矩陣,因為我們還要用他們計算前一個時間點的更新矩陣呢。 # 所以要等到我們完成了所有反向傳播誤差計算, 才會真正的去更新權重矩陣,我們暫時把更新矩陣存起來。 # 可以看這裡瞭解更多關於反向傳播的知識http://iamtrask.github.io/2015/07/12/basic-python-network/ 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 ------------ |
第一部分:什麼是神經元記憶
順著背出字母表,你很容易做到吧?
倒著背呢, 有點難哦。
試著想一首你記得的歌詞。為什麼順著回憶比倒著回憶難?你能直接跳到第二小節的中間麼?額, 好像有點難。 這是為什麼呢?
這其實很符合邏輯。 你記憶字母表或者歌詞並不是像計算機把資訊儲存在硬碟上那樣的(譯者注:計算機可以隨機訪問磁碟。)。你是順序記憶的。知道了前一個字母,你很容易知道下一個。這是一種條件記憶,只有你最近知道了前一個記憶,你才容易想起來下一個記憶,就想你熟悉的連結串列一樣。
但是,並不是說你不唱歌的時候,歌就不在你腦子裡了。而是說你如果想直接跳到中間那部分,你會發現很難直接找到其在腦中的呈現(也許是一堆神經元)。你想直接搜尋到一首歌的中間部分,這是很難的, 因為你以前沒有這樣做過,所以沒有索引可以指向歌曲的中間部分。 就好比你鄰居家有很多小路, 你從前門進去順著路走很容易找到後院,但是讓你直接到後院去就不太容易。想了解更過關於大腦的知識,請看這裡。
跟連結串列很像,記憶這樣儲存很高效。我們可以發現這樣儲存在解決很多問題時候有優勢。
如果你的資料是一個序列,那麼記憶就很重要(意味著你必須記住某些東西)。看下面的視訊:
每一個資料點就是視訊中的一幀。如果你想訓練一個神經網路來預測下一幀小球的位置, 那麼知道上一幀小球的位置就很重要。這樣的序列資料就是我們需要構建迴圈神經網路的原因。那麼, 神經網路怎麼記住以前的資訊呢?
神經網路有隱藏層。一般而言,隱藏層的狀態由輸入決定。所以,一般而言神經網路的資訊流如下圖:
1 |
input -> hidden -> output |
這很簡單直接。特定的輸入決定特定的隱藏層,特定的隱藏層又決定了輸出。這是一種封閉系統。記憶改變了這種狀況。記憶意味著,隱藏狀態是由當前時間點的輸入和上一個時間點的隱藏狀態決定的。
1 |
(input + prev_hidden) -> hidden -> output |
為什麼是隱藏層而不是輸入層呢?我們也可以這樣做呀:
1 |
(input + prev_input) -> hidden -> output |
現在,仔細想想,如果有四個時間點,如果我們採用隱藏層迴圈是如下圖:
如果採用輸入層迴圈會是:
看到區別沒,隱藏層記憶了之前所有的輸入資訊,而輸入層迴圈則只能利用到上一個輸入。舉個例子,假設一首歌詞裡面有”….I love you…”和”…I love carrots…”,如果採用輸入層迴圈,則沒法根據”I love”來預測下一個詞是什麼?因為當前輸入是love,前一個輸入是I,這兩種情況一致,所以沒法區分。 而隱藏層迴圈則可以記住更久之前的輸入資訊,因而能更好地預測下一個詞。理論上而言,隱藏層迴圈可以記住所有之前的輸入,當然記憶會隨著時間流逝逐漸忘卻。有興趣的可以看這篇blog。
1 |
停下來好好想想, 直到你感覺想明白了再繼續。 |
第二部分:RNN – 神經網路記憶
現在我們已經有了一些直觀認識, 接下來讓我們更進一步分析。正如在反向傳播這篇blog裡介紹的,神經網路的輸入層是由輸入資料集決定的。每一行輸入資料用來產生隱藏層(通過正向傳播)。每個隱藏層又用於產生輸出層(假設只有一層隱藏層)。如我們之前所說,記憶意味著隱藏層是由輸入資料和前一次的隱藏層組合而成。怎麼做的呢?很像神經網路裡面其他傳播的做法一樣, 通過矩陣!這個矩陣定義了當前隱藏層跟前一個隱藏層的關係。
這幅圖中很重要的一點是有三個權重矩陣。有兩個我們很熟悉了。SYNAPSE_0用於把輸入資料傳播到隱藏層。SYNAPSE_1把隱藏層傳播到輸出資料。新矩陣(SYNAPSE_h,用於迴圈)把當前的隱藏層(layer_1)傳播到下一個時間點的隱藏層(還是layer_1)。
1 |
停下來好好想想, 直到你感覺想明白了再繼續。 |
上面的gif圖展示了迴圈神經網路的神奇之處以及一些很重要的性質。它展示了四個時間點隱藏層的情況。第一個時間點,隱藏層僅由輸入資料決定。第二個時間點,隱藏層是由輸入資料和第一個時間點的隱藏層共同決定的。以此類推。你應該注意到了,第四個時間點的時候,網路已經“滿了”。所以大概第五個時間點來的時候,就要選擇哪些記憶保留,哪些記憶覆蓋。現實如此。這就是記憶“容量”的概念。如你所想,更大的隱藏層,就能記住更長時間的東西。同樣,這就需要神經網路學會忘記不相關的記憶然後記住重要的記憶。第三步有沒看出什麼重要資訊?為什麼綠色的要比其他顏色的多呢?
另外要注意的是隱藏層夾在輸入層和輸出層中間,所以輸出已經不僅僅取決於輸入了。輸入僅僅改變記憶,而輸出僅僅依賴於記憶。有趣的是,如果2,3,4時間節點沒有輸入資料的話,隱藏層同樣會隨著時間流逝而變化。
1 |
停下來好好想想,確保你明白了剛講的內容。 |
第三部分:基於時間的反向傳播
那麼迴圈神經網路是怎麼學習的呢?看看下面的圖。黑色表示預測結果,明黃色表示錯誤,褐黃色表示導數。
網路通過從1到4的全部前向傳播(可以是任意長度的整個序列),然後再從4到1的反向傳播導數來學習。你可以把它看成一個有點變形的普通神經網路,除了我們在不同的地方共享權值(synapses 0,1,and h)。除了這點, 它就是一個普通的神經網路。
我們的玩具程式碼
來,我們用迴圈神經網路做個模型來實現二進位制加法。看到下面的圖沒,你猜猜頂上的彩色的1表示什麼意思呢?
方框裡的彩色的1表示進位。我們就要用迴圈神經網路來記住這個進位。求和的時候需要記住進位(如果不懂,可以看這裡)。
二進位制加法做法就是,從右往左,根據上面兩行的bit來預測第三行的bit為1還是0。我們想要神經網路遍歷整個二進位制序列記住是否有進位,以便能計算出正確的結果。不要太糾結這個問題本身,神經網路也不在乎這個問題。它在乎的只是每個時刻它會收到兩個輸入(0或者1),然後它會傳遞給用於記憶是否有進位的隱藏層。神經網路會把所有這些資訊(輸入和隱藏層的記憶)考慮進去,來對每一位(每個時間點)做出正確的預測。
下面原文裡面是針對每行程式碼做的註釋, 為了方便閱讀, 我直接把註釋寫到了程式碼裡面, 便於大家閱讀。
譯者注:RNN在自然語言處理裡面大量使用,包括機器翻譯,對話系統,機器做詩詞等,本文只是簡單介紹了一下原理。後續我會寫一些應用方面的文章,敬請期待。