在自然语言处理领域,机器翻译一直是个经典难题。早期的翻译模型像一个"死记硬背"的学生:先把整篇中文课文背成一个固定长度的"小抄",再凭着这张小抄默写出英文。句子短的时候效果还行,一旦句子变长,小抄装不下了,翻译就开始胡说八道。后来,科学家发明了Attention机制(注意力机制),让模型学会了"翻书"------翻译每个词的时候,都回头去原文里找最相关的地方看一眼。这一招,彻底改变了NLP的格局。
今天,我们就用最通俗的语言,配合完整的代码,把Attention机制彻底讲清楚。从它的设计初衷,到三种评分函数,再到一个可以直接跑起来的中英翻译项目,手把手带你拿下这个知识点。
一、传统Seq2Seq模型的两大硬伤
先复习一下没有Attention的老方法。经典的Seq2Seq模型(序列到序列模型)包含两个部分:
-
编码器(Encoder)
:读入源句子(比如中文),把整个句子的信息压缩成一个固定长度的向量,通常叫做"上下文向量"或"语义向量"。
-
解码器(Decoder)
:基于这个固定向量,一步一步生成目标句子(比如英文)。
这个过程就像你把一本小说浓缩成一张便利贴,然后让别人根据这张便利贴还原整本小说------信息丢失是必然的。具体来说,传统方法存在两个致命问题:
问题一:信息压缩困难
无论源句子多长,哪怕是100个词的复杂长句,最终都只能塞进一个固定大小的向量里。这导致长句的关键信息很容易被"挤丢",模型翻译到后面时,前面的内容已经忘了。
问题二:缺乏动态感知
解码器在生成每个目标词时,用的是同一个静态向量。翻译"我"和翻译"散步"时,参考的信息源一模一样。这就像你写作文时不允许回看原文,只能凭印象写------开头还记得,结尾就开始编了。
为了解决这些问题,研究者提出了Attention机制。

二、Attention的核心思想:动态查阅
Attention机制的核心思想极其简单,却威力巨大:
解码器在生成目标序列的每一步时,不再依赖一个静态的上下文向量,而是根据当前的解码状态,动态地从编码器各时间步的隐藏状态中选取最相关的信息。
换句话说,解码器每生成一个词,都会重新"扫一遍"原文,用当前的"注意力"去加权提取原文中最有用的信息。这种机制让模型自动学会了对齐(Alignment)------它能判断出源句子中哪些位置与当前要生成的词关系最密切。
举个具体例子:翻译"我 爱 你" → "I love you"。当解码器要生成"love"时,注意力权重会集中在"爱"这个字上;生成"you"时,注意力会转向"你"。模型不需要任何人教,自己就学会了这种对应关系。

三、工作原理:一步一步拆解Attention计算
Attention机制的实现可以分成四个步骤。下面我们用最常用的点积评分函数来讲解。
3.1 计算注意力评分
第一步,我们需要衡量解码器当前隐藏状态 与编码器每个时间步的隐藏状态 之间的相关性。这个相关性分数就叫注意力评分。
假设解码器当前步的隐藏状态是 hdechdec(一个向量),编码器有T个时间步的隐藏状态 h1,h2,...,hTh1,h2,...,hT。最简单的评分方法就是计算点积:
scorei=hdec⋅hiscorei=hdec⋅hi
点积越大,说明两个向量方向越一致,相关性越强。如果你觉得"点积"不好理解,可以把它想象成两个向量的相似度------越像,分数越高。

3.2 归一化得到注意力权重
得到的分数大小不一,直接使用不方便。我们用一个Softmax函数把它们转换成概率分布,也就是注意力权重。Softmax的作用是:让所有分数变成0~1之间的数,并且加起来等于1。
αi=escorei∑j=1Tescorejαi=∑j=1Tescorejescorei
现在,αiαi 就代表了模型应该对第i个源位置投入多少"注意力"。分数越高的位置,权重越大。

