NLP机器翻译(2)——注意力机制

一、前置知识

1.编码器与解码器(Encoder-Decoder)

 

f10b4ee3d98d42b4a8d5146a5541e30c.png

含注意力机制的编码器解码器模型

Encoder顾名思义就是对输入句子 进行编码,将输入句子通过非线性变换转化为中间语义表示 C

而Decoder的任务是根据句子的中间语义表示C和之前已经生成的历史信息 y1, y2, … … yi-1 来生成 i 时刻要生成的单词  yi

62db63850993408da53de9675df7b4c1.png

 

2.编码器

作用:将文本表示成向量

原理:编码器接受输入序列,通过一系列神经网络层逐步提取输入序列的特征,并最终将整个输入序列的信息压缩到一个固定大小的上下文向量(Context Vector)。这个过程包括嵌入层、RNN层(如LSTM、GRU)或自注意力机制层(如Transformer)等。编码器的最终输出是一个包含输入序列全部信息的上下文向量或序列。

编码器的作用是把一个不定⻓的输入序列 eq?x_1%2Cx_2%2C...%2Cx_T转化成一个定⻓的上下文向量。

假设该循环神经网络单元为 f (可以为vanilla RNN, LSTM,GRU),那么

● 隐藏状态为        eq?h_t%3Df%28x_t%2Ch_%28t-1%29%20%29
● 编码器的context vector是所有时刻hidden state的函数     eq?c%3Dq%28h_1%2C...%2Ch_T%20%29

背景变量的输出可以是隐藏状态的均值,也可以是最后一个隐藏状态值

上述编码器是一个单向的循环神经网络,每个时间步的隐藏状态只取决于该时间步及之前的输入子序列。

3.解码器

作用:向量表示为输出

原理:解码器接收编码器生成的上下文向量,结合解码器自身的内部状态,逐步生成输出序列。解码器在每一步生成一个输出词或符号,并将这个输出和上下文向量一起作为下一步的输入。这个过程可以通过RNN、LSTM、GRU或Transformer等模型实现。解码器的生成过程可以通过贪婪搜索或其他搜索算法(如束搜索)来优化输出序列的质量。

  • 输入隐藏状态 s_t和上下文向量 c
  • 隐藏状态更新: eq?st%3Df%28s_t-1%2Cy_t-1%2Cc%29
  • 输出概率分布: P(y_t∣s_t)=softmax(W_st​+b)

 

4.贪婪搜索

贪婪搜索是一种序列生成算法,在解码器生成序列时,每一步都选择概率最高的下一个词。这种方法简单且计算效率高,但可能不是全局最优解,容易陷入局部最优。

在每一步生成过程中,贪婪搜索选择当前时间步下概率最大的词作为输出,并将其作为下一时间步的输入。

在每个时间步t,选择条件概率最大的词(c是编码器生成的上下文向量):

58f495fba4c7489da13e8d1731036e14.png

作为输出。一旦搜索出"<eos>"符号,或者输出序列长度已经达到了最大长度𝑇′,便完成输出。

下面来看一个例子。假设输出词典里面有“A”“B”“C”和“<eos>”这4个词。图1中每个时间步下的4个数字分别代表了该时间步生成“A”“B”“C”和“<eos>”这4个词的条件概率。在每个时间步,贪婪搜索选取条件概率最大的词。因此,图10.9中将生成输出序列“A”“B”“C”“<eos>”。该输出序列的条件概率是0.5×0.4×0.4×0.6=0.0480.5×0.4×0.4×0.6=0.048。

51158d29ac064137beff6a45e7464249.png

与图1中不同,图2在时间步2中选取了条件概率第二大的词“C”。由于时间步3所基于的时间步1和2的输出子序列由图1中的“A”“B”变为了图2中的“A”“C”,图2中时间步3生成各个词的条件概率发生了变化。我们选取条件概率最大的词“B”。此时时间步4所基于的前3个时间步的输出子序列为“A”“C”“B”,与图1中的“A”“B”“C”不同。因此,图2中时间步4生成各个词的条件概率也与图1中的不同。我们发现,此时的输出序列“A”“C”“B”“<eos>”的条件概率是0.5×0.3×0.6×0.6=0.0540.5×0.3×0.6×0.6=0.054,大于贪婪搜索得到的输出序列的条件概率。因此,贪婪搜索得到的输出序列“A”“B”“C”“<eos>”并非最优输出序列。

a8bf954b61114b2b86604e6d5bbfd89b.png

 

5.BLEU函数

根据精确率衡量翻译质量,侧重于翻译的准确性和精确匹配度。

BLEU通过计算N-gram的匹配程度,来评估机器翻译的精确率

什么是N-gram?

n表示连续n个词的组合

4eb88b13c48b46238abba8caf4aaf8d8.png

将一个句子按照不同方法分解:This is a sentence

1-gram:{This},{is},{a},{sentence}

2-gram:{This is},{ is a},{ a sentence}

3-gram:{This is a},{ is a sentence}

490aff48182d4bdf970a66408a682f31.png

 

BLEU计算

n-gram的精确度:

64e43ec694ce440d9f226aff2621098f.png

BP(长度惩罚):

3f6b7fc8949847bc95d984edf3f3bb03.png

BLEU得分:

5d0bb22d0c4b4f47bd144fa8625d4b4f.png

当预测序列与标签序列一致时,BLEU=1

 

6.ROUGE函数

根据召回率衡量翻译质量。ROUDE主要关注机器生成的摘要中是否捕捉到了参考摘要的信息,侧重于衡量摘要的信息完整性和涵盖程度。

ROUGE-N

在 N-gram 上计算召回率

bb0da6aea3b248c1b644706eab484629.png

其中eq?count_%7Bref%7D%20%28n-gram_i%20%29是参考文本中n-gram的计数

eq?count%20_%7Bgen%7D%28n-gram_i%29是生成文本中n-gram的计数

 

ROUGE-L

考虑了机器译文和参考译文之间的最长公共子序列(LCS)的匹配情况

2114aca0d5414200ac33ed4067c7bdc2.png


ROUGE-S

任意两个不相邻单词对的匹配情况

71584db4af2b4a4abbe797fa556267de.png

eq?count_%7Bmatch%7D是生成文本与参考文本之间匹配的单词对数

eq?count%20_%7Bref%7D是参考文本中的单词对数

二、代码实现

1.数据预处理

1)导入需要的库

import collections
import os
import io
import math
import torch
from torch import nn# PyTorch 主库,提供了张量运算和深度学习的基本功能
import torch.nn.functional as F # 从 PyTorch 中导入神经网络模块
import torchtext.vocab as Vocab # 从 torchtext 导入词汇表模块
import torch.utils.data as Data # 从 PyTorch 中导入数据加载模块

import sys # 导入系统模块
# sys.path.append("..") 
import d2lzh_pytorch as d2l# 导入自定义模块 d2lzh_pytorch

2)定义特殊符号

<pad>用于填充数据序列,使得每个序列具有相同的长度

 <bos>和<eos>表示序列的开始和结束

PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'    #定义符号
os.environ["CUDA_VISIBLE_DEVICES"] = "0"    # 设置环境变量,指定使用的 GPU 设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')   # 设置设备为 GPU,如果没有 GPU 可用则使用 CPU

#print(torch.__version__, device)#查看设备

3)定义process_one_seq函数和build_data函数

来给序列添加标记和构造词典

def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
    all_tokens.extend(seq_tokens)
     # 在序列末尾添加 EOS 标记,然后添加 PAD 标记直到序列长度达到 max_seq_len
    seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
    all_seqs.append(seq_tokens)

# 使用所有的词来构造词典。并将所有序列中的词变换为词索引后构造Tensor
def build_data(all_tokens, all_seqs):
     # 构建词汇表(vocab),使用 collections.Counter 统计词频,并指定特殊标记符
    vocab = Vocab.Vocab(collections.Counter(all_tokens),
                        specials=[PAD, BOS, EOS])
    indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]
    return vocab, torch.tensor(indices)

4)读取数据

  • 读取数据文件中的每一行,将其分割为输入序列和输出序列。
  • 对每个序列进行分词处理,并忽略长度超过最大限制的序列。
  • 使用辅助函数处理输入和输出序列,构建词汇表并转换为张量数据集。
def read_data(max_seq_len):
    in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []
    with io.open('fr-en-small.txt') as f:
        lines = f.readlines()# 读取所有行
    for line in lines:
        # 移除行末的换行符并使用制表符分割成输入序列和输出序列
        in_seq, out_seq = line.rstrip().split('\t')
        # 将输入序列和输出序列分割成词 token        
        in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ')
        # 如果加上 EOS 标记后的序列长度大于 max_seq_len,则忽略
        if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
            continue  # 如果加上EOS后长于max_seq_len,则忽略掉此样本
       
        process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)
        process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)
    
    # 用输入的 token 构建输入词汇表,并将所有输入序列转换为词索引的 Tensor    
    in_vocab, in_data = build_data(in_tokens, in_seqs)
    # 用输出的 token 构建输出词汇表,并将所有输出序列转换为词索引的 Tensor
    out_vocab, out_data = build_data(out_tokens, out_seqs)
    # 返回输入词汇表、输出词汇表,以及包含输入和输出数据的 Tensor 数据集
    return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)

eg:将序列的最大长度设成7,然后查看读取到的第一个样本。该样本分别包含法语词索引序列和英语词索引序列。

max_seq_len = 7
in_vocab, out_vocab, dataset = read_data(max_seq_len)
dataset[0]

输出结果为:(tensor([ 5, 4, 45, 3, 2, 0, 0]), tensor([ 8, 4, 27, 3, 2, 0, 0]))

 

2.建立模型—含注意力机制的编码器

 

(1)编码器

定义Encoder类,包含嵌入层和GRU层。嵌入层将词汇索引映射为密集的嵌入向量,GRU用于处理输入序列。

class Encoder(nn.Module):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 drop_prob=0, **kwargs):
        super(Encoder, self).__init__(**kwargs)
        #创建一个嵌入层,将词汇表中的索引映射为密集的嵌入向量
        self.embedding = nn.Embedding(vocab_size, embed_size)
        #GRU是一种循环神经网络,用于学习输入序列的表示
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)

    def forward(self, inputs, state):#state是rnn的初始状态
        # inputs形状是(批量大小, 时间步数)。将输出互换样本维和时间步维
        embedding = self.embedding(inputs.long()).permute(1, 0, 2) # (seq_len, batch, input_size)
        return self.rnn(embedding, state)

    def begin_state(self):#用于返回初始状态,这里不需要
        return None

(2)注意力机制

model:包含两层线性层和一个Tanh激活函数,用来计算注意力权重。

def attention_model(input_size, attention_size):
    # 使用 nn.Sequential 定义一个顺序容器,包含三个层次
    # 1. 线性层:将输入大小映射到注意力大小,不使用偏置
    # 2. Tanh 激活函数:用于引入非线性
    # 3. 线性层:将注意力大小映射到标量,不使用偏置
    model = nn.Sequential(nn.Linear(input_size, attention_size, bias=False),
                          nn.Tanh(),
                          nn.Linear(attention_size, 1, bias=False))
    return model

前向传播:计算注意力权重,并基于权重生成背景向量

def attention_forward(model, enc_states, dec_state):
    """
    enc_states: (时间步数, 批量大小, 隐藏单元个数)
    dec_state: (批量大小, 隐藏单元个数)
    """

    # 将解码器隐藏状态广播到和编码器隐藏状态形状相同后进行连结
  
    # dec_state 的形状 (批量大小, 隐藏单元个数) -> (1, 批量大小, 隐藏单元个数)
    # expand_as(enc_states) 将其扩展为 (时间步数, 批量大小, 隐藏单元个数)

    dec_states = dec_state.unsqueeze(dim=0).expand_as(enc_states)
    
    # 在最后一个维度上连接编码器隐藏状态 enc_states 和扩展后的解码器隐藏状态 dec_states
    # 结果形状为 (时间步数, 批量大小, 隐藏单元个数 * 2)

    enc_and_dec_states = torch.cat((enc_states, dec_states), dim=2)
    
    e = model(enc_and_dec_states)  # 形状为(时间步数, 批量大小, 1)
    alpha = F.softmax(e, dim=0)  # 在时间步维度做softmax运算
    return (alpha * enc_states).sum(dim=0)  

eg:编码器的时间步数为10,批量大小为4,编码器和解码器的隐藏单元个数均为8。注意力机制返回一个小批量的背景向量,每个背景向量的长度等于编码器的隐藏单元个数。因此输出的形状为(4, 8)

seq_len, batch_size, num_hiddens = 10, 4, 8
model = attention_model(2*num_hiddens, 10) 
enc_states = torch.zeros((seq_len, batch_size, num_hiddens))
dec_state = torch.zeros((batch_size, num_hiddens))
attention_forward(model, enc_states, dec_state).shape

运行结果:torch.Size([4, 8])

 

3.建立模型—含注意力机制的解码器

Decoder:包含嵌入层、注意力机制、GRU层和线性输出层。

forward:计算当前输入和编码器输出的注意力背景向量,并与嵌入向量连接并输入GRU

class Decoder(nn.Module):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 attention_size, drop_prob=0):
        super(Decoder, self).__init__()
        # 嵌入层:将词汇表中的词转换为嵌入向量
        self.embedding = nn.Embedding(vocab_size, embed_size)

        # 注意力模型:用于计算注意力权重和背景向量
        self.attention = attention_model(2*num_hiddens, attention_size)

        # GRU的输入包含attention输出的c和实际输入, 所以尺寸是 num_hiddens+embed_size
        self.rnn = nn.GRU(num_hiddens + embed_size, num_hiddens, 
                          num_layers, dropout=drop_prob)
 
        self.out = nn.Linear(num_hiddens, vocab_size)

    def forward(self, cur_input, state, enc_states):
        """
        cur_input shape: (batch, )
        state shape: (num_layers, batch, num_hiddens)
        """
        # 使用注意力机制计算背景向量
        c = attention_forward(self.attention, enc_states, state[-1])

        # 将嵌入后的输入和背景向量在特征维连结, (批量大小, num_hiddens+embed_size)
        input_and_c = torch.cat((self.embedding(cur_input), c), dim=1) 

        # 为输入和背景向量的连结增加时间步维,时间步个数为1
        output, state = self.rnn(input_and_c.unsqueeze(0), state)

        # 移除时间步维,输出形状为(批量大小, 输出词典大小)
        output = self.out(output).squeeze(dim=0)
        return output, state

    def begin_state(self, enc_state):
        # 直接将编码器最终时间步的隐藏状态作为解码器的初始隐藏状态
        return enc_state

 

4.训练模型

(1)损失函数batch_loss:计算批量损失,初始化编码器和解码器状态,通过强制教学逐步输入实际输出词,计算预测误差并累计损失

def batch_loss(encoder, decoder, X, Y, loss):
    batch_size = X.shape[0]
    enc_state = encoder.begin_state()
    enc_outputs, enc_state = encoder(X, enc_state)
    # 初始化解码器的隐藏状态
    dec_state = decoder.begin_state(enc_state)
    # 解码器在最初时间步的输入是BOS
    dec_input = torch.tensor([out_vocab.stoi[BOS]] * batch_size)
    # 我们将使用掩码变量mask来忽略掉标签为填充项PAD的损失, 初始全1
    mask, num_not_pad_tokens = torch.ones(batch_size,), 0
    l = torch.tensor([0.0])
    for y in Y.permute(1,0): # Y shape: (batch, seq_len)
        dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)
        l = l + (mask * loss(dec_output, y)).sum()
        dec_input = y  # 使用强制教学
        num_not_pad_tokens += mask.sum().item()
        # EOS后面全是PAD. 下面一行保证一旦遇到EOS接下来的循环中mask就一直是0
        mask = mask * (y != out_vocab.stoi[EOS]).float()
    return l / num_not_pad_tokens

(2)训练模型train:

         定义优化器和损失函数,迭代训练模型,每个epoch计算并更新模型参数,输出训练损失。

def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
    enc_optimizer = torch.optim.Adam(encoder.parameters(), lr=lr)
    dec_optimizer = torch.optim.Adam(decoder.parameters(), lr=lr)

    loss = nn.CrossEntropyLoss(reduction='none')
    data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)
    for epoch in range(num_epochs):
        l_sum = 0.0
        for X, Y in data_iter:
            enc_optimizer.zero_grad()
            dec_optimizer.zero_grad()
            l = batch_loss(encoder, decoder, X, Y, loss)
            l.backward()
            enc_optimizer.step()
            dec_optimizer.step()
            l_sum += l.item()
        if (epoch + 1) % 10 == 0:
            print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))

#训练模型
embed_size, num_hiddens, num_layers = 64, 64, 2
attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50
encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers,
                  drop_prob)
decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers,
                  attention_size, drop_prob)
train(encoder, decoder, dataset, lr, batch_size, num_epochs)

运行结果:

epoch 10, loss 0.490
epoch 20, loss 0.149
epoch 30, loss 0.061
epoch 40, loss 0.045
epoch 50, loss 0.011

 

5.翻译结果

有三种方法可以生成解码器在每个时间步的输出:贪婪搜索(Greedy Decoding)、束搜索(Beam Search)和穷举搜索。这里我们用的是最简单的贪婪搜索。

翻译函数translate:将输入序列分词并转换为张量,使用编码器生成编码表示,逐步使用解码器生成输出序列,通过贪婪搜索预测下一个词

def translate(encoder, decoder, input_seq, max_seq_len):
    # 将输入序列分词,并添加特殊符号
    in_tokens = input_seq.split(' ')
    in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
    
    # 将输入序列转换为张量,并添加批次维度
    enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]]) # batch=1
    
    # 初始化编码器状态
    enc_state = encoder.begin_state()
    # 前向传播编码器,得到编码器输出和状态
    enc_output, enc_state = encoder(enc_input, enc_state)
    
    # 解码器初始输入为 <BOS> 标记
    dec_input = torch.tensor([out_vocab.stoi[BOS]])
    # 初始化解码器状态
    dec_state = decoder.begin_state(enc_state)
    
    output_tokens = []  # 存储翻译结果的列表
    
    for _ in range(max_seq_len):
        # 前向传播解码器,得到解码器输出和状态
        dec_output, dec_state = decoder(dec_input, dec_state, enc_output)
        # 取概率最大的单词作为预测结果
        pred = dec_output.argmax(dim=1)
        # 将预测的单词索引转换为单词
        pred_token = out_vocab.itos[int(pred.item())]
        
        if pred_token == EOS:  # 当预测结果为 <EOS> 时,停止翻译
            break
        else:
            output_tokens.append(pred_token)  # 将预测结果添加到输出序列中
            dec_input = pred  # 将当前预测结果作为下一个时间步的输入
    
    return output_tokens  # 返回翻译结果

eg:简单测试一下,输入法语“ils regardent.”,翻译后的英语应该是“they are watching.”

input_seq = 'ils regardent .'
translate(encoder, decoder, input_seq, max_seq_len)

运行结果:['they', 'are', 'watching', '.']

 

6.评价翻译结果

在机器翻译中,通常使用BLEU(Bilingual Evaluation Understudy)来预测任意子序列是否在标签序列中。

bleu函数:算预测序列和标签序列之间的BLEU分数

def bleu(pred_tokens, label_tokens, k):#k:最大的n-gram的长度
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    score = math.exp(min(0, 1 - len_label / len_pred))# 计算长度惩罚项BP
    for n in range(1, k + 1):# 遍历所有的n-gram
        num_matches, label_subs = 0, collections.defaultdict(int)
        for i in range(len_label - n + 1):# 统计参考翻译中每个n-gram的出现次数
            label_subs[''.join(label_tokens[i: i + n])] += 1
        for i in range(len_pred - n + 1):# 统计预测翻译中每个n-gram的匹配次数
            if label_subs[''.join(pred_tokens[i: i + n])] > 0:
                num_matches += 1
                label_subs[''.join(pred_tokens[i: i + n])] -= 1
        # 计算当前n-gram的精确度,并加权
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    return score

score函数:调用翻译函数生成预测序列,并计算与标签序列的BLEU分数,打印评分和翻译结果

def score(input_seq, label_seq, k):#输入预测序列和标签序列,比较后打分
    pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)
    label_tokens = label_seq.split(' ')
    print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k),
                                      ' '.join(pred_tokens)))

eg1:翻译完全正确

score('ils regardent .', 'they are watching .', k=2)

运行结果:

bleu 1.000, predict: they are watching .

eg2:翻译有误

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

运行结果:

bleu 0.658, predict: they are actors .

 

三、总结

主要步骤就是数据处理,建立包含注意力机制的encoder-decoder模型(可以将单头注意力改为多头注意力以提高模型效果)、训练模型计算损失函数,进行翻译,最后用BLEU函数评价翻译效果。

 

  • 14
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值