2024年Datawhale-AI夏令营-基于术语词典干预的机器翻译挑战赛

189 阅读9分钟

#Datawhale AI 夏令营

  1. 跑通baseline
  2. 核心代码解读
  3. 基于理解对代码进行调试优化

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

作者:李嘉鑫


2024年07月013日

1 赛事背景

目前神经机器翻译技术已经取得了很大的突破,但在特定领域或行业中,由于机器翻译难以保证术语的一致性,导致翻译效果还不够理想。对于术语名词、人名地名等机器翻译不准确的结果,可以通过术语词典进行纠正,避免了混淆或歧义,最大限度提高翻译质量。

2 赛事任务

基于术语词典干预的机器翻译挑战赛选择以英文为源语言,中文为目标语言的机器翻译。本次大赛除英文到中文的双语数据,还提供英中对照的术语词典。参赛队伍需要基于提供的训练数据样本从多语言机器翻译模型的构建与训练,并基于测试集以及术语词典,提供最终的翻译结果,数据包括:

·训练集:双语数据:中英14万余双语句对

·开发集:英中1000双语句对

·测试集:英中1000双语句对

·术语词典:英中2226条

3 评审规则

3.1数据说明

所有文件均为UTF-8编码,其中测评官方发放的训练集、开发集、测试集和术语词典皆为文本文件,格式如下所示。

训练集为双语数据,每行为一个句对样本,其格式如图1所示。

图1 训练集格式

术语词典格式如图2所示。

图2 术语词典格式

3.2 评估指标

对于参赛队伍提交的测试集翻译结果文件,采用自动评价指标BLUE-4进行评价,具体工具使用sacrebleu开源版本。

3.3 评测及排行

1)提供下载数据,选手在本地进行算法调试,在比赛页面提交结果。

2)排行按照得分从高到低排序,排行榜将选择团队的历史最优成绩进行排名。

4.跑通baseline

1、跑通baseline 准备工作与环境搭建 Datawhale官方有提供详细的速通文档[:从零入门NLP竞赛](‍‬⁠‍​​‍‌‍⁠​​‍‌‬⁠‌⁠​​​​​⁠​​⁠​⁠​‍​‬‌‬​​​​​​​‬​‌​从零入门NLP竞赛 - 飞书云文档 (feishu.cn))

按照上述文档可以速通baseline。只要会点运行就可以!!!

Step 1:下载相关的代码和数据文件 在链接中下载名为 task1_termino.ipynb和dataset.zip两个文件。然后 启动魔搭GPU环境(点击即可跳转)

执行以下命令:

mkdir MT
cd MT
mkdir code

Step 2:配置导入

将下载好的ipynb代码文件拖进code文件夹中,在MT文件夹中将压缩包dataset.zip拖入并执行以下命令进行解压

unzip dataset.zip`

Step 3: 双击代码进入 点击>> 运行代码 Step 4: 将运行结束后的submit.txt文件进行提交,第一次未作修改的代码分数如下:

image.png 图3 第一次baseline分数

ps:对评估分数的解释:采用BLUE-4自动评价指标: BLUE 评估指标的特点:

  • 优点:计算速度快、计算成本低、容易理解、与具体语言无关、和人类给的评估高度相关。
  • 缺点:不考虑语言表达(语法)上的准确性;测评精度会受常用词的干扰;短译句的测评精度有时会较高;没有考虑同义词或相似表达的情况,可能会导致合理翻译被否定。

5 核心代码解读

第一部分代码实现了一个序列到序列(Seq2Seq)模型,用于机器翻译任务。Seq2Seq模型包括一个编码器(Encoder)和一个解码器(Decoder,第二部分代码程序的main函数

ff18d3c8540d78c77a60c31b7ef5c4b1.jpg 图4 Seq2Seq模型

这里的原理是Encoder 将输入的英文信息压缩成一个隐藏的语义变量c,这个c大概率可以理解为下图红框内的内容 0b026e4484203b21ab250d67bcbd0324.jpg 图5 句子翻译结构

5.1 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.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
  • __init__:初始化编码器的层,包括嵌入层、GRU层和Dropout层。
  • forward:前向传播。输入源语言序列 src,经过嵌入层和GRU层,得到编码后的序列 outputs 和最后一个时间步的隐藏状态 hidden

5.2 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.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
  • __init__:初始化解码器的层,包括嵌入层、GRU层、全连接层和Dropout层。
  • forward:前向传播。输入目标语言的一个词 input 和编码器的隐藏状态 hidden,经过嵌入层和GRU层,得到解码后的输出 prediction 和更新后的隐藏状态 hidden

5.3 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):
        # 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
  • __init__:初始化Seq2Seq模型,包括编码器、解码器和设备。

  • forward:前向传播。

    • 输入源语言序列 src 和目标语言序列 trg,以及教师强制比例 teacher_forcing_ratio

    • 初始化输出张量 outputs,尺寸为 [batch_size, trg_len, trg_vocab_size]

    • 编码器处理源语言序列 src,得到编码后的隐藏状态 hidden

    • 初始解码器输入 input 为目标序列的开始标记(start token)。

    • 循环遍历目标序列长度 trg_len,在每个时间步:

      • 解码器处理当前输入 input 和隐藏状态 hidden,得到输出 output 和更新后的隐藏状态 hidden
      • 将解码器输出 output 存储到 outputs 中。
      • 根据教师强制策略选择下一个输入:以一定概率选择目标序列中的下一个词,否则选择当前输出的最高概率词。

通过这种方式,Seq2Seq模型能够将源语言序列翻译成目标语言序列。教师强制策略有助于模型在训练过程中更快收敛,并减轻错误传播的影响。

5.4 main函数重要部分代码解读

5.4.1 选择训练样本
N = 1000  # int(len(dataset) * 1)
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)

这些代码选择数据集的前1000个样本用于训练,并使用 DataLoader 创建批量迭代器。collate_fn 是一个用于处理批次的函数。

5.4.2 定义模型参数
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

这些代码定义了模型的超参数:

  • INPUT_DIM:编码器的输入维度,即源语言词汇表的大小。
  • OUTPUT_DIM:解码器的输出维度,即目标语言词汇表的大小。
  • ENC_EMB_DIMDEC_EMB_DIM:编码器和解码器的嵌入维度。
  • HID_DIM:隐藏层维度。
  • N_LAYERS:编码器和解码器的层数。
  • ENC_DROPOUTDEC_DROPOUT:编码器和解码器的dropout概率。
5.4.3 定义优化器和损失函数
optimizer = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss(ignore_index=dataset.zh_word2idx['<pad>'])

这些代码定义了优化器(Adam)和损失函数(交叉熵损失)。ignore_index 参数指定在计算损失时忽略填充标记 <pad>

5.4.5 训练模型
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}')

这些代码执行模型的训练:

  • N_EPOCHS:训练的轮数。
  • CLIP:用于梯度剪裁的阈值。
  • 在每个轮次中,调用 train 函数训练模型,并打印训练损失。

6 代码调试优化lijie

基于我们以上对代码核心部分的理解我们可以进行一些代码的修改来观察修改前后效果的提升,我们将main函数中的模型参数做出调整如下:

    # 定义模型参数
    INPUT_DIM = len(dataset.en_vocab)
    OUTPUT_DIM = len(dataset.zh_vocab)
    ENC_EMB_DIM = 512   #更改编码器的输入维度
    DEC_EMB_DIM = 512   #更改译码器的输入维度
    HID_DIM = 1024      #更改隐藏层维度
    N_LAYERS = 4        #更改编码器和解码器的层数。
    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 = 20    #将轮次调整为20看看效果如何
    CLIP = 1

在我们进行轮次和encoder-decoder层数的修改之后代码的效果有一定提升但仍然很低,还需继续优化

image.png 图六 优化后评分

引发思考

在轮次和层数的修改后代码的跑分还是很低,是什么原因呢?我们可以从两个角度思考:

1.我们的数据是直接下载来的没有进行数据清洗,如果进行数据清洗会不会好一些? 2.从代码的角度来说我们对轮次和层数的修改肯还不够,如果对优化一下损失函数会不会更好?