自然語言處理(五)——實現機器翻譯Seq2Seq完整經過


參考書

《TensorFlow:實戰Google深度學習框架》(第2版)

我只能說這本書太爛了,看完這本書中關於自然語言處理的內容,代碼全部敲了一遍,感覺學的很絕望,代碼也運行不了。

具體原因,我也寫過一篇博客diss過這本書。可是既然學了,就要好好學呀。為了搞懂自然語言處理,我毅然決然的學習了網上的各位小伙伴的博客。這里是我學習的簡要過程,和代碼,以及運行結果。大家共勉。

 參考鏈接:

https://blog.csdn.net/qq_33431368/article/details/85782869


目錄

0. 數據准備

1. 數據切片

2. 數據集的預處理

3. 數據的batching方法

4. Seq2Seq模型的代碼實現

4.1 模型訓練

4.2 解碼或推理程序

5 Attention機制(注意力機制)

6. 總結


學習過程:

0. 數據准備

我用的數據就是參考鏈接里面的數據。即一個TED 演講的中英字幕。

下載地址:

https://wit3.fbk.eu/mt.php?release=2015-01

1. 數據切片

簡單來說,就是,我們得到的文件里面都是自然語言,“今天天氣很好。”這樣的句子。我們首先要做的就是要將這些句子里的每一個字以及標點符號,用空格隔開。所以第一步就是利用工具進行文本切片。(具體方法看鏈接,這里不贅述)

我們要進行處理的文件是下面兩個。

 但是在這兩個文件里面除了演講內容中英文之外,還有關於演講主題的一些信息,如下圖。

我用正則表達式的方法(現百度現用)去除了這些介紹部分的文字。英文和中文只需要改變名字和路徑就行了,下面貼代碼:

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: file_deal.py @time: 2019/3/19 11:05 @desc: 對兩個文件進行處理,將兩個文件中的表頭信息清除 """ import re def txt_clean(x): pattern = re.compile(r'<\w*>|</\w*>') result = pattern.search(x) return result def main(): path = 'D:/Python3Space/Seq2SeqLearning/en-zh/' # file = 'train.tags.en-zh.en' # save_path = 'D:/Python3Space/Seq2SeqLearning/train.en' file = 'train.tags.en-zh.zh' save_path = 'D:/Python3Space/Seq2SeqLearning/train.zh' path1 = path + file output = open(save_path, 'w', encoding='utf-8') with open(path1, 'r', encoding='utf-8') as f: x = f.readlines() for y in x: result = txt_clean(y) # print(result) if result is None: # print(y) output.write(y) output.close() if __name__ == '__main__': main()

運行這個程序就能得到兩個文件,分別是去除了介紹文字部分的英文和中文翻譯:

內容如下(我不知道為什么截圖之后圖片變窄了,很難受):

 接下來我們需要有那種每個元素都是由空格所分開的(包括所有符號)。在這里,我選取的中文和英文的分詞工具都是stanfordcorenlp,相關知識請參考這篇博客

很絕望的是,我安裝過程中Python還報錯了:

所以我借鑒知乎上面的回答解決了這個問題。

為什么要選用斯坦福的工具???

明明可以用nltk來對英文進行分詞,用jieba對中文進行分詞,可是當我搜到斯坦福這個自然語言處理工具的時候,我想了想,天啦,這個名字也太酷炫了吧,一看就很復雜啊,中英文用同一個庫進行分詞應該要比較好吧,我要搞定它。然后按照網上的教程,真的很容易的就安裝了,然后很容易的就是實現對英文的分詞了。可是!真的有毒吧!對中文進行分詞的時候,我特么輸出的是['', '', '', '']這種空值,無論怎么解決都沒有辦法,我真的要崩潰了。還好我看到了這篇文章。嗯。。。寫的非常不錯,竟然有我各種百度都沒找到的解決辦法,簡直是太開心了好嗎!可是!

里面寫的 corenlp.py 到底在哪兒阿喂!

我哭了真的!

你們看,里面明明就只有一個叫corenlp.sh的文件好嗎?打開之后里面也並不是知乎里面所說的內容啊!

在這個令人絕望的關頭!我靈光一閃。。。.py!.py!莫不是在python庫文件里面。。。然后我就立馬去python3目錄下找。

我是真的快樂,真的。。。特別是竟然出現了兩個corenlp.py(雖然其中一個是nltk的啦,nltk是可以調用斯坦福的模塊的,所以如果你百度的話,是可以查到如何用nlkt調用Stanfordcorenlp進行中文分詞啊,語義解析啊等等的。我瞄了一眼一看就覺得不適合我哈哈!)

然后就順利的根據上面知乎里的解決辦法,替換了corenlp.py文件里面的某些關鍵字(具體內容點開上面的鏈接就知道啦)。

下面是我寫的中英文分詞的demo。

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: seperate_word.py @time: 2019/4/1 14:34 @desc: 對處理好的中英文數據進行分詞操作(demo) """ from stanfordcorenlp import StanfordCoreNLP sentence1 = "大家想想,海洋占了地球面積的75%。" sentence2 = "When you think about it, the oceans are 75 percent of the planet." nlp = StanfordCoreNLP('D:/python包/stanford-corenlp-full-2016-10-31', lang='zh') nlp2 = StanfordCoreNLP('D:/python包/stanford-corenlp-full-2016-10-31', lang='en') print(nlp.word_tokenize(sentence1)) print(nlp2.word_tokenize(sentence2))

運行之后得到:

我現在是真的快樂!真的!真的是萬事開頭難,中間難,結尾難!

什么?你竟然不相信我會用jieba和nltk。

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: seperate_word.py @time: 2019/4/1 14:34 @desc: 對處理好的中英文數據進行分詞操作(demo) """ from stanfordcorenlp import StanfordCoreNLP import jieba import nltk # nltk.download("punkt") sentence1 = "大家想想,海洋占了地球面積的75%。" sentence2 = "When you think about it, the oceans are 75 percent of the planet." # nlp = StanfordCoreNLP('D:/python包/stanford-corenlp-full-2016-10-31', lang='zh') # nlp2 = StanfordCoreNLP('D:/python包/stanford-corenlp-full-2016-10-31', lang='en') # print(nlp.word_tokenize(sentence1)) # print(nlp2.word_tokenize(sentence2)) seg_list = jieba.cut(sentence1, cut_all=False) tokens = nltk.word_tokenize(sentence2) print(list(seg_list)) print(tokens)

運行之后得到:

要記得在使用nltk工具包的時候,要下載對應的語言包,不然就會報錯。也可以預先下載好所有的語言包,可是速度也太慢了吧,我還是用啥下啥好了。真的是巨慢。。。(下載完所有的好像得3、4個G)

那我給出大佬的解決辦法,大家自行下載呀。(這里是punkt庫)

如果想要所有的庫,可以去官網下載,離線解壓就完事兒了。

有了demo之后,我們就可以對中英文數據進行切片了:

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: seperate_word_en.py @time: 2019/4/7 15:37 @desc: 處理好的英文數據進行分詞操作 """ from stanfordcorenlp import StanfordCoreNLP import time path = 'D:/Python3Space/Seq2SeqLearning/' en_path = path + 'train.en' zh_path = path + 'train.zh' nlp = StanfordCoreNLP('D:/python包/stanford-corenlp-full-2016-10-31', lang='zh') # en = open(path + 'test.en', 'w', encoding='utf-8') zh = open(path + 'test.zh', 'w', encoding='utf-8') with open(zh_path, 'r', encoding='utf-8') as f: data = f.readlines() for text in data: print(text) if text != "\n": fenci = nlp.word_tokenize(text) sen = ' '.join(fenci) zh.write(sen + '\n') else: zh.write('\n') zh.close()

處理之后分別得到兩個文件:

里面是已經處理好的句子,各個句子進行了切片處理,每個元素用空格隔開。

 

2. 數據集的預處理

為了將文本轉化為模型可以讀入的單詞序列,需要將這4000個中文詞匯,10000個英文詞匯分別映射到0~9999之間的整數編號。

我們首先按照詞頻順序確定詞匯表,然后將詞匯表保存到兩個獨立的vocab的文件中。

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: statistic_word1.py @time: 2019/4/22 9:36 @desc: 首先按照詞頻順序為每個詞匯分配一個編號,然后將詞匯表保存到一個獨立的vocab文件中。 """ import codecs import collections from operator import itemgetter def deal(lang): # 訓練集數據文件 ROOT_PATH = "D:/Python3Space/Seq2SeqLearning/" if lang == "zh": RAW_DATA = ROOT_PATH + "test.zh" # 輸出的詞匯表文件 VOCAB_OUTPUT = ROOT_PATH + "zh.vocab" # 中文詞匯表單詞個數 VOCAB_SIZE = 4000 elif lang == "en": RAW_DATA = ROOT_PATH + "test.en" VOCAB_OUTPUT = ROOT_PATH + "en.vocab" VOCAB_SIZE = 10000 else: print("what?") # 統計單詞出現的頻率 counter = collections.Counter() with codecs.open(RAW_DATA, "r", "utf-8") as f: for line in f: for word in line.strip().split(): counter[word] += 1 # 按照詞頻順序對單詞進行排序 sorted_word_to_cnt = sorted(counter.items(), key=itemgetter(1), reverse=True) sorted_words = [x[0] for x in sorted_word_to_cnt] # 在后面處理機器翻譯數據時,出了"<eos>",還需要將"<unk>"和句子起始符"<sos>"加入 # 詞匯表,並從詞匯表中刪除低頻詞匯。在PTB數據中,因為輸入數據已經將低頻詞匯替換成了 # "<unk>",因此不需要這一步驟。 sorted_words = ["<unk>", "<sos>", "<eos>"] + sorted_words if len(sorted_words) > VOCAB_SIZE: sorted_words = sorted_words[:VOCAB_SIZE] with codecs.open(VOCAB_OUTPUT, 'w', 'utf-8') as file_output: for word in sorted_words: file_output.write(word + "\n") if __name__ == "__main__": # 處理的語言 lang = ["zh", "en"] for i in lang: deal(i) 

處理之后分別得到兩個文件:

每個文件的內容如下:

在確定了詞匯表之后,再講訓練文件、測試文件等都根據詞匯文件轉化為單詞編號。每個單詞的編號就是它在詞匯文件的行號。

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: statistic_word2.py @time: 2019/4/22 10:59 @desc: 在確定了詞匯表之后,再將訓練文件、測試文件等都根據詞匯文件轉化為單詞編號。每個單詞的編號就是它在詞匯文件中的行號。 """ import codecs def deal(lang): # 訓練集數據文件 ROOT_PATH = "D:/Python3Space/Seq2SeqLearning/" if lang == "zh": # 原始的訓練集數據文件 RAW_DATA = ROOT_PATH + "test.zh" # 上面生成的詞匯表文件 VOCAB = ROOT_PATH + "zh.vocab" # 將單詞替換成為單詞編號后的輸出文件 OUTPUT_DATA = ROOT_PATH + "zh.number" elif lang == "en": RAW_DATA = ROOT_PATH + "test.en" VOCAB = ROOT_PATH + "en.vocab" OUTPUT_DATA = ROOT_PATH + "en.number" else: print("what?") # 讀取詞匯表,並建立詞匯到單詞編號的映射。 with codecs.open(VOCAB, "r", "utf-8") as f_vocab: vocab = [w.strip() for w in f_vocab.readlines()] word_to_id = {k: v for (k, v) in zip(vocab, range(len(vocab)))} # 如果出現了被刪除的低頻詞,則替換為"<unk>"。 def get_id(word): return word_to_id[word] if word in word_to_id else word_to_id["<unk>"] fin = codecs.open(RAW_DATA, "r", "utf-8") fout = codecs.open(OUTPUT_DATA, 'w', 'utf-8') for line in fin: # 讀取單詞並添加<eos>結束符 words = line.strip().split() + ["<eos>"] # 將每個單詞替換為詞匯表中的編號 out_line = ' '.join([str(get_id(w)) for w in words]) + '\n' fout.write(out_line) fin.close() fout.close() if __name__ == "__main__": # 處理的語言 lang = ["zh", "en"] for i in lang: deal(i)

在這里我不得不說一句!


我是真的看不懂這句代碼

words = ["<sos>"] + line.strip().split() + ["<eos>"]

這句代碼是我寫的,但是在參考書中,還是我參考的別人的博客中。他們的都是下面這句

words = line.strip().split() + ["<eos>"]

書就不拍照了,嫌麻煩,別人的博客,我可以貼圖為證:

我是真的佛了,書里面明明說的清清楚楚,我們講道理

在后面處理機器翻譯數據時,除了"<eos>",還需要將"<unk>"和句子起始符"<sos>"加入詞匯表,並從詞匯表中刪除低頻詞匯。

我真的是找了全文,都沒有看到哪里在每個句子的前面加了“<eos>”的,我才幡然醒悟,不就是在這里加入句子起始符嗎?為啥句子結束符都加了,憑什么不加句子起始符啊。。。我真的是無語了,無話可說,搞不懂這些人的代碼是怎么跑的通的。。。


對不起!上面寫的都是放屁!

我希望所有跟我這樣想的人,都要注意!這里只加<eos>是有道理的!,在下面batching的時候,會說為什么數據預處理的時候只在每個句子的后面加<eos>!

運行之后,就把原來的中英文文件,變成了兩個中英文數字。

 

3. 數據的batching方法

在PTB的數據中,句子之間有上下文關聯,因此可以直接將句子連接起來成為一個大的段落。

而在機器翻譯的訓練樣本中,每個句子對通常都是作為獨立的數據來訓練的。由於每個句子的長短不一致,因此在將這些句子放到同一個batch時,需要將較短的句子補齊到與同 batch 內最長句子相同的長度。用於填充長度而填入的位置叫做填充(padding)。在TensorFlow中,tf.data.Dataset 的 padded_batch 函數可以解決這個問題。

假設一個數據集中有4句話,分別是 ”A1A2A3A4”,“B1B2”,“C1C2C3C4C5C6C7”和“D1”,將它們加入必要的填充並組成大小為2 的batch后,得到的batch如下圖所示:

循環神經網絡在讀取數據時會將填充位置的內容與其他內容一樣納入計算,因此為了不讓填充影響訓練,可能會影響訓練結果和loss的計算,所以需要以下兩個解決對策:

第一,循環神經網絡在讀取填充時,應當跳過這一位置的計算。以編碼器為例,如果編碼器在讀取填充時,像正常輸入一樣處理填充輸入,那么在讀取"B1B200”之后產生的最后一位隱藏序列就和讀取“B1B2”之后的隱藏狀態不同,會產生錯誤的結果。通俗一點來說就是通過編碼器預測,輸入原始數據+padding數據產生的結果變了。
但是TensorFlow提供了 tf.nn.dynamic_rnn函數來很方便的實現這一功能,解決這個問題。dynamic_rnn 對每一個batch的數據讀取兩個輸入。
①輸入數據的內容(維度為[batch_size, time])
②輸入數據的長度(維度為[time])
對於輸入batch里的每一條數據,在讀取了相應長度的內容后,dynamic_rnn就跳過后面的輸入,直接把前一步的計算結果復制到后面的時刻。這樣可以保證padding是否存在不影響模型效果。通俗來說就是用一個句子的長度也就是time來把控這一點。
並且使用dynamic_rnn時每個batch的最大序列長度不需要相同。例如上面的例子,batch大小為2,第一個batch的維度是2x4,而第二個batch的維度是2x7。在訓練中dynamic_rnn會根據每個batch的最大長度動態展開到需要的層數,其實就是對每個batch本身的最大長度沒有關系,函數會自動動態(dynamic)調整。
第二,在設計損失函數時需要特別將填充位置的損失權重設置為 0 ,這樣在填充位置產生的預測不會影響梯度的計算。

下面的代碼使用tf.data.Dataset.padded_batch 來進行填充和 batching,並記錄每個句子的序列長度以用作dynamic_rnn的輸入。與上篇文章PTB的例子不同,這里沒有將所有的數據讀入內存,而是使用Dataset從磁盤動態讀取數據。

沒錯,上面的我是抄的,抄的(別人抄書)的,為什么?

當然是因為我懶啊。

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: statistic_word3.py @time: 2019/4/22 15:13 @desc: 使用tf.data.Dataset.padded_batch來進行填充和batching,並記錄每個句子的序列長度以用作dynamic_rnn的輸入。 與前面PTB的例子不同,這里沒有將所有數據讀入內存,而是使用Dataset從磁盤動態讀取數據。 """ import tensorflow as tf # 限定句子的最大單詞數量 MAX_LEN = 50 # 目標語言詞匯表中<sos>的ID SOS_ID = 1 # 使用Dataset從一個文件中讀取一個語言的數據 # 數據的格式為每行一句話,單詞已經轉化為單詞的編號 def MakeDataset(file_path): dataset = tf.data.TextLineDataset(file_path) # 根據空格將單詞編號切分開並放入一個一維向量 dataset = dataset.map(lambda string: tf.string_split([string]).values) # 將字符串形式的單詞編號轉化為整數 dataset = dataset.map(lambda string: tf.string_to_number(string, tf.int32)) # 統計每個句子的單詞數量,並與句子內容一起放入Dataset中。 dataset = dataset.map(lambda x: (x, tf.size(x))) return dataset # 從源語言文件src_path和目標語言文件trg_path中分別讀取數據,並進行填充和batching操作 def MakeSrcTrgDataset(src_path, trg_path, batch_size): # 首先分別讀取源語言數據和目標語言數據。 src_data = MakeDataset(src_path) trg_data = MakeDataset(trg_path) # 通過zip操作將兩個Dataset合並為一個Dataset。現在每個Dataset中每一項數據ds由4個張量組成。 # ds[0][0]是源句子 # ds[0][1]是源句子長度 # ds[1][0]是目標句子 # ds[1][1]是目標句子長度 dataset = tf.data.Dataset.zip((src_data, trg_data)) # 刪除內容為空(只包含<eos>和<sos>)的句子和長度過長的句子 def FileterLength(src_tuple, trg_tuple): ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple) src_len_ok = tf.logical_and(tf.greater(src_len, 1), tf.less_equal(src_len, MAX_LEN)) trg_len_ok = tf.logical_and(tf.greater(trg_len, 1), tf.less_equal(trg_len, MAX_LEN)) return tf.logical_and(src_len_ok, trg_len_ok) dataset = dataset.filter(FileterLength) # 解碼器需要兩種格式的目標句子: # 1.解碼器的輸入(trg_input),形式如同"<sos> X Y Z" # 2.解碼器的目標輸出(trg_label),形式如同"X Y Z <eos>" # 上面從文件中讀到的目標句子是"X Y Z <eos>"的形式,我們需要從中生成"<sos> X Y Z" # 形式並加入到Dataset中。 def MakeTrgInput(src_tuple, trg_tuple): ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple) trg_input = tf.concat([[SOS_ID], trg_label[:-1]], axis=0) return (src_input, src_len), (trg_input, trg_label, trg_len) dataset = dataset.map(MakeTrgInput) # 隨機打亂訓練數據 dataset = dataset.shuffle(10000) # 規定填充后輸出的數據維度 padded_shapes = ( (tf.TensorShape([None]), # 源句子是長度未知的向量 tf.TensorShape([])), # 源句子長度是單個數字 (tf.TensorShape([None]), # 目標句子(解碼器輸入)是長度未知的向量 tf.TensorShape([None]), # 目標句子(解碼器目標輸出)是長度未知的向量 tf.TensorShape([])) # 目標句子長度是單個數字 ) # 調用padded_batch方法進行batching操作 batched_dataset = dataset.padded_batch(batch_size, padded_shapes) return batched_dataset 

你們看!這里就解釋了上面所說的:為什么在數據預處理部分,只給每個句子的末尾加入<eos>!

# 解碼器需要兩種格式的目標句子:
#   1.解碼器的輸入(trg_input),形式如同"<sos> X Y Z"
#   2.解碼器的目標輸出(trg_label),形式如同"X Y Z <eos>"
# 上面從文件中讀到的目標句子是"X Y Z <eos>"的形式,我們需要從中生成"<sos> X Y Z"
# 形式並加入到Dataset中。

至於為什么?別問!問我就是迷茫!反正我到現在已經被編碼器,解碼器輸入,解碼器輸出給整糊塗了。有沒有懂的大佬給解釋一下!評論區等你!

我懂了!話不多說,一張圖你就能懂!(知乎上的圖,侵刪)

你們看右邊的解碼器!再配上我下面的這個圖(這個是書上的圖,但我從別人的博客上拔下來的哈哈)

在這里插入圖片描述

解碼器輸入的序列:<sos>, x, y, z

解碼器輸出的序列:x, y, z, <eos>

好,這個問題不用我再多說了吧,還是不懂的朋友,評論區見!

4. Seq2Seq模型的代碼實現

4.1 模型訓練

LSTM 作為循環神經網絡的主體,並在 Softmax 層和詞向量層之間共享參數,增加如下:

增加了一個循環神經網絡作為編碼器(如前面示意圖)
使用 Dataset 動態讀取數據,而不是直接將所有數據讀入內存(這個就是Dataset輸入數據的特點)
每個 batch 完全獨立,不需要在batch之間傳遞狀態(因為不是一個文件整條句子,每個句子之間沒有傳遞關系)
每訓練200步便將模型參數保存到一個 checkpoint 中,以后用於測試。

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: statistic_word4.py @time: 2019/4/23 9:50 @desc: 完整實現一個Seq2Seq模型。 """ import tensorflow as tf from translate.dynamic_rnn_test1 import MakeSrcTrgDataset root_path = "D:/Python3Space/Seq2SeqLearning/" # 輸入數據已經轉換成了單詞編號的格式。 # 源語言輸入文件 SRC_TRAIN_DATA = root_path + "en.number" # 目標語言輸入文件 TRG_TRAIN_DATA = root_path + "zh.number" # checkpoint保存路徑 CHECKPOINT_PATH = root_path + "seq2seq_ckpt" # LSTM的隱藏層規模 HIDDEN_SIZE = 1024 # 深層循環神經網絡中LSTM結構的層數 NUM_LAYERS = 2 # 源語言詞匯表大小 SRC_VOCAB_SIZE = 10000 # 目標語言詞匯表大小 TRG_VOCAB_SIZE = 4000 # 訓練數據batch的大小 BATCH_SIZE = 100 # 使用訓練數據的輪數 NUM_EPOCH = 5 # 節點不被dropout的概率 KEEP_PROB = 0.8 # 用於控制梯度膨脹的梯度大小上限 MAX_GRAD_NROM = 5 # 在Softmax層和詞向量層之間共享參數 SHARE_EMB_AND_SOFTMAX = True # 定義NMTModel類來描述模型 class NMTModel(object): # 在模型的初始化函數中定義模型要用到的變量 def __init__(self): # 定義編碼器和解碼器所使用的LSTM結構 self.enc_cell = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)]) self.dec_cell = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)]) # 為源語言和目標語言分別定義詞向量 self.src_embedding = tf.get_variable("src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE]) self.trg_embedding = tf.get_variable("trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE]) # 定義softmax層的變量 if SHARE_EMB_AND_SOFTMAX: self.softmax_weight = tf.transpose(self.trg_embedding) else: self.softmax_weight = tf.get_variable("weight", [HIDDEN_SIZE, TRG_VOCAB_SIZE]) self.softmax_bias = tf.get_variable("softmax_bias", [TRG_VOCAB_SIZE]) # 在forward函數中定義模型的前向計算圖 # src_input, src_size, trg_input, trg_label, trg_size分別是上面MakeSrcTrgDataset函數產生的五種張量 def forward(self, src_input, src_size, trg_input, trg_label, trg_size): batch_size = tf.shape(src_input)[0] # 將輸入和輸出單詞編號轉為詞向量 src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input) trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input) # 在詞向量上進行dropout src_emb = tf.nn.dropout(src_emb, KEEP_PROB) trg_emb = tf.nn.dropout(trg_emb, KEEP_PROB) # 使用dynamic構造編碼器 # 編碼器讀取源句子每個位置的詞向量,輸出最后一步的隱藏狀態enc_state # 因為編碼器是一個雙層LSTM,因此enc_state是一個包含兩個LSTMStateTuple類的tuple,每個LSTMStateTuple對應編碼器中一層的狀態。 # enc_outputs是頂層LSTM在每一步的輸出,它的維度是[batch_size, max_time, HIDDEN_SIZE]。 # Seq2Seq模型中不需要用到enc_outputs,而在后面介紹的attention模型中會用到它。 with tf.variable_scope("encoder"): enc_outputs, enc_state = tf.nn.dynamic_rnn(self.enc_cell, src_emb, src_size, dtype=tf.float32) # 使用dynamic_rnn構造解碼器 # 解碼器讀取目標句子每個位置的詞向量,輸出的dec_outputs為每一步頂層LSTM的輸出。 # dec_outputs的維度是[batch_size, max_time, HIDDEN_SIZE] # initial_state=enc_state表示用編碼器的輸出來初始化第一步的隱藏狀態。 with tf.variable_scope("decoder"): dec_outputs, _ = tf.nn.dynamic_rnn(self.dec_cell, trg_emb, trg_size, initial_state=enc_state) # 計算解碼器每一步的log perplexity。這一步與語言模型的代碼相同。 output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE]) logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=tf.reshape(trg_label, [-1]), logits=logits) # 在計算平均損失時,需要將填充位置的權重設置為0,以避免無效位置的預測干擾模型的訓練。 label_weights = tf.sequence_mask(trg_size, maxlen=tf.shape(trg_label)[1], dtype=tf.float32) label_weights = tf.reshape(label_weights, [-1]) cost = tf.reduce_sum(loss * label_weights) cost_per_token = cost / tf.reduce_sum(label_weights) # 定義反向傳播操作。反向操作的實現與語言模型代碼相同。 trainable_variables = tf.trainable_variables() # 控制梯度大小,定義優化方法和訓練步驟。 grads = tf.gradients(cost / tf.to_float(batch_size), trainable_variables) grads, _ = tf.clip_by_global_norm(grads, MAX_GRAD_NROM) optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0) train_op = optimizer.apply_gradients(zip(grads, trainable_variables)) return cost_per_token, train_op # 使用給定的模型model上訓練一個epoch,並返回全局步數 # 每訓練200步便保存一個checkpoint def run_epoch(session, cost_op, train_op, saver, step): # 訓練一個epoch # 重復訓練步驟直至遍歷完Dataset中所有數據。 while True: try: # 運行train_op並計算損失值。訓練數據在main()函數中以Dataset方式提供 cost, _ = session.run([cost_op, train_op]) if step % 10 == 0: print("After %d steps, per token cost is %.3f" % (step, cost)) # 每200步保存一個checkoutpoint if step % 200 == 0: saver.save(session, CHECKPOINT_PATH, global_setp=step) step += 1 except tf.errors.OutOfRangeError: break return step def main(): # 定義初始化函數 initializer = tf.random_uniform_initializer(-0.05, 0.05) # 定義訓練用的循環神經網絡模型 with tf.variable_scope("nmt_model", reuse=None, initializer=initializer): train_model = NMTModel() # 定義輸入數據 data = MakeSrcTrgDataset(SRC_TRAIN_DATA, TRG_TRAIN_DATA, BATCH_SIZE) iterator = data.make_initializable_iterator() (src, src_size), (trg_input, trg_label, trg_size) = iterator.get_next() # 定義輸入數據 cost_op, train_op = train_model.forward(src, src_size, trg_input, trg_label, trg_size) # 訓練模型 saver = tf.train.Saver() step = 0 with tf.Session() as sess: tf.global_variables_initializer().run() for i in range(NUM_EPOCH): print("In iteration: %d" % (i + 1)) sess.run(iterator.initializer) step = run_epoch(sess, cost_op, train_op, saver, step) if __name__ == "__main__": main()

我一直以來都有一個疑問:詞向量究竟是啥意思,上述代碼中的這段代碼是啥意思?

# 為源語言和目標語言分別定義詞向量
self.src_embedding = tf.get_variable("src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE])
self.trg_embedding = tf.get_variable("trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE])

參考:這個鏈接

下面是我對上面問題的理解啊,也不知道對不對,大家多擔待:

  1. 首先,詞向量有兩種,一種是One-Hot Encoder,也就是以前常用的,當然現在也有用。
  2. 第二種,就是稠密向量,因為第一種每個詞向量也太大了,如果詞匯表有10000的話,那么對於某個詞來說,它的詞向量就是,一個1x10000的矩陣,其中的一個是1,其他9999都是0。
  3. 那么這第二種要怎么得到呢?(我的理解啊)就是用第一種方式轉化得到的。第一種方法得到的詞向量作為輸入,然后經過一個10000 x hidden_layer(隱藏層大小)的矩陣,矩陣乘法得到1 x hidden_layer 的稠密矩陣,這個矩陣就是我所說的第二種詞向量。
  4. 上述是我的理解,也不知道對不對。有大佬指點一下,我們就評論區見唄。

這里寫圖片描述

偷的別人博客中的圖,侵刪。。。

然后呢,我們再回到上面疑問的代碼中來。

# 為源語言和目標語言分別定義詞向量
self.src_embedding = tf.get_variable("src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE])
self.trg_embedding = tf.get_variable("trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE])

這部分代碼定義的就是第一種詞向量要乘的矩陣,shape是[詞匯表大小,隱藏層]。

# 將輸入和輸出單詞編號轉為詞向量
src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input)
trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input)

這一部分代碼是計算第二種詞向量的過程,雖然用的函數時tf.nn.embedding_lookup,實際上也就是矩陣乘法的意思。(附圖,侵刪)

 

右邊的就是我們所要求的的第二種詞向量啦。(這里隱藏層是3,我們設置的隱藏層是1024,為什么?別問!問就是不知道!)

第二種詞向量,也就是稠密詞向量,有什么用呢!

如果隱藏層是二維,或者三維的,我們不就能繪制出每個詞向量的圖了嗎!

比如說下面這個二維圖:(網上拿的圖,侵刪)

這不就能反映詞與詞之間的關系了!越接近的詞,詞義也越接近!

另外,在forward函數中定義模型的前向計算圖中的代碼好多根本沒看懂。跑就完事兒了,構造編碼器和解碼器的過程我是真的蒙了。以后有新的想法,弄懂了之后再回來補充好了。

運行出來的結果如下:

一萬年過后。。。(沒錯,中途我還換了個電腦跑程序,跑了大概兩天吧。。。)(這個截圖我也是醉了的。。。)

 得到的模型如下:

我保留了最后一個保存的模型(其他步保存的模型也有,但是太大太占位置了,就給刪了,主要也就是用最后這個)

4.2 解碼或推理程序

上面的程序完成了機器翻譯模型的訓練,並將訓練好的模型保存在checkpoint中。
下面是講解怎樣從checkpoint中讀取模型並對一個新的句子進行翻譯。對新輸入的句子進行翻譯的過程也稱為解碼或推理。
在訓練的時候解碼器是可以從輸入讀取到完整的目標訓練句子。
而在解碼或推理的過程中模型只能看到輸入句子,卻看不到目標句子
具體過程:和圖中描述的一樣,解碼器在第一步讀取<sos> 符,預測目標句子的第一個單詞,然后需要將這個預測的單詞復制到第二步作為輸入,再預測第二個單詞,直到預測的單詞為<eos>為止 。 這個過程需要使用一個循環結構來實現 。在TensorFlow 中,循環結構是由 tf.while_loop 來實現的 。

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: statistic_word5.py @time: 2019/4/29 10:39 @desc: 用tf.while_loop來實現解碼過程 """ import tensorflow as tf import codecs # 讀取checkpoint的路徑。9000表示是訓練程序在第9000步保存的checkpoint CHECKPOINT_PATH = "./seq2seq_ckpt-9800" # 模型參數。必須與訓練時的模型參數保持一致。 # LSTM的隱藏層規模 HIDDEN_SIZE = 1024 # 深層循環神經網絡中LSTM結構的層數 NUM_LAYERS = 2 # 源語言詞匯表大小 SRC_VOCAB_SIZE = 10000 # 目標語言詞匯表大小 TRG_VOCAB_SIZE = 4000 # 在Softmax層和詞向量層之間共享參數 SHARE_EMB_AND_SOFTMAX = True # 詞匯表中<sos>和<eos>的ID。在解碼過程中需要用<sos>作為第一步的輸入,並將檢查是否是<eos>,因此需要知道這兩個符號的ID SOS_ID = 1 EOS_ID = 2 # 詞匯表文件 SRC_VOCAB = "en.vocab" TRG_VOCAB = "zh.vocab" # 定義NMTModel類來描述模型 class NMTModel(object): # 在模型的初始化函數中定義模型要用到的變量 def __init__(self): # 與訓練時的__init__函數相同。通常在訓練程序和解碼程序中復用NMTModel類以及__init__函數,以確保解碼時和訓練時定義的變量是相同的 # 定義編碼器和解碼器所使用的LSTM結構 self.enc_cell = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)]) self.dec_cell = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)]) # 為源語言和目標語言分別定義詞向量 self.src_embedding = tf.get_variable("src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE]) self.trg_embedding = tf.get_variable("trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE]) # 定義softmax層的變量 if SHARE_EMB_AND_SOFTMAX: self.softmax_weight = tf.transpose(self.trg_embedding) else: self.softmax_weight = tf.get_variable("weight", [HIDDEN_SIZE, TRG_VOCAB_SIZE]) self.softmax_bias = tf.get_variable("softmax_bias", [TRG_VOCAB_SIZE]) def inference(self, src_input): # 雖然輸入只有一個句子,但因為dynamic_rnn要求輸入是batch的形式,因此這里將輸入句子整理為大小為1的batch src_size = tf.convert_to_tensor([len(src_input)], dtype=tf.int32) src_input = tf.convert_to_tensor([src_input], dtype=tf.int32) src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input) # 使用dynamic_rnn構造編碼器。這一步與訓練時相同 with tf.variable_scope("encoder"): enc_outputs, enc_state = tf.nn.dynamic_rnn(self.enc_cell, src_emb, src_size, dtype=tf.float32) # 設置解碼的最大步數。這是為了避免在極端情況出現無限循環的問題。 MAX_DEC_LEN = 100 with tf.variable_scope("decoder/rnn/multi_rnn_cell"): # 使用一個變長的TensorArray來存儲生成的句子 init_array = tf.TensorArray(dtype=tf.int32, size=0, dynamic_size=True, clear_after_read=False) # 填入第一個單詞<sos>作為解碼器的輸入 init_array = init_array.write(0, SOS_ID) # 構建初始的循環狀態。循環狀態包含循環神經網絡的隱藏狀態,保存生成句子的TensorArray,以及記錄解碼步數的一個整數step init_loop_var = (enc_state, init_array, 0) # tf.while_loop的循環條件 # 循環直到解碼器輸出<eos>,或者達到最大步數為止。 def continue_loop_condition(state, trg_ids, step): return tf.reduce_all(tf.logical_and(tf.not_equal(trg_ids.read(step), EOS_ID), tf.less(step, MAX_DEC_LEN-1))) def loop_body(state, trg_ids, step): # 讀取最后一步輸出的單詞,並讀取其詞向量 trg_input = [trg_ids.read(step)] trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input) # 這里不使用dynamic_rnn,而是直接調用dec_cell向前計算一步。 dec_outputs, next_state = self.dec_cell.call(state=state, inputs=trg_emb) # 計算每個可能的輸出單詞對應的logit,並選取logit值最大的單詞作為這一步的輸出。 output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE]) logits = (tf.matmul(output, self.softmax_weight) + self.softmax_bias) next_id = tf.argmax(logits, axis=1, output_type=tf.int32) # 將這一步輸出的單詞寫入循環狀態的trg_ids中 trg_ids = trg_ids.write(step+1, next_id[0]) return next_state, trg_ids, step+1 # 執行tf.while_loop,返回最終狀態 state, trg_ids, step = tf.while_loop(continue_loop_condition, loop_body, init_loop_var) return trg_ids.stack() def main(): # 定義訓練用的循環神經網絡模型 with tf.variable_scope("nmt_model", reuse=None): model = NMTModel() # 定義一個測試的例子 test_sentence = "This is a test ." print(test_sentence) # 根據英文詞匯表,將測試句子轉為單詞ID。結尾加上<eos>的編號 test_sentence = test_sentence + " <eos>" with codecs.open(SRC_VOCAB, 'r', 'utf-8') as vocab: src_vocab = [w.strip() for w in vocab.readlines()] # 運用dict,將單詞和id對應起來組成字典,用於后面的轉換 src_id_dict = dict((src_vocab[x], x) for x in range(SRC_VOCAB_SIZE)) test_en_ids = [(src_id_dict[en_text] if en_text in src_id_dict else src_id_dict['<unk>']) for en_text in test_sentence.split()] print(test_en_ids) # 建立解碼所需的計算圖 output_op = model.inference(test_en_ids) sess = tf.Session() saver = tf.train.Saver() saver.restore(sess, CHECKPOINT_PATH) # 讀取翻譯結果 output_ids = sess.run(output_op) print(output_ids) # 根據中文詞匯表,將翻譯結果轉換為中文文字。 with codecs.open(TRG_VOCAB, "r", "utf-8") as f_vocab: trg_vocab = [w.strip() for w in f_vocab.readlines()] output_text = ''.join([trg_vocab[x] for x in output_ids[1:-1]]) # 輸出翻譯結果 print(output_text) sess.close() if __name__ == "__main__": main()

OK!終於大功告成。。。讓我們來運行一下吧!

我哭了。真的。辛辛苦苦一頓操作,怎么就報錯了?我研讀了一下,發現上面說讀取的模型與我們定義的變量名不一致,報錯的變量名是

nmt_model/decoder/rnn/multi_rnn_cell/cell_0/basic_lstm_cell/bias

我去網上找了找解決辦法。。。啥也沒找到。算了,先想辦法看一下模型里面的變量是什么。

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: find_error.py @time: 2019/4/29 12:17 @desc: 報錯參數名字不一樣,檢查問題 """ import os from tensorflow.python import pywrap_tensorflow model_dir = './' checkpoint_path = os.path.join(model_dir, 'seq2seq_ckpt-9800') reader = pywrap_tensorflow.NewCheckpointReader(checkpoint_path) var_to_shape_map = reader.get_variable_to_shape_map() for key in var_to_shape_map: print("tensor_name: ", key) print(reader.get_tensor(key))

運行后:

為什么!!!解碼器和編碼器的參數前面都少了一個nmt_model。。。(有大佬知道嗎,求評論區解釋一下,我現在還沒搞懂。。。)

你們看其他的變量就正常的有nmt_model名。。。

我也是醉了。

然后我就去找有沒有修改已訓練模型變量名字的方法。。。

找了好久總算是讓我找到了!

參考:干貨!如何修改在TensorFlow框架下訓練保存的模型參數名稱

所以我們只需要把那些不知道為什么沒有"nmt_model/"前綴的tensor_name給他們加上這個前綴就OK了,下面貼代碼,我做了一定的修改

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: rename.py @time: 2019/4/29 14:23 @desc: 修改TensorFlow訓練保存的參數名 """ import tensorflow as tf import argparse import os parser = argparse.ArgumentParser(description='') # 原參數路徑 parser.add_argument("--checkpoint_path", default='./seq2seq_ckpt-9800', help="restore ckpt") # 新參數保存路徑 parser.add_argument("--new_checkpoint_path", default='./', help="path_for_new ckpt") # 新參數名稱中加入的前綴名 parser.add_argument("--add_prefix", default='nmt_model/', help="prefix for addition") args = parser.parse_args() l = ["encoder", "decoder"] def main(): # 如果改之后的模型路徑不存在就建立這個路徑 if not os.path.exists(args.new_checkpoint_path): os.makedirs(args.new_checkpoint_path) with tf.Session() as sess: # 新建一個空列表存儲更新后的Variable變量 new_var_list = [] # 得到checkpoint文件中所有的參數(名字,形狀)元組 for var_name, _ in tf.contrib.framework.list_variables(args.checkpoint_path): # 得到上述參數的值 var = tf.contrib.framework.load_variable(args.checkpoint_path, var_name) # 如果參數名是我們要修改的l中的兩個中的一個,我們就在前面加上nmt_model/ if l[0] in var_name or l[1] in var_name: new_name = var_name # 在這里加入了名稱前綴,大家可以自由地作修改 new_name = args.add_prefix + new_name else: new_name = var_name # 除了修改參數名稱,還可以修改參數值(var) print('Renaming %s to %s.' % (var_name, new_name)) # 使用加入前綴的新名稱重新構造了參數 renamed_var = tf.Variable(var, name=new_name) # 把賦予新名稱的參數加入空列表 new_var_list.append(renamed_var) print('starting to write new checkpoint !') # 構造一個保存器 saver = tf.train.Saver(var_list=new_var_list) # 初始化一下參數(這一步必做) sess.run(tf.global_variables_initializer()) # 構造一個保存的模型名稱 model_name = 'new_seq2seq_ckpt' # 構造一下保存路徑 checkpoint_path = os.path.join(args.new_checkpoint_path, model_name) # 直接進行保存 saver.save(sess, checkpoint_path) print("done !") if __name__ == '__main__': main() 

運行之后:

完事兒!開心!

我們再看一眼修改后模型的變量名是不是真的改了吧!(跟上面的代碼一樣,該個文件名,運行)

大功告成!!!

好,我們現在運行原來的翻譯程序吧!!!(改成修改后的模型)

CHECKPOINT_PATH = "./new_seq2seq_ckpt"

歐耶!!!翻譯成功!

5 Attention機制(注意力機制)

為什么需要Attention機制(參考鏈接

最基本的seq2seq模型包含一個encoder和一個decoder,通常的做法是將一個輸入的句子編碼成一個固定大小的state,然后作為decoder的初始狀態(當然也可以作為每一時刻的輸入),但這樣的一個狀態對於decoder中的所有時刻都是一樣的。 
attention即為注意力,人腦在對於的不同部分的注意力是不同的。需要attention的原因是非常直觀的,比如,我們期末考試的時候,我們需要老師划重點,划重點的目的就是為了盡量將我們的attention放在這部分的內容上,以期用最少的付出獲取盡可能高的分數;再比如我們到一個新的班級,吸引我們attention的是不是顏值比較高的人?普通的模型可以看成所有部分的attention都是一樣的,而這里的attention-based model對於不同的部分,重要的程度則不同。

其他細節就不贅述了,大家想要深入了解原理的話看參考鏈接。這里我主要弄代碼部分。

加入attention機制后模型訓練代碼(部分內容做了修改):

1. 需要修改的部分主要是把原來編碼器的多層rnn結構變成了一個雙向的rnn:

# 前向
self.enc_cell_fw = tf.nn.rnn_cell.LSTMCell(HIDDEN_SIZE)
# 反向
self.enc_cell_bw = tf.nn.rnn_cell.LSTMCell(HIDDEN_SIZE)

2. forward函數中的"encoder"和"decoder"部分都做了些許的修改。這里就不貼代碼了,大家一看便知。

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: attention_train.py @time: 2019/4/29 16:14 @desc: 注意力機制需要修改訓練中的代碼 """ import tensorflow as tf from Seq2SeqLearning.statistic_word3 import MakeSrcTrgDataset root_path = "D:/Python3Space/Seq2SeqLearning/" # 輸入數據已經轉換成了單詞編號的格式。 # 源語言輸入文件 SRC_TRAIN_DATA = root_path + "en.number" # 目標語言輸入文件 TRG_TRAIN_DATA = root_path + "zh.number" # checkpoint保存路徑 CHECKPOINT_PATH = root_path + "attention_ckpt" # LSTM的隱藏層規模 HIDDEN_SIZE = 1024 # 深層循環神經網絡中LSTM結構的層數 NUM_LAYERS = 2 # 源語言詞匯表大小 SRC_VOCAB_SIZE = 10000 # 目標語言詞匯表大小 TRG_VOCAB_SIZE = 4000 # 訓練數據batch的大小 BATCH_SIZE = 100 # 使用訓練數據的輪數 NUM_EPOCH = 5 # 節點不被dropout的概率 KEEP_PROB = 0.8 # 用於控制梯度膨脹的梯度大小上限 MAX_GRAD_NROM = 5 # 在Softmax層和詞向量層之間共享參數 SHARE_EMB_AND_SOFTMAX = True # 定義NMTModel類來描述模型,attention模型中 編碼器雙向循環,解碼器單向循環 class NMTModel(object): # 在模型的初始化函數中定義模型要用到的變量 def __init__(self): # 定義編碼器和解碼器所使用的LSTM結構 # 前向 self.enc_cell_fw = tf.nn.rnn_cell.LSTMCell(HIDDEN_SIZE) # 反向 self.enc_cell_bw = tf.nn.rnn_cell.LSTMCell(HIDDEN_SIZE) self.dec_cell = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)]) # 為源語言和目標語言分別定義詞向量 self.src_embedding = tf.get_variable("src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE]) self.trg_embedding = tf.get_variable("trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE]) # 定義softmax層的變量,只有解碼器需要用到softmax if SHARE_EMB_AND_SOFTMAX: self.softmax_weight = tf.transpose(self.trg_embedding) else: self.softmax_weight = tf.get_variable("weight", [HIDDEN_SIZE, TRG_VOCAB_SIZE]) self.softmax_bias = tf.get_variable("softmax_bias", [TRG_VOCAB_SIZE]) # 在forward函數中定義模型的前向計算圖 # src_input, src_size, trg_input, trg_label, trg_size分別是上面MakeSrcTrgDataset函數產生的五種張量 def forward(self, src_input, src_size, trg_input, trg_label, trg_size): batch_size = tf.shape(src_input)[0] # 將輸入和輸出單詞編號轉為詞向量 src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input) trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input) # 在詞向量上進行dropout src_emb = tf.nn.dropout(src_emb, KEEP_PROB) trg_emb = tf.nn.dropout(trg_emb, KEEP_PROB) # 使用dynamic構造編碼器 # 編碼器讀取源句子每個位置的詞向量,輸出最后一步的隱藏狀態enc_state # 因為編碼器是一個雙層LSTM,因此enc_state是一個包含兩個LSTMStateTuple類的tuple,每個LSTMStateTuple對應編碼器中一層的狀態。 # enc_outputs是頂層LSTM在每一步的輸出,它的維度是[batch_size, max_time, HIDDEN_SIZE]。 # Seq2Seq模型中不需要用到enc_outputs,而在后面介紹的attention模型中會用到它。 with tf.variable_scope("encoder"): # 構造編碼器時,使用bidirectional_dynamic_rnn構造雙向循環網絡。 # 雙向循環網絡的頂層輸出enc_outputs是一個包含兩個張量的tuple,每個張量的 # 維度都是[batch_size, max_time, HIDDEN_SIZE],代表兩個LSTM在每一步的輸出。 enc_outputs, enc_state = tf.nn.bidirectional_dynamic_rnn(self.enc_cell_fw, self.enc_cell_bw, src_emb, src_size, dtype=tf.float32) # 將兩個LSTM的輸出拼接為一個張量。 enc_outputs = tf.concat([enc_outputs[0], enc_outputs[1]], -1) # 使用dynamic_rnn構造解碼器 # 解碼器讀取目標句子每個位置的詞向量,輸出的dec_outputs為每一步頂層LSTM的輸出。 # dec_outputs的維度是[batch_size, max_time, HIDDEN_SIZE] # initial_state=enc_state表示用編碼器的輸出來初始化第一步的隱藏狀態。 with tf.variable_scope("decoder"): # 選擇注意力權重的計算模型。BahdanauAttention是使用一個隱藏層的前饋網絡。 # memory_sequence_length是一個維度為[batch_size]的張量,代表batch # 中每個句子的長度,Attention需要根據這個信息把填充位置的注意力權重設置為0。 attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(HIDDEN_SIZE, enc_outputs, memory_sequence_length=src_size) # 將解碼器的循環神經網絡self.dec_cell和注意力一起封裝成更高層的循環神經網絡。 attention_cell = tf.contrib.seq2seq.AttentionWrapper(self.dec_cell, attention_mechanism, attention_layer_size=HIDDEN_SIZE) # 使用attention_cell和dynamic_rnn構造編碼器。 # 這里沒有指定init_state,也就是沒有使用編碼器的輸出來初始化輸入,而完全依賴注意力作為信息來源。 dec_outputs, _ = tf.nn.dynamic_rnn(attention_cell, trg_emb, trg_size, dtype=tf.float32) # 計算解碼器每一步的log perplexity。這一步與語言模型的代碼相同。 output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE]) logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=tf.reshape(trg_label, [-1]), logits=logits) # 在計算平均損失時,需要將填充位置的權重設置為0,以避免無效位置的預測干擾模型的訓練。 label_weights = tf.sequence_mask(trg_size, maxlen=tf.shape(trg_label)[1], dtype=tf.float32) label_weights = tf.reshape(label_weights, [-1]) cost = tf.reduce_sum(loss * label_weights) cost_per_token = cost / tf.reduce_sum(label_weights) # 定義反向傳播操作。反向操作的實現與語言模型代碼相同。 trainable_variables = tf.trainable_variables() # 控制梯度大小,定義優化方法和訓練步驟。 grads = tf.gradients(cost / tf.to_float(batch_size), trainable_variables) grads, _ = tf.clip_by_global_norm(grads, MAX_GRAD_NROM) optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0) train_op = optimizer.apply_gradients(zip(grads, trainable_variables)) return cost_per_token, train_op # 使用給定的模型model上訓練一個epoch,並返回全局步數 # 每訓練200步便保存一個checkpoint def run_epoch(session, cost_op, train_op, saver, step): # 訓練一個epoch # 重復訓練步驟直至遍歷完Dataset中所有數據。 while True: try: # 運行train_op並計算損失值。訓練數據在main()函數中以Dataset方式提供 cost, _ = session.run([cost_op, train_op]) if step % 10 == 0: print("After %d steps, per token cost is %.3f" % (step, cost)) # 每200步保存一個checkoutpoint if step % 200 == 0: saver.save(session, CHECKPOINT_PATH, global_step=step) step += 1 except tf.errors.OutOfRangeError: break return step def main(): # 定義初始化函數 initializer = tf.random_uniform_initializer(-0.05, 0.05) # 定義訓練用的循環神經網絡模型 with tf.variable_scope("nmt_model", reuse=None, initializer=initializer): train_model = NMTModel() # 定義輸入數據 data = MakeSrcTrgDataset(SRC_TRAIN_DATA, TRG_TRAIN_DATA, BATCH_SIZE) iterator = data.make_initializable_iterator() (src, src_size), (trg_input, trg_label, trg_size) = iterator.get_next() # 定義前向計算圖。輸入數據以張量形式提供給forward函數。 cost_op, train_op = train_model.forward(src, src_size, trg_input, trg_label, trg_size) # 訓練模型 saver = tf.train.Saver() step = 0 with tf.Session() as sess: tf.global_variables_initializer().run() for i in range(NUM_EPOCH): print("In iteration: %d" % (i + 1)) sess.run(iterator.initializer) step = run_epoch(sess, cost_op, train_op, saver, step) if __name__ == "__main__": main()

跑模型實在是太耗時了,我掛了一夜就跑了2800步,旨在學習,就不像上面那樣跑兩天兩夜了。

運行結果:

我們就將就着2800步的模型用起來了。

加入attention機制后模型解碼代碼(部分內容做了修改,這里就不貼哪里修改了,跟訓練模型需要修改的地方相似):

在這里我們要注意,解碼的時候初始狀態並不是之前編碼器最后的隱藏狀態,而是什么都沒有的初始狀態:

init_loop_var = (attention_cell.zero_state(batch_size=1, dtype=tf.float32), init_array, 0)

#!/usr/bin/env python # -*- coding: UTF-8 -*- # coding=utf-8 """ @author: Li Tian @contact: 694317828@qq.com @software: pycharm @file: attention_decoding.py @time: 2019/4/29 16:35 @desc: 注意力機制需要修改解碼中的代碼 """ import tensorflow as tf import codecs # 讀取checkpoint的路徑。9000表示是訓練程序在第9000步保存的checkpoint CHECKPOINT_PATH = "./attention_ckpt-2800" # 模型參數。必須與訓練時的模型參數保持一致。 # LSTM的隱藏層規模 HIDDEN_SIZE = 1024 # 深層循環神經網絡中LSTM結構的層數 NUM_LAYERS = 2 # 源語言詞匯表大小 SRC_VOCAB_SIZE = 10000 # 目標語言詞匯表大小 TRG_VOCAB_SIZE = 4000 # 在Softmax層和詞向量層之間共享參數 SHARE_EMB_AND_SOFTMAX = True # 詞匯表中<sos>和<eos>的ID。在解碼過程中需要用<sos>作為第一步的輸入,並將檢查是否是<eos>,因此需要知道這兩個符號的ID SOS_ID = 1 EOS_ID = 2 # 詞匯表文件 SRC_VOCAB = "en.vocab" TRG_VOCAB = "zh.vocab" # 定義NMTModel類來描述模型 class NMTModel(object): # 在模型的初始化函數中定義模型要用到的變量 def __init__(self): # 與訓練時的__init__函數相同。通常在訓練程序和解碼程序中復用NMTModel類以及__init__函數,以確保解碼時和訓練時定義的變量是相同的 # 定義編碼器和解碼器所使用的LSTM結構 # 前向 self.enc_cell_fw = tf.nn.rnn_cell.LSTMCell(HIDDEN_SIZE) # 反向 self.enc_cell_bw = tf.nn.rnn_cell.LSTMCell(HIDDEN_SIZE) self.dec_cell = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)]) # 為源語言和目標語言分別定義詞向量 self.src_embedding = tf.get_variable("src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE]) self.trg_embedding = tf.get_variable("trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE]) # 定義softmax層的變量 if SHARE_EMB_AND_SOFTMAX: self.softmax_weight = tf.transpose(self.trg_embedding) else: self.softmax_weight = tf.get_variable("weight", [HIDDEN_SIZE, TRG_VOCAB_SIZE]) self.softmax_bias = tf.get_variable("softmax_bias", [TRG_VOCAB_SIZE]) def inference(self, src_input): # 雖然輸入只有一個句子,但因為dynamic_rnn要求輸入是batch的形式,因此這里將輸入句子整理為大小為1的batch src_size = tf.convert_to_tensor([len(src_input)], dtype=tf.int32) src_input = tf.convert_to_tensor([src_input], dtype=tf.int32) src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input) # 構造編碼器時,使用bidirectional_dynamic_rnn構造雙向循環網絡。這一步與訓練時相同 with tf.variable_scope("encoder"): # 構造編碼器時,使用bidirectional_dynamic_rnn構造雙向循環網絡。 # 雙向循環網絡的頂層輸出enc_outputs是一個包含兩個張量的tuple,每個張量的 # 維度都是[batch_size, max_time, HIDDEN_SIZE],代表兩個LSTM在每一步的輸出。 enc_outputs, enc_state = tf.nn.bidirectional_dynamic_rnn(self.enc_cell_fw, self.enc_cell_bw, src_emb, src_size, dtype=tf.float32) # 將兩個LSTM的輸出拼接為一個張量。 enc_outputs = tf.concat([enc_outputs[0], enc_outputs[1]], -1) with tf.variable_scope("decoder"): # 選擇注意力權重的計算模型。BahdanauAttention是使用一個隱藏層的前饋網絡。 # memory_sequence_length是一個維度為[batch_size]的張量,代表batch # 中每個句子的長度,Attention需要根據這個信息把填充位置的注意力權重設置為0。 attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(HIDDEN_SIZE, enc_outputs, memory_sequence_length=src_size) # 將解碼器的循環神經網絡self.dec_cell和注意力一起封裝成更高層的循環神經網絡。 attention_cell = tf.contrib.seq2seq.AttentionWrapper(self.dec_cell, attention_mechanism, attention_layer_size=HIDDEN_SIZE) # 設置解碼的最大步數。這是為了避免在極端情況出現無限循環的問題。 MAX_DEC_LEN = 100 with tf.variable_scope("decoder/rnn/attention_wrapper"): # 使用一個變長的TensorArray來存儲生成的句子 init_array = tf.TensorArray(dtype=tf.int32, size=0, dynamic_size=True, clear_after_read=False) # 填入第一個單詞<sos>作為解碼器的輸入 init_array = init_array.write(0, SOS_ID) # 構建初始的循環狀態。循環狀態包含循環神經網絡的隱藏狀態,保存生成句子的TensorArray,以及記錄解碼步數的一個整數step init_loop_var = (attention_cell.zero_state(batch_size=1, dtype=tf.float32), init_array, 0) # tf.while_loop的循環條件 # 循環直到解碼器輸出<eos>,或者達到最大步數為止。 def continue_loop_condition(state, trg_ids, step): return tf.reduce_all(tf.logical_and(tf.not_equal(trg_ids.read(step), EOS_ID), tf.less(step, MAX_DEC_LEN-1))) def loop_body(state, trg_ids, step): # 讀取最后一步輸出的單詞,並讀取其詞向量 trg_input = [trg_ids.read(step)] trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input) # 這里不使用dynamic_rnn,而是直接調用dec_cell向前計算一步。 dec_outputs, next_state = attention_cell.call(state=state, inputs=trg_emb) # 計算每個可能的輸出單詞對應的logit,並選取logit值最大的單詞作為這一步的輸出。 output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE]) logits = (tf.matmul(output, self.softmax_weight) + self.softmax_bias) next_id = tf.argmax(logits, axis=1, output_type=tf.int32) # 將這一步輸出的單詞寫入循環狀態的trg_ids中 trg_ids = trg_ids.write(step+1, next_id[0]) return next_state, trg_ids, step+1 # 執行tf.while_loop,返回最終狀態 state, trg_ids, step = tf.while_loop(continue_loop_condition, loop_body, init_loop_var) return trg_ids.stack() def main(): # 定義訓練用的循環神經網絡模型 with tf.variable_scope("nmt_model", reuse=None): model = NMTModel() # 定義一個測試的例子 test_sentence = "This is a test ." print(test_sentence) # 根據英文詞匯表,將測試句子轉為單詞ID。結尾加上<eos>的編號 test_sentence = test_sentence + " <eos>" with codecs.open(SRC_VOCAB, 'r', 'utf-8') as vocab: src_vocab = [w.strip() for w in vocab.readlines()] # 運用dict,將單詞和id對應起來組成字典,用於后面的轉換 src_id_dict = dict((src_vocab[x], x) for x in range(SRC_VOCAB_SIZE)) test_en_ids = [(src_id_dict[en_text] if en_text in src_id_dict else src_id_dict['<unk>']) for en_text in test_sentence.split()] print(test_en_ids) # 建立解碼所需的計算圖 output_op = model.inference(test_en_ids) sess = tf.Session() saver = tf.train.Saver() saver.restore(sess, CHECKPOINT_PATH) # 讀取翻譯結果 output_ids = sess.run(output_op) print(output_ids) # 根據中文詞匯表,將翻譯結果轉換為中文文字。 with codecs.open(TRG_VOCAB, "r", "utf-8") as f_vocab: trg_vocab = [w.strip() for w in f_vocab.readlines()] output_text = ''.join([trg_vocab[x] for x in output_ids[1:-1]]) # 輸出翻譯結果 print(output_text) sess.close() if __name__ == "__main__": main()

OK!來吧,運行吧!!!

???我絕望了。。。又跟上面的一樣???我酸了。。。

又是變量名的原因??

行吧,這次我就不用上面的解決辦法了,不改模型了,我要修改解碼代碼中的變量名。。。

1. 首先去掉主函數的模型名,然后下面的代碼縮進。這樣所有的變量名前面都沒有nmt_model了。

2. 然后修改模型類里面初始化函數里面的變量名,給他們加上nmt_model

3. 這樣就滿足了模型中,有的變量名有nmt_model前綴,有的沒有的問題了,而不是直接修改模型里面的。。。(修改模型后meta文件變得特別大,不知道為什么。總之就是絕望。。。)運行!!!

成功!歐耶!完事兒!

這次把句號翻譯出來了,有進步!

6. 總結

在Attention機制這部分,原書里面就給了訓練模型需要修改的部分,也並沒有拿出來說明,哪里修改了,怎么修改的。最可怕的是原書中沒有給出跑出模型后解碼的部分,更別說學習書里面除了語言模型之外的那些模型了,我簡直就是雙眼一抹黑,口吐芬芳。輕嘆一口氣,學習果然還是得靠自己下功夫啊。十分感謝這篇文章給出的完整代碼,給了我從頭到尾體驗Seq2Seq的機會。希望大家在學習過程中也依然保有耐心,永不言棄。

如果有讀者,真的從頭到尾,看完我的文章,真的是非常不容易,祝賀你,你不僅努力勤奮,還很堅強,脾氣一定很好吧哈哈哈。如果有任何問題,歡迎跟我一起探討。

最后附上整個代碼的github地址,歡迎大家Watch,Star,Fork~

https://github.com/TinyHandsome/BookStudy/tree/master/book2/Seq2SeqLearning

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM