机器翻译(Attention&self-attention)——从小白开始进阶

一:机器翻译

1.1:基本概念

机器翻译(Machine Translation, MT)是使用计算机程序将文字或语音从一种语言自动翻译到另一种语言的过程。它是自然语言处理(NLP)领域的一个重要分支,目的是减少跨语言交流的障碍,提高翻译效率和可扩展性。

1.2:方法分类


1. 基于规则的机器翻译(Rule-Based Machine Translation, RBMT)

定义: 这种方法依赖于大量的语言学规则和词典。翻译过程中,系统会使用这些规则来找出源语言和目标语言之间的对应关系。
特点: 需要语言学家和专家系统地编写规则,维护成本较高,但在处理特定语言对或术语严格的专业领域时表现较好。

2. 基于统计的机器翻译(Statistical Machine Translation, SMT)

定义: 使用大量的双语文本数据(称为语料库)来训练模型。系统学习这些数据中的统计规律,以预测最可能的翻译。
特点: 不依赖于语言学规则,而是通过分析和比较大量的双语文本数据来学习如何翻译。效果依赖于训练数据的质量和量。

3. 神经机器翻译(Neural Machine Translation, NMT)

定义: 使用深度学习模型,特别是序列到序列的神经网络(如长短时记忆网络LSTM或门控循环单元GRU)进行翻译。NMT系统通常采用端到端的训练方法,直接从源语言文本到目标语言文本。
特点: 能够更好地处理不同语言的复杂映射关系和长距离依赖关系。提供流畅且准确的翻译,已成为当前机器翻译领域的主流技术。

这些方法各有优势和适用场景,随着技术的进步,尤其是在深度学习和大数据的推动下,神经机器翻译已逐渐成为主流,提供了更自然和准确的翻译结果。其中主流方法有:基于注意力机制的翻译模型和基于Transfromer的翻译模型,下面依次介绍其原理和具体实现示例。

二:带有注意力机制的编码器-解码器结构实现机器翻译

2.1 Encoder-Decoder

2.1.1Encoder-Decoder基本知识

Encoder-Decoder是一个通用的构架,是一类算法统称。根据不同的任务可以选择不同的编码器和解码器比如:
·循环神经网络:RNN、LSTM、GRU
·卷积神经网络:CNN
·注意力机制:Attention、Transformer

结构如下:

编码器是一个循环神经网络,通常使用LSTM或GRU,负责将一个不定长的输入序列变换成一个定长的语义向量。

解码器也是一个循环神经网络,负责根据语义向量生成指定的序列,这个过程称为解码,解码的过程有两种不同的结构,其不同点在于语义向量是否应用于每一时间步的输出。

模型的训练主要需要考虑三个部分:编码器的输入、解码器的输入和解码器的输出。

2.1.2Seq2Seq

Seq2Seq属于Encoder-Decoder结构的一种,所谓序列到序列(sequence-to-sequence,seq2seq)模型,就是一种能够根据给定的序列,通过特定的方法生成另一个序列的方法。
序列到序列模型,简单来说就是一个翻译模型,把一个语言序列翻译成另一种语言序列,即将一个序列作为输入映射为另外一个输出序列。
总体来说,序列到序列任务往往具有以下两个特点:
输入、输出长度不确定。例如要构建一个聊天机器人,我们说的话和它的回复长度都是不固定的;
输入输出元素之间具有顺序关系。不同的顺序,得到的结果应该是不同的,例如“黄蓉的女儿是谁”和“谁的女儿是黄蓉”,这两个短语的意思是不同的。

2.2Attention

Attention顾名思义把注意力集中在重要的点上。

Attention特点:一步到位获取全局与局部的联系。没有位置信息,不依赖序列顺序
优点:可以并行
缺点:无法学习顺序关系

Attention具体实现:

注意力机制通过对编码器所有时间步的隐状态做加权平均来得到语义向量。通过引入注意力机制,解码器输出序列的每个词条都会依赖一个与“上下文”相关的可变语义向量,而不再依赖一个相同的语义向量,其输出如下:

