当前位置:   article > 正文

NLP实验——基于编码器—解码器和注意力机制的机器翻译

NLP实验——基于编码器—解码器和注意力机制的机器翻译

一、机器翻译

1.1 概念

机器翻译(Machine Translation, MT)是指利用计算机技术和算法来实现不同自然语言之间的自动翻译。其目标是将一段文本从一种语言(源语言)自动翻译成另一种语言(目标语言),并尽可能保留原文的语义和句法结构。机器翻译系统通过分析和处理输入的源语言文本,然后生成等效的目标语言文本。

机器翻译的应用场景广泛,比如翻译软件,翻译机,划词翻译,网页翻译,同声传递等等,体现在人们生活的方方面面。

1.2 发展历史

机器翻译的发展历史显示了从早期的规则系统到统计模型再到现代的神经网络模型的演进,每一阶段都显著提升了翻译质量和效率,使机器翻译在实际应用中具有了更广泛的应用前景和实用性。

  1. 早期阶段:1950年代至1960年代,以《翻译备忘录》为机器翻译的起步,参考翻译员的翻译过程,基于词典和语法等规则生成翻译,被称为基于规则的机器翻译(RBMT)。70年代《语言与机器》全面否定机器翻译的可行性,研究停止。直到80年代基于实例的机器翻译(EBMT)出现后,对机器翻译的研究才逐渐恢复。

  2. 统计机器翻译的兴起:1990年代至21世纪初,IBM提出了基于词对齐的翻译模型,统计机器翻译(Statistical Machine Translation, SMT)取代了传统的规则系统,使用大量的双语语料库和统计模型进行翻译。

  3. 神经机器翻译的崛起:2010年代至今,随着深度学习技术的发展,神经机器翻译(Neural Machine Translation, NMT)取得了显著进展。NMT利用神经网络模型(如循环神经网络和Transformer)进行端到端的翻译,不再依赖于传统的特征工程和对齐模型。

  4. 现代趋势:近年来,随着计算能力的增强和数据集的增大,NMT模型在翻译质量和速度上持续改善。同时,出现了预训练语言模型(如BERT、GPT等)的引入,为机器翻译提供了更多的语境和语义信息。

二、理论基础

2.1编码器-解码器

2.1.1 Endocder-Decoder结构

概念:编码器-解码器(Encoder-Decoder)是一种常见的神经网络结构,例如机器翻译、语音识别、文本摘要等。它分为两部分:编码器负责将输入序列编码为一个固定长度的上下文向量(或称为编码向量),解码器则使用该向量生成目标序列。

根据不同任务的需求,编码器和解码器可以选择不同类型的深度学习模型,比如卷积神经网络CNN,循环神经网络RNN及其变体长短期记忆网络LSTM、门控循环单元GRU,注意力机制Attention、Transformer等等。

结构:包含编码器(Encoder)和解码器(Decoder)两部分。

2.1.2 seq2seq模型

概念:序列到序列模型(seq2seq)是一种特殊的编码器-解码器结构,专门用于处理输入序列和输出序列长度不固定的任务。最典型的应用是机器翻译,其中输入序列是源语言句子,输出序列是目标语言句子。

seq2seq任务的特点:

1.输入、输出长度不固定;

2.输入输出元素之间具有顺序关系。

编码器部分是一个循环神经网络,通常使用LSTM或者GRU。编码器接受输入序列的词嵌入作为输入,作用是实现将一个不定长的输入序列变换为定长的背景向量C,并在该背景变量中编码输入序列信息。编码器结构如下图所示:

解码器部分也是一个循环神经网络,接受编码器生成的语义向量C作为初始状态,作用是实现根据C生成指定的序列,其过程称为解码。解码器结构有两种,区别在于背景向量C是否应用于每一时间步的输出,如下图所示:

2.1.3 注意力机制

注意力机制是一种在计算机科学和机器学习中广泛使用的技术,它模仿人类的注意力过程,使模型能够集中处理输入数据中的关键信息,同时忽略不太相关的部分。这种机制通过为输入数据的不同部分分配不同的权重(或注意力分数),帮助模型识别最重要的信息,从而提高模型的准确性和效率。注意力机制的应用领域广泛,包括自然语言处理、计算机视觉、语音识别等。

