Tensorflow2.0之基於注意力的神經機器翻譯

代碼實現

1、處理數據集

1.1 導入需要的庫

import tensorflow as tf

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from sklearn.model_selection import train_test_split

import unicodedata
import re
import numpy as np
import os
import io
import time

1.2 下載文件

# 下載文件
path_to_zip = tf.keras.utils.get_file(
    'spa-eng.zip', origin='http://storage.googleapis.com/download.tensorflow.org/data/spa-eng.zip',
    extract=True)

path_to_file = os.path.dirname(path_to_zip)+"/spa-eng/spa.txt"

1.3 處理西班牙語中的重音

西班牙語中經常會出現帶重音的字母,如 ,我們希望把它轉換成英文中的 a
unidecode 庫可以將任何 unicode 字符串音譯爲 ascii 文本中最接近的可能表示形式。
相關代碼爲:

def unicode_to_ascii(s):
    output = []
    for c in unicodedata.normalize('NFD', s):
        if unicodedata.category(c) != 'Mn':
            output.append(c)
    return ''.join(output)

當然也可以採用下面這種更簡潔的寫法。

def unicode_to_ascii(s):
    return ''.join(c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn')

其中:unicodedata.normalize(‘NFD’, s) 返回標準化後的文本,如:

test = u'¿Váy:?'
print(unicodedata.normalize('NFD', test))
print(test)
print(len(unicodedata.normalize('NFD', test)))
print(len(test))
¿Váy:?
¿Váy:?
7
6

雖然打印出來發現跟沒標準化時的一樣,但是通過打印長度可以看出,標準化後的文本會比之前的文本多出一個。

for c in unicodedata.normalize('NFD', test):
    print(c)
¿
V
a
́
y
:
?

通過打印標準化文本中的每個單詞,我們發現標準化後多出一個單詞的原因是它將帶重音的單詞分成了兩個單詞。

然後,我們使用 unicodedata.category( c ) 來判斷這個單詞是不是重音,如果是重音的話,它會返回 ‘Mn’,如:

for c in unicodedata.normalize('NFD', test):
    print(c)
    print(unicodedata.category(c))
¿
Po
V
Lu
a
Ll
́
Mn
y
Ll
:
Po
?
Po

1.4 處理ascii文本

def preprocess_sentence(w):
    w = unicode_to_ascii(w.lower().strip())
    
    w = re.sub(r"([?.!,¿])", r" \1 ", w)
    w = re.sub(r'[" "]+', " ", w)

    w = re.sub(r"[^a-zA-Z?.!,¿]+", " ", w)

    w = w.strip()
    
    w = '<start> ' + w + ' <end>'
    return w

其中:

  • w.lower().strip() 將所有字母轉換成小寫字母;
  • unicode_to_ascii(test.lower().strip()) 去掉重音;
  • re.sub(r"([?.!,¿])", r" \1 ", w) 在單詞與跟在其後的標點符號("?", “.”, “!”, “,”, “¿”)之間插入一個空格;
  • re.sub(r’[" "]+’, " ", w) 將連在一起的多個空格合併爲一個空格;
  • re.sub(r"[^a-zA-Z?.!,¿]+", " ", w) 除了 (a-z, A-Z, “.”, “?”, “!”, “,”, “¿”)之外,將所有字符替換爲空格;
  • w.strip() 用來去除頭尾空格;
  • ’<start> ’ + w + ’ <end>’ 給句子加上開始和結束標記,以便模型知道何時開始和結束預測。

仍用之前的 test 舉例:

w = test.lower().strip()
# '¿váy:?'
w = unicode_to_ascii(w)
# '¿vay:?'
w = re.sub(r"([?.!,¿])", r" \1 ", w)
# ' ¿ vay: ? '
w = re.sub(r'[" "]+', " ", w)
# ' ¿ vay: ? '
w = re.sub(r"[^a-zA-Z?.!,¿]+", " ", w)
# ' ¿ vay ? '
w = w.strip()
# '¿ vay ?'
w = '<start> ' + w + ' <end>'
# '<start> ¿ vay ? <end>'

因爲英文中不存在 ‘¿’ 標點符號,所以在處理西班牙語文本時,我們最後要將其編碼爲 ‘utf-8’ 格式:

w.encode('utf-8')
# b'<start> \xc2\xbf vay ? <end>'

1.5 返回單詞對

我們需要返回這樣格式的單詞對:[ENGLISH, SPANISH]

def create_dataset(path, num_examples):
    lines = io.open(path, encoding='UTF-8').read().strip().split('\n')

    word_pairs = [[preprocess_sentence(w) for w in l.split('\t')]  for l in lines[:num_examples]]

    return zip(*word_pairs)

其中:
io.open(path_to_file, encoding=‘UTF-8’).read() 用來輸出編碼後的文本;.strip() 用來去掉文本頭尾的空格;.split(’\n’) 使原來的一串字符串按 ‘\n’ 劃分爲很多字符串,然後放到一個列表中。

num_examples 表示要返回單詞對的對數,當 num_examples=None 時,返回所有單詞對。

比如:

en, sp = create_dataset(path_to_file, 3)
print(en)
print(sp)

返回:

('<start> go . <end>', '<start> go . <end>', '<start> go . <end>')
('<start> ve . <end>', '<start> vete . <end>', '<start> vaya . <end>')

1.6 生成文檔詞典

text.Tokenizer 類用來對文本中的詞進行統計計數,生成文檔詞典,以支持基於詞典位序生成文本的向量表示。

def tokenize(lang):
    lang_tokenizer = tf.keras.preprocessing.text.Tokenizer(filters='')
    lang_tokenizer.fit_on_texts(lang)

    tensor = lang_tokenizer.texts_to_sequences(lang)

    tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor,
                                                            padding='post')

    return tensor, lang_tokenizer

其中 fit_on_text(lang) 使用一系列文檔來生成 token 詞典,lang 是一個列表,每個元素爲一個文檔。輸出的 lang_tokenizer 即爲基於以上文檔生成的詞典。

tensor = lang_tokenizer.texts_to_sequences(lang) 將多個文檔轉換爲 word 下標的向量形式,shape[len(texts),len(text)] 即 (文檔數,每條文檔的長度)。

tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor, padding=‘post’) 的操作是先遍歷一遍上一步得到的列表中的每個元素,找出最長的一個元素(即包含的單詞最多),然後把所有元素都用 0 進行擴充至最長元素的長度。

比如,我們取前25個文本進行實驗:

targ_lang, inp_lang = create_dataset(path_to_file, 25)
lang_tokenizer = tf.keras.preprocessing.text.Tokenizer(filters='')
lang_tokenizer.fit_on_texts(targ_lang)
tensor1 = lang_tokenizer.texts_to_sequences(targ_lang)
tensor2 = tf.keras.preprocessing.sequence.pad_sequences(tensor, padding='post')
print('tensor1: ', tensor1)
print('tensor2: ', tensor2)
tensor1:  [[1, 5, 4, 2], [1, 5, 4, 2], [1, 5, 4, 2], [1, 5, 4, 2], [1, 13, 4, 2], [1, 9, 3, 2], [1, 9, 4, 2], [1, 14, 15, 2], [1, 6, 3, 2], [1, 6, 3, 2], [1, 6, 3, 2], [1, 7, 3, 2], [1, 7, 3, 2], [1, 7, 3, 2], [1, 10, 3, 2], [1, 10, 4, 2], [1, 8, 3, 2], [1, 8, 3, 2], [1, 8, 3, 2], [1, 11, 3, 2], [1, 11, 4, 2], [1, 5, 12, 4, 2], [1, 5, 12, 4, 2], [1, 16, 3, 2], [1, 17, 18, 4, 2]]
tensor2:  [[ 1  5  4  2  0]
 [ 1  5  4  2  0]
 [ 1  5  4  2  0]
 [ 1  5  4  2  0]
 [ 1 13  4  2  0]
 [ 1  9  3  2  0]
 [ 1  9  4  2  0]
 [ 1 14 15  2  0]
 [ 1  6  3  2  0]
 [ 1  6  3  2  0]
 [ 1  6  3  2  0]
 [ 1  7  3  2  0]
 [ 1  7  3  2  0]
 [ 1  7  3  2  0]
 [ 1 10  3  2  0]
 [ 1 10  4  2  0]
 [ 1  8  3  2  0]
 [ 1  8  3  2  0]
 [ 1  8  3  2  0]
 [ 1 11  3  2  0]
 [ 1 11  4  2  0]
 [ 1  5 12  4  2]
 [ 1  5 12  4  2]
 [ 1 16  3  2  0]
 [ 1 17 18  4  2]]

我們也可以查看生成的詞典:

lang_tokenizer.index_word
{1: '<start>',
 2: '<end>',
 3: '!',
 4: '.',
 5: 'go',
 6: 'fire',
 7: 'help',
 8: 'stop',
 9: 'run',
 10: 'jump',
 11: 'wait',
 12: 'on',
 13: 'hi',
 14: 'who',
 15: '?',
 16: 'hello',
 17: 'i',
 18: 'ran'}

1.7 加載數據集

def load_dataset(path, num_examples=None):
    # 創建經過處理後的輸入輸出對
    targ_lang, inp_lang = create_dataset(path, num_examples)

    input_tensor, inp_lang_tokenizer = tokenize(inp_lang)
    target_tensor, targ_lang_tokenizer = tokenize(targ_lang)

    return input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer

num_examples = 30000
input_tensor, target_tensor, inp_lang, targ_lang = load_dataset(path_to_file, num_examples)

1.8 計算目標張量的最大長度

def max_length(tensor):
    return max(len(t) for t in tensor)
    
max_length_targ, max_length_inp = max_length(target_tensor), max_length(input_tensor)

1.9 劃分訓練集和測試集

# 採用 80 - 20 的比例切分訓練集和驗證集
input_tensor_train, input_tensor_val, target_tensor_train, target_tensor_val = train_test_split(input_tensor, target_tensor, test_size=0.2)

# 顯示長度
print(len(input_tensor_train), len(target_tensor_train), len(input_tensor_val), len(target_tensor_val))
24000 24000 6000 6000

1.10 將數字向量轉化爲文本

def convert(lang, tensor):
    for t in tensor:
        if t!=0:
            print ("%d ----> %s" % (t, lang.index_word[t]))

比如我們取 input_tensor_train[0] 數字向量,將其輸入 convert(lang, tensor) 函數:

# input_tensor_train[0] = array([1, 6, 11, 7415, 5, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
print ("Input Language; index to word mapping")
convert(inp_lang, input_tensor_train[0])
Input Language; index to word mapping
1 ----> <start>
6 ----> ¿
11 ----> que
7415 ----> aprendiste
5 ----> ?
2 ----> <end>

1.11 創建一個 tf.data 數據集

BUFFER_SIZE = len(input_tensor_train)
BATCH_SIZE = 64
steps_per_epoch = len(input_tensor_train)//BATCH_SIZE
embedding_dim = 256
units = 1024
vocab_inp_size = len(inp_lang.word_index)+1
vocab_tar_size = len(targ_lang.word_index)+1

dataset = tf.data.Dataset.from_tensor_slices((input_tensor_train, target_tensor_train)).shuffle(BUFFER_SIZE)
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)

我們可以查看每次從數據集中取出的樣本形狀:

example_input_batch, example_target_batch = next(iter(dataset))
example_input_batch.shape, example_target_batch.shape
(TensorShape([64, 16]), TensorShape([64, 11]))

2、編寫模型

在這裏插入圖片描述

2.1 編碼器

首先,我們將每個單詞嵌入 embedding_dim 維的空間向量中,然後輸入 GRU 單元。

