• 教學 >
  • 從頭開始的 NLP:使用序列到序列網路和注意力機制進行翻譯
捷徑

從頭開始的 NLP:使用序列到序列網路和注意力機制進行翻譯

建立於:2017 年 3 月 24 日 | 最後更新:2024 年 10 月 21 日 | 最後驗證:2024 年 11 月 05 日

作者Sean Robertson

本教學是三部分系列的一部分

這是關於從頭開始的 NLP的第三個也是最後一個教學,我們將編寫自己的類別和函數來預處理資料,以執行我們的 NLP 建模任務。

在這個專案中,我們將教導一個神經網路將法語翻譯成英語。

[KEY: > input, = target, < output]

> il est en train de peindre un tableau .
= he is painting a picture .
< he is painting a picture .

> pourquoi ne pas essayer ce vin delicieux ?
= why not try that delicious wine ?
< why not try that delicious wine ?

> elle n est pas poete mais romanciere .
= she is not a poet but a novelist .
< she not not a poet but a novelist .

> vous etes trop maigre .
= you re too skinny .
< you re all alone .

… 達到不同程度的成功。

這得益於序列到序列網路這個簡單而強大的想法,其中兩個循環神經網路協同工作,將一個序列轉換為另一個序列。 編碼器網路將輸入序列壓縮為向量,而解碼器網路將該向量展開為一個新序列。

為了改進這個模型,我們將使用注意力機制,它讓解碼器學會關注輸入序列的特定範圍。

建議閱讀

我假設您至少已安裝 PyTorch,了解 Python 並且了解張量

了解序列到序列網路及其運作方式也很有用

您還會發現以前關於從頭開始的 NLP:使用字元級 RNN 對名稱進行分類從頭開始的 NLP:使用字元級 RNN 產生名稱的教學很有幫助,因為這些概念分別與編碼器和解碼器模型非常相似。

需求

from __future__ import unicode_literals, print_function, division
from io import open
import unicodedata
import re
import random

import torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as F

import numpy as np
from torch.utils.data import TensorDataset, DataLoader, RandomSampler

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

載入資料檔案

這個專案的資料是一組數千個英語到法語的翻譯對。

Open Data Stack Exchange 上的這個問題將我指向開放翻譯網站https://tatoeba.org/,該網站可在https://tatoeba.org/eng/downloads下載 - 更好的是,有人額外完成了將語言對拆分為單獨文字檔案的工作: https://www.manythings.org/anki/

英語到法語的配對太大而無法包含在儲存庫中,因此請在繼續之前下載到 data/eng-fra.txt。 該檔案是以 Tab 分隔的翻譯對清單

I am cold.    J'ai froid.

注意

這裡下載資料並將其解壓縮到目前目錄。

與字元級 RNN 教學中使用的字元編碼類似,我們將使用 one-hot 向量(或巨大的零向量,除了單個一(位於單字的索引處))來表示語言中的每個單字。 與語言中可能存在的數十個字元相比,單字要多得多,因此編碼向量要大得多。 但是,我們會稍微作弊並修剪資料,以便每個語言僅使用幾千個單字。

我們需要每個單字的唯一索引,以便稍後用作網路的輸入和目標。 為了追蹤所有這些,我們將使用一個名為 Lang 的輔助類別,它具有單字 → 索引 (word2index) 和索引 → 單字 (index2word) 字典,以及每個單字的計數 word2count,稍後將用於取代稀有單字。

SOS_token = 0
EOS_token = 1

class Lang:
    def __init__(self, name):
        self.name = name
        self.word2index = {}
        self.word2count = {}
        self.index2word = {0: "SOS", 1: "EOS"}
        self.n_words = 2  # Count SOS and EOS

    def addSentence(self, sentence):
        for word in sentence.split(' '):
            self.addWord(word)

    def addWord(self, word):
        if word not in self.word2index:
            self.word2index[word] = self.n_words
            self.word2count[word] = 1
            self.index2word[self.n_words] = word
            self.n_words += 1
        else:
            self.word2count[word] += 1

這些檔案都是 Unicode 格式,為了簡化,我們將 Unicode 字元轉換為 ASCII,將所有內容轉換為小寫,並修剪大部分標點符號。

# Turn a Unicode string to plain ASCII, thanks to
# https://stackoverflow.com/a/518232/2809427
def unicodeToAscii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
    )

# Lowercase, trim, and remove non-letter characters
def normalizeString(s):
    s = unicodeToAscii(s.lower().strip())
    s = re.sub(r"([.!?])", r" \1", s)
    s = re.sub(r"[^a-zA-Z!?]+", r" ", s)
    return s.strip()

為了讀取資料檔案,我們會將檔案拆分成多行,然後將每行拆分成詞組對。這些檔案都是 英文 → 其他語言 的格式,因此如果我們要從 其他語言 → 英文 翻譯,我加入了 reverse 旗標來反轉詞組對。

def readLangs(lang1, lang2, reverse=False):
    print("Reading lines...")

    # Read the file and split into lines
    lines = open('data/%s-%s.txt' % (lang1, lang2), encoding='utf-8').\
        read().strip().split('\n')

    # Split every line into pairs and normalize
    pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]

    # Reverse pairs, make Lang instances
    if reverse:
        pairs = [list(reversed(p)) for p in pairs]
        input_lang = Lang(lang2)
        output_lang = Lang(lang1)
    else:
        input_lang = Lang(lang1)
        output_lang = Lang(lang2)

    return input_lang, output_lang, pairs

由於範例句子很多,而且我們想要快速訓練出東西,我們會將資料集修剪成相對簡短且簡單的句子。這裡的最大長度是 10 個詞(包含結尾標點符號),並且我們篩選出翻譯成 “I am” 或 “He is” 等形式的句子(考慮到先前已替換掉的省略符號)。

MAX_LENGTH = 10

eng_prefixes = (
    "i am ", "i m ",
    "he is", "he s ",
    "she is", "she s ",
    "you are", "you re ",
    "we are", "we re ",
    "they are", "they re "
)

def filterPair(p):
    return len(p[0].split(' ')) < MAX_LENGTH and \
        len(p[1].split(' ')) < MAX_LENGTH and \
        p[1].startswith(eng_prefixes)


def filterPairs(pairs):
    return [pair for pair in pairs if filterPair(pair)]

準備資料的完整流程如下:

  • 讀取文字檔案並拆分成行,將行拆分成詞組對

  • 正規化文字,依長度和內容篩選

  • 從詞組對中的句子建立單字列表

def prepareData(lang1, lang2, reverse=False):
    input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)
    print("Read %s sentence pairs" % len(pairs))
    pairs = filterPairs(pairs)
    print("Trimmed to %s sentence pairs" % len(pairs))
    print("Counting words...")
    for pair in pairs:
        input_lang.addSentence(pair[0])
        output_lang.addSentence(pair[1])
    print("Counted words:")
    print(input_lang.name, input_lang.n_words)
    print(output_lang.name, output_lang.n_words)
    return input_lang, output_lang, pairs

input_lang, output_lang, pairs = prepareData('eng', 'fra', True)
print(random.choice(pairs))
Reading lines...
Read 135842 sentence pairs
Trimmed to 11445 sentence pairs
Counting words...
Counted words:
fra 4601
eng 2991
['tu preches une convaincue', 'you re preaching to the choir']

Seq2Seq 模型

遞迴神經網路 (Recurrent Neural Network, RNN) 是一種在序列上運作,並將自身輸出作為後續步驟輸入的網路。

序列到序列網路 (Sequence to Sequence network, seq2seq network),或稱 編碼器-解碼器網路 (Encoder Decoder network),是一種由兩個 RNN 組成的模型,分別稱為編碼器 (encoder) 和解碼器 (decoder)。編碼器讀取輸入序列並輸出單一向量,而解碼器讀取該向量以產生輸出序列。

與使用單一 RNN 的序列預測不同,在單一 RNN 中,每個輸入對應一個輸出,seq2seq 模型使我們擺脫了序列長度和順序的限制,這使得它非常適合兩種語言之間的翻譯。

考慮以下句子 Je ne suis pas le chat noirI am not the black cat。輸入句子中的大多數單字在輸出句子中都有直接翻譯,但順序略有不同,例如 chat noirblack cat。由於 ne/pas 的結構,輸入句子中還有一個額外的單字。直接從輸入單字的序列產生正確的翻譯將會很困難。

使用 seq2seq 模型,編碼器會建立一個單一向量,在理想情況下,該向量將輸入序列的「意義」編碼成一個單一向量 — 在某個 N 維句子空間中的單一點。

編碼器