而Attention机制:每个时间输入不同的C充分提取了整个序列的内在信息,不同位置关注度不一样。

三:具体示例:含注意力机制的编码器—解码器将一段简短的法语翻译成英语

3.1读取和预处理数据

我们先定义一些特殊符号。其中“<pad>”(padding)符号用来添加在较短序列后,直到每个序列等长,而“<bos>”和“<eos>”符号分别表示序列的开始和结束.

!tar -xf d2lzh_pytorch.tar
import collections  # 导入collections模块,用于提供额外的数据容器
import os  # 导入os模块,用于操作系统相关功能
import io  # 导入io模块,提供了核心的文件操作功能
import math  # 导入math模块,提供了数学运算函数
import torch  # 导入PyTorch深度学习库
from torch import nn  # 导入神经网络模块
import torch.nn.functional as F  # 导入神经网络函数模块
import torchtext.vocab as Vocab  # 导入torchtext的词汇表模块
import torch.utils.data as Data  # 导入PyTorch数据处理工具

import sys  # 导入sys模块,提供系统相关的参数和函数
# sys.path.append("..")  # 可能用于设置上层目录的导入路径,但当前被注释掉了
import d2lzh_pytorch as d2l  # 导入名为d2lzh_pytorch的自定义模块(可能是教学或辅助模块)

PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'  # 定义特殊标记:填充、开始、结束
os.environ["CUDA_VISIBLE_DEVICES"] = "0"  # 设置环境变量,指定使用GPU设备0(如果可用)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  # 检查是否有CUDA(GPU),将设备设置为cuda或cpu

print(torch.__version__, device)  # 打印PyTorch版本和当前使用的设备(cuda或cpu)

接着定义两个辅助函数对后面读取的数据进行预处理。

# 将一个序列中所有的词记录在all_tokens中以便之后构造词典,然后在该序列后面添加PAD直到序列
# 长度变为max_seq_len,然后将序列保存在all_seqs中
def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
    all_tokens.extend(seq_tokens)
    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 = 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)

为了演示方便,我们在这里使用一个很小的法语—英语数据集。在这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用'\t'隔开。在读取数据时,我们在句末附上“<eos>”符号,并可能通过添加“<pad>”符号使每个序列的长度均为max_seq_len。我们为法语词和英语词分别创建词典。法语词的索引和英语词的索引相互独立.

def read_data(max_seq_len):
    # in和out分别是input和output的缩写
    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')
        in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ')
        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)
    in_vocab, in_data = build_data(in_tokens, in_seqs)
    out_vocab, out_data = build_data(out_tokens, out_seqs)
    return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)

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

max_seq_len = 7
in_vocab, out_vocab, dataset = read_data(max_seq_len)

dataset[0]
# dataset[0] 表示数据集中的第一个样本,是一个包含两个元素的元组:
# - `dataset[0][0]` 是第一个样本的输入序列,一个长度为 `max_seq_len` 的整数序列,
#   表示输入序列的索引化版本,包含 `<SOS>` 和 `<EOS>`。
# - `dataset[0][1]` 是第一个样本的输出序列,同样是长度为 `max_seq_len` 的整数序列,
#   表示输出序列的索引化版本,包含 `<SOS>` 和 `<EOS>`。

# 这种结构和处理方式适用于序列到序列模型的训练或评估,如机器翻译、对话生成等任务。
# 确保序列长度一致并包含起始和结束标记,有助于模型正确学习和生成序列之间的映射关系。

3.2编码器

在编码器中,我们将输入语言的词索引通过词嵌入层得到词的表征,然后输入到一个多层门控循环单元中。正如我们在6.5节(循环神经网络的简洁实现)中提到的,PyTorch的nn.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、时间步数为7的小批量序列输入。设门控循环单元的隐藏层个数为2,隐藏单元个数为16。编码器对该输入执行前向计算后返回的输出形状为(时间步数, 批量大小, 隐藏单元个数)。门控循环单元在最终时间步的多层隐藏状态的形状为(隐藏层个数, 批量大小, 隐藏单元个数)。对于门控循环单元来说,state就是一个元素,即隐藏状态;如果使用长短期记忆,state是一个元组,包含两个元素即隐藏状态和记忆细胞。

encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)

# 使用编码器对象进行前向传播,输入是一个大小为 (4, 7) 的零张量,代表四个样本每个长度为7的序列
# encoder.begin_state() 返回None,因为GRU的初始隐藏状态可以默认为0

output, state = encoder(torch.zeros((4, 7)), encoder.begin_state())

# output 是 GRU 层的输出序列,形状为 (seq_len, batch_size, num_hiddens),这里 seq_len=7, batch_size=4, num_hiddens=16
# state 是 GRU 层的最终隐藏状态,形状为 (num_layers*num_directions, batch_size, num_hiddens),这里 num_layers=2, batch_size=4, num_hiddens=16

output.shape, state.shape

3.3注意力机制

定义的函数:将输入连结后通过含单隐藏层的多层感知机变换。其中隐藏层的输入是解码器的隐藏状态与编码器在所有时间步上隐藏状态的一一连结,且使用tanh函数作为激活函数。输出层的输出个数为1。两个Linear实例均不使用偏差。其中函数定义里向量𝑣的长度是一个超参数,即attention_size

import torch.nn as nn

def attention_model(input_size, attention_size):
    """
    创建一个简单的注意力模型。

    参数:
    - input_size: 输入的特征大小。
    - attention_size: 注意力模型中间层的大小,决定了注意力权重的计算复杂度和模型表达能力。

    返回:
    - model: 一个序列模块,包括了两个线性层和一个Tanh激活函数,用于计算注意力权重。

    模型结构:
    - 第一个线性层: 输入大小为 input_size,输出大小为 attention_size。没有偏置项(bias=False)。
    - Tanh 激活函数: 将线性层的输出通过双曲正切函数映射到 [-1, 1] 的范围内。
    - 第二个线性层: 输入大小为 attention_size,输出大小为 1。用于计算单个注意力权重值,没有偏置项(bias=False)。

    注意力模型的输出是一个单独的注意力权重值,用于加权输入特征以生成加权特征表示。
    """
    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_states = dec_state.unsqueeze(dim=0).expand_as(enc_states)
    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)  # 返回背景变量

在下面的例子中,编码器的时间步数为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

运行结果:

3.4含注意力机制的解码器

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

在解码器的前向计算中,我们先通过刚刚介绍的注意力机制计算得到当前时间步的背景向量。由于解码器的输入来自输出语言的词索引,我们将输入通过词嵌入层得到表征,然后和背景向量在特征维连结。我们将连结后的结果与上一时间步的隐藏状态通过门控循环单元计算出当前时间步的输出与隐藏状态。最后,我们将输出通过全连接层变换为有关各个输出词的预测,形状为(批量大小, 输出词典大小)。

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

3.5训练模型

我们先实现batch_loss函数计算一个小批量的损失。解码器在最初时间步的输入是特殊字符BOS。之后,解码器在某时间步的输入为样本输出序列在上一时间步的词,即强制教学。我们在这里使用掩码变量避免填充项对损失函数计算的影响。

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

在训练函数中,我们需要同时迭代编码器和解码器的模型参数。

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)

3.6预测不定长的序列

这里使用贪婪搜索。

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)
    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

简单测试一下模型。输入法语句子“ils regardent.”,翻译后的英语句子应该是“they are watching.”。

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

四:基于Transformer的机器翻译

在现代机器翻译领域,基于Transformer的模型已经成为了一种革命性的进步。这种模型首次由Vaswani等人在2017年的论文《Attention is All You Need》中提出,它通过引入了一种全新的架构,大幅提高了翻译的质量和效率。

4.1Transformer模型概述

