【TensorFlow實戰筆記】對於TED(en-zh)數據集進行Seq2Seq模型實戰,以及對應的Attention機制(tf保存模型讀取模型)

個人公衆號

AI蝸牛車

作者是南京985AI碩士,CSDN博客專家,研究方向主要是時空序列預測和時間序列數據挖掘,獲國家獎學金,校十佳大學生,省優秀畢業生,阿里天池時空序列比賽rank3。公衆號致力於技術專欄化,主要包括ML、DL、NLP、CV、個人思考總結等

個人微信號id:Che_Hongshu,歡迎來交流,加的時候請備註好CSDN,謝謝配合
在這裏插入圖片描述

一、前言

其他實戰筆記
建議先看:

其他選看

environment:

  • os: linux
  • python: 2.7
  • tf: 1.12.0

本文中所有的代碼來自於本人github,希望您star.
https://github.com/chehongshu/DL-tenserflow/tree/master/Seq2Seq_Attention

二、Se2Seq 模型的基本思想

使用一個循環神經網絡讀取輸入句子,將整個句子的信息壓縮到一個固定維度的編碼中;再使用另一個循環神經網絡讀取這個編碼,將其“解壓”爲目標語言的一個句子。
這兩個循環神經網絡分別稱爲編碼器( Encoder )和解碼器( Decoder ),這個模型也稱爲
encoder-decoder模型.
在這裏插入圖片描述從途中可以看到解碼器第一個input爲SOS,結束爲EOS,解釋如下。

  • SOS Start of sentence
  • EOS End of sentence

在圖中也可以看出來編碼器沒有輸出,而解碼器有輸出,並且每次輸出作爲下次的輸入

  • 編碼器(Encoder):詞向量層(embedding)+rnn,因爲沒有輸出不需要softmax層

  • 解碼器(Decoder):輸入爲單詞的詞向量 ,輸出爲 softmax層產生的單詞概率,損失函數爲 log perplexity,解決器其實就是一個以編碼爲前提的一個語言模型,所以很多trick和上次講的LSTM一樣可以利用,比如softmax和embedding層可以共享參數。

作爲翻譯model評測方法直接用翻譯結果來評測即可,比如輸入一段文字,輸出對應的結果,利用人工或者其他方式來判別是否正確,輸出結果的過程爲解碼器的解碼過程,每次輸出爲概率最大的詞,當然他也作爲下一次的輸入。

三、下載數據集

選擇較小的IWLST-TED數據集(以英文-中文數據爲例)它的英文- 中文訓練數據包含約 21 萬個句子對, 內容是 TED 演講的中英字幕。
下載地址: 
https://wit3.fbk.eu/mt.php?release=2015-01
在這裏插入圖片描述
點擊如上圖所示,下載en-zh.tgz文件,並解壓。

四、預處理數據

說白了,預處理數據其實就是將數據進行數字化處理和上一篇我寫的預處理方式差不多參考
《tensorflow實戰筆記》通俗詳述RNN理論,LSTM理論,以及LSTM對於PTB數據集進行實戰
基本步驟就是:

  1. 統計預料中出現的單詞,並存到vocab文件中。
  2. 爲每個單詞分配一個單詞的id,在這邊處理上爲行數作爲id,這個id就是最後用來轉換爲數字化的規則。
  3. 最後通過這種規則來把文本轉換爲單詞編號形式的文件。

1.處理原文本格式(切片)

這個文件裏的句子如下所示
i was studying dl in the ‘60 s,’ 70s.
我喜歡寫代碼。
而我們需要有那種每個元素都是由空格所分開的(包括所有符號:’ ’ , .等等)
所以處理之後的句子爲
i was studying dl in the ‘ 60 s , ’ 70s .
我 喜 歡 寫 代 碼 。
每個元素都可以由空格直接分開(代碼上好統一操作)
所以當務之急是將這個數據進行相應的切片操作,常用的爲moses
進入這個網站

https://github.com/moses-smt/mosesdecoder/blob/master/scripts/tokenizer/tokenizer.perl

在這裏插入圖片描述
鼠標右鍵,從鏈接另存文件到本地一個文件夾裏去。我習慣直接放在dataset文件夾中
在這裏插入圖片描述
這裏面的兩個文件 train.tags.en-zh.en, train.tags.en-zh.zh
需要自行將裏面的沒有用的部分刪掉因爲帶有一些tags,把文件裏面的類似於url,keywords,speaker, talkid, title,description的這種結構的全部刪掉。
在這裏插入圖片描述
類似於這種情況的全部刪掉,有些在前面,有些在後面,有些在文章中間。
這些東西應該是類似於每個talk的簡介那種性質,我查了一下一個差不多有3000多個,難受了,大家還是不要在這個上面浪費時間了,直接用我的弄好的得了,在下面的github的repo裏。

以下是利用工具進行文本切片。

# train.raw.en 是原始輸入數據,格式爲每行一句話: train.txt.en 是輸出的文件名。
# -no-escape 參數表示不把標點符號替換爲HTML編碼(如把引號替換爲 ”"”) 。
# -1 en 參數表示輸入文件的語言是英文。
perl ./moses_tokenizer.perl -no-escape -1 en < ./train.raw.en > train.txt.en
# train.raw.zh 是原始輸入數據,格式爲每行一句話: tra 工 n .t xt.zh 是輸出的文件名。
# sed ’ s/ I 忡 ’ 表示去除文本中已有的空格。 ’ s/\B/ /g e 將每個字之間的邊界替換爲空格。
sed ’ s/ //g; s/\B/ /g ’ . / train . raw .zh > train.txt.zh

中文這裏處理是單箇中文或者符號進行分割切片。
我弄好的數據集在我的個人github中:
https://github.com/chehongshu/DL-tenserflow/tree/master/Seq2Seq_Attention/en-zh

2.處理原文本格式(切片)

完成切詞後,再使用《tensorflow實戰筆記》通俗詳述RNN理論,LSTM理論,以及LSTM對於PTB數據集進行實戰中處理PTB數據相同的方法,分別生成英文文本和中文文本詞彙文件即vocab,再將文本轉化爲單詞編號。生成詞彙文件時,需要注意將 <sos>、<eso>、<unk>這三個詞手動加入到詞彙表中,並且要限制詞匯表大小,將詞頻過低的詞替換爲 <unk>,在這裏我們假定英文詞彙表大小爲 10000 ,中文詞彙表大小爲 4000。
generate_VOCAB.py

# -*- coding:UTF-8 -*-

"""
@Author:Che_Hongshu
@Modify:2019.1.8
"""
import codecs
import collections
from operator import itemgetter

DATA_TYPE = "english"  # 將DATA_TYPE先後設置爲chinese,english得到中英文VOCAB文件

if DATA_TYPE == "chinese":  # 翻譯語料的中文部分
    RAW_DATA = "./en-zh/train.txt.zh"
    VOCAB_OUTPUT = "zh.vocab"
    VOCAB_SIZE = 4000  #中文詞彙表單詞個數
elif DATA_TYPE == "english":  # 翻譯語料的英文部分
    RAW_DATA = "./en-zh/train.txt.en"
    VOCAB_OUTPUT = "en.vocab"
    VOCAB_SIZE = 10000  #英文詞彙表單詞個數

counter = collections.Counter() #一個計數器,統計每個單詞出現的次數

with codecs.open(RAW_DATA, "r", "utf-8") as f: #utf-8格式讀取
    for line in f:
        for word in line.strip().split(): #line.strip().split()相當於把每一行的前後空格去掉,再根據空格分詞生成list
            counter[word] += 1 #統計相同單詞出現次數+1
#  Counter 集成於 dict 類,因此也可以使用字典的方法,此類返回一個以元素爲 key 、元素個數爲 value 的 Counter 對象集合
# 依據key排序 itermgetter(1)爲降序
sorted_word_to_cnt = sorted(counter.items(), key=itemgetter(1), reverse=True)

#  轉換成單詞string的list
sorted_words_list = [x[0] for x in sorted_word_to_cnt]

#  加入句子結束符
sorted_words_list = ["<unk>", "<sos>", "<eos>"] + sorted_words_list

if len(sorted_words_list) > VOCAB_SIZE:
    sorted_words_list = sorted_words_list[:VOCAB_SIZE]

with codecs.open(VOCAB_OUTPUT, 'w', 'utf-8') as file_output:
    for word in sorted_words_list:
        file_output.write(word + '\n')

產生兩個vocab文件 分別爲中文英文的詞彙文件。
再通過VOCAB_transfrom_sequence.py產生數據集對應的id文件

# -*- coding:UTF-8 -*-

"""
@Author:Che_Hongshu
@Modify:2018.1.8
"""
import codecs

DATA_TYPE = "chinese"  # 將DATA_TYPE先後設置爲chinese,english得到中英文VOCAB文件

if DATA_TYPE == "chinese":  # 翻譯語料的中文部分
    RAW_DATA = "./en-zh/train.txt.zh"
    VOCAB = "zh.vocab"
    OUTPUT_DATA = "train.zh"
elif DATA_TYPE == "english":  # 翻譯語料的英文部分
    RAW_DATA = "./en-zh/train.txt.en"
    VOCAB = "en.vocab"
    OUTPUT_DATA = "train.en"


with codecs.open(VOCAB, 'r', 'utf-8') as f_vocab:  #打開文件進入讀操作
    vocab = [w.strip() for w in f_vocab.readlines()]  # 先把所有詞轉換成list
    # 把每個詞和所在行數對應起來並且zip打包成(“詞”,行數)格式轉換成dict格式
word_to_id = {k: v for (k, v) in zip(vocab, range(len(vocab)))}

# 返回id 如果在詞彙表文件中則返回對應的id即可,如果沒有則返回'<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:
    words = line.strip().split() + ["<eos>"] #每一行的單詞變成sring list格式,每一句話後面加上一個結束符號
    out_line = ' '.join([str(get_id(w)) for w in words]) + '\n' #這一行中的每個單詞取出對應的id之後用空格相連接 
    fout.write(out_line)

# 關閉文件
fin.close()
fout.close()

3.語料的填充( padding )和 batching

在PTB的數據中,句子之間有上下文關聯,因此可以直接將句子連接起來成爲一個大的段落,這樣就可以利用《tensorflow實戰筆記》通俗詳述RNN理論,LSTM理論,以及LSTM對於PTB數據集進行實戰這篇文還章中的辦法來進行相應的batching操作。而在機器翻譯的訓練樣本中,每個句子對通常都是作爲獨立的數據來訓練的。由於每個句子的長短不一致,因此在將這些句子放到同一個batch時,需要將較短的句子補齊到與同 batch 內最長句子相同的長度。用於填充長度而填入的位置叫做填充(padding)。在TensorFlow中,tf.data.Dataset 的 padded_batch 函數可以解決這個問題。
example:
假設一個數據集中有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從磁盤動態讀取數據。

# -*- coding:UTF-8 -*-

import tensorflow as tf

MAX_LEN = 50                        # 限定句子的最大單詞數量
SOS_ID = 1                          # 目標語言詞彙表中<sos>的ID

"""
function: 數據batching,產生最後輸入數據格式
Parameters:
    file_path-數據路徑
Returns:
    dataset- 每個句子-對應的長度組成的TextLineDataset類的數據集對應的張量
CSDN:
    http://blog.csdn.net/qq_33431368
"""
# 使用Dataset從一個文件中讀取一個語言的數據。
# 數據的格式爲每一句話,單詞已經轉化爲單詞編號
def MakeDataset(file_path):
    dataset = tf.data.TextLineDataset(file_path)

    # map(function, sequence[, sequence, ...]) -> list
    # 通過定義可以看到,這個函數的第一個參數是一個函數,剩下的參數是一個或多個序列,返回值是一個集合。
    # function可以理解爲是一個一對一或多對一函數,map的作用是以參數序列中的每一個元素調用function函數,返回包含每次function函數返回值的list。
    # lambda argument_list: expression
    # 其中lambda是Python預留的關鍵字,argument_list和expression由用戶自定義
    # argument_list參數列表, expression 爲函數表達式
    # 根據空格將單詞編號切分開並放入一個一維向量
    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

"""
function: 從源語言文件src_path和目標語言文件trg_path中分別讀取數據,並進行填充和batching操作
Parameters:
    src_path-源語言,即被翻譯的語言,英語.
    trg_path-目標語言,翻譯之後的語言,漢語.
    batch_size-batch的大小
Returns:
    dataset- 每個句子-對應的長度 組成的TextLineDataset類的數據集
CSDN:
    http://blog.csdn.net/qq_33431368
"""
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]是目標句子長度
    #https://blog.csdn.net/qq_32458499/article/details/78856530這篇博客看一下可以細緻瞭解一下Dataset這個庫,以及.map和.zip的用法
    dataset = tf.data.Dataset.zip((src_data, trg_data))

    # 刪除內容爲空(只包含<eos>)的句子和長度過長的句子
    def FilterLength(src_tuple, trg_tuple):
        ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
        # tf.logical_and 相當於集合中的and做法,後面兩個都爲true最終結果纔會爲true,否則爲false
        # tf.greater Returns the truth value of (x > y),所以以下所說的是句子長度必須得大於一也就是不能爲空的句子
        # tf.less_equal Returns the truth value of (x <= y),所以所說的是長度要小於最長長度
        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) #兩個都滿足才返回true

    # filter接收一個函數Func並將該函數作用於dataset的每個元素,根據返回值True或False保留或丟棄該元素,True保留該元素,False丟棄該元素
    # 最後得到的就是去掉空句子和過長的句子的數據集
    dataset = dataset.filter(FilterLength)

    # 解碼器需要兩種格式的目標句子:
    # 1.解碼器的輸入(trg_input), 形式如同'<sos> X Y Z'
    # 2.解碼器的目標輸出(trg_label), 形式如同'X Y Z <eos>'
    # 上面從文件中讀到的目標句子是'X Y Z <eos>'的形式,我們需要從中生成'<sos> X Y Z'形式並加入到Dataset
    # 編碼器只有輸入,沒有輸出,而解碼器有輸入也有輸出,輸入爲<sos>+(除去最後一位eos的label列表)
    # 例如train.en最後都爲2,id爲2就是eos
    def MakeTrgInput(src_tuple, trg_tuple):
        ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
        # tf.concat用法 https://blog.csdn.net/qq_33431368/article/details/79429295
        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

五、seq2seq模型建立

因爲這邊沒有對應的一個acc相似的評判對model本身而是直接輸出預測結果,所以需要兩個模型,一個是train ,另一個是test

1.train_model

《tensorflow實戰筆記》通俗詳述RNN理論,LSTM理論,以及LSTM對於PTB數據集進行實戰一樣也使用一個雙層
LSTM 作爲循環神經網絡的主體,並在 Softmax 層和詞向量層之間共享參數,增加如下:

  • 增加了一個循環神經網絡作爲編碼器(如前面示意圖)
  • 使用 Dataset 動態讀取數據,而不是直接將所有數據讀入內存(這個就是Dataset輸入數據的特點)
  • 每個 batch 完全獨立,不需要在batch之間傳遞狀態(因爲不是一個文件整條句子,每個句子之間沒有傳遞關係)
  • 每訓練200步便將模型參數保存到一個 checkpoint 中,以後用於測試。
"""
function: seq2seq模型
Parameters:
Returns:
CSDN:
    http://blog.csdn.net/qq_33431368
"""
class NMTModel(object):
    """
    function: 模型初始化
    Parameters:
    Returns:
    CSDN:
        http://blog.csdn.net/qq_33431368
    """
    def __init__(self):

        # 定義編碼器和解碼器所使用的LSTM結構
        self.enc_cell = tf.nn.rnn_cell.MultiRNNCell(
            [tf.nn.rnn_cell.LSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)])
        self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
            [tf.nn.rnn_cell.LSTMCell(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_loss', [TRG_VOCAB_SIZE])

   """
    function: 在forward函數中定義模型的前向計算圖
    Parameters:
      MakeSrcTrgDataset函數產生的五種張量如下(全部爲張量)
        src_input: 編碼器輸入(源數據)
        src_size : 輸入大小
        trg_input:解碼器輸入(目標數據)
        trg_label:解碼器輸出(目標數據)
        trg_size: 輸出大小
    Returns:
        cost_per_token: cost操作op
        train_op: 訓練操作op
    CSDN:
        http://blog.csdn.net/qq_33431368
    """
    def forward(self, src_input, src_size, trg_input, trg_label, trg_size):
        batch_size = tf.shape(src_input)[0]
        # 將輸入和輸出單詞轉爲詞向量(rnn中輸入數據都要轉換成詞向量)
        # 相當於input中的每個id對應的embedding中的向量轉換
        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_rnn構造編碼器
        # 編碼器讀取源句子每個位置的詞向量,輸出最後一步的隱藏狀態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
        # 輸出重新轉換成shape爲[,HIDDEN_SIZE]
        output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
        #  計算解碼器每一步的softmax概率值
        logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias
        #  交叉熵損失函數,算loss
        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_NORM)
        # 利用梯度下降優化算法進行優化.學習率爲1.0
        optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
        # 相當於minimize的第二步,正常來講所得到的list[grads,vars]由compute_gradients得到,返回的是執行對應變量的更新梯度操作的op 
        train_op = optimizer.apply_gradients(zip(grads, trainable_variables))
        return cost_per_token, train_op

2.全部訓練代碼

# -*- coding:UTF-8 -*-
"""
@Author:Che_Hongshu
@Modify:2018.1.9
"""
import tensorflow as tf

SRC_TRAIN_DATA = 'train.en'  # 源語言輸入文件
TRG_TRAIN_DATA = 'train.zh'  # 目標語言輸入文件
CHECKPOINT_PATH = './model/seq2seq_ckpt'  # checkpoint保存路徑
HIDDEN_SIZE = 1024                  # LSTM的隱藏層規模
NUM_LAYERS = 2                      # 深層循環神經網絡中LSTM結構的層數
SRC_VOCAB_SIZE = 10000              # 源語言詞彙表大小
TRG_VOCAB_SIZE = 4000               # 目標語言詞彙表大小
BATCH_SIZE = 100                    # 訓練數據batch的大小
NUM_EPOCH = 5                       # 使用訓練數據的輪數
KEEP_PROB = 0.8                     # 節點不被dropout的概率
MAX_GRAD_NORM = 5                   # 用於控制梯度膨脹的梯度大小上限
SHARE_EMB_AND_SOFTMAX = True        # 在softmax層和詞向量層之間共享參數
MAX_LEN = 50                        # 限定句子的最大單詞數量
SOS_ID = 1                          # 目標語言詞彙表中<sos>的ID


"""
function: 數據batching,產生最後輸入數據格式
Parameters:
    file_path-數據路徑
Returns:
    dataset- 每個句子-對應的長度組成的TextLineDataset類的數據集對應的張量
CSDN:
    http://blog.csdn.net/qq_33431368
"""
def MakeDataset(file_path):
    dataset = tf.data.TextLineDataset(file_path)

    # map(function, sequence[, sequence, ...]) -> list
    # 通過定義可以看到,這個函數的第一個參數是一個函數,剩下的參數是一個或多個序列,返回值是一個集合。
    # function可以理解爲是一個一對一或多對一函數,map的作用是以參數序列中的每一個元素調用function函數,返回包含每次function函數返回值的list。
    # lambda argument_list: expression
    # 其中lambda是Python預留的關鍵字,argument_list和expression由用戶自定義
    # argument_list參數列表, expression 爲函數表達式
    # 根據空格將單詞編號切分開並放入一個一維向量
    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

"""
function: 從源語言文件src_path和目標語言文件trg_path中分別讀取數據,並進行填充和batching操作
Parameters:
    src_path-源語言,即被翻譯的語言,英語.
    trg_path-目標語言,翻譯之後的語言,漢語.
    batch_size-batch的大小
Returns:
    dataset- 每個句子-對應的長度 組成的TextLineDataset類的數據集
CSDN:
    http://blog.csdn.net/qq_33431368
"""
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]是目標句子長度
    #https://blog.csdn.net/qq_32458499/article/details/78856530這篇博客看一下可以細緻瞭解一下Dataset這個庫,以及.map和.zip的用法
    dataset = tf.data.Dataset.zip((src_data, trg_data))

    # 刪除內容爲空(只包含<eos>)的句子和長度過長的句子
    def FilterLength(src_tuple, trg_tuple):
        ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
        # tf.logical_and 相當於集合中的and做法,後面兩個都爲true最終結果纔會爲true,否則爲false
        # tf.greater Returns the truth value of (x > y),所以以下所說的是句子長度必須得大於一也就是不能爲空的句子
        # tf.less_equal Returns the truth value of (x <= y),所以所說的是長度要小於最長長度
        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) #兩個都滿足才返回true

    # filter接收一個函數Func並將該函數作用於dataset的每個元素,根據返回值True或False保留或丟棄該元素,True保留該元素,False丟棄該元素
    # 最後得到的就是去掉空句子和過長的句子的數據集
    dataset = dataset.filter(FilterLength)

    # 解碼器需要兩種格式的目標句子:
    # 1.解碼器的輸入(trg_input), 形式如同'<sos> X Y Z'
    # 2.解碼器的目標輸出(trg_label), 形式如同'X Y Z <eos>'
    # 上面從文件中讀到的目標句子是'X Y Z <eos>'的形式,我們需要從中生成'<sos> X Y Z'形式並加入到Dataset
    # 編碼器只有輸入,沒有輸出,而解碼器有輸入也有輸出,輸入爲<sos>+(除去最後一位eos的label列表)
    # 例如train.en最後都爲2,id爲2就是eos
    def MakeTrgInput(src_tuple, trg_tuple):
        ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
        # tf.concat用法 https://blog.csdn.net/qq_33431368/article/details/79429295
        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方法進行padding 和 batching操作
    batched_dataset = dataset.padded_batch(batch_size, padded_shapes)

    return batched_dataset

"""
function: seq2seq模型
Parameters:
Returns:
CSDN:
    http://blog.csdn.net/qq_33431368
"""
class NMTModel(object):
    """
    function: 模型初始化
    Parameters:
    Returns:
    CSDN:
        http://blog.csdn.net/qq_33431368
    """
    def __init__(self):

        # 定義編碼器和解碼器所使用的LSTM結構
        self.enc_cell = tf.nn.rnn_cell.MultiRNNCell(
            [tf.nn.rnn_cell.LSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)])
        self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
            [tf.nn.rnn_cell.LSTMCell(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_loss', [TRG_VOCAB_SIZE])

    """
    function: 在forward函數中定義模型的前向計算圖
    Parameters:
      MakeSrcTrgDataset函數產生的五種張量如下(全部爲張量)
        src_input: 編碼器輸入(源數據)
        src_size : 輸入大小
        trg_input:解碼器輸入(目標數據)
        trg_label:解碼器輸出(目標數據)
        trg_size: 輸出大小
    Returns:
    CSDN:
        http://blog.csdn.net/qq_33431368
    """
    def forward(self, src_input, src_size, trg_input, trg_label, trg_size):
        batch_size = tf.shape(src_input)[0]
        # 將輸入和輸出單詞轉爲詞向量(rnn中輸入數據都要轉換成詞向量)
        # 相當於input中的每個id對應的embedding中的向量轉換
        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_rnn構造編碼器
        # 編碼器讀取源句子每個位置的詞向量,輸出最後一步的隱藏狀態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
        # 輸出重新轉換成shape爲[,HIDDEN_SIZE]
        output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
        #  計算解碼器每一步的softmax概率值
        logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias
        #  交叉熵損失函數,算loss
        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_NORM)
        # 利用梯度下降優化算法進行優化.學習率爲1.0
        optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
        # 相當於minimize的第二步,正常來講所得到的list[grads,vars]由compute_gradients得到,返回的是執行對應變量的更新梯度操作的op
        train_op = optimizer.apply_gradients(zip(grads, trainable_variables))
        return cost_per_token, train_op
        
"""
function: 使用給定的模型model上訓練一個epoch,並返回全局步數,每訓練200步便保存一個checkpoint
Parameters:
    session :  會議
    cost_op :  計算loss的操作op
    train_op: 訓練的操作op
    saver:  保存model的類
    step:   訓練步數
Returns:
CSDN:
    http://blog.csdn.net/qq_33431368
"""
def run_epoch(session, cost_op, train_op, saver, step):
    # 訓練一個epoch
    # 重複訓練步驟直至遍歷完Dataset中所有數據
    while True:
        try:
            # 運行train_op並計算cost_op的結果也就是損失值,訓練數據在main()函數中以Dataset方式提供
            cost, _ = session.run([cost_op, train_op])
            # 步數爲10的倍數進行打印
            if step % 10 == 0:
                print('After %d steps, per token cost is %.3f' % (step, cost))
            # 每200步保存一個checkpoint
            if step % 200 == 0:
                saver.save(session, CHECKPOINT_PATH, global_step=step)
            step += 1
        except tf.errors.OutOfRangeError:
            break
    return step

"""
function: 主函數
Parameters:
Returns:
CSDN:
    http://blog.csdn.net/qq_33431368
"""
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()
        # 進行NUM_EPOCH輪數
        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()

3.全部測試代碼

模型保存在checkpoint中
測試也可以稱爲完全解碼或者推理過程
可以由上面的圖可知:
在訓練的時候解碼器是可以從輸入讀取到完整的目標訓練句子。
而在解碼或推理的過程中模型只能看到輸入句子,卻看不到目標句子

具體過程:和圖中描述的一樣,解碼器在第一步讀取<sos> 符,預測目標句子的第一個單詞,然後需要將這個預測的單詞複製到第二步作爲輸入, 再預測第二個單詞,直到預測的單詞爲<eso> 爲止 。 這個過程需要使用一個循環結構來實現 。在TensorFlow 中,循環結構是由 tf.while_loop 來實現的 。

# -*- coding:UTF-8 -*-

"""
@Author:Che_Hongshu
@Modify:2018.1.10
"""
import tensorflow as tf
import codecs
import sys

# 讀取checkpoint的路徑。4800表示是訓練程序在第4800步保存的checkpoint
CHECKPOINT_PATH = 'model/seq2seq_ckpt-9000'

# 模型參數。必須與訓練時的模型參數保持一致
HIDDEN_SIZE = 1024              # LSTM的隱藏層規模
NUM_LAYERS = 2                  # 深層循環神經網絡中LSTM結構的層數
SRC_VOCAB_SIZE = 10000          # 源語言詞彙表大小
TRG_VOCAB_SIZE = 4000           # 目標語言詞彙表大小
SHARE_EMB_AND_SOFTMAX = True    # 在softmax層和詞向量層之間共享參數

# 詞彙表文件
SRC_VOCAB = "en.vocab"
TRG_VOCAB = "zh.vocab"

# 詞彙表中<sos>和<eos>的ID,在解碼過程中需要用<sos>作爲第一步的輸入,<eos>爲最終的輸出結果,因此需要知道這兩個符號的ID
SOS_ID = 1
EOS_ID = 2

"""
function: seq2seq模型
Parameters:
Returns:
CSDN:
    http://blog.csdn.net/qq_33431368
"""
class NMTModel(object):
    # 在模型的初始化函數中定義模型要用到的變量
    def __init__(self):
        # 定義編碼器和解碼器所使用的LSTM結構
        self.enc_cell = tf.nn.rnn_cell.MultiRNNCell(
            [tf.nn.rnn_cell.LSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)])
        self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
            [tf.nn.rnn_cell.LSTMCell(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_loss', [TRG_VOCAB_SIZE])

    """
    function: 利用模型進行推理
    Parameters:
       src_input: 輸入句子,在這裏就是已經轉換成id文本的英文句子。
    Returns:
    CSDN:
        http://blog.csdn.net/qq_33431368
    """
    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, dtype=tf.float32)
        # 設置解碼的最大步數 避免在極端情況出現無限循環的問題
        MAX_DEC_LEN = 100
        with tf.variable_scope('decoder/rnn/multi_rnn_cell'):
            # 使用一個變長的TensorArray來存儲生成的句子
            # dynamic_size=True 動態大小 clear_after_read=False 每次讀完之後不清除
            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)
            """
            function: tf.while_loop的循環條件
            Parameters:
               state: 隱藏狀態 
                 trg_ids: 目標句子的id的集合,也就是上面定義的 TensorArray
                 step: 解碼步數
            Returns: 
                 解碼器沒有輸出< eos > , 或者沒有達到最大步數則輸出True,循環繼續。
            CSDN:
                http://blog.csdn.net/qq_33431368
            """
            def contunue_loop_condition(state, trg_ids, step):
                return tf.logical_and(tf.not_equal(trg_ids.read(step), EOS_ID),
                                                    tf.less(step, MAX_DEC_LEN-1))

            """
            function: tf.while_loop的循環條件
            Parameters:
                state: 隱藏狀態 
               trg_ids: 目標句子的id的集合,也就是上面定義的 TensorArray
                step: 解碼步數
            Returns: 
                next_state: 下一個隱藏狀態
                trg_ids: 新的得到的目標句子
                step+1: 下一步
            CSDN:
                http://blog.csdn.net/qq_33431368
            """
            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向前計算一步
                # 每個RNNCell都有一個call方法,使用方式是:(output, next_state) = call(input, state)。
                # 每調用一次RNNCell的call方法,就相當於在時間上“推進了一步”,這就是RNNCell的基本功能。
                dec_outputs, next_state = self.dec_cell.call(inputs=trg_emb, state=state)
                # 計算每個可能的輸出單詞對應的logit,並選取logit值最大的單詞作爲這一步的輸出
                # 解碼器輸出經過softmax層,算出每個結果的概率取最大爲最終輸出
                output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
                logits = (tf.matmul(output, self.softmax_weight) + self.softmax_bias)
                # tf.argmax(logits, axis=1, output_type=tf.int32)相當於一維裏的數據概率返回醉倒的索引值,即比step小一個
                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,返回最終狀態
            # while_loop(contunue_loop_condition, loop_body, init_loop_var)
            # contunue_loop_condition 循環條件
            # loop_body 循環體
            # 循環的起始狀態,所以循環條件和循環體的輸入參數就是起始狀態
            state, trg_ids, step = tf.while_loop(contunue_loop_condition, loop_body, init_loop_var)
            # 將TensorArray中元素疊起來當做一個Tensor輸出
            return trg_ids.stack()

def main():

    # 定義訓練用的循環神經網絡模型。
    with tf.variable_scope("nmt_model", reuse=None):
        model = NMTModel()

    # 定義個測試句子。
    test_en_text = "The sea is blue . <eos>"
    print(test_en_text)

    # 根據英文詞彙表,將測試句子轉爲單詞ID。
    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_en_text.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])

    # 輸出翻譯結果。 utf-8編碼
    print(output_text.encode('utf8'))
    sess.close()


if __name__ == '__main__':
    main()

六、Attention機制(注意力機制)

1.先看Attention理論

https://blog.csdn.net/wuzqchom/article/details/75792501
https://blog.csdn.net/guohao_zhang/article/details/79540014
這兩篇寫的不錯,先仔細看完,也謝謝這兩位博主
總結一下,其實從理解的角度爲:Attention機制的作用就是爲了讓輸入不光是編碼器的最終狀態,還可以和之前少許忘記的輸入編碼的部分相對應,看是否有關聯。
在這裏插入圖片描述
如圖所示,在代碼上的直觀改變爲

  • 編碼器從單向的LSTM結構,更改爲雙向的LSTM結構,所以編碼器的輸出也隨之變爲兩個方向一同的LSTM合成輸出
  • 需要根據編碼器輸出來計算Attention權重(有相應的計算模型)
  • 解碼器和Attention機制封裝成一個更爲高級的循環神經網絡結構