seq2seq 網路的編碼器是一個 RNN,它會為輸入句子中的每個單字輸出一些值。對於每個輸入單字,編碼器會輸出一個向量和一個隱藏狀態,並使用該隱藏狀態來處理下一個輸入單字。

class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size, dropout_p=0.1):
        super(EncoderRNN, self).__init__()
        self.hidden_size = hidden_size

        self.embedding = nn.Embedding(input_size, hidden_size)
        self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
        self.dropout = nn.Dropout(dropout_p)

    def forward(self, input):
        embedded = self.dropout(self.embedding(input))
        output, hidden = self.gru(embedded)
        return output, hidden

解碼器

解碼器是另一個 RNN,它接收編碼器的輸出向量,並輸出單字序列以產生翻譯。

簡單解碼器

在最簡單的 seq2seq 解碼器中,我們僅使用編碼器的最後輸出。此最後輸出有時稱為上下文向量,因為它編碼了整個序列的上下文。此上下文向量用作解碼器的初始隱藏狀態。

在解碼的每個步驟中,解碼器都會收到一個輸入符記和一個隱藏狀態。初始輸入符記是字串開始符記 <SOS>,而第一個隱藏狀態是上下文向量(編碼器的最後一個隱藏狀態)。

class DecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size):
        super(DecoderRNN, self).__init__()
        self.embedding = nn.Embedding(output_size, hidden_size)
        self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
        self.out = nn.Linear(hidden_size, output_size)

    def forward(self, encoder_outputs, encoder_hidden, target_tensor=None):
        batch_size = encoder_outputs.size(0)
        decoder_input = torch.empty(batch_size, 1, dtype=torch.long, device=device).fill_(SOS_token)
        decoder_hidden = encoder_hidden
        decoder_outputs = []

        for i in range(MAX_LENGTH):
            decoder_output, decoder_hidden  = self.forward_step(decoder_input, decoder_hidden)
            decoder_outputs.append(decoder_output)

            if target_tensor is not None:
                # Teacher forcing: Feed the target as the next input
                decoder_input = target_tensor[:, i].unsqueeze(1) # Teacher forcing
            else:
                # Without teacher forcing: use its own predictions as the next input
                _, topi = decoder_output.topk(1)
                decoder_input = topi.squeeze(-1).detach()  # detach from history as input

        decoder_outputs = torch.cat(decoder_outputs, dim=1)
        decoder_outputs = F.log_softmax(decoder_outputs, dim=-1)
        return decoder_outputs, decoder_hidden, None # We return `None` for consistency in the training loop

    def forward_step(self, input, hidden):
        output = self.embedding(input)
        output = F.relu(output)
        output, hidden = self.gru(output, hidden)
        output = self.out(output)
        return output, hidden

我鼓勵您訓練並觀察此模型的結果,但為了節省空間,我們將直接深入核心並介紹注意力機制。

注意力解碼器

如果僅在編碼器和解碼器之間傳遞上下文向量,則該單一向量將承擔編碼整個句子的重擔。

注意力機制允許解碼器網路在解碼器自身輸出的每個步驟中「關注」編碼器輸出的不同部分。首先,我們計算一組注意力權重。這些權重將乘以編碼器輸出向量以產生加權組合。結果(在程式碼中稱為 attn_applied)應包含有關輸入序列的特定部分的信息,從而幫助解碼器選擇正確的輸出單字。

注意力權重的計算是透過另一個前饋層 attn 完成的,使用解碼器的輸入和隱藏狀態作為輸入。由於訓練資料中存在各種大小的句子,為了實際建立和訓練此層,我們必須選擇一個最大句子長度(輸入長度,對於編碼器輸出),它可以應用於該長度。最大長度的句子將使用所有注意力權重,而較短的句子將僅使用前幾個權重。

Bahdanau 注意力,也稱為加法注意力,是序列到序列模型中常用的注意力機制,特別是在神經機器翻譯任務中。它由 Bahdanau 等人在他們題為 Neural Machine Translation by Jointly Learning to Align and Translate 的論文中提出。這種注意力機制採用學習到的對齊模型來計算編碼器和解碼器隱藏狀態之間的注意力分數。它利用前饋神經網路來計算對齊分數。

但是,還有其他可用的注意力機制,例如 Luong 注意力,它透過取解碼器隱藏狀態和編碼器隱藏狀態之間的點積來計算注意力分數。它不涉及 Bahdanau 注意力中使用的非線性轉換。

