Task01 笔记
Task01笔记
由比赛任务可知我们的任务是一个机器翻译任务,它是在术语字典的干预下进行的,通过术语词典的干预可以获得更好的结果。
很感谢datawhale提供的baseline,在这篇笔记中我首先会对baseline进行分析,分享我的理解,并在这个过程中穿插一些我掌握的nlp相关知识
1. 数据集类的定义
继承了torch
中的dataset类
,定义了TranslationDataset
类来处理英中翻译的数据集,主要包括以下功能:
- 读取并解析文件中的英中句子对。
- 创建词汇表,即token(包含术语词典中的词)。
- 通过token将中英文句子对转换为张量以便于输入模型进行训练。
class TranslationDataset(Dataset):
def __init__(self, filename, terminology):
# ...
self.data = []
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
en, zh = line.strip().split('\t')
self.data.append((en, zh))
self.terminology = terminology
# 创建词汇表,确保术语词典中的词也被包含在词汇表中
self.en_tokenizer = get_tokenizer('basic_english')
self.zh_tokenizer = list # 使用字符级分词
en_vocab = Counter(self.terminology.keys())
zh_vocab = Counter()
for en, zh in self.data:
en_vocab.update(self.en_tokenizer(en))
zh_vocab.update(self.zh_tokenizer(zh))
self.en_vocab = ['<pad>', '<sos>', '<eos>'] + list(self.terminology.keys()) + [word for word, _ in en_vocab.most_common(10000)]
self.zh_vocab = ['<pad>', '<sos>', '<eos>'] + [word for word, _ in zh_vocab.most_common(10000)]
self.en_word2idx = {word: idx for idx, word in enumerate(self.en_vocab)}
self.zh_word2idx = {word: idx for idx, word in enumerate(self.zh_vocab)}
def __len__(self):
return len(self.data)
def __getitem__(self, idx): # 获取中英文句子对以及索引
en, zh = self.data[idx]
en_tensor = torch.tensor([self.en_word2idx.get(word, self.en_word2idx['<sos>']) for word in self.en_tokenizer(en)] + [self.en_word2idx['<eos>']])
zh_tensor = torch.tensor([self.zh_word2idx.get(word, self.zh_word2idx['<sos>']) for word in self.zh_tokenizer(zh)] + [self.zh_word2idx['<eos>']])
return en_tensor, zh_tensor
def collate_fn(batch):
en_batch, zh_batch = [], []
for en_item, zh_item in batch:
en_batch.append(en_item)
zh_batch.append(zh_item)
en_batch = nn.utils.rnn.pad_sequence(en_batch, padding_value=0, batch_first=True)
zh_batch = nn.utils.rnn.pad_sequence(zh_batch, padding_value=0, batch_first=True)
return en_batch, zh_batch
2. 模型定义
定义了Encoder、Decoder和Seq2Seq模型。
解码器和编码器是解决序列问题的经典模型,Seq2Seq(Seq to Seq)模型是解码器-编码器结构的进一步改进。
RNN(Recurrent Neural Network),即循环神经网络,是一种适用于序列数据的神经网络,通过循环连接处理输入序列。编码器-解码器结构用于序列到序列任务中,编码器将输入序列编码为固定长度的上下文向量,解码器根据该向量生成输出序列。
GRU(Gated Recurrent Unit),即门控单元,是RNN的一种变体,通过更新门和重置门控制信息流动,解决了传统RNN中的梯度消失问题。LSTM(Long Short-Term Memory)是另一种RNN变体,包含输入门、遗忘门和输出门,通过这些门来保存和更新长期信息,更好地捕捉长距离依赖。
RNN适用于序列处理,但在长序列中存在梯度消失问题;GRU和LSTM通过门机制缓解该问题,广泛用于NLP等领域。
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.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
self.dropout = nn.Dropout(dropout)
def forward(self, src):
embedded = self.dropout(self.embedding(src))
outputs, hidden = self.rnn(embedded)
return outputs, hidden
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.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
self.fc_out = nn.Linear(hid_dim, output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, input, hidden):
embedded = self.dropout(self.embedding(input))
output, hidden = self.rnn(embedded, hidden)
prediction = self.fc_out(output.squeeze(1))
return prediction, hidden
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 = src.shape[0]
trg_len = trg.shape[1]
trg_vocab_size = self.decoder.output_dim
outputs = torch.zeros(batch_size, trg_len, trg_vocab_size).to(self.device)
_, hidden = self.encoder(src)
input = trg[:, 0].unsqueeze(1) # Start token
for t in range(1, trg_len):
output, hidden = self.decoder(input, hidden)
outputs[:, t, :] = output
teacher_force = random.random() < teacher_forcing_ratio
top1 = output.argmax(1)
input = trg[:, t].unsqueeze(1) if teacher_force else top1.unsqueeze(1)
return outputs
在Seq2Seq
类中运用到了Teacher Forcing
技术,它是一种在训练序列到序列(seq2seq)模型时使用的技术。它通过将目标序列的真实值作为解码器的下一步输入,而不是使用模型的预测值来加速训练过程。
在没有Teacher Forcing的情况下,模型生成每个时间步的输出,并将其用作下一个时间步的输入。这种方法可能导致误差累积,因为每个时间步的预测误差会影响后续时间步的预测。
Teacher Forcing通过使用真实目标值作为下一步的输入,确保解码器在每个时间步都接收到正确的上下文信息。这可以显著加快训练速度和稳定性。然而,这也会使模型在推理时面临更大的挑战,因为它必须在没有真实目标值作为输入的情况下生成整个序列。因此,在实际应用中,通常会在训练过程中逐渐减少Teacher Forcing的比例,以帮助模型适应不依赖真实值输入的生成过程。
3. 术语词典加载
定义了一个函数load_terminology_dictionary
来加载术语词典。
def load_terminology_dictionary(dict_file):
terminology = {}
with open(dict_file, 'r', encoding='utf-8') as f: # 打开术语词典进行读写
for line in f:
en_term, ch_term = line.strip().split('\t')
terminology[en_term] = ch_term
return terminology
4. 训练函数
定义了一个train
函数,用于训练模型。
def train(model, iterator, optimizer, criterion, clip): # 函数中传入了一个生成器
model.train()
epoch_loss = 0
for i, (src, trg) in enumerate(iterator):
src, trg = src.to(device), trg.to(device)
optimizer.zero_grad() # 清空梯度
output = model(src, trg)
output_dim = output.shape[-1]
output = output[:, 1:].contiguous().view(-1, output_dim)
trg = trg[:, 1:].contiguous().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)
5. 主程序
初始化数据集、模型、优化器和损失函数,进行模型训练并保存模型参数。
if __name__ == '__main__':
start_time = time.time() # 开始计时
# 如果是有GPU的环境,就可以使用cuda进行训练加速
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
terminology = load_terminology_dictionary('../dataset/en-zh.dic')
# 加载数据
dataset = TranslationDataset('../dataset/train.txt', terminology=terminology)
N = 1000 # 选择数据集的前N个样本进行训练,也可以修改为len(dataset)
subset_indices = list(range(N))
subset_dataset = Subset(dataset, subset_indices)
train_loader = DataLoader(subset_dataset, batch_size=32, shuffle=True, collate_fn=collate_fn)
# 定义模型超参数
INPUT_DIM = len(dataset.en_vocab)
OUTPUT_DIM = len(dataset.zh_vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
HID_DIM = 512
N_LAYERS = 2
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5
# 初始化模型
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT)
model = Seq2Seq(enc, dec, device).to(device)
# 定义优化器和损失函数
optimizer = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss(ignore_index=dataset.zh_word2idx['<pad>'])
# 训练模型
N_EPOCHS = 10
CLIP = 1
for epoch in range(N_EPOCHS):
train_loss = train(model, train_loader, optimizer, criterion, CLIP)
print(f'Epoch: {epoch+1:02} | Train Loss: {train_loss:.3f}')
# 保存模型
torch.save(model.state_dict(), './translation_model_GRU.pth')
end_time = time.time() # 结束计时
# 计算并打印运行时间
elapsed_time_minute = (end_time - start_time)/60
print(f"Total running time: {elapsed_time_minute:.2f} minutes")
6. BLEU
BLEU (Bilingual Evaluation Understudy) 是一种用于评估机器翻译质量的指标,通过计算候选译文与参考译文之间的 n-gram 重合度来衡量翻译的准确性。BLEU 分数介于 0 到 1 之间,通常表示为百分比形式,分数越高表示翻译质量越好。
BLEU 评分的计算过程
-
n-gram 重合度:
BLEU 评分基于 n-gram 重合度,即候选译文和参考译文中 n-gram 的重叠程度。n-gram 是长度为 n 的词序列,例如 "the cat" 是一个 2-gram,"the cat sat" 是一个 3-gram。 -
精确度 (Precision):
计算候选译文中每个 n-gram 与参考译文中相应 n-gram 的匹配个数,然后除以候选译文中的 n-gram 总数,得到每个 n-gram 的精确度。\( P_n = \frac{\sum_{ngram \in \text{候选译文}} \min(\text{候选译文中的 n-gram 计数}, \text{参考译文中的 n-gram 计数})}{\sum_{ngram \in \text{候选译文}} \text{候选译文中的 n-gram 计数}} \)
-
BP (Brevity Penalty):
为了防止候选译文过短,BLEU 评分引入了长度惩罚项。若候选译文长度小于参考译文,BP 会降低 BLEU 分数。[\(BP = \begin{cases} 1 & \text{如果候选译文长度 > 参考译文长度} \\ e^{1-\frac{\text{参考译文长度}}{\text{候选译文长度}}} & \text{如果候选译文长度 <= 参考译文长度} \end{cases}\)]
-
BLEU 分数:
最终的 BLEU 分数通过将不同 n-gram 的精确度取几何平均后,再乘以 BP 计算得出。\( \text{BLEU} = BP \cdot \exp \left( \sum_{n=1}^{N} w_n \log P_n \right) \)
其中 (\(w_n\)) 是每个 n-gram 的权重,通常设置为均匀分布,即 (\(w_n = \frac{1}{N}\))。
通过计算模型在验证集上的 BLEU 评分,可以衡量模型的翻译质量并指导模型的改进。定义了一个evaluate_bleu
函数来计算模型在开发集上的BLEU评分。
import torch
from sacrebleu.metrics import BLEU
from typing import List
def load_sentences(file_path: str) -> List[str]:
with open(file_path, 'r', encoding='utf-8') as f:
return [line.strip() for line in f]
def evaluate_bleu(model, dataset, dataloader):
model.eval()
references, candidates = [], []
with torch.no_grad():
for src, trg in dataloader:
src, trg = src.to(model.device), trg.to(model.device)
output = model(src, trg, teacher_forcing_ratio=0)
output_dim = output.shape[-1]
output = output.argmax(dim=-1)
for i in range(len(trg)):
references.append([dataset.zh_tokenizer(trg[i])])
candidates.append([dataset.zh_tokenizer(output[i])])
bleu = BLEU()
return bleu.corpus_score(candidates, references)
if __name__ == '__main__':
terminology = load_terminology_dictionary('../dataset/en-zh.dic')
dataset = TranslationDataset('../dataset/valid.txt', terminology=terminology)
valid_loader = DataLoader(dataset, batch_size=32, shuffle=False, collate_fn=collate_fn)
INPUT_DIM = len(dataset.en_vocab)
OUTPUT_DIM = len(dataset.zh_vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
HID_DIM = 512
N_LAYERS = 2
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT)
model = Seq2Seq(enc, dec, device).to(device)
model.load_state_dict(torch.load('./translation_model_GRU.pth'))
bleu_score = evaluate_bleu(model, dataset, valid_loader)
print(f'BLEU score: {bleu_score.score:.2f}')
7. 推理部分
定义了一个translate_sentence
函数来翻译单个句子,并优先使用术语词典中的术语。
def translate_sentence(sentence, model, dataset, max_len=50):
model.eval()
tokens = dataset.en_tokenizer(sentence)
tokens = [dataset.en_word2idx['<sos>']] + [dataset.en_word2idx.get(token, dataset.en_word2idx['<sos>']) for token in tokens] + [dataset.en_word2idx['<eos>']]
src_tensor = torch.LongTensor(tokens).unsqueeze(0).to(model.device)
with torch.no_grad():
encoder_outputs, hidden = model.encoder(src_tensor)
trg_indexes = [dataset.zh_word2idx['<sos>']]
for _ in range(max_len):
trg_tensor = torch.LongTensor([trg_indexes[-1]]).unsqueeze(0).to(model.device)
with torch.no_grad():
output, hidden = model.decoder(trg_tensor, hidden)
pred_token = output.argmax(1).item()
trg_indexes.append(pred_token)
if pred_token == dataset.zh_word2idx['<eos>']:
break
trg_tokens = [dataset.zh_vocab[i] for i in trg_indexes]
return trg_tokens[1:-1] # 返回去除<eos>和<sos>的部分
if __name__ == '__main__':
terminology = load_terminology_dictionary('../dataset/en-zh.dic')
dataset = TranslationDataset('../dataset/test.txt', terminology=terminology)
INPUT_DIM = len(dataset.en_vocab)
OUTPUT_DIM = len(dataset.zh_vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
HID_DIM = 512
N_LAYERS = 2
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT)
model = Seq2Seq(enc, dec, device).to(device)
model.load_state_dict(torch.load('./translation_model_GRU.pth'))
with open('../dataset/test.txt', 'r', encoding='utf-8') as f, open('./output.txt', 'w', encoding='utf-8') as out_f:
for line in f:
en_sentence = line.strip()
zh_sentence = translate_sentence(en_sentence, model, dataset)
out_f.write(' '.join(zh_sentence) + '\n')
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY