如何掌握机器翻译

如何掌握机器翻译

1. 引言

NLP和机器翻译的背景

在当今信息爆炸的时代,自然语言处理(Natural Language Processing, NLP)和机器翻译技术正扮演着越来越重要的角色。NLP是人工智能(AI)领域的一个重要分支,旨在使计算机能够理解、解释、操作人类语言的能力。这种能力不仅限于简单的语法和词汇理解,还包括语义理解、情感分析以及生成自然语言文本的能力。

同时,机器翻译作为NLP技术的一个应用领域,其意义更为直接和实际。它不仅帮助跨文化交流变得更加便捷,还在国际贸易、学术研究、旅游业等领域发挥着重要作用。随着全球化的加深和信息技术的快速发展,机器翻译的需求和应用场景也在不断扩展和深化。

NLP和机器翻译的定义和重要性

NLP涉及将人类语言与计算机语言相结合,使计算机能够处理和理解自然语言数据。其核心挑战之一是克服语言的复杂性和多义性,确保计算机能够准确地理解和产生语言。NLP的发展经历了从早期基于规则的系统到如今基于统计和深度学习模型的进步,这使得计算机在语言理解和生成方面取得了巨大进步。

机器翻译则是NLP技术在实际应用中的一个典型例子,其目标是通过计算机自动翻译一种语言到另一种语言,从而消除语言壁垒,促进跨文化交流和理解。随着神经机器翻译(NMT)技术的崛起,机器翻译的质量和效率有了显著提升,尤其是在处理复杂语言结构和语境依赖性较强的翻译任务时。

本文的目的和结构

本文旨在探讨NLP和机器翻译技术的基本原理、发展历程以及当前面临的挑战与未来的发展方向。具体来说,将会深入探讨以下几个方面:

  1. 机器翻译:机器翻译是指将一段文本从一种语言自动翻译到另一种语言。因为一段文本序列在不同语言中的长度不一定相同,所以我们使用机器翻译为例来介绍编码器—解码器和注意力机制的应用。
  2. 读取和预处理数据:首先,我们定义了特殊符号如“”(padding)、“”和“”,用于数据预处理和标记序列开始和结束。接着,我们读取并预处理数据,构建词汇表并将文本转换为索引表示,以便模型处理。
  3. 含注意力机制的编码器—解码器:我们介绍了使用注意力机制的编码器—解码器结构。编码器通过词嵌入和多层门控循环单元将输入序列编码为隐藏状态。解码器则利用注意力机制和门控循环单元生成输出序列,以实现文本翻译。。
  4. 训练模型:我们定义了损失函数和训练函数,通过优化器迭代编码器和解码器的参数,使模型能够学习翻译任务中的复杂映射关系。训练过程中,我们使用强制教学机制和掩码变量来优化模型。
  5. **预测不定长的序列:**最后,我们实现了简单的贪婪搜索算法来生成解码器的输出序列,以便将输入的法语文本翻译为英语文本。该算法通过解码器和注意力机制生成预测序列,以实现精确的机器翻译。

通过这些内容的深入分析,读者将能够更全面地了解NLP和机器翻译在现代社会中的重要性和应用价值,以及未来的发展方向和潜力。

2. 机器翻译技术详解

读取和预处理数据

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

!tar -xf d2lzh_pytorch.tar
import collections
import os
import io
import math
import torch
from torch import nn
import torch.nn.functional as F
import torchtext.vocab as Vocab
import torch.utils.data as Data

import sys
# sys.path.append("..") 
import d2lzh_pytorch as d2l

PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'  # 定义特殊token
os.environ["CUDA_VISIBLE_DEVICES"] = "0"  # 设置CUDA环境变量,指定GPU设备编号为0
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  # 检测并选择使用CUDA设备或CPU进行计算

print(torch.__version__, device)  # 打印PyTorch版本号和当前选择的设备(CUDA或CPU)
1.5.0 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)  # 在序列末尾添加EOS,并用PAD填充至最大长度
    all_seqs.append(seq_tokens)  # 将处理后的序列加入到总序列列表中