3.3 生成上下文向量
有了注意力权重,我们就可以对编码器的所有隐藏状态进行加权求和 ,得到当前步的上下文向量:
context=∑i=1Tαi⋅hicontext=i=1∑Tαi⋅hi
这个上下文向量就是模型从原文中提取的"重点笔记"------它融合了所有源位置的信息,但更强调当前最相关的部分。

3.4 解码信息融合
最后一步,解码器把当前步的GRU输出(hdechdec)和上下文向量拼接到一起,组成一个更丰富的信息向量,再通过一个线性层(全连接层)和Softmax,预测下一个词的概率分布。
combined=[hdec;context]combined=[hdec;context]output=softmax(W⋅combined+b)output=softmax(W⋅combined+b)
这样,解码器在预测每个词时,既用到了自己当前的记忆(hdechdec),也用到了从原文动态提取的外部信息(context),翻译效果自然大幅提升。

四、三种注意力评分函数,各有千秋
刚才我们用的是最简单的点积评分(Dot)。但实际应用中,编码器和解码器的隐藏状态维度可能不一样,或者任务复杂需要更强的表达能力。因此研究者提出了多种评分函数,常见的有三种:
4.1 点积评分(Dot)

通俗解释 :直接计算两个向量的内积,值越大表示越相关。
优点 :计算最快,实现简单。
缺点:要求 hdechdec 和 henchenc 维度相同,且表达能力有限。
4.2 通用点积评分(General)

通俗解释 :先对编码器状态做一个线性变换(乘以矩阵W),把它的维度变成和解码器一致,然后再点积。
优点 :可以处理维度不匹配的情况,W是可学习的,增加了模型灵活性。
缺点:比点积多了一些计算量。
4.3 拼接评分(Concat)/ 加性注意力

