NO IMAGE

tensorflow筆記:多層LSTM程式碼分析

標籤(空格分隔): tensorflow筆記


tensorflow筆記系列:
(一) tensorflow筆記:流程,概念和簡單程式碼註釋
(二) tensorflow筆記:多層CNN程式碼分析
(三) tensorflow筆記:多層LSTM程式碼分析
(四) tensorflow筆記:常用函式說明
(五) tensorflow筆記:模型的儲存與訓練過程視覺化
(六)tensorflow筆記:使用tf來實現word2vec


之前講過了tensorflow中CNN的示例程式碼,現在我們來看RNN的程式碼。不過好像官方只給了LSTM的程式碼。那麼我們就來看LSTM吧。LSTM的具體原理就不講了,可以參見深度學習筆記(五):LSTM,講的非常清楚。

坦白說,這份寫LSTM的程式碼有點難,倒不是說LSTM的原理有多難,而是這份程式碼中使用了大量tf提供的現成的操作函式。在精簡了程式碼的同時,也增加了初學者閱讀的難度。很多函式的用法我是去看原始碼,然後自己寫示例程式碼才搞懂的。當然如果能把整份程式碼搞清楚的話,掌握這麼多操作函式還是非常有用的。

這份程式碼並沒有完整的出現在tf給出的示例中見這裡,而是隻挑選了幾個片段簡略的介紹了一下。我當時看完之後簡直是一頭霧水。後來在github找到了這份程式碼的完整檔案,發現這份檔案只能在命令列裡面執行,需要輸入引數,例如

python ptb_word_lm.py --data_path=/tmp/simple-examples/data/ --model small

後來我改寫了一下,使之可以直接執行。當然,執行之前需要先手動下載資料集,資料集的地址在這裡


分段講解

總的來看,這份程式碼主要由三步分組成。
第一部分,是PTBModel,也是最核心的部分,負責tf中模型的構建和各種操作(op)的定義。
第二部分,是run_epoch函式,負責將所有文字內容分批餵給模型(PTBModel)訓練。
第三部分,就是main函式了,負責將第二部分的run_epoch執行多遍,也就是說,文字中的每個內容都會被重複多次的輸入到模型中進行訓練。隨著訓練的進行,會適當的進行一些引數的調整。
下面就按照這幾部分來分開講一下。我在後面提供了完整的程式碼,所以可以將完整程式碼和分段講解對照著看。


引數設定

在構建模型和訓練之前,我們首先需要設定一些引數。tf中可以使用tf.flags來進行全域性的引數設定

flags = tf.flags
logging = tf.logging    
flags.DEFINE_string(    # 定義變數 model的值為small, 後面的是註釋
"model", "small",
"A type of model. Possible options are: small, medium, large.")
flags.DEFINE_string("data_path",   #定義下載好的資料的存放位置
'/home/multiangle/download/simple-examples/data/', 
"data_path")
flags.DEFINE_bool("use_fp16", False,    # 是否使用 float16格式?
"Train using 16-bit floats instead of 32bit floats")
FLAGS = flags.FLAGS     # 可以使用FLAGS.model來呼叫變數 model的值。
def data_type():
return tf.float16 if FLAGS.use_fp16 else tf.float32

細心的人可能會注意到上面有行程式碼定義了model的值為small.這個是什麼意思呢?其實在後面的完整程式碼部分可以看到,作者在其中定義了幾個引數類,分別有small,medium,large和test這4種引數。如果model的值為small,則會呼叫SmallConfig,其他同樣。在SmallConfig中,有如下幾個引數:

init_scale = 0.1        # 相關引數的初始值為隨機均勻分佈,範圍是[-init_scale, init_scale]
learning_rate = 1.0     # 學習速率,在文字迴圈次數超過max_epoch以後會逐漸降低
max_grad_norm = 5       # 用於控制梯度膨脹,如果梯度向量的L2模超過max_grad_norm,則等比例縮小
num_layers = 2          # lstm層數
num_steps = 20          # 單個資料中,序列的長度。
hidden_size = 200       # 隱藏層中單元數目
max_epoch = 4           # epoch<max_epoch時,lr_decay值=1,epoch>max_epoch時,lr_decay逐漸減小
max_max_epoch = 13      # 指的是整個文字迴圈次數。
keep_prob = 1.0         # 用於dropout.每批資料輸入時神經網路中的每個單元會以1-keep_prob的概率不工作,可以防止過擬合
lr_decay = 0.5          # 學習速率衰減
batch_size = 20         # 每批資料的規模,每批有20個。
vocab_size = 10000      # 詞典規模,總共10K個詞

其他的幾個引數類中,引數型別都是一樣的,只是引數的值各有所不同。


PTBModel

這個可以說是核心部分了。而具體來說,又可以分成幾個小部分:多層LSTM結構的構建,輸入預處理,LSTM的迴圈,損失函式計算,梯度計算和修剪


LSTM結構

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  # 詞典規模
self._input_data = tf.placeholder(tf.int32, [batch_size, num_steps])    # 輸入
self._targets = tf.placeholder(tf.int32, [batch_size, num_steps])       # 預期輸出,兩者都是index序列,長度為num_step

首先引進引數,然後定義2個佔位符,分別表示輸入和預期輸出。注意此時不論是input還是target都是用詞典id來表示單詞的。

lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(size, forget_bias=0.0, state_is_tuple=True)

首先使用tf.nn.rnn_cell.BasicLSTMCell定義單個基本的LSTM單元。這裡的size其實就是hidden_size。
從原始碼中可以看到,在LSTM單元中,有2個狀態值,分別是c和h,分別對應於下圖中的c和h。其中h在作為當前時間段的輸出的同時,也是下一時間段的輸入的一部分。

此處輸入圖片的描述

那麼當state_is_tuple=True的時候,state是元組形式,state=(c,h)。如果是False,那麼state是一個由c和h拼接起來的張量,state=tf.concat(1,[c,h])。在執行時,則返回2值,一個是h,還有一個state。


DropoutWrapper

if is_training and config.keep_prob < 1: # 在外面包裹一層dropout
lstm_cell = tf.nn.rnn_cell.DropoutWrapper(
lstm_cell, output_keep_prob=config.keep_prob)

我們在這裡使用了dropout方法。所謂dropout,就是指網路中每個單元在每次有資料流入時以一定的概率(keep prob)正常工作,否則輸出0值。這是是一種有效的正則化方法,可以有效防止過擬合。在rnn中使用dropout的方法和cnn不同,推薦大家去把recurrent neural network regularization看一遍。
在rnn中進行dropout時,對於rnn的部分不進行dropout,也就是說從t-1時候的狀態傳遞到t時刻進行計算時,這個中間不進行memory的dropout;僅在同一個t時刻中,多層cell之間傳遞資訊的時候進行dropout,如下圖所示

此處輸入圖片的描述

上圖中,t-2時刻的輸入xt−2x_{t-2}首先傳入第一層cell,這個過程有dropout,但是從t−2時刻的第一層cell傳到t−1,t,t 1的第一層cell這個中間都不進行dropout。再從t 1時候的第一層cell向同一時刻內後續的cell傳遞時,這之間又有dropout了。

在使用tf.nn.rnn_cell.DropoutWrapper時,同樣有一些引數,例如input_keep_prob,output_keep_prob等,分別控制輸入和輸出的dropout概率,很好理解。


多層LSTM結構和狀態初始化

cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * config.num_layers, state_is_tuple=True) 
# 引數初始化,rnn_cell.RNNCell.zero_stat
self._initial_state = cell.zero_state(batch_size, data_type()) 

在這個示例中,我們使用了2層的LSTM網路。也就是說,前一層的LSTM的輸出作為後一層的輸入。使用tf.nn.rnn_cell.MultiRNNCell可以實現這個功能。這個基本沒什麼好說的,state_is_tuple用法也跟之前的類似。構造完多層LSTM以後,使用zero_state即可對各種狀態進行初始化。


輸入預處理