# 使用所有的词来构造词典,并将所有序列中的词变换为词索引后构造Tensor
def build_data(all_tokens, all_seqs):
    vocab = Vocab.Vocab(collections.Counter(all_tokens),  # 使用所有词构建词汇表,包括特殊token
                        specials=[PAD, BOS, EOS])  # 指定特殊token
    indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]  # 将所有序列中的词转换为对应的索引
    return vocab, torch.tensor(indices)  # 返回词汇表对象和包含索引的Tensor

为了演示方便,我们在这里使用一个很小的法语—英语数据集。在这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用'\t'隔开。在读取数据时,我们在句末附上“”符号,并可能通过添加“”符号使每个序列的长度均为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)  # 构建输入数据的词汇表和Tensor
    out_vocab, out_data = build_data(out_tokens, out_seqs)  # 构建输出数据的词汇表和Tensor
    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]
(tensor([ 5,  4, 45,  3,  2,  0,  0]), tensor([ 8,  4, 27,  3,  2,  0,  0]))

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

我们将使用含注意力机制的编码器—解码器来将一段简短的法语翻译成英语。下面我们来介绍模型的实现。

编码器

在编码器中,我们将输入语言的词索引通过词嵌入层得到词的表征,然后输入到一个多层门控循环单元中。正如我们在6.5节(循环神经网络的简洁实现)中提到的,PyTorch的nn.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)  # 初始化词嵌入层
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)  # 初始化GRU循环神经网络层

    def forward(self, inputs, state):
        # 输入形状是(批量大小, 时间步数)。将输出互换样本维和时间步维
        embedding = self.embedding(inputs.long()).permute(1, 0, 2)  # 嵌入输入并调整维度顺序为(seq_len, batch, input_size)
        return self.rnn(embedding, state)  # 返回GRU层的输出和状态

    def begin_state(self):
        return None  # 返回初始状态,这里可以根据需要进行更改或扩展

下面我们来创建一个批量大小为4、时间步数为7的小批量序列输入。设门控循环单元的隐藏层个数为2,隐藏单元个数为16。编码器对该输入执行前向计算后返回的输出形状为(时间步数, 批量大小, 隐藏单元个数)。门控循环单元在最终时间步的多层隐藏状态的形状为(隐藏层个数, 批量大小, 隐藏单元个数)。对于门控循环单元来说,state就是一个元素,即隐藏状态;如果使用长短期记忆,state是一个元组,包含两个元素即隐藏状态和记忆细胞。

# 创建Encoder实例,指定词汇表大小为10,词嵌入维度为8,隐藏单元数为16,层数为2
encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)

# 对Encoder进行前向传播计算,输入是一个形状为(4, 7)的张量(批量大小为4,时间步数为7)
# 初始状态通过encoder.begin_state()获取,默认返回None
output, state = encoder(torch.zeros((4, 7)), encoder.begin_state())

# 输出output的形状和state的形状
output.shape, state.shape

(torch.Size([7, 4, 16]), torch.Size([2, 4, 16]))
注意力机制

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

def attention_model(input_size, attention_size):
    # 定义一个序列模型,包括两个线性层和一个Tanh激活函数
    model = nn.Sequential(
        nn.Linear(input_size, attention_size, bias=False),  # 第一个线性层,输入大小为input_size,输出大小为attention_size
        nn.Tanh(),  # Tanh激活函数,增强模型的非线性能力
        nn.Linear(attention_size, 1, bias=False)  # 第二个线性层,输入大小为attention_size,输出大小为1
    )
    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)  # 创建注意力模型,输入大小为2*num_hiddens,注意力大小为10
enc_states = torch.zeros((seq_len, batch_size, num_hiddens))  # 创建编码器状态张量,形状为(seq_len, batch_size, num_hiddens)
dec_state = torch.zeros((batch_size, num_hiddens))  # 创建解码器初始状态张量,形状为(batch_size, num_hiddens)
attention_forward(model, enc_states, dec_state).shape
torch.Size([4, 8])
含注意力机制的解码器

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

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

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)  # 定义GRU循环神经网络层
        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

训练模型

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

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):  # 遍历每个epoch
        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)))  # 每10个epoch打印损失平均值

接下来,创建模型实例并设置超参数。然后,我们就可以训练模型了。

# 定义模型超参数
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.466
epoch 20, loss 0.179
epoch 30, loss 0.048
epoch 40, loss 0.023
epoch 50, loss 0.009

预测不定长的序列