在本教學課程中,我們將使用 Bahdanau 注意力。但是,探索修改注意力機制以使用 Luong 注意力將是一個有價值的練習。

class BahdanauAttention(nn.Module):
    def __init__(self, hidden_size):
        super(BahdanauAttention, self).__init__()
        self.Wa = nn.Linear(hidden_size, hidden_size)
        self.Ua = nn.Linear(hidden_size, hidden_size)
        self.Va = nn.Linear(hidden_size, 1)

    def forward(self, query, keys):
        scores = self.Va(torch.tanh(self.Wa(query) + self.Ua(keys)))
        scores = scores.squeeze(2).unsqueeze(1)

        weights = F.softmax(scores, dim=-1)
        context = torch.bmm(weights, keys)

        return context, weights

class AttnDecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size, dropout_p=0.1):
        super(AttnDecoderRNN, self).__init__()
        self.embedding = nn.Embedding(output_size, hidden_size)
        self.attention = BahdanauAttention(hidden_size)
        self.gru = nn.GRU(2 * hidden_size, hidden_size, batch_first=True)
        self.out = nn.Linear(hidden_size, output_size)
        self.dropout = nn.Dropout(dropout_p)

    def forward(self, encoder_outputs, encoder_hidden, target_tensor=None):
        batch_size = encoder_outputs.size(0)
        decoder_input = torch.empty(batch_size, 1, dtype=torch.long, device=device).fill_(SOS_token)
        decoder_hidden = encoder_hidden
        decoder_outputs = []
        attentions = []

        for i in range(MAX_LENGTH):
            decoder_output, decoder_hidden, attn_weights = self.forward_step(
                decoder_input, decoder_hidden, encoder_outputs
            )
            decoder_outputs.append(decoder_output)
            attentions.append(attn_weights)

            if target_tensor is not None:
                # Teacher forcing: Feed the target as the next input
                decoder_input = target_tensor[:, i].unsqueeze(1) # Teacher forcing
            else:
                # Without teacher forcing: use its own predictions as the next input
                _, topi = decoder_output.topk(1)
                decoder_input = topi.squeeze(-1).detach()  # detach from history as input

        decoder_outputs = torch.cat(decoder_outputs, dim=1)
        decoder_outputs = F.log_softmax(decoder_outputs, dim=-1)
        attentions = torch.cat(attentions, dim=1)

        return decoder_outputs, decoder_hidden, attentions


    def forward_step(self, input, hidden, encoder_outputs):
        embedded =  self.dropout(self.embedding(input))

        query = hidden.permute(1, 0, 2)
        context, attn_weights = self.attention(query, encoder_outputs)
        input_gru = torch.cat((embedded, context), dim=2)

        output, hidden = self.gru(input_gru, hidden)
        output = self.out(output)

        return output, hidden, attn_weights

注意

還有其他形式的注意力機制可以透過使用相對位置方法來解決長度限制。請閱讀 Effective Approaches to Attention-based Neural Machine Translation 中的「局部注意力」。

訓練

準備訓練資料

為了進行訓練,對於每個詞組對,我們都需要一個輸入張量(輸入句子中單字的索引)和一個目標張量(目標句子中單字的索引)。在建立這些向量時,我們會將 EOS 符記附加到兩個序列。

def indexesFromSentence(lang, sentence):
    return [lang.word2index[word] for word in sentence.split(' ')]

def tensorFromSentence(lang, sentence):
    indexes = indexesFromSentence(lang, sentence)
    indexes.append(EOS_token)
    return torch.tensor(indexes, dtype=torch.long, device=device).view(1, -1)

def tensorsFromPair(pair):
    input_tensor = tensorFromSentence(input_lang, pair[0])
    target_tensor = tensorFromSentence(output_lang, pair[1])
    return (input_tensor, target_tensor)

def get_dataloader(batch_size):
    input_lang, output_lang, pairs = prepareData('eng', 'fra', True)

    n = len(pairs)
    input_ids = np.zeros((n, MAX_LENGTH), dtype=np.int32)
    target_ids = np.zeros((n, MAX_LENGTH), dtype=np.int32)

    for idx, (inp, tgt) in enumerate(pairs):
        inp_ids = indexesFromSentence(input_lang, inp)
        tgt_ids = indexesFromSentence(output_lang, tgt)
        inp_ids.append(EOS_token)
        tgt_ids.append(EOS_token)
        input_ids[idx, :len(inp_ids)] = inp_ids
        target_ids[idx, :len(tgt_ids)] = tgt_ids

    train_data = TensorDataset(torch.LongTensor(input_ids).to(device),
                               torch.LongTensor(target_ids).to(device))

    train_sampler = RandomSampler(train_data)
    train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size)
    return input_lang, output_lang, train_dataloader