with tf.device("/cpu:0"):
embedding = tf.get_variable(
# vocab size * hidden size, 將單詞轉成embedding描述
"embedding", [vocab_size, size], dtype=data_type()) 
# 將輸入seq用embedding表示, shape=[batch, steps, hidden_size]
inputs = tf.nn.embedding_lookup(embedding, self._input_data)
if is_training and config.keep_prob < 1:
inputs = tf.nn.dropout(inputs, config.keep_prob)

之前有提到過,輸入模型的input和target都是用詞典id表示的。例如一個句子,“我/是/學生”,這三個詞在詞典中的序號分別是0,5,3,那麼上面的句子就是[0,5,3]。顯然這個是不能直接用的,我們要把詞典id轉化成向量,也就是embedding形式。可能有些人已經聽到過這種描述了。實現的方法很簡單。

第一步,構建一個矩陣,就叫embedding好了,尺寸為[vocab_size, embedding_size],分別表示詞典中單詞數目,以及要轉化成的向量的維度。一般來說,向量維度越高,能夠表現的資訊也就越豐富。

第二步,使用tf.nn.embedding_lookup(embedding,input_ids) 假設input_ids的長度為len,那麼返回的張量尺寸就為[len,embedding_size]。舉個栗子

# 示例程式碼
import tensorflow as tf
import numpy as np
sess = tf.InteractiveSession()
embedding = tf.Variable(np.identity(5,dtype=np.int32))
input_ids = tf.placeholder(dtype=tf.int32,shape=[None])
input_embedding = tf.nn.embedding_lookup(embedding,input_ids)
sess.run(tf.initialize_all_variables())
print(sess.run(embedding))
#[[1 0 0 0 0]
# [0 1 0 0 0]
# [0 0 1 0 0]
# [0 0 0 1 0]
# [0 0 0 0 1]]
print(sess.run(input_embedding,feed_dict={input_ids:[1,2,3,0,3,2,1]}))
#[[0 1 0 0 0]
# [0 0 1 0 0]
# [0 0 0 1 0]
# [1 0 0 0 0]
# [0 0 0 1 0]
# [0 0 1 0 0]
# [0 1 0 0 0]]

第三步,如果keep_prob<1, 那麼還需要對輸入進行dropout。不過這邊跟rnn的dropout又有所不同,這邊使用tf.nn.dropout。


LSTM迴圈

現在,多層lstm單元已經定義完畢,輸入也已經經過預處理了。那麼現在要做的就是將資料輸入lstm進行訓練了。其實很簡單,只要按照文字順序依次向cell輸入資料就好了。lstm上一時間段的狀態會自動參與到當前時間段的輸出和狀態的計算當中。

outputs = []
state = self._initial_state # state 表示 各個batch中的狀態
with tf.variable_scope("RNN"):
for time_step in range(num_steps):
if time_step > 0: tf.get_variable_scope().reuse_variables()
# cell_out: [batch, hidden_size]
(cell_output, state) = cell(inputs[:, time_step, :], state) # 按照順序向cell輸入文字資料
outputs.append(cell_output)  # output: shape[num_steps][batch,hidden_size]
# 把之前的list展開,成[batch, hidden_size*num_steps],然後 reshape, 成[batch*numsteps, hidden_size]
output = tf.reshape(tf.concat(1, outputs), [-1, size])

這邊要注意,tf.get_variable_scope().reuse_variables()這行程式碼不可少,不然會報錯,應該是因為同一命名域(variable_scope)內不允許存在多個同一名字的變數的原因。


損失函式計算

# softmax_w , shape=[hidden_size, vocab_size], 用於將distributed表示的單詞轉化為one-hot表示
softmax_w = tf.get_variable(
"softmax_w", [size, vocab_size], dtype=data_type())
softmax_b = tf.get_variable("softmax_b", [vocab_size], dtype=data_type())
# [batch*numsteps, vocab_size] 從隱藏語義轉化成完全表示
logits = tf.matmul(output, softmax_w)   softmax_b
# loss , shape=[batch*num_steps]
# 帶權重的交叉熵計算
loss = tf.nn.seq2seq.sequence_loss_by_example(
[logits],   # output [batch*numsteps, vocab_size]
[tf.reshape(self._targets, [-1])],  # target, [batch_size, num_steps] 然後展開成一維【列表】
[tf.ones([batch_size * num_steps], dtype=data_type())]) # weight
self._cost = cost = tf.reduce_sum(loss) / batch_size # 計算得到平均每批batch的誤差
self._final_state = state