在10.10节(束搜索)中我们介绍了3种方法来生成解码器在每个时间步的输出。这里我们实现最简单的贪婪搜索。

def translate(encoder, decoder, input_seq, max_seq_len):
    # 将输入序列按空格分割为单词列表
    in_tokens = input_seq.split(' ')
    # 添加结束符EOS和填充符PAD,确保序列长度为max_seq_len
    in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
    # 将输入单词转换为对应的索引,并构建张量作为编码器的输入,batch=1
    enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]])
    # 初始化编码器的状态
    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 = []
    
    # 循环生成最大长度为max_seq_len的输出序列
    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())]
        # 如果预测到结束符EOS,停止生成序列
        if pred_token == 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)
['they', 'are', 'watching', '.']

评价翻译结果

评价机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy)[1]。对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。

具体来说,设词数为𝑛的子序列的精度为𝑝𝑛。它是预测序列与标签序列匹配词数为𝑛的子序列的数量与预测序列中词数为𝑛的子序列的数量之比。举个例子,假设标签序列为𝐴、𝐵、𝐶、𝐷、𝐸、𝐹,预测序列为𝐴、𝐵、𝐵、𝐶、𝐷,那么𝑝1=4/5,𝑝2=3/4,𝑝3=1/3,𝑝4=0𝑝1=4/5,𝑝2=3/4,𝑝3=1/3,𝑝4=0。设
l e n label 和 l e n pred len_{\text{label}}和len_{\text{pred}} lenlabellenpred
分别为标签序列和预测序列的词数,那么,BLEU的定义为

exp ⁡ ( min ⁡ ( 0 , 1 − l e n label l e n pred ) ) ∏ n = 1 k p n 1 / 2 n \exp\left(\min\left(0, 1 - \frac{len_{\text{label}}}{len_{\text{pred}}}\right)\right) \prod_{n=1}^k p_n^{1/2^n} exp(min(0,1lenpredlenlabel))n=1kpn1/2n

其中𝑘是我们希望匹配的子序列的最大词数。可以看到当预测序列和标签序列完全一致时,BLEU为1。

因为匹配较长子序列比匹配较短子序列更难,BLEU对匹配较长子序列的精度赋予了更大权重。另外,模型预测较短序列往往会得到较高𝑝𝑛值。因此,上式中连乘项前面的系数是为了惩罚较短的输出而设的。举个例子,当𝑘=2𝑘=2时,假设标签序列为𝐴、𝐵、𝐶、𝐷、𝐸、𝐹,而预测序列为𝐴𝐴、𝐵𝐵。虽然𝑝1=𝑝2=1𝑝1=𝑝2=1,但惩罚系数exp(1−6/2)≈0.14exp⁡(1−6/2)≈0.14,因此BLEU也接近0.14。

下面来实现BLEU的计算。

import math
import collections

def bleu(pred_tokens, label_tokens, k):
    # 计算预测序列和参考序列的长度
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    
    # 初始化BLEU分数,考虑长度惩罚
    score = math.exp(min(0, 1 - len_label / len_pred))
    
    # 计算n-gram匹配
    for n in range(1, k + 1):
        num_matches, label_subs = 0, collections.defaultdict(int)
        
        # 统计参考序列中所有长度为n的子序列出现次数
        for i in range(len_label - n + 1):
            label_subs[''.join(label_tokens[i: i + n])] += 1
        
        # 计算预测序列中与参考序列匹配的n-gram数量
        for i in range(len_pred - n + 1):
            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

接下来,定义一个辅助打印函数。

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

预测正确则分数为1。

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

bleu 1.000, predict: they are watching .
score('ils sont canadienne .', 'they are canadian .', k=2)

bleu 0.658, predict: they are russian .

3. 基于Transformer和PyTorch深度学习库来实现的日中机器翻译模型

导入所需的包

首先,让我们确保在系统中安装了以下包。如果发现缺少某些包,请确保安装它们。

import math
import torchtext
import torch
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
import io
import time
import pandas as pd
import numpy as np
import pickle
import tqdm
import sentencepiece as spm
torch.manual_seed(0)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# print(torch.cuda.get_device_name(0)) ## 如果你有GPU,请在你自己的电脑上尝试运行这一套代码
NVIDIA GeForce RTX 4090

获取平行数据集