通俗解释 :把解码器状态和编码器状态拼成一个长向量,扔进一个带激活函数的小型神经网络,最后用另一个向量v投影成一个分数。
优点 :表达能力最强,因为引入了非线性变换(tanh),可以捕捉更复杂的交互关系。
缺点:计算量最大。
实际工程中,点积评分因为简单高效被广泛使用;在Transformer中,还使用了缩放点积注意力(除以维度的平方根,防止梯度消失)。我们的代码示例就采用最基本的点积评分,便于理解。
五、完整代码实战:中英翻译V2.0(带Attention)
理论讲完,我们上代码。下面是一个完整的中英翻译项目,在传统Seq2Seq基础上引入了Attention机制。你可以按顺序创建文件,然后运行训练和预测。
项目结构
bash
translation_attention/
│
├── data/
│ ├── raw/ # 存放原始数据 cmn.txt
│ └── processed/ # 预处理后的数据
├── models/ # 保存训练好的模型
├── logs/ # TensorBoard日志
└── src/
├── config.py
├── tokenizer.py
├── process.py
├── dataset.py
├── model.py
├── train.py
├── predict.py
└── evaluate.py
5.1 配置文件 config.py
bash
"""
配置文件:集中管理所有超参数和路径
"""
from pathlib import Path
# 项目根目录(src的父目录)
BASE_DIR = Path(__file__).parent.parent
# 数据路径
RAW_DATA_DIR = BASE_DIR / 'data' / 'raw'
PROCESSED_DATA_DIR = BASE_DIR / 'data' / 'processed'
MODELS_DIR = BASE_DIR / 'models'
LOGS_DIR = BASE_DIR / 'logs'
# 模型参数
EMBEDDING_DIM = 128 # 词向量维度
ENCODER_HIDDEN_DIM = 512 # 编码器GRU隐藏维度
DECODER_HIDDEN_DIM = 1024 # 解码器GRU隐藏维度(是编码器的2倍,因为编码器双向)
ENCODER_LAYERS = 1 # 编码器层数
# 训练参数
BATCH_SIZE = 128
SEQ_LEN = 30 # 输入/输出序列最大长度
LEARNING_RATE = 1e-3
EPOCHS = 30
5.2 分词器 tokenizer.py
bash
"""
分词器:中文按字切分,英文按词切分,提供编码/解码/词表构建功能
"""
from abc import abstractmethod
from nltk import word_tokenize, TreebankWordDetokenizer
from tqdm import tqdm
class BaseTokenizer:
"""分词器基类"""
unk_token = '<unk>' # 未知词
pad_token = '<pad>' # 填充符
sos_token = '<sos>' # 开始符
eos_token = '<eos>' # 结束符
@staticmethod
@abstractmethod
def tokenize(sentence):
pass
@abstractmethod
def decode(self, indexes):
pass
@classmethod
def build_vocab(cls, sentences, vocab_file):
"""从句子列表构建词表并保存"""
unique_words = set()
for sentence in tqdm(sentences, desc='构建词表'):
for word in cls.tokenize(sentence):
unique_words.add(word)
# 特殊标记放在最前面,保证索引固定
vocab_list = [cls.pad_token, cls.unk_token, cls.sos_token, cls.eos_token] + list(unique_words)
with open(vocab_file, 'w', encoding='utf-8') as f:
for w in vocab_list:
f.write(w + '\n')
def __init__(self, vocab_list):
self.vocab_list = vocab_list
self.vocab_size = len(vocab_list)
self.word2index = {w: i for i, w in enumerate(vocab_list)}
self.index2word = {i: w for i, w in enumerate(vocab_list)}
self.unk_token_index = self.word2index[self.unk_token]
self.pad_token_index = self.word2index[self.pad_token]
self.sos_token_index = self.word2index[self.sos_token]
self.eos_token_index = self.word2index[self.eos_token]
@classmethod
def from_vocab(cls, vocab_file):
with open(vocab_file, 'r', encoding='utf-8') as f:
vocab_list = [line.strip() for line in f.readlines()]
return cls(vocab_list)
def encode(self, sentence, seq_len, add_sos_eos=False):
"""将句子转为索引序列,长度截断/补齐"""
tokens = self.tokenize(sentence)
indexes = [self.word2index.get(t, self.unk_token_index) for t in tokens]
if add_sos_eos:
indexes = indexes[:seq_len - 2]
indexes = [self.sos_token_index] + indexes + [self.eos_token_index]
else:
indexes = indexes[:seq_len]
if len(indexes) < seq_len:
indexes += [self.pad_token_index] * (seq_len - len(indexes))
return indexes
class ChineseTokenizer(BaseTokenizer):
@staticmethod
def tokenize(sentence):
return list(sentence) # 中文按字切分
def decode(self, indexes):
return ''.join([self.index2word[i] for i in indexes])
class EnglishTokenizer(BaseTokenizer):
@staticmethod
def tokenize(sentence):
return word_tokenize(sentence) # 英文按词切分
def decode(self, indexes):
tokens = [self.index2word[i] for i in indexes]
return TreebankWordDetokenizer().detokenize(tokens)
5.3 数据预处理 process.py
bash
"""
预处理原始中英文对齐文件,划分训练/测试集,构建词表,编码数据
"""
import pandas as pd
from sklearn.model_selection import train_test_split
from tokenizer import ChineseTokenizer, EnglishTokenizer
import config
def process():
print("开始处理数据...")
df = pd.read_csv(config.RAW_DATA_DIR / 'cmn.txt', sep='\t', header=None,
usecols=[0,1], names=['en','zh'])
df = df.dropna()
df = df[df['en'].str.strip().ne('') & df['zh'].str.strip().ne('')]
train_df, test_df = train_test_split(df, test_size=0.2, random_state=42)
# 构建词表
EnglishTokenizer.build_vocab(train_df['en'].tolist(), config.PROCESSED_DATA_DIR / 'en_vocab.txt')
ChineseTokenizer.build_vocab(train_df['zh'].tolist(), config.PROCESSED_DATA_DIR / 'zh_vocab.txt')
en_tokenizer = EnglishTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'en_vocab.txt')
zh_tokenizer = ChineseTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'zh_vocab.txt')
# 编码训练集(英文加sos/eos,中文不加)
train_df['en'] = train_df['en'].apply(
lambda x: en_tokenizer.encode(x, config.SEQ_LEN, add_sos_eos=True))
train_df['zh'] = train_df['zh'].apply(
lambda x: zh_tokenizer.encode(x, config.SEQ_LEN, add_sos_eos=False))
train_df.to_json(config.PROCESSED_DATA_DIR / 'indexed_train.jsonl', orient='records', lines=True)
# 编码测试集
test_df['en'] = test_df['en'].apply(
lambda x: en_tokenizer.encode(x, config.SEQ_LEN, add_sos_eos=True))
test_df['zh'] = test_df['zh'].apply(
lambda x: zh_tokenizer.encode(x, config.SEQ_LEN, add_sos_eos=False))
test_df.to_json(config.PROCESSED_DATA_DIR / 'indexed_test.jsonl', orient='records', lines=True)
print("数据预处理完成!")
if __name__ == '__main__':
process()
5.4 数据集 dataset.py
bash
"""
PyTorch Dataset和DataLoader封装
"""
import pandas as pd
import torch
from torch.utils.data import Dataset, DataLoader
import config
class TranslationDataset(Dataset):
def __init__(self, data_path):
self.data = pd.read_json(data_path, lines=True).to_dict(orient='records')
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
src = torch.tensor(self.data[idx]['zh'], dtype=torch.long)
tgt = torch.tensor(self.data[idx]['en'], dtype=torch.long)
return src, tgt
def get_dataloader(train=True):
path = config.PROCESSED_DATA_DIR / ('indexed_train.jsonl' if train else 'indexed_test.jsonl')
ds = TranslationDataset(path)
return DataLoader(ds, batch_size=config.BATCH_SIZE, shuffle=train)
5.5 模型定义 model.py(核心:Attention模块)
bash
"""
模型定义:编码器(双向GRU) + 注意力模块 + 解码器(单向GRU+Attention)
"""
import torch
import torch.nn as nn
import config
class Attention(nn.Module):
"""注意力机制模块(点积评分)"""
def forward(self, decoder_hidden, encoder_outputs):
"""
decoder_hidden: (1, batch, hidden_dim) 当前解码器隐藏状态
encoder_outputs: (batch, seq_len, hidden_dim*2) 编码器所有输出
返回 context: (batch, 1, hidden_dim)
"""
# 步骤1:计算注意力分数 (batch, 1, seq_len)
# 将 decoder_hidden 转置为 (batch, 1, hidden)
# 将 encoder_outputs 转置为 (batch, hidden, seq_len)
scores = torch.bmm(
decoder_hidden.transpose(0, 1), # (batch, 1, hidden)
encoder_outputs.transpose(1, 2) # (batch, hidden, seq_len)
)
# 步骤2:Softmax归一化得到权重
weights = torch.softmax(scores, dim=2) # (batch, 1, seq_len)
# 步骤3:加权求和得到上下文向量
context = torch.bmm(weights, encoder_outputs) # (batch, 1, hidden)
return context
class TranslationEncoder(nn.Module):
"""编码器:双向GRU"""
def __init__(self, vocab_size, padding_idx):
super().__init__()
self.embedding = nn.Embedding(vocab_size, config.EMBEDDING_DIM, padding_idx=padding_idx)
self.rnn = nn.GRU(config.EMBEDDING_DIM, config.ENCODER_HIDDEN_DIM,
num_layers=config.ENCODER_LAYERS,
batch_first=True, bidirectional=True)
def forward(self, src):
embedded = self.embedding(src) # (batch, seq_len, embed_dim)
outputs, hidden = self.rnn(embedded)
# outputs: (batch, seq_len, hidden_dim*2)
# hidden: (2*num_layers, batch, hidden_dim)
return outputs, hidden
class TranslationDecoder(nn.Module):
"""解码器:单向GRU + Attention"""
def __init__(self, vocab_size, padding_idx):
super().__init__()
self.embedding = nn.Embedding(vocab_size, config.EMBEDDING_DIM, padding_idx=padding_idx)
self.rnn = nn.GRU(config.EMBEDDING_DIM, config.DECODER_HIDDEN_DIM, batch_first=True)
self.attention = Attention()
self.linear = nn.Linear(config.DECODER_HIDDEN_DIM * 2, vocab_size)
def forward(self, tgt, hidden, encoder_outputs):
"""
tgt: (batch, 1) 当前输入token
hidden: (1, batch, decoder_hidden_dim) 上一步隐藏状态
encoder_outputs: (batch, seq_len, encoder_hidden_dim*2)
"""
embedded = self.embedding(tgt) # (batch, 1, embed_dim)
output, new_hidden = self.rnn(embedded, hidden) # output: (batch, 1, dec_hidden)
context = self.attention(new_hidden, encoder_outputs) # (batch, 1, dec_hidden)
combined = torch.cat((output, context), dim=2) # (batch, 1, dec_hidden*2)
logits = self.linear(combined) # (batch, 1, vocab_size)
return logits, new_hidden
5.6 训练脚本 train.py
bash
"""
训练主程序:包含一个epoch的训练函数和主循环
"""
import time
from itertools import chain
import torch
import torch.nn as nn
from torch.utils.tensorboard import SummaryWriter
from tqdm import tqdm
from dataset import get_dataloader
from tokenizer import ChineseTokenizer, EnglishTokenizer
import config
from model import TranslationEncoder, TranslationDecoder
def train_one_epoch(dataloader, encoder, decoder, loss_fn, optimizer, device):
encoder.train()
decoder.train()
total_loss = 0
for src, tgt in tqdm(dataloader, desc="训练"):
src = src.to(device) # (batch, seq_len)
tgt = tgt.to(device) # (batch, seq_len)
optimizer.zero_grad()
# 编码器前向
encoder_outputs, encoder_hidden = encoder(src)
# encoder_hidden: (2, batch, enc_hidden) 因为双向
# 将双向最后一层的两个方向拼接,作为解码器的初始隐藏状态
forward_hidden = encoder_hidden[-2] # (batch, enc_hidden)
backward_hidden = encoder_hidden[-1] # (batch, enc_hidden)
decoder_hidden = torch.cat([forward_hidden, backward_hidden], dim=1) # (batch, dec_hidden)
decoder_hidden = decoder_hidden.unsqueeze(0) # (1, batch, dec_hidden)
# 解码器逐词预测
decoder_input = tgt[:, 0:1] # 第一个输入是 <sos>
decoder_outputs = []
for step in range(1, config.SEQ_LEN):
logits, decoder_hidden = decoder(decoder_input, decoder_hidden, encoder_outputs)
decoder_outputs.append(logits)
decoder_input = tgt[:, step:step+1] # teacher forcing
decoder_outputs = torch.cat(decoder_outputs, dim=1) # (batch, seq_len-1, vocab_size)
targets = tgt[:, 1:] # 去掉开头的<sos>
loss = loss_fn(decoder_outputs.reshape(-1, decoder_outputs.shape[-1]), targets.reshape(-1))
loss.backward()
optimizer.step()
total_loss += loss.item()
return total_loss / len(dataloader)
def train():
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")
train_loader = get_dataloader(train=True)
zh_tokenizer = ChineseTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'zh_vocab.txt')
en_tokenizer = EnglishTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'en_vocab.txt')
encoder = TranslationEncoder(zh_tokenizer.vocab_size, zh_tokenizer.pad_token_index).to(device)
decoder = TranslationDecoder(en_tokenizer.vocab_size, en_tokenizer.pad_token_index).to(device)
loss_fn = nn.CrossEntropyLoss(ignore_index=en_tokenizer.pad_token_index)
optimizer = torch.optim.Adam(chain(encoder.parameters(), decoder.parameters()), lr=config.LEARNING_RATE)
writer = SummaryWriter(log_dir=config.LOGS_DIR / time.strftime('%Y-%m-%d_%H-%M-%S'))
best_loss = float('inf')
for epoch in range(1, config.EPOCHS+1):
print(f"\n========== Epoch {epoch} ==========")
avg_loss = train_one_epoch(train_loader, encoder, decoder, loss_fn, optimizer, device)
print(f"平均损失: {avg_loss:.4f}")
writer.add_scalar('Loss', avg_loss, epoch)
if avg_loss < best_loss:
best_loss = avg_loss
torch.save(encoder.state_dict(), config.MODELS_DIR / 'encoder.pt')
torch.save(decoder.state_dict(), config.MODELS_DIR / 'decoder.pt')
print("保存最佳模型")
writer.close()
if __name__ == '__main__':
train()
5.7 预测脚本 predict.py
bash
"""
交互式翻译预测:加载训练好的模型,对输入的中文句子进行翻译
"""
import torch
from tokenizer import ChineseTokenizer, EnglishTokenizer
from model import TranslationEncoder, TranslationDecoder
import config
def predict_batch(input_tensor, encoder, decoder, en_tokenizer, device):
encoder.eval()
decoder.eval()
with torch.no_grad():
encoder_outputs, encoder_hidden = encoder(input_tensor)
# 初始化解码器隐藏状态
forward_hidden = encoder_hidden[-2]
backward_hidden = encoder_hidden[-1]
decoder_hidden = torch.cat([forward_hidden, backward_hidden], dim=1).unsqueeze(0)
batch_size = input_tensor.size(0)
decoder_input = torch.full((batch_size, 1), en_tokenizer.sos_token_index, device=device)
generated = [[] for _ in range(batch_size)]
finished = [False] * batch_size
for _ in range(1, config.SEQ_LEN):
logits, decoder_hidden = decoder(decoder_input, decoder_hidden, encoder_outputs)
preds = logits.argmax(dim=-1) # (batch, 1)
for i in range(batch_size):
if finished[i]:
continue
token_id = preds[i].item()
if token_id == en_tokenizer.eos_token_index:
finished[i] = True
else:
generated[i].append(token_id)
if all(finished):
break
decoder_input = preds
return generated
def predict_sentence(zh_sentence, encoder, decoder, zh_tokenizer, en_tokenizer, device):
input_ids = zh_tokenizer.encode(zh_sentence, config.SEQ_LEN, add_sos_eos=False)
input_tensor = torch.tensor([input_ids], device=device)
gen = predict_batch(input_tensor, encoder, decoder, en_tokenizer, device)
return en_tokenizer.decode(gen[0])
def run_predict():
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
zh_tokenizer = ChineseTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'zh_vocab.txt')
en_tokenizer = EnglishTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'en_vocab.txt')
encoder = TranslationEncoder(zh_tokenizer.vocab_size, zh_tokenizer.pad_token_index).to(device)
decoder = TranslationDecoder(en_tokenizer.vocab_size, en_tokenizer.pad_token_index).to(device)
encoder.load_state_dict(torch.load(config.MODELS_DIR / 'encoder.pt', map_location=device))
decoder.load_state_dict(torch.load(config.MODELS_DIR / 'decoder.pt', map_location=device))
print("翻译系统已启动,输入中文句子(输入 q 退出):")
while True:
zh = input("中文: ").strip()
if zh in ('q', 'quit'):
break
if not zh:
continue
en = predict_sentence(zh, encoder, decoder, zh_tokenizer, en_tokenizer, device)
print(f"英文: {en}")
if __name__ == '__main__':
run_predict()
5.8 评估脚本 evaluate.py
bash
"""
使用BLEU分数评估模型性能
"""
import torch
from nltk.translate.bleu_score import corpus_bleu
from tqdm import tqdm
import config
from tokenizer import ChineseTokenizer, EnglishTokenizer
from model import TranslationEncoder, TranslationDecoder
from dataset import get_dataloader
from predict import predict_batch
def evaluate():
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
zh_tokenizer = ChineseTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'zh_vocab.txt')
en_tokenizer = EnglishTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'en_vocab.txt')
encoder = TranslationEncoder(zh_tokenizer.vocab_size, zh_tokenizer.pad_token_index).to(device)
decoder = TranslationDecoder(en_tokenizer.vocab_size, en_tokenizer.pad_token_index).to(device)
encoder.load_state_dict(torch.load(config.MODELS_DIR / 'encoder.pt', map_location=device))
decoder.load_state_dict(torch.load(config.MODELS_DIR / 'decoder.pt', map_location=device))
test_loader = get_dataloader(train=False)
refs = []
hyps = []
specials = {zh_tokenizer.pad_token_index, zh_tokenizer.sos_token_index, zh_tokenizer.eos_token_index}
for src, tgt in tqdm(test_loader, desc="评估"):
src = src.to(device)
pred_idxs = predict_batch(src, encoder, decoder, en_tokenizer, device)
hyps.extend(pred_idxs)
for seq in tgt:
filtered = [idx.item() for idx in seq if idx.item() not in specials]
refs.append([filtered])
bleu = corpus_bleu(refs, hyps)
print(f"\nBLEU-4 分数: {bleu:.4f}")
if __name__ == '__main__':
evaluate()
六、运行你的翻译模型
-
依次运行:python src/process.py python src/train.py python src/predict.py
-
训练大约30个epoch后,模型会保存在 models/ 目录下。然后运行 predict.py 就可以交互式翻译了。

