Recurrent Neural Network系列2--利用Python,Theano实现RNN
作者:zhbzz2007 出處:http://www.cnblogs.com/zhbzz2007 歡迎轉載,也請保留這段聲明。謝謝!
本文翻譯自 RECURRENT NEURAL NETWORKS TUTORIAL, PART 2 – IMPLEMENTING A RNN WITH PYTHON, NUMPY AND THEANO 。
github地址
在這篇博文中,我們將會使用Python從頭開始實現一個循環神經網絡,并且利用Theano(一個在GPU上執行操作的庫)優化原始的實現。所有的代碼可以在github上獲得。我將會跳過一些不影響理解循環神經網絡的樣例代碼,所有這些代碼都在github上。
1 語言模型
我們的目的就是使用循環神經網絡建立語言模型 。假如我們有一個包含m個單詞的句子。語言模型允許我們預測在已知數據集中觀察到的句子的概率,如下所示,
\(P(w_{1},...,w_{m}) = \prod_{i=1}^{m}P(w_{i}|w_{1},..w_{i-1})\)
總之,一個句子的概率就是每個單詞在給定前面的單詞后的概率乘積。所以,句子“He went to buy some chocolate”就是給定“He went to buy some”的“chocolate”的概率 乘以 給定“He went to buy”的“some”的概率,等等。
為什么這樣就是有用的?為什么我們想要給一個觀測到的句子賦予概率?
首先,這類模型可以用作打分機制。例如,一個機器翻譯系統對于一個輸入句子通常會產生多個候選解。你可以使用語言模型來選擇最大可能的句子。直觀上,最大可能的句子有可能就是語法正確的。相似的打分在語音識別中也有。
在解決語言模型問題時,也有一個很酷的副產品。因為我們可以預測已知前面詞的詞的概率,我們也可以生成新的文本。這個就是生成式模型。給定詞的序列,我們從預測概率中采樣出下一個詞,重復這個過程直到我們得到整個句子。Andrej Karparthy的這篇博文很好的顯示了語言模型的能力。他的模型是通過單個字符而不是整個單詞進行訓練的,因此可以生成莎士比亞風格、Linux代碼等各種文本。
請注意,上述的公式中每個詞的概率是前面所有詞的條件概率。實際上,由于計算或者內存限制,許多模型很難表示這么長的依賴。通常的做法是限制到只看前面幾個詞。理論上,循環神經網絡可以獲取這么長的依賴,但是在實際中,這個有些復雜,我們將會在后續的博文中介紹。
2 訓練數據和預處理
為了訓練我們的語言模型,我們需要待學習的文本。幸運的是,我們訓練語言模型并不需要任何的有標簽數據,原始文本就夠用了。我從 Google's BigQuery 下載了15,000 條reddit評論數據。通過我們的模型生成的文本,看起來就像reddit的評論。和其它的機器學習項目一樣,我們首先對數據進行預處理,將其轉換為正確的格式。
2.1 分詞
我們手里現在是原始的文本,但是我們想在每個詞的基礎上進行預測。這就意味著我們需要將評論數據進行分句,然后再對句子進行分詞。我們可以通過空格將每條評論進行切分,但是這種方式并不能很好的處理標點符號。句子“He left!”應該是3個詞:“He”,“left”,“!”。我們將會使用nltk提供的方法word_tokenize和sent_tokenize來為我們做最為困難的任務,其中word_tokenize用于分詞,sent_tokenize用于分句。
2.2 去除低頻詞
文本中大部分詞只出現一次或者兩次。將這些低頻詞刪除,是個不錯的想法。一個巨大的詞匯表將會導致模型的訓練速度很慢(我們將會在后面解釋原因),并且我們針對這些低頻詞并沒有很多上下文,所以我們無法學習到如何正確的使用它們。這一點很類似于人類學習。為了真正的理解如何合適的使用一個詞,你需要在不同的上下文中看到它。
在我們的代碼中,我們將詞匯表限制為vocabulary_size個常用詞(這里我設置為8000,你也可以修改為其它值)。我們將沒有出現在詞匯表的其它詞都替換為 UNKNOWN_TOKEN。例如,如果我們的詞匯表沒有“nonlinearities”,那么句子“nonlineraties are important in neural networks”就會變為“UNKNOWN_TOKEN are important in neural networks”。詞 UNKNOWN_TOKEN將會是詞匯表的一部分,我們也會像其他詞一樣預測它。當我們生成新的文本時,我們可以再次將UNKNOWN_TOKEN替換掉,例如,對詞匯表之外的詞進行隨機采樣,或者我們可以只生成句子,直到我們得到一個不包含未知詞的句子。
2.3 準備特殊的開始和結束符
我們也想學習哪些詞趨向于句子的開始和結束。為了處理這個任務,我們在每個句子的開始加入了 SENTENCE_START 這個詞,在句子的結束加入了 SENTENCE_END 這個詞。這就會讓我們不禁想問:已知第一個詞是 SENTENCE_START ,那么下一個最有可能的詞(句子中第一個真實的詞)是什么?
2.4 建立訓練數據矩陣
循環神經網絡的輸入是向量,而非字符串。所以我們在詞和索引之間創建了一個映射,index_to_word 和 word_to_index。例如,詞“friendly”的索引或許是2001。一個訓練例子 x 或許是[0,179,341,416],0對應 SENTENCE_START 。相應的標簽y就是[179,341,416,1]。請記住,我們的目標是預測下一個詞,所以 y 就是向量 x 左移一位,最后一個元素是 SENTENCE_END。也就是說,詞 179 的正確預測就是 341,也就是真實的下一個詞。
代碼如下,
vocabulary_size = 8000 unknown_token = "UNKNOWN_TOKEN" sentence_start_token = "SENTENCE_START" sentence_end_token = "SENTENCE_END"# Read the data and append SENTENCE_START and SENTENCE_END tokens print "Reading CSV file..." with open('data/reddit-comments-2015-08.csv', 'rb') as f:reader = csv.reader(f, skipinitialspace=True)reader.next()# Split full comments into sentencessentences = itertools.chain(*[nltk.sent_tokenize(x[0].decode('utf-8').lower()) for x in reader])# Append SENTENCE_START and SENTENCE_ENDsentences = ["%s %s %s" % (sentence_start_token, x, sentence_end_token) for x in sentences] print "Parsed %d sentences." % (len(sentences))# Tokenize the sentences into words tokenized_sentences = [nltk.word_tokenize(sent) for sent in sentences]# Count the word frequencies word_freq = nltk.FreqDist(itertools.chain(*tokenized_sentences)) print "Found %d unique words tokens." % len(word_freq.items())# Get the most common words and build index_to_word and word_to_index vectors vocab = word_freq.most_common(vocabulary_size-1) index_to_word = [x[0] for x in vocab] index_to_word.append(unknown_token) word_to_index = dict([(w,i) for i,w in enumerate(index_to_word)])print "Using vocabulary size %d." % vocabulary_size print "The least frequent word in our vocabulary is '%s' and appeared %d times." % (vocab[-1][0], vocab[-1][1])# Replace all words not in our vocabulary with the unknown token for i, sent in enumerate(tokenized_sentences):tokenized_sentences[i] = [w if w in word_to_index else unknown_token for w in sent]print "\nExample sentence: '%s'" % sentences[0] print "\nExample sentence after Pre-processing: '%s'" % tokenized_sentences[0]# Create the training data X_train = np.asarray([[word_to_index[w] for w in sent[:-1]] for sent in tokenized_sentences]) y_train = np.asarray([[word_to_index[w] for w in sent[1:]] for sent in tokenized_sentences])下面是文本中真實的訓練示例,
x: SENTENCE_START what are n't you understanding about this ? ! [0, 51, 27, 16, 10, 856, 53, 25, 34, 69]y: what are n't you understanding about this ? ! SENTENCE_END [51, 27, 16, 10, 856, 53, 25, 34, 69, 1]3 建立循環神經網絡
如果你想對循環神經網絡有一個整體的了解,可以閱讀 Recurrent Neural Network系列1--RNN(循環神經網絡)概述 這篇博文。
讓我們看看用于訓練語言模型的循環神經網絡具體是什么樣子。輸入 x 是一個詞的序列(就像上面例子的輸出) ,每個 \(x_{t}\) 是一個單獨的詞。這里還需要注意一件事:由于矩陣相乘,我們不能僅僅使用詞的索引作為輸入。我們將詞表示為一個one-hot向量,向量維度為 vocabulary_size 。例如,表示索引為36的詞的向量,所有位置均為0,只有位置36為1。這樣,每個 \(x_{t}\) 將會是一個向量,x將會是一個矩陣,每一行表示一個詞。沒有在預處理階段處理,我們將會在神經網絡的代碼中做這個變換。網絡的輸出 o 有相似的格式。每個 \(o_{t}\) 是一個 vocabulary_size 大小的向量,每個元素表示這個詞成為句子中下一個詞的概率。
我們從第一篇博文中得到循環神經網絡的公式,如下所示:
\(s_{t} = tanh(U x_{t} + W s_{t-1})\)
\(o_{t} = softmax(V s_{t})\)
我發現將矩陣和向量的維度寫出來很有用。假設,我們設置詞匯表大小 C = 8000,隱藏層大小 H = 100。你可以將隱藏層大小認為是網絡的記憶。讓它更大,可以允許我們學習到更加復雜的模式,當然這也會導致額外的計算量。我們將會得到各個矩陣和向量的維度,如下所示,
\(x_{t} \in R^{8000}\)
\(o_{t} \in R^{8000}\)
\(s_{t} \in R^{100}\)
\(U \in R^{100 * 8000}\)
\(V \in R^{8000 * 100}\)
\(W \in R^{100 * 100}\)
這是非常有價值的信息。U,V和W就是我們從數據中學習得到的網絡參數。因此,我們總共需要學習 \(2*H*C + H^{2}\) 個參數。在我們這里,C = 8000,H = 100,總共參數的數量是161萬。這個維度也告訴我們模型的瓶頸在哪。由于 \(x_{t}\) 是一個one-hot向量,將它與 U 進行相乘,就等于選擇 U 的某一列,所以我們并不需要執行全部的乘法。網絡中最大的矩陣相乘是 \(Vs_{t}\) 。這也是我們盡可能的讓詞匯表比較小的原因了。
有了這些,是時候開始我們的實現。
3.1 初始化
我們首先定義RNN類用于初始化我們的參數。我在這里稱這個類為RNNNumpy,因為我們將會在后續實現一個基于Theano版本的。初始化U、V、W會有些麻煩。我們不能簡單地將它們都初始化為0,因為這樣就會導致所有層出現對稱的計算。我們必須隨機初始化。因為合理的初始化似乎會對訓練結果有影響(這個領域已經有很多研究了)。最終發現,最好的初始化依賴于激活函數(我們的例子是tanh),有一個推薦的初始化方法就是在區間 \([-\frac{1}{\sqrt{n}},\frac{1}{\sqrt{n}}]\) 內初始化權重,這里n是上一層的輸入鏈接數目。這個聽起來似乎有些復雜,但是不要太過于擔心。只要你將參數初始化為一個小的隨機值,通常都可以正常的運行。
代碼如下,
class RNNNumpy:def __init__(self, word_dim, hidden_dim=100, bptt_truncate=4):# Assign instance variablesself.word_dim = word_dimself.hidden_dim = hidden_dimself.bptt_truncate = bptt_truncate# Randomly initialize the network parametersself.U = np.random.uniform(-np.sqrt(1./word_dim), np.sqrt(1./word_dim), (hidden_dim, word_dim))self.V = np.random.uniform(-np.sqrt(1./hidden_dim), np.sqrt(1./hidden_dim), (word_dim, hidden_dim))self.W = np.random.uniform(-np.sqrt(1./hidden_dim), np.sqrt(1./hidden_dim), (hidden_dim, hidden_dim))上面代碼中, word_dim 就是我們的詞匯表大小,hidden_dim 是隱層的數目。現在不要擔心參數 bptt_truncate ,我們將會在后續講解。
3.2 前向傳播
現在讓我們按照上述定義的公式,實現前向傳播過程(預測詞的概率)。
代碼如下,
def forward_propagation(self, x):# The total number of time stepsT = len(x)# During forward propagation we save all hidden states in s because need them later.# We add one additional element for the initial hidden, which we set to 0s = np.zeros((T + 1, self.hidden_dim))s[-1] = np.zeros(self.hidden_dim)# The outputs at each time step. Again, we save them for later.o = np.zeros((T, self.word_dim))# For each time step...for t in np.arange(T):# Note that we are indxing U by x[t]. This is the same as multiplying U with a one-hot vector.s[t] = np.tanh(self.U[:,x[t]] + self.W.dot(s[t-1]))o[t] = softmax(self.V.dot(s[t]))return [o, s]RNNNumpy.forward_propagation = forward_propagation我們不僅僅返回輸出值,也將隱藏狀態返回。我們將會在計算梯度的時候用到隱藏狀態,將隱藏狀態返回,可以避免后續的重復計算。每個 \(o_{t}\) 是一個概率向量,用于表示詞匯表中的詞,但是有時候,例如,當評估模型時,我們想要的是下一個詞要有最高的概率。我們會調用predict函數,代碼如下,
def predict(self, x):# Perform forward propagation and return index of the highest scoreo, s = self.forward_propagation(x)return np.argmax(o, axis=1)RNNNumpy.predict = predict讓我們嘗試新實現的方法,看下一個例子的輸出,代碼如下,
np.random.seed(10) model = RNNNumpy(vocabulary_size) o, s = model.forward_propagation(X_train[10]) print o.shape print o輸出結果如下,
(45, 8000) [[ 0.00012408 0.0001244 0.00012603 ..., 0.00012515 0.000124880.00012508][ 0.00012536 0.00012582 0.00012436 ..., 0.00012482 0.000124560.00012451][ 0.00012387 0.0001252 0.00012474 ..., 0.00012559 0.000125880.00012551]...,[ 0.00012414 0.00012455 0.0001252 ..., 0.00012487 0.000124940.0001263 ][ 0.0001252 0.00012393 0.00012509 ..., 0.00012407 0.000125780.00012502][ 0.00012472 0.0001253 0.00012487 ..., 0.00012463 0.000125360.00012665]]對于句子中的每個詞(上述例子是45個詞),我們的模型會有8000個預測用于表示下一個詞的概率。請注意,因為我們是隨機初始化U、V、W,所以目前這些預測也是完全的隨機值。下面代碼給出了每個詞的最高概率的索引,代碼如下,
predictions = model.predict(X_train[10]) print predictions.shape print predictions輸出結果如下,
(45,) [1284 5221 7653 7430 1013 3562 7366 4860 2212 6601 7299 4556 2481 238 253921 6548 261 1780 2005 1810 5376 4146 477 7051 4832 4991 897 3485 217291 2007 6006 760 4864 2182 6569 2800 2752 6821 4437 7021 7875 6912 3575]3.3 計算損失
為了訓練我們的網絡,我們需要一種方式來度量網絡的誤差。我們稱之為損失函數L,我們的目標就是找出參數U、V、W,使得損失函數在訓練數據上最小。通常,損失函數選擇為 互熵損失 。如果我們有N條訓練樣本數據(語料中的詞)和C個類(詞匯表的大小),預測值是 o ,真實值 y ,那么損失函數為:
\(L(y,o) = -\frac{1}{N}\sum_{m \in N}y_{n}log(o_{n})\)
公式看起來有些復雜,它所做的其實就是基于預測值,將所有訓練樣本的損失相加。我們將在 calculate_loss 函數中實現。
def calculate_total_loss(self, x, y):L = 0# For each sentence...for i in np.arange(len(y)):o, s = self.forward_propagation(x[i])# We only care about our prediction of the "correct" wordscorrect_word_predictions = o[np.arange(len(y[i])), y[i]]# Add to the loss based on how off we wereL += -1 * np.sum(np.log(correct_word_predictions))return Ldef calculate_loss(self, x, y):# Divide the total loss by the number of training examplesN = np.sum((len(y_i) for y_i in y))return self.calculate_total_loss(x,y)/NRNNNumpy.calculate_total_loss = calculate_total_loss RNNNumpy.calculate_loss = calculate_loss讓我們想想對于隨機預測,損失函數的值為多少。這個將會給我們確立一個基礎,可以保證我們的實現是正確的。我們在詞匯表中有C個詞,所以對于每個詞應該(平均)的預測概率 是 \(\frac{1}{C}\) ,損失值為 \(L = -\frac{1}{N}Nlog(\frac{1}{C})=logC\) 。
# Limit to 1000 examples to save time print "Expected Loss for random predictions: %f" % np.log(vocabulary_size) print "Actual loss: %f" % model.calculate_loss(X_train[:1000], y_train[:1000])輸出為,
Expected Loss for random predictions: 8.987197 Actual loss: 8.987440這兩個值非常接近。請注意,在整個數據集上評估損失需要很大的計算量,如果你的數據很多,可能需要花費數小時。
3.4 利用SGD訓練RNN以及BPTT
請記住,我們想要尋找參數U、V和W,使得訓練數據上的損失函數最小。最常用的方法就是SGD,隨機梯度下降。SGD背后的思想很簡單。在所有訓練數據上迭代,在每次迭代中,我們朝誤差減少的方向調整參數。這個方法就是損失函數在這些參數上的梯度, \(\frac{\partial L}{\partial U}\),\(\frac{\partial L}{\partial V}\),\(\frac{\partial L}{\partial W}\) 。SGD也需要一個學習率,決定了我們想在每次迭代中想要多大的步伐。SGD是最流行的優化方法,不僅僅用于神經網絡,也可用于許多其它的機器學習方法。目前有很多研究去優化SGD,例如,使用批處理,并行以及自適應學習率。即使基本思想很簡單,但是想實現一個高效的SGD,依然比較復雜。如果你想了解更多SGD的知識,可以看 這篇文章 。由于它的流行,網絡上有很多的教程,在此,我并不想重復。我將會實現一個SGD的簡單版本,即使沒有優化方面的背景知識,依然可以理解。
我們如何計算上述提到的梯度呢?在 傳統的神經網絡中 , 我們通過反向傳播算法來計算。在循環神經網絡中,我們使用這個算法的簡單修改后的版本,稱之為基于時間的反向傳播算法(BPTT)。由于網絡中這些參數在所有時刻是共享的,每個輸出的梯度不僅依賴當前時刻的計算,也依賴之前所有時刻。如果你了解微積分,它就是鏈式法則的應用。下一篇文章就是關于BPTT的,所以我在這里暫時先不講。關于反向傳播,如果想要繼續了解,可以看這兩篇文章: Calculus on Computational Graphs: Backpropagation 和 CS231n Convolutional Neural Networks for Visual Recognition ?,F在你可以將BPTT視為一個黑盒。它將訓練示例(x,y)作為輸入,輸出梯度:\(\frac{\partial L}{\partial U}\),\(\frac{\partial L}{\partial V}\),\(\frac{\partial L}{\partial W}\) 。
代碼如下,
def bptt(self, x, y):T = len(y)# Perform forward propagationo, s = self.forward_propagation(x)# We accumulate the gradients in these variablesdLdU = np.zeros(self.U.shape)dLdV = np.zeros(self.V.shape)dLdW = np.zeros(self.W.shape)delta_o = odelta_o[np.arange(len(y)), y] -= 1.# For each output backwards...for t in np.arange(T)[::-1]:dLdV += np.outer(delta_o[t], s[t].T)# Initial delta calculationdelta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2))# Backpropagation through time (for at most self.bptt_truncate steps)for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:# print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step)dLdW += np.outer(delta_t, s[bptt_step-1]) dLdU[:,x[bptt_step]] += delta_t# Update delta for next stepdelta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2)return [dLdU, dLdV, dLdW]RNNNumpy.bptt = bptt3.5 梯度檢查
當你實現反向傳播算法時,最好也要實現梯度檢查,這是一種確認你的實現是否正確的方式。梯度檢查的思想就是參數的微分等于那個點的斜率,我們通過輕微地改變這個參數,然后除以這個變化來近似這個斜率:
\(\frac{\partial L}{\partial \theta} \approx lim_{h->0}\frac{J(\theta + h) - J(\theta - h)}{2h}\)
我們將會比較通過反向傳播計算的梯度和上述方法估計的梯度。如果二者之間沒有太大的差異,就說明我們的實現是正確的。需要計算損失函數在所有的參數上的近似,所以梯度檢查計算量很大(請記住,我們在這個例子中有超過一百萬的參數)。所以最好在一個小詞匯表上的模型來執行梯度檢查。
代碼如下,
def gradient_check(self, x, y, h=0.001, error_threshold=0.01):# Calculate the gradients using backpropagation. We want to checker if these are correct.bptt_gradients = self.bptt(x, y)# List of all parameters we want to check.model_parameters = ['U', 'V', 'W']# Gradient check for each parameterfor pidx, pname in enumerate(model_parameters):# Get the actual parameter value from the mode, e.g. model.Wparameter = operator.attrgetter(pname)(self)print "Performing gradient check for parameter %s with size %d." % (pname, np.prod(parameter.shape))# Iterate over each element of the parameter matrix, e.g. (0,0), (0,1), ...it = np.nditer(parameter, flags=['multi_index'], op_flags=['readwrite'])while not it.finished:ix = it.multi_index# Save the original value so we can reset it lateroriginal_value = parameter[ix]# Estimate the gradient using (f(x+h) - f(x-h))/(2*h)parameter[ix] = original_value + hgradplus = self.calculate_total_loss([x],[y])parameter[ix] = original_value - hgradminus = self.calculate_total_loss([x],[y])estimated_gradient = (gradplus - gradminus)/(2*h)# Reset parameter to original valueparameter[ix] = original_value# The gradient for this parameter calculated using backpropagationbackprop_gradient = bptt_gradients[pidx][ix]# calculate The relative error: (|x - y|/(|x| + |y|))relative_error = np.abs(backprop_gradient - estimated_gradient)/(np.abs(backprop_gradient) + np.abs(estimated_gradient))# If the error is to large fail the gradient checkif relative_error > error_threshold:print "Gradient Check ERROR: parameter=%s ix=%s" % (pname, ix)print "+h Loss: %f" % gradplusprint "-h Loss: %f" % gradminusprint "Estimated_gradient: %f" % estimated_gradientprint "Backpropagation gradient: %f" % backprop_gradientprint "Relative Error: %f" % relative_errorreturnit.iternext()print "Gradient check for parameter %s passed." % (pname)RNNNumpy.gradient_check = gradient_check# To avoid performing millions of expensive calculations we use a smaller vocabulary size for checking. grad_check_vocab_size = 100 np.random.seed(10) model = RNNNumpy(grad_check_vocab_size, 10, bptt_truncate=1000) model.gradient_check([0,1,2,3], [1,2,3,4])3.6 實現SGD
現在我們通過SGD計算參數的梯度。我通常按照兩步來進行:1、定義函數sgd_step,用于計算一個批處理中的梯度并更新;2、外層循環用于在所有訓練集上迭代并調整學習率。
用于計算一個批處理中的梯度并更新的代碼如下,
# Performs one step of SGD. def numpy_sdg_step(self, x, y, learning_rate):# Calculate the gradientsdLdU, dLdV, dLdW = self.bptt(x, y)# Change parameters according to gradients and learning rateself.U -= learning_rate * dLdUself.V -= learning_rate * dLdVself.W -= learning_rate * dLdWRNNNumpy.sgd_step = numpy_sdg_step外層循環的代碼如下,
# Outer SGD Loop # - model: The RNN model instance # - X_train: The training data set # - y_train: The training data labels # - learning_rate: Initial learning rate for SGD # - nepoch: Number of times to iterate through the complete dataset # - evaluate_loss_after: Evaluate the loss after this many epochs def train_with_sgd(model, X_train, y_train, learning_rate=0.005, nepoch=100, evaluate_loss_after=5):# We keep track of the losses so we can plot them laterlosses = []num_examples_seen = 0for epoch in range(nepoch):# Optionally evaluate the lossif (epoch % evaluate_loss_after == 0):loss = model.calculate_loss(X_train, y_train)losses.append((num_examples_seen, loss))time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')print "%s: Loss after num_examples_seen=%d epoch=%d: %f" % (time, num_examples_seen, epoch, loss)# Adjust the learning rate if loss increasesif (len(losses) > 1 and losses[-1][1] > losses[-2][1]):learning_rate = learning_rate * 0.5print "Setting learning rate to %f" % learning_ratesys.stdout.flush()# For each training example...for i in range(len(y_train)):# One SGD stepmodel.sgd_step(X_train[i], y_train[i], learning_rate)num_examples_seen += 1讓我們嘗試一下,在我們的網絡上訓練所需的時間,代碼如下,
np.random.seed(10) model = RNNNumpy(vocabulary_size) %timeit model.sgd_step(X_train[10], y_train[10], 0.005)在我的筆記本上,SGD一步需要將近350毫秒。我們的訓練數據大概有80,000個樣例,所以一個更新周期(在整個數據集上全部迭代一次)將會花費數小時。多個更新周期將會花費數天,甚至數周。我們依然在一個小數據集進行驗證,正如許多公司和研究員也是這樣做的。
幸運的是目前已經有很多方式用于加速我們的代碼。我們將會堅持相同的模型并且讓代碼運行更快,或者修改我們的代碼降低計算量,或者這兩種方法都用。研究人員已經研究出很多方式用于降低模型的計算量,例如,使用層次softmax或者增加投射層以避免大的矩陣相乘(可以看這兩篇文章 Efficient Estimation of Word Representations in Vector Space 和 EXTENSIONS OF RECURRENT NEURAL NETWORK LANGUAGE MODEL) 。但是我在這里想保持模型的 簡單,就選擇了第一種方式:使用GPU使得代碼運行更加快速。在做這些之前,讓我們僅僅在小數據集上運行SGD,然后檢查損失是否降低。
代碼如下,
np.random.seed(10) # Train on a small subset of the data to see what happens model = RNNNumpy(vocabulary_size) losses = train_with_sgd(model, X_train[:100], y_train[:100], nepoch=10, evaluate_loss_after=1)輸出如下,
2017-01-16 17:42:39: Loss after num_examples_seen = 0 epoch = 0: 8.987425 2017-01-16 17:42:50: Loss after num_examples_seen = 100 epoch = 1: 8.976270 2017-01-16 17:43:01: Loss after num_examples_seen = 200 epoch = 2: 8.960212 2017-01-16 17:43:12: Loss after num_examples_seen = 300 epoch = 3: 8.930430 2017-01-16 17:43:22: Loss after num_examples_seen = 400 epoch = 4: 8.862264 2017-01-16 17:43:33: Loss after num_examples_seen = 500 epoch = 5: 6.913570 2017-01-16 17:43:44: Loss after num_examples_seen = 600 epoch = 6: 6.302493 2017-01-16 17:43:54: Loss after num_examples_seen = 700 epoch = 7: 6.014995 2017-01-16 17:44:05: Loss after num_examples_seen = 800 epoch = 8: 5.833877 2017-01-16 17:44:16: Loss after num_examples_seen = 900 epoch = 9: 5.710718看起來我們的實現最起碼是有用的,并且降低了損失,正如我們所期待的。
3.7 使用Theano和GPU訓練網絡
我已經在前面的文章 SPEEDING UP YOUR NEURAL NETWORK WITH THEANO AND THE GPU 中介紹了Theano,現在我們的邏輯依然不變,我們在這里并不會繼續優化代碼。我定義了 RNNTheano 這個類,使用Theano的計算來替換numpy的計算。正如這篇文章的剩余部分,github代碼鏈接 在此。
代碼如下,
np.random.seed(10) model = RNNTheano(vocabulary_size) %timeit model.sgd_step(X_train[10], y_train[10], 0.005)這一次,SGD在沒有GPU的Mac上一步需要70毫秒,在亞馬遜的帶有GPU 的EC2上一步需要23毫秒。相比于剛開始的實現,速度上有著15倍的提升,這意味著我們可以在數小時/天而非數周來訓練模型。這里依然有很多優化可以做,我們認為已經足夠了。
為了讓你避免花費數天來訓練一個模型,我已經訓練好一個Theano模型,隱層數為50,詞匯表大小為8000。我訓練了50個周期,大概20個小時。損失依然在降低,訓練更久或許會產生更好的模型。你可以訓練的更久一些。你可以在Github代碼倉庫中的data/trained-model-theano.npz中找到模型的參數,使用load_model_parameters_theano用于加載這個模型。
代碼如下,
from utils import load_model_parameters_theano, save_model_parameters_theanomodel = RNNTheano(vocabulary_size, hidden_dim=50) # losses = train_with_sgd(model, X_train, y_train, nepoch=50) # save_model_parameters_theano('./data/trained-model-theano.npz', model) load_model_parameters_theano('./data/trained-model-theano.npz', model)3.8 文本生成
現在我們已經得到了模型,我們可以讓它為我們生成新的文本。讓我們實現一個生成新句子的函數,代碼如下,
def generate_sentence(model):# We start the sentence with the start tokennew_sentence = [word_to_index[sentence_start_token]]# Repeat until we get an end tokenwhile not new_sentence[-1] == word_to_index[sentence_end_token]:next_word_probs = model.forward_propagation(new_sentence)sampled_word = word_to_index[unknown_token]# We don't want to sample unknown wordswhile sampled_word == word_to_index[unknown_token]:samples = np.random.multinomial(1, next_word_probs[-1])sampled_word = np.argmax(samples)new_sentence.append(sampled_word)sentence_str = [index_to_word[x] for x in new_sentence[1:-1]]return sentence_strnum_sentences = 10 senten_min_length = 7for i in range(num_sentences):sent = []# We want long sentences, not sentences with one or two wordswhile len(sent) == senten_min_length:sent = generate_sentence(model)print " ".join(sent)這里需要將代碼
next_word_probs = model.forward_propagation(new_sentence)修改為如下代碼,具體原因見 error: multinomial in generate_sentence of RNNLM.ipynb #11 。
next_word_probs,_ = model.forward_propagation(new_sentence)生成的句子如下,
un the at enough actors the a . pulse electric and the the really strict wednesday a the the that out so on donation zip aware at kill had simply ; , players idea answering that your were is current| giveaway the . . alongside granted at to that the is take , is there ideas of tricks arena the and ? the i do the sound , like even , and my had in the 's good the allowing ] ; the ; that . is going the one the charge pill sellers somewhat i out you back do this logic officer or bags secondly n't think pills is a end general horrendous the `` would really if as bros 's charge which such . time that that jazz constant parody the but blatantly the ready gt they for said or be her is and ourselves regard is really she internet be dot unit what was . inner rolls some think point ? ; . who the all be matchups having echo plan do sword players the a and i and the . tower i rate pre have private be the put the last think for parts life played me and the of ( . was other the think on be the : lies working the put . bc 30 be who would the the of genetics your were would is fade the . nervous or than be powered shown .對于生成的句子,有一些有意思的事情需要注意。模型很成功地學習到了語法。它合適的放置逗號(通常在and's和or's之前)以及使用標點符號結束句子。有時,它模仿網絡用語,例如多個感嘆號或者笑臉。
但是,很多生成的句子并沒有實際意義或者有語法錯誤(我在上面僅僅是選出最好的結果)。一個原因就是我們并沒有對我們的網絡訓練足夠長的時間(或者并沒有使用足夠的訓練數據)。這或許是對的,但這并非主要的原因。普通的循環神經網絡不能產生有意義的文本,是因為它不能學習到相隔數步的詞之間的依賴。這也是循環神經網絡第一次被提出來之后卻不流行的原因。理論上很漂亮,但是實際中效果并不好,我們也無法理解為什么。
幸運的是,訓練循環神經網絡的困難現在 很容易理解 。在下一篇文章中,我們將會探索BPTT算法更多的細節并展示什么是梯度損失問題。這將會激發我們選擇更加復雜的循環神經網絡模型,例如LSTM,目前在很多NLP任務(包括產生更好的reddit評論)是最先進的方法。你在這篇文章所學的知識都可以應用到LSTM和其它的循環神經網絡模型,所以當普通循環神經網絡的結果沒有達到你的期望的時候,不要氣餒。
Reference
RECURRENT NEURAL NETWORKS TUTORIAL, PART 2 – IMPLEMENTING A RNN WITH PYTHON, NUMPY AND THEANO
轉載于:https://www.cnblogs.com/zhbzz2007/p/6291652.html
總結
以上是生活随笔為你收集整理的Recurrent Neural Network系列2--利用Python,Theano实现RNN的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux中rpm命令管理
- 下一篇: Linux下配置汇编编译器NASM和bo