解讀tensorflow之rnn 的示例 ptb_word_lm.py
解讀tensorflow之rnn 的示例 ptb_word_lm.py
這兩天想搞清楚用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的程式碼看多了之後就知道其實官方程式碼的這個結構並不好:
- graph的構建和訓練部分放在了一個檔案中,至少也應該分開成model.py和train.py兩個檔案,model.py中只有一個PTBModel類
- 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。
上圖中,xt-2時刻的輸入首先傳入第一層cell,這個過程有dropout,但是從 t-2 時刻的第一層cell傳到 t-1, 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)
|
首先要明確幾點:
- 既然我們要在訓練過程中不斷更新embedding矩陣,那麼embedding必須是tf.Variable並且
trainable=True
(default) - 目前tensorflow對於lookup embedding的操作只能再cpu上進行
- 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中講到的,
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?
相關文章
- RNN程式碼解讀之char-RNN with TensorFlow(model.py)RNN
- [譯] TensorFlow 中的 RNN 串流RNN
- CRF as RNN 程式碼解讀CRFRNN
- TensorFlow框架下的RNN實踐小結框架RNN
- 【精讀】自然語言處理基礎之RNN自然語言處理RNN
- TensorFlow中RNN實現的正確開啟方式RNN
- ML.NET 示例:深度學習之整合TensorFlow深度學習
- TensorFlow系列專題(八):七步帶你實現RNN迴圈神經網路小示例RNN神經網路
- 【Tensorflow_DL_Note9】Tensorflow原始碼解讀1原始碼
- 示例解讀 Python 2 和 Python 3 之間的主要差異Python
- tensorflow教程:tf.contrib.rnn.DropoutWrapperRNNAPP
- Tensorflow實現RNN(LSTM)手寫數字識別RNN
- 資深 Googler 深度解讀 TensorFlowGo
- 技術解讀 | 基於fastText和RNN的語義消歧實戰ASTRNN
- RNN 結構詳解RNN
- [原始碼解析]深度學習利器之自動微分(3) --- 示例解讀原始碼深度學習
- PHP8 新特性解讀和示例PHP
- [論文閱讀] RNN 在阿里DIEN中的應用RNN阿里
- ML.NET呼叫Tensorflow模型示例——MNIST模型
- LSTM解決RNN梯度爆炸(消失)RNN梯度
- 官方解讀:TensorFlow 2.0中即將到來的所有新特性
- NPOI讀取示例
- TensorFlow讀寫資料
- 【Tensorflow_DL_Note13】TensorFlow中資料的讀取方式(1)
- AttributeError: module ‘tensorflow._api.v1.nn.rnn_cell‘ has no attribute ‘InputProjectionWrapper‘ErrorAPIRNNProjectAPP
- TensorFlow學習筆記(8):基於MNIST資料的迴圈神經網路RNN筆記神經網路RNN
- 貌離神合的RNN與ODE:花式RNN簡介RNN
- TensorFlow系列專題(七):一文綜述RNN迴圈神經網路RNN神經網路
- TensorFlow讀取CSV資料
- 使用Keras進行深度學習:(五)RNN和雙向RNN講解及實踐Keras深度學習RNN
- RNN、LSTMRNN
- 你一定需要的關於CNN、RNN的講解CNNRNN
- Flutter之Navigator解讀Flutter
- ThreadLocal之深度解讀thread
- Webpack解讀之loaderWeb
- TensorFlow系列專題(九):常用RNN網路結構及依賴優化問題RNN優化
- Tensorflow2.0-mnist手寫數字識別示例
- 解讀敏捷3 - 解讀敏捷實踐之結對Review敏捷View