Transformer模型是基于自注意力(Self-Attention)机制的,这意味着它能够在处理输入数据时,对数据的不同部分赋予不同的关注权重。这种机制使得Transformer在处理长距离依赖的语言元素时,表现出非凡的能力,这在传统的循环神经网络(RNN)或长短时记忆网络(LSTM)中是很难实现的。

核心技术:

  • 自注意力机制:允许模型在编码句子时,同时考虑句子中的所有词汇,从而捕捉词与词之间的关系。
  • 多头注意力:通过将注意力分解为多个“头”,并行处理信息,提高了模型捕捉不同类型信息的能力。
  • 位置编码:由于Transformer完全基于注意力,没有循环结构,位置编码用于给模型提供词汇在句子中位置的线索。

在机器翻译任务中,Transformer模型通常被用作编码器-解码器架构的基础。编码器部分负责理解源语言的输入序列,而解码器则用于生成目标语言的输出序列。这种分离的结构使得Transformer不仅在翻译质量上具有优势,而且在处理速度上也远超以前的模型。

4.2Transformer的优势

  • 并行处理能力:由于缺乏循环结构,Transformer允许数据在处理时进行更高效的并行计算。
  • 灵活性和通用性:Transformer模型可以很容易地适应不同的NLP任务,如文本摘要、情感分析等。
  • 更好的性能:多项研究表明,基于Transformer的模型在多语种和复杂语言对的翻译任务中,表现优于其他所有类型的模型。

4.3面临的挑战和未来方向

尽管Transformer模型已经取得了显著的成就,但它仍然面临一些挑战,如计算成本高、对大量训练数据的依赖等。未来的研究可能会集中在优化模型的计算效率,减少对大数据的依赖,以及探索更多创新的自注意力机制,以持续推动机器翻译技术的发展。总之,基于Transformer的机器翻译模型已经并将继续改变我们处理和理解多语言信息的方式。随着技术的进步,我们有理由相信这种模型将带来更多突破性的应用,从而在全球范围内促进信息和知识的自由流动。

五:具体示例:基于Transformer实现机器翻译(日译中)

5.1准备工作

首先,我们需要确保系统中已安装以下包,如果发现缺少某些包,请确保进行安装。

import math  # 导入数学函数库
import torchtext  # 导入PyTorch的文本处理库
import torch  # 导入PyTorch深度学习框架
import torch.nn as nn  # 导入神经网络模块
from torch import Tensor  # 导入张量类
from torch.nn.utils.rnn import pad_sequence  # 导入序列填充工具
from torch.utils.data import DataLoader  # 导入数据加载器
from collections import Counter  # 导入计数器类,用于统计对象出现次数
from torchtext.vocab import Vocab  # 导入词汇表类,用于处理文本的词汇
from torch.nn import TransformerEncoder, TransformerDecoder, TransformerEncoderLayer, TransformerDecoderLayer  # 导入Transformer模型相关组件
import io  # 导入IO操作模块
import time  # 导入时间处理模块
import pandas as pd  # 导入数据处理库pandas
import numpy as np  # 导入数值计算库numpy
import pickle  # 导入序列化模块pickle
import tqdm  # 导入进度条显示模块tqdm
import sentencepiece as spm  # 导入文本分词工具sentencepiece

torch.manual_seed(0)  # 设置随机种子为0,用于实验结果的可重现性
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  # 检测CUDA是否可用,选择相应的设备(GPU或CPU)
# print(torch.cuda.get_device_name(0))  # 打印当前CUDA设备的名称(注释掉了,不会执行)

5.2获取并行数据集

在本例中,我们将使用从JParaCrawl下载的日英并行数据集![http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl],这个数据集被描述为由NTT创建的“最大的公开可用的英日平行语料库。它主要通过网络抓取并自动对齐平行句子创建而成。”您也可以在这里查看相关论文。这里已经下载好了,路径为在我电脑中运行的路径。

