轉載自公眾號:磐創AI
作者 | Nechu BM
編譯 | VK
來源 | Towards Data Science
基礎知識:了解本文之前最好擁有關於循環神經網絡(RNN)和編解碼器的知識。
本文是關於如何使用Python和Keras開發一個編解碼器模型的實用教程,更精確地說是一個序列到序列(Seq2Seq)。在上一個教程中,我們開發了一個多對多翻譯模型,如下圖所示:
這種結構有一個重要的限制,即序列長度。正如我們在圖像中看到的,輸入序列和輸出序列的長度必須相同。如果我們需要不同的長度呢?
例如,我們想實現一個接受不同序列長度的模型,它接收一個單詞序列並輸出一個數字,或者是圖像字幕模型,其中輸入是一個圖像,輸出是一個單詞序列。
如果我們要開發的模型是輸入和輸出長度不同,我們需要開發一個編解碼器模型。通過本教程,我們將了解如何開發模型,並將其應用於翻譯練習。模型的表示如下所示。
我們將模型分成兩部分,首先,我們有一個編碼器,輸入西班牙語句子並產生一個隱向量。編碼器是用一個嵌入層將單詞轉換成一個向量然後用一個循環神經網絡(RNN)來計算隱藏狀態,這裡我們將使用長短期記憶(LSTM)層。
然後編碼器的輸出將被用作解碼器的輸入。對於解碼器,我們將再次使用LSTM層,以及預測英語單詞的全連接層。
實現示例數據來自manythings.org。它是由語言的句子對組成的。在我們的案例中,我們將使用西班牙語-英語對。
建立模型首先需要對數據進行預處理,得到西班牙語和英語句子的最大長度。
1-預處理先決條件:了解Keras中的類「tokenizer」和「pad_sequences」。如果你想詳細回顧一下,我們在上一個教程中討論過這個主題。
首先,我們將導入庫,然後讀取下載的數據。
import string
import numpy as np
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import Model
from keras.layers import LSTM, Input, TimeDistributed, Dense, Activation, RepeatVector, Embedding
from keras.optimizers import Adam
from keras.losses import sparse_categorical_crossentropy
# 翻譯文件的路徑
path_to_data = 'data/spa.txt'
# 讀文件
translation_file = open(path_to_data,"r", encoding='utf-8')
raw_data = translation_file.read()
translation_file.close()
# 解析數據
raw_data = raw_data.split('\n')
pairs = [sentence.split('\t') for sentence in raw_data]
pairs = pairs[1000:20000]一旦我們閱讀了數據,我們將保留第一個例子,以便更快地進行訓練。如果我們想開發更高性能的模型,我們需要使用完整的數據集。然後我們必須通過刪除大寫字母和標點符號來清理數據。
def clean_sentence(sentence):
# 把這個句子小寫
lower_case_sent = sentence.lower()
# 刪除標點
string_punctuation = string.punctuation + "¡" + '¿'
clean_sentence = lower_case_sent.translate(str.maketrans('', '', string_punctuation))
return clean_sentence接下來,我們將句子標識化並分析數據。
def tokenize(sentences):
# 創建 tokenizer
text_tokenizer = Tokenizer()
# 應用到文本上
text_tokenizer.fit_on_texts(sentences)
return text_tokenizer.texts_to_sequences(sentences), text_tokenizer創建完函數後,我們可以進行預處理:
# 清理句子
english_sentences = [clean_sentence(pair[0]) for pair in pairs]
spanish_sentences = [clean_sentence(pair[1]) for pair in pairs]
# 標識化單詞
spa_text_tokenized, spa_text_tokenizer = tokenize(spanish_sentences)
eng_text_tokenized, eng_text_tokenizer = tokenize(english_sentences)
print('Maximum length spanish sentence: {}'.format(len(max(spa_text_tokenized,key=len))))
print('Maximum length english sentence: {}'.format(len(max(eng_text_tokenized,key=len))))
# 檢查長度
spanish_vocab = len(spa_text_tokenizer.word_index) + 1
english_vocab = len(eng_text_tokenizer.word_index) + 1
print("Spanish vocabulary is of {} unique words".format(spanish_vocab))
print("English vocabulary is of {} unique words".format(english_vocab))上面的代碼列印以下結果
根據之前的代碼,西班牙語句子的最大長度為12個單詞,英語句子的最大長度為6個單詞。在這裡我們可以看到使用編解碼器模型的優勢。以前我們處理等長句子有局限性,所以我們需要對英語句子應用填充到12,現在只需要一半。因此,更重要的是,它還減少了LSTM時間步數,減少了計算需求和複雜性。
我們使用填充來使每種語言中句子的最大長度相等。
max_spanish_len = int(len(max(spa_text_tokenized,key=len)))
max_english_len = int(len(max(eng_text_tokenized,key=len)))
spa_pad_sentence = pad_sequences(spa_text_tokenized, max_spanish_len, padding = "post")
eng_pad_sentence = pad_sequences(eng_text_tokenized, max_english_len, padding = "post")
# 重塑
spa_pad_sentence = spa_pad_sentence.reshape(*spa_pad_sentence.shape, 1)
eng_pad_sentence = eng_pad_sentence.reshape(*eng_pad_sentence.shape, 1)現在我們已經準備好了數據,讓我們構建模型。
2.模型開發在下一節中,我們將創建模型,並在python代碼中解釋添加的每一層。
2.1-編碼器我們定義的第一層是圖像的嵌入層。為此,我們首先必須添加一個輸入層,這裡唯一要考慮的參數是「shape」,這是西班牙語句子的最大長度,在我們的例子中是12。
然後我們將其連接到嵌入層,這裡要考慮的參數是「input_dim」(西班牙語詞彙表的長度)和「output_dim」(嵌入向量的形狀)。此層將把西班牙語單詞轉換為輸出維度形狀的向量。
這背後的概念是以空間表示的形式提取單詞的含義,其中每個維度都是定義單詞的特徵。例如,「sol」將轉換為形狀為128的向量。輸出維越高,從每個單詞中提取的語義意義就越多,但所需的計算和處理時間也就越高。我們也需要在速度和性能之間找到平衡。
input_sequence = Input(shape=(max_spanish_len,))
embedding = Embedding(input_dim=spanish_vocab, output_dim=128,)(input_sequence)接下來,我們將添加大小為64的LSTM層。即使LSTM的每一個時間步都輸出一個隱藏向量,我們會把注意力集中在最後一個,因此參數「return_sequences」 是'False'。我們將看到LSTM層如何在解碼器的return_sequences=True的情況下工作。
input_sequence = Input(shape=(max_spanish_len,))
embedding = Embedding(input_dim=spanish_vocab, output_dim=128,)(input_sequence)
encoder = LSTM(64, return_sequences=False)(embedding)當返回序列為'False'時,輸出是最後一個隱藏狀態。
2.2-解碼器編碼器層的輸出將是最後一個時間步的隱藏狀態。然後我們需要把這個向量輸入解碼器。讓我們更精確地看一下解碼器部分,並了解它是如何工作的。
正如我們在圖像中看到的,隱藏向量被重複n次,因此LSTM的每個時間步都接收相同的向量。為了使每個時間步都有相同的向量,我們需要使用層RepeatVector,因為它的名字意味著它的作用是重複它接收的向量,我們需要定義的唯一參數是n,重複次數。這個數字等於解碼器部分的時間步數,換句話說就是英語句子的最大長度6。
input_sequence = Input(shape=(max_spanish_len,))
embedding = Embedding(input_dim=spanish_vocab, output_dim=128,)(input_sequence)
encoder = LSTM(64, return_sequences=False)(embedding)
r_vec = RepeatVector(max_english_len)(encoder)一旦我們準備好輸入,我們將繼續解碼器。這也是用LSTM層構建的,區別在於參數return_sequences,在本例中為'True'。這個參數是用來做什麼的?在編碼器部分,我們只期望在最後一個時間步中有一個向量,而忽略了其他所有的向量,這裡我們期望每個時間步都有一個輸出向量,這樣全連接層就可以進行預測。
input_sequence = Input(shape=(max_spanish_len,))
embedding = Embedding(input_dim=spanish_vocab, output_dim=128,)(input_sequence)
encoder = LSTM(64, return_sequences=False)(embedding)
r_vec = RepeatVector(max_english_len)(encoder)
decoder = LSTM(64, return_sequences=True, dropout=0.2)(r_vec)我們還有最後一步,預測翻譯的單詞。為此,我們需要使用全連接層。我們需要定義的參數是單元數,這個單元數是輸出向量的形狀,它需要與英語詞彙的長度相同。為什麼?這個向量的值都接近於零,除了其中一個單位接近於1。然後我們需要將輸出1的單元的索引映射到字典中,在字典中我們將每個單元映射到一個單詞。
例如,如果輸入是單詞『sun』,而輸出是一個向量,其中所有都是零,然後單元472是1,那麼我們將該索引映射到包含英語單詞的字典上,並得到值『sun』。
我們剛剛看到了如何應用全連接層來預測一個單詞,但是我們如何對整個句子進行預測呢?因為我們使用return_sequence=True,所以LSTM層在每個時間步輸出一個向量,所以我們需要在每個時間步應用前面解釋過的全連接層層,讓其每次預測一個單詞。
為此,Keras開發了一個稱為TimeDistributed的特定層,它將相同的全連接層應用於每個時間步。
input_sequence = Input(shape=(max_spanish_len,))
embedding = Embedding(input_dim=spanish_vocab, output_dim=128,)(input_sequence)
encoder = LSTM(64, return_sequences=False)(embedding)
r_vec = RepeatVector(max_english_len)(encoder)
decoder = LSTM(64, return_sequences=True, dropout=0.2)(r_vec)
logits = TimeDistributed(Dense(english_vocab))(decoder)最後,我們創建模型並添加一個損失函數。
enc_dec_model = Model(input_sequence, Activation('softmax')(logits))
enc_dec_model.compile(loss=sparse_categorical_crossentropy,
optimizer=Adam(1e-3),
metrics=['accuracy'])
enc_dec_model.summary()一旦我們定義了模型,我們就可以訓練它了。
model_results = enc_dec_model.fit(spa_pad_sentence, eng_pad_sentence, batch_size=30, epochs=100)當模型訓練好後,我們就可以進行第一次翻譯了。你還可以找到函數「logits_to_sentence」,它將全連接層的輸出與英語詞彙進行映射。
def logits_to_sentence(logits, tokenizer):
index_to_words = {idx: word for word, idx in tokenizer.word_index.items()}
index_to_words[0] = '<empty>'
return ' '.join([index_to_words[prediction] for prediction in np.argmax(logits, 1)])
index = 14
print("The english sentence is: {}".format(english_sentences[index]))
print("The spanish sentence is: {}".format(spanish_sentences[index]))
print('The predicted sentence is :')
print(logits_to_sentence(enc_dec_model.predict(spa_pad_sentence[index:index+1])[0], eng_text_tokenizer))
結論編解碼器結構允許不同的輸入和輸出序列長度。首先,我們使用嵌入層來創建單詞的空間表示,並將其輸入LSTM層,因為我們只關注最後一個時間步的輸出,我們使用return_sequences=False。
這個輸出向量需要重複的次數與解碼器部分的時間步數相同,為此我們使用RepeatVector層。解碼器將使用LSTM,參數return_sequences=True,因此每個時間步的輸出都會傳遞到全連接層。
儘管這個模型已經是上一個教程的一個很好的改進,我們仍然可以提高準確性。我們可以在一層的編碼器和解碼器中增加一層。我們也可以使用預訓練的嵌入層,比如word2vec或Glove。最後,我們可以使用注意機制,這是自然語言處理領域的一個主要改進。我們將在下一個教程中介紹這個概念。
附錄:不使用重複向量的編解碼器在本教程中,我們了解了如何使用RepeatVector層構建編碼器-解碼器。還有第二個選項,我們使用模型的輸出作為下一個時間步驟的輸入,而不是重複隱藏的向量,如圖所示。
實現這個模型的代碼可以在Keras文檔中找到,它需要對Keras庫有更深入的理解,並且開發要複雜得多:https://blog.keras.io/a-ten-minute-introduction-to-sequence-to-sequence-learning-in-keras.html
原文連結:https://towardsdatascience.com/how-to-build-an-encoder-decoder-translation-model-using-lstm-with-python-and-keras-a31e9d864b9b