《使用Encoder-Decoder架构+LSTM完成机器翻译任务》

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!!!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容