轉自:https://blog.csdn.net/mydear_11000/article/details/52414342node
from: http://lan2720.github.io/2016/07/16/%E8%A7%A3%E8%AF%BBtensorflow%E4%B9%8Brnn/git
這兩天想搞清楚用tensorflow來實現rnn/lstm如何作,可是google了半天,發現tf在rnn方面的實現代碼或者教程都太少了,僅有的幾個教程講的又過於簡單。沒辦法,只能親自動手一步步研究官方給出的代碼了。github
本文研究的代碼主體來自官方源碼ptb-word-lm。可是,若是你直接運行這個代碼,能夠看到warning:網絡
WARNING:tensorflow:: Using a concatenated state is slower and will soon be deprecated. Use state_is_tuple=True.session
因而根據這個warning,找到了一個相關的issue:https://github.com/tensorflow/tensorflow/issues/2695
回答中有人給出了對應的修改,加入了state_is_tuple=True
,筆者就是基於這段代碼學習的。app
tf的代碼看多了以後就知道其實官方代碼的這個結構並很差:函數
剛好看到了一篇專門講如何構建tensorflow模型代碼的blog,值得學習,來重構本身的代碼吧。學習
雖然說官方給出的代碼結構上有點小缺陷,可是畢竟都是大神們寫出來的,值得咱們學習的地方不少,來總結一下:google
(1) 設置is_training這個標誌
這個頗有必要,由於training階段和valid/test階段參數設置上會有小小的區別,好比test時不進行dropout
(2) 將必要的各種參數都寫在config類中獨立管理
這個的好處就是各種參數的配置工做和model類解耦了,不須要將大量的參數設置寫在model中,那樣可讀性不只差,還不容易看清究竟設置了哪些超參數spa
兩個,分別命名爲self._input_data和self._target,只是注意一下,因爲咱們如今要訓練的模型是language model,也就是給一個word,預測最有可能的下一個word,所以能夠看出來,input和output是同型的。而且,placeholder只存儲一個batch的data,input接收的是個word在vocabulary中對應的index【後續會將index轉成dense embedding】,每次接收一個seq長度的words,那麼,input shape=[batch_size, num_steps]
這其中的每一個小長方形就表示一個cell。每一個cell中又是一個略複雜的結構,以下圖:
圖中的context就是一個cell結構,能夠看到它接受的輸入有input(t),context(t-1),而後輸出output(t),好比像咱們這個任務中,用到多層堆疊的rnn cell的話,也就是當前層的cell的output還要做爲下一層cell的輸入,所以可推出每一個cell的輸入和輸出的shape是同樣。若是輸入的shape=(None, n),加上context(t-1)同時做爲輸入部分,所以能夠知道
的shape=(2n, n)。
說了這麼多,其實我只是想表達一個重點,就是
別小看那一個小小的cell,它並非只有1個neuron unit,而是n個hidden units
所以,咱們注意到tensorflow中定義一個cell(BasicRNNCell/BasicLSTMCell/GRUCell/RNNCell/LSTMCell)結構的時候須要提供的一個參數就是hidden_units_size。
弄明白這個以後,再看tensorflow中定義cell的代碼就無比簡單了:
1 2 3 4 5 |
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(size, forget_bias=0.0, state_is_tuple=True) if is_training and config.keep_prob < 1: lstm_cell = tf.nn.rnn_cell.DropoutWrapper( lstm_cell, output_keep_prob=config.keep_prob) cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * config.num_layers, state_is_tuple=True) |
首先,定義一個最小的cell單元,也就是小長方形,BasicLSTMCell。
你確定會問,這個類和LSTMCell有什麼區別呢?good question,文檔給出的解釋是這樣的:
劃一下重點就是倒數第二句話,意思是說這個類沒有實現clipping,projection layer,peep-hole等一些lstm的高級變種,僅做爲一個基本的basicline結構存在,若是要使用這些高級variant要用LSTMCell這個類。
由於咱們如今只是想搭建一個基本的lstm-language model模型,可以訓練出必定的結果就好了,所以現階段BasicLSTMCell夠用。這就是爲何這裏用的是BasicLSTMCell這個類而不是別的什麼。
(此圖偷自recurrent neural network regularization)
能夠看到,每一個lstm cell在t時刻都會產生兩個內部狀態
和
,都是在t-1時刻計算要用到的。這兩個狀態在tensorflow中都要記錄,記住這個就好理解了。
來看官方對這個的解釋:
意思是說,若是state_is_tuple=True,那麼上面咱們講到的狀態
和
就是分開記錄,放在一個tuple中,若是這個參數沒有設定或設置成False,兩個狀態就按列鏈接起來,成爲[batch, 2n](n是hidden units個數)返回。官方說這種形式立刻就要被deprecated了,全部咱們在使用LSTM的時候要加上state_is_tuple=True
暫時還沒管這個參數的含義
dropout是一種很是efficient的regularization方法,在rnn中如何使用dropout和cnn不一樣,推薦你們去把recurrent neural network regularization看一遍。我在這裏僅講結論,
對於rnn的部分不進行dropout,也就是說從t-1時候的狀態傳遞到t時刻進行計算時,這個中間不進行memory的dropout;僅在同一個t時刻中,多層cell之間傳遞信息的時候進行dropout
上圖中,
時刻的輸入首先傳入第一層cell,這個過程有dropout,可是從時刻的第一層cell傳到,,的第一層cell這個中間都不進行dropout。再從
時候的第一層cell向同一時刻內後續的cell傳遞時,這之間又有dropout了。
所以,咱們在代碼中定義完cell以後,在cell外部包裹上dropout,這個類叫DropoutWrapper,這樣咱們的cell就有了dropout功能!
能夠從官方文檔中看到,它有input_keep_prob和output_keep_prob,也就是說裹上這個DropoutWrapper以後,若是我但願是input傳入這個cell時dropout掉一部分input信息的話,就設置input_keep_prob,那麼傳入到cell的就是部分input;若是我但願這個cell的output只部分做爲下一層cell的input的話,就定義output_keep_prob。不要太方便。
根據Zaremba在paper中的描述,這裏應該給cell設置output_keep_prob。
1 2 3 |
if is_training and config.keep_prob < 1: lstm_cell = tf.nn.rnn_cell.DropoutWrapper( lstm_cell, output_keep_prob=config.keep_prob) |
如今咱們定義了一個lstm cell,這個cell僅是整個圖中的一個小長方形,咱們但願整個網絡能更deep的話,應該stack多個這樣的lstm cell,tensorflow給咱們提供了MultiRNNCell(注意:multi只有這一個類,並無MultiLSTMCell之類的),所以堆疊多層只生成這個類便可。
1 |
cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * config.num_layers, state_is_tuple=True) |
咱們仍是看看官方文檔,
咱們能夠從描述中看出,tensorflow並非簡單的堆疊了多個single cell,而是將這些cell stack以後當成了一個完整的獨立的cell,每一個小cell的中間狀態仍是保存下來了,按n_tuple存儲,可是輸出output只用最後那個cell的輸出。
這樣,咱們就定義好了每一個t時刻的總體cell,接下來只要每一個時刻傳入不一樣的輸入,再在時間上展開,就能獲得上圖多個時間上unroll graph。
接下來就須要給咱們的multi lstm cell進行狀態初始化。怎麼作呢?Zaremba已經告訴咱們了
We initialize the hidden states to zero. We then use the
final hidden states of the current minibatch as the initial hidden state of the subsequent minibatch
(successive minibatches sequentially traverse the training set).
也就是初始時所有賦值爲0狀態。
那麼就須要有一個self._initial_state
來保存咱們生成的全0狀態,最後直接調用MultiRNNCell的zero_state()方法便可。
1 |
self._initial_state = cell.zero_state(batch_size, tf.float32) |
注意:這裏傳入的是batch_size,我一開始沒看懂爲何,那就看文檔的解釋吧!
state_size是咱們在定義MultiRNNCell的時就設置好了的,只是咱們的輸入input shape=[batch_size, num_steps],咱們剛剛定義好的cell會依次接收num_steps個輸入而後產生最後的state(n-tuple,n表示堆疊的層數)可是一個batch內有batch_size這樣的seq,所以就須要[batch_size,s]來存儲整個batch每一個seq的狀態。
咱們預處理了數據以後獲得的是一個二維array,每一個位置的元素表示這個word在vocabulary中的index。
可是傳入graph的數據不能講word用index來表示,這樣詞和詞之間的關係就無法刻畫了。咱們須要將word用dense vector表示,這也就是廣爲人知的word embedding。
paper中並無使用預訓練的word embedding,全部的embedding都是隨機初始化,而後在訓練過程當中不斷更新embedding矩陣的值。
1 2 3 |
with tf.device("/cpu:0"): embedding = tf.get_variable("embedding", [vocab_size, size]) inputs = tf.nn.embedding_lookup(embedding, self._input_data) |
首先要明確幾點:
trainable=True
(default)最後生成真正的inputs節點,也就是從embedding_lookup以後獲得的結果,這個tensor的shape=batch_size, num_stemps, size
剛纔咱們定義了每一個cell的輸出要wrap一個dropout,可是根據paper中講到的,
We can see that the information is corrupted by the dropout operator exactly L + 1 times
We use the activations
to predict , since
is the number of layers
in our deep LSTM.
cell的層數一共定義了L層,爲何dropout要進行L+1次呢?就是由於輸入這個地方要進行1次dropout。好比,咱們設置cell的hidden units size=200的話,input embbeding dim=200維度較高,dropout一部分,防止overfitting。
1 2 |
if is_training and config.keep_prob < 1: inputs = tf.nn.dropout(inputs, config.keep_prob) |
和上面的DropoutWrapper同樣,都是在is_training and config.keep_prob < 1的條件下才進行dropout。
因爲這個僅對tensor進行dropout(而非rnn_cell進行wrap),所以調用的是tf.nn.dropout。
到上面這一步,咱們的基本單元multi cell和inputs算是所有準備好啦,接下來就是在time上進行recurrent,獲得num_steps每一時刻的output和states。
那麼很天然的咱們能夠猜想output的shape=[batch_size, num_steps, size],states的shape=[batch_size, n(LSTMStateTuple)]【state是整個seq輸入完以後獲得的每層的state
1 2 3 4 5 6 7 |
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() (cell_output, state) = cell(inputs[:, time_step, :], state) outputs.append(cell_output) |
能夠看到,有四個函數能夠用來構建rnn,咱們一個個的講。
(1) dynamic rnn
這個方法給rnn()很相似,只是它的inputs不是list of tensors,而是一整個tensor,num_steps是inputs的一個維度。這個方法的輸出是一個pair,
因爲咱們preprocessing以後獲得的input shape=[batch_size, num_steps, size]所以,time_major=False。
最後的到的這個pair的shape正如咱們猜想的輸出是同樣的。
sequence_length: (optional) An int32/int64 vector sized [batch_size].表示的是batch中每行sequence的長度。
調用方法是:
1 |
outputs, state = tf.nn.dynamic_rnn(cell, inputs, sequence_length=..., initial_state=state) |
state是final state,若是有n layer,則是final state也有n個元素,對應每一層的state。
(2)tf.nn.rnn
這個函數和dynamic_rnn的區別就在於,這個須要的inputs是a list of tensor,這個list的長度是num_steps,也就是將每個時刻的輸入切分出來了,tensor的shape=[batch_size, input_size]【這裏的input每個都是word embedding,所以input_size=hidden_units_size】
除了輸出inputs是list以外,輸出稍有差異。
能夠看到,輸出也是一個長度爲T(num_steps)的list,每個output對應一個t時刻的input(batch_size, hidden_units_size),output shape=[batch_size, hidden_units_size]
(3)state_saving_rnn
這個方法能夠接收一個state saver對象,這是和以上兩個方法不一樣之處,另外其inputs和outputs也都是list of tensors。
(4)bidirectional_rnn
等研究bi-rnn網絡的時候再講。
以上介紹了四種rnn的構建方式,這裏選擇dynamic_rnn
.由於inputs中的第2個維度已是num_steps了。
既然咱們用的是dynamic_rnn,那麼outputs shape=[batch_size, num_steps, size],而接下來須要將output傳入到softmax層,softmax層並無顯式地使用tf.nn.softmax函數,而是隻是計算了wx+b獲得logits(其實是同樣的,softmax函數僅僅只是將logits再rescale到0-1之間)
獲得logits後,用到了nn.seq2seq.sequence_loss_by_example函數來計算「所謂的softmax層」的loss。這個loss是整個batch上累加的loss,須要除上batch_size,獲得平均下來的loss,也就是self._cost。
1 2 3 4 5 6 |
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 |
若是is_training=False,也就是僅valid or test的話,計算出loss這一步也就終止了。之因此要求導,就是train的過程。因此這個地方對is_training
進行一個判斷。
1 2 |
if not is_training: return |
若是想在訓練過程當中調節learning rate的話,生成一個lr的variable,可是trainable=False,也就是不進行求導。
1 |
self._lr = tf.Variable(0.0, trainable=False) |
gradient在backpropagate過程當中,很容易出現vanish&explode現象,尤爲是rnn這種back不少個time step的結構。
所以都要使用clip來對gradient值進行調節。
既然要調節了就不能簡單的調用optimizer.minimize(loss)
,而是須要顯式的計算gradients,而後進行clip,將clip後的gradient進行apply。
官方文檔說明了這種操做:
並給出了一個例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
# Create an optimizer. opt = GradientDescentOptimizer(learning_rate=0.1) # Compute the gradients for a list of variables. grads_and_vars = opt.compute_gradients(loss, <list of variables>) # grads_and_vars is a list of tuples (gradient, variable). Do whatever you # need to the 'gradient' part, for example cap them, etc. capped_grads_and_vars = [(MyCapper(gv[0]), gv[1]) for gv in grads_and_vars] # Ask the optimizer to apply the capped gradients. opt.apply_gradients(capped_grads_and_vars) |
模仿這個代碼,咱們能夠寫出以下的僞代碼:
1 2 3 4 5 6 7 8 |
optimizer = tf.train.AdamOptimizer(learning_rate=self._lr) # gradients: return A list of sum(dy/dx) for each x in xs. grads = optimizer.gradients(self._cost, <list of variables>) clipped_grads = tf.clip_by_global_norm(grads, config.max_grad_norm) # accept: List of (gradient, variable) pairs, so zip() is needed self._train_op = optimizer.apply_gradients(zip(grads, <list of variables>)) |
能夠看到,此時就差一個<list of variables>不知道了,也就是須要對哪些variables進行求導。
答案是:trainable variables
所以,咱們獲得
1 |
tvars = tf.trainable_variables() |
用tvars帶入上面的代碼中便可。
使用tf.assign(ref, value)
函數。ref應該是個variable node,這個assign是個operation,所以須要在sess.run()中進行才能生效。這樣以後再調用ref的值就發現改變成新值了。
在這個模型中用於改變learning rate這個variable的值。
1 2 |
def assign_lr(self, session, lr_value): session.run(tf.assign(self.lr, lr_value)) |
好比定義了一個tensor x,x.eval(feed_dict={xxx})
就能夠獲得x的值,而不用sess.run(x, feed_dict={xxx})。返回值是一個numpy array。
1 2 3 4 5 6 7 |
state = m.initial_state.eval() for step, (x, y) in enumerate(reader.ptb_iterator(data, m.batch_size, m.num_steps)): cost, state, _ = session.run([m.cost, m.final_state, eval_op], {m.input_data: x, m.targets: y, m.initial_state: state}) |
爲何feed_dict中還須要傳入initial_statel?