df = pd.read_csv('zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
trainen = df[2].values.tolist()  # 从DataFrame中获取第三列数据,并转换为列表
trainja = df[3].values.tolist()  # 从DataFrame中获取第四列数据,并转换为列表

导入所有的日语及其英语对应文本后,我删除了数据集中的最后一个数据,因为它存在缺失值。总共,在trainen和trainja中的句子数量为5,973,071条。然而,出于学习目的,通常建议对数据进行抽样,确保一切按预期运行,然后再一次性使用所有数据,以节省时间。

5.3准备分词器

与英语或其他字母语言不同,日语句子中没有空格来分隔单词。我们可以使用JParaCrawl提供的分词器,它们使用了SentencePiece来分别处理日语和英语。您可以访问JParaCrawl的网站下载这些分词器,或者点击这里。

en_tokenizer = spm.SentencePieceProcessor(model_file='spm.en.nopretok.model')  # 使用spm.en.nopretok.model文件初始化英语的SentencePieceProcessor
ja_tokenizer = spm.SentencePieceProcessor(model_file='spm.ja.nopretok.model')  # 使用spm.ja.nopretok.model文件初始化日语的SentencePieceProcessor
en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.", out_type='str')
#使用en_tokenizer对给定的英语句子进行编码,返回编码后的结果,类型为字符串

ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str')
# 使用en_tokenizer对给定的日语句子进行编码,返回编码后的结果,类型为字符串

5.4构建TorchText的词汇表对象,并将句子转换为Torch张量。

使用来自TorchText的分词器和原始句子,我们构建TorchText的Vocab对象,并将句子转换为Torch张量。这个过程可能需要几秒钟或几分钟,具体取决于数据集的大小和计算能力。不同的分词器也会影响构建词汇表所需的时间。我尝试了几种日语分词器,但是SentencePiece对我来说工作得很好,速度也足够快。

from collections import Counter

# 定义一个函数用于构建词汇表
def build_vocab(sentences, tokenizer):
    # 创建一个空的计数器对象
    counter = Counter()
    
    # 遍历每个句子
    for sentence in sentences:
        # 使用tokenizer对句子进行编码,并将编码结果作为字符串返回,更新计数器
        counter.update(tokenizer.encode(sentence, out_type=str))
    
    # 返回一个词汇表对象,包括特殊标记
    return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])

# 假设trainja是日语训练数据列表,ja_tokenizer是日语的tokenizer对象
ja_vocab = build_vocab(trainja, ja_tokenizer)

# 假设trainen是英语训练数据列表,en_tokenizer是英语的tokenizer对象
en_vocab = build_vocab(trainen, en_tokenizer)

当我们拥有了词汇表对象之后,我们可以利用词汇表和分词器对象来构建我们训练数据的张量。

import torch

# 定义数据处理函数
def data_process(ja, en):
    data = []
    # 使用zip同时迭代日语列表和英语列表
    for (raw_ja, raw_en) in zip(ja, en):
        # 对日语句子进行编码并转换为张量
        ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
                                  dtype=torch.long)
        # 对英语句子进行编码并转换为张量
        en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
                                  dtype=torch.long)
        # 将处理后的日语张量和英语张量添加到data列表中
        data.append((ja_tensor_, en_tensor_))
    
    # 返回处理后的数据列表
    return data

# 调用data_process函数处理训练数据集trainja和trainen,得到训练数据train_data
train_data = data_process(trainja, trainen)

5.5创建DataLoader对象,以便在训练过程中进行迭代。

这里,我将BATCH_SIZE设置为16,以防止“cuda内存不足”的错误,但这取决于诸如您的机器内存容量、数据大小等各种因素,因此根据需要随时更改批量大小(注意:PyTorch的教程在使用Multi30k德英数据集时将批量大小设置为128)。

# 定义批处理大小和特殊词汇的索引
BATCH_SIZE = 8
PAD_IDX = ja_vocab['<pad>']
BOS_IDX = ja_vocab['<bos>']
EOS_IDX = ja_vocab['<eos>']

