“Datawhale AI 夏令营”--学习解决机器翻译任务--笔记

大致流程:

先看懂了代码 才能自己做改进,所以第一步先来分析理解一下代码。

一、配置环境 

!pip install torchtext
!pip install jieba
!pip install sacrebleu

!pip install -U pip setuptools wheel -i https://pypi.tuna.tsinghua.edu.cn/simple
!pip install -U 'spacy[cuda12x]' -i https://pypi.tuna.tsinghua.edu.cn/simple
!pip install ../dataset/en_core_web_trf-3.7.3-py3-none-any.whl
!python -m spacy download en_core_web_sm
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.nn.utils import clip_grad_norm_
from torchtext.data.metrics import bleu_score
from torch.utils.data import Dataset, DataLoader
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
from typing import List, Tuple
import jieba
import random
from torch.nn.utils.rnn import pad_sequence
import sacrebleu
import time
import math

 笔记:

 使用的是魔塔平台的notebook,下面详细介绍一下这几个包:

  • torchtext :是一个用于自然语言处理(NLP)任务的库,它提供了丰富的功能,包括数据预处理、词汇构建、序列化和批处理等,特别适合于文本分类、情感分析、机器翻译等任务

  • jieba是一个中文分词库,用于将中文文本切分成有意义的词语

  • sacrebleu:用于评估机器翻译质量的工具,主要通过计算BLEU(Bilingual Evaluation Understudy)得分来衡量生成文本与参考译文之间的相似度

  • spacy:是一个强大的自然语言处理库,支持70+语言的分词与训练(这里主要用于英文的分词)

第二段代码导入了很多模块,看不明白没关系 ,后面的处理中都会用到的,跟着我一起慢慢的了解它们吧!

二、数据预处理 

# 定义tokenizer
en_tokenizer = get_tokenizer('spacy', language='en_core_web_trf')
zh_tokenizer = lambda x: list(jieba.cut(x))  # 使用jieba分词

笔记:

get_tokenizer函数用于获取一个文本分词器,它可以根据指定的参数返回不同的分词器。

get_tokenizer('spacy', language='en_core_web_trf'),指定了使用spacy中的en_core_web_trf模型,这是一个英文的分词模型,适合用于处理英文文本,返回的是一个函数,这个函数可以用于将英文文本转换为单词的列表。

lambda x: list(jieba.cut(x))是一个lambda函数,它接受一个字符串x作为输入,并使用jieba.cut方法对字符串进行分词,返回一个迭代器,可以用于将中文文本转换为词语的列表。

# 读取数据函数
def read_data(file_path: str) -> List[str]:
    with open(file_path, 'r', encoding='utf-8') as f:
        return [line.strip() for line in f]

# 数据预处理函数
def preprocess_data(en_data: List[str], zh_data: List[str]) -> List[Tuple[List[str], List[str]]]:
    processed_data = []
    for en, zh in zip(en_data, zh_data):
        en_tokens = en_tokenizer(en.lower())[:MAX_LENGTH]
        zh_tokens = zh_tokenizer(zh)[:MAX_LENGTH]
        if en_tokens and zh_tokens:  # 确保两个序列都不为空
            processed_data.append((en_tokens, zh_tokens))
    return processed_data

# 构建词汇表
def build_vocab(data: List[Tuple[List[str], List[str]]]):
    en_vocab = build_vocab_from_iterator(
        (en for en, _ in data),
        specials=['<unk>', '<pad>', '<bos>', '<eos>']
    )
    zh_vocab = build_vocab_from_iterator(
        (zh for _, zh in data),
        specials=['<unk>', '<pad>', '<bos>', '<eos>']
    )
    en_vocab.set_default_index(en_vocab['<unk>'])
    zh_vocab.set_default_index(zh_vocab['<unk>'])
    return en_vocab, zh_vocab

 笔记:

数据预处理函数:接受两组数据:一组是英文数据,一组是中文数据。它使用两个分词器en_tokenizerzh_tokenizer(前面定义的)对每组数据进行分词,并截断到指定的最大长度(MAX_LENGTH)。如果两个序列都不为空,则将它们作为一对数据添加到processed_data列表中。最后,函数返回处理后的数据列表。

