1 背景
在neural machine translation的任务中,由于RNN每个时间步只能看到过去的信息,无法看到完整的上下文。除此之外,由于source language(源语言) 和 target language(目标语言)的长度可能不同,而传统的RNN输入和输出的序列必须相同,导致传统的RNN在机器翻译任务(Seq2Seq任务)上十分受限。2014年:KyungHyun Cho 和 Yoshua Bengio 等人首次在论文 “Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation“ 中提出Encoder-Decoder 架构。我给诸君画了一个简单的演示图。

Encoder-Decoder
2 Encoder-Decoder工作原理
根据机器翻译任务,我也为诸君画了一个简图,来向各位说明Encoder-Decoder单独是怎么样工作的,以及是如何一起工作的。

working principle of Encoder

working principle of Decoder

working princple of Seq2Seq model
3 LSTM and Encoder-Decoder demo
# LSTM and encoder and decoder
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torch.nn.utils.rnn import pad_sequence
from torchtext.vocab import build_vocab_from_iterator
from tqdm import tqdm
import spacy
import random
from datasets import load_dataset
# 设置随机种子保证可重复性
random.seed(42)
torch.manual_seed(42)
torch.backends.cudnn.deterministic = True
# 加载数据集
import os
import pickle
if os.path.exists("multi30k_dataset.pkl"):
pickle_file = "multi30k_dataset.pkl"
with open(pickle_file, "rb") as f:
ds = pickle.load(f)
else:
ds = load_dataset("bentrevett/multi30k")
with open("multi30k_dataset.pkl", "wb") as f:
pickle.dump(ds, f)
# 加载分词器
try:
spacy_en = spacy.load('en_core_web_sm')
spacy_de = spacy.load('de_core_news_sm')
except OSError:
print("请先安装spacy语言模型:")
print("python -m spacy download en_core_web_sm")
print("python -m spacy download de_core_news_sm")
exit()
# 分词函数
def tokenize_en(text):
return [token.text for token in spacy_en.tokenizer(text)]
def tokenize_de(text):
return [token.text for token in spacy_de.tokenizer(text)]
from torchtext.vocab import Vocab
from collections import Counter
import torchtext
def yield_tokens(data_iter, tokenizer, lang):
for example in data_iter:
yield tokenizer(example[lang])
def build_vocab_compatible(data_iter, tokenizer, lang, min_freq=2):
# 统计词频
counter = Counter()
for tokens in yield_tokens(data_iter, tokenizer, lang):
counter.update(tokens)
# 根据版本选择不同的构建方式
if hasattr(torchtext.vocab, 'build_vocab_from_iterator'):
# 新版 torchtext (0.12+)
vocab = Vocab(counter,
min_freq=min_freq,
specials=['<unk>', '<pad>', '<sos>', '<eos>'])
# 新版默认已经处理了未知词
else:
# 旧版 torchtext (<0.12)
vocab = Vocab(counter,
min_freq=min_freq,
specials=['<unk>', '<pad>', '<sos>', '<eos>'])
vocab.set_default_index(vocab['<unk>'])
return vocab
# 使用示例
src_vocab = build_vocab_compatible(ds['train'], tokenize_de, 'de', min_freq=2)
trg_vocab = build_vocab_compatible(ds['train'], tokenize_en, 'en', min_freq=2)
# 编码器(Encoder)
class Encoder(nn.Module):
def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
super().__init__()
self.embedding = nn.Embedding(input_dim, emb_dim)
self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=False)
self.dropout = nn.Dropout(dropout)
def forward(self, src):
embedded = self.dropout(self.embedding(src))
outputs, (hidden, cell) = self.rnn(embedded)
return hidden, cell
# 解码器(Decoder)
class Decoder(nn.Module):
def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout):
super().__init__()
self.output_dim = output_dim
self.embedding = nn.Embedding(output_dim, emb_dim)
self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=False)
self.fc_out = nn.Linear(hid_dim, output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, input, hidden, cell):
input = input.unsqueeze(0)
embedded = self.dropout(self.embedding(input))
output, (hidden, cell) = self.rnn(embedded, (hidden, cell))
prediction = self.fc_out(output.squeeze(0))
return prediction, hidden, cell
# Seq2Seq模型
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder, device):
super().__init__()
self.encoder = encoder
self.decoder = decoder
self.device = device
def forward(self, src, trg, teacher_forcing_ratio=0.5):
batch_size = trg.shape[1]
trg_len = trg.shape[0]
trg_vocab_size = self.decoder.output_dim
outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
hidden, cell = self.encoder(src)
# input是1维张量,形状是 (batch_size,)
input = trg[0, :]
for t in range(1, trg_len):
output, hidden, cell = self.decoder(input, hidden, cell)
outputs[t,:,:] = output
teacher_force = random.random() < teacher_forcing_ratio
# top1是1维张量,形状是 (batch_size,)
top1 = output.argmax(1)
input = trg[t] if teacher_force else top1
return outputs
# 超参数(根据词汇表大小动态设置)
INPUT_DIM = len(src_vocab)
OUTPUT_DIM = len(trg_vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
HID_DIM = 512
N_LAYERS = 2
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5
BATCH_SIZE = 128
N_EPOCHS = 10
CLIP = 1
LEARNING_RATE = 0.001
# 设备设置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 初始化模型
encoder = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT).to(device)
decoder = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT).to(device)
model = Seq2Seq(encoder, decoder, device).to(device)
# 打印参数量
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"模型参数量: {count_parameters(model):,}")
# 优化器和损失函数
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
criterion = nn.CrossEntropyLoss(ignore_index=trg_vocab['<pad>'])
# 数据批处理函数
def collate_fn(batch):
src_batch, trg_batch = [], []
for example in batch:
src = torch.tensor([src_vocab[token] for token in ['<sos>'] + tokenize_de(example['de']) + ['<eos>']])
trg = torch.tensor([trg_vocab[token] for token in ['<sos>'] + tokenize_en(example['en']) + ['<eos>']])
src_batch.append(src)
trg_batch.append(trg)
# pad_sequence 返回的形状是:(max_seq_len, batch_size)
src_batch = pad_sequence(src_batch, padding_value=src_vocab['<pad>'])
trg_batch = pad_sequence(trg_batch, padding_value=trg_vocab['<pad>'])
return src_batch, trg_batch
# 创建DataLoader
train_loader = DataLoader(ds['train'], batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_fn)
valid_loader = DataLoader(ds['validation'], batch_size=BATCH_SIZE, collate_fn=collate_fn)
test_loader = DataLoader(ds['test'], batch_size=BATCH_SIZE, collate_fn=collate_fn)
def train(model, iterator, optimizer, criterion, clip):
model.train()
epoch_loss = 0
for src, trg in tqdm(iterator, desc='Training'):
src, trg = src.to(device), trg.to(device)
optimizer.zero_grad()
output = model(src, trg)
output = output[1:].view(-1, output.shape[-1])
trg = trg[1:].view(-1)
loss = criterion(output, trg)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
optimizer.step()
epoch_loss += loss.item()
return epoch_loss / len(iterator)
def evaluate(model, iterator, criterion):
model.eval()
epoch_loss = 0
with torch.no_grad():
for src, trg in tqdm(iterator, desc='Evaluating'):
src, trg = src.to(device), trg.to(device)
output = model(src, trg, teacher_forcing_ratio=0)
output = output[1:].view(-1, output.shape[-1])
trg = trg[1:].view(-1)
loss = criterion(output, trg)
epoch_loss += loss.item()
return epoch_loss / len(iterator)
best_valid_loss = float('inf')
for epoch in range(N_EPOCHS):
train_loss = train(model, train_loader, optimizer, criterion, CLIP)
valid_loss = evaluate(model, valid_loader, criterion)
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'best-model.pt')
print(f'Epoch: {epoch+1:02}')
print(f'\tTrain Loss: {train_loss:.3f}')
print(f'\t Val. Loss: {valid_loss:.3f}')
# 加载最佳模型测试
model.load_state_dict(torch.load('best-model.pt'))
test_loss = evaluate(model, test_loader, criterion)
print(f'Test Loss: {test_loss:.3f}')
def translate_sentence(sentence, model, src_vocab, trg_vocab, device, max_len=50):
model.eval()
# 分词与数值化
tokens = ['<sos>'] + tokenize_de(sentence) + ['<eos>']
src_indexes = [src_vocab[token] for token in tokens]
src_tensor = torch.LongTensor(src_indexes).unsqueeze(1).to(device)
# 编码器
with torch.no_grad():
hidden, cell = model.encoder(src_tensor)
# 解码器
trg_indexes = [trg_vocab['<sos>']]
for _ in range(max_len):
# trg_indexes[-1] 将上一个时间步的预测作为当前时间步的输入
trg_tensor = torch.LongTensor([trg_indexes[-1]]).to(device)
with torch.no_grad():
output, hidden, cell = model.decoder(trg_tensor, hidden, cell)
pred_token = output.argmax(1).item()
trg_indexes.append(pred_token)
if pred_token == trg_vocab['<eos>']:
break
# 索引转单词
trg_tokens = [trg_vocab.itos[i] for i in trg_indexes[1:-1]] # 排除<sos>和<eos>
return ' '.join(trg_tokens)
# 示例翻译
examples = [
"Ein Mann läuft auf der Straße.",
"Eine Frau liest ein Buch.",
"Kinder spielen im Park.",
"Ein Mann läuft auf der Straße und ein Hund folgt ihm."
]
for ex in examples:
translation = translate_sentence(ex, model, src_vocab, trg_vocab, device)
print(f"德文: {ex}")
print(f"英文: {translation}\n")
训练阶段
Training: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227/227 [09:44<00:00, 2.57s/it]
Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:03<00:00, 2.06it/s]
Epoch: 01
Train Loss: 5.046
Val. Loss: 4.842
Training: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227/227 [06:33<00:00, 1.73s/it]
Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:03<00:00, 2.13it/s]
Epoch: 02
Train Loss: 4.444
Val. Loss: 4.570
Training: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227/227 [06:28<00:00, 1.71s/it]
Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:03<00:00, 2.18it/s]
Epoch: 03
Train Loss: 4.120
Val. Loss: 4.337
Training: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227/227 [07:10<00:00, 1.90s/it]
Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:04<00:00, 1.79it/s]
Epoch: 04
Train Loss: 3.890
Val. Loss: 4.253
Training: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227/227 [08:23<00:00, 2.22s/it]
Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:03<00:00, 2.24it/s]
Epoch: 05
Train Loss: 3.722
Val. Loss: 4.166
Training: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227/227 [06:14<00:00, 1.65s/it]
Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:03<00:00, 2.14it/s]
Epoch: 06
Train Loss: 3.569
Val. Loss: 4.031
Training: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227/227 [06:25<00:00, 1.70s/it]
Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:03<00:00, 2.19it/s]
Epoch: 07
Train Loss: 3.432
Val. Loss: 3.977
Training: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227/227 [07:08<00:00, 1.89s/it]
Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:04<00:00, 1.98it/s]
Epoch: 08
Train Loss: 3.324
Val. Loss: 3.885
Training: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227/227 [07:50<00:00, 2.07s/it]
Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:04<00:00, 1.95it/s]
Epoch: 09
Train Loss: 3.218
Val. Loss: 3.857
Training: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 227/227 [08:44<00:00, 2.31s/it]
Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:04<00:00, 1.68it/s]
Epoch: 10
Train Loss: 3.141
Val. Loss: 3.819
示例翻译
德文: Ein Mann läuft auf der Straße.
英文: A man is walking on the sidewalk .
德文: Eine Frau liest ein Buch.
英文: A woman is a a .
德文: Kinder spielen im Park.
英文: Children playing in a park .
德文: Ein Mann läuft auf der Straße und ein Hund folgt ihm.
英文: A man is walking on the sidewalk and a dog walks on the . .
4 需要注意的点
# 在Seq2Seq model中,需要注意由于decoder的输入就是<sos>,
# 所以decoder的第一个输出是"I"(假如预测的目标语言句子是"I love you."),
# 所以Seq2Seq model 的outputs的第0个时间步仍然是0,根据下面这个遍历循环我们就可以知道:
for t in range(1, trg_len)
# 在计算损失之前,需要做如下变换
output = output[1:].view(-1, output.shape[-1])
trg = trg[1:].view(-1)
# 这是为什么呢?
# 跟上面提到的很相关,我举一个例子
# output是["","我","爱","你","<eos>"]
# trg是["<sos>","我","爱","你","<eos>"]
# 其实就是decoder的输入是<sos>(trg[0,:]),所以输出是"我",并没有<sos>,虽然output的张量的# 第0个时间步有位置,但仍然是最开始的0。
# 所以计算损失的时候,把第0个时间步去掉。
5
<sos>: start of string
<eos>: end of string
<unk>: unknown
<pad>: pad(填充)
6
敬请期待,Transformer!!!!!