# 定义生成批数据的函数
def generate_batch(data_batch):
    ja_batch, en_batch = [], []
    for (ja_item, en_item) in data_batch:
        # 在日语和英语序列的开头和结尾添加特殊标记的索引
        ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
        en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))
    
    # 对日语和英语序列进行填充,使它们具有相同的长度
    ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
    en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)
    
    return ja_batch, en_batch

# 创建数据加载器,用于批量加载和处理训练数据
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)

5.6Sequence-to-sequence Transformer

下面的几段代码和文本解释(以斜体显示)来自原始的PyTorch教程[https://pytorch.org/tutorials/beginner/translationtransformer.html]。我没有做任何改动,除了BATCHSIZE和将devocab改为了javocab。Transformer是一种Seq2Seq模型,最初在“Attention is all you need”论文中引入,用于解决机器翻译任务。Transformer模型包括编码器和解码器块,每个块包含固定数量的层。编码器通过一系列的多头注意力和前馈网络层处理输入序列。编码器的输出称为记忆(memory),将其与目标张量一起馈送给解码器。编码器和解码器使用教师强制(teacher forcing)技术进行端到端训练。

from torch.nn import (TransformerEncoder, TransformerDecoder,
                      TransformerEncoderLayer, TransformerDecoderLayer)


class Seq2SeqTransformer(nn.Module):
    def __init__(self, num_encoder_layers: int, num_decoder_layers: int,
                 emb_size: int, src_vocab_size: int, tgt_vocab_size: int,
                 dim_feedforward:int = 512, dropout:float = 0.1):
        super(Seq2SeqTransformer, self).__init__()
        encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)
        self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
        decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)
        self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)

        self.generator = nn.Linear(emb_size, tgt_vocab_size)
        self.src_tok_emb = TokenEmbedding(src_vocab_size, emb_size)
        self.tgt_tok_emb = TokenEmbedding(tgt_vocab_size, emb_size)
        self.positional_encoding = PositionalEncoding(emb_size, dropout=dropout)

    def forward(self, src: Tensor, trg: Tensor, src_mask: Tensor,
                tgt_mask: Tensor, src_padding_mask: Tensor,
                tgt_padding_mask: Tensor, memory_key_padding_mask: Tensor):
        src_emb = self.positional_encoding(self.src_tok_emb(src))
        tgt_emb = self.positional_encoding(self.tgt_tok_emb(trg))
        memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask)
        outs = self.transformer_decoder(tgt_emb, memory, tgt_mask, None,
                                        tgt_padding_mask, memory_key_padding_mask)
        return self.generator(outs)

    def encode(self, src: Tensor, src_mask: Tensor):
        return self.transformer_encoder(self.positional_encoding(
                            self.src_tok_emb(src)), src_mask)

    def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
        return self.transformer_decoder(self.positional_encoding(
                          self.tgt_tok_emb(tgt)), memory,
                          tgt_mask)

文本标记通过使用标记嵌入来表示。为了引入单词顺序的概念,对标记嵌入进行了位置编码。


class PositionalEncoding(nn.Module):
    def __init__(self, emb_size: int, dropout, maxlen: int = 5000):
        super(PositionalEncoding, self).__init__()

        # 计算每个位置的正弦和余弦值的周期
        den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)
        
        # 根据最大长度创建位置编码矩阵
        pos = torch.arange(0, maxlen).reshape(maxlen, 1)
        pos_embedding = torch.zeros((maxlen, emb_size))
        
        # 根据位置编码矩阵填充正弦和余弦值
        pos_embedding[:, 0::2] = torch.sin(pos * den)
        pos_embedding[:, 1::2] = torch.cos(pos * den)
        
        # 将位置编码矩阵增加一个维度,用于后续的广播
        pos_embedding = pos_embedding.unsqueeze(-2)

        # 定义丢弃层
        self.dropout = nn.Dropout(dropout)
        # 注册位置编码矩阵为缓冲区,使其在模型推断时保持不变
        self.register_buffer('pos_embedding', pos_embedding)

    def forward(self, token_embedding: Tensor):
        # 将位置编码矩阵与输入的 token embedding 相加,并应用丢弃层
        return self.dropout(token_embedding +
                            self.pos_embedding[:token_embedding.size(0), :])