完整代码下载:pan.baidu.com/s/10pbHn5UW...
七、Attention机制的两大遗留问题
尽管Attention机制极大地提升了Seq2Seq的能力,但它仍然建立在RNN(或GRU/LSTM)的基础之上,因此无法避免RNN固有的缺陷:
问题一:计算过程无法并行
RNN必须按时间步顺序执行:计算第t步需要第t-1步的输出。这种强依赖关系导致GPU的并行能力无法充分发挥,训练速度慢,尤其对于长序列更是如此。
问题二:长期依赖问题仍未根除
虽然Attention让解码器可以直接"看到"所有编码器状态,但编码器内部仍然要靠RNN一步步传递信息。对于超长序列(比如几百个词的句子),RNN依然会面临梯度消失或梯度爆炸,难以真正捕捉远距离依赖。
正是为了解决这两个问题,Google在2017年提出了Transformer 架构------彻底抛弃RNN,完全基于自注意力(Self-Attention) 来建模。Transformer通过多头注意力和位置编码,实现了全局并行计算和超长距离依赖建模,成为了今天GPT、BERT等大模型的基石。
八、总结与思考
从最初"死记硬背"的固定向量,到"动态查阅"的注意力机制,再到彻底革命的自注意力Transformer,机器翻译乃至整个NLP领域经历了一场深刻的思维变革。
Attention机制的核心贡献在于:它让模型学会了选择性关注,不再平等对待所有输入信息,而是根据当前任务需求动态分配计算资源。这种思想不仅适用于机器翻译,在图像描述、语音识别、推荐系统等领域也同样大放异彩。
通过本文的代码实战,你可以亲手体验Attention的力量:即使是一个简单的GRU模型,加上注意力模块后,翻译长句的流畅度和准确度都会有肉眼可见的提升。而如果你有兴趣,还可以进一步尝试替换评分函数(比如改成General或Concat),或者实现Beam Search解码,看看能带来多少改善。
Attention机制不是终点,但它无疑是通向现代深度学习的一扇重要大门。希望这篇文章能帮你把这扇门推开,看到更广阔的风景。