自然语言处理前馈网络

多层感知器

多层感知机(Multilayer Perceptron, MLP)是一种基本的人工神经网络模型,通常用于解决分类和回归问题。它由多个全连接的神经网络层组成,每一层都包含多个神经元(节点),各层之间全连接,即每个神经元都与前一层的所有神经元相连。

主要特点和结构:

1. 层次结构:
输入层:接受输入特征向量。
隐藏层:中间层,可以有多个,每一层都有多个神经元。隐藏层的存在使得MLP能够学习复杂的非线性关系。
输出层:输出层通常用于分类(softmax激活函数)或回归(线性激活函数)任务,其神经元数量等于分类或回归的输出维度。

2. 激活函数:
隐藏层:常用的激活函数包括ReLU(Rectified Linear Unit)、Sigmoid和Tanh等,用于引入非线性特性,增强模型的表达能力。
输出层:根据任务的不同选择不同的激活函数,如softmax用于多类别分类,线性激活函数用于回归。

3. 训练过程:
MLP通常通过反向传播算法(Backpropagation)和梯度下降优化算法来训练,如随机梯度下降(SGD)或其变种(如Adam),训练过程中,通过最小化损失函数来优化网络参数,使得模型能够更准确地预测输出。

4. 应用和限制:
应用:MLP适用于各种任务,包括图像分类、语音识别、自然语言处理等。它的设计简单直观,易于实现和调试。
限制:MLP在处理复杂的非线性数据关系时可能表现不佳,且对输入特征的处理较为敏感。在处理序列数据或处理高维稀疏数据时,可能需要结合其他技术或模型来提升性能。

总体而言,多层感知机作为神经网络的基础模型,提供了理解深度学习基础概念和建立复杂模型的框架。它的简单性和灵活性使其成为学习神经网络的良好起点,并且在实际应用中仍然具有重要的地位和应用前景。

Implementing MLPs in PyTorch

我们将介绍PyTorch中的一个实现。如前所述,MLP除了简单的感知器之外,还有一个额外的计算层。线性对象被命名为fc1和fc2,它们遵循一个通用约定,即将线性模块称为“完全连接层”,简称为“fc层”。除了这两个线性层外,还有一个修正的线性单元(ReLU)非线性,它在被输入到第二个线性层之前应用于第一个线性层的输出。由于层的顺序性,必须确保层中的输出数量等于下一层的输入数量。使用两个线性层之间的非线性是必要的,因为没有它,两个线性层在数学上等价于一个线性层4,因此不能建模复杂的模式。MLP的实现只实现反向传播的前向传递。这是因为PyTorch根据模型的定义和向前传递的实现,自动计算出如何进行向后传递和梯度更新。

import torch.nn as nn
import torch.nn.functional as F

