跳转至

基于术语词典干预的机器翻译

AI +X | Datawhale 学习指南:https://datawhaler.feishu.cn/wiki/X9AVwtmvyi87bIkYpi2cNGlIn3v

机器翻译的发展历程:基于规则的机器翻译(1950s-1980s)->基于统计的机器翻译(1990s-2000s)->基于神经网络机器翻译(2010s-present)

数据划分

在机器学习和深度学习项目中,数据集通常被划分为三个部分:训练集(Training Set)、开发集(Development Set,也常被称为验证集,Validation Set)和测试集(Test Set)

task-1_terminology.ipynb解释

TranslationDataset

__init__ 方法

  1. 初始化数据和术语
  2. self.data = []:初始化一个空列表,用于存储从文件中读取的数据。
  3. with open(filename, 'r', encoding='utf-8') as f::打开文件读取数据,假设文件中的每一行包含一对中英文翻译,使用制表符(\t)分隔。
  4. self.data.append((en, zh)):将读取的中英文对添加到 self.data 列表中。
  5. self.terminology = terminology:存储传入的术语词典。
  6. 创建词汇表
  7. self.en_tokenizer = get_tokenizer('basic_english'):使用 torchtext 提供的基本英文分词器。
  8. self.zh_tokenizer = list:使用字符级分词器来处理中文。
  9. 构建词汇表
  10. en_vocab = Counter(self.terminology.keys()):初始化英文词汇表,确保术语词典中的词被包含。
  11. zh_vocab = Counter():初始化中文词汇表。
  12. 更新词汇表
  13. 遍历数据中的每一对句子,更新英文和中文的词汇表。
  14. 添加特殊符号和术语到词汇表
  15. self.en_vocabself.zh_vocab:添加特殊符号(<pad><sos><eos>)和术语词典中的词,接着添加最常见的10000个词。
  16. self.en_word2idxself.zh_word2idx:创建从词到索引的映射词典。

__len__ 方法

  • 返回数据集的大小。

__getitem__ 方法

  • 获取数据集中指定索引的中英文对,并将其转换为张量(tensor)。
  • 添加特殊符号 <eos> 到每个句子的末尾。
  • 使用词汇表将每个单词转换为索引,默认情况下,如果单词不在词汇表中,使用 <sos> 索引。

collate_fn 函数

  • 这个函数用于在 DataLoader 中进行批处理。
  • 批处理填充
  • en_batchzh_batch:分别存储英文和中文句子的张量。
  • 使用 nn.utils.rnn.pad_sequence 对批次中的序列进行填充,确保每个序列的长度相同。填充值为0(即 <pad> 索引)。
# 定义数据集类
# 修改TranslationDataset类以处理术语
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

Encoder

__init__ 方法

  • 初始化编码器的各个层和参数:
  • self.embedding:定义一个嵌入层,将输入的索引转换为对应的嵌入向量。
  • self.rnn:定义一个带有多层和 dropout 的 GRU(门控循环单元)层,用于处理嵌入向量。
  • self.dropout:定义一个 dropout 层,用于正则化以防止过拟合。

forward 方法

  • 输入 src(形状为 [batch_size, src_len]),表示批次中的源序列。
  • 经过嵌入层和 dropout 处理,得到嵌入向量 embedded(形状为 [batch_size, src_len, emb_dim])。
  • 经过 GRU 层,得到 outputs(形状为 [batch_size, src_len, hid_dim])和 hidden(形状为 [n_layers, batch_size, hid_dim])。
  • 返回 outputshidden

Decoder

__init__ 方法

  • 初始化解码器的各个层和参数:
  • self.embedding:定义一个嵌入层,将输入的索引转换为对应的嵌入向量。
  • self.rnn:定义一个带有多层和 dropout 的 GRU 层,用于处理嵌入向量。
  • self.fc_out:定义一个全连接层,用于将 GRU 的输出映射到目标词汇表的大小。
  • self.dropout:定义一个 dropout 层,用于正则化。

forward 方法

  • 输入 input(形状为 [batch_size, 1]),表示批次中的目标序列的当前时间步的词索引。
  • 输入 hidden(形状为 [n_layers, batch_size, hid_dim]),表示编码器的隐藏状态。
  • 经过嵌入层和 dropout 处理,得到嵌入向量 embedded(形状为 [batch_size, 1, emb_dim])。
  • 经过 GRU 层,得到 output(形状为 [batch_size, 1, hid_dim])和 hidden(形状为 [n_layers, batch_size, hid_dim])。
  • 经过全连接层,得到预测 prediction(形状为 [batch_size, output_dim])。
  • 返回 predictionhidden

Seq2Seq

__init__ 方法

  • 初始化 seq2seq 模型的编码器、解码器和设备(如 CPU 或 GPU)。

forward 方法

  • 输入 src(形状为 [batch_size, src_len])和 trg(形状为 [batch_size, trg_len]),表示批次中的源序列和目标序列。
  • 初始化 outputs 张量,用于存储每个时间步的解码器输出(形状为 [batch_size, trg_len, trg_vocab_size])。
  • 使用编码器对源序列进行编码,得到 hidden(形状为 [n_layers, batch_size, hid_dim])。
  • 初始化解码器的输入为目标序列的起始标记(trg[:, 0].unsqueeze(1))。
  • 对于目标序列的每个时间步,使用解码器生成输出和新的隐藏状态:
  • 使用 teacher_forcing_ratio 决定是否使用真实的目标序列词作为下一时间步的输入(教师强制),或使用解码器的预测。
  • 返回 outputs,即每个时间步的预测结果。
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):
        # src shape: [batch_size, src_len]
        embedded = self.dropout(self.embedding(src))
        # embedded shape: [batch_size, src_len, emb_dim]
        outputs, hidden = self.rnn(embedded)
        # outputs shape: [batch_size, src_len, hid_dim]
        # hidden shape: [n_layers, batch_size, hid_dim]
        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):
        # input shape: [batch_size, 1]
        # hidden shape: [n_layers, batch_size, hid_dim]

        embedded = self.dropout(self.embedding(input))
        # embedded shape: [batch_size, 1, emb_dim]

        output, hidden = self.rnn(embedded, hidden)
        # output shape: [batch_size, 1, hid_dim]
        # hidden shape: [n_layers, batch_size, hid_dim]

        prediction = self.fc_out(output.squeeze(1))
        # prediction shape: [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 shape: [batch_size, src_len]
        # trg shape: [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)

        _, 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

load_terminology_dictionary

函数定义和参数

  • def load_terminology_dictionary(dict_file)::定义一个函数,接受一个参数 dict_file,表示包含术语词典的文件路径。

初始化术语词典

  • terminology = {}:初始化一个空字典 terminology,用于存储术语词典。

打开文件并读取内容

  • with open(dict_file, 'r', encoding='utf-8') as f::使用 utf-8 编码打开文件进行读取操作。
  • for line in f::逐行读取文件内容。

解析并存储术语对

  • en_term, ch_term = line.strip().split('\t'):去除每行首尾的空白字符,然后使用制表符(\t)将每行分割成英文术语和中文术语。
  • terminology[en_term] = ch_term:将英文术语作为键,中文术语作为值,存储到字典 terminology 中。

返回术语词典

  • return terminology:返回加载好的术语词典。

python # 新增术语词典加载部分 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

主函数

这个函数通过批次数据训练 seq2seq 模型,并返回平均训练损失。主要步骤包括前向传播、损失计算、反向传播、梯度裁剪和参数更新。可以在每个训练轮次(epoch)中调用这个函数以训练模型。

train

参数

  • model:要训练的 seq2seq 模型。
  • iterator:数据加载器,用于迭代数据批次。
  • optimizer:优化器,用于更新模型参数。
  • criterion:损失函数,用于计算预测值和真实值之间的误差。
  • clip:梯度裁剪的阈值,用于防止梯度爆炸。

函数内容

  1. 设置模型为训练模式python model.train() 这会启用 dropout 和 batch normalization。

  2. 初始化累积损失python epoch_loss = 0

  3. 迭代数据批次python for i, (src, trg) in enumerate(iterator): 使用数据加载器 iterator 获取每个批次的源序列 src 和目标序列 trg

  4. 将数据移到设备(CPU 或 GPU)python src, trg = src.to(device), trg.to(device)

  5. 梯度清零python optimizer.zero_grad()

  6. 前向传播python output = model(src, trg)

  7. 调整输出和目标形状python output_dim = output.shape[-1] output = output[:, 1:].contiguous().view(-1, output_dim) trg = trg[:, 1:].contiguous().view(-1)

    • output[:, 1:]:移除 <sos> token。
    • contiguous().view(-1, output_dim):调整 output 的形状以适应损失计算。
    • trg[:, 1:]:移除 <sos> token。
    • contiguous().view(-1):调整 trg 的形状以适应损失计算。
  8. 计算损失python loss = criterion(output, trg)

  9. 反向传播python loss.backward()

  10. 梯度裁剪python torch.nn.utils.clip_grad_norm_(model.parameters(), clip) 这可以防止梯度爆炸。

  11. 更新模型参数python optimizer.step()

  12. 累积损失python epoch_loss += loss.item()

返回平均损失

return epoch_loss / len(iterator)

将累积损失除以迭代次数,返回平均每批次的损失。

Task02

image-20240717200826293

第二次训练使用了第二份代码来运行,训练了两次但效果不佳哈哈哈,分数现在还没有评出来。对于我这种只是大概知道要划分训练集测试集的崽来讲,具体训练的方法还是晓得哈哈哈,于是这份笔记补充了一些资料。

首先是需要额外安装的几个包

torchtext

PyTorch 的一个扩展库,专门用于自然语言处理 (NLP) 任务。提供了一组工具和数据处理流水线,用于处理文本数据,构建词汇表,加载预训练的词嵌入,以及创建和操作文本数据集。

数据加载和预处理

  • 提供了从常见的文本格式(如 CSV、JSON、TSV 等)加载数据的功能。
  • 支持文本数据的分词、标记化、数字化等预处理操作。

词汇表

  • 允许创建和管理词汇表,包括计算词频、过滤低频词和创建词到索引的映射。
  • 支持从预训练的嵌入(如 GloVe, FastText)中加载词向量。

嵌入表示

  • 方便地加载和使用预训练的词嵌入。
  • 支持自定义词嵌入的训练和微调。

数据集和迭代器

  • 提供了常用 NLP 数据集的预定义加载器,如 IMDb, AG News, SQuAD 等。
  • 提供了灵活的数据迭代器,支持批处理和动态填充,方便与 PyTorch 的数据加载器集成。

Jieba

是一个非常流行的中文分词工具库,它能够高效地将中文文本切分成词语。由于中文没有明显的词边界,分词是自然语言处理 (NLP) 中的一个重要步骤。Jieba 提供了以下主要功能:

  1. 精确模式:试图将句子最精确地切开,适合文本分析。
  2. 全模式:把句子中所有可能的词语都扫描出来,速度非常快,但是不能解决歧义。
  3. 搜索引擎模式:在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。

SacreBLEU

是一个标准化和便捷的 BLEU 分数计算工具,主要用于机器翻译评估。

我们该次大赛的评分规则似乎就是基于BLUE来的

随后就是数据的预处理,清洗工作,训练验证模型。这是之前照猫画虎的一个图像识别系统

image-20240717204005759

对于数据量较大的模型(样本数量达到万级以上),对训练集,验证集,测试集的划分一般为为98:1:1,而万级以下数据的训练,最典型的划分比例是6:2:2。划分数据如下图所示。image-20240717204102240

循环神经网络、卷积神经网络、自注意力机制等都是模型常用的结构。

Task03

在task3的模型中,运用了Transformer ,Transformer基于循环卷积神经网络的序列到序列建模方法是现存机器翻译任务中的经典方法,对于经典的卷积神经网络

卷积神经网络算法作为一种经典的深度学习算法,又被称作为CNN算法.,最初是用来针对手写数字识别的,该是由多个层组成,主要包括输入层、卷积层、池化层、全连接层和输出层。

输入层代表着输入的数据,它可以是一维也可以是多维;卷积层在整个网络结构当中的位置处于最关键的位置,每个卷积层是由若干个卷积核组成,通过卷积运算对输入层的特征进行提取,并将其传递到下一层网络;卷积层后一般会加入池化层,池化层的作用是通过下采样的操作,将特征划分为多个矩阵,并在每个矩阵中取相应的特征值,来减少数据的维度而不改变数据的深度,从而达到减少参数数量提高运算速率的目的。具有平移不变性的特点;全连接层处于整个网络结构的尾端,在整个卷积神经网络模型当中起到“分类器”的作用。全连接层通过对卷积层或者池化层中具 有类别区分性的局部信息进行整合,将前一层的特征信息从高维空间转到低维空间,并将前一层的特征信息转化成输出值,同时可以通过 softmax逻辑回归进行对输出值分类,该层也可称为 softmax 层。

img

而对于Transformer,一张十分景点的图片展示了它的魅力:

cac07437-283a-4e79-a5a4-c2b40c2dc405

在调整过程中,最简单的就是调参,将 epochs 调大一点,使用全部训练集,以及调整模型的参数,如head、layers等。如果数据量允许,增加模型的深度(更多的编码器/解码器层)或宽度(更大的隐藏层尺寸),这通常可以提高模型的表达能力和翻译质量,尤其是在处理复杂或专业内容时。