构建词汇表函数

  1. 词汇表(Vocabulary):在自然语言处理中,词汇表是一个包含所有唯一单词的集合,以及每个单词对应的索引。在文本到索引的转换过程中,词汇表用于将单词映射到其对应的数字表示。

  2. 特殊标记(Special Tokens):在构建词汇表时,通常会添加一些特殊标记,如<unk>(未知单词)、<pad>(填充)、<bos>(句子开始)、<eos>(句子结束)。这些特殊标记用于处理输入序列的开始和结束,以及未知单词的编码。

  3. build_vocab_from_iterator函数用来构建词汇表,并在构建词汇表时,添加一些特殊标记。

  4. set_default_index函数:在构建词汇表时,每个单词都分配了一个唯一的索引。但是,如果遇到一个不在词汇表中的单词,我们需要一个默认索引来表示它。set_default_index函数将词汇表的默认索引设置为<unk>的索引。

class TranslationDataset(Dataset):
    def __init__(self, data: List[Tuple[List[str], List[str]]], en_vocab, zh_vocab):
        self.data = data
        self.en_vocab = en_vocab
        self.zh_vocab = zh_vocab

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        en, zh = self.data[idx]
        en_indices = [self.en_vocab['<bos>']] + [self.en_vocab[token] for token in en] + [self.en_vocab['<eos>']]
        zh_indices = [self.zh_vocab['<bos>']] + [self.zh_vocab[token] for token in zh] + [self.zh_vocab['<eos>']]
        return en_indices, zh_indices

 笔记:这一步是将词汇表转换成数据集,目的是为了将文本数据转换成模型可以处理的数字表示。

 

def collate_fn(batch):
    en_batch, zh_batch = [], []
    for en_item, zh_item in batch:
        if en_item and zh_item:  # 确保两个序列都不为空
            # print("都不为空")
            en_batch.append(torch.tensor(en_item))
            zh_batch.append(torch.tensor(zh_item))
        else:
            print("存在为空")
    if not en_batch or not zh_batch:  # 如果整个批次为空,返回空张量
        return torch.tensor([]), torch.tensor([])
    
    # src_sequences = [item[0] for item in batch]
    # trg_sequences = [item[1] for item in batch]
    
    en_batch = nn.utils.rnn.pad_sequence(en_batch, batch_first=True, padding_value=en_vocab['<pad>'])
    zh_batch = nn.utils.rnn.pad_sequence(zh_batch, batch_first=True, padding_value=zh_vocab['<pad>'])

    # en_batch = pad_sequence(en_batch, batch_first=True, padding_value=en_vocab['<pad>'])
    # zh_batch = pad_sequence(zh_batch, batch_first=True, padding_value=zh_vocab['<pad>'])
    
    return en_batch, zh_batch

 笔记:

1.这个函数用于将一个批次的数据(batch)转换成一个PyTorch张量(tensor)。在深度学习中,当使用模型进行训练时,数据通常是以批量的形式输入的。这个函数确保了所有批量的数据都被正确地处理和打包。

2.nn.utils.rnn.pad_sequence函数用于将列表中的所有序列(在本例中是英文和中文句子的整数索引)合并成一个批次,并且填充到相同的长度。batch_first=True表示第一个维度是批次大小,padding_value是用来填充序列末尾的值,通常是<pad>标记的索引。

3.什么是张量呢:张量是多维数组,用于表示各种类型的数学对象,包括标量、向量、矩阵、多维数组等。特别是在深度学习中,张量是数据的基本表示形式。PyTorch和TensorFlow等深度学习框架广泛使用张量来表示和操作数据。