在本教程中,我们将使用从 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()#[:10000]
trainja = df[3].values.tolist()#[:10000]
# trainen.pop(5972)
# trainja.pop(5972)

在导入所有日语和相应的英语句子后,我删除了数据集中最后一条数据,因为它有一个缺失值。总共有 5,973,071 个句子在 trainentrainja 中。然而,为了学习目的,通常建议先对数据进行采样,确保一切正常运行,然后再一次性使用所有数据,以节省时间。

下面是数据集中包含的一个句子的示例。

print(trainen[500])
print(trainja[500])
Chinese HS Code Harmonized Code System < HS编码 2905 无环醇及其卤化、磺化、硝化或亚硝化衍生物 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...
Japanese HS Code Harmonized Code System < HSコード 2905 非環式アルコール並びにそのハロゲン化誘導体、スルホン化誘導体、ニトロ化誘導体及びニトロソ化誘導体 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...

我们也可以使用不同的平行数据集来跟随本文的内容,只需确保我们能够将数据处理成如上所示的两个字符串列表,分别包含日语和英语句子。

准备分词器

与英语或其他字母语言不同,日语句子中不包含用空格分隔的单词。我们可以使用 JParaCrawl 提供的分词器,这些分词器是使用 SentencePiece 为日语和英语创建的。您可以访问 JParaCrawl 网站下载这些分词器,或者点击此处。

en_tokenizer = spm.SentencePieceProcessor(model_file='spm.en.nopretok.model')
ja_tokenizer = spm.SentencePieceProcessor(model_file='spm.ja.nopretok.model')

加载分词器后,您可以通过执行以下代码来测试它们。例如:

en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.", out_type='str')
['▁All',
 '▁residents',
 '▁aged',
 '▁20',
 '▁to',
 '▁59',
 '▁years',
 '▁who',
 '▁live',
 '▁in',
 '▁Japan',
 '▁must',
 '▁enroll',
 '▁in',
 '▁public',
 '▁pension',
 '▁system',
 '.']
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str')
['▁',
 '年',
 '金',
 '▁日本',
 'に住んでいる',
 '20',
 '歳',
 '~',
 '60',
 '歳の',
 '全ての',
 '人は',
 '、',
 '公的',
 '年',
 '金',
 '制度',
 'に',
 '加入',
 'しなければなりません',
 '。']

构建 TorchText 的 Vocab 对象并将句子转换为 Torch 张量

使用从 TorchText 导入的分词器和原始句子,我们可以构建 Vocab 对象。这个过程的耗时取决于数据集的大小和计算能力,可能需要几秒钟或几分钟。不同的分词器也会影响构建词汇表所需的时间。我尝试过几种日语的分词器,但是 SentencePiece 对我来说效果良好且速度足够快。

def build_vocab(sentences, tokenizer):
    # 初始化一个计数器
    counter = Counter()
    
    # 遍历每个句子
    for sentence in sentences:
        # 使用指定的分词器对句子进行编码,并更新计数器
        counter.update(tokenizer.encode(sentence, out_type=str))
    
    # 返回一个包含特殊标记的词汇表对象
    return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])

# 使用训练数据和对应的分词器构建日语词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)

# 使用训练数据和对应的分词器构建英语词汇表
en_vocab = build_vocab(trainen, en_tokenizer)

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

def data_process(ja, en):
    data = []
    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.append((ja_tensor_, en_tensor_))
    
    # 返回处理后的数据列表
    return data

# 使用训练数据集进行数据处理,得到训练数据
train_data = data_process(trainja, trainen)

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

这里,我将 BATCH_SIZE 设置为 16,以防止出现 “cuda out of memory” 错误,但这取决于诸如您的机器内存容量、数据大小等各种因素,因此根据需要自由调整批处理大小(注意: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 = [], []
    
    # 遍历批处理数据中的每一对(ja_item, en_item)
    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

# 使用DataLoader创建训练数据迭代器,进行批处理、打乱顺序并生成批处理数据
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)

序列到序列的Transformer

下面的几段代码和文本解释(用斜体表示)来自原始的PyTorch教程[https://pytorch.org/tutorials/beginner/translation_transformer.html]。我没有进行任何修改,除了将BATCH_SIZE和单词de_vocab改为ja_vocab。

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__()
        
        # 定义 Transformer 编码器层
        encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)
        self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
        
        # 定义 Transformer 解码器层
        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)