訓練模型

為了訓練,我們會將輸入的句子透過編碼器,並追蹤每一個輸出和最新的隱藏狀態。接著,解碼器會收到 <SOS> token 作為它的第一個輸入,以及編碼器的最後一個隱藏狀態作為它的第一個隱藏狀態。

“Teacher forcing”(強制教師)是指使用真實的目標輸出作為每一個下一步的輸入,而不是使用解碼器的猜測作為下一步輸入的概念。使用 teacher forcing 會使其收斂得更快,但是當訓練好的網路被使用時,它可能會表現出不穩定性

你可以觀察到使用了 teacher forcing 的網路輸出,它們的文法連貫,但是會偏離正確的翻譯很遠 - 直觀上,它已經學會了呈現輸出文法,並且一旦教師告訴它前幾個單字,它就能「理解」含義,但它並沒有真正學會如何從一開始就從翻譯建立句子。

由於 PyTorch 的 autograd 給了我們自由,我們可以透過簡單的 if 語句隨機選擇是否使用 teacher forcing。提高 teacher_forcing_ratio 來更多地使用它。

def train_epoch(dataloader, encoder, decoder, encoder_optimizer,
          decoder_optimizer, criterion):

    total_loss = 0
    for data in dataloader:
        input_tensor, target_tensor = data

        encoder_optimizer.zero_grad()
        decoder_optimizer.zero_grad()

        encoder_outputs, encoder_hidden = encoder(input_tensor)
        decoder_outputs, _, _ = decoder(encoder_outputs, encoder_hidden, target_tensor)

        loss = criterion(
            decoder_outputs.view(-1, decoder_outputs.size(-1)),
            target_tensor.view(-1)
        )
        loss.backward()

        encoder_optimizer.step()
        decoder_optimizer.step()

        total_loss += loss.item()

    return total_loss / len(dataloader)

這是一個輔助函式,用於在給定目前時間和進度百分比的情況下,印出經過的時間和估計的剩餘時間。

import time
import math

def asMinutes(s):
    m = math.floor(s / 60)
    s -= m * 60
    return '%dm %ds' % (m, s)

def timeSince(since, percent):
    now = time.time()
    s = now - since
    es = s / (percent)
    rs = es - s
    return '%s (- %s)' % (asMinutes(s), asMinutes(rs))

整個訓練過程看起來像這樣

  • 啟動計時器

  • 初始化優化器和損失函數

  • 建立訓練配對集

  • 開始空的損失陣列,用於繪圖

然後我們多次呼叫 train,並且偶爾印出進度(範例百分比、目前時間、估計時間)和平均損失。

def train(train_dataloader, encoder, decoder, n_epochs, learning_rate=0.001,
               print_every=100, plot_every=100):
    start = time.time()
    plot_losses = []
    print_loss_total = 0  # Reset every print_every
    plot_loss_total = 0  # Reset every plot_every

    encoder_optimizer = optim.Adam(encoder.parameters(), lr=learning_rate)
    decoder_optimizer = optim.Adam(decoder.parameters(), lr=learning_rate)
    criterion = nn.NLLLoss()

    for epoch in range(1, n_epochs + 1):
        loss = train_epoch(train_dataloader, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion)
        print_loss_total += loss
        plot_loss_total += loss

        if epoch % print_every == 0:
            print_loss_avg = print_loss_total / print_every
            print_loss_total = 0
            print('%s (%d %d%%) %.4f' % (timeSince(start, epoch / n_epochs),
                                        epoch, epoch / n_epochs * 100, print_loss_avg))

        if epoch % plot_every == 0:
            plot_loss_avg = plot_loss_total / plot_every
            plot_losses.append(plot_loss_avg)
            plot_loss_total = 0

    showPlot(plot_losses)

繪製結果

繪圖是使用 matplotlib 完成的,使用在訓練時儲存的損失值陣列 plot_losses

import matplotlib.pyplot as plt
plt.switch_backend('agg')
import matplotlib.ticker as ticker
import numpy as np