class Encoder(tf.keras.Model):
    def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz):
        super(Encoder, self).__init__()
        self.batch_sz = batch_sz
        self.enc_units = enc_units
        self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
        self.gru = tf.keras.layers.GRU(self.enc_units,
                                       return_sequences=True,
                                       return_state=True,
                                       recurrent_initializer='glorot_uniform')

    def call(self, x, hidden):
        x = self.embedding(x)
        output, state = self.gru(x, initial_state = hidden)
        return output, state

    def initialize_hidden_state(self):
        return tf.zeros((self.batch_sz, self.enc_units))

在詞嵌入層中,樣本形狀從 (64, 16) 變成 (64, 16, 256),即每個單詞都變成了一個256維的向量。
在含有1024個神經元的 GRU 層中,樣本形狀又從 (64, 16, 256) 變成 (64, 16, 1024),GRU 層裏的隱藏向量的形狀爲 (64, 1024)。

encoder = Encoder(vocab_inp_size, embedding_dim, units, BATCH_SIZE)

# 樣本輸入
sample_hidden = encoder.initialize_hidden_state()
sample_output, sample_hidden = encoder(example_input_batch, sample_hidden)
print ('Encoder output shape: (batch size, sequence length, units) {}'.format(sample_output.shape))
print ('Encoder Hidden state shape: (batch size, units) {}'.format(sample_hidden.shape))
Encoder output shape: (batch size, sequence length, units) (64, 16, 1024)
Encoder Hidden state shape: (batch size, units) (64, 1024)

2.2 Bahdanau 注意力

相關論文參考:BahdanauAttention

class BahdanauAttention(tf.keras.Model):
    def __init__(self, units):
        super(BahdanauAttention, self).__init__()
        self.W1 = tf.keras.layers.Dense(units)
        self.W2 = tf.keras.layers.Dense(units)
        self.V = tf.keras.layers.Dense(1)

    def call(self, query, values):
        hidden_with_time_axis = tf.expand_dims(query, 1)

        score = self.V(tf.nn.tanh(
            self.W1(values) + self.W2(hidden_with_time_axis)))

        attention_weights = tf.nn.softmax(score, axis=1)

        context_vector = attention_weights * values
        context_vector = tf.reduce_sum(context_vector, axis=1)

        return context_vector, attention_weights

這裏的 values 其實就是編碼器中輸出的結果,經過含10個神經元的 Dense 層之後,其形狀從 (64, 16, 1024) 變成了 (64, 16, 10)。

這裏的 query 其實就是編碼器中輸出的隱層向量,我們需要將其維度從 (64, 1024) 變成 (64, 1, 1024) 來執行之後的加法以計算分數,將增加維度後的向量經過含10個神經元的 Dense 層之後,其形狀從 (64, 1, 1024) 變成了 (64, 1, 10)。

將以上兩個輸出相加得到的形狀爲 (64, 16, 10),經過含1個神經元的 Dense 層之後得到 score,其形狀變成 (64, 16, 1)。

Softmax 默認被應用於最後一個軸,但是這裏我們想將它應用於第二個軸(即 axis=1),因爲分數 (score) 的形狀是 (批大小,最大長度,隱藏層大小)。最大長度是我們的輸入的長度。因爲我們想爲每個輸入分配一個權重,所以 softmax 應該用在這個軸上。經過 Softmax 層之後,得到的注意力權重形狀和 score 的形狀相同,都是 (64, 16, 1)。
【注】Softmax 的不同的軸的計算規則參考:tf.nn.softmax(x, axis)裏axis起什麼作用?

將注意力權重和 values 相乘,得到上下文向量,其形狀爲 (64, 16, 1024)。此向量也就是加了權重的編碼向量。將上下文向量基於第二個軸求和(原因與之前相同),得到最終的上下文向量,其形狀爲 (64, 1024)。

attention_layer = BahdanauAttention(10)
attention_result, attention_weights = attention_layer(sample_hidden, sample_output)

print("Attention result shape: (batch size, units) {}".format(attention_result.shape))
print("Attention weights shape: (batch_size, sequence_length, 1) {}".format(attention_weights.shape))
Attention result shape: (batch size, units) (64, 1024)
Attention weights shape: (batch_size, sequence_length, 1) (64, 16, 1)

2.3 解碼器

class Decoder(tf.keras.Model):
    def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz):
        super(Decoder, self).__init__()
        self.batch_sz = batch_sz
        self.dec_units = dec_units
        self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
        self.gru = tf.keras.layers.GRU(self.dec_units,
                                       return_sequences=True,
                                       return_state=True,
                                       recurrent_initializer='glorot_uniform')
        self.fc = tf.keras.layers.Dense(vocab_size)
        
        self.attention = BahdanauAttention(self.dec_units)

    def call(self, x, hidden, enc_output):
        context_vector, attention_weights = self.attention(hidden, enc_output)

        x = self.embedding(x)

        x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
        
        output, state = self.gru(x)
        
        output = tf.reshape(output, (-1, output.shape[2]))

        x = self.fc(output)

        return x, state, attention_weights

在解碼器中,首先利用注意力層獲得上下文向量 (64, 1024) 和注意力權重 (64, 16, 1)。

將輸入通過詞嵌入層,其形狀從 (64, 1) 變成了 (64, 1, 256)。

在上下文向量上添加一個維度,使它的形狀變成 (64, 1, 1024),然後和通過詞嵌入層的輸入合併,得到形狀爲 (64, 1, 1280) 的向量。

將合併後的向量傳送到 GRU,得到輸出向量 (64, 1, 1024) 和隱藏狀態 (64, 1024)。

將輸出向量以最後一維的維數不變的形式轉換成二維數組,即形狀爲 (64*1, 1024)。

最後,將這個向量通過一個含有詞彙表大小 (4935) 個神經元的全連接層,得到最終輸出 (64, 4935)。

decoder = Decoder(vocab_tar_size, embedding_dim, units, BATCH_SIZE)

sample_decoder_output, _, _ = decoder(tf.random.uniform((64, 1)),
                                      sample_hidden, sample_output)

print ('Decoder output shape: (batch_size, vocab size) {}'.format(sample_decoder_output.shape))
Decoder output shape: (batch_size, vocab size) (64, 4935)

總結如圖所示:
在這裏插入圖片描述

3、定義優化器和損失函數

每次輸入解碼器的向量是一個批次中所有文本中的一個單詞,即每次輸入的向量形狀都是 (64, 1)。

當輸入的向量中出現0元素,說明這個元素所在的文本已經結束了,這個文本不再參與損失的計算,所以在計算損失的時候,要使用掩膜處理,將已結束文本的損失置零。

optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
    from_logits=True, reduction='none')

def loss_function(real, pred):
    mask = tf.math.logical_not(tf.math.equal(real, 0))
    loss_ = loss_object(real, pred)

    mask = tf.cast(mask, dtype=loss_.dtype)
    loss_ *= mask

    return tf.reduce_mean(loss_)

4、訓練

4.1 梯度下降

  • a. 將輸入傳送至編碼器,編碼器返回編碼器輸出編碼器隱藏層狀態
  • b. 將編碼器輸出、編碼器隱藏層狀態和解碼器輸入(即 ‘<start>’ )傳送至解碼器。
  • c. 解碼器返回預測解碼器隱藏層狀態
  • d. 解碼器隱藏層狀態被傳送回模型,預測被用於計算損失。
  • e. 使用教師強制(teacher forcing)決定解碼器的下一個輸入。
    PS:教師強制是將目標詞作爲下一個輸入傳送至解碼器的技術。
  • f. 最後一步是計算梯度,並將其應用於優化器和反向傳播。