文本标记通过使用标记嵌入(token embeddings)表示。为了引入单词顺序的概念,还会添加位置编码(positional encoding)到标记嵌入中。

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)

        # 定义 Dropout 层
        self.dropout = nn.Dropout(dropout)
        
        # 注册位置编码矩阵为缓冲区
        self.register_buffer('pos_embedding', pos_embedding)

    def forward(self, token_embedding: Tensor):
        # 将位置编码添加到词嵌入上,并应用 Dropout
        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__()
        
        # 定义词嵌入层
        self.embedding = nn.Embedding(vocab_size, emb_size)
        self.emb_size = emb_size

    def forward(self, tokens: Tensor):
        # 返回词嵌入,并进行缩放
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

我们创建一个后续词掩码(subsequent word mask),以防止目标词汇关注其后续词汇。我们还创建掩码,用于遮蔽源和目标的填充标记。

def generate_square_subsequent_mask(sz):
    # 生成大小为 (sz, sz) 的上三角矩阵,且对角线和上三角部分元素为 1
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    
    # 将 0 的位置填充为 -inf,将 1 的位置填充为 0.0
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask

def create_mask(src, tgt):
    src_seq_len = src.shape[0]
    tgt_seq_len = tgt.shape[0]

    # 生成目标序列的掩码
    tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
    
    # 源序列的掩码全部为 0
    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

# 定义超参数
SRC_VOCAB_SIZE = len(ja_vocab)
TGT_VOCAB_SIZE = len(en_vocab)
EMB_SIZE = 512
NHEAD = 8
FFN_HID_DIM = 512
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 或 CPU)
transformer = transformer.to(device)

# 定义交叉熵损失函数,忽略填充标记的损失
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, :]  # 目标输入为目标序列去掉最后一个 token

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

        # 前向传播计算模型输出
        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:, :]  # 目标输出为目标序列去掉第一个 token
        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, :]  # 目标输入为目标序列去掉最后一个 token

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

        # 前向传播计算模型输出
        logits = model(src, tgt_input, src_mask, tgt_mask,
                       src_padding_mask, tgt_padding_mask, src_padding_mask)
        tgt_out = tgt[1:, :]  # 目标输出为目标序列去掉第一个 token
        loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))  # 计算损失
        losses += loss.item()
    return losses / len(val_iter)  # 返回平均损失

开始训练

最终,在准备好必要的类和函数之后,我们准备好训练我们的模型了。不过需要指出的是,训练完成所需的时间可能会因计算能力、参数设置和数据集大小等因素而大相径庭。

for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
    start_time = time.time()  # 记录 epoch 开始时间
    train_loss = train_epoch(transformer, train_iter, optimizer)  # 训练一个 epoch 并获取训练损失
    end_time = time.time()  # 记录 epoch 结束时间

    # 打印当前 epoch 的训练损失和时间
    print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
           f"Epoch time = {(end_time - start_time):.3f}s"))

  0%|                                                                                                                                                                                            | 0/16 [00:00<?, ?it/s]

试着使用训练模型翻译一个日语句子。

首先,我们创建用于翻译新句子的函数。这包括获取日语句子、分词、转换为张量、推断翻译结果,然后将结果解码回英文句子。

def greedy_decode(model, src, src_mask, max_len, start_symbol):
    src = src.to(device)  # 将输入移动到设备上
    src_mask = src_mask.to(device)  # 将输入掩码移动到设备上
    memory = model.encode(src, src_mask)  # 编码输入
    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()  # 获取单词索引
        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()  # 设定模型为评估模式
    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) )  # 将标记转换为张量
    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)

' ▁H S ▁ 代 码 ▁85 15 ▁ 焊 接 设 备 ( 包 括 电 气 加 热 ) 。 '
trainen.pop(5)
'Chinese HS Code Harmonized Code System < HS编码 8515 : 电气(包括电热气体)、激光、其他光、光子束、超声波、电子束、磁脉冲或等离子弧焊接机器及装置,不论是否 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...'
trainja.pop(5)
'Japanese HS Code Harmonized Code System < HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)、レーザーその他の光子ビーム式、超音波式、電子ビーム式、 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...'

保存词汇表对象和训练好的模型

最后,在训练完成后,我们将首先使用 Pickle 保存词汇表对象(en_vocab 和 ja_vocab)。