def showPlot(points):
    plt.figure()
    fig, ax = plt.subplots()
    # this locator puts ticks at regular intervals
    loc = ticker.MultipleLocator(base=0.2)
    ax.yaxis.set_major_locator(loc)
    plt.plot(points)

評估

評估與訓練大致相同,但沒有目標,因此我們只是在每個步驟將解碼器的預測回饋給自身。每次它預測一個單字時,我們都會將它添加到輸出字串中,如果它預測到 EOS token,我們就會在那裡停止。我們也儲存解碼器的注意力輸出,以便稍後顯示。

def evaluate(encoder, decoder, sentence, input_lang, output_lang):
    with torch.no_grad():
        input_tensor = tensorFromSentence(input_lang, sentence)

        encoder_outputs, encoder_hidden = encoder(input_tensor)
        decoder_outputs, decoder_hidden, decoder_attn = decoder(encoder_outputs, encoder_hidden)

        _, topi = decoder_outputs.topk(1)
        decoded_ids = topi.squeeze()

        decoded_words = []
        for idx in decoded_ids:
            if idx.item() == EOS_token:
                decoded_words.append('<EOS>')
                break
            decoded_words.append(output_lang.index2word[idx.item()])
    return decoded_words, decoder_attn

我們可以評估來自訓練集的隨機句子,並印出輸入、目標和輸出,以做出一些主觀的品質判斷

def evaluateRandomly(encoder, decoder, n=10):
    for i in range(n):
        pair = random.choice(pairs)
        print('>', pair[0])
        print('=', pair[1])
        output_words, _ = evaluate(encoder, decoder, pair[0], input_lang, output_lang)
        output_sentence = ' '.join(output_words)
        print('<', output_sentence)
        print('')

訓練和評估

有了所有這些輔助函式(看起來像是額外的工作,但它使執行多個實驗更容易),我們實際上可以初始化一個網路並開始訓練。

請記住,輸入句子經過了大量過濾。對於這個小型資料集,我們可以使用相對較小的網路,包含 256 個隱藏節點和單個 GRU 層。在 MacBook CPU 上大約 40 分鐘後,我們將獲得一些合理的結果。

注意

如果你執行此 notebook,你可以訓練、中斷 kernel、評估,然後稍後繼續訓練。註解掉初始化編碼器和解碼器的行,然後再次執行 trainIters

hidden_size = 128
batch_size = 32

input_lang, output_lang, train_dataloader = get_dataloader(batch_size)

encoder = EncoderRNN(input_lang.n_words, hidden_size).to(device)
decoder = AttnDecoderRNN(hidden_size, output_lang.n_words).to(device)

train(train_dataloader, encoder, decoder, 80, print_every=5, plot_every=5)
  • seq2seq translation tutorial
  • seq2seq translation tutorial
Reading lines...
Read 135842 sentence pairs
Trimmed to 11445 sentence pairs
Counting words...
Counted words:
fra 4601
eng 2991
0m 32s (- 8m 1s) (5 6%) 1.5304
1m 3s (- 7m 27s) (10 12%) 0.6776
1m 35s (- 6m 54s) (15 18%) 0.3528
2m 7s (- 6m 22s) (20 25%) 0.1948
2m 38s (- 5m 48s) (25 31%) 0.1203
3m 9s (- 5m 15s) (30 37%) 0.0834
3m 40s (- 4m 42s) (35 43%) 0.0637
4m 10s (- 4m 10s) (40 50%) 0.0521
4m 41s (- 3m 39s) (45 56%) 0.0452
5m 12s (- 3m 7s) (50 62%) 0.0404
5m 43s (- 2m 36s) (55 68%) 0.0374
6m 13s (- 2m 4s) (60 75%) 0.0346
6m 44s (- 1m 33s) (65 81%) 0.0326
7m 16s (- 1m 2s) (70 87%) 0.0314
7m 48s (- 0m 31s) (75 93%) 0.0299
8m 20s (- 0m 0s) (80 100%) 0.0291

將 dropout 層設定為 eval 模式

encoder.eval()
decoder.eval()
evaluateRandomly(encoder, decoder)
> il est si mignon !
= he s so cute
< he s so cute <EOS>

> je vais me baigner
= i m going to take a bath
< i m going to take a bath <EOS>

> c est un travailleur du batiment
= he s a construction worker
< he s a construction worker <EOS>

> je suis representant de commerce pour notre societe
= i m a salesman for our company
< i m a salesman for our company <EOS>

