【seq2seq】代碼案例解讀
RNN 模型作為一個(gè)可以學(xué)習(xí)時(shí)間序列的模型被認(rèn)為是深度學(xué)習(xí)中比較重要的一類模型。在Tensorflow的官方教程中,有兩個(gè)與之相關(guān)的模型被實(shí)現(xiàn)出來。第一個(gè)模型是圍繞著Zaremba的論文Recurrent Neural Network Regularization,以Tensorflow框架為載體進(jìn)行的實(shí)驗(yàn)再現(xiàn)工作。第二個(gè)模型則是較為實(shí)用的英語法語翻譯器。在這篇博客里,我會(huì)主要針對第一個(gè)模型的代碼進(jìn)行解析。在之后的隨筆里我會(huì)進(jìn)而解析英語法語翻譯器的機(jī)能。論文以及Tensorflow官方教程介紹:Zaremba設(shè)計(jì)了一款帶有regularization機(jī)制的RNN模型。該模型是基于RNN模型的一個(gè)變種,叫做LSTM。論文中,框架被運(yùn)用在語言模型,語音識(shí)別,機(jī)器翻譯以及圖片概括等應(yīng)用的建設(shè)上來驗(yàn)證架構(gòu)的優(yōu)越性。作為Tensorflow的官方demo,該模型僅僅被運(yùn)用在了語言模型的建設(shè)上來試圖重現(xiàn)論文中的數(shù)據(jù)。官方已經(jīng)對他們的模型制作了一部教程,點(diǎn)擊這里查看官方教程(英語版)。代碼解析:代碼可以在github找到,這里先放上代碼地址。點(diǎn)擊這里查看代碼。代碼框架很容易理解,一開始,PTB模型被設(shè)計(jì)入了一個(gè)類。該類的init函數(shù)為多層LSTM語言模型的架構(gòu),代碼如下:
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,我們可見這里有兩個(gè)輸入,
# 一個(gè)是數(shù)據(jù),一個(gè)是目標(biāo)
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.
# 這里首先定義了一單個(gè)lstm的cell,這個(gè)cell有五個(gè)parameter,依次是
# number of units in the lstm cell, forget gate bias, 一個(gè)已經(jīng)deprecated的
# parameter input_size, state_is_tuple=False, 以及activation=tanh.這里我們
# 僅僅用了兩個(gè)parameter,即size,也就是隱匿層的單元數(shù)量以及設(shè)forget gate
# 的bias為0. 上面那段英文注視其實(shí)是說如果把這個(gè)bias設(shè)為1效果更好,雖然
# 會(huì)制造出不同于原論文的結(jié)果。
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(size, forget_bias=0.0)
if is_training and config.keep_prob < 1: # 在訓(xùn)練以及為輸出的保留幾率小于1時(shí)
# 這里這個(gè)dropoutwrapper其實(shí)是為每一個(gè)lstm cell的輸入以及輸出加入了dropout機(jī)制
lstm_cell = tf.nn.rnn_cell.DropoutWrapper(
lstm_cell, output_keep_prob=config.keep_prob)
# 這里的cell其實(shí)就是一個(gè)多層的結(jié)構(gòu)了。它把每一曾的lstm cell連在了一起得到多層
# 的RNN
cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * config.num_layers)
# 根據(jù)論文地4頁章節(jié)4.1,隱匿層的初始值是設(shè)為0
self._initial_state = cell.zero_state(batch_size, tf.float32)
with tf.device("/cpu:0"):
# 設(shè)定embedding變量以及轉(zhuǎn)化輸入單詞為embedding里的詞向量(embedding_lookup函數(shù))
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:
# 對輸入進(jìn)行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開始運(yùn)行RNN架構(gòu),輸出為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函數(shù)是average negative log probability, 這里我們有現(xiàn)成的函數(shù)sequence_loss_by_example
# 來達(dá)到這個(gè)效果。
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()
# 根據(jù)張量間的和的norm來clip多個(gè)張量
grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars),
config.max_grad_norm)
# 用之前的變量learning rate來起始梯度下降優(yōu)化器。
optimizer = tf.train.GradientDescentOptimizer(self.lr)
# 一般的minimize為先取compute_gradient,再用apply_gradient
# 這里我們不需要compute gradient, 所以直接等于叫了minimize函數(shù)的后半段。
self._train_op = optimizer.apply_gradients(zip(grads, tvars))##
上面代碼注釋已就框架進(jìn)行了解釋。但我有意的留下了一個(gè)最為關(guān)鍵的部分沒有解釋,即variable_scope以及reuse_variable函數(shù)。該類函數(shù)有什么特殊意義呢?我們這里先賣個(gè)關(guān)子,下面的內(nèi)容會(huì)就這個(gè)問題深入探究。模型建立好后該類還有其他如assign_lr(self,session,lr_value)以及property函數(shù)如input_data(self). 這些函數(shù)淺顯易懂,就不在這里解釋了。之后,官方代碼設(shè)計(jì)了小模型(原論文中沒有regularized的模型)外,還原了論文里的中等模型以及大模型。這些模型是基于同樣的框架,不過不同在迭代數(shù),神經(jīng)元數(shù)以及dropout概率等地方。另有由于小模型的keep_prob概率被設(shè)計(jì)為1,將不會(huì)運(yùn)用dropout。另外,由于系統(tǒng)的運(yùn)行是在terminal里輸入”python 文件名 --參數(shù) 參數(shù)值“格式,名為get_config()的函數(shù)的意義在于把用戶輸入,如small,換算成運(yùn)用SmallConfig()類。最后,我們來看一看main函數(shù)以及run_epoch函數(shù)。首先來看下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函數(shù)在接受了輸入,batch size以及運(yùn)行的step數(shù)后輸出
# 步驟數(shù)以及每一步驟所對應(yīng)的一對x和y的batch數(shù)據(jù),大小為 [batch_size, num_step]
for step, (x, y) in enumerate(reader.ptb_iterator(data, m.batch_size,
m.num_steps)):
# 在函數(shù)傳遞入的session里運(yùn)行rnn圖的cost和 fina_state結(jié)果,另外也計(jì)算eval_op的結(jié)果
# 這里eval_op是作為該函數(shù)的輸入。
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
# 每一定量運(yùn)行后輸出目前結(jié)果
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)
該函數(shù)很正常,邏輯也比較清晰,容易理解。現(xiàn)在,讓我們重點(diǎn)看看我們的main函數(shù):
def main(_):
# 需要首先確認(rèn)輸入數(shù)據(jù)的path,不然沒法訓(xùn)練模型
if not FLAGS.data_path:
raise ValueError("Must set --data_path to PTB data directory")
# 讀取輸入數(shù)據(jù)并將他們拆分開
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
# 建立了一個(gè)default圖并開始session
with tf.Graph().as_default(), tf.Session() as session:
#先進(jìn)行initialization
initializer = tf.random_uniform_initializer(-config.init_scale,
config.init_scale)
#注意,這里就是variable scope的運(yùn)用了!
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)
還記得之前賣的關(guān)子么?這個(gè)重要的variable_scope函數(shù)的目的其實(shí)是允許我們在保留模型權(quán)重的情況下運(yùn)行多個(gè)模型。首先,從RNN的根源上說,因?yàn)檩斎胼敵鲇兄鴷r(shí)間關(guān)系,我們的模型在訓(xùn)練時(shí)每此迭代都要運(yùn)用到之前迭代的結(jié)果,所以如果我們直接使用(cell_output, state) = cell(inputs[:, time_step, :], state)我們可能會(huì)得到一堆新的RNN模型,而不是我們所期待的前一時(shí)刻的RNN模型。再看main函數(shù),當(dāng)我們訓(xùn)練時(shí),我們需要的是新的模型,所以我們在定義了一個(gè)scope名為model的模型時(shí)說明了我們不需要使用以存在的參數(shù),因?yàn)槲覀儽緛淼哪康木褪侨ビ?xùn)練的。而在我們做validation和test的時(shí)候呢?訓(xùn)練新的模型將會(huì)非常不妥,所以我們需要運(yùn)用之前訓(xùn)練好的模型的參數(shù)來測試他們的效果,故定義reuse=True。這個(gè)概念有需要的朋友可以參考Tensorflow的官方文件對共享變量的描述。
好了,我們了解了這個(gè)模型代碼的架構(gòu)以及運(yùn)行的機(jī)制,那么他在實(shí)際運(yùn)行中效果如何呢?讓我們來實(shí)際測試一番。由于時(shí)間問題,我只運(yùn)行了小模型,也就是不用dropout的模型。運(yùn)行方式為在ptb_word_lm.py的文件夾下輸入python ptb_word_lm.py --data_path=/tmp/simple-examples/data/ --model small。這里需要注意的是你需要下載simple-examples.tar.gz包,下載地址點(diǎn)擊這里。運(yùn)行結(jié)果如下:
這里簡便的放入了最后結(jié)果,我們可見,在13個(gè)epoch時(shí),我們的測試perplexity為117.605, 對應(yīng)了論文里non-regularized LSTM的114.5,運(yùn)行時(shí)間約5到6小時(shí)。