《Neural Machine Translation by Jointly Learning To Align and Translate》閱讀心得分享
-
- 論文原文連結
- 論文導讀
- 論文abstract和introduction
- 背景-傳統RNN Encoder-Decoder模型
- 本文提出的模型-加入了attenton機制的模型
- 代碼複現、詳細講解及我的Github位址
-
- 語料預處理
- 構模組化型以及訓練模型
- 模型的預測(也就是inference階段)
- 模型訓練階段的截圖和預測階段的截圖
論文原文連結
《Neural Machine Translation by Jointly Learning To Align and Translate》
論文導讀
機器翻譯本質上是源語言和目智語言的一種等價資訊轉換,也成為自動翻譯,是将一種語言(源語言)轉化為另一種語言的過程。從曆史上來看,機器翻譯的發展曆史經過了三個階段。在1980年代出現的基于規則的機器翻譯、1990年代出現的基于統計的翻譯和2013年左右出現的基于神經網絡的翻譯。基于統計的翻譯和基于神經網絡的翻譯又被稱作為基于資料驅動的機器翻譯。
最古老的機器翻譯方法就是基于規則的機器翻譯,在翻譯的時候,首先根據源語言的詞的詞性,然後将詞語翻譯成目智語言,接着,再使用文法規則,對翻譯後的句子進行調整。根據翻譯方式的不同,有基于詞的方式、基于結構轉換的翻譯和基于中間語的翻譯。舉個例子來說,”We do achieve the target“就有可能被翻譯為“我們 做 實作 目标”,而實際上“do”隻是強調的作用。有限的文法無法覆寫各種各樣的語言現象,并且這種實作方法非常依賴于專家的知識。
第二類方法就是基于統計的機器翻譯。這種方法通過對語料進行統計分析,構模組化型。它的本質是如何找到源語言被翻譯成目智語言後,出現最大機率的那句話,也就是對機率進行模組化。舉個例子來說,“我 已經 看 了 這篇 部落格”這句話,就會被翻譯成“I have read this blog”。從直覺上來說,在翻譯的時候,我們可以枚舉所有的英文句子來建立機率分布,但這種方法顯然是不合适的,因為句子有“無限”多個。在這個時候,我們就引入了隐變量。主流的方法就是2002年提出的“隐變量對數線性模型”,它的方法是設計特征函數,也就是在隐式語言結構上設計特征。
第三類就是基于神經網絡的機器翻譯。它通過學習大量的成對的語料,讓模型可以自己學習語言的特征,找到輸入和輸出之間的映射。它的核心理念就是建立一種端到端模型(end-to-end),讓算法自動找到映射的關系,是以這類方法也被稱為表示學習。主流的方法有2014年Kyunghyun等人提出的encoder-decoder模型和Sutskever等人提出的sequence-to-sequence模型。和第二類基于統計的機器翻譯方法不一樣的地方在于,它并沒有引入隐變量,它利用馬爾科夫性,将句子生成的機率分解成了各個條件機率的乘積。同時,不像基于統計的機器翻譯方法那樣是離散形式的模組化,它可以對模型進行連續的模組化。這也是深度學習帶來的革命性的變化。
論文abstract和introduction
神經機器翻譯是最近新興的一種機器翻譯的方法。大部分的神經機器翻譯都是基于enoder-decoder架構的。并且它們都會将源語言的句子壓縮成一個固定的向量,然後傳遞給decoder。這存在一些潛在的問題,比如,從我們直覺上來感受,如果句子長,那麼強行的将句子中的有效資訊全部壓縮在一個固定的向量中的話,資訊肯定會丢失。
于是,該篇論文的作者提出了一種新的方法,這個方法也是基于encoder-decoder的。與之前的encoder-decoder的模型不同之處在于,每次在翻譯一個單詞的時候,模型會自動的搜尋該單詞與源語言哪些單詞有關聯,并将這種關聯的強度進行數字化表示(在模型中就是權重)。并且,實驗得出,這種方法可以解決長句子翻譯不準的問題(相較于傳統的encoder-decoder模型)。
背景-傳統RNN Encoder-Decoder模型
傳統的RNN Encoder-Decoder模型在訓練階段的時候,會使模型去最大化源語言翻譯成目智語言的條件機率。當模型訓練好後,當待翻譯的源語言句子放入到模型中的時候,模型會自動的計算最大的目智語言句子的機率,并且将這個句子當做是翻譯後的句子。下面具體講一下傳統的RNN Encoder-Decoder模型,直接上圖,
如上圖所示,假設我們有一句待翻譯的話,“我/已經/閱讀/了/這篇/部落格",我們要将它翻譯成英文。圖中”C“的左側是encoder,右側是decoder,”C"是待翻譯語句的語義資訊。
首先,“我/已經/閱讀/了/這篇/部落格"這句話會經過encoder,encoder會将這句話進行編碼,encoder用到的模型是RNN,RNN的原理可以見我的部落格,RNN會通過一個時刻,一個時刻地對“我/已經/閱讀/了/這篇/部落格"這句話進行編碼,當編碼結束後,我們會将最後一個時刻的RNN的隐層的輸出當做“我/已經/閱讀/了/這篇/部落格"這句話的語義壓縮,在圖中也就是C。
接着,解碼器每次在産生一個翻譯後的英文單詞的時候,它都會利用這個編碼器的語義壓縮C。那具體是如何做到的呢?解碼器的模型也是RNN,首先,在時刻為0的時候,RNN會利用語義壓縮C,并且這個RNN同時還會接收輸入為“”的token來作為這個時刻的輸入,接着這個時刻的輸出端就會産生第一個單詞“I”(這裡利用了softmax,輸出層是一個詞典大小次元的向量,哪個次元的值最大,就取那個次元所對應的單詞作為所預測的單詞),大家可以想的到,在訓練階段,解碼器不可能可以立馬産生“I”,而是産生其他的單詞,如“tell”等等單詞。是以,訓練階段的目的,就是讓編碼器和解碼器的參數,随着訓練次數的增加,往“正确”的方向改變,以至于讓其産生單詞“I”。換句話說,在時刻為0的時候,我們希望解碼器産生單詞“I”。接着,無論在時刻為0的時候産生什麼單詞,它都會被當做時刻為1(也就是s1)的輸入。是以,如上圖,在時刻為1的時候,共有3個輸入,一個是C,一個是時刻為0時候産生的隐層結果,最後一個時刻為0的時候的輸出。直到這句話産生結束(結束的标志是解碼器産生了/s等特殊标志,這個可以在訓練的時候指定)。至此,decoder的任務也就完成了。
本文提出的模型-加入了attenton機制的模型
本文提出的模型在文章中叫做RNNsearch模型,流程圖如下圖所示,
首先,右上角的圖檔是encoder,這個部分和RNNenc模型是一樣的,接着,在decoder部分,就會有巨大的差别。我們以decoder時刻為0時舉例子,在時刻為0時,decoder的BiLSTM會接受三個地方的輸入,第1個輸入的來源是時刻為0時的初始狀态s0,這個狀态是随機初始化的(無論在訓練階段還是在預測階段,都是随機的,這個在代碼裡可以看到)。第2個輸入是來源于""這個token的embedding後的向量。第3個輸入就有些複雜了,并且,這個第3個輸入也就是這篇論文提出的核心創新。第3個輸入計算方法如下:
首先,将随機初始化的s0拿來,和encoder所輸出的h1~h6,各自做一次餘弦相似度的計算(這個計算方式可以自己定義),各自會得到一個e1 ~ e6的數值,然後将6個數值做一次softmax操作,會得到α1 ~ α6,那麼我們可以知道,α1+α2+α3+α4+α5+α6=1,是以,我們可以将α1,α2,α3,α4,α5,α6中的每一個,都分别當作是s0和h1 ~ h6的相似度。接着,α1,α2,α3,α4,α5,α6和h1,h2,h3,h4,h5,h6這6個向量分别做一次元素乘積,所得的6個向量再做一次元素的相加,得到最終的向量。接着,就把這個向量當作時刻0時,BiLSTM的第3個輸入。這裡要注意,代碼中的BahdanauAttention機制,實際上是做了一次卷積操作,這個操作等價于上述所講的attention原理,具體大家可以去百度下BahdanauAttention的原理。
就這樣,在時刻為0時,BiLSTM就産生了一個輸出。那麼此時,時刻就變為了1,接下去的過程就和時刻為0的過程是一樣的。大家可以自己仔細了解了解。
代碼複現、詳細講解及我的Github位址
完整代碼位址:https://github.com/haitaifantuan/nlp_paper_understand
語料預處理
首先,我們要進行的是語料預處理部分。我們可以從我的github位址下載下傳代碼以及從騰訊雲盤下載下傳資料集,TODO,解壓出來後,需要運作的是“data_preprocessing.py”這個子產品,詳細代碼如下,代碼主要包含tokenize語料部分、建構token dictionary部分以及将語料從token轉換為id部分。tokenize語料部分就是代碼中對應的tokenize_corpus(self)這個方法。建構token dictionary部分就是build_token_dictionary(self)這個方法。将語料從token轉換為id部分就是convert_data_to_id_pad_eos(self)這個方法。
#coding=utf-8
'''
Author:Haitaifantuan
'''
import os
import nltk
import pickle
import train_args
import collections
class Data_preprocess(object):
def __init__(self):
pass
def tokenize_corpus(self):
'''
該函數的作用是:将英文語料和中文語料進行tokenize,然後儲存到本地。
'''
# 将英文語料tokenize,儲存下來。
if not os.path.exists(train_args.raw_train_english_after_tokenization_data_path.replace('train.raw.en.after_tokenization.txt', '')):
os.mkdir(train_args.raw_train_english_after_tokenization_data_path.replace('train.raw.en.after_tokenization.txt', ''))
fwrite = open(train_args.raw_train_english_after_tokenization_data_path, 'w', encoding='utf-8')
with open(train_args.raw_train_english_data_path, 'r', encoding='utf-8') as file:
for line in file:
line = line.strip()
line = nltk.word_tokenize(line)
# 将tokenization後的句子寫入檔案
fwrite.write(' '.join(line) + '\n')
fwrite.close()
# 将中文語料tokenize,儲存下來。
if not os.path.exists(train_args.raw_train_chinese_after_tokenization_data_path.replace('train.raw.zh.after_tokenization.txt', '')):
os.mkdir(train_args.raw_train_chinese_after_tokenization_data_path.replace('train.raw.zh.after_tokenization.txt', ''))
fwrite = open(train_args.raw_train_chinese_after_tokenization_data_path, 'w', encoding='utf-8')
with open(train_args.raw_train_chinese_data_path, 'r', encoding='utf-8') as file:
for line in file:
line = line.strip()
line = list(line)
# 将tokenization後的句子寫入檔案
fwrite.write(' '.join(line) + '\n')
fwrite.close()
print('語料tokenization完成')
def build_token_dictionary(self):
'''
該函數的作用是:根據英文語料和中文語料,建立各自的,以字為機關的token dictionary。
'''
# 生成英文的token_dictionary
english_token_id_dictionary = {}
# 我們定義unk的id是0,unk的意思是,
# 當句子中碰到token dictionary裡面沒有的token的時候,就轉換為這個
english_token_id_dictionary['<unk>'] = 0
english_token_id_dictionary['<sos>'] = 1 # 我們定義sos的id是1
english_token_id_dictionary['<eos>'] = 2 # 我們定義eos的id是1
en_counter = collections.Counter(
) # 建立一個英文token的計數器,專門拿來計算每個token出現了多少次
with open(train_args.raw_train_english_after_tokenization_data_path, 'r', encoding='utf-8') as file:
for line in file:
line = line.strip().split(' ')
for token in line:
en_counter[token] += 1
most_common_en_token_list = en_counter.most_common(train_args.Source_vocab_size - 3) # 找出最常見的Source_vocab_size-3的token
for token_tuple in most_common_en_token_list:
english_token_id_dictionary[token_tuple[0]] = len(english_token_id_dictionary)
# 儲存english_token_id_dictionary
if not os.path.exists(train_args.english_token_id_dictionary_pickle_path.replace('english_token_id_dictionary.pickle', '')):
os.mkdir(train_args.english_token_id_dictionary_pickle_path.replace('english_token_id_dictionary.pickle', ''))
with open(train_args.english_token_id_dictionary_pickle_path, 'wb') as file:
pickle.dump(english_token_id_dictionary, file)
# 生成中文的token_dictionary 以及把 tokenization後的結果儲存下來
chinese_token_id_dictionary = {}
# 我們定義unk的id是0,unk的意思是,
# 當句子中碰到token dictionary裡面沒有的token的時候,就轉換為這個
chinese_token_id_dictionary['<unk>'] = 0
chinese_token_id_dictionary['<sos>'] = 1 # 我們定義sos的id是1
chinese_token_id_dictionary['<eos>'] = 2 # 我們定義eos的id是1
# 建立一個中文token的計數器,專門拿來計算每個token出現了多少次
zh_counter = collections.Counter()
with open(train_args.raw_train_chinese_after_tokenization_data_path, 'r', encoding='utf-8') as file:
for line in file:
line = line.strip().split(' ')
for token in line:
zh_counter[token] += 1
most_common_zh_token_list = zh_counter.most_common(train_args.Target_vocab_size - 3) # 找出最常見的Target_vocab_size-3的token
for token_tuple in most_common_zh_token_list:
chinese_token_id_dictionary[token_tuple[0]] = len(chinese_token_id_dictionary)
# 儲存token_dictionary
if not os.path.exists(train_args.chinese_token_id_dictionary_pickle_path.replace('chinese_token_id_dictionary.pickle', '')):
os.mkdir(train_args.chinese_token_id_dictionary_pickle_path.replace('chinese_token_id_dictionary.pickle', ''))
with open(train_args.chinese_token_id_dictionary_pickle_path, 'wb') as file:
pickle.dump(chinese_token_id_dictionary, file)
print('英文token_dictionary和中文token_dictionary建立完畢')
def convert_data_to_id_pad_eos(self):
'''
該函數的作用是:
将英文語料轉換成id形式,并在末尾添加[EOS]
将中文語料轉換成id形式,并在句子開頭添加[SOS]
'''
# 讀取英文的token_dictionary
with open(train_args.english_token_id_dictionary_pickle_path, 'rb') as file:
english_token_id_dictionary = pickle.load(file)
if not os.path.exists(train_args.train_en_converted_to_id_path.replace('train.en.converted_to_id.txt', '')):
os.mkdir(train_args.train_en_converted_to_id_path.replace('train.en.converted_to_id.txt', ''))
fwrite = open(train_args.train_en_converted_to_id_path, 'w', encoding='utf-8')
# 讀取tokenization後的英文語料,并将其轉換為id形式。
with open(train_args.raw_train_english_after_tokenization_data_path, 'r', encoding='utf-8') as file:
for line in file:
line_converted_to_id = []
line = line.strip().split(' ')
for token in line:
# 将token轉換成id
token_id = english_token_id_dictionary.get(
token, english_token_id_dictionary['<unk>'])
line_converted_to_id.append(str(token_id))
# 在英文語料最後加上EOS
line_converted_to_id.append(
str(english_token_id_dictionary['<eos>']))
# 寫入本地檔案
fwrite.write(' '.join(line_converted_to_id) + '\n')
fwrite.close()
# 讀取中文的token_dictionary
with open(train_args.chinese_token_id_dictionary_pickle_path, 'rb') as file:
chinese_token_id_dictionary = pickle.load(file)
if not os.path.exists(train_args.train_zh_converted_to_id_path.replace('train.zh.converted_to_id.txt', '')):
os.mkdir(train_args.train_zh_converted_to_id_path.replace('train.zh.converted_to_id.txt', ''))
fwrite = open(train_args.train_zh_converted_to_id_path, 'w', encoding='utf-8')
# 讀取tokenization後的中語料,并将其轉換為id形式。
with open(train_args.raw_train_chinese_after_tokenization_data_path, 'r', encoding='utf-8') as file:
for line in file:
line_converted_to_id = []
line = line.strip().split(' ')
for token in line:
# 将token轉換成id
token_id = chinese_token_id_dictionary.get(
token, english_token_id_dictionary['<unk>'])
line_converted_to_id.append(str(token_id))
# 因為這個中文語料是當做目标詞的,是以也需要在中文語料最後面加上EOS
# decoder的輸入的最開始的BOS,會在train.py裡面添加。
line_converted_to_id.append(
str(chinese_token_id_dictionary['<eos>']))
# 寫入本地檔案
fwrite.write(' '.join(line_converted_to_id) + '\n')
fwrite.close()
print('英文語料轉換為id并且添加[EOS]标緻完畢')
print('中文語料轉換為id并且添加[EOS]标緻完畢')
# 建立預處理data對象
data_obj = Data_preprocess()
# 将英文語料和中文語料進行tokenization
data_obj.tokenize_corpus()
# 建立英文語料和中文語料的token_dictionary
data_obj.build_token_dictionary()
# 根據token_dictionary将英文語料和中文語料轉換為id形式
# 并且在英文語料的最後添加[EOS]标緻,在中文語料的最開始添加[SOS]标緻
# 并将轉化後的語料儲存下來
data_obj.convert_data_to_id_pad_eos()
構模組化型以及訓練模型
語料預處理完成後,我們要做的是構模組化型,然後訓練模型。這一部分代碼在train.py子產品中,代碼如下,可以看出,要想訓練模型,分為3個步驟。
第1個步驟是再次處理下語料,并且搞一個疊代器,每次可以疊代出1個batch_size個樣本以供模型訓練,這一部分的代碼就是class data_batch_generation(object)這個裡面。
然後進行第2個步驟,當data_batch_generation()的疊代器建構好後,我們便可以構模組化型了,這一部分代碼就是class Model(object)這個類裡面。
第3個步驟,就是開啟一個sesstion,然後開始訓練模型的部分。這一部分代碼存在于建立session_config這個變量開始到最後。
#coding=utf-8
'''
Author:Haitaifantuan
'''
import tensorflow as tf
import train_args
import os
# 如果有GPU的同學,可以把這個打開,或者自己研究下怎麼打開。
#os.environ["CUDA_VISIBLE_DEVICES"] = "0"
# 首先判斷模型儲存的路徑存不存在,不存在就建立
if not os.path.exists('./saved_things/'):
os.mkdir('./saved_things/')
if not os.path.exists('./saved_things/doesnt_finish_training_model/'):
os.mkdir('./saved_things/doesnt_finish_training_model/')
if not os.path.exists('./saved_things/finish_training_model/'):
os.mkdir('./saved_things/finish_training_model/')
mt_graph = tf.Graph() # 建立machine translation 專用的graph
data_graph = tf.Graph() # 建立資料專用的graph
class data_batch_generation(object):
def __init__(self):
with data_graph.as_default(): # 定義在這個圖下面建立模型
# 通過tf.data.TextLineDataset()來讀取訓練集資料
self.src_data = tf.data.TextLineDataset(train_args.train_en_converted_to_id_path)
self.trg_data = tf.data.TextLineDataset(train_args.train_zh_converted_to_id_path)
# 因為剛讀進來是string格式,這裡将string改為int,并形成tensor形式。
self.src_data = self.src_data.map(lambda line: tf.string_split([line], delimiter=' ').values)
self.src_data = self.src_data.map(lambda line: tf.string_to_number(line, tf.int32))
self.trg_data = self.trg_data.map(lambda line: tf.string_split([line], delimiter=' ').values)
self.trg_data = self.trg_data.map(lambda line: tf.string_to_number(line, tf.int32))
# 為self.src_data添加一下每個句子的長度
self.src_data = self.src_data.map(lambda x: (x, tf.size(x)))
# 為self.trg_data添加一下decoder的輸入。形式為(dec_input, trg_label, trg_length)
# tf.size(x)後面計算loss的時候拿來mask用的以及
# 使用tf.nn.bidirectional_dynamic_rnn()這個函數的時候使用的。
self.trg_data = self.trg_data.map(lambda x: (tf.concat([[1], x[:-1]], axis=0), x, tf.size(x)))
# 将self.src_data和self.trg_data zip起來,友善後面過濾資料。
self.data = tf.data.Dataset.zip((self.src_data, self.trg_data))
# 将句子長度小于1和大于train_args.train_max_sent_len的都去掉。
def filter_according_to_length(src_data, trg_data):
((enc_input, enc_input_size), (dec_input, dec_target_label, dec_target_label_size)) = (src_data, trg_data)
enc_input_flag = tf.logical_and(tf.greater(enc_input_size, 1), tf.less_equal(enc_input_size, train_args.train_max_sent_len))
# decoder的input的長度和decoder的label是一樣的,是以這裡可以這樣用。
dec_input_flag = tf.logical_and(tf.greater(dec_target_label_size, 1), tf.less_equal(dec_target_label_size, train_args.train_max_sent_len))
flag = tf.logical_and(enc_input_flag, dec_input_flag)
return flag
self.data = self.data.filter(filter_according_to_length)
# 由于句子長短不同,我們這裡将句子的長度pad成固定的,pad成目前batch裡面最長的那個。
# 我們使用0來pad,也就是['<unk>']标志
# 後續計算loss的時候,會根據trg_label的長度來mask掉pad的部分。
# 設定為None的時候,就代表把這個句子pad到目前batch的樣本下最長的句子的長度。
# enc_input_size本來就是單個數字,是以不用pad。
self.padded_data = self.data.padded_batch(
batch_size=train_args.train_batch_size,
padded_shapes=((tf.TensorShape([None]), tf.TensorShape([])), (tf.TensorShape([None]), tf.TensorShape([None]), tf.TensorShape([]))))
self.padded_data = self.padded_data.shuffle(10000)
# 建立一個iterator
self.padded_data_iterator = self.padded_data.make_initializable_iterator(
)
self.line = self.padded_data_iterator.get_next()
def iterator_initialization(self, sess):
# 初始化iterator
sess.run(self.padded_data_iterator.initializer)
def next_batch(self, sess):
# 擷取一個batch_size的資料
((enc_inp, enc_size), (dec_inp, dec_trg, dec_trg_size)) = sess.run(self.line)
return ((enc_inp, enc_size), (dec_inp, dec_trg, dec_trg_size))
class Model(object):
def __init__(self):
with mt_graph.as_default():
# 建立placeholder
with tf.variable_scope("ipt_placeholder"):
# None因為batch_size在變化,第2個None是因為句長不确定
self.enc_inp = tf.placeholder(tf.int32, shape=[train_args.train_batch_size, None])
# None是代表batch_size
self.enc_inp_size = tf.placeholder(tf.int32, shape=[train_args.train_batch_size])
# None是因為句長不确定
self.dec_inp = tf.placeholder(tf.int32, shape=[train_args.train_batch_size, None])
# None是因為句長不确定
self.dec_label = tf.placeholder(tf.int32, shape=[train_args.train_batch_size, None])
# None是代表batch_size
self.dec_label_size = tf.placeholder(tf.int32, shape=[train_args.train_batch_size])
# 建立源語言的token的embedding和目智語言的token的embedding
with tf.variable_scope("token_embedding"):
# 源語言的token的embedding
self.src_embedding = tf.Variable(initial_value=tf.truncated_normal(shape=[train_args.Source_vocab_size, train_args.RNN_hidden_size ], dtype=tf.float32), trainable=True)
# 目智語言的token的embedding
self.trg_embedding = tf.Variable(initial_value=tf.truncated_normal(shape=[train_args.Target_vocab_size, train_args.RNN_hidden_size], dtype=tf.float32), trainable=True)
# 全連接配接層的參數
if train_args.Share_softmax_embedding:
self.full_connect_weights = tf.transpose(self.trg_embedding)
else:
self.full_connect_weights = tf.Variable(initial_value=tf.truncated_normal(shape=[train_args.RNN_hidden_size, train_args.Target_vocab_size], dtype=tf.float32), trainable=True)
self.full_connect_biases = tf.Variable(initial_value=tf.truncated_normal(shape=[train_args.Target_vocab_size], dtype=tf.float32))
with tf.variable_scope("encoder"):
# 根據輸入,得到輸入的token的向量
self.src_emb_inp = tf.nn.embedding_lookup(self.src_embedding, self.enc_inp)
# 建構編碼器中的雙向LSTM
self.enc_forward_lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=train_args.RNN_hidden_size)
self.enc_backward_lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=train_args.RNN_hidden_size)
# 使用bidirectional_dynamic_rnn構造雙向RNN網絡。
# 把輸入的token的向量放入到encoder裡面去,得到輸出。
# enc_top_outputs包含了前向LSTM和反向LSTM的輸出。enc_top_states也一樣。
# 我們把前向的LSTM頂層的outputs和反向的LSTM頂層的outputs concat一下,
# 作為attention的輸入。
# enc_top_outputs這個tuple,每一個元素的shape都是[batch_size, time_step, hidden_size]
self.enc_top_outputs, self.enc_top_states = tf.nn.bidirectional_dynamic_rnn(
cell_fw=self.enc_forward_lstm_cell,
cell_bw=self.enc_backward_lstm_cell,
inputs=self.src_emb_inp,
sequence_length=self.enc_inp_size,
dtype=tf.float32)
self.enc_outpus = tf.concat([self.enc_top_outputs[0], self.enc_top_outputs[1]], -1)
with tf.variable_scope("decoder"):
# 建立多層decoder。
self.dec_lstm_cell = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.BasicLSTMCell(num_units=train_args.RNN_hidden_size) for _ in range(train_args.num_decoder_layers)])
# 選擇BahdanauAttention作為注意力機制。它是使用一層隐藏層的前饋神經網絡。
attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(
num_units=train_args.RNN_hidden_size,
memory=self.enc_outpus,
memory_sequence_length=self.enc_inp_size)
# 将elf.dec_lstm_cell和attention_mechanism封裝成更進階的API
after_attention_cell = tf.contrib.seq2seq.AttentionWrapper(
self.dec_lstm_cell,
attention_mechanism,
attention_layer_size=train_args.RNN_hidden_size)
# 目标token的embedding
self.trg_emb_inp = tf.nn.embedding_lookup(self.trg_embedding, self.dec_inp)
self.dec_top_outpus, self.dec_states = tf.nn.dynamic_rnn(
after_attention_cell,
self.trg_emb_inp,
self.dec_label_size,
dtype=tf.float32)
# 将輸出經過一個全連接配接層
# shape=[None, 1024]
self.outpus = tf.reshape(self.dec_top_outpus, [-1, train_args.RNN_hidden_size])
# shape=[None, 4003]
self.logits = tf.matmul(self.outpus, self.full_connect_weights) + self.full_connect_biases
# tf.nn.sparse_softmax_cross_entropy_with_logits可以不需要将label變成one-hot形式,
# 減少了步驟,大家後續可以自己嘗試下。
self.dec_label_reshaped = tf.reshape(self.dec_label, [-1])
# 将self.dec_label_reshaped轉換成one-hot的形式
self.dec_label_after_one_hot = tf.one_hot(self.dec_label_reshaped, train_args.Target_vocab_size)
# 計算交叉熵損失函數
self.loss = tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.dec_label_after_one_hot, logits=self.logits)
# 由于我們在構造資料的時候,将沒到長度的地方用[UNK]補全了,
# 是以這些地方的loss不能參與計算,我們要将它們mask掉。
# 這裡我們設定dtype=tf.float32,意思是讓沒有mask掉的地方輸出為1,
# 被mask掉的地方輸出為0,友善我們後面做乘積。
# 如果不設定dtype=tf.float32的話,預設輸出是True或者False
self.mask_result = tf.sequence_mask(lengths=self.dec_label_size,
maxlen=tf.shape(
self.dec_inp)[1],
dtype=tf.float32)
self.mask_result = tf.reshape(self.mask_result, [-1])
self.loss = self.loss * self.mask_result
self.loss = tf.reduce_sum(self.loss)
# 計算平均損失
self.per_token_loss = self.loss / tf.reduce_sum(self.mask_result)
# 定義train操作
self.trainable_variables = tf.trainable_variables()
self.optimizer = tf.train.GradientDescentOptimizer(
learning_rate=train_args.learning_rate) # 定義optimizer
# 計算梯度
self.grads = tf.gradients(self.loss / tf.to_float(train_args.train_batch_size), self.trainable_variables)
# 設定一個最大的梯度值,防止梯度爆炸。
self.grads, _ = tf.clip_by_global_norm(self.grads, 7)
# apply 梯度到每個Variable上去。
self.train_op = self.optimizer.apply_gradients(
zip(self.grads, self.trainable_variables))
# 建構global_step,後面儲存模型的時候使用
self.global_step = tf.Variable(initial_value=0)
self.global_step_op = tf.add(self.global_step, 1)
self.global_step_assign = tf.assign(self.global_step, self.global_step_op)
self.global_step_per_epoch = tf.Variable(initial_value=1000000)
def train(self, sess, data):
# 訓練的操作
((enc_inp, enc_size), (dec_inp, dec_trg, dec_trg_size)) = data
feed = {
self.enc_inp: enc_inp,
self.enc_inp_size: enc_size,
self.dec_inp: dec_inp,
self.dec_label: dec_trg,
self.dec_label_size: dec_trg_size
}
_, per_token_loss, current_global_step = sess.run(
[self.train_op, self.per_token_loss, self.global_step_assign],
feed_dict=feed)
return per_token_loss, current_global_step
data_batch_generation_obj = data_batch_generation()
sess_data = tf.Session(graph=data_graph) # 建立一個圖專用的session
nm_model = Model()
session_config = tf.ConfigProto(allow_soft_placement=True) # sesstion的config
session_config.gpu_options.allow_growth = True
# 打開Sesstion,開始訓練模型
with tf.Session(graph=mt_graph) as sess: # 建立一個模型的圖的sesstion
saver = tf.train.Saver(max_to_keep=5) # 建構saver
data_batch_generation_obj.iterator_initialization(sess_data)
sess.run(tf.global_variables_initializer())
current_epoch = 0
# 從未訓練完的模型加載,繼續斷點訓練。
if os.path.exists(train_args.doesnt_finish_model_saved_path_cheackpoint):
restore_path = tf.train.latest_checkpoint(train_args.doesnt_finish_model_saved_path.replace('/model', ''))
saver.restore(sess, restore_path)
current_epoch = sess.run(nm_model.global_step) // sess.run(nm_model.global_step_per_epoch)
print('從未訓練完的模型加載-----未訓練完的模型已訓練完第{}個epoch-----共需要訓練{}個epoch'.format(
current_epoch, train_args.max_global_epochs))
global_step_per_epoch_count = 0
while sess.run(nm_model.global_step) < train_args.max_global_epochs * sess.run(nm_model.global_step_per_epoch):
try:
# 這裡要傳入data的sesstion
data = data_batch_generation_obj.next_batch(sess_data)
if data[0][0].shape[0] == train_args.train_batch_size:
per_token_loss, current_global_step = nm_model.train(sess, data)
print("目前為第{}個epoch-----第{}個global_step-----每個token的loss是-----{}".format(current_epoch, current_global_step, per_token_loss))
global_step_per_epoch_count += 1
except tf.errors.OutOfRangeError as e:
current_epoch += 1
with mt_graph.as_default():
_ = sess.run(tf.assign(nm_model.global_step_per_epoch, global_step_per_epoch_count))
global_step_per_epoch_count = 0
# 如果報tf.errors.OutOfRangeError這個錯,說明資料已經被周遊完了,
# 也就是一個epoch結束了。我們重新initialize資料集一下,進行下一個epoch。
data_batch_generation_obj.iterator_initialization(sess_data) # 這裡要傳入data的sesstion
# 暫時儲存下未訓練完的模型
if current_epoch % train_args.num_epoch_per_save == 0:
saver.save(sess=sess,
save_path=train_args.doesnt_finish_model_saved_path,
global_step=sess.run(nm_model.global_step))
# 跳出while循環說明整個global_epoch訓練完畢,那就儲存最終訓練好的模型。
saver.save(sess=sess,
save_path=train_args.finish_model_saved_path,
global_step=sess.run(nm_model.global_step))
模型的預測(也就是inference階段)
模型的預測分為3個階段。
第1個階段是構模組化型,這一部分代碼在class Model(object)這個類裡面,這裡要注意,預測階段的class Model(object)和訓練階段的class Model(object)是不一樣的。大家注意觀察。
第2個階段就是從儲存下來的模型參數中恢複模型參數,其實就是把儲存下來的變量及變量的值,在模型中恢複。比如源語言的embedding table,目智語言的embedding table等等。
第3個階段就是預測階段。模型加載好後,就可以開始預測了。
代碼如下,
#coding=utf-8
'''
Author:Haitaifantuan
'''
import tensorflow as tf
import train_args
import pickle
import nltk
mt_graph = tf.Graph()
class Model(object):
def __init__(self):
with mt_graph.as_default():
# 建立placeholder
with tf.variable_scope("ipt_placeholder"):
self.enc_inp = tf.placeholder(tf.int32, shape=[1, None]) # None是因為句長不确定
self.enc_inp_size = tf.placeholder(tf.int32, shape=[1]) # batch_size是1
# 建立源語言的token的embedding和目智語言的token的embedding
with tf.variable_scope("token_embedding"):
# 源語言的token的embedding。這一層裡面的都是變量,resotre的時候會被恢複。
self.src_embedding = tf.Variable(initial_value=tf.truncated_normal(shape=[train_args.Source_vocab_size, train_args.RNN_hidden_size], dtype=tf.float32), trainable=True)
# 目智語言的token的embedding
self.trg_embedding = tf.Variable(initial_value=tf.truncated_normal(shape=[train_args.Target_vocab_size, train_args.RNN_hidden_size], dtype=tf.float32), trainable=True)
# 全連接配接層的參數
if train_args.Share_softmax_embedding:
self.full_connect_weights = tf.transpose(self.trg_embedding)
else:
self.full_connect_weights = tf.Variable(initial_value=tf.truncated_normal(shape=[train_args.RNN_hidden_size, train_args.Target_vocab_size], dtype=tf.float32), trainable=True)
self.full_connect_biases = tf.Variable(initial_value=tf.truncated_normal(shape=[train_args.Target_vocab_size], dtype=tf.float32))
with tf.variable_scope("encoder"):
# 根據輸入,得到輸入的token的向量
self.src_emb_inp = tf.nn.embedding_lookup(self.src_embedding, self.enc_inp) # 這是變量,resotre的時候會被恢複。
# 建構編碼器中的雙向LSTM
self.enc_forward_lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(
num_units=train_args.RNN_hidden_size) # 這是變量,resotre的時候會被恢複。
self.enc_backward_lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(
num_units=train_args.RNN_hidden_size) # 這是變量,resotre的時候會被恢複。
# 使用bidirectional_dynamic_rnn構造雙向RNN網絡。
# 把輸入的token的向量放入到encoder裡面去,得到輸出。
# enc_top_outputs包含了前向LSTM和反向LSTM的輸出。
# enc_top_states也一樣。
# 我們把前向的LSTM頂層的outputs和反向的LSTM頂層的outputs concat一下,
# 作為attention的輸入。
# enc_top_outputs這個tuple,每一個元素的shape都是[batch_size, time_step, hidden_size]
# 這一層以下兩個操作,不是變量。resotre的時候對它們沒有影響。
self.enc_top_outputs, self.enc_top_states = tf.nn.bidirectional_dynamic_rnn(
cell_fw=self.enc_forward_lstm_cell,
cell_bw=self.enc_backward_lstm_cell,
inputs=self.src_emb_inp,
sequence_length=self.enc_inp_size,
dtype=tf.float32)
self.enc_outpus = tf.concat(
[self.enc_top_outputs[0], self.enc_top_outputs[1]], -1)
with tf.variable_scope("decoder"):
# 建立多層decoder。這是變量,resotre的時候會被恢複。
self.dec_lstm_cell = tf.nn.rnn_cell.MultiRNNCell(
[tf.nn.rnn_cell.BasicLSTMCell(num_units=train_args.RNN_hidden_size) for _ in range(train_args.num_decoder_layers)])
# 選擇BahdanauAttention作為注意力機制。它是使用一層隐藏層的前饋神經網絡。
# 這個操作不是變量。resotre的時候對它們沒有影響。
self.attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(
num_units=train_args.RNN_hidden_size,
memory=self.enc_outpus,
memory_sequence_length=self.enc_inp_size)
# 将self.dec_lstm_cell和self.attention_mechanism封裝成更進階的API。
# 這個操作不是變量。resotre的時候對它們沒有影響。
self.after_attention_cell = tf.contrib.seq2seq.AttentionWrapper(
self.dec_lstm_cell,
self.attention_mechanism,
attention_layer_size=train_args.RNN_hidden_size)
# 這裡的tf.variable_scope()一定是"decoder/rnn/attention_wrapper"。
# 否則decoder的參數加載不進來。
# 大家可以在train.py檔案和這個檔案裡面寫tf.trainable_variables(),
# 然後打斷點檢視下變量以及變量域
with tf.variable_scope("decoder/rnn/attention_wrapper"):
# 這裡我們使用變長的tf.TensorArray()來放置decoder的輸入和輸出内容。
self.dec_inp = tf.TensorArray(size=0,
dtype=tf.int32,
dynamic_size=True,
clear_after_read=False)
# 我們先在self.dec_inp裡放入[SOS]的id,代表開始标緻。
self.dec_inp = self.dec_inp.write(0, 1) # 1代表[SOS]的id
# 我們接下去會使用tf.while_loop()來不斷的讓decoder輸出,
# 是以我們需要提前定義好兩個函數。
# 一個是循環條件,另一個是循環體,還有一個是初始變量。
# 我們先來定義初始變量,decoder有狀态,輸入兩個變量,我們還要加一個step_count變量。
# 當step_count超出我們設定的範圍的時候,就跳出循環。防止decoder無休止的産生outputs。
init_dec_state = self.after_attention_cell.zero_state(
batch_size=1, dtype=tf.float32)
input_index_ = 0
init_variables = (init_dec_state, self.dec_inp, input_index_)
def continue_loop_condition(state, dec_inp, input_index):
end_flag = tf.not_equal(dec_inp.read(input_index),
2) # 2代表[EOS]的标緻
length_flag = tf.less_equal(
input_index,
train_args.test_max_output_sentence_length)
continue_flag = tf.logical_and(end_flag, length_flag)
continue_flag = tf.reduce_all(continue_flag)
return continue_flag
def loop_body_func(state, dec_inp, input_index):
# 讀取decoder的輸入
inp = [dec_inp.read(input_index)]
inp_embedding = tf.nn.embedding_lookup(
self.trg_embedding, inp)
# 調用call函數,向前走一步
new_output, new_state = self.after_attention_cell.call(
state=state, inputs=inp_embedding)
# 将new_output再做一次映射,映射到字典的次元
# 先将它reshape一下。
new_output = tf.reshape(new_output, [-1, train_args.RNN_hidden_size])
logits = (tf.matmul(new_output, self.full_connect_weights) + self.full_connect_biases)
# 做一次softmax操作
predict_idx = tf.argmax(logits, axis=1, output_type=tf.int32)
# 把infer出的下一個idx加入到dec_inp裡面去。
dec_inp = dec_inp.write(input_index + 1, predict_idx[0])
return new_state, dec_inp, input_index + 1
# 執行tf.while_loop(),它就會傳回最終的結果
self.final_state_op, self.final_dec_inp_op, self.final_input_index_op = tf.while_loop(
continue_loop_condition, loop_body_func, init_variables)
# 将最後的結果stack()一下
self.final_dec_inp_op = self.final_dec_inp_op.stack()
# 讀取英文的token_dictionary
with open(train_args.english_token_id_dictionary_pickle_path, 'rb') as file:
english_token_id_dictionary = pickle.load(file)
# 讀取中文的token_dictionary
with open(train_args.chinese_token_id_dictionary_pickle_path, 'rb') as file:
chinese_token_id_dictionary = pickle.load(file)
chinese_id_token_dictionary = {idx: token for token, idx in chinese_token_id_dictionary.items()}
nmt_model = Model() # 建立模型
# sesstion 的config
session_config = tf.ConfigProto(allow_soft_placement=True)
session_config.gpu_options.allow_growth = True
# 打開sesstion,開始進行翻譯的預測。
with tf.Session(graph=mt_graph, config=session_config) as sess:
# 建構saver
saver = tf.train.Saver(max_to_keep=5)
sess.run(tf.global_variables_initializer())
restore_path = tf.train.latest_checkpoint(
train_args.finish_model_saved_path.replace('/model', ''))
saver.restore(sess, restore_path)
while True:
sentence = input("請輸入英文句子:")
# 将英文句子根據token_dictionary轉換成idx的形式
sentence = nltk.word_tokenize(sentence)
# 将輸入的英文句子tokenize成token後轉換成id形式。
for idx, word in enumerate(sentence):
sentence[idx] = english_token_id_dictionary.get(
word, english_token_id_dictionary['<unk>'])
# 在英文句子的最後添加一個'<eos>'
sentence.append(english_token_id_dictionary['<eos>'])
# 句子的長度
sentence_length = len(sentence)
translation_result = sess.run(nmt_model.final_dec_inp_op,
feed_dict={
nmt_model.enc_inp: [sentence],
nmt_model.enc_inp_size:
[sentence_length]
})
translation_result = list(translation_result)
for index, idx in enumerate(translation_result):
translation_result[index] = chinese_id_token_dictionary[idx]
# 因為傳回的屎decoder的輸入部分,是以第1位為<sos>,不需要展現出來。
print(''.join(
translation_result[1:]))
模型訓練階段的截圖和預測階段的截圖
哈哈~語料還不夠多,是以所能展現的效果也就這樣了。各位同學可以增加語料試試。說不定翻譯的效果會讓你驚訝哦。