@tf.function
def train_step(inp, targ, enc_hidden):
    loss = 0

    with tf.GradientTape() as tape:
        enc_output, enc_hidden = encoder(inp, enc_hidden)

        dec_hidden = enc_hidden

        dec_input = tf.expand_dims([targ_lang.word_index['<start>']] * BATCH_SIZE, 1)
        
        for t in range(1, targ.shape[1]):
            predictions, dec_hidden, _ = decoder(dec_input, dec_hidden, enc_output)

            loss += loss_function(targ[:, t], predictions)

            dec_input = tf.expand_dims(targ[:, t], 1)

    batch_loss = (loss / int(targ.shape[1]))

    variables = encoder.trainable_variables + decoder.trainable_variables

    gradients = tape.gradient(loss, variables)

    optimizer.apply_gradients(zip(gradients, variables))

    return batch_loss

4.2 訓練過程

EPOCHS = 10

for epoch in range(EPOCHS):
    start = time.time()

    enc_hidden = encoder.initialize_hidden_state()
    total_loss = 0

    for (batch, (inp, targ)) in enumerate(dataset.take(steps_per_epoch)):
        batch_loss = train_step(inp, targ, enc_hidden)
        total_loss += batch_loss

        if batch % 100 == 0:
            print('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1,
                                                         batch,
                                                         batch_loss.numpy()))

    print('Epoch {} Loss {:.4f}'.format(epoch + 1,
                                      total_loss / steps_per_epoch))
    print('Time taken for 1 epoch {} sec\n'.format(time.time() - start))

5、預測

5.1 預測函數

  • 1、用 preprocess_sentence 函數處理ascii文本;
  • 2、將文本轉換成數字向量,並進行填充;
  • 3、將數字向量轉化爲張量;
  • 4、初始化編碼器的隱藏狀態;
  • 5、將數字張量和初始化隱藏狀態輸入編碼器;
  • 6、初始化解碼器輸入;
  • 7、逐字輸入解碼器,得到預測的文本。
def evaluate(sentence):
    attention_plot = np.zeros((max_length_targ, max_length_inp))

    sentence = preprocess_sentence(sentence)

    inputs = [inp_lang.word_index[i] for i in sentence.split(' ')]
    inputs = tf.keras.preprocessing.sequence.pad_sequences([inputs],
                                                           maxlen=max_length_inp,
                                                           padding='post')
    inputs = tf.convert_to_tensor(inputs)

    result = ''

    hidden = [tf.zeros((1, units))]
    enc_out, enc_hidden = encoder(inputs, hidden)

    dec_hidden = enc_hidden
    dec_input = tf.expand_dims([targ_lang.word_index['<start>']], 0)

    for t in range(max_length_targ):
        predictions, dec_hidden, attention_weights = decoder(dec_input,
                                                             dec_hidden,
                                                             enc_out)

        # 存儲注意力權重以便後面製圖
        attention_weights = tf.reshape(attention_weights, (-1, ))
        attention_plot[t] = attention_weights.numpy()

        predicted_id = tf.argmax(predictions[0]).numpy()

        result += targ_lang.index_word[predicted_id] + ' '

        if targ_lang.index_word[predicted_id] == '<end>':
            return result, sentence, attention_plot

        # 預測的 ID 被輸送回模型
        dec_input = tf.expand_dims([predicted_id], 0)

    return result, sentence, attention_plot

5.2 注意力權重製圖函數

def plot_attention(attention, sentence, predicted_sentence):
    fig = plt.figure(figsize=(10,10))
    ax = fig.add_subplot(1, 1, 1)
    ax.matshow(attention, cmap='viridis')

    fontdict = {'fontsize': 14}

    ax.set_xticklabels([''] + sentence, fontdict=fontdict, rotation=90)
    ax.set_yticklabels([''] + predicted_sentence, fontdict=fontdict)

    ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
    ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

    plt.show()

5.3 翻譯函數

def translate(sentence):
    result, sentence, attention_plot = evaluate(sentence)

    print('Input: %s' % (sentence))
    print('Predicted translation: {}'.format(result))

    attention_plot = attention_plot[:len(result.split(' ')), :len(sentence.split(' '))]
    plot_attention(attention_plot, sentence.split(' '), result.split(' '))
translate(u'hace mucho frio aqui.')
Input: <start> hace mucho frio aqui . <end>
Predicted translation: it s very cold here . <end> 

在這裏插入圖片描述

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