上面程式碼的上半部分主要用來將多層lstm單元的輸出轉化成one-hot表示的向量。關於one-hot presentation和distributed presentation的區別,可以參考這裡

程式碼的下半部分,正式開始計算損失函式。這裡使用了tf提供的現成的交叉熵計算函式,tf.nn.seq2seq.sequence_loss_by_example。不知道交叉熵是什麼?見這裡各個變數的具體shape我都在註釋中標明瞭。注意其中的self._targets是詞典id表示的。這個函式的具體實現方式不明。我曾經想自己手寫一個交叉熵,不過好像tf不支援對張量中單個元素的操作。


梯度計算

之前已經計算得到了每批資料的平均誤差。那麼下一步,就是根據誤差來進行引數修正了。當然,首先必須要求梯度

self._lr = tf.Variable(0.0, trainable=False)  # lr 指的是 learning_rate
tvars = tf.trainable_variables()

通過tf.trainable_variables 可以得到整個模型中所有trainable=True的Variable。實際得到的tvars是一個列表,裡面存有所有可以進行訓練的變數。

grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars),
config.max_grad_norm)

這一行程式碼其實使用了兩個函式,tf.gradients 和 tf.clip_by_global_norm。 我們一個一個來。

tf.gradients
用來計算導數。該函式的定義如下所示

def gradients(ys,
xs,
grad_ys=None,
name="gradients",
colocate_gradients_with_ops=False,
gate_gradients=False,
aggregation_method=None):

雖然可選引數很多,但是最常使用的還是ys和xs。根據說明得知,ys和xs都可以是一個tensor或者tensor列表。而計算完成以後,該函式會返回一個長為len(xs)的tensor列表,列表中的每個tensor是ys中每個值對xs[i]求導之和。如果用數學公式表示的話,那麼 g = tf.gradients(y,x)可以表示成

gi=∑j=0len(y)∂yj∂xig=[g0,g1,…,glen(x)]

g_i=\sum_{j=0}^{len(y)} \frac{\partial y_j}{\partial x_i}\\
g=[g_0,g_1,…,g_{len(x)}]


梯度修剪

tf.clip_by_global_norm
修正梯度值,用於控制梯度爆炸的問題。梯度爆炸和梯度彌散的原因一樣,都是因為鏈式法則求導的關係,導致梯度的指數級衰減。為了避免梯度爆炸,需要對梯度進行修剪。
先來看這個函式的定義:

def clip_by_global_norm(t_list, clip_norm, use_norm=None, name=None):

輸入引數中:t_list為待修剪的張量, clip_norm 表示修剪比例(clipping ratio).

函式返回2個引數: list_clipped,修剪後的張量,以及global_norm,一箇中間計算量。當然如果你之前已經計算出了global_norm值,你可以在use_norm選項直接指定global_norm的值。

那麼具體如何計算呢?根據原始碼中的說明,可以得到
list_clipped[i]=t_list[i] * clip_norm / max(global_norm, clip_norm),其中
global_norm = sqrt(sum([l2norm(t)**2 for t in t_list]))

如果你更熟悉數學公式,則可以寫作

Lic=Lit∗Ncmax(Nc,Ng)Ng=∑i(Lit)2‾‾‾‾‾‾‾‾√

L_c^i=\frac{L_t^i*N_c}{max(N_c,N_g)} \\
N_g = \sqrt{\sum_i(L_t^i)^2}

其中,
LicL_c^i和LigL_g^i代表t_list[i]和list_clipped[i],
NcN_c和NgN_g代表clip_norm 和 global_norm的值。
其實也可以看到其實NgN_g就是t_list的L2模。上式也可以進一步寫作

