解讀tensorflow之rnn

from: http://lan2720.github.io/2016/07/16/%E8%A7%A3%E8%AF%BBtensorflow%E4%B9%8Brnn/

這兩天想搞清楚用tensorflow來實現rnn/lstm如何做,但是google了半天,發現tf在rnn方面的實現代碼或者教程都太少了,僅有的幾個教程講的又過於簡單。沒辦法,只能親自動手一步步研究官方給出的代碼了。

本文研究的代碼主體來自官方源碼ptb-word-lm。但是,如果你直接運行這個代碼,可以看到warning:

WARNING:tensorflow:: Using a concatenated state is slower and will soon be deprecated. Use state_is_tuple=True.

於是根據這個warning,找到了一個相關的issue:https://github.com/tensorflow/tensorflow/issues/2695
回答中有人給出了對應的修改,加入了state_is_tuple=True,筆者就是基於這段代碼學習的。

代碼結構

tf的代碼看多了之後就知道其實官方代碼的這個結構並不好:

  1. graph的構建和訓練部分放在了一個文件中,至少也應該分開成model.py和train.py兩個文件,model.py中只有一個PTBModel類
  2. graph的構建部分全部放在了PTBModel類的constructor中

恰好看到了一篇專門講如何構建tensorflow模型代碼的blog,值得學習,來重構自己的代碼吧。

值得學習的地方

雖說官方給出的代碼結構上有點小缺陷,但是畢竟都是大神們寫出來的,值得我們學習的地方很多,來總結一下:

(1) 設置is_training這個標誌
這個很有必要,因爲training階段和valid/test階段參數設置上會有小小的區別,比如test時不進行dropout
(2) 將必要的各類參數都寫在config類中獨立管理
這個的好處就是各類參數的配置工作和model類解耦了,不需要將大量的參數設置寫在model中,那樣可讀性不僅差,還不容易看清究竟設置了哪些超參數

placeholder

兩個,分別命名爲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

在很多用到rnn的paper中我們會看到類似的圖:

這其中的每個小長方形就表示一個cell。每個cell中又是一個略複雜的結構,如下圖:

圖中的context就是一個cell結構,可以看到它接受的輸入有input(t),context(t-1),然後輸出output(t),比如像我們這個任務中,用到多層堆疊的rnn cell的話,也就是當前層的cell的output還要作爲下一層cell的輸入,因此可推出每個cell的輸入和輸出的shape是一樣。如果輸入的shape=(None, n),加上context(t-1)同時作爲輸入部分,因此可以知道W

的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。

問題1:爲什麼是BasicLSTMCell

你肯定會問,這個類和LSTMCell有什麼區別呢?good question,文檔給出的解釋是這樣的:

劃一下重點就是倒數第二句話,意思是說這個類沒有實現clipping,projection layer,peep-hole等一些lstm的高級變種,僅作爲一個基本的basicline結構存在,如果要使用這些高級variant要用LSTMCell這個類。
因爲我們現在只是想搭建一個基本的lstm-language model模型,能夠訓練出一定的結果就行了,因此現階段BasicLSTMCell夠用。這就是爲什麼這裏用的是BasicLSTMCell這個類而不是別的什麼。

問題2:state_is_tuple=True是什麼


(此圖偷自recurrent neural network regularization)
可以看到,每個lstm cell在t時刻都會產生兩個內部狀態ct

ht

,都是在t-1時刻計算要用到的。這兩個狀態在tensorflow中都要記錄,記住這個就好理解了。

來看官方對這個的解釋:

意思是說,如果state_is_tuple=True,那麼上面我們講到的狀態ct

ht

就是分開記錄,放在一個tuple中,如果這個參數沒有設定或設置成False,兩個狀態就按列連接起來,成爲[batch, 2n](n是hidden units個數)返回。官方說這種形式馬上就要被deprecated了,所有我們在使用LSTM的時候要加上state_is_tuple=True

問題3:forget_bias是什麼

暫時還沒管這個參數的含義

DropoutWrapper

dropout是一種非常efficient的regularization方法,在rnn中如何使用dropout和cnn不同,推薦大家去把recurrent neural network regularization看一遍。我在這裏僅講結論,

對於rnn的部分不進行dropout,也就是說從t-1時候的狀態傳遞到t時刻進行計算時,這個中間不進行memory的dropout;僅在同一個t時刻中,多層cell之間傳遞信息的時候進行dropout

上圖中,xt2

時刻的輸入首先傳入第一層cell,這個過程有dropout,但是從t2時刻的第一層cell傳到t1,t,t+1的第一層cell這個中間都不進行dropout。再從t+1

時候的第一層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)

Stack MultiCell

現在我們定義了一個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。

initial states

接下來就需要給我們的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的狀態。

embedding input

我們預處理了數據之後得到的是一個二維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)

首先要明確幾點:

  1. 既然我們要在訓練過程中不斷更新embedding矩陣,那麼embedding必須是tf.Variable並且trainable=True(default)
  2. 目前tensorflow對於lookup embedding的操作只能再cpu上進行
  3. embedding矩陣的大小是多少:每個word都需要有對應的embedding vector,總共就是vocab_size那麼多個embedding,每個word embed成多少維的vector呢?因爲我們input embedding後的結果就直接輸入給了第一層cell,剛纔我們知道cell的hidden units size,因此這個embedding dim要和hidden units size對應上(這也才能和內部的各種門的W和b完美相乘)。因此,我們就確定下來embedding matrix shape=[vocab_size, hidden_units_size]

最後生成真正的inputs節點,也就是從embedding_lookup之後得到的結果,這個tensor的shape=batch_size, num_stemps, size

input data dropout

剛纔我們定義了每個cell的輸出要wrap一個dropout,但是根據paper中講到的,

We can see that the information is corrupted by the dropout operator exactly L + 1 times

We use the activations hLt

to predict yt , since L

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。

RNN循環起來!

到上面這一步,我們的基本單元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了。

得到output之後傳到下一層softmax layer

既然我們用的是dynamic_rnn,那麼outputs shape=[batch_size, num_steps, size],而接下來需要將output傳入到softmax層,softmax層並沒有顯式地使用tf.nn.softmax函數,而是隻是計算了wx+b得到logits(實際上是一樣的,softmax函數僅僅只是將logits再rescale到0-1之間)

計算loss

得到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

求導,定義train_op

如果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帶入上面的代碼中即可。

how to change Variable value

使用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))

run_epoch()

Tensor.eval()


比如定義了一個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?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章