2.把NMTmodel替換成NMTmodel_Attention

train中需要替代的代碼如下:

"""
function: seq2seq模型-Attention機制
Parameters:
Returns:
CSDN:
    http://blog.csdn.net/qq_33431368
"""
# attention 編碼器雙向循環,解碼器單向循環
class NMTModel_Attention(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.LSTMCell(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_loss', [TRG_VOCAB_SIZE])

    """
      function: 在forward函數中定義模型的前向計算圖
      Parameters:
        MakeSrcTrgDataset函數產生的五種張量如下(全部爲張量)
          src_input: 編碼器輸入(源數據)
          src_size : 輸入大小
          trg_input:解碼器輸入(目標數據)
          trg_label:解碼器輸出(目標數據)
          trg_size: 輸出大小
      Returns:
      CSDN:
          http://blog.csdn.net/qq_33431368
      """
    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)
        # 編碼器
        with tf.variable_scope('encoder'):
            # 構造編碼器時,使用birdirectional_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構造解碼器
        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_NORM)
        optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
        train_op = optimizer.apply_gradients(zip(grads, trainable_variables))
        return cost_per_token, train_op

decoding需要替代的代碼

"""
function: seq2seq模型-Attention
Parameters:
Returns:
CSDN:
    http://blog.csdn.net/qq_33431368
"""
class NMTModel_Attention(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.LSTMCell(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_loss', [TRG_VOCAB_SIZE])

    """
      function: 利用模型進行推理
      Parameters:
         src_input: 輸入句子,在這裏就是已經轉換成id文本的英文句子。
      Returns:
      CSDN:
          http://blog.csdn.net/qq_33431368
    """
    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 構造編碼器(雙向LSTM)
        with tf.variable_scope('encoder'):
            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的循環條件
            """
            function: tf.while_loop的循環條件
            Parameters:
                state: 隱藏狀態 
                trg_ids: 目標句子的id的集合,也就是上面定義的 TensorArray
                step: 解碼步數
            Returns: 
                解碼器沒有輸出< eos > , 或者沒有達到最大步數則輸出True,循環繼續。
            CSDN:
                http://blog.csdn.net/qq_33431368
            """
            def contunue_loop_condition(state, trg_ids, step):
                return tf.logical_and(tf.not_equal(trg_ids.read(step), EOS_ID),
                                                    tf.less(step, MAX_DEC_LEN-1))

            """
            function: tf.while_loop的循環條件
            Parameters:
                 state: 隱藏狀態 
                 trg_ids: 目標句子的id的集合,也就是上面定義的 TensorArray
                 step: 解碼步數
            Returns: 
                 next_state: 下一個隱藏狀態
                 trg_ids: 新的得到的目標句子
                 step+1: 下一步
            CSDN:
                 http://blog.csdn.net/qq_33431368
            """
            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,而是直接調用call向前計算一步
                # 每個RNNCell都有一個call方法,使用方式是:(output, next_state) = call(input, state)。
                # 每調用一次RNNCell的call方法,就相當於在時間上“推進了一步”,這就是RNNCell的基本功能。
                dec_outputs, next_state = attention_cell.call(inputs=trg_emb, state=state)
                # 計算每個可能的輸出單詞對應的logit,並選取logit值最大的單詞作爲這一步的輸出
                # 解碼器輸出經過softmax層,算出每個結果的概率取最大爲最終輸出
                output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
                logits = (tf.matmul(output, self.softmax_weight) + self.softmax_bias)
                # tf.argmax(logits, axis=1, output_type=tf.int32)相當於一維裏的數據概率返回醉倒的索引值,即比step小一個
                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
            # while_loop(contunue_loop_condition, loop_body, init_loop_var)
            # contunue_loop_condition 循環條件
            # loop_body 循環體
            #  循環的起始狀態,所以循環條件和循環體的輸入參數就是起始狀態
            state, trg_ids, step = tf.while_loop(contunue_loop_condition, loop_body, init_loop_var)
            # 將TensorArray中元素疊起來當做一個Tensor輸出
            return trg_ids.stack()

七、測試結果(以下爲Attention_model訓練測試結果圖)

1.
在這裏插入圖片描述
2.
在這裏插入圖片描述
3.
在這裏插入圖片描述

八、github代碼地址(歡迎star, fork)

訓練的最後的model存在百度雲中,想抓緊試試代碼就下載,要不就自己train也行,裏面只上傳seq2seq_attention的最後得到的model了

鏈接: https://pan.baidu.com/s/1QFJxFzBFZJfZChHoB1Cy4w 提取碼: mnwa

github代碼地址:

https://github.com/chehongshu/DL-tenserflow/tree/master/Seq2Seq_Attention

下載訓練好的model文件之後把文件夾複製到工程裏去,之後重命名爲model即可使用
類似於以下所示:
在這裏插入圖片描述
PS: 如果覺得本篇本章對您有所幫助,歡迎關注、評論、點贊!Github給個Star就更完美了_!

歡迎關注本人公衆號

在這裏插入圖片描述

Reference

https://www.w3cschool.cn/tensorflow_python/tensorflow_python-8pwb2dbr.html

http://www.runoob.com/python/python-func-map.html

https://blog.csdn.net/zjuxsl/article/details/79437563

https://blog.csdn.net/lvjc2010/article/details/78777098/

https://blog.csdn.net/chixujohnny/article/details/51025336

https://blog.csdn.net/qq_32458499/article/details/78856530

https://blog.csdn.net/qq_16234613/article/details/81703228

《TensorFlow: 實戰Google深度學習框架》

https://blog.csdn.net/weixin_31767897/article/details/79365968

https://blog.csdn.net/guolindonggld/article/details/79256018

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章