注意力机制在Endocder-Decoder模型中的应用可以帮助解决长距离依赖问题,允许模型在解码过程中动态地将注意力集中在输入序列的不同部分,而不是仅仅依赖于最后的编码结果。

  • 改善了模型对长距离依赖的处理能力。
  • 提高了翻译质量和生成序列的流畅性
  • 输入、输出长度不固定输入输出元素之间具有顺序关系。

解码的输出对比:不再使用固定的背景向量C,而是使用一个动态的语义编码向量C',是由Encoder中每一时刻的隐藏状态向量计算得到的,即C'=attention(h1,...hn)

传统的Endocder-Decoder:P(Yt-1)=g(Yt-1,St,C)

包含注意力机制的Endocder-Decoder:P(Yt-1)=g(Yt-1,St,attention(h1,...hn))

其中,隐藏状态向量(h1,...hn),上一个时刻输出Yt-1,以及当前时刻Decoder中的隐藏状态向量St。

2.2 搜索方法

2.2.1 贪婪搜索(Greedy Search)

贪婪解码是一种简单直观的解码策略,每个时间步选择概率最高的单词作为输出,作为下一个时间步的输入。

具体步骤:使用编码器生成的上下文向量作为解码器的初始状态,生成输出序列从开始符号(如<start>)开始,重复以下步骤直到生成结束符号(如<end>):

(1)预测:当前解码器状态下,计算输出词的概率分布。

(2)选择:选择概率最高的词作为当前时间步的输出。

(2)更新状态:将选择的词作为下一个时间步的输入,更新解码器的状态。

2.2.2 穷举搜索

如果目标是得到最优输出序列,我们可以考虑穷举搜索(exhaustive search):穷举所有可能的输出序列,输出条件概率最大的序列。但是这种方法会让计算开销变大。

2.2.3 束搜索(Beam Search)

集束搜索是一种更加复杂和有效的解码策略,它考虑多个备选序列,并尝试在多个可能的输出序列中找到最优解。

具体步骤:使用编码器生成的上下文向量作为解码器的初始状态,

生成输出序列,重复以下步骤当所有序列生成结束符号(如<end>)时停止:

(1)多假设扩展:维护一个大小为K的集束(beam),每个假设是一个候选输出序列,

(2)扩展候选:对于集束中的每个假设,生成下一个可能的词,并计算每个扩展后的序列的分数(通常是对数概率)。

(3)选择:根据得分选择前K个最优的序列作为下一个时间步的候选集束。

束搜索VS贪婪搜索:

前者能够在多个可能的输出序列中进行搜索,更有可能找到全局最优解;通常能够生成更流畅和语义上更合理的序列;但是计算量较大,因为需要同时处理多个假设和计算多个候选序列的分数。

后者简单快速,计算效率高;但是可能导致局部最优解,不一定能够产生全局最优序列;缺乏全局信息,可能导致生成不够流畅或不符合整体语义的序列。

三、实现过程

3.1 数据预处理

首先需要解压 d2lzh_pytorch .tar压缩包,是需要导入的自定义库。

!tar -xf d2lzh_pytorch.tar # 解压缩名为 d2lzh_pytorch.tar 的文件

实验需要导入如下相关的库,同时需要我们定义一些特殊符号。其中“<pad>”(padding)符号即填充符,用来添加在较短序列后,直到每个序列等长,而“<bos>”和“<eos>”符号分别表示序列的开始和结束。

  1. #导入所需的Python模块和库
  2. import collections
  3. import os
  4. import io
  5. import math
  6. import torch
  7. from torch import nn
  8. import torch.nn.functional as F
  9. import torchtext.vocab as Vocab
  10. import torch.utils.data as Data
  11. import sys
  12. # sys.path.append("..")
  13. import d2lzh_pytorch as d2l#导入自定义的 d2lzh_pytorch 模块
  14. #定义了特殊标记 <pad>、<bos>、<eos>,分别表示填充符、序列开始符和序列结束符
  15. PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'
  16. os.environ["CUDA_VISIBLE_DEVICES"] = "0" #设置了环境变量 CUDA_VISIBLE_DEVICES 为 "0"
  17. #检查当前是否有CUDA设备可用,将设备类型存储在 device 变量中
  18. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  19. print(torch.__version__, device)#打印出当前使用的PyTorch版本和设备类型(GPU或CPU)