# 数据加载函数
def load_data(train_path: str, dev_en_path: str, dev_zh_path: str, test_en_path: str):
    # 读取训练数据
    train_data = read_data(train_path)
    train_en, train_zh = zip(*(line.split('\t') for line in train_data))
    
    # 读取开发集和测试集
    dev_en = read_data(dev_en_path)
    dev_zh = read_data(dev_zh_path)
    test_en = read_data(test_en_path)

    # 预处理数据
    train_processed = preprocess_data(train_en, train_zh)
    dev_processed = preprocess_data(dev_en, dev_zh)
    test_processed = [(en_tokenizer(en.lower())[:MAX_LENGTH], []) for en in test_en if en.strip()]

    # 构建词汇表
    global en_vocab, zh_vocab
    en_vocab, zh_vocab = build_vocab(train_processed)

    # 创建数据集
    train_dataset = TranslationDataset(train_processed, en_vocab, zh_vocab)
    dev_dataset = TranslationDataset(dev_processed, en_vocab, zh_vocab)
    test_dataset = TranslationDataset(test_processed, en_vocab, zh_vocab)
    
    from torch.utils.data import Subset

    # 假设你有10000个样本,你只想用前1000个样本进行测试
    indices = list(range(N))
    train_dataset = Subset(train_dataset, indices)

    # 创建数据加载器
    train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_fn, drop_last=True)
    dev_loader = DataLoader(dev_dataset, batch_size=BATCH_SIZE, collate_fn=collate_fn, drop_last=True)
    test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, collate_fn=collate_fn, drop_last=True)

    return train_loader, dev_loader, test_loader, en_vocab, zh_vocab

笔记: 这个函数算是对上面的总结,也增加了一些新函数。

1.DataLoader函数用来创建训练、开发和测试的数据加载器。batch_size指定每个批次的大小,shuffle=True表示在每次迭代时打乱数据顺序,collate_fn用于批处理,drop_last=True表示如果最后一个批次的大小小于批次大小,则丢弃该批次。

2.可以看出Subset是用来取数据集的一部分。

三、模型的构建

class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()
        self.hid_dim = hid_dim
        self.n_layers = n_layers
        
        self.embedding = nn.Embedding(input_dim, emb_dim)
        self.gru = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, src):
        # src = [batch size, src len]
        embedded = self.dropout(self.embedding(src))
        # embedded = [batch size, src len, emb dim]
        
        outputs, hidden = self.gru(embedded)
        # outputs = [batch size, src len, hid dim * n directions]
        # hidden = [n layers * n directions, batch size, hid dim]
        
        return outputs, hidden

class Attention(nn.Module):
    def __init__(self, hid_dim):
        super().__init__()
        self.attn = nn.Linear(hid_dim * 2, hid_dim)
        self.v = nn.Linear(hid_dim, 1, bias=False)
        
    def forward(self, hidden, encoder_outputs):
        # hidden = [1, batch size, hid dim]
        # encoder_outputs = [batch size, src len, hid dim]
        
        batch_size = encoder_outputs.shape[0]
        src_len = encoder_outputs.shape[1]
        
        hidden = hidden.repeat(src_len, 1, 1).transpose(0, 1)
        # hidden = [batch size, src len, hid dim]
        
        energy = torch.tanh(self.attn(torch.cat((hidden, encoder_outputs), dim=2)))
        # energy = [batch size, src len, hid dim]
        
        attention = self.v(energy).squeeze(2)
        # attention = [batch size, src len]
        
        return F.softmax(attention, dim=1)