class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size: int, emb_size):
        super(TokenEmbedding, self).__init__()
        # 创建词嵌入层,vocab_size 表示词汇表大小,emb_size 表示词嵌入的维度
        self.embedding = nn.Embedding(vocab_size, emb_size)
        self.emb_size = emb_size

    def forward(self, tokens: Tensor):
        # 输入 tokens 是一个张量,包含了批次中的所有标记
        # 返回词嵌入后的张量,乘以 math.sqrt(self.emb_size) 进行缩放
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

我们创建一个后续词掩码,防止目标词注意到其后续词。我们还创建掩码,用于掩盖源和目标填充标记。


def generate_square_subsequent_mask(sz):
    """
    生成一个用于Transformer解码器的下三角形掩码(针对自回归生成)。

    Args:
    - sz (int): 方阵的大小,即序列长度。

    Returns:
    - torch.Tensor: sz x sz 的下三角形掩码张量,形状为 (sz, sz)。
    """
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask

def create_mask(src, tgt, PAD_IDX=0):
    """
    创建用于Transformer模型的掩码张量。

    Args:
    - src (torch.Tensor): 源序列张量,形状为 (src_seq_len, batch_size).
    - tgt (torch.Tensor): 目标序列张量,形状为 (tgt_seq_len, batch_size).
    - PAD_IDX (int): 填充标记的索引,默认为0.

    Returns:
    - torch.Tensor: 源序列掩码张量,形状为 (src_seq_len, src_seq_len).
    - torch.Tensor: 目标序列掩码张量,形状为 (tgt_seq_len, tgt_seq_len).
    - torch.Tensor: 源序列填充掩码张量,形状为 (batch_size, src_seq_len).
    - torch.Tensor: 目标序列填充掩码张量,形状为 (batch_size, tgt_seq_len).
    """
    src_seq_len = src.shape[0]
    tgt_seq_len = tgt.shape[0]

    # 生成目标序列的下三角形掩码
    tgt_mask = generate_square_subsequent_mask(tgt_seq_len)

    # 创建全零的源序列掩码(为了与pad mask组合使用)
    src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)

    # 创建源序列和目标序列的填充掩码
    src_padding_mask = (src == PAD_IDX).transpose(0, 1)
    tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)

    return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask

5.7定义模型

SRC_VOCAB_SIZE = len(ja_vocab)  # 源语言词汇表大小
TGT_VOCAB_SIZE = len(en_vocab)  # 目标语言词汇表大小
EMB_SIZE = 512  # 词嵌入维度
NHEAD = 8  # 注意力头的数量
FFN_HID_DIM = 512  # Feedforward层隐藏单元数
BATCH_SIZE = 16  # 批量大小
NUM_ENCODER_LAYERS = 3  # 编码器层数
NUM_DECODER_LAYERS = 3  # 解码器层数
NUM_EPOCHS = 16  # 训练轮数

# 创建Seq2SeqTransformer模型
transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
                                 EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
                                 FFN_HID_DIM)

# 对模型参数进行初始化,使用Xavier初始化方法
for p in transformer.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

# 将模型移动到设备(如GPU)上
transformer = transformer.to(device)

# 定义损失函数,使用交叉熵损失函数,忽略填充的部分(PAD_IDX)
loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)

# 定义优化器,使用Adam优化器
optimizer = torch.optim.Adam(
    transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9
)

