前言
學習RNN的時候很多人應該都有看過Andrej Karpathy寫的The Unreasonable Effectiveness of Recurrent Neural Networks,使用基於字符粒度的RNN讓機器學會自己生成文本,比如令自己訓練的RNN學會寫歌詞、寫代碼、寫小說、寫詩,聽着就很新奇。
github上雖然已經有實現好的Char RNN,比如
但是想要學習,最好的方式就是自己動手實現一遍。自己寫一遍好處還是很多的,比如加深對RNN(LSTM)的理解,可以熟悉深度學習的框架。因為我主要用tensorflow,所以就基於tensorflow實現了一遍Char-RNN。
注:本文使用的tensorflow版本為1.0.0
個人經驗,在實現的過程中最好是拋開別人代碼的影響,只根據基本理論以及所用的框架的API文檔一步步把代碼寫出來跑通,這樣自己的收益才是最大的。
模型選擇
要讓機器生成文本,本質上是需要一個語言模型。語言模型可以用來評估一句話是自然語言的概率,即根據一句話中已觀測到的詞,預測下一個詞出現的概率。也就是要能夠處理序列數據,根據已有的序列數據,推斷接下來可能的數據。如一句話“已經到了午餐時間,我正准備去吃{?}”,根據前面的描述,可以推斷“吃”字背后是要接上可食用的東西,並且是可以作為午餐的,可能是“飯”、“面”等等,通常不可能是“汽車”、“樹木”之類…因此我們需要一個能夠處理序列數據,並且能夠抽象出過去序列與任務相關方面的信息,再根據這些信息預測未來的模型。
神經網絡中,RNN天然適合用於處理序列數據,它可以提取任意長度序列(x(t),x(t−1),...,x(1))的摘要,選擇性地精確保留過去序列的某些方面。而保留這些信息的方式則是通過RNN內部的隱藏狀態。
但是RNN又有很多變體,因為基本RNN只有一個隱藏狀態,對長距離的記憶效果不好,在模型參數迭代優化的時候存在梯度彌散的問題,因此又有了采用LSTM單元的RNN以及其他的變體,如GRU等等。
因此,在Char RNN的實踐當中,就選用LSTM作為基本的模型。
因為tensorflow中已經實現了LSTM的單元,如果不是為了學習LSTM的原理,可以不需要自己去實現它。相應的API為
模型定義
我們需要定義一個class用來定義網絡的結構,以及實現inference的接口。如果初次接觸RNN,剛開始動手寫的時候可能會一頭霧水,我們已經有了LSTM的API,怎么把它拓展成可以接受文本的訓練數據進行訓練,最后再根據輸入的一些文字,輸出接下來文字的模型呢?
我的做法是先明確輸入與輸出,以及我所知道的必備要素,然后再把它們銜接拼湊起來。
基本LSTM單元
首先我們要用到LSTMCell,它的必填參數是num_units,也就是每個LSTM Cell中的單元數,與輸入向量的維度是一致的。我們的輸入是詞向量,維度是我們自己定義的,這里用一個參數rnn_size來表示。定義基本LSTM Cell的代碼如下
# 定義基本lstm單元
lstm_cell_list = [tf.contrib.rnn.LSTMCell(rnn_size) for _ in xrange(layer_size)]
# 使用MultiRNNCell 接口連接多層lstm, 並加上dropout
self.cell = tf.contrib.rnn.DropoutWrapper(tf.contrib.rnn.MultiRNNCell(lstm_cell_list), output_keep_prob=output_keep_prob)
明確輸入
在訓練的過程中,每次都feed進一個batch的數據,batch的大小也是我們定義的,用batch_size表示,因此LSTM模型所接受輸入的shape為(batch_size, rnn_size)。
如果我們使用預訓練好的詞向量作為輸入,那么這里就可以寫成
tf.input_data = tf.placeholder(tf.float32, shape=[batch_size, rnn_size], name='input_data')
但我們希望詞向量可以在train的過程中被改變,更適應我們的訓練數據。那就要用Variable來表示詞向量矩陣。因此我們要定義一個變量來表示,詞向量矩陣的維度應該是 vocab_size * rnn_size。 即每一行代表一個詞,列數就是我們需要自己定義的詞向量維度。定義了詞向量矩陣的變量,每次輸入的時候,還需要為輸入的詞找到對應的詞向量,這些tensorflow都為我們封裝好了,代碼如下
embedding = tf.Variable(tf.truncated_normal([vocab_size, rnn_size], stddev=0.1), name='embedding')
inputs = tf.nn.embedding_lookup(embedding, self.input_data)
tf.nn.embedding_lookup這個函數就是用於返回所查找的詞向量Tensor的。
embedding_lookup(params, ids, partition_strategy=’mod’, name=None, validate_indices=True, max_norm=None)
其中params是詞向量矩陣,ids是需要需要查找的詞的id。舉個簡單的例子如下
# 假設有詞向量空間x
x = [[1.0,2.0,3.0],[4.0,5.0,6.0],[7.0,8.0,9.0]]
vx = tf.Variable(x, name='vx')
ids = tf.placeholder(tf.int32, name='ids')
inputs = tf.nn.embedding_lookup(vx, ids)
# 假如每個batch有3個句子,每個句子有兩個詞,詞的id如下
input_data = [[0,1],[1,2],[0,2]]
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
sess.run(inputs, feed_dict={ids:input_data})
# 輸出結果如下
>>> array([[[ 1., 2., 3.],
[ 4., 5., 6.]],
[[ 4., 5., 6.],
[ 7., 8., 9.]],
[[ 1., 2., 3.],
[ 7., 8., 9.]http://www.ysbyl.biz/]], dtype=float32)
輸出結果的shape為(3,2,3)
用上述方式就可以查出來一個batch中每個句子的每個詞對應的詞向量。所以我們原始輸入的batch中,每個元素是一個sequence,sequence中的元素又是每個詞對應的id。
這部分的完整代碼如下
self.input_data = tf.placeholder(tf.int32, shape=[batch_size, sequence_length], name='input_data')
# 指定這部分使用CPU進行計算
with tf.device('/cpu:0'):
embedding = tf.Variable(tf.truncated_normal([vocab_size, rnn_size], stddev=0.1), name='embedding')
inputs = tf.nn.embedding_lookup(embedding, self.input_data)
明確輸出
因為在Char RNN中,每一時刻的輸出都是下一時刻的輸入,因此LSTM的輸出ot與輸入xt維度是一樣的。但ot並不是Char RNN模型的輸出,ot之后還需要跟全連接層以及softmax層來判斷每個詞出現的概率。每一時刻都有一個輸出,在訓練的階段,需要收集每一時刻的輸出,以便與targets進行比較來計算loss。因此需要有一個循環來展開整個lstm。展開的這部分tensorflow也有API可以調用,但是為了更好的理解,還是自己實現一遍比較好。代碼如下
# 定義初始狀態
self.initial_state = self.cell.zero_state(batch_size, tf.float32)
with tf.variable_scope('RNN'):
for time_step in xrange(sequence_length):
# 因為LSTM Cell調用__call__(027yeshenghuowang.com)方法時,會使用到get_variable()獲取內部變量
# 如果reuse的flag是False,調用get_variable()后會查找該variable_scope中有沒有重名的變量,如果有就報錯
# 如果reuse的flag是True,調用get_variable()后則是在當前的variable_scope找不到變量時報錯
# 因此在這部分需要reuse的時候要定義一個variable_scope,否則之后想用get_variable()定義新變量都會報錯
if time_step > 0:
tf.get_variable_scope().reuse_variables()
if time_step == 0:
output, state = self.cell(inputs[:, time_step, :], self.initial_state)
else:
output, state = self.cell(inputs[:, time_step, :], state)
outputs.append(output)
self.final_state = state
softmax_w = tf.Variable(tf.truncated_normal([rnn_size, vocab_size], stddev=0.1), name='softmax_w')
softmax_b = tf.Variable(tf.zeros([vocab_size]www.22yigouyule.cn/), name='softmax_b')
# 執行完循環以后,outputs的shape=(sequence_length, batch_size, rnn_size)
# 而matmul接受的矩陣的rank必須是2,因此還需要做一下轉換
# tf.concat()轉換后的outputs的shape為(batch_size * sequence_size, rnn_size)
outputs = tf.concat(outputs, 0)
self.logits = tf.matmul(outputs, softmax_w) + softmax_b
self.prob = tf.nn.softmax(self.logits)
定義loss與train_op
要定義loss函數首先要有正確的輸入,因此先定義targets。在實際feed的時候,要注意targets中的順序必須與outputs中預測結果是對應的。這個之后寫一個輔助函數來對輸入的targets進行轉換。
loss函數的定義使用cross_entropy,tensorflow中有相應的API tf.losses.softmax_cross_entropy, 這個API封裝了softmax步驟,因此應該傳入logits而不是把softmax之后的prob傳進去。
定義完loss之后就需要定義optimizer與train_op。
通常可以直接train_op = tf.train.AdamOptimizer(self.lr).minimize(self.cost)。但是RNN的訓練中很有可能因為梯度過大導致訓練過程不穩定而不收斂,因此需要對計算出的梯度做一步裁剪,再手動更新梯度。
這部分的代碼如下
self.targets = tf.placeholder(tf.int32, shape=[None, vocab_size], name='targets')
self.cost = tf.losses.softmax_cross_entropy(self.targets, self.logits)
self.lr = tf.Variable(0.0, trainable=False)
tvars = tf.trainable_variables(www.yihuanyule.cn)
grads, _ = tf.clip_by_global_norm(tf.gradients(www.xuancayule.com self.cost, tvars), grad_clip)
optimizer = tf.train.AdamOptimizer(self.lr)
self.train_op = optimizer.apply_gradients(zip(grads, tvars))
-
到這里為止Char RNN的主要部分,即模型的結構及其訓練所需的op都定義完成了, 還剩下inference的接口,以及啟動訓練模型的部分未完成。