import pickle

# 打开一个文件,用于存储数据
file = open('en_vocab.pkl', 'wb')
# 将信息写入该文件
pickle.dump(en_vocab, file)
file.close()

# 打开另一个文件,用于存储数据
file = open('ja_vocab.pkl', 'wb')
# 将信息写入该文件
pickle.dump(ja_vocab, file)
file.close()

最后,我们还可以使用 PyTorch 的保存和加载函数将模型保存供以后使用。一般来说,根据以后使用的目的,有两种保存模型的方式。第一种是仅用于推断,我们可以稍后加载模型并用它来从日语翻译成英语。

# save model for inference
torch.save(transformer.state_dict(), 'inference_model')

第二种方式也是用于推断,但同时也适用于以后加载模型并恢复训练的情况。

# 保存模型和检查点以便以后恢复训练
torch.save({
  'epoch': NUM_EPOCHS,  # 训练的轮数
  'model_state_dict': transformer.state_dict(),  # 模型的状态字典
  'optimizer_state_dict': optimizer.state_dict(),  # 优化器的状态字典
  'loss': train_loss,  # 训练损失
  }, 'model_checkpoint.tar')  # 保存检查点文件

总结

NVIDIA GeForce RTX 4090
Chinese HS Code Harmonized Code System < HS编码 2905 无环醇及其卤化、磺化、硝化或亚硝化衍生物 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...
Japanese HS Code Harmonized Code System < HSコード 2905 非環式アルコール並びにそのハロゲン化誘導体、スルホン化誘導体、ニトロ化誘導体及びニトロソ化誘導体 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...
['▁All', '▁residents', '▁aged', '▁20', '▁to', '▁59', '▁years', '▁who', '▁live', '▁in', '▁Japan', '▁must', '▁enroll', '▁in', '▁public', '▁pension', '▁system', '.']
['▁', '年', '金', '▁日本', 'に住んでいる', '20', '歳', '~', '60', '歳の', '全ての', '人は', '、', '公的', '年', '金', '制度', 'に', '加入', 'しなければなりません', '。']
C:\Users\GCheney\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\torch\nn\modules\transformer.py:306: UserWarning: enable_nested_tensor is True, but self.use_nested_tensor is False because encoder_layer.self_attn.batch_first was not True(use batch_first for better inference performance)
  warnings.warn(f"enable_nested_tensor is True, but self.use_nested_tensor is False because {why_not_sparsity_fast_path}")
  0%|          | 0/16 [00:00<?, ?it/s]C:\Users\GCheney\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\torch\nn\functional.py:5504: UserWarning: 1Torch was not compiled with flash attention. (Triggered internally at ..\aten\src\ATen\native\transformers\cuda\sdp_utils.cpp:455.)
  attn_output = scaled_dot_product_attention(q, k, v, attn_mask, dropout_p, is_causal)