> vous etes grande
= you re big
< you are big <EOS>

> tu n es pas normale
= you re not normal
< you re not normal <EOS>

> je n en ai pas encore fini avec vous
= i m not done with you yet
< i m not done with you yet <EOS>

> je suis desole pour ce malentendu
= i m sorry about my mistake
< i m sorry about my mistake <EOS>

> nous ne sommes pas impressionnes
= we re not impressed
< we re not impressed <EOS>

> tu as la confiance de tous
= you are trusted by every one of us
< you are trusted by every one of us <EOS>

視覺化注意力

注意力機制的一個有用的屬性是其高度可解釋的輸出。因為它用於權衡輸入序列的特定編碼器輸出,我們可以想像在每個時間步驟,網路最關注的位置。

你可以簡單地執行 plt.matshow(attentions) 來查看注意力輸出顯示為矩陣。為了獲得更好的觀看體驗,我們將額外花費精力添加軸和標籤

def showAttention(input_sentence, output_words, attentions):
    fig = plt.figure()
    ax = fig.add_subplot(111)
    cax = ax.matshow(attentions.cpu().numpy(), cmap='bone')
    fig.colorbar(cax)

    # Set up axes
    ax.set_xticklabels([''] + input_sentence.split(' ') +
                       ['<EOS>'], rotation=90)
    ax.set_yticklabels([''] + output_words)

    # Show label at every tick
    ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
    ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

    plt.show()


def evaluateAndShowAttention(input_sentence):
    output_words, attentions = evaluate(encoder, decoder, input_sentence, input_lang, output_lang)
    print('input =', input_sentence)
    print('output =', ' '.join(output_words))
    showAttention(input_sentence, output_words, attentions[0, :len(output_words), :])


evaluateAndShowAttention('il n est pas aussi grand que son pere')

evaluateAndShowAttention('je suis trop fatigue pour conduire')

evaluateAndShowAttention('je suis desole si c est une question idiote')

evaluateAndShowAttention('je suis reellement fiere de vous')
  • seq2seq translation tutorial
  • seq2seq translation tutorial
  • seq2seq translation tutorial
  • seq2seq translation tutorial
input = il n est pas aussi grand que son pere
output = he is not as tall as his father <EOS>
/var/lib/workspace/intermediate_source/seq2seq_translation_tutorial.py:827: UserWarning:

set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.

/var/lib/workspace/intermediate_source/seq2seq_translation_tutorial.py:829: UserWarning:

set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.

input = je suis trop fatigue pour conduire
output = i m too tired to drive <EOS>
/var/lib/workspace/intermediate_source/seq2seq_translation_tutorial.py:827: UserWarning:

set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.

/var/lib/workspace/intermediate_source/seq2seq_translation_tutorial.py:829: UserWarning:

set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.

input = je suis desole si c est une question idiote
output = i m sorry if this is a stupid question <EOS>
/var/lib/workspace/intermediate_source/seq2seq_translation_tutorial.py:827: UserWarning:

set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.

/var/lib/workspace/intermediate_source/seq2seq_translation_tutorial.py:829: UserWarning:

set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.

input = je suis reellement fiere de vous
output = i m really proud of you guys <EOS>
/var/lib/workspace/intermediate_source/seq2seq_translation_tutorial.py:827: UserWarning:

set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.

/var/lib/workspace/intermediate_source/seq2seq_translation_tutorial.py:829: UserWarning:

set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.

練習

  • 嘗試使用不同的資料集

    • 另一種語言配對

    • 人類 → 機器(例如,IOT 命令)

    • 聊天 → 回應

    • 問題 → 答案

  • 使用預訓練的詞嵌入(例如 word2vecGloVe)替換嵌入

  • 嘗試使用更多層、更多隱藏單元和更多句子。比較訓練時間和結果。

  • 如果你使用一個翻譯檔案,其中配對有兩個相同的短語(I am test \t I am test),你可以將其用作自動編碼器。嘗試這個

    • 訓練為自動編碼器

    • 僅儲存編碼器網路

    • 從那裡訓練一個新的解碼器用於翻譯

腳本的總運行時間: ( 8 分鐘 28.541 秒)

由 Sphinx-Gallery 產生的圖庫

文件

存取 PyTorch 的完整開發者文件

檢視文件

教學

取得適合初學者和進階開發者的深入教學課程

檢視教學課程

資源

尋找開發資源並獲得問題解答

檢視資源