Lic={Lit,(Ng<=Nc)Lit∗NcNg,(Ng>Nc)Ng=∑i(Lit)2‾‾‾‾‾‾‾‾√

\begin{align}
&L_c^i=
\begin{cases}
L_t^i, (N_gN_c)
\end{cases}\\
&N_g = \sqrt{\sum_i(L_t^i)^2}
\end{align}

也就是說,當t_list的L2模大於指定的NcN_c時,就會對t_list做等比例縮放


優化引數

之前的程式碼已經求得了合適的梯度,現在需要使用這些梯度來更新引數的值了。

# 梯度下降優化,指定學習速率
optimizer = tf.train.GradientDescentOptimizer(self._lr)
# optimizer = tf.train.AdamOptimizer()
# optimizer = tf.train.GradientDescentOptimizer(0.5)
self._train_op = optimizer.apply_gradients(zip(grads, tvars))  # 將梯度應用於變數
# self._train_op = optimizer.minimize(grads)

這一部分就比較自由了,tf提供了很多種優化器,例如最常用的梯度下降優化(GradientDescentOptimizer)也可以使用AdamOptimizer。這裡使用的是梯度優化。值得注意的是,這裡使用了optimizer.apply_gradients來將求得的梯度用於引數修正,而不是之前簡單的optimizer.minimize(cost)

還有一點,要留心一下self._train_op,只有該操作被模型執行,才能對引數進行優化。如果沒有執行該操作,則引數就不會被優化。


run_epoch

這就是我之前講的第二部分,主要功能是將所有文件分成多個批次交給模型去訓練,同時記錄模型返回的cost,state等記錄,並階段性的將結果輸出。

def run_epoch(session, model, data, eval_op, verbose=False):
"""Runs the model on the given data."""
# epoch_size 表示批次總數。也就是說,需要向session喂這麼多批資料
epoch_size = ((len(data) // model.batch_size) - 1) // model.num_steps  # // 表示整數除法
start_time = time.time()
costs = 0.0
iters = 0
state = session.run(model.initial_state)
for step, (x, y) in enumerate(reader.ptb_iterator(data, model.batch_size,
model.num_steps)):
fetches = [model.cost, model.final_state, eval_op] # 要獲取的值
feed_dict = {}      # 設定input和target的值
feed_dict[model.input_data] = x
feed_dict[model.targets] = y
for i, (c, h) in enumerate(model.initial_state):
feed_dict[c] = state[i].c  
feed_dict[h] = state[i].h
cost, state, _ = session.run(fetches, feed_dict) # 執行session,獲得cost和state
costs  = cost   # 將 cost 累積
iters  = model.num_steps
if verbose and step % (epoch_size // 10) == 10:  # 也就是每個epoch要輸出10個perplexity值
print("%.3f perplexity: %.3f speed: %.0f wps" %
(step * 1.0 / epoch_size, np.exp(costs / iters),
iters * model.batch_size / (time.time() - start_time)))
return np.exp(costs / iters)

基本沒什麼其他的,就是要注意傳入的eval_op。在訓練階段,會往其中傳入train_op,這樣模型就會自動進行優化;而在交叉檢驗和測試階段,傳入的是tf.no_op,此時模型就不會優化。


main函式

這裡略去了資料讀取和引數讀取的程式碼,只貼了最關鍵的一部分。

with tf.Graph().as_default(), tf.Session() as session:
# 定義如何對引數變數初始化
initializer = tf.random_uniform_initializer(-config.init_scale, 
config.init_scale)
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)

注意這裡定義了3個模型,對於訓練模型,is_trainable=True; 而對於交叉檢驗和測試模型,is_trainable=False

    summary_writer = tf.train.SummaryWriter('/tmp/lstm_logs',session.graph)
tf.initialize_all_variables().run()  # 對引數變數初始化
for i in range(config.max_max_epoch):   # 所有文字要重複多次進入模型訓練
# learning rate 衰減
# 在 遍數小於max epoch時, lr_decay = 1 ; > max_epoch時, lr_decay = 0.5^(i-max_epoch)
lr_decay = config.lr_decay ** max(i - config.max_epoch, 0.0)
m.assign_lr(session, config.learning_rate * lr_decay) # 設定learning rate
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)

注意上面train_perplexity操作中傳入了m.train_op,表示要進行優化,而在valid_perplexity和test_perplexity中均傳入了tf.no_op,表示不進行優化。


完整程式碼和註釋

# Copyright 2015 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Example / benchmark for building a PTB LSTM model.
Trains the model described in:
(Zaremba, et. al.) Recurrent Neural Network Regularization
http://arxiv.org/abs/1409.2329
There are 3 supported model configurations:
===========================================
| config | epochs | train | valid  | test
===========================================
| small  | 13     | 37.99 | 121.39 | 115.91
| medium | 39     | 48.45 |  86.16 |  82.07
| large  | 55     | 37.87 |  82.62 |  78.29
The exact results may vary depending on the random initialization.
The hyperparameters used in the model:
- init_scale - the initial scale of the weights
- learning_rate - the initial value of the learning rate
- max_grad_norm - the maximum permissible norm of the gradient
- num_layers - the number of LSTM layers
- num_steps - the number of unrolled steps of LSTM
- hidden_size - the number of LSTM units
- max_epoch - the number of epochs trained with the initial learning rate
- max_max_epoch - the total number of epochs for training
- keep_prob - the probability of keeping weights in the dropout layer
- lr_decay - the decay of the learning rate for each epoch after "max_epoch"
- batch_size - the batch size
The data required for this example is in the data/ dir of the
PTB dataset from Tomas Mikolov's webpage:
$ wget http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz
$ tar xvf simple-examples.tgz
To run:
$ python ptb_word_lm.py --data_path=simple-examples/data/
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import time
import numpy as np
import tensorflow as tf
from tensorflow.models.rnn.ptb import reader
flags = tf.flags
logging = tf.logging
flags.DEFINE_string(
"model", "small",
"A type of model. Possible options are: small, medium, large.")
flags.DEFINE_string("data_path", '/home/multiangle/download/simple-examples/data/', "data_path")
flags.DEFINE_bool("use_fp16", False,
"Train using 16-bit floats instead of 32bit floats")
FLAGS = flags.FLAGS
def data_type():
return tf.float16 if FLAGS.use_fp16 else tf.float32
class PTBModel(object):
"""The PTB model."""
def __init__(self, is_training, config):
"""
:param is_training: 是否要進行訓練.如果is_training=False,則不會進行引數的修正。
"""
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
self._input_data = tf.placeholder(tf.int32, [batch_size, num_steps])    # 輸入
self._targets = tf.placeholder(tf.int32, [batch_size, num_steps])       # 預期輸出,兩者都是index序列,長度為num_step
# 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 = tf.nn.rnn_cell.BasicLSTMCell(size, forget_bias=0.0, state_is_tuple=True)
if is_training and config.keep_prob < 1: # 在外面包裹一層dropout
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) # 多層lstm cell 堆疊起來
self._initial_state = cell.zero_state(batch_size, data_type()) # 引數初始化,rnn_cell.RNNCell.zero_state
with tf.device("/cpu:0"):
embedding = tf.get_variable(
"embedding", [vocab_size, size], dtype=data_type()) # vocab size * hidden size, 將單詞轉成embedding描述
# 將輸入seq用embedding表示, shape=[batch, steps, hidden_size]
inputs = tf.nn.embedding_lookup(embedding, self._input_data)
if is_training and config.keep_prob < 1:
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:
#
# inputs = [tf.squeeze(input_, [1])
#           for input_ in tf.split(1, num_steps, inputs)]
# outputs, state = tf.nn.rnn(cell, inputs, initial_state=self._initial_state)
outputs = []
state = self._initial_state # state 表示 各個batch中的狀態
with tf.variable_scope("RNN"):
for time_step in range(num_steps):
if time_step > 0: tf.get_variable_scope().reuse_variables()
# cell_out: [batch, hidden_size]
(cell_output, state) = cell(inputs[:, time_step, :], state)
outputs.append(cell_output)  # output: shape[num_steps][batch,hidden_size]
# 把之前的list展開,成[batch, hidden_size*num_steps],然後 reshape, 成[batch*numsteps, hidden_size]
output = tf.reshape(tf.concat(1, outputs), [-1, size])
# softmax_w , shape=[hidden_size, vocab_size], 用於將distributed表示的單詞轉化為one-hot表示
softmax_w = tf.get_variable(
"softmax_w", [size, vocab_size], dtype=data_type())
softmax_b = tf.get_variable("softmax_b", [vocab_size], dtype=data_type())
# [batch*numsteps, vocab_size] 從隱藏語義轉化成完全表示
logits = tf.matmul(output, softmax_w)   softmax_b
# loss , shape=[batch*num_steps]
# 帶權重的交叉熵計算
loss = tf.nn.seq2seq.sequence_loss_by_example(
[logits],   # output [batch*numsteps, vocab_size]
[tf.reshape(self._targets, [-1])],  # target, [batch_size, num_steps] 然後展開成一維【列表】
[tf.ones([batch_size * num_steps], dtype=data_type())]) # weight
self._cost = cost = tf.reduce_sum(loss) / batch_size # 計算得到平均每批batch的誤差
self._final_state = state
if not is_training:  # 如果沒有訓練,則不需要更新state的值。
return
self._lr = tf.Variable(0.0, trainable=False)
tvars = tf.trainable_variables()
# clip_by_global_norm: 梯度衰減,具體演算法為t_list[i] * clip_norm / max(global_norm, clip_norm)
# 這裡gradients求導,ys和xs都是張量
# 返回一個長為len(xs)的張量,其中的每個元素都是\grad{\frac{dy}{dx}}
# clip_by_global_norm 用於控制梯度膨脹,前兩個引數t_list, global_norm, 則
# t_list[i] * clip_norm / max(global_norm, clip_norm)
# 其中 global_norm = sqrt(sum([l2norm(t)**2 for t in t_list]))
grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars),
config.max_grad_norm)
# 梯度下降優化,指定學習速率
optimizer = tf.train.GradientDescentOptimizer(self._lr)
# optimizer = tf.train.AdamOptimizer()
# optimizer = tf.train.GradientDescentOptimizer(0.5)
self._train_op = optimizer.apply_gradients(zip(grads, tvars))  # 將梯度應用於變數
self._new_lr = tf.placeholder(
tf.float32, shape=[], name="new_learning_rate")     #   用於外部向graph輸入新的 lr值
self._lr_update = tf.assign(self._lr, self._new_lr)     #   使用new_lr來更新lr的值
def assign_lr(self, session, lr_value):
# 使用 session 來呼叫 lr_update 操作
session.run(self._lr_update, feed_dict={self._new_lr: lr_value})
@property
def input_data(self):
return self._input_data
@property
def targets(self):
return self._targets
@property
def initial_state(self):
return self._initial_state
@property
def cost(self):
return self._cost
@property
def final_state(self):
return self._final_state
@property
def lr(self):
return self._lr
@property
def train_op(self):
return self._train_op
class SmallConfig(object):
"""Small config."""
init_scale = 0.1        #
learning_rate = 1.0     # 學習速率
max_grad_norm = 5       # 用於控制梯度膨脹,
num_layers = 2          # lstm層數
num_steps = 20          # 單個資料中,序列的長度。
hidden_size = 200       # 隱藏層規模
max_epoch = 4           # epoch<max_epoch時,lr_decay值=1,epoch>max_epoch時,lr_decay逐漸減小
max_max_epoch = 13      # 指的是整個文字迴圈13遍。
keep_prob = 1.0
lr_decay = 0.5          # 學習速率衰減
batch_size = 20         # 每批資料的規模,每批有20個。
vocab_size = 10000      # 詞典規模,總共10K個詞
class MediumConfig(object):
"""Medium config."""
init_scale = 0.05
learning_rate = 1.0
max_grad_norm = 5
num_layers = 2
num_steps = 35
hidden_size = 650
max_epoch = 6
max_max_epoch = 39
keep_prob = 0.5
lr_decay = 0.8
batch_size = 20
vocab_size = 10000
class LargeConfig(object):
"""Large config."""
init_scale = 0.04
learning_rate = 1.0
max_grad_norm = 10
num_layers = 2
num_steps = 35
hidden_size = 1500
max_epoch = 14
max_max_epoch = 55
keep_prob = 0.35
lr_decay = 1 / 1.15
batch_size = 20
vocab_size = 10000
class TestConfig(object):
"""Tiny config, for testing."""
init_scale = 0.1
learning_rate = 1.0
max_grad_norm = 1
num_layers = 1
num_steps = 2
hidden_size = 2
max_epoch = 1
max_max_epoch = 1
keep_prob = 1.0
lr_decay = 0.5
batch_size = 20
vocab_size = 10000
def run_epoch(session, model, data, eval_op, verbose=False):
"""Runs the model on the given data."""
# epoch_size 表示批次總數。也就是說,需要向session喂這麼多次資料
epoch_size = ((len(data) // model.batch_size) - 1) // model.num_steps  # // 表示整數除法
start_time = time.time()
costs = 0.0
iters = 0
state = session.run(model.initial_state)
for step, (x, y) in enumerate(reader.ptb_iterator(data, model.batch_size,
model.num_steps)):
fetches = [model.cost, model.final_state, eval_op] # 要進行的操作,注意訓練時和其他時候eval_op的區別
feed_dict = {}      # 設定input和target的值
feed_dict[model.input_data] = x
feed_dict[model.targets] = y
for i, (c, h) in enumerate(model.initial_state):
feed_dict[c] = state[i].c   # 這部分有什麼用?看不懂
feed_dict[h] = state[i].h
cost, state, _ = session.run(fetches, feed_dict) # 執行session,獲得cost和state
costs  = cost   # 將 cost 累積
iters  = model.num_steps
if verbose and step % (epoch_size // 10) == 10:  # 也就是每個epoch要輸出10個perplexity值
print("%.3f perplexity: %.3f speed: %.0f wps" %
(step * 1.0 / epoch_size, np.exp(costs / iters),
iters * model.batch_size / (time.time() - start_time)))
return np.exp(costs / iters)
def get_config():
if FLAGS.model == "small":
return SmallConfig()
elif FLAGS.model == "medium":
return MediumConfig()
elif FLAGS.model == "large":
return LargeConfig()
elif FLAGS.model == "test":
return TestConfig()
else:
raise ValueError("Invalid model: %s", FLAGS.model)
# def main(_):
if __name__=='__main__':
if not FLAGS.data_path:
raise ValueError("Must set --data_path to PTB data directory")
print(FLAGS.data_path)
raw_data = reader.ptb_raw_data(FLAGS.data_path) # 獲取原始資料
train_data, valid_data, test_data, _ = raw_data
config = get_config()
eval_config = get_config()
eval_config.batch_size = 1
eval_config.num_steps = 1
with tf.Graph().as_default(), tf.Session() as session:
initializer = tf.random_uniform_initializer(-config.init_scale, # 定義如何對引數變數初始化
config.init_scale)
with tf.variable_scope("model", reuse=None,initializer=initializer):
m = PTBModel(is_training=True, config=config)   # 訓練模型, is_trainable=True
with tf.variable_scope("model", reuse=True,initializer=initializer):
mvalid = PTBModel(is_training=False, config=config) #  交叉檢驗和測試模型,is_trainable=False
mtest = PTBModel(is_training=False, config=eval_config)
summary_writer = tf.train.SummaryWriter('/tmp/lstm_logs',session.graph)
tf.initialize_all_variables().run()  # 對引數變數初始化
for i in range(config.max_max_epoch):   # 所有文字要重複多次進入模型訓練
# learning rate 衰減
# 在 遍數小於max epoch時, lr_decay = 1 ; > max_epoch時, lr_decay = 0.5^(i-max_epoch)
lr_decay = config.lr_decay ** max(i - config.max_epoch, 0.0)
m.assign_lr(session, config.learning_rate * lr_decay) # 設定learning rate
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)
# if __name__ == "__main__":
#     tf.app.run()