# 10.12 機器翻譯
機器翻譯是指將一段文本從一種語言自動翻譯到另一種語言。因為一段文本序列在不同語言中的長度不一定相同,所以我們使用機器翻譯為例來介紹編碼器—解碼器和注意力機制的應用。
## 10.12.1 讀取和預處理數據
我們先定義一些特殊符號。其中“<pad>”(padding)符號用來添加在較短序列后,直到每個序列等長,而“<bos>”和“<eos>”符號分別表示序列的開始和結束。
``` python
import collections
import os
import io
import math
import torch
from torch import nn
import torch.nn.functional as F
import torchtext.vocab as Vocab
import torch.utils.data as Data
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
```
接著定義兩個輔助函數對后面讀取的數據進行預處理。
``` python
# 將一個序列中所有的詞記錄在all_tokens中以便之后構造詞典,然后在該序列后面添加PAD直到序列
# 長度變為max_seq_len,然后將序列保存在all_seqs中
def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
all_tokens.extend(seq_tokens)
seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
all_seqs.append(seq_tokens)
# 使用所有的詞來構造詞典。并將所有序列中的詞變換為詞索引后構造Tensor
def build_data(all_tokens, all_seqs):
vocab = Vocab.Vocab(collections.Counter(all_tokens),
specials=[PAD, BOS, EOS])
indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]
return vocab, torch.tensor(indices)
```
為了演示方便,我們在這里使用一個很小的法語—英語數據集。在這個數據集里,每一行是一對法語句子和它對應的英語句子,中間使用`'\t'`隔開。在讀取數據時,我們在句末附上“<eos>”符號,并可能通過添加“<pad>”符號使每個序列的長度均為`max_seq_len`。我們為法語詞和英語詞分別創建詞典。法語詞的索引和英語詞的索引相互獨立。
``` python
def read_data(max_seq_len):
# in和out分別是input和output的縮寫
in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []
with io.open('../../data/fr-en-small.txt') as f:
lines = f.readlines()
for line in lines:
in_seq, out_seq = line.rstrip().split('\t')
in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ')
if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
continue # 如果加上EOS后長于max_seq_len,則忽略掉此樣本
process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)
process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)
in_vocab, in_data = build_data(in_tokens, in_seqs)
out_vocab, out_data = build_data(out_tokens, out_seqs)
return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)
```
將序列的最大長度設成7,然后查看讀取到的第一個樣本。該樣本分別包含法語詞索引序列和英語詞索引序列。
``` python
max_seq_len = 7
in_vocab, out_vocab, dataset = read_data(max_seq_len)
dataset[0]
```
輸出:
```
(tensor([ 5, 4, 45, 3, 2, 0, 0]), tensor([ 8, 4, 27, 3, 2, 0, 0]))
```
## 10.12.2 含注意力機制的編碼器—解碼器
我們將使用含注意力機制的編碼器—解碼器來將一段簡短的法語翻譯成英語。下面我們來介紹模型的實現。
### 10.12.2.1 編碼器
在編碼器中,我們將輸入語言的詞索引通過詞嵌入層得到詞的表征,然后輸入到一個多層門控循環單元中。正如我們在6.5節(循環神經網絡的簡潔實現)中提到的,PyTorch的`nn.GRU`實例在前向計算后也會分別返回輸出和最終時間步的多層隱藏狀態。其中的輸出指的是最后一層的隱藏層在各個時間步的隱藏狀態,并不涉及輸出層計算。注意力機制將這些輸出作為鍵項和值項。
``` python
class Encoder(nn.Module):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
drop_prob=0, **kwargs):
super(Encoder, self).__init__(**kwargs)
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)
def forward(self, inputs, state):
# 輸入形狀是(批量大小, 時間步數)。將輸出互換樣本維和時間步維
embedding = self.embedding(inputs.long()).permute(1, 0, 2) # (seq_len, batch, input_size)
return self.rnn(embedding, state)
def begin_state(self):
return None # 隱藏態初始化為None時PyTorch會自動初始化為0
```
下面我們來創建一個批量大小為4、時間步數為7的小批量序列輸入。設門控循環單元的隱藏層個數為2,隱藏單元個數為16。編碼器對該輸入執行前向計算后返回的輸出形狀為(時間步數, 批量大小, 隱藏單元個數)。門控循環單元在最終時間步的多層隱藏狀態的形狀為(隱藏層個數, 批量大小, 隱藏單元個數)。對于門控循環單元來說,`state`就是一個元素,即隱藏狀態;如果使用長短期記憶,`state`是一個元組,包含兩個元素即隱藏狀態和記憶細胞。
``` python
encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
output, state = encoder(torch.zeros((4, 7)), encoder.begin_state())
output.shape, state.shape # GRU的state是h, 而LSTM的是一個元組(h, c)
```
輸出:
```
(torch.Size([7, 4, 16]), torch.Size([2, 4, 16]))
```
### 10.12.2.2 注意力機制
我們將實現10.11節(注意力機制)中定義的函數`$ a $`:將輸入連結后通過含單隱藏層的多層感知機變換。其中隱藏層的輸入是解碼器的隱藏狀態與編碼器在所有時間步上隱藏狀態的一一連結,且使用tanh函數作為激活函數。輸出層的輸出個數為1。兩個`Linear`實例均不使用偏差。其中函數`$ a $`定義里向量`$ \boldsymbol{v} $`的長度是一個超參數,即`attention_size`。
``` python
def attention_model(input_size, attention_size):
model = nn.Sequential(nn.Linear(input_size,
attention_size, bias=False),
nn.Tanh(),
nn.Linear(attention_size, 1, bias=False))
return model
```
注意力機制的輸入包括查詢項、鍵項和值項。設編碼器和解碼器的隱藏單元個數相同。這里的查詢項為解碼器在上一時間步的隱藏狀態,形狀為(批量大小, 隱藏單元個數);鍵項和值項均為編碼器在所有時間步的隱藏狀態,形狀為(時間步數, 批量大小, 隱藏單元個數)。注意力機制返回當前時間步的背景變量,形狀為(批量大小, 隱藏單元個數)。
``` python
def attention_forward(model, enc_states, dec_state):
"""
enc_states: (時間步數, 批量大小, 隱藏單元個數)
dec_state: (批量大小, 隱藏單元個數)
"""
# 將解碼器隱藏狀態廣播到和編碼器隱藏狀態形狀相同后進行連結
dec_states = dec_state.unsqueeze(dim=0).expand_as(enc_states)
enc_and_dec_states = torch.cat((enc_states, dec_states), dim=2)
e = model(enc_and_dec_states) # 形狀為(時間步數, 批量大小, 1)
alpha = F.softmax(e, dim=0) # 在時間步維度做softmax運算
return (alpha * enc_states).sum(dim=0) # 返回背景變量
```
在下面的例子中,編碼器的時間步數為10,批量大小為4,編碼器和解碼器的隱藏單元個數均為8。注意力機制返回一個小批量的背景向量,每個背景向量的長度等于編碼器的隱藏單元個數。因此輸出的形狀為(4, 8)。
``` python
seq_len, batch_size, num_hiddens = 10, 4, 8
model = attention_model(2*num_hiddens, 10)
enc_states = torch.zeros((seq_len, batch_size, num_hiddens))
dec_state = torch.zeros((batch_size, num_hiddens))
attention_forward(model, enc_states, dec_state).shape # torch.Size([4, 8])
```
### 10.12.2.3 含注意力機制的解碼器
我們直接將編碼器在最終時間步的隱藏狀態作為解碼器的初始隱藏狀態。這要求編碼器和解碼器的循環神經網絡使用相同的隱藏層個數和隱藏單元個數。
在解碼器的前向計算中,我們先通過剛剛介紹的注意力機制計算得到當前時間步的背景向量。由于解碼器的輸入來自輸出語言的詞索引,我們將輸入通過詞嵌入層得到表征,然后和背景向量在特征維連結。我們將連結后的結果與上一時間步的隱藏狀態通過門控循環單元計算出當前時間步的輸出與隱藏狀態。最后,我們將輸出通過全連接層變換為有關各個輸出詞的預測,形狀為(批量大小, 輸出詞典大小)。
``` python
class Decoder(nn.Module):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
attention_size, drop_prob=0):
super(Decoder, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
self.attention = attention_model(2*num_hiddens, attention_size)
# GRU的輸入包含attention輸出的c和實際輸入, 所以尺寸是 2*embed_size
self.rnn = nn.GRU(2*embed_size, num_hiddens, num_layers, dropout=drop_prob)
self.out = nn.Linear(num_hiddens, vocab_size)
def forward(self, cur_input, state, enc_states):
"""
cur_input shape: (batch, )
state shape: (num_layers, batch, num_hiddens)
"""
# 使用注意力機制計算背景向量
c = attention_forward(self.attention, enc_states, state[-1])
# 將嵌入后的輸入和背景向量在特征維連結
input_and_c = torch.cat((self.embedding(cur_input), c), dim=1) # (批量大小, 2*embed_size)
# 為輸入和背景向量的連結增加時間步維,時間步個數為1
output, state = self.rnn(input_and_c.unsqueeze(0), state)
# 移除時間步維,輸出形狀為(批量大小, 輸出詞典大小)
output = self.out(output).squeeze(dim=0)
return output, state
def begin_state(self, enc_state):
# 直接將編碼器最終時間步的隱藏狀態作為解碼器的初始隱藏狀態
return enc_state
```
## 10.12.3 訓練模型
我們先實現`batch_loss`函數計算一個小批量的損失。解碼器在最初時間步的輸入是特殊字符`BOS`。之后,解碼器在某時間步的輸入為樣本輸出序列在上一時間步的詞,即強制教學。此外,同10.3節(word2vec的實現)中的實現一樣,我們在這里也使用掩碼變量避免填充項對損失函數計算的影響。
``` python
def batch_loss(encoder, decoder, X, Y, loss):
batch_size = X.shape[0]
enc_state = encoder.begin_state()
enc_outputs, enc_state = encoder(X, enc_state)
# 初始化解碼器的隱藏狀態
dec_state = decoder.begin_state(enc_state)
# 解碼器在最初時間步的輸入是BOS
dec_input = torch.tensor([out_vocab.stoi[BOS]] * batch_size)
# 我們將使用掩碼變量mask來忽略掉標簽為填充項PAD的損失
mask, num_not_pad_tokens = torch.ones(batch_size,), 0
l = torch.tensor([0.0])
for y in Y.permute(1,0): # Y shape: (batch, seq_len)
dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)
l = l + (mask * loss(dec_output, y)).sum()
dec_input = y # 使用強制教學
num_not_pad_tokens += mask.sum().item()
# 將PAD對應位置的掩碼設成0, 原文這里是 y != out_vocab.stoi[EOS], 感覺有誤
mask = mask * (y != out_vocab.stoi[PAD]).float()
return l / num_not_pad_tokens
```
在訓練函數中,我們需要同時迭代編碼器和解碼器的模型參數。
``` python
def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
enc_optimizer = torch.optim.Adam(encoder.parameters(), lr=lr)
dec_optimizer = torch.optim.Adam(decoder.parameters(), lr=lr)
loss = nn.CrossEntropyLoss(reduction='none')
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)
for epoch in range(num_epochs):
l_sum = 0.0
for X, Y in data_iter:
enc_optimizer.zero_grad()
dec_optimizer.zero_grad()
l = batch_loss(encoder, decoder, X, Y, loss)
l.backward()
enc_optimizer.step()
dec_optimizer.step()
l_sum += l.item()
if (epoch + 1) % 10 == 0:
print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))
```
接下來,創建模型實例并設置超參數。然后,我們就可以訓練模型了。
``` python
embed_size, num_hiddens, num_layers = 64, 64, 2
attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50
encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers,
drop_prob)
decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers,
attention_size, drop_prob)
train(encoder, decoder, dataset, lr, batch_size, num_epochs)
```
輸出:
```
epoch 10, loss 0.441
epoch 20, loss 0.183
epoch 30, loss 0.100
epoch 40, loss 0.046
epoch 50, loss 0.025
```
## 10.12.4 預測不定長的序列
在10.10節(束搜索)中我們介紹了3種方法來生成解碼器在每個時間步的輸出。這里我們實現最簡單的貪婪搜索。
``` python
def translate(encoder, decoder, input_seq, max_seq_len):
in_tokens = input_seq.split(' ')
in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]]) # batch=1
enc_state = encoder.begin_state()
enc_output, enc_state = encoder(enc_input, enc_state)
dec_input = torch.tensor([out_vocab.stoi[BOS]])
dec_state = decoder.begin_state(enc_state)
output_tokens = []
for _ in range(max_seq_len):
dec_output, dec_state = decoder(dec_input, dec_state, enc_output)
pred = dec_output.argmax(dim=1)
pred_token = out_vocab.itos[int(pred.item())]
if pred_token == EOS: # 當任一時間步搜索出EOS時,輸出序列即完成
break
else:
output_tokens.append(pred_token)
dec_input = pred
return output_tokens
```
簡單測試一下模型。輸入法語句子“ils regardent.”,翻譯后的英語句子應該是“they are watching.”。
``` python
input_seq = 'ils regardent .'
translate(encoder, decoder, input_seq, max_seq_len)
```
輸出:
```
['they', 'are', 'watching', '.']
```
## 10.12.5 評價翻譯結果
評價機器翻譯結果通常使用BLEU(Bilingual Evaluation Understudy)[1]。對于模型預測序列中任意的子序列,BLEU考察這個子序列是否出現在標簽序列中。
具體來說,設詞數為$n$的子序列的精度為`$ p_n $`。它是預測序列與標簽序列匹配詞數為`$ n $`的子序列的數量與預測序列中詞數為`$ n $`的子序列的數量之比。舉個例子,假設標簽序列為`$ A $`、`$ B $`、`$ C $`、`$ D $` 、`$ E $`、`$ F $`,預測序列為`$ A $` 、`$ B $`、`$ B $`、`$ C $`、`$ D $`,那么`$ p_1 = 4/5,\ p_2 = 3/4,\ p_3 = 1/3,\ p_4 = 0 $`。設`$ len_{\text{label}} $`和`$ len_{\text{pred}} $`分別為標簽序列和預測序列的詞數,那么,BLEU的定義為
```[tex]
\exp\left(\min\left(0, 1 - \frac{len_{\text{label}}}{len_{\text{pred}}}\right)\right) \prod_{n=1}^k p_n^{1/2^n},
```
其中`$ k $`是我們希望匹配的子序列的最大詞數。可以看到當預測序列和標簽序列完全一致時,BLEU為1。
因為匹配較長子序列比匹配較短子序列更難,BLEU對匹配較長子序列的精度賦予了更大權重。例如,當`$ p_n $`固定在0.5時,隨著`$ n $`的增大,`$ 0.5^{1/2} \approx 0.7, 0.5^{1/4} \approx 0.84, 0.5^{1/8} \approx 0.92, 0.5^{1/16} \approx 0.96 $`。另外,模型預測較短序列往往會得到較高`$ p_n $`值。因此,上式中連乘項前面的系數是為了懲罰較短的輸出而設的。舉個例子,當`$ k=2 $`時,假設標簽序列為`$ A $`、`$ B $`、`$ C $`、`$ D $`、`$ E $`、`$ F $`,而預測序列為`$ A $`、`$ B $`。雖然`$ p_1 = p_2 = 1 $`,但懲罰系數`$ \exp(1-6/2) \approx 0.14 $`,因此BLEU也接近0.14。
下面來實現BLEU的計算。
``` python
def bleu(pred_tokens, label_tokens, k):
len_pred, len_label = len(pred_tokens), len(label_tokens)
score = math.exp(min(0, 1 - len_label / len_pred))
for n in range(1, k + 1):
num_matches, label_subs = 0, collections.defaultdict(int)
for i in range(len_label - n + 1):
label_subs[''.join(label_tokens[i: i + n])] += 1
for i in range(len_pred - n + 1):
if label_subs[''.join(pred_tokens[i: i + n])] > 0:
num_matches += 1
label_subs[''.join(pred_tokens[i: i + n])] -= 1
score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
return score
```
接下來,定義一個輔助打印函數。
``` python
def score(input_seq, label_seq, k):
pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)
label_tokens = label_seq.split(' ')
print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k),
' '.join(pred_tokens)))
```
預測正確則分數為1。
``` python
score('ils regardent .', 'they are watching .', k=2)
```
輸出:
```
bleu 1.000, predict: they are watching .
```
測試一個不在訓練集中的樣本。
``` python
score('ils sont canadiens .', 'they are canadian .', k=2)
```
輸出:
```
bleu 0.658, predict: they are russian .
```
## 小結
* 可以將編碼器—解碼器和注意力機制應用于機器翻譯中。
* BLEU可以用來評價翻譯結果。
## 參考文獻
[1] Papineni, K., Roukos, S., Ward, T., & Zhu, W. J. (2002, July). BLEU: a method for automatic evaluation of machine translation. In Proceedings of the 40th annual meeting on association for computational linguistics (pp. 311-318). Association for Computational Linguistics.
[2] WMT. http://www.statmt.org/wmt14/translation-task.html
[3] Tatoeba Project. http://www.manythings.org/anki/
-----------
> 注:本節除代碼外與原書基本相同,[原書傳送門](https://zh.d2l.ai/chapter_natural-language-processing/machine-translation.html)
- Home
- Introduce
- 1.深度學習簡介
- 深度學習簡介
- 2.預備知識
- 2.1環境配置
- 2.2數據操作
- 2.3自動求梯度
- 3.深度學習基礎
- 3.1 線性回歸
- 3.2 線性回歸的從零開始實現
- 3.3 線性回歸的簡潔實現
- 3.4 softmax回歸
- 3.5 圖像分類數據集(Fashion-MINST)
- 3.6 softmax回歸的從零開始實現
- 3.7 softmax回歸的簡潔實現
- 3.8 多層感知機
- 3.9 多層感知機的從零開始實現
- 3.10 多層感知機的簡潔實現
- 3.11 模型選擇、反向傳播和計算圖
- 3.12 權重衰減
- 3.13 丟棄法
- 3.14 正向傳播、反向傳播和計算圖
- 3.15 數值穩定性和模型初始化
- 3.16 實戰kaggle比賽:房價預測
- 4 深度學習計算
- 4.1 模型構造
- 4.2 模型參數的訪問、初始化和共享
- 4.3 模型參數的延后初始化
- 4.4 自定義層
- 4.5 讀取和存儲
- 4.6 GPU計算
- 5 卷積神經網絡
- 5.1 二維卷積層
- 5.2 填充和步幅
- 5.3 多輸入通道和多輸出通道
- 5.4 池化層
- 5.5 卷積神經網絡(LeNet)
- 5.6 深度卷積神經網絡(AlexNet)
- 5.7 使用重復元素的網絡(VGG)
- 5.8 網絡中的網絡(NiN)
- 5.9 含并行連結的網絡(GoogLeNet)
- 5.10 批量歸一化
- 5.11 殘差網絡(ResNet)
- 5.12 稠密連接網絡(DenseNet)
- 6 循環神經網絡
- 6.1 語言模型
- 6.2 循環神經網絡
- 6.3 語言模型數據集(周杰倫專輯歌詞)
- 6.4 循環神經網絡的從零開始實現
- 6.5 循環神經網絡的簡單實現
- 6.6 通過時間反向傳播
- 6.7 門控循環單元(GRU)
- 6.8 長短期記憶(LSTM)
- 6.9 深度循環神經網絡
- 6.10 雙向循環神經網絡
- 7 優化算法
- 7.1 優化與深度學習
- 7.2 梯度下降和隨機梯度下降
- 7.3 小批量隨機梯度下降
- 7.4 動量法
- 7.5 AdaGrad算法
- 7.6 RMSProp算法
- 7.7 AdaDelta
- 7.8 Adam算法
- 8 計算性能
- 8.1 命令式和符號式混合編程
- 8.2 異步計算
- 8.3 自動并行計算
- 8.4 多GPU計算
- 9 計算機視覺
- 9.1 圖像增廣
- 9.2 微調
- 9.3 目標檢測和邊界框
- 9.4 錨框
- 10 自然語言處理
- 10.1 詞嵌入(word2vec)
- 10.2 近似訓練
- 10.3 word2vec實現
- 10.4 子詞嵌入(fastText)
- 10.5 全局向量的詞嵌入(Glove)
- 10.6 求近義詞和類比詞
- 10.7 文本情感分類:使用循環神經網絡
- 10.8 文本情感分類:使用卷積網絡
- 10.9 編碼器--解碼器(seq2seq)
- 10.10 束搜索
- 10.11 注意力機制
- 10.12 機器翻譯