class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout, attention):
        super().__init__()
        self.output_dim = output_dim
        self.hid_dim = hid_dim
        self.n_layers = n_layers
        self.attention = attention
        
        self.embedding = nn.Embedding(output_dim, emb_dim)
        self.gru = nn.GRU(hid_dim + emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True)
        self.fc_out = nn.Linear(hid_dim * 2 + emb_dim, output_dim)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, input, hidden, encoder_outputs):
        # input = [batch size, 1]
        # hidden = [n layers, batch size, hid dim]
        # encoder_outputs = [batch size, src len, hid dim]
        
        input = input.unsqueeze(1)
        embedded = self.dropout(self.embedding(input))
        # embedded = [batch size, 1, emb dim]
        
        a = self.attention(hidden[-1:], encoder_outputs)
        # a = [batch size, src len]
        
        a = a.unsqueeze(1)
        # a = [batch size, 1, src len]
        
        weighted = torch.bmm(a, encoder_outputs)
        # weighted = [batch size, 1, hid dim]
        
        rnn_input = torch.cat((embedded, weighted), dim=2)
        # rnn_input = [batch size, 1, emb dim + hid dim]
        
        output, hidden = self.gru(rnn_input, hidden)
        # output = [batch size, 1, hid dim]
        # hidden = [n layers, batch size, hid dim]
        
        embedded = embedded.squeeze(1)
        output = output.squeeze(1)
        weighted = weighted.squeeze(1)
        
        prediction = self.fc_out(torch.cat((output, weighted, embedded), dim=1))
        # prediction = [batch size, output dim]
        
        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):
        # src = [batch size, src len]
        # trg = [batch size, trg len]
        
        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)
        encoder_outputs, hidden = self.encoder(src)
        
        input = trg[:, 0]
        
        for t in range(1, trg_len):
            output, hidden = self.decoder(input, hidden, encoder_outputs)
            outputs[:, t] = output
            teacher_force = random.random() < teacher_forcing_ratio
            top1 = output.argmax(1)
            input = trg[:, t] if teacher_force else top1
        
        return outputs

 笔记:

1.这个是task2用到的模型,实现了一个经典的序列到序列(Seq2Seq)模型,中间层使用的GRU网络,并且网络中加入了注意力机制(Attention Mechanism)。大致过程就是先编码(encoder),再解码(decoder)。

2.注意力机制作用:它允许模型在不同输入特征之间分配不同的权重,从而增强重要特征的表示,并抑制不重要特征的影响。

3.task3使用的是Transformer 模型,task2用到的模型在建模文本长程依赖方面存在一定的局限性,Transformer 模型摒弃了循环结构,并完全通过注意力机制完成对源语言序列和目标语言序列全局依赖的建模。在抽取每个单词的上下文特征时,Transformer 通过自注意力机制(self-attention)衡量上下文中每一个单词对当前单词的重要程度。

四、模型训练

def train(model, iterator, optimizer, criterion, clip):
    model.train()
    epoch_loss = 0
    
    for i, batch in enumerate(iterator):
        #print(f"Training batch {i}")
        src, trg = batch
        #print(f"Source shape before: {src.shape}, Target shape before: {trg.shape}")
        if src.numel() == 0 or trg.numel() == 0:
            #print("Empty batch detected, skipping...")
            continue  # 跳过空的批次
        
        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()
        
        clip_grad_norm_(model.parameters(), clip)
        optimizer.step()
        
        epoch_loss += loss.item()

    print(f"Average loss for this epoch: {epoch_loss / len(iterator)}")
    return epoch_loss / len(iterator)

def evaluate(model, iterator, criterion):
    model.eval()
    epoch_loss = 0
    with torch.no_grad():
        for i, batch in enumerate(iterator):
            #print(f"Evaluating batch {i}")
            src, trg = batch
            if src.numel() == 0 or trg.numel() == 0:
                continue  # 跳过空批次
            
            src, trg = src.to(DEVICE), trg.to(DEVICE)
            
            output = model(src, trg, 0)  # 关闭 teacher forcing
            
            output_dim = output.shape[-1]
            output = output[:, 1:].contiguous().view(-1, output_dim)
            trg = trg[:, 1:].contiguous().view(-1)
            
            loss = criterion(output, trg)
            epoch_loss += loss.item()
        
    return epoch_loss / len(iterator)

笔记:

1.定义了训练函数和评估函数,每次训练一轮后输出在训练集和开发集上的损失,通过两者的比较可以判断是否过拟合,是否可以停止继续训练。

