完整示例訓練過程用到的APIphp
1 import tensorflow as tf 2 3 dataset = tf.data.TextLineDataset(file_path) 4 # tf.string_split 5 # tf.string_to_number 6 # tf.size 7 # tf.data.Dataset.zip 8 # tf.concat 9 10 # tf.logical_and 11 # tf.logical_not 12 # tf.logical_or 13 # 14 # tf.greater 15 # tf.greater_equal 16 # tf.less 17 # tf.less_equal 18 # tf.not_equal 19 20 # dataset.map 21 # dataset.filter 22 # dataset.shuffle 23 # dataset.padded_batch 24 # dataset.make_one_shot_iterator 25 # dataset.make_initializable_iterator 26 27 # tf.TensorShape 28 # tf.nn.rnn_cell.MultiRNNCell 29 # tf.nn.rnn_cell.BasicLSTMCell 30 # tf.transpose 31 # tf.random_uniform_initializer 32 # tf.nn.embedding_lookup 33 # tf.nn.dropout 34 # tf.nn.dynamic_rnn 35 # tf.nn.sparse_softmax_cross_entropy_with_logits 36 # tf.nn.softmax_cross_entropy_with_logits 37 # tf.sequence_mask 38 39 # tf.reduce_sum 40 # tf.reduce_mean 41 # tf.reduce_max 42 # tf.reduce_min 43 # tf.reduce_any 44 # tf.reduce_all 45 # tf.reduce_prod 46 47 # tf.to_float 48 # tf.to_double 49 # tf.to_int32 50 # tf.to_int64 51 # tf.to_bfloat16 52 53 # optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0) 54 # optimizer.apply_gradients 55 56 # tf.clip_by_global_norm 57 # tf.clip_by_value 58 # tf.clip_by_norm 59 # tf.clip_by_average_norm
tf.reduce_all和tf.reduce_anygit
1 a = [[False, True], [True, True], [True, True]] 2 l = tf.convert_to_tensor(a) 3 s1 = tf.reduce_all(l) # Computes the "logical and" of elements across dimensions of a tensor. 4 s2 = tf.reduce_any(l) # Computes the "logical or" of elements across dimensions of a tensor. 5 6 print(l, s1, s2) 7 with tf.Session() as sess: 8 print(sess.run([l, s1, s2])) 9 10 # [array([[False, True], 11 # [ True, True], 12 # [ True, True]]), False, True]
編碼過程用到的APIgithub
1 # tf.convert_to_tensor 2 # init_array = tf.TensorArray 3 # init_array.read 返回一個元素 4 # init_array.write 返回整個數組 5 # init_array.stack 一次性取出所有 6 # 7 # tf.reduce_all 8 # self.dec_cell.call 9 # tf.while_loop
這一章將介紹如何使用深度學習方法解決天然語言處理問題。算法
9.1 語言模型的背景知識數組
語言模型是天然語言處理問題中一類最基本的問題,它有着很是普遍的應用,也是理解更加複雜的天然語言處理問題的基礎。網絡
9.1.1 語言模型簡介session
假設一門語言中全部可能的句子服從某一個機率分佈,每一個句子出現的機率加起來爲1,那麼「語言模型」的任務就是預測每一個句子在語言中出現的機率。把句子當作單詞的序列,語言模型能夠表示爲一個計算p(w1, w2, w3,…, wm)的模型。語言模型僅僅對句子出現的機率進行建模,並不嘗試去「理解」句子的內容含義。app
語言模型有不少應用。不少生成天然語言文本的應用都依賴語言模型來優化輸出文本的流暢性。生成的句子在語言模型中的機率越高,說明其越有多是一個流程、天然的句子。例如在輸入法中,假設輸入的拼音串爲「xianzaiquna」,輸出多是」西安在去哪「,也多是」如今去哪「,這時輸入法就利用語言模型比較兩個輸出的機率,得出」如今去哪「更有多是用戶所須要的輸出。在統計機器翻譯的噪聲信道模型(Noise Channel Model)中,每一個候選翻譯的機率由一個翻譯模型和一個語言模型共同決定,其中的語言模型就起到了在目標語言中挑選較爲合理的句子的做用。在9.3小節將看到,神經網絡機器翻譯的Seq2Seq模型能夠看做是一個條件語言模型(Conditional Language Model),它至關因而在給定輸入的狀況下對目標語言的全部句子估算機率,並選擇其中機率最大的句子做爲輸出。框架
計算一個句子的機率:less
首先一個句子能夠被當作是一個單詞序列:
S = (w1, w2, w3,…, wm),
其中m爲句子的長度。那麼,它的機率能夠表示爲
p(S) = p(w1, w2, w3,…, wm) =
p(w1)p(w2|w1)p(w3|w1, w2,) … p(wm|w1, w2, w3,…, wm-1),
p(wm|w1, w2, w3,…, wm-1)表示,已知前m-1個單詞時,第m個單詞爲wm的條件機率。若是能對這一項建模,那麼只要把每一個位置的條件機率相乘,就能計算一個句子出現的機率。然而通常來講,任何一門語言的詞彙量都很大,詞彙的組合更是不可勝數。假設一門語言的詞彙量爲V,若是要將p(wm|w1, w2, w3,…, wm-1)的全部參數保存一個模型裏,將須要vm個參數,通常的句子長度遠遠超出了實際可行的範圍。爲了評估這些參數的取值,常見的方法由n-gram模型、決策樹、最大熵模型、條件隨機場、神經網絡模型等。這裏先以其中最簡單的n-gram模型來介紹語言模型問題。
爲了控制參數數量,n-gram模型作了一個有限歷史假設:當前單詞的出現機率僅僅與前面的n-1個單詞相關,所以以上公式能夠近似爲
p(S) = p(w1, w2, w3,…, wm) = Πimp(wi| w1, w2, w3,…, wi-1)。
n-gram模型裏的n指的是當前單詞依賴它前面的單詞的個數。一般n能夠取一、二、三、4。n-gram模型中須要預估的參數爲條件機率p(wi|wi-n+1,…, wi-1)。假設某種語言的單詞表大小爲V,那麼n-gram模型須要估計的不一樣參數數量爲O(vn)量級。當n越大時,模型在理論上越準確,但也越複雜,須要的計算量和訓練語料數據量也就越大,由於n取>=4的狀況很是少。
n-gram模型的參數通常採用最大似然估計(Maximum Likelihood Estimation, MLE)方法計算:
p(wi|wi-n+1,…, wi-1) = C(wi-n+1,…, wi-1, wi) / C(wi-n+1,…, wi-1),
其中C(X)表示單詞序列X在訓練語料中出現的次數。訓練語料的規模越大,參數估計的結果越可靠。但即便訓練數據的規模很是大時,仍是有不少單詞序列在訓練語料中不會出現,這就會致使不少參數爲0。爲了不由於乘以0而致使整個句子機率爲0,使用最大似然估計方法時須要加入平滑避免參數取值爲0。
9.1.2 語言模型的評價方法
語言模型效果好壞的經常使用評價標準是複雜度(perplexity)。在測試集上獲得的複雜度越低,說明建模效果越好。計算perplexity值的公式以下:
簡單來講,perplexity值刻畫的是語言模型預測一個語言樣本的能力。
從上面的定義中能夠看出,perplexity實際是計算每一個單詞獲得的機率倒數的幾何平均,所以perplexity能夠理解爲平均分支系數(average branching factor),即模型預測下一個詞時的平都可選擇數量。例如,考慮一個由0~9這10個數字隨機組成的長度爲m的序列,因爲這10個數字出現的機率是隨機的,因此每一個數字出現的機率是1//10,所以,在任什麼時候刻,模型都有10個等機率的候選答案能夠選擇,因而perplexity就是10。計算過程以下:
在語言模型的訓練中,一般採用perplexity的對數表達形式:
相比先乘積再求平方根的方式,採用對數形式(由於能夠轉爲加法運算)能夠加速計算,同時避免機率乘積數值太小致使浮點數向下溢出的問題。
在數學上,log perplexity能夠當作真實分佈與預測分佈之間的交叉熵。假設x是一個離散變量,μ(x)和v(x)是兩個與x相關的機率分佈,那麼μ和v之間交叉熵的定義是在分佈μ下-log(v(x))的指望值:
把x看做單詞,μ(x)爲每一個位置上單詞的真實分佈,v(x)爲模型的預測分佈p(wi|wi-n+1,…, wi-1),就能夠看出log perplexity和交叉熵是等價的。惟一的區別在於,因爲語言的真實分佈是未知的,所以在log perplexity的定義中,真實分佈用測試語料中的取樣代替,即認爲在給定上文w1, w2, w3,…, wi-1的條件下,語料中出現單詞 wi 的機率爲1,出現其餘單詞的機率均爲0。
在神經網絡模型中,p(wi| w1, w2, w3,…, wi-1)分佈一般是由一個softmax層產生的。這時tensorflow提供了兩個方便計算交叉熵的函數:tf.nn.softmax_cross_entropy_with_logits和tf.nn.sparse_softmax_cross_entropy_with_logits。
1 import tensorflow as tf 2 3 # tf.nn.softmax_cross_entropy_with_logits與tf.nn.sparse_softmax_cross_entropy_with_logits的區別在於 4 # 前者須要預測目標(即label)以機率分佈的形式給出 5 6 # 假設詞彙表大小爲3,語料包含有2個單詞,分別爲2和0 7 word_labels = tf.constant([2, 0]) 8 9 # 假設模型對兩個單詞預測時,產生的logit分別爲[2.0, -1.0, 3.0]和[1.0, 0.0, -0.5] 10 predict_logits = tf.constant([[2.0, -1.0, 3.0], [1.0, 0.0, -0.5]]) 11 12 # 交叉熵 13 loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=word_labels, logits=predict_logits) 14 15 # softmax_cross_entropy_with_logits須要將預測目標(即label)以機率分佈的形式給出 16 word_prob_distribution = tf.constant([[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]]) 17 loss2 = tf.nn.softmax_cross_entropy_with_logits(labels=word_prob_distribution, logits=predict_logits) 18 19 with tf.Session() as sess: 20 print(sess.run([loss, loss2])) 21 22 # 結果: 對應兩個預測的perplexity損失 23 # [array([0.32656264, 0.4643688 ], dtype=float32), array([0.32656264, 0.4643688 ], dtype=float32)] 24 25 # 因爲softmax_cross_entropy_with_logits容許提供一個機率分佈,所以在使用時有更大的自由度。 26 # 例如,一種叫label smoothing的技巧是將正確數據的機率設爲一個比1.0略小的值,將錯誤數據的機率設爲比0.0略大的值,這樣能夠避免模型與數據過擬合,在某些時候能夠提升訓練效果。 27 word_prob_smooth = tf.constant([[0.01, 0.01, 0.98], [0.98, 0.01, 0.01]]) 28 loss3 = tf.nn.softmax_cross_entropy_with_logits(labels=word_prob_smooth, logits=predict_logits) 29 30 with tf.Session() as sess: 31 print(sess.run(loss3)) 32 # [0.37656265 0.48936883]
9.2 神經語言模型
上節提到,n-gram模型爲了控制參數數量,須要將上下文信息控制在幾個單詞之內。也就是說,在預測下一個單詞時,n-gram模型只能考慮前n個單詞的信息,這對語言模型的能力形成了很大的限制。與之相比,循環神經網絡能夠將任意長度的上文信息存儲在隱藏狀態中。所以使用循環神經網絡做爲語言模型有着自然的優點。
因爲隱藏狀態的維度有限,它並不能真的存儲’全部‘的上文信息,一般來講,距離越遠的上文對下一個單詞的影響越小,所以存儲’全部‘信息也並不是必要。研究表面,在神經語言模型中保留13個單詞的上文信息大體能夠取得與保留全部上文信息相同的效果。
與圖像數據不一樣,天然語言文本數據沒法直接被當作數據值提供給神經網絡,須要進行預處理。
9.2.1 預處理
9.2.2 batching
9.2.3 完整示例
9.2.1 PTB數據集的預處理
PTB(Penn Treebank Dataset)文本數據集是目前語言模型學習中使用最普遍的數據集。
下載地址:http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz
1 import collections 2 from operator import itemgetter 3 4 RAW_DATA = '/home/yangxl/files/ptb/ptb.train.txt' 5 VOCAB_OUTPUT = '/home/yangxl/files/ptb/ptb.vocab' 6 7 counter = collections.Counter() 8 with open(RAW_DATA, 'r') as f: 9 for line in f.readlines(): 10 for word in line.strip().split(): 11 counter[word] += 1 12 13 # 按詞頻順序對單詞進行排序 14 sorted_word_to_cnt = sorted(counter.items(), key=itemgetter(1), reverse=True) 15 sorted_words = [x[0] for x in sorted_word_to_cnt] 16 17 # 稍後咱們須要在文本換行處加入句子結束符'<eos>',這裏預先將其加入詞彙表。
# 這個句子結束符應該就至關於翻譯模型中的'_'吧。 18 sorted_words = ['<eos>'] + sorted_words 19 20 # 在9.3.2小節處理機器翻譯數據時,除了'<eos>',還須要將'<unk>'和句子起始符'<sos>'加入詞彙表,並從詞彙表中刪除低頻慈湖。
# 在PTB數據中,由於輸入數據已經將低頻詞彙替換爲'unk',所以不須要這一步驟。
# 剛好10000個詞彙。 21 # sorted_words = ['<unk>', '<sos>', '<eos>'] + sorted_words 22 # if len(sorted_words) > 10000: 23 # sorted_words = sorted_words[:10000] 24 25 with open(VOCAB_OUTPUT, 'w') as f_out: 26 for word in sorted_words: 27 f_out.write(word + '\n')
在肯定了詞彙表(就一個詞彙表)以後,再將訓練文件、測試文件等根據詞彙表文件轉化爲單詞編號。每一個單詞的編號就是它在詞彙文件中的行號。(說是替換,實際上是從新建立了一個新文件)
1 RAW_DATA = '/home/error/simple-examples/data/ptb.test.txt' 2 VACAB = '/home/error/ptb/test.vocab' 3 OUTPUT_DATA = '/home/error/ptb/ptb.test' 4 5 with codecs.open(VACAB, 'r', 'utf-8') as f: 6 vocab = [word.strip() for word in f.readlines()] 7 word_to_id = {v: k for k, v in enumerate(vocab)} 8 print(word_to_id) 9 print(word_to_id['<eos>']) 10 11 def get_id(word): 12 return word_to_id[word] if word in word_to_id else word_to_id['<unk>'] 13 14 fin = codecs.open(RAW_DATA, 'r', 'utf-8') 15 fout = codecs.open(OUTPUT_DATA, 'w', 'utf-8') 16 17 for line in fin: 18 words = line.strip().split() + ['<eos>'] # 讀取出來的行,每行都要加一個換行符(爲了替換爲序號),在讀取並strip時去掉了,因此得加上 19 out_line = ' '.join([str(get_id(w)) for w in words]) + '\n' # 這個換行符只是爲了換行(上面那個是爲了替換爲序號) 20 fout.write(out_line) 21 22 fin.close() 23 fout.close()
上面的實例使用了文本文件來保存通過處理的數據。在實際工程中,一般使用TFRecord格式來提升讀寫效率。
雖然預處理原則上能夠放在TF的Dataset框架中與讀取文本同時進行,但在工程實踐上,保存處理好的數據有幾個重要的優勢:第一,在調試模型的過程當中,能夠保證不一樣模型採起的預處理步驟相同;第二,減少文件體積,節省磁盤讀取的時間;第三,方便對預處理步驟自己進行debug,例如,在模型訓練效果不理想時,只需檢查最終的數據文件就能夠知道是否是預處理過程出了問題。
9.2.2 PTB數據的batching方法
在文本數據中,因爲每一個句子的長度不一樣,又沒法像圖像那樣調整到固定維度,由於在對文本數據進行batching時須要採起一些特殊操做。最多見的方法是使用填充(padding),將同一batch內的句子長度補齊。
可是,在PTB數據集中,每一個句子並不是隨機抽取的文本,而是在上下文之間有關聯的內容。語言模型爲了利用上下文信息,必須將前面句子的信息傳遞到後面的句子。爲了解決這個問題,一般採用的是另外一種batching方法。
若是模型大小沒有限制,那麼最理想的設計是將整個文檔先後鏈接起來,當作一個句子來訓練。但現實中這是沒法實現的。例如PTB數據總共約有19萬詞,若將整個文檔放入一個計算圖,循環神經網絡將展開成一個19萬層的前饋網絡。這樣會致使計算圖過大,另外序列過長可能形成訓練中梯度爆炸的問題。
對此問題的解決方法是,將長序列截斷爲固定長度的子序列。循環神經網絡在處理完一個子序列後,它最終的隱藏狀態將複製到下一個序列做爲初始值,這樣在前向計算時,效果等同於一次性順序地讀取了整個文檔;而在反向傳播時,梯度則只在每一個子序列內部傳播。
爲了利用計算時的並行能力,咱們但願每一次計算能夠對多個句子進行並行處理,同時又要儘可能保證batch之間的上下文連續。解決方案是,先將整個文檔切分紅若干連續段落(能夠理解爲幾行),再讓batch中的每一個位置負責其中一段(能夠理解爲縱向,一個batch含有全部連續段落的一小部分)。例如,若是batch大小爲4,爲了讓batch的每一個位置負責一個子序列,須要將全文平均分爲4個子序列,這樣每一個子文檔內部的全部數據仍能夠被順序處理。
下面的代碼從文本文件中讀取數據,並按上面介紹的方案將數據整理成batch。因爲PTB數據集比較小,所以能夠直接將整個數據集一次性讀入內存。
1 import numpy as np 2 3 TRAIN_DATA = '/home/yangxl/files/ptb/ptb.train' 4 TRAIN_BATCH_SIZE = 20 5 TRAIN_NUM_STEP = 35 6 7 8 # 從文件中讀取數據,並返回包含單詞編號的數組 9 def read_data(file_path): 10 with open(file_path, 'r') as fin: 11 # 將整個文檔讀進一個長字符串 12 id_string = ' '.join([line.strip() for line in fin]) 13 # 將讀取的單詞編號轉爲整數 14 id_list = [int(w) for w in id_string.split()] 15 return id_list 16 17 18 def make_batches(id_list, batch_size, num_step): 19 # batch的數量,即每個子序列在縱向上能切分爲多少小段:1327段;num_step爲每一小段的大小,即一個句子的長度; 20 # 每一個batch包含的單詞數量爲batch_size * num_step 21 num_batches = (len(id_list) - 1) // (batch_size * num_step) # 這裏減1,是爲了預留一個數據看成label使用 22 23 # 如9-4圖所示,將數據整理成一個維度爲[batch_size, num_batches * num_step]的二維數組 24 data = np.array(id_list[: num_batches * batch_size * num_step]) # 後面689個數據未用到。 25 data = np.reshape(data, [batch_size, num_batches * num_step]) # 26 # print(data.shape) # (20, 46445) 27 28 # 沿着第二個維度將數據切分紅num_batches個shape爲(batch_size, num_step)的batch,存入一個列表 29 data_batched = np.split(data, num_batches, axis=1) # (1327, 20, 35) 30 31 # label的處理與data同樣。 32 label = np.array(id_list[1: num_batches * batch_size * num_step + 1]) 33 label = np.reshape(label, [batch_size, num_batches * num_step]) 34 label_batches = np.split(label, num_batches, axis=1) 35 36 # 返回一個長度爲num_batches的數組, 37 return list(zip(data_batched, label_batches)) # (1327, 2, 20, 35) 38 39 40 def main(): 41 train_batches = make_batches(read_data(TRAIN_DATA), TRAIN_BATCH_SIZE, TRAIN_NUM_STEP) 42 43 44 if __name__ == '__main__': 45 main()
9.2.3 基於循環神經網絡的神經語言模型
如圖9-1所示,與第8章介紹的循環神經網絡相比,NLP應用中主要多了兩個層:詞向量層(embedding)和softmax層。
詞向量層
在輸入層,每一個單詞用一個實數向量表示,這個向量被稱爲「詞向量」。詞向量能夠形象地理解爲將詞彙表嵌入到一個固定維度的實數空間裏。將單詞編號轉化爲詞向量主要有兩大做用。
1. 下降輸入的維度。若是不使用詞向量,而直接將單詞以one-hot vector的形式輸入循環神經網絡,那麼輸入的維度大小將與詞彙表大小相同,一般在10000以上。而詞向量的維度一般在200~1000之間,這將大大減小循環神經網絡的參數數量與計算量。
2. 增長語義信息。簡單的單詞編號是不包含任何語義信息的。兩個單詞之間編號相近,並不意味着它們的含義有任何關聯。而詞向量層將稀疏的編號轉化爲稠密的向量表示,這使得詞向量有可能包含更爲豐富的信息。在天然語言應用中學習獲得的詞向量一般會將含義類似的詞賦予取值相近的詞向量值,使得上層的網絡能夠更容易地抓住類似單詞之間的共性。例如,由於貓和狗都須要吃東西,所以在預測下文中出現單詞「吃」的機率時,上文中出現「貓」或「狗」帶來的影響多是類似的。這樣的任務訓練出來的詞向量中,表明「貓」和「狗」的詞向量取值極可能是類似的。
假設詞向量的維度是EMB_SIZE,詞彙表的大小爲VOCAB_SIZE,那麼全部單詞的詞向量能夠放入一個大小爲VOCAB_SIZE × EME_SIZE的矩陣內。在讀取詞向量時,能夠調用tf.nn.embedding_lookup方法。
1 import tensorflow as tf 2 3 BATCH_SIZE = 20 4 NUM_STEP = 35 5 VOCAB_SIZE = 1000 6 EMB_SIZE = 200 7 8 input_data = tf.placeholder(dtype=tf.int64, shape=[BATCH_SIZE, NUM_STEP]) # int32, int64 9 embedding = tf.get_variable('embedding', [VOCAB_SIZE, EMB_SIZE]) 10 input_embedding = tf.nn.embedding_lookup(embedding, input_data) 11 # 輸出矩陣比輸入矩陣多一個維度,新增維度的大小爲EMB_SIZE。 12 # 在語言模型中,通常input_data的維度爲batch_size * num_steps,而輸出的input_embedding的維度是batch_size * num_steps * EMB_SIZE. 13 print(input_embedding) # Tensor("embedding_lookup/Identity:0", shape=(20, 35, 200), dtype=float32)
softmax層
softmax層的做用是將循環神經網絡的輸出轉化爲一個單詞表中每一個單詞的輸出機率。
爲此須要兩個步驟:第一,使用一個線性映射將循環神經網絡的輸出映射爲一個維度與詞彙表大小相同的向量,這一步的輸出叫作logits.
1 import tensorflow as tf 2 3 BATCH_SIZE = 20 4 NUM_STEP = 35 5 HIDDEN_SIZE = 10 6 VOCAB_SIZE = 1000 7 8 # 定義線性映射用到的參數。 9 weight = tf.get_variable('weight', shape=[HIDDEN_SIZE, VOCAB_SIZE]) 10 bias = tf.get_variable('bias', [VOCAB_SIZE]) 11 # 計算線性映射。output是RNN的輸出,shape=(batch_size * num_steps, HIDDEN_SIZE) 12 output = tf.placeholder(tf.float32, shape=[BATCH_SIZE * NUM_STEP, HIDDEN_SIZE]) 13 logits = tf.nn.bias_add(tf.matmul(output, weight), bias) 14 print(logits) # Tensor("BiasAdd:0", shape=(700, 1000), dtype=float32)
第二,調用softmax將logits轉化爲和爲1的機率分佈。語言模型的每一步輸出均可以看做一個分類問題:在VOCAB_SIZE個可能的類別中決定這一步最可能輸出的單詞。
1 probs = tf.nn.softmax(logits) # 機率;probs的維度與logits的維度相同
模型訓練一般並不關心機率的具體取值,而更關心最終的log perplexity,所以能夠調用tf.nn.sparse_softmax_cross_entropy_with_logits方法直接從logits計算log perplexity做爲損失函數:
1 # labels是一個大小爲[batch_size * num_step]的一維數組,包含每一個位置正確的單詞編號。 2 # logits的維度爲[batch_size * num_step, HIDDEN_SIZE VOCAB_SIZE](書上是錯的),loss的維度與labels相同,表明每一個位置上的log perplexity。 3 labels = tf.placeholder(tf.int64, shape=[BATCH_SIZE * NUM_STEP]) 4 loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=labels, logits=logits) 5 print(loss) # shape=(700,)
經過共享參數減小參數數量
softmax層和詞向量層的參數數量都與詞彙表大小VOCAB_SIZE成正比。VOCAB_SIZE的數值一般較大,而HIDDEN_SIZE相對較小,致使softmax和embedding在整個網絡的參數數量中佔很大比例。例如,VOCAB_SIZE爲10000,HIDDEN_SIZE和EMB_SIZE都爲512,循環神經網絡採用雙層LSTM,那麼詞向量層和Softmax層的參數數量均爲10000*512=5120000,而循環神經網絡自己的參數數量僅爲2*4*2*512*512=4194304(LSTM有4個參數矩陣,每一個參數矩陣的維度是[2*HIDDEN_SIZE, HIDDEN_SIZE],忽略偏置項, 見圖8-7),少於詞向量層和Softmax層的參數數量,僅佔總參數數量的29%。
注意,在上面的例子中,詞向量層和softmax層的參數數量是相等的,它們都爲每一個單詞分配了長度爲512的向量。若是共享詞向量層和softmax層的參數,不只能大幅減小參數數量,還能提供最終模型的效果。(可以共享參數的緣由,多是兩層都是線性變換,可使用任意參數系,那麼使用相同的參數系也無妨,進一步能夠說,它們是不可訓練的)。下面的完整代碼樣例中實現了這一方法。
語言模型完整實例:
1 import numpy as np 2 import tensorflow as tf 3 4 TRAIN_DATA = '/home/yangxl/files/ptb/ptb.train' 5 EVAL_DATA = '/home/yangxl/files/ptb/ptb.valid' 6 TEST_DATA = '/home/yangxl/files/ptb/ptb.test' 7 HIDDEN_SIZE = 300 # 隱藏層大小 8 NUM_LAYERS = 2 # 深層循環神經網絡中LSTM結構的層數 9 VOCAB_SIZE = 10000 # 詞彙表大小 10 TRAIN_BATCH_SIZE = 20 # 訓練數據batch大小 11 TRAIN_NUM_STEP = 35 # 訓練數據截斷長度 12 13 EVAL_BATCH_SIZE = 1 # 測試數據batch大小 14 EVAL_NUM_STEP = 1 # 測試數據截斷長度 15 NUM_EPOCH = 10 # 使用訓練數據的輪數 16 LSTM_KEEP_PROB = 0.9 # LSTM節點不被dropout的機率 17 EMBEDDING_KEEP_PROB = 0.9 # 詞向量不被dropout的機率 18 MAX_GRAD_NORM = 5 # 用於控制梯度膨脹的梯度大小上限 19 SHARE_EMB_AND_SOFTMAX = True # 在softmax層和詞向量層之間共享參數 20 21 22 # 從文件中讀取數據,並返回包含單詞編號的數組 23 def read_data(file_path): 24 with open(file_path, 'r') as fin: 25 # 將整個文檔讀進一個長字符串 26 id_string = ' '.join([line.strip() for line in fin]) 27 # 將讀取的單詞編號轉爲整數 28 id_list = [int(w) for w in id_string.split()] # 長度爲929589 29 return id_list 30 31 32 def make_batches(id_list, batch_size, num_step): 33 # batch的數量,即每個子序列在縱向上能切分爲多少小段:1327段;num_step爲每一小段的大小,即一個句子的長度; 34 # 每一個batch包含的單詞數量爲batch_size * num_step 35 num_batches = (len(id_list) - 1) // (batch_size * num_step) # 這裏減1,是爲了預留一個數據看成label使用 36 37 # 如9-4圖所示,將數據整理成一個維度爲[batch_size, num_batches * num_step]的二維數組 38 data = np.array(id_list[: num_batches * batch_size * num_step]) # 後面689個數據未用到。 39 data = np.reshape(data, [batch_size, num_batches * num_step]) # 40 # print(data.shape) # (20, 46445) 41 42 # 沿着第二個維度將數據切分紅num_batches個shape爲(batch_size, num_step)的batch,存入一個列表 43 data_batched = np.split(data, num_batches, axis=1) # (1327, 20, 35) 44 45 # label的處理與data同樣。 46 label = np.array(id_list[1: num_batches * batch_size * num_step + 1]) 47 label = np.reshape(label, [batch_size, num_batches * num_step]) 48 label_batches = np.split(label, num_batches, axis=1) 49 50 # 返回一個長度爲num_batches的數組, 51 return list(zip(data_batched, label_batches)) # (1327, 2, 20, 35) 52 53 54 # 經過一個PTBModel類來描述模型,這樣方便維護循環神經網絡中的狀態。 55 class PTBModel(object): 56 def __init__(self, is_training, batch_size, num_steps): 57 self.batch_size = batch_size 58 self.num_steps = num_steps # 在run_epoch()中計算iters時會用到 59 60 # 定義每一步的輸入和預期輸出,兩者維度相同。 61 self.input_data = tf.placeholder(dtype=tf.int32, shape=[batch_size, num_steps]) 62 self.targets = tf.placeholder(dtype=tf.int32, shape=[batch_size, num_steps]) 63 64 # 定義以LSTM爲循環體結構且使用dropout的深層循環神經網絡。 65 dropout_keep_prob = LSTM_KEEP_PROB if is_training else 1.0 66 lstm_cells = [ 67 tf.nn.rnn_cell.DropoutWrapper( 68 tf.nn.rnn_cell.LSTMCell(HIDDEN_SIZE), 69 output_keep_prob=dropout_keep_prob) 70 for _ in range(NUM_LAYERS)] 71 cell = tf.nn.rnn_cell.MultiRNNCell(lstm_cells) 72 73 # 初始化最初的狀態,即全零的向量。這個量只在每一個epoch初始化第一個batch時使用。 74 # 和其餘神經網絡相似,在優化循環神經網絡時,每次也會使用一個batch的訓練樣本。 c和h的shape都爲[batch_size, HIDDEN_SIZE] 75 self.initial_state = cell.zero_state(batch_size, tf.float32) 76 77 # 定義單詞的詞向量矩陣 78 embedding = tf.get_variable('embedding', [VOCAB_SIZE, HIDDEN_SIZE]) 79 80 # 將輸入單詞轉爲詞向量 81 inputs = tf.nn.embedding_lookup(embedding, self.input_data) 82 83 # 只在訓練時使用dropout 84 if is_training: 85 # 在輸入以前要dropout, 各個循環層也要dropout。不改變inputs.shape 86 inputs = tf.nn.dropout(inputs, keep_prob=EMBEDDING_KEEP_PROB) 87 88 # 定義輸出列表。在這裏先將不一樣時刻LSMT的輸出收集起來,再一塊兒提供給softmax層。 89 # outputs = [] 90 # state = self.initial_state 91 # with tf.variable_scope('RNN'): 92 # for time_step in range(num_steps): 93 # if time_step > 0: 94 # tf.get_variable_scope().reuse_variables() 95 # cell_output, state = cell(inputs[:, time_step, :], state) 96 # outputs.append(cell_output) # 長度爲35,元素的shape=(20, 300) 97 # 把輸出隊列展開爲[batch, num_steps*hidden_size]的形狀,而後在reshape成[batch*num_steps, hidden_size]的形狀 98 # 比直接按行拼接區別在於可以讓同一片斷的數據放在一塊兒, 也是爲了在計算loss時方便使用reshape獲得labels 99 # output = tf.reshape(tf.concat(outputs, axis=1), [-1, HIDDEN_SIZE]) # shape=(700, 300) 100 # print('output', output) 101 # print('state', state) 102 103 # 下面這樣自動計算的更牛 104 outputs, state = tf.nn.dynamic_rnn(cell, inputs, dtype=tf.float32) 105 output = tf.reshape(outputs, [-1, HIDDEN_SIZE]) 106 print('output2', output) 107 print('state2', state) 108 109 # Softmax層:將RNN在每一個位置上的輸出轉化爲各個單詞的logits. 110 if SHARE_EMB_AND_SOFTMAX: # 共享參數,不會建立新的變量 111 weight = tf.transpose(embedding) 112 else: 113 weight = tf.get_variable('weight', [HIDDEN_SIZE, VOCAB_SIZE]) 114 bias = tf.get_variable('bias', [VOCAB_SIZE]) 115 logits = tf.matmul(output, weight) + bias # output、weight的前後問題?? 116 117 # 定義交叉熵損失函數和平均損失 118 # loss = tf.losses.mean_squared_error(labels=tf.reshape(self.targets, [-1]), predictions=logits) 119 loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=tf.reshape(self.targets, [-1]), logits=logits) 120 self.cost = tf.reduce_sum(loss) / batch_size 121 self.final_state = state # 保存了每次batch的狀態 122 123 if not is_training: 124 return 125 126 trainable_variables = tf.trainable_variables() 127 # print('train', trainable_variables) 128 # [<tf.Variable 'language_model/embedding:0' shape=(10000, 300) dtype=float32_ref>, 129 # <tf.Variable 'language_model/RNN/multi_rnn_cell/cell_0/lstm_cell/kernel:0' shape=(600, 1200) dtype=float32_ref>, 每層有4個維度爲[2n, n]的參數矩陣,因此kernel.shape=[600, 1200] 130 # <tf.Variable 'language_model/RNN/multi_rnn_cell/cell_0/lstm_cell/bias:0' shape=(1200,) dtype=float32_ref>, 131 # <tf.Variable 'language_model/RNN/multi_rnn_cell/cell_1/lstm_cell/kernel:0' shape=(600, 1200) dtype=float32_ref>, 132 # <tf.Variable 'language_model/RNN/multi_rnn_cell/cell_1/lstm_cell/bias:0' shape=(1200,) dtype=float32_ref>, 133 # <tf.Variable 'language_model/bias:0' shape=(10000,) dtype=float32_ref>] 134 # 控制梯度大小,定義優化方法和訓練步驟 135 grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, trainable_variables), MAX_GRAD_NORM) 136 optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0) 137 self.train_op = optimizer.apply_gradients(zip(grads, trainable_variables)) 138 139 140 def run_epoch(session, model, batches, train_op, output_log, step): 141 # train_op做爲一個專門的參數而不是使用model.train_op,是爲了區分訓練過程和測試/驗證過程 142 143 # 計算平均perplexity的輔助變量 144 total_costs = 0.0 145 iters = 0 146 state = session.run(model.initial_state) 147 # 訓練一個epoch 148 for x, y in batches: 149 # 在當前batch上運行train_op並計算損失值。交叉熵損失函數計算的就是下一個單詞爲給定單詞的機率 150 cost, state, _ = session.run( 151 # 最須要注意的是state,每一個epoch都要從新提供初始化狀態。 152 [model.cost, model.final_state, train_op], feed_dict={model.input_data: x, model.targets: y, model.initial_state: state} # feed_dict裏面的值不僅是placeholder! 153 ) 154 total_costs += cost 155 iters += model.num_steps 156 157 # 只有在訓練時輸出日誌 158 if output_log and step % 100 == 0: 159 print('After %d steps, perplexity is %.3f' % (step, np.exp(total_costs / iters))) 160 161 step += 1 162 163 return step, np.exp(total_costs / iters) 164 165 166 def main(): 167 # 定義初始化函數 168 initializer = tf.random_normal_initializer(-0.05, 0.05) 169 170 # 定義訓練用的循環神經網絡模型 171 with tf.variable_scope('language_model', reuse=None, initializer=initializer): 172 train_model = PTBModel(True, TRAIN_BATCH_SIZE, TRAIN_NUM_STEP) 173 174 # 定義測試用的循環神經網絡模型,它與train_model共用參數,但沒有dropout 175 with tf.variable_scope('language_model', reuse=True, initializer=initializer): 176 eval_model = PTBModel(False, EVAL_BATCH_SIZE, EVAL_NUM_STEP) 177 178 # 訓練模型 179 with tf.Session() as session: 180 tf.global_variables_initializer().run() 181 train_batches = make_batches(read_data(TRAIN_DATA), TRAIN_BATCH_SIZE, TRAIN_NUM_STEP) 182 eval_batches = make_batches(read_data(EVAL_DATA), EVAL_BATCH_SIZE, EVAL_NUM_STEP) 183 test_batches = make_batches(read_data(TEST_DATA), EVAL_BATCH_SIZE, EVAL_NUM_STEP) 184 185 step = 0 186 187 for i in range(NUM_EPOCH): 188 print('In iteration: %d' % (i+1)) 189 step, train_pplx = run_epoch(session, train_model, train_batches, train_model.train_op, True, step) 190 print('Epoch: %d Train PerPlexity: %.3f' % (i+1, train_pplx)) 191 192 _, eval_pplx = run_epoch(session, eval_model, eval_batches, tf.no_op(), False, 0) 193 print('Epoch: %d Eval Perplexity: %.3f' % (i+1, eval_pplx)) 194 195 _, test_pplx = run_epoch(session, eval_model, test_batches, tf.no_op(), False, 0) 196 print('Test Perplexity: %.3f' % test_pplx) 197 198 199 if __name__ == '__main__': 200 main()
也就吃個飯的時間(一個多小時),TIME+都到478了,看來還真不是常規的時間。
top命令的TIME/TIME+是指的進程所使用的CPU時間,不是進程啓動到如今的時間,所以,若是一個進程使用的cpu不多,那即便這個進程已經存在N長時間,TIME/TIME+也是很小的數值。此外,若是你的系統有多個CPU,或者是多核CPU的話,那麼,進程佔用多個cpu的時間是累加的,上邊的示例,一個多小時對應478,就說明機器是8核的。
經過調整LSTM隱藏層的節點個數和大小以及訓練迭代的輪數還能夠將perplexity值降到最低。
很是多的天然語言處理應用的技術都是基於語言模型。
9.3 神經網絡機器翻譯
最基礎的機器翻譯算法——Seq2Seq模型。
9.3.1 機器翻譯背景與Seq2Seq模型介紹
Seq2Seq模型的基本思想:使用一個循環神經網絡讀取輸入句子,將整個句子的信息壓縮到一個固定維度的編碼中,再使用另外一個循環神經網絡讀取這個編碼,將其「解壓」爲目標語言的一個句子。這兩個循環神經網絡分別成爲編碼器(Encoder)和解碼器(Decoder),這個模型也稱爲encoder-decoder模型。
解碼器部分的結構與語言模型幾乎徹底相同:輸入爲單詞的詞向量,輸出爲softmax層產生的單詞機率,損失函數爲log perplexity。解碼器能夠理解爲一個以輸入編碼爲前提的語言模型。語言模型中使用的一些技巧,如共享softmax層和詞向量的參數,均可以直接應用到Seq2Seq模型的解碼器中。
編碼器分佈則更爲簡單。它與解碼器同樣擁有詞向量層和循環神經網絡,可是因爲編碼階段並未輸出,所以不須要softmax層。
在訓練過程當中,編碼器順序讀入每一個單詞的詞向量,而後將最終的隱藏狀態複製到解碼器做爲初始狀態。解碼器的第一個輸入是一個特殊的<sos>字符(start-of-sentence),每一步預測的單詞是訓練數據的目標句子,預測序列的最後一個單詞是與語言模型相同的'<eos>'字符(end-of-sentence)。
在機器翻譯應用中,真實應用場景中的測試步驟與語言模型的測試步驟有所不一樣。語言模型中測試的標準是給定目標句子上的perplexity。而機器翻譯的測試方法是,讓解碼器在沒有「正確答案」的狀況下自主生成一個翻譯句子,而後採用人工或自動的方法對翻譯句子的質量進行評測。
在解碼過程當中,每一步預測的單詞中機率最大的單詞被選爲這一步的輸出,並複製到下一步的輸入中(圖9-3中用虛線表示)。這裏描述的是最簡單的貪心算法,在真實應用中廣泛採用集束搜索(Beam Search)方法來得到最好的翻譯效果。
9.3.2 機器翻譯文本數據的預處理
機器翻譯領域最重要的公開數據集是WMT數據集(Workshop on Statistical Machine Translation),下載地址:http://data.statmt.org/wmt17/translation-task/,該會議從2016年起改成Conference on Machine Translation。每一年,該會議就會組織一次機器翻譯領域的競賽,其提供的訓練和測試數據也成爲了機器翻譯領域論文的標準數據集。然而因爲WMT數據集較大,訓練時間較長,所以本節採用一個較小的IWLST TED演講數據集做爲示例,下載地址:https://wit3.fbk.eu/mt.php?release=2015-01
點擊'0.51'下載的就是了。
它的英文-中文訓練數據包含21萬個句子對,內容是TED演講的中英字幕。
對於平行語料的預處理,其步驟和9.2.1小節中關於PTB數據的預處理基本同樣。首先,須要統計語料中出現的單詞,爲每一個單詞分配一個ID,將詞彙表存入一個vocab文件,而後將文本轉化爲用單詞編號的形式來表示。
與前面不一樣的地方主要在於,下載的文本沒有通過預處理,尤爲沒有通過切詞。例如,因爲每一個英文單詞和標點符號之間緊密相連,致使不能像處理PTB數據那樣直接用空格對單詞進行切割。爲此須要用一些獨立的工具來進行切詞操做。
最經常使用的切詞工具是moses:(書上的地址不對)
1 git clone https://github.com/moses-smt/mosesdecoder.git
切詞操做:
1 # 英文切詞 2 error@error-F14CU27:~/moses/mosesdecoder/scripts/tokenizer$ perl ./tokenizer.perl -no-escape -l en < /home/error/en-zh/train.tags.en-zh.en > /home/error/codes/train.tags.en-zh.en 3 Tokenizer Version 1.1 4 Language: en 5 Number of threads: 1 6 7 # 中文切詞 8 # 先把漢字和漢字切分開 9 error@error-F14CU27:~/en-zh$ sed 's/ //g; s/\B/ /g' ./train.tags.en-zh.zh > /home/error/codes/train.tags.en-zh.zh 10 11 # 切分漢字和標點符號 12 error@error-F14CU27:~/moses/mosesdecoder/scripts/tokenizer$ perl ./tokenizer.perl -no-escape -l en < /home/error/codes/train.tags.en-zh.zh > /home/error/codes/train.tags.en-zh.zh2 13 Tokenizer Version 1.1 14 Language: en 15 Number of threads: 1
$ cat train.tags.en-zh.en | grep -v '<url>'| grep -v '<keywords>' | grep -v '<speaker>' | grep -v '<talkid>' | grep -v '<translator' | grep -v '<reviewer' > train.en
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/ //g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/\B/ /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/,/ , /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/。 / 。/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/!/ !/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/!/ !/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/,/ , /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/\./ \. /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/?/ ? /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/?/ ? /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/《/ 《 /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/》/ 》 /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/"/ " /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/— —/ —— /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/•/ • /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/、/ 、 /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/·/ · /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/:/ : /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/…/ … /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/【/ 【 /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/】/ 】 /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/(/ ( /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/)/ ) /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/-/ - /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/- -/--/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/- -/--/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/— —/ —— /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/- -/ - - /g' train.zh.bk yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/- -/--/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/-/ - /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/- -/--/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/(/ ( /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/)/ ) /g' train.zh.bk
完成切詞後,再使用以前處理PTB數據的方法,分別生成英文文本和中文文本詞彙文件,並將文本轉化爲單詞編號。生成詞彙文件時,須要注意將<sos>、<eos>、<unk>這3個詞手動加入到詞彙表中,而且要限制詞匯表大小,將詞頻太低的次替換爲<unk>。假定英文詞彙表大小爲10000,中文詞彙表大小爲4000。
在PTB 數據中,因爲句子之間有上下文關聯,所以能夠直接將連續的句子鏈接起來稱爲一個大的段落。而在機器翻譯的訓練樣本中,每一個句子對一般是做爲獨立的數據來訓練的。因爲每一個句子的長短不一致,所以在將這些句子放入同一個batch 時,須要將較短的句子補齊到與同batch內最長句子相同的長度。用於填充長度而填入的位置叫做填充(padding)。在TensorFlow中,tf.data.Dataset.padded_ batch函數提供了這一功能。
循環神經網絡在讀取數據時會將填充位置的內容與其餘內容同樣歸入計算,所以爲了避免讓填充影響訓練,有兩方面須要注意:
第一,循環神經網絡在讀取填充時,應當跳過這一位置的計算。以編碼器爲例,若是編碼器在讀取填充時,像正常輸入同樣處理填充輸入,那麼在讀取「B1B200」以後產生的最後一位隱藏狀態就和讀取「B1B2」以後的隱藏狀態不一樣,會產生錯誤的結果。
TensorFlow提供了tf.nn.dynamic_rnn方法來實現這一功能。dynamic_rnn對每個batch的數據讀取兩個輸入:輸入數據的內容(維度爲[batch_size, time])和輸入數據的長度(維度爲[time])。對於輸入batch裏的每一條數據,在讀取了相應長度的內容後,dynamic_rnn就跳事後面的輸入,直接把前一步的計算結果複製到後面的時刻。這樣能夠保證padding是否存在不影響模型效果。
另外值得注意的是,使用dyanmic_rnn時每一個batch的最大序列長度不須要相同。例如,在上面的例子中,第一個batch的維度是2×4,而第二個batch的維度是2×7。在訓練中dynamic_rnn會根據每一個batch的最大長度動態展開到須要的層數,這就是它被稱爲「dynamic」的緣由。
第二,在設計損失函數時須要特別將填充位置的損失的權重設置爲0,這樣在填充位置產生的預測不會影響梯度的計算。
下面的代碼使用tf.data.Dataset.padded_batch來進行填充和batching,並記錄每一個句子的序列長度以用做dynamic_rnn的輸入。與前面PTB的例子不一樣,這裏沒有將全部數據讀入內存,而是使用Dataset從磁盤動態讀取數據。
數據處理的示例見完整代碼的前面部分。
sequence_mask方法:
1 g = tf.sequence_mask([8, 9, 7, 5, 8], maxlen=10, dtype=tf.float32) 2 3 with tf.Session() as sess: 4 print(sess.run(g)) 5 6 # 結果: 7 # [[1. 1. 1. 1. 1. 1. 1. 1. 0. 0.] 8 # [1. 1. 1. 1. 1. 1. 1. 1. 1. 0.] 9 # [1. 1. 1. 1. 1. 1. 1. 0. 0. 0.] 10 # [1. 1. 1. 1. 1. 0. 0. 0. 0. 0.] 11 # [1. 1. 1. 1. 1. 1. 1. 1. 0. 0.]]
seq2seq模型完整實例:
1 #!coding:utf8 2 3 import tensorflow as tf 4 import time 5 6 MAX_LEN = 50 # 限定句子的最大單詞數量 7 SOS_ID = 1 # 目標語言詞彙表中<sos>的ID 8 9 SRC_TRAIN_DATA = 'D:\\files\\tf\\train.txt.en.num' 10 TRG_TRAIN_DATA = 'D:\\files\\tf\\train.txt.zh2.num' 11 CHECKPOINT_PATH = 'D:\\files\\tf\\seq222seq_ckpt' 12 HIDDEN_SIZE = 1024 # 隱藏層大小 13 NUM_LAYERS = 2 # 層數 14 SRC_VOCAB_SIZE = 10000 # 源語言詞彙表大小 15 TRG_VOCAB_SIZE = 4000 # 目標語言詞彙表大小 16 BATCH_SIZE = 100 17 NUM_EPOCH = 1 18 KEEP_PROB = 0.8 # 節點不被dropout的機率 19 MAX_GRAD_NORM = 5 # 用於控制梯度膨脹的梯度大小上限 20 SHARE_EMB_AND_SOFTMAX = True 21 22 23 # 使用Dataset從一個文件中讀取一個語言的數據。 24 # 數據格式爲每行一句話,單詞已經轉化爲單詞編號 25 # 行數據,當前行單詞個數 26 def MakeDataset(file_path): 27 dataset = tf.data.TextLineDataset(file_path) # 取出來的是一行,bytes類型 28 # b'95 13 1590 0 4 11 90 4870 0 4 2' 29 30 # 根據空格將單詞編號切分開並放入一個一維向量 31 dataset = dataset.map(lambda string: tf.string_split([string]).values) # '[]'不能丟 32 # [b'95' b'13' b'1590' b'0' b'4' b'11' b'90' b'4870' b'0' b'4' b'2'] 33 34 # 將字符串形式的單詞編號轉化爲整數 35 dataset = dataset.map(lambda string: tf.string_to_number(string, tf.int32)) 36 # [ 95 13 1590 0 4 11 90 4870 0 4 2] 37 38 # 統計每一個句子的單詞數量,並與句子內容一塊兒放入Dataset中 39 dataset = dataset.map(lambda x: (x, tf.size(x))) # ([ 95 13 1590 0 4 11 90 4870 0 4 2], 11) 40 return dataset 41 42 43 # 從源語言文件src_path和目標語言文件trg_path中分別讀取數據,並進行填充和batching操做 44 def MakeSrcTrgDataset(src_path, trg_path, batch_size): 45 # 首先分別讀取源語言數據和目標語言數據 46 src_data = MakeDataset(src_path) # ([ 95 13 1590 0 4 11 90 4870 0 4 2], 11) 47 trg_data = MakeDataset(trg_path) # ([ 40, 5545, 610, 118, 10, 261, 7, 171, 4827, 507, 4, 5, 7, 40, 5545, 610, 6, 2], 18) 48 49 # 經過zip操做將兩個Dataset合併爲一個Dataset。 50 dataset = tf.data.Dataset.zip((src_data, trg_data)) 51 52 # 刪除內容爲空的句子和長度過長的句子 53 def FilterLength(src_tuple, trg_tuple): 54 ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple) 55 src_len_ok = tf.logical_and(tf.greater(src_len, 1), tf.less_equal(src_len, MAX_LEN)) # 句子長度大於1且不大於50 56 trg_len_ok = tf.logical_and(tf.greater(trg_len, 1), tf.less_equal(trg_len, MAX_LEN)) 57 return tf.logical_and(src_len_ok, trg_len_ok) 58 dataset = dataset.filter(FilterLength) # 做爲過濾器的函數必須可以返回布爾值。 59 60 # 解碼器須要兩種格式的目標句子。 61 # 1. 解碼器的輸入(trg_input), 形式如同'<sos> X Y Z' 62 # 2. 解碼器的目標輸出(trg_label), 形式如同'X Y Z <eos>' 63 # 上面從目標文件中讀到的目標句子是'X Y Z <eos>'的形式,須要從中生成'<sos> X Y Z'形式並加入到dataset中。 64 def MakeTrgInput(src_tuple, trg_tuple): 65 ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple) # [ 40, 5545, 610, 118, 10, 261, 7, 171, 4827, 507, 4, 5, 7, 40, 5545, 610, 6, 2] 66 trg_input = tf.concat([[SOS_ID], trg_label[:-1]], axis=0) 67 return ((src_input, src_len), (trg_input, trg_label, trg_len)) 68 dataset = dataset.map(MakeTrgInput) 69 70 # 隨機打亂循環數據 71 dataset = dataset.shuffle(10000) 72 73 # 規定填充後輸出的數據維度 74 padded_shapes = ( 75 (tf.TensorShape([None]), # 源句子是長度未知的向量 76 tf.TensorShape([])), # 源句子長度是單個數字 77 78 (tf.TensorShape([None]), # 目標句子(解碼器輸入)是長度未知的向量 79 tf.TensorShape([None]), # 目標句子(解碼器目標輸出)是長度未知的向量 80 tf.TensorShape([])) # 目標句子長度是單個數字 81 ) 82 83 # batching操做, 84 # Defaults are `0` for numeric types and the empty string for string types. 85 batched_dataset = dataset.padded_batch(batch_size, padded_shapes) # src.shape=(batch_size, max_length) 86 return batched_dataset 87 88 89 # 定義模型 90 class NMTModel(object): 91 def __init__(self): 92 self.enc_cell = tf.nn.rnn_cell.MultiRNNCell( 93 [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)] 94 ) 95 self.dec_cell = tf.nn.rnn_cell.MultiRNNCell( 96 [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)] 97 ) 98 99 # 爲源語言和目標語言分別定義詞向量 100 # 注意VOCAB_SIZE是詞彙表大小,不是文本大小。 101 # 初始化的變量取什麼值都無所謂吧 102 self.src_embedding = tf.get_variable('src_emb', [SRC_VOCAB_SIZE, HIDDEN_SIZE]) 103 self.trg_embedding = tf.get_variable('trg_emb', [TRG_VOCAB_SIZE, HIDDEN_SIZE]) 104 105 # softmax層的變量都是trg的,由於是輸出層進行分類 106 if SHARE_EMB_AND_SOFTMAX: 107 self.softmax_weight = tf.transpose(self.trg_embedding) 108 else: 109 self.softmax_weight = tf.get_variable('weight', [HIDDEN_SIZE, TRG_VOCAB_SIZE]) 110 self.softmax_bias = tf.get_variable('softmax_bias', [TRG_VOCAB_SIZE]) 111 112 def forward(self, src_input, src_size, trg_input, trg_label, trg_size): 113 # src_input.shape=(5, 17) src_size=[10 9 13 11 17] # (5, 17)是由於沒有去掉文件頭部幾行,看了不下一遍了。 114 # trg_label.shape=(5, 30) trg_size=[30 21 26 18 21] 115 batch_size = tf.shape(src_input)[0] 116 117 src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input) # (5, 17, 1024) 118 trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input) # (5, 30, 1024) # 解碼器的輸入trg_input 119 120 # 在詞向量上進行dropout,dropout不會改變shape。 121 src_emb = tf.nn.dropout(src_emb, KEEP_PROB) # (5, 17, 1024) 122 trg_emb = tf.nn.dropout(trg_emb, KEEP_PROB) # (5, 30, 1024) 123 124 # 使用dynamic_rnn構造編碼器 125 # 不改變shape 126 with tf.variable_scope('encoder'): 127 # 由於編碼器是一個雙層LSTM結構,所以enc_state是一個包含兩個LSTMStateTuple類的tuple,每一個類對應編碼器中一層的狀態。 128 # state_c和state_h的shape都爲[batch_size, HIDDEN_SIZE],即(5, 1024) 129 # enc_outputs是頂層LSTM在每一步的輸出,維度爲[batch_size, max_time, HIDDEN_SIZE],同輸入(5, 17, 1024) 130 # 後兩個參數的做用?? 131 enc_outputs, enc_state = tf.nn.dynamic_rnn( 132 self.enc_cell, src_emb, src_size, dtype=tf.float32 133 ) 134 135 # 構造解碼器 136 with tf.variable_scope('decoder'): 137 # dec_outputs.shape=(5, 30, 1024), 同輸入 138 # 輸出隱藏狀態,4個都是(5, 1024) 139 # 解碼器的初始狀態爲編碼器的輸出狀態 140 dec_outputs, _ = tf.nn.dynamic_rnn( 141 self.dec_cell, trg_emb, trg_size, initial_state=enc_state # 編碼器的最終狀態,做爲解碼器的初始狀態。 142 ) 143 144 # 計算解碼器每一步的log perplexity 145 output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE]) # shape=(150, 1024) 146 147 # softmax層輸出 148 logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias # shape=(150, 4000) 149 loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=tf.reshape(trg_label, [-1]), logits=logits) # shape=(150,) 一個batch(150個單詞編號)的總損失。 150 151 # 在計算平均損失時,須要將填充位置的權重設置爲0,以免無效位置的預測干擾模型的訓練 152 label_weight = tf.sequence_mask( 153 trg_size, maxlen=tf.shape(trg_label)[1], dtype=tf.float32 154 ) # 假如trg_size=[30 21 26 18 21], maxlen=30, 那麼結果的第二個元素爲[21個1.0,(30-21)個0.0] 155 156 label_weight = tf.reshape(label_weight, [-1]) # shape=(150,) 157 158 cost = tf.reduce_sum(loss * label_weight) # 一個batch的損失,1035.8269 # 把填充位置的損失過濾掉了 159 cost_per_token = cost / tf.reduce_sum(label_weight) # 一個單詞編號對應的損失,8.704428 # 也是把填充位置排除了 160 161 # 定義反向傳播 162 trainable_variables = tf.trainable_variables() 163 164 # 控制梯度大小,定義優化方法和訓練步驟 165 grads = tf.gradients(cost / tf.to_float(batch_size), trainable_variables) # 習慣上,一個batch有batch_size個數據,除以batch_size表示一個`數據`(一個句子)的損失 166 grads, _ = tf.clip_by_global_norm(grads, MAX_GRAD_NORM) 167 optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0) 168 train_op = optimizer.apply_gradients(zip(grads, trainable_variables)) 169 return cost_per_token, train_op 170 # return src_input, src_size, src_emb, dropout_src_emb, enc_outputs, trg_input, trg_label, trg_size, trg_emb, dropout_trg_emb, dec_outputs, output, logits, loss, cost_per_token, train_op 171 172 173 def run_epoch(sess, cost_op, train_op, saver, step): 174 # 重複訓練步驟直至遍歷完dataset中的全部數據 175 # 訓練以前不知道須要訓練多少輪 176 while True: 177 try: 178 cost, _ = sess.run([cost_op, train_op]) 179 if step % 10 == 0: 180 print('After %s steps, per token cost is %.3f' % (step, cost)) 181 # 每200步保存一個checkpoint 182 if step % 200 == 0: 183 saver.save(sess, CHECKPOINT_PATH, global_step=step) 184 step += 1 185 except tf.errors.OutOfRangeError: 186 break 187 return step 188 189 190 def main(): 191 initializer = tf.random_uniform_initializer(-0.05, 0.05) 192 193 # 定義訓練用的循環神經網絡模型 194 with tf.variable_scope('nmt_model', reuse=None, initializer=initializer): 195 train_model = NMTModel() 196 197 # 定義輸入數據 198 data = MakeSrcTrgDataset(SRC_TRAIN_DATA, TRG_TRAIN_DATA, BATCH_SIZE) 199 iter = data.make_initializable_iterator() 200 (src, src_size), (trg_input, trg_label, trg_size) = iter.get_next() # trg_input的shape=[batch_size, max_length] 201 202 cost_op, train_op = train_model.forward(src, src_size, trg_input, trg_label, trg_size) 203 # src_input, src_size, src_emb, dropout_src_emb, enc_outputs, trg_input, trg_label, trg_size, trg_emb, dropout_trg_emb, dec_outputs, output, logits, loss, cost_per_token, train_op = train_model.forward(None, src, src_size, trg_input, trg_label, trg_size) 204 205 # 訓練模型 206 saver = tf.train.Saver() 207 step = 0 208 with tf.Session() as sess: 209 tf.global_variables_initializer().run() 210 211 # src_input, src_size, src_emb, dropout_src_emb, enc_outputs, trg_input, trg_label, trg_size, trg_emb, dropout_trg_emb, dec_outputs, output, logits, loss, cost_per_token, train_op = sess.run([src_input, src_size, src_emb, dropout_src_emb, enc_outputs, trg_input, trg_label, trg_size, trg_emb, dropout_trg_emb, dec_outputs, output, logits, loss, cost_per_token, train_op]) 212 # print(src_input.shape, src_size, src_emb.shape, dropout_src_emb.shape, enc_outputs.shape, trg_input.shape, trg_label.shape, trg_size, trg_emb.shape, dropout_trg_emb.shape, dec_outputs.shape, output.shape, logits.shape, loss.shape, cost_per_token, train_op) 213 # return 214 215 for i in range(NUM_EPOCH): 216 print('In iterator: %d' % (i+1)) 217 # iterator爲什麼放在循環中,遍歷第二遍時還要從新初始化?? 218 sess.run(iter.initializer) # 取值以前必定不要忘記迭代器初始化 219 step = run_epoch(sess, cost_op, train_op, saver, step) 220 print('In interator %d is end, step is %d' % (i+1, step)) 221 222 if __name__ == '__main__': 223 print(int(time.time())) 224 main() 225 print(int(time.time()))
在解碼的程序中,解碼器的實現與訓練時有很大不一樣。由於訓練時解碼器能夠從輸入中讀取完整的目標訓練句子,所以能夠用dynamic_rnn簡單地展開成前饋網絡。而在解碼過程當中,模型只能看到輸入句子,卻不能看到目標句子。解碼器的第一步讀取<sos>符,預測目標句子的第一個單詞,而後須要將這個預測的單詞複製到第二步做爲輸入,再預測第二個單詞,知道預測的單詞爲<eos>爲止。這個過程須要使用一個循環結構來實現。tf.while_loop。
翻譯過程:
1 #!coding:utf8 2 3 import tensorflow as tf 4 5 CHECKPOINT_PATH = '/home/yangxl/codes/seq2seq_ckpt-400' 6 7 # 必需要與訓練模型的參數一致 8 HIDDEN_SIZE = 1024 # 隱藏層大小 9 NUM_LAYERS = 2 # 層數 10 SRC_VOCAB_SIZE = 10000 # 源語言詞彙表大小 11 TRG_VOCAB_SIZE = 10000 # 目標語言詞彙表大小 12 SHARE_EMB_AND_SOFTMAX = True 13 14 SOS_ID = 1 15 EOS_ID = 2 16 17 class NMTModel(object): 18 def __init__(self): 19 # 與訓練模型中的__init__函數相同。 20 self.enc_cell = tf.nn.rnn_cell.MultiRNNCell( 21 [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)] 22 ) 23 self.dec_cell = tf.nn.rnn_cell.MultiRNNCell( 24 [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)] 25 ) 26 27 # 爲源語言和目標語言分別定義詞向量 28 self.src_embedding = tf.get_variable('src_emb', [SRC_VOCAB_SIZE, HIDDEN_SIZE]) 29 self.trg_embedding = tf.get_variable('trg_emb', [TRG_VOCAB_SIZE, HIDDEN_SIZE]) 30 31 # softmax層的變量 32 if SHARE_EMB_AND_SOFTMAX: 33 self.softmax_weight = tf.transpose(self.trg_embedding) 34 else: 35 self.softmax_weight = tf.get_variable('weight', [HIDDEN_SIZE, TRG_VOCAB_SIZE]) 36 self.softmax_bias = tf.get_variable('softmax_bias', [TRG_VOCAB_SIZE]) 37 38 def inference(self, src_input): 39 # dynamic_rnn要求輸入是batch形式,所以這裏須要把輸入整理爲大小爲1的batch 40 src_size = tf.convert_to_tensor([len(src_input)], dtype=tf.int32) # 加上`[]`使其batch爲1 # shape=(1,) 41 src_input = tf.convert_to_tensor([src_input], dtype=tf.int32) # 這也要加, 與上面對應,shape=(1, 6) 42 src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input) # (1, 6, 1024) 43 44 # 構造編碼器,與訓練時一致 45 with tf.variable_scope('encoder'): 46 # 輸入數據的rank至少爲3(batch_size, max_time, ...) 47 # enc_output.shape=(1, 6, 1024) 48 enc_output, enc_state = tf.nn.dynamic_rnn( 49 self.enc_cell, src_emb, src_size, dtype=tf.float32 50 ) 51 52 # 設置解碼的最大步數,以免極端狀況下出現無限循環問題。 53 MAX_DEC_LEN = 100 54 55 with tf.variable_scope('decoder/rnn/multi_rnn_cell'): # 這樣寫是爲了與加載文件中的一致 56 # 使用一個變長的tensorarray來存儲生成的句子 57 init_array = tf.TensorArray(dtype=tf.int32, size=0, dynamic_size=True, clear_after_read=False) 58 # 填入第一個單詞<sos>做爲解碼器的輸入 59 # init_array.read() 60 # init_array.write() 61 # init_array.stack() 62 init_array = init_array.write(0, SOS_ID) 63 # 構建初始的循環狀態,循環狀態包含循環神經網絡的隱藏狀態,保存生成句子的tensorarry,以及記錄解碼步數step 64 init_loop_var = (enc_state, init_array, 0) 65 66 # tf.while_loop的循環條件: 直到解碼器輸出<sos>,或者達到最大步數 67 def continue_loop_condition(state, trg_ids, step): 68 # trg_ids.read(step), 即init_array.read(step) 69 # tf.reduce_all 70 return tf.reduce_all(tf.logical_and( # 去掉reduce_all也沒問題 71 tf.not_equal(trg_ids.read(step), EOS_ID), 72 tf.less(step, MAX_DEC_LEN-1) 73 )) 74 75 def loop_body(state, trg_ids, step): 76 # 讀取最後一步輸出的單詞,並讀取其詞向量 77 trg_input = [trg_ids.read(step)] # 加一箇中括號 # (1,1) 78 trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input) # (1, 1, 1024) 79 # 這裏不使用dynamic_rnn,而是直接調用dec_cell向前計算一步 80 dec_outputs, next_state = self.dec_cell.call(state=state, inputs=trg_emb) # (1, 1, 1024) 81 # 計算每一個可能的輸出單詞對應的logits 82 output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE]) # (1, 1024) 83 logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias # (1, 4000) 84 # 選取logit值最大的單詞做爲這一步的輸出 85 next_id = tf.argmax(logits, axis=1, output_type=tf.int32) 86 # 將這一步輸出的單詞寫入循環狀態的trg_ids中 87 trg_ids = trg_ids.write(step+1, next_id[0]) # 寫入,而後下一步輸出 88 return next_state, trg_ids, step+1 89 90 # 執行循環tf.while_loop,返回最終狀態 91 state, trg_ids, step = tf.while_loop(continue_loop_condition, loop_body, init_loop_var) 92 return trg_ids.stack() 93 94 95 def main(x): 96 with tf.variable_scope('nmt_model', reuse=None): 97 model = NMTModel() 98 99 test_sentence = [90, 13, 9, 689, 4, 2] 100 output_op = model.inference(test_sentence) 101 102 saver = tf.train.Saver() 103 with tf.Session() as sess: 104 105 saver.restore(sess, CHECKPOINT_PATH) 106 107 # 讀取翻譯結果 108 output = sess.run(output_op) 109 print('result: ', output) 110 111 if __name__ == '__main__': 112 tf.app.run()
API:
tf.string_split()、tf.string_join()
1 strs = tf.string_split([b'95 13 1590 0 4 11 90 4870 0 4 2']) 2 SparseTensor(indices=Tensor("StringSplit:0", shape=(?, 2), dtype=int64), values=Tensor("StringSplit:1", shape=(?,), dtype=string), dense_shape=Tensor("StringSplit:2", shape=(2,), dtype=int64)) # 有3個屬性:indices、values、dense_shape 3 4 with tf.Session() as sess: 5 print(sess.run(strs)) 6 SparseTensorValue(indices=array([[ 0, 0], 7 [ 0, 1], 8 [ 0, 2], 9 [ 0, 3], 10 [ 0, 4], 11 [ 0, 5], 12 [ 0, 6], 13 [ 0, 7], 14 [ 0, 8], 15 [ 0, 9], 16 [ 0, 10]], dtype=int64), values=array([b'95', b'13', b'1590', b'0', b'4', b'11', b'90', b'4870', b'0', 17 b'4', b'2'], dtype=object), dense_shape=array([ 1, 11], dtype=int64))
驗證是否改變shape:embedding改變、dropout、dynastic_rnn不變。具體代碼看訓練過程。