class MultilayerPerceptron(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        Args:
            input_dim (int): the size of the input vectors
            hidden_dim (int): the output size of the first Linear layer
            output_dim (int): the output size of the second Linear layer
        """
        super(MultilayerPerceptron, self).__init__() # 调用父类 nn.Module 的构造函数
        self.fc1 = nn.Linear(input_dim, hidden_dim)# 定义第一层全连接层(输入层到隐藏层)
        self.fc2 = nn.Linear(hidden_dim, output_dim)# 定义第二层全连接层(隐藏层到输出层)

    def forward(self, x_in, apply_softmax=False):
        """The forward pass of the MLP

        Args:
            x_in (torch.Tensor): an input data tensor.
                x_in.shape should be (batch, input_dim)
            apply_softmax (bool): a flag for the softmax activation
                should be false if used with the Cross Entropy losses
        Returns:
            the resulting tensor. tensor.shape should be (batch, output_dim)
        """
        intermediate = F.relu(self.fc1(x_in))# 应用 ReLU 激活函数到第一层全连接层的输出
        output = self.fc2(intermediate)# 将激活后的输出传递到第二层全连接层

        if apply_softmax:
            output = F.softmax(output, dim=1) # 如果 apply_softmax 为 True,则应用 softmax 激活函数
        return output

我们实例化了MLP。由于MLP实现的通用性,可以为任何大小的输入建模。为了演示,我们使用大小为3的输入维度、大小为4的输出维度和大小为100的隐藏维度。请注意,在print语句的输出中,每个层中的单元数很好地排列在一起,以便为维度3的输入生成维度4的输出。

batch_size = 2 # number of samples input at once
input_dim = 3
hidden_dim = 100 # 第一层全连接层的输出维度(隐藏层)
output_dim = 4 # 第二层全连接层的输出维度(输出层)

# 初始化多层感知机(MLP)模型
mlp = MultilayerPerceptron(input_dim, hidden_dim, output_dim)
print(mlp)
MultilayerPerceptron(
  (fc1): Linear(in_features=3, out_features=100, bias=True)
  (fc2): Linear(in_features=100, out_features=4, bias=True)
)

我们可以通过传递一些随机输入来快速测试模型的“连接”。因为模型还没有经过训练,所以输出是随机的。在花费时间训练模型之前,这样做是一个有用的完整性检查。请注意PyTorch的交互性是如何让我们在开发过程中实时完成所有这些工作的,这与使用NumPy或panda没有太大区别:

import torch
def describe(x):
    print("Type: {}".format(x.type()))# 打印张量的数据类型
    print("Shape/size: {}".format(x.shape))# 打印张量的形状和尺寸
    print("Values: \n{}".format(x))# 打印张量的值

x_input = torch.rand(batch_size, input_dim)# 生成一个随机张量,形状为 (batch_size, input_dim)
describe(x_input)# 调用 describe 函数描述 x_input
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values: 
tensor([[0.3892, 0.8762, 0.9623],
        [0.8448, 0.7626, 0.6932]])
y_output = mlp(x_input, apply_softmax=False)# 使用 MLP 模型进行前向传播,不应用 softmax 激活函数
describe(y_output)# 调用 describe 函数描述 y_output
Type: torch.FloatTensor
Shape/size: torch.Size([2, 4])
Values: 
tensor([[ 0.1192, -0.1784,  0.3633, -0.2881],
        [ 0.2197, -0.0943,  0.4036, -0.1368]], grad_fn=<AddmmBackward>)

学习如何读取PyTorch模型的输入和输出非常重要。在前面的例子中,MLP模型的输出是一个有两行四列的张量。这个张量中的行与批处理维数对应,批处理维数是小批处理中的数据点的数量。列是每个数据点的最终特征向量。在某些情况下,例如在分类设置中,特征向量是一个预测向量。名称为“预测向量”表示它对应于一个概率分布。预测向量会发生什么取决于我们当前是在进行训练还是在执行推理。在训练期间,输出按原样使用,带有一个损失函数和目标类标签的表示。我们将在“示例:带有多层感知器的姓氏分类”中对此进行深入介绍。

但是,如果想将预测向量转换为概率,则需要额外的步骤。具体来说,需要softmax函数,它用于将一个值向量转换为概率。softmax有许多根。在物理学中,它被称为玻尔兹曼或吉布斯分布;在统计学中,它是多项式逻辑回归;在自然语言处理(NLP)社区,它是最大熵(MaxEnt)分类器。不管叫什么名字,这个函数背后的直觉是,大的正值会导致更高的概率,小的负值会导致更小的概率。在示例4-3中,apply_softmax参数应用了这个额外的步骤。在例4-4中,可以看到相同的输出,但是这次将apply_softmax标志设置为True:

y_output = mlp(x_input, apply_softmax=True)# 使用 MLP 模型进行前向传播,应用 softmax 激活函数
describe(y_output)# 调用 describe 函数描述 y_output
<span style="color:#000000"><span style="color:var(--jp-content-font-color1)"><span style="color:inherit"><span style="color:var(--jp-mirror-editor-variable-color)">describe</span>(<span style="color:var(--jp-mirror-editor-variable-color)">y_output</span>)<span style="color:var(--jp-mirror-editor-comment-color)"><em># 调用 describe 函数描述 y_output</em></span></span></span></span>
Type: torch.FloatTensor
Shape/size: torch.Size([2, 4])
Values: 
tensor([[0.2714, 0.2015, 0.3465, 0.1806],
        [0.2753, 0.2011, 0.3309, 0.1927]], grad_fn=<SoftmaxBackward>)

机器翻译

机器翻译(Machine Translation, MT)是利用计算机技术将一种自然语言的文本自动翻译成另一种自然语言的文本的过程。它是人工智能领域中自然语言处理(NLP)的重要应用之一,具有广泛的实际应用价值。

主要特点和方法:

1. 基本原理:
机器翻译的基本原理是将源语言的文本序列转换为目标语言的文本序列,保持语义内容的准确性和流畅性,主要涉及到词汇翻译、语法结构调整和语言风格转换等问题。

2. 传统方法:
基于规则的方法:通过语法、词汇和语言规则的约束,手工设计规则实现翻译。这种方法需要大量的人工工作和专业知识,适用于特定领域和语言对。
统计机器翻译(SMT):基于统计模型,如短语表和语言模型,通过大规模语料库学习翻译模型参数。主要包括基于短语的方法和基于句子的方法(如IBM模型、统计语言模型等)。

3. 深度学习方法:
神经机器翻译(NMT):随着深度学习技术的发展,神经网络在机器翻译中的应用逐渐取代了传统的统计方法。NMT使用编码器-解码器结构,通过编码器将源语言序列编码为语义向量表示,然后解码器将此向量转换为目标语言序列。
注意力机制:解决了长距离依赖和对输入序列各部分的不均衡关注问题,提升了翻译质量。

4. 应用和挑战:
应用:机器翻译在国际交流、跨语言信息检索、跨文化交流、技术文档翻译等领域有广泛应用。
挑战:语言之间的语法结构、词汇差异、歧义性和文化差异是机器翻译面临的主要挑战。特别是低资源语言对和领域特定的术语翻译问题。

总体而言,机器翻译技术在不断发展和进步,深度学习方法的应用使得翻译质量得到显著提升。随着数据量的增加和技术的进步,机器翻译在实现高质量、高效率翻译方面具有广阔的发展前景。

读取和预处理数据

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

!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模块
# sys.path.append("..")  # 将上级目录添加到sys.path中

import d2lzh_pytorch as d2l  # 导入d2lzh_pytorch模块并简写为d2l

# 定义特殊标记
PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'
# 设置可见的CUDA设备为0
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
# 根据是否有可用的GPU设置设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 打印PyTorch的版本和设备信息
print(torch.__version__, device)
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中
    all_tokens.extend(seq_tokens)
    # 在序列后添加EOS,然后用PAD填充直到序列长度达到max_seq_len
    seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
    # 将处理后的序列添加到all_seqs中
    all_seqs.append(seq_tokens)

# 使用所有的词来构造词典,并将所有序列中的词变换为词索引后构造Tensor
def build_data(all_tokens, all_seqs):
    # 构建词典,使用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]
    # 返回词典和转换后的索引Tensor
    return vocab, torch.tensor(indices)

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

def read_data(max_seq_len):
    in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []
    # 打开文件 'fr-en-small.txt'
    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(' ')
        # 如果加上EOS后长于max_seq_len,则忽略掉此样本
        if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
            continue
        # 处理输入序列并添加到相应的列表中
        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]
(tensor([ 5,  4, 45,  3,  2,  0,  0]), tensor([ 8,  4, 27,  3,  2,  0,  0]))

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

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

编码器

在编码器中,我们将输入语言的词索引通过词嵌入层得到词的表征,然后输入到一个多层门控循环单元中,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)
        # 定义GRU层,接收词嵌入后的向量作为输入,输出隐藏状态
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)

    def forward(self, inputs, state):
        # 输入形状是(批量大小, 时间步数)。将输出互换样本维和时间步维
        embedding = self.embedding(inputs.long()).permute(1, 0, 2)  # (seq_len, batch, input_size)
        # 将输入经过词嵌入层后的张量输入GRU模型,返回输出张量和更新后的状态
        return self.rnn(embedding, state)

    def begin_state(self):
        # 返回GRU的初始状态,这里简化为返回None,实际应根据需要初始化为零向量等
        return None

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

# 创建一个编码器对象,设置词汇表大小为10,词嵌入维度为8,GRU隐藏单元数为16,GRU层数为2
encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)

# 定义一个全零张量作为输入,形状为(4, 7),表示批量大小为4,序列长度为7的输入序列
zero_input = torch.zeros((4, 7))

# 调用编码器的begin_state方法,获取GRU的初始状态
initial_state = encoder.begin_state()

# 将输入序列和初始状态传递给编码器的forward方法,得到输出张量output和最终状态state
output, state = encoder(zero_input, initial_state)

# 打印输出张量output的形状和最终状态state的形状
output.shape, state.shape
(torch.Size([7, 4, 16]), torch.Size([2, 4, 16]))

注意力机制

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

def attention_model(input_size, attention_size):
    # 定义一个Sequential模型,用于堆叠多个层
    model = nn.Sequential(
        # 第一层是线性变换层,将输入大小为input_size的特征转换为大小为attention_size的特征,不使用偏置
        nn.Linear(input_size, attention_size, bias=False),
        # 第二层是Tanh激活函数,增加模型的非线性能力
        nn.Tanh(),
        # 第三层是线性变换层,将attention_size大小的特征转换为大小为1的特征,不使用偏置
        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
# 调用 attention_forward 函数计算注意力机制的输出
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层,输入包含注意力输出的背景向量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
        c = attention_forward(self.attention, enc_states, state[-1])
        # 将当前输入的词嵌入向量和背景向量c在特征维度上连接
        input_and_c = torch.cat((self.embedding(cur_input), c), dim=1)
        # 增加时间步维度,因为GRU接收的输入期望有时间步维度
        output, state = self.rnn(input_and_c.unsqueeze(0), state)
        # 去除时间步维度,将GRU的输出映射到词汇表大小的空间
        output = self.out(output).squeeze(dim=0)
        return output, state

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

训练模型

我们先实现batch_loss函数计算一个小批量的损失。解码器在最初时间步的输入是特殊字符BOS。之后,解码器在某时间步的输入为样本输出序列在上一时间步的词,即强制教学。此外,同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)  
    # 使用编码器的最终状态初始化解码器的隐藏状态
    
    dec_input = torch.tensor([out_vocab.stoi[BOS]] * batch_size) 
    # 解码器初始输入为BOS的索引,长度为批次大小
    
    mask, num_not_pad_tokens = torch.ones(batch_size,), 0  
    # 创建掩码变量,用于忽略填充项PAD的损失,初始全1
    
    l = torch.tensor([0.0])  
    # 初始化损失为0
    
    for y in Y.permute(1,0):  
        # 对目标序列Y进行逐时间步的循环处理
        
        dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)  
        # 解码器前向传播,获取解码器输出和更新的隐藏状态
        
        l = l + (mask * loss(dec_output, y)).sum()  
        # 计算当前时间步的损失,并累加到总损失l中
        
        dec_input = y  
        # 强制教学:将当前目标序列作为下一个时间步的解码器输入
        
        num_not_pad_tokens += mask.sum().item()  
        # 统计有效的非填充词汇数量
        
        mask = mask * (y != out_vocab.stoi[EOS]).float() 
        # 更新掩码变量,如果遇到EOS则停止掩码
        
    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')  
    # 定义交叉熵损失函数,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:  
            # 每10个epoch打印一次损失
            
            print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))  
            # 输出当前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  
# 注意力机制大小、dropout概率、学习率、批大小和训练轮数

# 创建编码器和解码器实例
encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers, drop_prob)  
# 创建编码器实例,输入词汇表大小、嵌入大小、隐藏层大小、层数和dropout率

decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers, attention_size, drop_prob)  
# 创建解码器实例,输出词汇表大小、嵌入大小、隐藏层大小、层数、注意力机制大小和dropout率

# 训练模型
train(encoder, decoder, dataset, lr, batch_size, num_epochs)  
# 调用训练函数,传入编码器、解码器、数据集、学习率、批大小和训练轮数进行模型训练
epoch 10, loss 0.479
epoch 20, loss 0.179
epoch 30, loss 0.115
epoch 40, loss 0.035
epoch 50, loss 0.022

预测不定长的序列

def translate(encoder, decoder, input_seq, max_seq_len):
    # 将输入序列分割成词汇,并添加EOS和PAD直到序列长度为max_seq_len
    in_tokens = input_seq.split(' ')
    in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
    
    # 将输入序列转换成Tensor,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 = []  # 初始化输出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())]  
        # 将预测的下一个词的索引转换成实际词汇
        if pred_token == EOS:  
            # 如果预测到了EOS(结束标记),则停止翻译
            break
        else:
            output_tokens.append(pred_token) 
            # 将预测的词汇添加到输出tokens列表中
            dec_input = pred 
            # 将当前预测的词汇作为下一个时间步的解码器输入
    
    return output_tokens 
    # 返回翻译得到的输出tokens列表

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

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

['they', 'are', 'watching', '.']

下面来实现BLEU的计算。

# 定义计算BLEU分数的函数,接受预测tokens、参考tokens和n-gram最大长度k作为参数
def bleu(pred_tokens, label_tokens, k):
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    
    # 计算brevity penalty,使用exp函数,最小为1,惩罚长度差距
    score = math.exp(min(0, 1 - len_label / len_pred))
    
    # 遍历不同的n-gram,从1到k
    for n in range(1, k + 1):
        num_matches, label_subs = 0, collections.defaultdict(int)
        
        # 统计参考tokens中每个n-gram的出现次数
        for i in range(len_label - n + 1):
            label_subs[''.join(label_tokens[i: i + n])] += 1
        
        # 计算预测tokens中与参考tokens中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精确度,并将其乘以权重,n-gram权重衰减
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    
    return score  # 返回BLEU分数

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

# 定义评分函数,接受输入序列、参考序列和n-gram最大长度k作为参数
def score(input_seq, label_seq, k):
    # 使用翻译函数得到预测tokens
    pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)
    
    # 将参考序列按空格分割为tokens
    label_tokens = label_seq.split(' ')
    
    # 计算BLEU分数并打印预测结果
    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 arguing .

小结

  • 可以将编码器—解码器和注意力机制应用于机器翻译中。
  • BLEU可以用来评价翻译结果。

练习

  • 如果编码器和解码器的隐藏单元个数不同或层数不同,我们该如何改进解码器的隐藏状态初始化方法?
线性变换: 使用线性变换将编码器最终时间步的隐藏状态映射到解码器隐藏状态的维度。可以使用全连接层或者简单的线性变换来调整维度。
重复: 如果编码器和解码器的层数不同,可以通过重复编码器隐藏状态的某些层来匹配解码器的层数。
使用额外参数: 在模型初始化时,可以通过额外的参数来指定如何初始化解码器的隐藏状态,以适应不同的隐藏单元个数或层数。
  • 在训练中,将强制教学替换为使用解码器在上一时间步的输出作为解码器在当前时间步的输入。结果有什么变化吗?
自回归特性加强: 解码器更加依赖自身的输出来生成下一个词汇,这样可以增强模型对上下文的理解和生成序列的连贯性。
训练过程变得更加复杂: 解码器在自回归训练中可能会面临积累误差和生成偏差的问题,需要更多的训练步骤和技术来稳定训练。
生成结果可能更加流畅: 如果模型能够良好地训练,使用自回归方法生成的结果可能会比强制教学方法生成的结果更加流畅和自然

使用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,请在你自己的电脑上尝试运行这一套代码
device
device(type='cpu')

获取并行数据集

在本教程中,我们将使用从 JParaCrawl 下载的日英并行数据集![JParaCrawl],它被描述为“NTT创建的最大的公开可用的英日平行语料库。它是通过大量抓取网络并自动对齐平行句子而创建的。你也可以在这里看到这篇论文。

import pandas as pd

# 从CSV文件中读取数据集,使用'\t'作为分隔符,header=None表示没有列名
df = pd.read_csv('./zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)

# 将第2列(英语句子)和第3列(日语句子)分别转换为列表
trainen = df[2].values.tolist()
trainja = df[3].values.tolist()

在导入所有日语和英语对应数据后,我删除了数据集中的最后一个数据,因为它缺少值。总的来说,trainen 和 trainja 中的句子数为 5,973,071,但是,出于学习目的,通常建议在一次性使用所有数据之前对数据进行采样并确保一切按预期工作,以节省时间。

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

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网站下载它们,或单击此处。

import sentencepiece as spm
# 使用SentencePiece加载英语和日语的tokenizer模型
en_tokenizer = spm.SentencePieceProcessor(model_file='spm.en.nopretok.model')
ja_tokenizer = spm.SentencePieceProcessor(model_file='spm.ja.nopretok.model'

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

# 使用英语的tokenizer对文本进行编码,返回编码后的字符串形式
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',
 '.']
# 使用日语的tokenizer对文本进行编码,返回编码后的字符串形式
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str')
['▁',
 '年',
 '金',
 '▁日本',
 'に住んでいる',
 '20',
 '歳',
 '~',
 '60',
 '歳の',
 '全ての',
 '人は',
 '、',
 '公的',
 '年',
 '金',
 '制度',
 'に',
 '加入',
 'しなければなりません',
 '。']

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

然后使用分词器和原始句子,构建从TorchText导入的Vocab对象。根据数据集的大小和计算能力,这个过程可能需要几秒钟或几分钟。不同的分词器也会影响构建词汇所需的时间,我尝试了其他几种日语分词器,但sentencepece对我来说似乎工作得很好,足够快。

# 定义构建词汇表的函数,接受句子列表和tokenizer作为参数
def build_vocab(sentences, tokenizer):
    counter = Counter()
    for sentence in sentences:
        counter.update(tokenizer.encode(sentence, out_type=str))
    # 使用Counter中的统计结果构建词汇表,并包含特殊标记'<unk>', '<pad>', '<bos>', '<eos>'
    return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])

# 使用日语tokenizer构建日语词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)
# 使用英语tokenizer构建英语词汇表
en_vocab = build_vocab(trainen, en_tokenizer)

在我们有了词汇表对象之后,我们就可以使用词汇表和tokenizer对象来为我们的训练数据构建张量。

# 定义数据处理函数,接受日语和英语句子列表作为输入
def data_process(ja, en):
    data = []
    # 遍历日语和英语句子列表,对每一对句子进行处理
    for (raw_ja, raw_en) in zip(ja, en):
        # 使用日语tokenizer将日语句子编码为tensor
        ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
                                  dtype=torch.long)
        # 使用英语tokenizer将英语句子编码为tensor
        en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
                                  dtype=torch.long)
        # 将处理后的日语tensor和英语tensor组成一个元组,并添加到数据列表中
        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)。

# 定义批量生成函数,接受一个批量的数据列表作为输入
def generate_batch(data_batch):
    ja_batch, en_batch = [], []
    # 遍历批量数据中的每一条数据
    for (ja_item, en_item) in data_batch:
        # 在日语句子的开头和结尾添加<BOS>和<EOS>标记,并拼接成一个tensor
        ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
        # 在英语句子的开头和结尾添加<BOS>和<EOS>标记,并拼接成一个tensor
        en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))
    
    # 对日语和英语批量数据进行填充,使用<PAD>标记进行填充
    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=1,
                        shuffle=True, collate_fn=generate_batch)

Sequence-to-sequence Transformer


接下来的几个代码和文本解释(以斜体书写)来自原始的PyTorch教程

除了BATCH_SIZE和单词de_vocab被更改为ja_vocab之外,我没有做任何更改。

Transformer是在“Attention is all you need”论文中提出的用于解决机器翻译任务的Seq2Seq模型。变压器模型由编码器和解码器块组成,每个块包含固定数量的层。

编码器通过一系列多头注意和前馈网络层对输入序列进行传播处理。编码器的输出称为存储器,与目标张量一起馈送到解码器。

# 导入Transformer相关模块
from torch.nn import (TransformerEncoder, TransformerDecoder,
                      TransformerEncoderLayer, TransformerDecoderLayer)

# 定义Seq2SeqTransformer模型类,继承自nn.Module
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)
        
        decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)
        self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)
        
        # 输出层,将Transformer的输出映射到目标词汇表的维度上
        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)

文本标记通过使用标记嵌入表示。位置编码被添加到标记嵌入中以引入词序的概念。

# 定义位置编码器类 PositionalEncoding
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), :])

# 定义词嵌入层类 TokenEmbedding
class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size: int, emb_size):
        super(TokenEmbedding, self).__init__()
        
        # 使用nn.Embedding定义词嵌入层,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)

我们创建一个后续单词掩码来阻止目标单词关注它的后续单词。我们还创建遮罩,用于屏蔽源和目标填充令牌

# 定义函数生成Transformer模型中的上三角掩码
def generate_square_subsequent_mask(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

# 创建用于Transformer模型的掩码
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)
    
    # 源序列的掩码初始化为全零矩阵
    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  # 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)

# 定义损失函数
loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)

# 定义优化器
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)  # 将源语言序列移动到GPU
        tgt = tgt.to(device)  # 将目标语言序列移动到GPU

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

        # 创建掩码
        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:,:]  # 去除目标语言序列的第一个词,作为损失计算的标签
        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)  # 将源语言序列移动到GPU
        tgt = tgt.to(device)  # 将目标语言序列移动到GPU

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

        # 创建掩码
        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:,:]  # 去除目标语言序列的第一个词,作为损失计算的标签
        loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))  # 计算损失
        losses += loss.item()  # 累加损失值
    return losses / len(val_iter)  # 返回平均损失值
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-24-39dd6d33a730> in <module>
     23 
     24 # 定义损失函数
---> 25 loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)
     26 
     27 # 定义优化器

NameError: name 'PAD_IDX' is not defined
for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):  # 使用tqdm显示进度条
    start_time = time.time()  # 记录开始时间
    train_loss = train_epoch(transformer, train_iter, optimizer)  # 训练一个epoch
    end_time = time.time()  # 记录结束时间
    # 打印训练信息:当前epoch数、训练损失、该epoch训练时间
    print(f"Epoch: {epoch}, Train loss: {train_loss:.3f}, Epoch time = {(end_time - start_time):.3f}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()
    # 对输入的源语言句子进行tokenization并添加起始和结束标记
    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)
    
    # 调用greedy_decode进行解码得到目标语言句子的token序列
    tgt_tokens = greedy_decode(model, src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()
    
    # 将token序列转换为目标语言文本并去除特殊标记
    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)
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 ...'
import pickle
# 导入 pickle 模块用于数据持久化

# 打开一个文件,准备存储数据
file = open('en_vocab.pkl', 'wb')
# 使用 pickle 将 en_vocab 对象存储到文件中
pickle.dump(en_vocab, file)
file.close()

# 打开一个文件,准备存储数据
file = open('ja_vocab.pkl', 'wb')
# 使用 pickle 将 ja_vocab 对象存储到文件中
pickle.dump(ja_vocab, file)
file.close()
# 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')  # 将这些信息保存为名为 'model_checkpoint.tar' 的文件
### 使用PE工具制作启动盘并安装Windows #### 准备工作 在开始之前,需准备一个容量至少为8GB的U盘以及一台能够正常运行操作系统的计算机。确保下载最新版本的PE工具箱,并解压至本地磁盘以便后续使用[^1]。 #### 制作启动盘的具体方法 启动PE工具箱后,在其主界面上找到“一键制作为USB启动盘”的按钮点击执行。此时会弹出警告提示框询问是否继续操作(此过程将会清除掉U盘中的全部数据),确认无误后再按确定键进行下一步设置。等待进度条完成整个写入流程即可得到一张可用于修复或者全新部署环境下的引导介质。 #### 开始安装Windows系统 将刚刚做好的PE启动U盘连接目标主机上电重启机器进入BIOS界面调整优先级最高的加载项为我们刚才插入的那个存储装置——即我们的PE启动U盘[^3]。保存更改退出回到正常的自检画面直至看到熟悉的桌面背景图案出现为止。接着按照屏幕上的指示依次选择合适的参数配置比如指定源文件位置、分配给各逻辑卷大小等等最后敲定所有细节之后按下那个显眼的大按钮:“开始安装”。静候片刻让后台自动处理剩下的事务直到新立起来的操作平台呈现在眼前[^2]。 ```python # 这是一个简单的Python脚本示例,用于展示如何自动化一些基本的任务。 import os def create_directory(path): try: os.makedirs(path) print(f"Directory '{path}' created successfully.") except Exception as e: print(f"Failed to create directory: {e}") create_directory("C:\\NewSystem") ``` 上述代码仅为演示目的编写的一个创建目录的小函数,实际应用中可根据具体需求扩展更多功能来辅助完成复杂的任务流如批量复制重要文档等动作前后的准备工作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值