2.源语言句子(src)和目标语言句子(trg

3.src, trg = src.to(DEVICE), trg.to(DEVICE):将批次移动到指定的设备(通常是GPU)

4.其实是深度学习中神经网络的训练过程:前向传播:output = model(src, trg),计算损失:loss = criterion(output, trg),反向传播:loss.backward(),clip_grad_norm_(model.parameters(), clip):梯度裁剪以防止梯度爆炸,更新参数:optimizer.step()

五、模型翻译

def translate_sentence(src_indexes, src_vocab, tgt_vocab, model, device, max_length=50):
    model.eval()
    
    src_tensor = src_indexes.unsqueeze(0).to(device)  # 添加批次维度
    
    # with torch.no_grad():
    #     encoder_outputs = model.encoder(model.positional_encoding(model.src_embedding(src_tensor) * math.sqrt(model.d_model)))

    trg_indexes = [tgt_vocab['<bos>']]
    for i in range(max_length):
        trg_tensor = torch.LongTensor(trg_indexes).unsqueeze(0).to(device)
        # print("src_tensor:",src_tensor)
        # print("trg_tensor:",trg_tensor)
        with torch.no_grad():
            output = model(src_tensor, trg_tensor)
        
        pred_token = output.argmax(2)[:, -1].item()
        trg_indexes.append(pred_token)
        
        if pred_token == tgt_vocab['<eos>']:
            break
    
    trg_tokens = [tgt_vocab.get_itos()[i] for i in trg_indexes]
    return trg_tokens[1:-1]  # 移除<bos>和<eos>标记

笔记: 

  1. 在这个函数的循环中,我们预测目标语言的下一个词。我们首先将trg_indexes列表转换为一个张量,并将其移动到指定的设备上。然后,我们使用模型来预测输出。output.argmax(2)[:, -1]获取最后一个词的预测概率最高的词,而output.argmax(2)[:, -1].item()将其转换为整数。如果预测的词是<eos>,则循环结束。

save_dir = '../results/submit_task2.txt'
with open(save_dir, 'w') as f:
    translated_sentences = []
    for batch in test_loader:  # 遍历所有数据
        src, _ = batch
        src = src.to(DEVICE)
        translated = translate_sentence(src[0], en_vocab, zh_vocab, model, DEVICE, max_length=50)  # 翻译结果,max_length生成翻译的最大长度
        #print(translated)
        results = "".join(translated)
        f.write(results + '\n')  # 将结果写入文件
    print(f"翻译完成,结果已保存到{save_dir}")

 笔记:遍历测试数据,用translate_sentence逐句翻译,到此,整个过程就结束了。

理解完了代码中每个函数的逻辑和作用,就开始上分了! 

六、上分历程

第一次还不是很明白,只跟着task1走了一遍,得分0.2889(训练数据集1000,训练了10轮)。

后来又用整个训练集训练,训练了20轮,得分1.7174。

看task2的时候决定着重理解代码,训练时发现第一轮用了62分钟,于是在助教的建议下去尝试了task3。我先用全部训练数据训练了10轮,发现在训练集和验证集上loss都一直在降,如果是之前不明白代码的情况下,我可能又重新增加epoch,从头开始再训练一遍。但现在我可以想到在train_model函数中把初始化模型替换成加载最佳模型,就可以接着训练啦,节省了很多重复训练的时间。于是又接着训练了10轮,loss降的越来越慢了,我想这虽然降的慢但也一直在降,于是又训了10轮,大概到第四轮就开始升了,后面就是升升降降,不太稳定了。不过结果还不错,经过这一波训练,得分11.9904,直接上了10分!

然后我就去看了一下翻译的文档,有很多“笑笑笑....”,“鼓掌”等重复的词语,结合task3中的上分技巧,我又尝试做一下数据清理。第一次清理完满怀期待的等待结果,12.1093分,没涨太多呢,我又去看了翻译完的文档,发现很多“()”,哈哈哈,只清理了关于声音的词,忘了带着括号一块清理了。然后又清理了一次,得分12.3434。

七、总结

整个学习过程还是很充实很有成就感的,从一无所知到能自己想方法改进代码,提高得分,一步一步的走下去,能有所收获,学到知识,就很成功了!

  • 29
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值