Datawhale AI 夏令营- 讯飞机器翻译挑战赛task2:baseline代码详解

NeuroSoon 2024-08-02 13:01:02 阅读 77

赛事任务

        在官方提供的数据集中,训练集有14多万条数据,开发集(验证集)和测试集分别包含1000条数据用作模型评估,还提供了一个术语词典,作为一些特殊词语的翻译对照表。

配置环境

Baseline代码中首先安装了torchtext,jieba,sacrebleu,spacy这些库。

torchtext是一个用于处理自然语言处理(NLP)任务的PyTorch库。它提供了一系列用于数据处理、数据加载、预处理和数据集管理的工具。torchtext可以帮助用户加载文本数据集并进行词嵌入、词典构建、数据划分等预处理步骤,以便进行文本分类、序列标注、机器翻译等任务。jieba是一个流行的中文分词工具,用于将中文文本进行分词处理。分词是将连续的文本序列切分成具有独立含义的词语的过程,是中文自然语言处理的重要预处理步骤之一。sacrebleu是一个用于计算机器翻译任务中BLEU评估指标的工具包。BLEU(Bilingual Evaluation Understudy)是一种机器翻译质量评估指标,通常用于评估翻译系统生成的译文与参考答案之间的相似程度。spaCy是一个用于自然语言处理的Python库,提供了许多功能强大的NLP工具和模型,包括分词、词性标注、句法分析、命名实体识别等功能。

<code>!pip install torchtext

!pip install jieba

!pip install sacrebleu

!pip install -U pip setuptools wheel

!pip install -U 'spacy[cuda12x]'

!pip install ../dataset/en_core_web_trf-3.7.3-py3-none-any.whl

数据预处理

机器翻译任务的预处理是确保模型能够有效学习源语言到目标语言映射的关键步骤,目的主要是使数据变成神经网络可识别到的数据类型,并提高模型的泛化能力、准确性和鲁棒性。如:

分词

# 定义tokenizer

en_tokenizer = get_tokenizer('spacy', language='en_core_web_trf')code>

zh_tokenizer = lambda x: list(jieba.cut(x)) # 使用jieba分词

构建词汇表和词向量

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

序列截断和填充

# 假设你有10000个样本,你只想用前1000个样本进行测试

indices = list(range(N))

train_dataset = Subset(train_dataset, 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

通常还需要划分训练集,验证集和测试集,占比为60-80%,10-20%,10-20%。本赛题中已经给划分好了,直接load就可以。

n-gram Language Model

在 NLP 中,n-gram 用于模拟文本中单词或字符序列的频率。n-gram 模型多作为一种指标来衡量翻译系统的性能,但通常并不直接用于训练模型或定义损失函数。

语言模型需要计算特定序列中多个单词出现的概率。将 m 个单词序列 {w1, ..., wm} 的概率表示为 P(w1, ..., wm)。由于单词 wi 之前的单词数量根据其在输入文档中的位置而变化,因此 P(w1, ..., wm) 通常以包含 n 个先前单词而不是所有先前单词的窗口为条件:

在机器翻译中,模型通过为每个输出的单词序列赋予goodness score来选择输入短语的最佳词序。模型可以在不同的词序之间进行选择,它将通过为每个词分配一个分数的概率函数运行所有词序列来实现这一目标,得分最高的序列是翻译的输出。

为了计算上述概率,可以将每个 n-gram 的计数与每个单词的频率进行比较。例如,如果模型采用2-gram,则通过将单词与其前一个单词组合来计算的每个 2-gram 的频率将除以相应的uni-gram的频率。

2-gram:

3-gram:

 

但 n-gram 模型存在两个主要问题:稀疏性问题和存储问题

稀疏性问题

这些模型的稀疏性问题是由两个问题引起的。以 3-gram 为例:

注意等式 3 的分子。如果 w1、w2 和 w3 从未一起出现在 corpus 中,则 w3 的概率为 0。为了解决这个问题,可以在词汇表中每个单词的计数中添加一个小的 δ,这称为 Smoothing

注意等式 3 的分母。如果 w1 和 w2 在 corpus 中从未一起出现,则无法计算 w3 的概率。为了解决这个问题,我们可以单独以 w2 为条件,这称为 Backoff 。而且增加 n 会使稀疏问题变得更糟,所以通常我们设定n ≤ 5。

存储问题

随着 n 的增加(或语料库大小的增加),模型的大小也会增加。

n的选择

当 n 越大时,可以捕捉更深层次的语言结构和语境信息,从而提供更加精准的语义理解和预测能力。

当 n 较小时,可以更好地处理数据稀疏性和维度灾难等问题。

因此,选择合适的 n 值取决于具体的任务需求和数据特点。在实际应用中,通常需要根据实验和调参来确定最适合的 n 值以获得最佳的性能和效果。

Model

首先介绍一下 NLP 领域常用的模型

Recurrent Neural Networks (RNN)

与传统的翻译模型不同,传统的翻译模型仅考虑先前单词的有限窗口来调节语言模型,而 RNN 能够根据语料库中的所有先前单词来调节模型。

这张图展示了经典的 RNN 架构,其中每个垂直矩形框都是时间步长 t 的隐藏层,每个这样的层都包含若干神经元,每个神经元对其输入执行线性矩阵运算,然后执行非线性运算(例如 tanh())。在每个时间步长,隐藏层有两个输入:前一层 ht−1 的输出和该时间步长 xt 的输入。前者乘以权重矩阵 W(hh),后者乘以权重矩阵 W(hx) 以生成输出特征 ht,输出特征 ht 乘以权重矩阵 W(S),并通过词汇表上的 softmax 来运行获得下一个单词的预测输出 y 。简单来说就是通过下面两个公式进行计算:

并且在每个时间步重复应用相同的权重 W(hh) 和 W(hx)。因此,模型必须学习的参数数量较少,最重要的是,与输入序列的长度无关,从而克服了维数灾难!

Bi-RNN

RNN主要关注以过去的单词为条件来预测序列中下一个单词的 RNN,而通过让 RNN 模型向后读取语料库,可以根据未来的单词进行预测。Bi-RNN则是在每个时间步 t,该网络存在两个隐藏层,一个用于从左到右的传播,另一个用于从右到左的传播。最终的分类结果 y 是通过组合两个 RNN 隐藏层产生的分数结果生成的。Bi-RNN架构:

这两个公式显示了Bi-RNN 隐藏层背后的数学公式,两个公式唯一区别在于在 corpus 中递归的方向。

这个公式计算通过总结过去和未来单词表示来预测下一个单词的分类关系。

Gated Recurrent Units (GRU)

baseline中使用的就是GRU这个RNN的变体,旨在解决传统RNN存在的梯度消失和梯度爆炸等问题。相对于普通的RNN,GRU引入了门控机制,减少了网络需要记忆的信息量,使得模型在长序列任务上更有效地捕捉长期依赖关系。

GRU有四个基本的操作阶段:New memory generation,Reset Gate,Update Gate,Hidden state。

New memory generation 这个阶段将新观察到的单词xt 与过去的隐藏状态 ht−1 相结合。

Reset Gate:用于控制网络在输入数据进行处理时保留多少先前信息。Reset Gate接收当前时间步的输入和前一个时间步的隐藏状态作为输入,然后决定将前一个时间步的隐藏状态中的多少信息传递给当前时间步。通过控制Reset Gate的输出,GRU可以灵活地调整网络对历史信息的依赖程度,帮助网络在处理长序列数据时更好地捕捉长期依赖关系。

Update Gate:负责控制网络如何将新的候选状态与先前的隐藏状态进行结合。更新门的功能类似于 LSTM 中的遗忘门和输入门的结合,通过调整先前的隐藏状态与新的候选隐藏状态之间的权重,以决定将多少以前的信息保留,以及要保留多少来自新的候选状态的信息。

Hidden state:使用过去的隐藏输入 ht−1 和生成的 new memory 最终生成隐藏状态 ht 。

LSTM

LSTM (Long Short-Term Memory) 和 GRU 是两种常用的RNN变体,都用于处理序列数据并解决传统 RNN 的梯度消失或梯度爆炸等问题,但在结构和门控机制上有一些区别。

GRU 的设计比LSTM更加简单,参数较少,计算效率也较高,比起LSTM我更常用GRU。

Transformer

Transformer 是一种基于自注意力的架构,由堆叠的块组成,每个块都包含自注意力和前馈层。并将多头自注意力、层归一化、残差连接和注意力缩放等组件组合起来形成 Transformer。具体内容就等到下一个task的笔记再详细讲述了,推荐看一看3b1b在YouTube上的可视化视频来辅助理解。

Seq2Seq

Seq2Seq(Sequence-to-Sequence)是一种用于处理序列数据的深度学习模型架构,主要应用于自然语言处理和机器翻译等任务。

Seq2Seq模型由两个主要部分组成:编码器(Encoder)和解码器(Decoder)。

Encoder负责将输入序列编码为一个固定长度的向量(通常称为上下文向量),捕捉输入序列的语义信息。

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

Decoder负责根据编码器输出的上下文向量生成目标序列,即将上下文向量解码为目标序列。

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

将Encoder和Decoder拼接在一起,形成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 = [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

Dropout

Baseline中使用了 dropout 技术。Dropout 是一种用于防止神经网络过拟合的正则化技术。在训练神经网络时,dropout 随机地将部分神经元的输出设为零,以减少神经元之间的依赖关系,从而有效地减少过拟合并提高泛化能力。Baseline 中 dropout 设为了0.5。

DROPOUT = 0.5

优化器

Baseline 中使用的是 Adam 优化器,这也是 NLP 中最常使用的优化器。Adam结合了动量优化和自适应学习率机制,并且把一阶动量和二阶动量结合了起来。

def initialize_optimizer(model, learning_rate=0.001):

return optim.Adam(model.parameters(), lr=learning_rate)

这是Adam的伪代码和Pytorch官方Adam的代码:

<code>def _single_tensor_adam(params: List[Tensor],

grads: List[Tensor],

exp_avgs: List[Tensor],

exp_avg_sqs: List[Tensor],

max_exp_avg_sqs: List[Tensor],

state_steps: List[Tensor],

grad_scale: Optional[Tensor],

found_inf: Optional[Tensor],

*,

amsgrad: bool,

beta1: float,

beta2: float,

lr: float,

weight_decay: float,

eps: float,

maximize: bool,

capturable: bool,

differentiable: bool):

assert grad_scale is None and found_inf is None

for i, param in enumerate(params):

grad = grads[i] if not maximize else -grads[i]

exp_avg = exp_avgs[i]

exp_avg_sq = exp_avg_sqs[i]

step_t = state_steps[i]

if capturable:

assert param.is_cuda and step_t.is_cuda, "If capturable=True, params and state_steps must be CUDA tensors."

# update step

step_t += 1

if weight_decay != 0:

grad = grad.add(param, alpha=weight_decay)

if torch.is_complex(param):

grad = torch.view_as_real(grad)

exp_avg = torch.view_as_real(exp_avg)

exp_avg_sq = torch.view_as_real(exp_avg_sq)

param = torch.view_as_real(param)

# Decay the first and second moment running average coefficient

exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1)

exp_avg_sq.mul_(beta2).addcmul_(grad, grad.conj(), value=1 - beta2)

if capturable or differentiable:

step = step_t

# 1 - beta1 ** step can't be captured in a CUDA graph, even if step is a CUDA tensor

# (incurs "RuntimeError: CUDA error: operation not permitted when stream is capturing")

bias_correction1 = 1 - torch.pow(beta1, step)

bias_correction2 = 1 - torch.pow(beta2, step)

step_size = lr / bias_correction1

step_size_neg = step_size.neg()

bias_correction2_sqrt = bias_correction2.sqrt()

if amsgrad:

# Maintains the maximum of all 2nd moment running avg. till now

if differentiable:

max_exp_avg_sqs_i = max_exp_avg_sqs[i].clone()

else:

max_exp_avg_sqs_i = max_exp_avg_sqs[i]

max_exp_avg_sqs[i].copy_(torch.maximum(max_exp_avg_sqs_i, exp_avg_sq))

# Uses the max. for normalizing running avg. of gradient

# Folds in (admittedly ugly) 1-elem step_size math here to avoid extra param-set-sized read+write

# (can't fold it into addcdiv_ below because addcdiv_ requires value is a Number, not a Tensor)

denom = (max_exp_avg_sqs[i].sqrt() / (bias_correction2_sqrt * step_size_neg)).add_(eps / step_size_neg)

else:

denom = (exp_avg_sq.sqrt() / (bias_correction2_sqrt * step_size_neg)).add_(eps / step_size_neg)

param.addcdiv_(exp_avg, denom)

else:

step = _get_value(step_t)

bias_correction1 = 1 - beta1 ** step

bias_correction2 = 1 - beta2 ** step

step_size = lr / bias_correction1

bias_correction2_sqrt = _dispatch_sqrt(bias_correction2)

if amsgrad:

# Maintains the maximum of all 2nd moment running avg. till now

torch.maximum(max_exp_avg_sqs[i], exp_avg_sq, out=max_exp_avg_sqs[i])

# Use the max. for normalizing running avg. of gradient

denom = (max_exp_avg_sqs[i].sqrt() / bias_correction2_sqrt).add_(eps)

else:

denom = (exp_avg_sq.sqrt() / bias_correction2_sqrt).add_(eps)

param.addcdiv_(exp_avg, denom, value=-step_size)

Loss function

Baseline使用的是交叉熵损失函数(CrossEntropyLoss),它是在分类问题中常用的损失函数,用于衡量模型输出概率分布与实际标签之间的差异。

交叉熵损失函数通常用于多分类问题,计算模型输出的概率分布与实际标签之间的交叉熵损失。对于每个样本,交叉熵损失函数会将模型对每个类别的预测概率与实际标签(one-hot编码)进行比较,然后计算损失值。

<code>criterion = nn.CrossEntropyLoss(ignore_index=zh_vocab['<pad>'])

BLEU

BLEU(Bilingual Evaluation Understudy)是一种常用的用于评估机器翻译质量的指标,它通过比较机器翻译输出与人工参考翻译之间的相似度来进行评估。在机器翻译研究领域中,BLEU指标被广泛应用于评估不同机器翻译系统的性能。

BLEU指标通过计算机器翻译输出中与参考翻译匹配的n-gram的精确度来评估翻译质量。通常,BLEU指标会计算多个不同长度的n-gram的精确度,然后将这些精确度进行加权平均,并通过对短句进行惩罚来避免短句优势。

Baseline中使用的是BLEU-4,指计算时考虑四元组(即连续四个词)的匹配情况。

def calculate_bleu(dev_loader, src_vocab, tgt_vocab, model, device):

model.eval()

translations = []

references = []

with torch.no_grad():

for src, tgt in dev_loader:

src = src.to(device)

for sentence in src:

translated = translate_sentence(sentence, src_vocab, tgt_vocab, model, device)

translations.append(' '.join(translated))

for reference in tgt:

ref_tokens = [tgt_vocab.get_itos()[idx] for idx in reference if idx not in [tgt_vocab['<bos>'], tgt_vocab['<eos>'], tgt_vocab['<pad>']]]

references.append([' '.join(ref_tokens)])

bleu = sacrebleu.corpus_bleu(translations, references)

return bleu.score

# 计算BLEU分数

bleu_score = calculate_bleu(dev_loader, en_vocab, zh_vocab, model, DEVICE)

print(f'BLEU score = {bleu_score*100:.2f}')

结果

分数还是不高,只有12分。训练集中的拟声词实在是太多了,对结果影响很大。而且空白字符也很多,不知道是否需要去除,下一次直播QA的时候问一下。准备先好好清洗一下拟声词数据再跑一下试试看。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。