RNN 模型作為一個可以學習時間序列的模型被認為是深度學習中比較重要的一類模型。在Tensorflow的官方教程中,有兩個與之相關的模型被實現出來。第一個模型是圍繞着Zaremba的論文Recurrent Neural Network Regularization,以Tensorflow框架為載體進行的實驗再現工作。第二個模型則是較為實用的英語法語翻譯器。在這篇博客里,我會主要針對第一個模型的代碼進行解析。在之后的隨筆里我會進而解析英語法語翻譯器的機能。
論文以及Tensorflow官方教程介紹:
Zaremba設計了一款帶有regularization機制的RNN模型。該模型是基於RNN模型的一個變種,叫做LSTM。論文中,框架被運用在語言模型,語音識別,機器翻譯以及圖片概括等應用的建設上來驗證架構的優越性。作為Tensorflow的官方demo,該模型僅僅被運用在了語言模型的建設上來試圖重現論文中的數據。官方已經對他們的模型制作了一部教程,點擊這里查看官方教程(英語版)。
代碼解析:
代碼可以在github找到,這里先放上代碼地址。點擊這里查看代碼。
代碼框架很容易理解,一開始,PTB模型被設計入了一個類。該類的init函數為多層LSTM語言模型的架構,代碼如下:
def __init__(self, is_training, config): self.batch_size = batch_size = config.batch_size self.num_steps = num_steps = config.num_steps size = config.hidden_size vocab_size = config.vocab_size #這里是定義輸入tensor的placeholder,我們可見這里有兩個輸入, # 一個是數據,一個是目標 self._input_data = tf.placeholder(tf.int32, [batch_size, num_steps]) self._targets = tf.placeholder(tf.int32, [batch_size, num_steps]) # Slightly better results can be obtained with forget gate biases # initialized to 1 but the hyperparameters of the model would need to be # different than reported in the paper. # 這里首先定義了一單個lstm的cell,這個cell有五個parameter,依次是 # number of units in the lstm cell, forget gate bias, 一個已經deprecated的 # parameter input_size, state_is_tuple=False, 以及activation=tanh.這里我們 # 僅僅用了兩個parameter,即size,也就是隱匿層的單元數量以及設forget gate # 的bias為0. 上面那段英文注視其實是說如果把這個bias設為1效果更好,雖然 # 會制造出不同於原論文的結果。 lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(size, forget_bias=0.0) if is_training and config.keep_prob < 1: # 在訓練以及為輸出的保留幾率小於1時 # 這里這個dropoutwrapper其實是為每一個lstm cell的輸入以及輸出加入了dropout機制 lstm_cell = tf.nn.rnn_cell.DropoutWrapper( lstm_cell, output_keep_prob=config.keep_prob) # 這里的cell其實就是一個多層的結構了。它把每一曾的lstm cell連在了一起得到多層 # 的RNN cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * config.num_layers) # 根據論文地4頁章節4.1,隱匿層的初始值是設為0 self._initial_state = cell.zero_state(batch_size, tf.float32) with tf.device("/cpu:0"): # 設定embedding變量以及轉化輸入單詞為embedding里的詞向量(embedding_lookup函數) embedding = tf.get_variable("embedding", [vocab_size, size]) inputs = tf.nn.embedding_lookup(embedding, self._input_data) if is_training and config.keep_prob < 1: # 對輸入進行dropout inputs = tf.nn.dropout(inputs, config.keep_prob) # Simplified version of tensorflow.models.rnn.rnn.py's rnn(). # This builds an unrolled LSTM for tutorial purposes only. # In general, use the rnn() or state_saving_rnn() from rnn.py. # # The alternative version of the code below is: # # from tensorflow.models.rnn import rnn # inputs = [tf.squeeze(input_, [1]) # for input_ in tf.split(1, num_steps, inputs)] # outputs, state = rnn.rnn(cell, inputs, initial_state=self._initial_state) outputs = [] state = self._initial_state with tf.variable_scope("RNN"): for time_step in range(num_steps): if time_step > 0: tf.get_variable_scope().reuse_variables() # 從state開始運行RNN架構,輸出為cell的輸出以及新的state. (cell_output, state) = cell(inputs[:, time_step, :], state) outputs.append(cell_output) # 輸出定義為cell的輸出乘以softmax weight w后加上softmax bias b. 這被叫做logit output = tf.reshape(tf.concat(1, outputs), [-1, size]) softmax_w = tf.get_variable("softmax_w", [size, vocab_size]) softmax_b = tf.get_variable("softmax_b", [vocab_size]) logits = tf.matmul(output, softmax_w) + softmax_b # loss函數是average negative log probability, 這里我們有現成的函數sequence_loss_by_example # 來達到這個效果。 loss = tf.nn.seq2seq.sequence_loss_by_example( [logits], [tf.reshape(self._targets, [-1])], [tf.ones([batch_size * num_steps])]) self._cost = cost = tf.reduce_sum(loss) / batch_size self._final_state = state if not is_training: return # learning rate self._lr = tf.Variable(0.0, trainable=False) tvars = tf.trainable_variables() # 根據張量間的和的norm來clip多個張量 grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars), config.max_grad_norm) # 用之前的變量learning rate來起始梯度下降優化器。 optimizer = tf.train.GradientDescentOptimizer(self.lr) # 一般的minimize為先取compute_gradient,再用apply_gradient # 這里我們不需要compute gradient, 所以直接等於叫了minimize函數的后半段。 self._train_op = optimizer.apply_gradients(zip(grads, tvars))##
上面的代碼注釋已就框架進行了解釋。但我有意的留下了一個最為關鍵的部分沒有解釋,即variable_scope以及reuse_variable函數。該類函數有什么特殊意義呢?我們這里先賣個關子,下面的內容會就這個問題深入探究。
模型建立好后該類還有其他如assign_lr(self,session,lr_value)以及property函數如input_data(self). 這些函數淺顯易懂,就不在這里解釋了。
之后,官方代碼設計了小模型(原論文中沒有regularized的模型)外,還原了論文里的中等模型以及大模型。這些模型是基於同樣的框架,不過不同在迭代數,神經元數以及dropout概率等地方。另有由於小模型的keep_prob概率被設計為1,將不會運用dropout。
另外,由於系統的運行是在terminal里輸入”python 文件名 --參數 參數值“格式,名為get_config()的函數的意義在於把用戶輸入,如small,換算成運用SmallConfig()類。
最后,我們來看一看main函數以及run_epoch函數。首先來看下run_epoch:
def run_epoch(session, m, data, eval_op, verbose=False): """Runs the model on the given data.""" epoch_size = ((len(data) // m.batch_size) - 1) // m.num_steps start_time = time.time() costs = 0.0 iters = 0 state = m.initial_state.eval() # ptb_iterator函數在接受了輸入,batch size以及運行的step數后輸出 # 步驟數以及每一步驟所對應的一對x和y的batch數據,大小為[batch_size, num_step] for step, (x, y) in enumerate(reader.ptb_iterator(data, m.batch_size, m.num_steps)): # 在函數傳遞入的session里運行rnn圖的cost和 fina_state結果,另外也計算eval_op的結果 # 這里eval_op是作為該函數的輸入。 cost, state, _ = session.run([m.cost, m.final_state, eval_op], {m.input_data: x, m.targets: y, m.initial_state: state}) costs += cost iters += m.num_steps # 每一定量運行后輸出目前結果 if verbose and step % (epoch_size // 10) == 10: print("%.3f perplexity: %.3f speed: %.0f wps" % (step * 1.0 / epoch_size, np.exp(costs / iters), iters * m.batch_size / (time.time() - start_time))) return np.exp(costs / iters)
該函數很正常,邏輯也比較清晰,容易理解。現在,讓我們重點看看我們的main函數:
def main(_): # 需要首先確認輸入數據的path,不然沒法訓練模型 if not FLAGS.data_path: raise ValueError("Must set --data_path to PTB data directory") # 讀取輸入數據並將他們拆分開 raw_data = reader.ptb_raw_data(FLAGS.data_path) train_data, valid_data, test_data, _ = raw_data # 讀取用戶輸入的config,這里用具決定了是小,中還是大模型 config = get_config() eval_config = get_config() eval_config.batch_size = 1 eval_config.num_steps = 1 # 建立了一個default圖並開始session with tf.Graph().as_default(), tf.Session() as session: #先進行initialization initializer = tf.random_uniform_initializer(-config.init_scale, config.init_scale) #注意,這里就是variable scope的運用了! with tf.variable_scope("model", reuse=None, initializer=initializer): m = PTBModel(is_training=True, config=config) with tf.variable_scope("model", reuse=True, initializer=initializer): mvalid = PTBModel(is_training=False, config=config) mtest = PTBModel(is_training=False, config=eval_config) tf.initialize_all_variables().run() for i in range(config.max_max_epoch): # 遞減learning rate lr_decay = config.lr_decay ** max(i - config.max_epoch, 0.0) m.assign_lr(session, config.learning_rate * lr_decay) #打印出perplexity print("Epoch: %d Learning rate: %.3f" % (i + 1, session.run(m.lr))) train_perplexity = run_epoch(session, m, train_data, m.train_op, verbose=True) print("Epoch: %d Train Perplexity: %.3f" % (i + 1, train_perplexity)) valid_perplexity = run_epoch(session, mvalid, valid_data, tf.no_op()) print("Epoch: %d Valid Perplexity: %.3f" % (i + 1, valid_perplexity)) test_perplexity = run_epoch(session, mtest, test_data, tf.no_op()) print("Test Perplexity: %.3f" % test_perplexity)
還記得之前賣的關子么?這個重要的variable_scope函數的目的其實是允許我們在保留模型權重的情況下運行多個模型。首先,從RNN的根源上說,因為輸入輸出有着時間關系,我們的模型在訓練時每此迭代都要運用到之前迭代的結果,所以如果我們直接使用(cell_output, state) = cell(inputs[:, time_step, :], state)我們可能會得到一堆新的RNN模型,而不是我們所期待的前一時刻的RNN模型。再看main函數,當我們訓練時,我們需要的是新的模型,所以我們在定義了一個scope名為model的模型時說明了我們不需要使用以存在的參數,因為我們本來的目的就是去訓練的。而在我們做validation和test的時候呢?訓練新的模型將會非常不妥,所以我們需要運用之前訓練好的模型的參數來測試他們的效果,故定義reuse=True。這個概念有需要的朋友可以參考Tensorflow的官方文件對共享變量的描述。
好了,我們了解了這個模型代碼的架構以及運行的機制,那么他在實際運行中效果如何呢?讓我們來實際測試一番。由於時間問題,我只運行了小模型,也就是不用dropout的模型。運行方式為在ptb_word_lm.py的文件夾下輸入python ptb_word_lm.py --data_path=/tmp/simple-examples/data/ --model small。這里需要注意的是你需要下載simple-examples.tar.gz包,下載地址點擊這里。運行結果如下:
這里簡便的放入了最后結果,我們可見,在13個epoch時,我們的測試perplexity為117.605, 對應了論文里non-regularized LSTM的114.5,運行時間約5到6小時。