这里我们定义两个辅助函数方便对后面读取的数据进行预处理。

process_one_seq 函数:用于处理一个序列,完成以下操作:

(1)将当前序列中的词加入到总词列表 all_tokens 中;

(2)在当前序列末尾添加 <eos> 标记,并使用 <pad> 填充至指定的最大长度 max_seq_len;

(3)将处理后的序列加入到总序列列表 all_seqs 中。

build_data 函数:用于构建词典并将所有序列中的词转换为词索引后构造成张量:

(1)根据 all_tokens 构建词典 vocab,其中包括了特殊标记 <pad><bos><eos>;

(2)将所有序列 all_seqs 中的词根据 vocab 转换为对应的索引,并构建成张量 indices;

(3)返回词典 vocab 和序列索引张量 indices

  1. # 将一个序列中所有的词记录在all_tokens中以便之后构造词典
  2. #然后在该序列后面添加PAD直到序列长度变为max_seq_len,然后将序列保存在all_seqs中
  3. def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
  4. '''
  5. 参数:
  6. seq_tokens 是当前序列的词列表,
  7. all_tokens 是所有词的列表,
  8. all_seqs 是所有序列的列表,
  9. max_seq_len 是指定的最大序列长度
  10. '''
  11. all_tokens.extend(seq_tokens)#将当前序列的词加入到总词列表 all_tokens 中
  12. #在当前序列末尾添加 <eos> 标记,并使用 <pad> 填充至指定的最大长度 max_seq_len
  13. seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
  14. all_seqs.append(seq_tokens)#将处理后的序列加入到总序列列表 all_seqs 中
  15. # 使用所有的词来构造词典。并将所有序列中的词变换为词索引后构造Tensor
  16. def build_data(all_tokens, all_seqs):
  17. '''
  18. 参数:
  19. all_tokens 是所有词的列表,
  20. all_seqs 是所有序列的列表
  21. '''
  22. #根据 all_tokens 构建词典 vocab,其中包括了特殊标记 <pad>、<bos>、<eos>
  23. vocab = Vocab.Vocab(collections.Counter(all_tokens),
  24. specials=[PAD, BOS, EOS])
  25. #将所有序列 all_seqs 中的词根据 vocab 转换为对应的索引,并构建成张量 indices
  26. indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]
  27. #返回词典 vocab 和序列索引张量 indices
  28. return vocab, torch.tensor(indices)

read_data 函数:实现了整体的数据处理流程,返回输入词典 in_vocab、输出词典 out_vocab 和数据集 TensorDataset。

在这里我们使用了一个很小的法语—英语数据集'fr-en-small.txt'。在这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用'\t'隔开。

  1. def read_data(max_seq_len):
  2. # in和out分别是input和output的缩写
  3. in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []# 初始化列表来存储输入和输出的tokens和sequences
  4. # 打开包含数据的文件
  5. with io.open('fr-en-small.txt') as f:
  6. lines = f.readlines()# 读取文件中的所有行
  7. # 处理文件中的每一行数据
  8. for line in lines:
  9. in_seq, out_seq = line.rstrip().split('\t') # 使用'\t'分割输入和输出序列
  10. in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ') # 对序列进行分词
  11. # 检查是否有序列超过最大长度max_seq_len - 1(考虑到加上EOS标记后的长度)
  12. if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
  13. continue # 如果加上EOS后长于max_seq_len,则忽略掉此样本
  14. # 处理输入和输出的tokens和sequences
  15. process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)
  16. process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)
  17. # 构建输入和输出的词汇表及数据张量
  18. in_vocab, in_data = build_data(in_tokens, in_seqs)
  19. out_vocab, out_data = build_data(out_tokens, out_seqs)
  20. # 返回输入和输出的词汇表,以及包含数据的PyTorch TensorDataset
  21. return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)
  22. max_seq_len = 7
  23. in_vocab, out_vocab, dataset = read_data(max_seq_len)#调用函数读取数据
  24. dataset[0]

序列的最大长度max_seq_len = 7,然后查看读取到的第一个样本。从结果中可以看到该样本分别包含法语词索引序列和英语词索引序列。

3.2 含注意力机制的编码器—解码器

3.2.1 编码器

这里我们定义 Encoder其中初始化方法 _init_方法定义了编码器模型的结构;

前向传播方法 forward 定义了数据从输入到输出的传播过程;

初始状态方法 begin_state 方法返回 RNN(GRU)的初始状态,这里返回 None,意味着在每次调用时使用 PyTorch 默认的零初始化状态。

  1. class Encoder(nn.Module):
  2. def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
  3. drop_prob=0, **kwargs):
  4. super(Encoder, self).__init__(**kwargs)
  5. # 定义编码器的各层
  6. self.embedding = nn.Embedding(vocab_size, embed_size) # 嵌入层,,将输入的词索引映射为词向量
  7. # GRU层,使用GRU作为RNN的实现,输入维度为embed_size,隐藏单元数为num_hiddens,层数为num_layers,dropout概率为drop_prob
  8. self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)
  9. def forward(self, inputs, state):
  10. # inputs的形状是(batch_size, seq_len),其中batch_size是批量大小,seq_len是时间步数
  11. # 将输入的词索引inputs转换为词向量embedding,然后交换维度以适应GRU的输入要求
  12. embedding = self.embedding(inputs.long()).permute(1, 0, 2) # 输入形状从(batch_size, seq_len)变为(seq_len, batch_size, embed_size)
  13. # 将交换维度后的embedding作为GRU的输入,state作为初始状态,计算GRU的输出和最终状态
  14. return self.rnn(embedding, state)
  15. def begin_state(self):
  16. return None # 初始化RNN(GRU)的初始状态

创建了一个名为 encoder 的编码器实例,指定了词汇表大小为 10,嵌入向量大小为 8,GRU 的隐藏单元数为 16,层数为 2。调用 encoder 实例的 forward 方法,传入一个大小为 (4, 7) 的张量(假设 batch_size=4, seq_len=7)作为输入,并使用 begin_state 方法获取初始状态。返回 outputstate。

  1. # 创建Encoder实例
  2. encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
  3. # 调用Encoder实例的forward方法,传入输入和初始状态,获取输出output和状态state
  4. output, state = encoder(torch.zeros((4, 7)), encoder.begin_state())
  5. #输出output的形状和状态state的形状
  6. output.shape, state.shape

查看输出结果 :output 是 GRU 层的输出,形状为 (seq_len, batch_size, num_hiddens),在这里是 (7, 4, 16)state 是 GRU层的最终状态,形状为 (num_layers, batch_size, num_hiddens),在这里是 (2, 4, 16)

3.2.2 注意力机制

这里我们定义了注意力机制,包括两个函数。

attention_model 函数:定义了一个简单的注意力模型,包括两个线性层和一个Tanh激活函数;

ttention_forward 函数:实现了注意力机制的前向传播过程。

  1. def attention_model(input_size, attention_size):
  2. # 定义注意力模型,包括两个线性层和一个Tanh激活函数
  3. model = nn.Sequential(nn.Linear(input_size, attention_size, bias=False), # 输入到注意力大小的线性层
  4. nn.Tanh(),# Tanh激活函数
  5. nn.Linear(attention_size, 1, bias=False)) # 注意力大小到标量1的线性层
  6. return model
  7. def attention_forward(model, enc_states, dec_state):
  8. """
  9. enc_states: (时间步数, 批量大小, 隐藏单元个数)
  10. dec_state: (批量大小, 隐藏单元个数)
  11. """
  12. # 将解码器隐藏状态广播到和编码器隐藏状态形状相同后进行连结
  13. dec_states = dec_state.unsqueeze(dim=0).expand_as(enc_states)
  14. enc_and_dec_states = torch.cat((enc_states, dec_states), dim=2)
  15. e = model(enc_and_dec_states) # 形状为(时间步数, 批量大小, 1)
  16. alpha = F.softmax(e, dim=0) # 在时间步维度做softmax运算
  17. return (alpha * enc_states).sum(dim=0) # 返回注意力加权编码器状态

创建了一个注意力模型 model,定义了编码器状态 enc_states 和解码器状态 dec_state,都是用全零张量初始化,最后调用 attention_forward 函数。

  1. seq_len, batch_size, num_hiddens = 10, 4, 8
  2. model = attention_model(2*num_hiddens, 10) # 创建一个注意力模型实例,输入大小为2*num_hiddens,注意力大小为10
  3. enc_states = torch.zeros((seq_len, batch_size, num_hiddens)) # 创建编码器状态张量,形状为(seq_len, batch_size, num_hiddens)
  4. dec_state = torch.zeros((batch_size, num_hiddens)) # 创建解码器状态张量,形状为(batch_size, num_hiddens)
  5. attention_forward(model, enc_states, dec_state).shape # 调用attention_forward函数并打印输出的形状

输出其返回值的形状,即注意力加权后的编码器状态的形状,在这里是(4,8)。

3.2.3 解码器

这里我们定义Decoder类 ,其中初始化方法 init方法定义了包含注意力机制attention的解码器模型的结构;前向传播方法 forward 定义了数据从输入到输出的传播过程;

初始状态方法 begin_state 方法直接将编码器在最终时间步的隐藏状态作为解码器的初始隐藏状态。这要求编码器和解码器的循环神经网络使用相同的隐藏层个数和隐藏单元个数。

  1. class Decoder(nn.Module):
  2. def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
  3. attention_size, drop_prob=0):
  4. super(Decoder, self).__init__()
  5. # 创建一个嵌入层,将词汇表大小为 vocab_size 的词索引映射为 embed_size 维的向量
  6. self.embedding = nn.Embedding(vocab_size, embed_size)
  7. self.attention = attention_model(2*num_hiddens, attention_size)# 创建注意力模型,使用上面定义的attention_model
  8. # 创建一个GRU(门控循环单元)模型,输入大小为 num_hiddens + embed_size,输出大小为 num_hiddens
  9. # 层数为 num_layers,且有一个可选的dropout参数
  10. self.rnn = nn.GRU(num_hiddens + embed_size, num_hiddens,
  11. num_layers, dropout=drop_prob)
  12. # 创建线性层,用于最终输出结果的映射,GRU的输出映射到词汇表大小 vocab_size
  13. self.out = nn.Linear(num_hiddens, vocab_size)
  14. def forward(self, cur_input, state, enc_states):
  15. # 获取注意力权重的加权编码器状态
  16. c = attention_forward(self.attention, enc_states, state[-1])
  17. # 将当前输入和注意力加权编码器状态连接起来
  18. input_and_c = torch.cat((self.embedding(cur_input), c), dim=1)
  19. # 将连接后的输入传入GRU
  20. output, state = self.rnn(input_and_c.unsqueeze(0), state)
  21. # 将GRU的输出通过线性层得到最终的预测输出
  22. output = self.out(output).squeeze(dim=0)
  23. return output, state
  24. def begin_state(self, enc_state):
  25. # 直接将编码器最终时间步的隐藏状态作为解码器的初始隐藏状态
  26. return enc_state

3.3 模型训练

这里我们定义了两个函数用于模型训练:

batch_loss损失函数:计算一个小批量的损失;

train训练函数:进行模型训练,需要同时迭代编码器和解码器的模型参数。

  1. def batch_loss(encoder, decoder, X, Y, loss):
  2. batch_size = X.shape[0] # 获取批量大小
  3. enc_state = encoder.begin_state() # 初始化编码器的隐藏状态
  4. enc_outputs, enc_state = encoder(X, enc_state) # 编码器处理输入序列X,获取输出和最终状态
  5. # 初始化解码器的隐藏状态,使用编码器的最终状态作为解码器的初始状态
  6. dec_state = decoder.begin_state(enc_state)
  7. # 解码器在最初时间步的输入是BOS
  8. dec_input = torch.tensor([out_vocab.stoi[BOS]] * batch_size)
  9. # 我们将使用掩码变量mask来忽略掉标签为填充项PAD的损失, 初始全1
  10. mask, num_not_pad_tokens = torch.ones(batch_size,), 0
  11. l = torch.tensor([0.0])# 初始化损失为0
  12. # 对Y进行转置,按照时间步迭代
  13. for y in Y.permute(1,0): # Y shape: (batch, seq_len)
  14. # 解码器进行一步解码,输出dec_output和更新后的解码器状态dec_state
  15. dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)
  16. # 计算当前时间步的损失,并乘以mask以忽略填充项PAD的损失
  17. l = l + (mask * loss(dec_output, y)).sum()
  18. dec_input = y # 使用强制教学,下一个时间步的输入是当前时间步的真实标签y
  19. num_not_pad_tokens += mask.sum().item() # 统计非填充标记的数量
  20. # 更新mask,确保一旦遇到EOS(结束标记),接下来的时间步mask就一直是0
  21. mask = mask * (y != out_vocab.stoi[EOS]).float()
  22. # 计算平均损失,除以非填充标记的数量
  23. return l / num_not_pad_tokens
  24. def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
  25. enc_optimizer = torch.optim.Adam(encoder.parameters(), lr=lr) # 定义编码器优化器,使用Adam优化器,学习率为lr
  26. dec_optimizer = torch.optim.Adam(decoder.parameters(), lr=lr) # 定义解码器优化器,使用Adam优化器,学习率为lr
  27. loss = nn.CrossEntropyLoss(reduction='none') # 定义损失函数为交叉熵损失函数,reduction='none'表示不进行降维
  28. data_iter = Data.DataLoader(dataset, batch_size, shuffle=True) # 创建数据迭代器,用于每次生成一个batch的数据进行训练
  29. for epoch in range(num_epochs): # 迭代训练多个epochs
  30. l_sum = 0.0 # 初始化总损失和为0
  31. for X, Y in data_iter: # 遍历每个batch的数据
  32. enc_optimizer.zero_grad() # 清零编码器优化器的梯度
  33. dec_optimizer.zero_grad() # 清零解码器优化器的梯度
  34. l = batch_loss(encoder, decoder, X, Y, loss) # 计算当前batch的损失
  35. l.backward() # 反向传播,计算梯度
  36. enc_optimizer.step() # 更新编码器参数
  37. dec_optimizer.step() # 更新解码器参数
  38. l_sum += l.item() # 累加当前batch的损失值到总损失和
  39. if (epoch + 1) % 10 == 0: # 每10个epoch打印一次损失
  40. print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter))) # 打印当前epoch的平均损失

创建模型实例并设置超参数,就可以开始训练模型了。

  1. #设置参数
  2. embed_size, num_hiddens, num_layers = 64, 64, 2# 嵌入大小、隐藏单元数和层数
  3. # 注意力大小、drop率、学习率、批次大小和训练周期数
  4. attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50
  5. encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers,
  6. drop_prob) # 编码encoder
  7. decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers,
  8. attention_size, drop_prob) # 解码decoder
  9. train(encoder, decoder, dataset, lr, batch_size, num_epochs) # 调用训练函数

这里训练周期数设置为50轮,从训练结果中可以看到经过不断迭代训练损失从0.454下降至0.014。

3.4 预测不定长序列

上文提到过有3种实现预测不定长序列的策略,我们选择最简单的贪婪搜索进行序列预测。

  1. def translate(encoder, decoder, input_seq, max_seq_len):
  2. in_tokens = input_seq.split(' ') # 将输入序列按空格分割成单词
  3. in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1) # 添加结束符号EOS和填充符号PAD,使其长度达到max_seq_len
  4. # 将输入单词转换为对应的索引,形成一个Tensor作为编码器的输入,batch大小为1
  5. enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]]) # in_vocab为输入语言的词汇表
  6. enc_state = encoder.begin_state() # 初始化编码器的初始状态
  7. enc_output, enc_state = encoder(enc_input, enc_state)# 编码器对输入序列进行编码,得到编码器的输出和最终状态
  8. # 解码器的初始输入为目标语言的起始符号BOS的索引
  9. dec_input = torch.tensor([out_vocab.stoi[BOS]])# out_vocab为目标语言的词汇表
  10. dec_state = decoder.begin_state(enc_state)# 使用编码器的最终状态初始化解码器的初始状态
  11. output_tokens = [] # 存储输出的目标语言单词序列
  12. # 开始解码过程,最多进行max_seq_len次解码
  13. for _ in range(max_seq_len):
  14. dec_output, dec_state = decoder(dec_input, dec_state, enc_output)#解码
  15. pred = dec_output.argmax(dim=1)# 预测下一个token
  16. pred_token = out_vocab.itos[int(pred.item())]# 将预测的token转换为字符串形式
  17. # 如果预测的token是EOS(句子结束标记),则停止生成序列
  18. if pred_token == EOS:
  19. break
  20. else:
  21. # 将预测的token添加到输出序列中
  22. output_tokens.append(pred_token)
  23. # 将当前预测的token作为下一个解码器的输入
  24. dec_input = pred
  25. # 返回生成的输出序列tokens
  26. return output_tokens

让我们来try一下简单的测试:输入法语句子“ils regardent.”,翻译后的英语句子应该是“they are watching.”。

  1. input_seq = 'ils regardent .' # 输入的源语言序列
  2. translate(encoder, decoder, input_seq, max_seq_len) # 调用translate函数进行翻译

预测效果如何?看起来是正确的。

3.4 机器翻译评价指标BLEU

BLEU,全称Bilingual Evaluation Understudy,是一种经典的机器翻译评价指标。它由美国国家标准与技术研究院(NIST)于2002年提出,广泛应用于自动翻译系统的效果评估。BLEU指标主要关注翻译结果与人工翻译参考之间的相似度,通过计算多个词或短语的准确率来评价翻译质量。

这里我们定义了两个函数:

bleu损失函数:计算BLEU分数;

score函数:辅助打印函数。

  1. def bleu(pred_tokens, label_tokens, k):
  2. len_pred, len_label = len(pred_tokens), len(label_tokens)
  3. # 根据句子长度计算惩罚分数(长度惩罚)
  4. score = math.exp(min(0, 1 - len_label / len_pred))
  5. # 计算BLEU的n-gram匹配得分
  6. for n in range(1, k + 1):
  7. num_matches, label_subs = 0, collections.defaultdict(int)
  8. # 统计参考句子中的n-gram出现次数
  9. for i in range(len_label - n + 1):
  10. label_subs[''.join(label_tokens[i: i + n])] += 1
  11. # 统计预测句子中与参考句子n-gram匹配的数量
  12. for i in range(len_pred - n + 1):
  13. if label_subs[''.join(pred_tokens[i: i + n])] > 0:
  14. num_matches += 1
  15. label_subs[''.join(pred_tokens[i: i + n])] -= 1
  16. # 计算n-gram精确度,并应用n-gram权重
  17. score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
  18. # 返回最终的BLEU分数
  19. return score
  20. def score(input_seq, label_seq, k):
  21. pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)# 生成预测的token序列
  22. label_tokens = label_seq.split(' ')# 将参考的label序列按空格分割为token列表
  23. # 计算并打印BLEU分数,以及生成的预测序列
  24. print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k),
  25. ' '.join(pred_tokens)))

让我们使用BLEU来评价一下模型翻译的效果吧!在这里束宽设置为2。

  1. # 调用score函数,传入输入序列、标签序列和k值
  2. score('ils regardent .', 'they are watching .', k=2)

score('ils sont canadienne .', 'they are canadian .', k=2)

四、拓展练习

4.1 练习一

  • 在训练中,将强制教学替换为使用解码器在上一时间步的输出作为解码器在当前时间步的输入。结果有什么变化吗?

在这里我们要将上面batch_loss函数中使用强制教学下一个时间步的输入是当前时间步的真实标签y的代码更改为使用解码器在当前时间步的输入,如下所示:

  1. # 更新输入为当前时间步的解码器输出
  2. dec_input = dec_output.argmax(dim=-1)

训练效果如何呢?

将强制教学替换为使用解码器在上一时间步的输出作为解码器在当前时间步的输入,这样的修改会导致训练过程中,解码器在每个时间步的输入都依赖于其上一个时间步的输出,而不是直接依赖于真实的标签。从训练结果对比可以看出这种方法可能会增加训练的难度,因为误差在传播时会积累,但有时也能带来更好的模型泛化能力和对未知序列的适应能力。

4.2 练习二

  • 试着使用更大的翻译数据集来训练模型,例如 WMT 和 Tatoeba Project。

WMT:http://www.statmt.org/wmt14/translation-task.html

Tatoeba Project:http://www.manythings.org/anki/

在这里我们可以尝试使用更大的翻译数据集来训练模型,下载的网站提供在上面。

此次练习选取了Tatoeba Project中翻译数据集南非语-英语afr-eng.zip ,解压后包含918条训练数据,并对数据集先进行预处理再进行模型的训练,使其变为南非-英语的语句对,便于训练。

  1. input_file = 'afr.txt' # 输入文件名
  2. output_file = 'afr_modified.txt' # 输出文件名
  3. with open(input_file, 'r', encoding='utf-8') as f_in, open(output_file, 'w', encoding='utf-8') as f_out:
  4. for line in f_in:
  5. line = line.strip() # 去除首尾空白符
  6. if line:
  7. parts = line.split('\t') # 使用制表符分割
  8. if len(parts) >= 2:
  9. source_sentence = parts[1].strip() # 南非语语句
  10. target_sentence = parts[0].strip() # 英语语句
  11. f_out.write(f"{source_sentence}\t{target_sentence}\n") # 写入到输出文件中

读取我们处理好的文本数据。

  1. def read_data(max_seq_len):
  2. # in和out分别是input和output的缩写
  3. in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []# 初始化列表来存储输入和输出的tokens和sequences
  4. # 打开包含数据的文件
  5. with io.open('afr_modified.txt') as f:
  6. lines = f.readlines()# 读取文件中的所有行
  7. # 处理文件中的每一行数据
  8. for line in lines:
  9. in_seq, out_seq = line.rstrip().split('\t') # 使用'\t'分割输入和输出序列
  10. in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ') # 对序列进行分词
  11. # 检查是否有序列超过最大长度max_seq_len - 1(考虑到加上EOS标记后的长度)
  12. if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
  13. continue # 如果加上EOS后长于max_seq_len,则忽略掉此样本
  14. # 处理输入和输出的tokens和sequences
  15. process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)
  16. process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)
  17. # 构建输入和输出的词汇表及数据张量
  18. in_vocab, in_data = build_data(in_tokens, in_seqs)
  19. out_vocab, out_data = build_data(out_tokens, out_seqs)
  20. # 返回输入和输出的词汇表,以及包含数据的PyTorch TensorDataset
  21. return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)
  22. max_seq_len =27
  23. in_vocab, out_vocab, dataset = read_data(max_seq_len)#调用函数读取数据
  24. dataset[917] # 查看索引为917的数据

查看的结果如下所示:

再次调用训练函数进行训练,看看训练效果怎么样?

在前面使用小数据集fr-en-small.txt (仅20条训练数据)上训练的结果,损失在早期迭代中迅速下降,最终的训练损失非常低(0.014),这可能表明模型在小数据集上有较好的拟合能力。但是在小模型上进行训练,也有可能存在过拟合的风险,因为模型可能会过度适应小数据集中的噪声或特定样本。

使用更大的数据集afr_modified.txt(近1000条训练数据)进行训练的结果如上所示,训练损失从初始阶段开始就相对较高,且下降速度较小。这可能是因为模型在大数据集上需要更多的迭代来学习到数据的复杂性和多样性。也有可能是因为两者翻译的语言不同,翻译的难度也有所不同。但是随着训练的继续,损失仍然在逐步下降,尽管速度比小数据集下降缓慢,但是这表明模型仍在不断优化;与小数据集相比,损失的波动较少,这表明模型在更大的数据集上更加稳定,尽力避免了小数据集中可能出现的过拟合问题。

五、实验总结

在本次实验中,我们探索了基于编码器-解码器架构和注意力机制的机器翻译任务,进一步加深对encoder-decoder架构和注意力机制的学习和理解。

编码器(Encoder):实验中设计和实现基于门控循环单元GRU的编码器,知道编码器是负责将输入文本序列编码成上下文向量或隐藏状态。

解码器(Decoder):接收编码器生成的背景向量,并逐步生成输出序列,更加熟悉解码器在机器翻译任务中的作用和调整。

尝试实现注意力机制的编码器-解码器的过程中,深入体会注意力机制(Attention)可以帮助模型在解码时聚焦于输入序列的不同部分,提高了对长距离依赖的建模能力。

在拓展练习中,我们感受到使用强制教学和使用解码器在上一时间步的输出对于训练结果的影响;也使用了更大的翻译数据集来进行模型的训练,进一步熟悉了数据预处理和模型训练的过程,以及数据集大小会对训练效果产生的影响。

综上所述,这次实验让我们对机器翻译的关键技术有了更深入的理解,同时也认识到在应用中还存在许多挑战和改进空间。通过持续的实践和探索,期待我们能够进一步提升在自然语言处理领域的能力和成果。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/772243
推荐阅读
相关标签
  

闽ICP备14008679号