C:\Users\GCheney\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\torch\nn\functional.py:5137: UserWarning: Support for mismatched key_padding_mask and attn_mask is deprecated. Use same type for both instead.
  warnings.warn(
Epoch: 1, Train loss: 4.470, Epoch time = 160.630s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
  6%|▋         | 1/16 [02:40<40:11, 160.78s/it]Epoch: 2, Train loss: 3.483, Epoch time = 185.904s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
 12%|█▎        | 2/16 [05:46<40:58, 175.64s/it]Epoch: 3, Train loss: 3.079, Epoch time = 152.645s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
 19%|█▉        | 3/16 [08:19<35:47, 165.21s/it]Epoch: 4, Train loss: 2.784, Epoch time = 152.868s
 25%|██▌       | 4/16 [10:52<32:04, 160.40s/it]Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
Epoch: 5, Train loss: 2.566, Epoch time = 181.054s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
 31%|███▏      | 5/16 [13:53<30:47, 167.92s/it]Epoch: 6, Train loss: 2.400, Epoch time = 169.233s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
 38%|███▊      | 6/16 [16:43<28:04, 168.42s/it]Epoch: 7, Train loss: 2.291, Epoch time = 167.876s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
 44%|████▍     | 7/16 [19:31<25:14, 168.29s/it]Epoch: 8, Train loss: 2.194, Epoch time = 177.605s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
 50%|█████     | 8/16 [22:29<22:50, 171.30s/it]Epoch: 9, Train loss: 2.111, Epoch time = 175.049s
 56%|█████▋    | 9/16 [25:24<20:07, 172.52s/it]Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
Epoch: 10, Train loss: 2.041, Epoch time = 153.576s
 62%|██████▎   | 10/16 [27:57<16:40, 166.72s/it]Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
Epoch: 11, Train loss: 1.980, Epoch time = 165.039s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
 69%|██████▉   | 11/16 [30:43<13:51, 166.25s/it]Epoch: 12, Train loss: 1.928, Epoch time = 233.401s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
 75%|███████▌  | 12/16 [34:36<12:26, 186.73s/it]Epoch: 13, Train loss: 1.882, Epoch time = 162.404s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
 81%|████████▏ | 13/16 [37:19<08:58, 179.40s/it]Epoch: 14, Train loss: 1.841, Epoch time = 163.605s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
 88%|████████▊ | 14/16 [40:03<05:49, 174.68s/it]Epoch: 15, Train loss: 1.804, Epoch time = 188.958s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
 94%|█████████▍| 15/16 [43:12<02:59, 179.03s/it]Epoch: 16, Train loss: 1.769, Epoch time = 165.612s
Saved model checkpoint to E:\PythonProject\nlp_transformer\transformer_epoch_{epoch}.pt
100%|██████████| 16/16 [45:57<00:00, 172.37s/it]
Traceback (most recent call last):
  File "E:\PythonProject\nlp_transformer\main.py", line 364, in <module>
    trainja.pop(5)
    ^^^^^^^^^^^^^^^
  File "E:\PythonProject\nlp_transformer\main.py", line 356, in translate
    src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool)  # 创建源句子的掩码
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\PythonProject\nlp_transformer\main.py", line 356, in <listcomp>
    src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool)  # 创建源句子的掩码
                          ^^^^^^^^^^^^^^^^^^^^
TypeError: unsupported operand type(s) for +: 'Vocab' and 'str'

进程已结束,退出代码为 1
我在本地运行该代码,修改许久仍然报错
尝试更改许久代码,最终卡在得到了训练好的模型(保存在我本地)以及如下内容:
NVIDIA GeForce RTX 4090
Chinese HS Code Harmonized Code System < HS编码 2905 无环醇及其卤化、磺化、硝化或亚硝化衍生物 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...
Japanese HS Code Harmonized Code System < HSコード 2905 非環式アルコール並びにそのハロゲン化誘導体、スルホン化誘導体、ニトロ化誘導体及びニトロソ化誘導体 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...
['▁All', '▁residents', '▁aged', '▁20', '▁to', '▁59', '▁years', '▁who', '▁live', '▁in', '▁Japan', '▁must', '▁enroll', '▁in', '▁public', '▁pension', '▁system', '.']
['▁', '年', '金', '▁日本', 'に住んでいる', '20', '歳', '~', '60', '歳の', '全ての', '人は', '、', '公的', '年', '金', '制度', 'に', '加入', 'しなければなりません', '。']

4. 当前挑战与未来展望

当前机器翻译面临的挑战主要包括多义性、文化差异和语法结构复杂性。多义性指的是同一个词或句子在不同上下文中可能有不同的含义,而文化差异使得一些习语、成语和文化背景相关的表达在翻译时难以准确传达。语法结构的复杂性则包括不同语言的语序差异、形态变化和长句处理的难度。这些问题使得机器翻译在精确和自然表达方面面临诸多困难。

5. 结论

自然语言处理(NLP)和机器翻译在现代社会中具有重要性,因其能促进跨语言交流,消除语言障碍,提升全球化进程。它们在多个领域有广泛应用,包括跨国企业的多语言客户服务、全球电子商务的产品信息翻译、国际新闻传播、多语言教育平台、以及帮助移民和难民融入新环境等。此外,NLP技术还广泛应用于情感分析、语音识别、智能助手和信息检索等方面,进一步提升了人们的生活质量和工作效率。

6. 参考文献

[1] Papineni, K., Roukos, S., Ward, T., & Zhu, W. J. (2002, July). BLEU: a method for automatic evaluation of machine translation. In Proceedings of the 40th annual meeting on association for computational linguistics (pp. 311-318). Association for Computational Linguistics.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值