# 定义训练函数,用于训练一个epoch
def train_epoch(model, train_iter, optimizer):
    model.train()
    losses = 0
    for idx, (src, tgt) in enumerate(train_iter):
        src = src.to(device)
        tgt = tgt.to(device)

        tgt_input = tgt[:-1, :]  # 去掉目标序列的最后一个标记,作为解码器的输入

        # 创建掩码
        src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)

        # 前向传播获取预测结果logits
        logits = model(src, tgt_input, src_mask, tgt_mask,
                       src_padding_mask, tgt_padding_mask, src_padding_mask)

        optimizer.zero_grad()  # 梯度清零

        tgt_out = tgt[1:, :]  # 去掉目标序列的第一个标记,作为损失函数的目标

        # 计算损失
        loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
        loss.backward()  # 反向传播

        optimizer.step()  # 更新模型参数
        losses += loss.item()  # 累加损失值
    return losses / len(train_iter)  # 返回平均损失

# 定义评估函数,用于在验证集上评估模型性能
def evaluate(model, val_iter):
    model.eval()
    losses = 0
    for idx, (src, tgt) in enumerate(val_iter):
        src = src.to(device)
        tgt = tgt.to(device)

        tgt_input = tgt[:-1, :]  # 去掉目标序列的最后一个标记,作为解码器的输入

        # 创建掩码
        src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)

        # 前向传播获取预测结果logits
        logits = model(src, tgt_input, src_mask, tgt_mask,
                       src_padding_mask, tgt_padding_mask, src_padding_mask)

        tgt_out = tgt[1:, :]  # 去掉目标序列的第一个标记,作为损失函数的目标

        # 计算损失
        loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
        losses += loss.item()  # 累加损失值
    return losses / len(val_iter)  # 返回平均损失

5.8开始训练

for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
  start_time = time.time()
  train_loss = train_epoch(transformer, train_iter, optimizer)
  end_time = time.time()
  print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
          f"Epoch time = {(end_time - start_time):.3f}s"))

5.9Try translating a Japanese sentence using the trained model

def greedy_decode(model, src, src_mask, max_len, start_symbol):
    src = src.to(device)  # 将源语言输入数据移动到指定的设备上(如GPU)
    src_mask = src_mask.to(device)  # 将源语言掩码数据移动到指定的设备上

    # 编码源语言输入序列
    memory = model.encode(src, src_mask)

    # 初始化目标语言序列,以起始符号填充,维度为 [1, 1]
    ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device)

    # 循环生成目标语言序列直到最大长度
    for i in range(max_len - 1):
        memory = memory.to(device)  # 将编码的记忆数据移动到设备上
        memory_mask = torch.zeros(ys.shape[0], memory.shape[0]).to(device).type(torch.bool)  # 创建记忆掩码
        tgt_mask = (generate_square_subsequent_mask(ys.size(0))  # 生成目标语言序列掩码
                    .type(torch.bool)).to(device)

        # 解码得到输出
        out = model.decode(ys, memory, tgt_mask)
        out = out.transpose(0, 1)  # 转置输出张量的维度
        prob = model.generator(out[:, -1])  # 使用生成器模型获取概率分布
        _, next_word = torch.max(prob, dim=1)  # 获取下一个词的索引
        next_word = next_word.item()  # 转换为Python整数

        ys = torch.cat([ys,
                        torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=0)  # 将预测的下一个词添加到目标语言序列中

        if next_word == EOS_IDX:  # 如果预测的下一个词是终止符号,则停止生成
            break

    return ys  # 返回生成的目标语言序列

def translate(model, src, src_vocab, tgt_vocab, src_tokenizer):
    model.eval()  # 设置模型为评估模式,不使用dropout等
    tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)] + [EOS_IDX]  # 将源语言文本转换为索引序列
    num_tokens = len(tokens)  # 获取序列长度
    src = (torch.LongTensor(tokens).reshape(num_tokens, 1))  # 将源语言序列转换为Tensor,并进行reshape操作
    src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool)  # 创建源语言掩码
    tgt_tokens = greedy_decode(model, src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()  # 使用贪婪解码生成目标语言序列的索引
    return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")  # 将生成的目标语言索引序列转换为文本并返回

translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)

由于我的电脑没用gpu这里就不运行了,有条件的uu可以运行尝试一下。

  • 35
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值