08. Transformer模型

一、什么是 Transformer 模型

  此前的 Seq2Seq 模型通过注意力机制取得了一定提升,但由于整体结构仍依赖 RNN,依然存在计算效率低、难以建模长距离依赖等结构性限制。

  为了解决这些问题,Google 在 2017 年发表一篇论文《Attention Is All You Need》,提出了一种全新的模型架构 ——Transformer。该模型完全摒弃了 RNN 结构,转而 使用注意力机制直接建模序列中各位置之间的关系。通过这种方式,Transformer 不仅显著提升了训练效率,也增强了模型对长距离依赖的建模能力。

  Transformer 的提出对自然语言处理产生了深远影响。在机器翻译任务中,它首次超越了 RNN 模型的表现,并成为后续各类预训练语言模型的基础框架,如 BERT、GPT 等。这些模型推动 NLP 进入了 “预训练 + 微调” 的新时代,极大地提升了模型在多种任务上的通用性与性能。如今,Transformer 架构不仅广泛应用于 NLP,还扩展至语音识别、图像处理、代码生成等多个领域,成为现代深度学习中最具代表性的通用模型之一。

  PyTorch 提供了对 Transformer 的官方实现,该模块封装了完整的编码器-解码器结构,可直接应用于机器翻译、文本生成等典型的序列建模任务。我们在终端中输入 pip 指令安装 PyTorch。默认是从国外的主站上下载,因此,我们可能会遇到网络不好的情况导致下载失败。我们可以在 pip 指令后通过 -f 指定国内镜像源下载。

pip install torch torchvision -f https://mirrors.aliyun.com/pytorch-wheels/cu132

  国内常用的 pip 下载源列表:

  安装完 PyTorch 之后,我们可以通过如下代码查看 CUDA 是否可用:

import torch

print(f"PyTorch的版本:{torch.__version__}")
print(f"CUDA是否可用:{torch.cuda.is_available()}")

如果输出 CUDA 不可用,有可能你使用国内镜像只同步 PyPI 主站,不包含 PyTorch 官方 CUDA 专用包,pip 只会找到 CPU 版,找不到 CUDA 版。因此,我们还需要使用 --extra-index-url https://download.pytorch.org/whl/cu132 补充官方 CUDA 源 。

二、核心思想

  在 Seq2Seq 模型中,注意力机制的引入显著增强了模型的表达能力。它允许 解码器在生成每一个目标词时,根据当前解码状态动态选择源序列中最相关的位置,并据此融合信息。这一机制有效缓解了将整句信息压缩为固定向量所带来的信息瓶颈,显著提升了翻译等任务中的建模效果。

  进一步分析可以发现,注意力机制不仅是信息提取的工具,其本质是 在每一个目标位置上,显式建模该位置与源序列中各位置之间的依赖关系。

  与此同时,循环神经网络(RNN)作为 Seq2Seq 模型的核心结构,其作用也在于建模序列中的依赖关系。通过隐藏状态的递归传递,RNN 使当前位置的表示能够整合前文信息,从而隐式捕捉上下文依赖。从功能角度看,RNN 与注意力机制完成的是同一类任务:建立序列中不同位置之间的依赖联系。

  既然注意力机制也具备建模依赖关系的能力,那么理论上,它就可以在功能上替代 RNN。

  此外,相比 RNN,注意力机制在结构上具备明显优势:无需顺序计算,便于并行处理,任意位置间可直接建立联系,更适合捕捉长距离依赖。因此,它不仅具备替代的可能,也在效率与效果上表现更优。

  既然如此,我们可以将 Seq2Seq 中的 RNN 结构全部替换为注意力机制。

使用Transformer代替RNN

三、整体结构

  Transformer 的整体结构延续了 Seq2Seq 模型中 “编码器-解码器” 的设计理念,其中,编码器(Encoder)负责 对输入序列进行理解和表示,而 解码器(Decoder)则 根据编码器的输出逐步生成目标序列。

  与基于 RNN 的 Seq2Seq 模型一样,Transformer 的解码器采用 自回归方式生成目标序列。不同之处在于,每一步的输入是此前已生成的全部词,模型会输出一个与输入长度相同的序列,但我们 只取最后一个位置的结果作为当前预测。这个过程不断重复,直到生成结束标记 <eos>。

  此外,Transformer 的编码器和解码器模块分别由多个结构相同的层堆叠而成。通过层层堆叠,模型能够逐步提取更深层次的语义特征,从而增强对复杂语言现象的建模能力。标准的 Transformer 模型通常包含 6个编码器层和 6 个解码器层。

Transformer的整体结构

  在 Pytorch 中,我们可以使用 Transformer 类创建 Transformer 模型对象。

torch.nn.Transformer(
    d_model: int = 512,                                                         # 编码器和解码器输入的特征维度
    nhead: int = 8,                                                             # 多头注意力机制中并行注意力头的数量
    num_encoder_layers: int = 6,                                                # 编码器一侧堆叠的层数,即模型 “深度“
    num_decoder_layers: int = 6,                                                # 解码器一侧堆叠的层数
    dim_feedforward: int = 2048,                                                # 前馈网络(FFN)隐藏层的维度
    dropout: float = 0.1,                                                       # 随机失活的比例
    activation: str | ((Tensor) -> Tensor) = F.relu,                            # FFN 中间层的激活函数
    custom_encoder: Any | None = None,                                          # 自定义编码器模块
    custom_decoder: Any | None = None,                                          # 自定义解码器模块
    layer_norm_eps: float = 0.00001,                                            # Layer Normalization 分母中的微小常数
    batch_first: bool = False,                                                  # 是否将批量维度作为第一个维度
    norm_first: bool = False,                                                   # 残差连接中 Layer Norm 的位置是否在前馈网络之前或之后
    bias: bool = True,                                                          # 是否在 QKV 投影层和 FFN 线性层中添加偏置项
    device: Unknown | None = None,                                              # 模型参数所使用的设备
    dtype: Unknown | None = None                                                # 模型参数的类型
)

  参数 d_model 是整个模型内部流转的 特征向量维度(即词嵌入或隐状态的维度)。编码器/解码器的输入输出、以及注意力机制中的 Query/Key/Value 都使用这个维度。所有输入张量最后一维必须是它。

  参数 nhead 是 多头注意力的头数。原始输入 d_model 会被切分成 nhead 份(每份维度 = d_model / nhead),各头并行计算注意力再拼接。必须满足 d_model 能被 nhead 整除。

  参数 dropout 是 随机失活比例。作用于注意力权重、FFN 内部以及残差连接相加后的输出,用于防止过拟合。

  参数 batch_first 用于 控制输入张量的维度顺序,形状为 (src_len, batch_size, d_model)。如果 batch_first = True,则为 (batch_size, src_len, d_model)。

  参数 norm_first 是 残差连接中 Layer Norm 的位置。如果为 True(前归一化 Pre-Norm)则 输出 = x + Sublayer(Norm(x)),训练更稳定,收敛更快,现代大模型(如 GPT)几乎都使用此方式。如果为 False(后归一化 Post-Norm)则输出 = Norm(x + Sublayer(x)),原始论文所用,但对学习率敏感。

  torch.nn.Transformer 封装了完整的前向传播逻辑,其 forward() 方法定义了 \(编码器 \rarr 解码器\) 的执行流程。该函数接收源语言序列(src_sequence,编码器输入)和目标语言序列(tgt_sequence,解码器输入)作为输入,以解码器预测结果作为输出。

torch.nn.Transformer.forward(
    src: Tensor,                                                                # 输入编码器的源序列
    tgt: Tensor,                                                                # 输入解码器的目标序列
    src_mask: Tensor | None = None,                                             # 编码器的自注意力掩码
    tgt_mask: Tensor | None = None,                                             # 解码器的自注意力掩码
    memory_mask: Tensor | None = None,                                          # 解码器交叉注意力的掩码
    src_key_padding_mask: Tensor | None = None,                                 # 编码器的键值填充掩码
    tgt_key_padding_mask: Tensor | None = None,                                 # 解码器的键值填充掩码
    memory_key_padding_mask: Tensor | None = None,                              # 编码器输出的键值填充掩码
    src_is_causal: bool | None = None,                                          # 编码器是否使用自动生成上三角因果掩码
    tgt_is_causal: bool | None = None,                                          # 解码器是否使用自动生成上三角因果掩码
    memory_is_causal: bool = False                                              # 编码器输出是否使用自动生成上三角因果掩码
) -> Tensor                                                                     # 解码器最终的输出序列

  参数 src 是 源序列的嵌入表示,通常由词向量与位置编码相加得到,作为编码器的输入,形状为 (src_len, batch_size, d_model)。如果 batch_first = True,则为 (batch_size, src_len, d_model)。其中,src_len 为 源序列的长度,batch_size 为 批量大小,d_model 为 编码器输入的特征维度。

  参数 tgt 是 目标序列的嵌入表示,通常由词向量与位置编码相加得到,作为解码器的输入,形状为 (tgt_len, batch_size, d_model)。如果 batch_first = True,则为 (batch_size, tgt_len, d_model)。其中,tgt_len 为 目标序列序列的长度,batch_size 为 批量大小,d_model 为 解码器输入的特征维度。

  参数 tgt_mask 用于 解码器的自注意力机制,常用于训练阶段的自回归任务,防止模型关注当前位置之后的 token,避免信息泄露。是一个形状为 (tgt_len, tgt_len) 的上三角矩阵,类型为 float,遮挡位置为 -inf,其余为 0。也支持 bool 类型(True 表示遮挡),内部会自动转换为加性掩码。

  参数 src_key_padding_mask 用于 编码器中的自注意力机制,用以 屏蔽源序列中填充 <pad> 的位置,防止模型在计算注意力时关注无意义的内容。其张量形状为 (batch_size, src_len),其中值为 True 的位置表示应被忽略(即不参与注意力计算)。

  参数 tgt_key_padding_mask 用于 解码器中的自注意力机制,用以 屏蔽目标序列中填充 <pad> 的位置,防止模型在计算注意力时关注无意义的内容。其张量形状为 (batch_size, tgt_len),其中值为 True 的位置表示应被忽略(即不参与注意力计算)。

  参数 memory_key_padding_mask 用于 解码器的交叉注意力机制,屏蔽编码器输出中的 <pad> 位置,防止解码器关注源序列中的无效 token。形状为 (batch_size, src_len),值为 True 的位置将被忽略。通常与 src_key_padding_mask 相同。

  该函数返回 解码器输出的隐藏状态序列,形状为 (batch_size, tgt_len, d_model)。表示目标序列中每个位置的上下文表示,通常会送入线性层和 softmax(),用于生成词表上的预测概率。

四、编码器

4.1、编码器结构

  Transformer 的编码器用于 理解输入序列的语义信息,并生成每个 token 的上下文表示,为解码器生成目标序列提供基础。编码器由多个结构相同的编码器层(Encoder Layer)堆叠而成。

Transformer的编码器

  每个 Encoder Layer 的主要任务都是对其 输入序列进行上下文建模,使每个位置的表示都能融合来自 整个序列的全局信息。每个 Encoder Layer 都包含两个子层(sublayer),分别是 自注意力子层(Self-Attention Sublayer)和 前馈神经网络子层(Feed-Forward Sublayer)。

编码器层的结构

  各层作用如下:

  • 自注意力子层(Self-Attention Sublayer):用于捕捉序列中各位置之间的依赖关系。
  • 前馈神经网络子层(Feed-Forward Sublayer):用于对每个位置的表示进行非线性变换,从而提升模型的表达能力。

  torch.nn.Transformer 模块中包含一个编码器部分,可通过属性 transformer.encoder 访问,其本质是一个 torch.nn.TransformerEncoder 实例。通过其 forward() 方法,可以对源序列进行编码,提取上下文相关的语义表示。

torch.nn.TransformerEncoder.forward(
    src: Tensor,                                                                # 输入编码器的源序列张量
    mask: Tensor | None = None,                                                 # 注意力掩码
    src_key_padding_mask: Tensor | None = None,                                 # 填充掩码
    is_causal: bool | None = None                                               # 是否自动生成下三角因果掩码
) -> Tensor                                                                     # 编码后的输出张量

4.2、自注意力层

  自注意力机制(Self-Attention)是 Transformer 编码器的核心结构之一,它的作用是 在序列内部建立各位置之间的依赖关系,使模型能够为每个位置生成融合全局信息的表示。

自注意力层

之所以被称为 “自” 注意力,是因为模型在计算每个位置的表示时,所参考的信息全部来自同一个输入序列本身,而不是来自另一个序列。

  自注意力的完整计算过程如下:

【1】、生成 Query、Key、Value 向量

  自注意力机制的第一步,是将输入序列中的每个位置表示映射为三个不同的向量,分别是 查询(Query)、键(Key) 和 值(Value)。这些向量的作用如下:

  • Query:表示当前词的用于发起注意力匹配的向量。
  • Key:表示序列中每个位置的内容标识,用于与 Query 进行匹配。
  • Value:表示该位置携带的信息,用于加权汇总得到新的表示。

  自注意力的核心思想是:每个位置用自身的 Query 向量,与整个序列中所有位置的 Key 向量进行相关性计算,从而得到注意力权重,并据此对对应的 Value 向量加权汇总,形成新的表示。

  三个向量的计算公式如下:

QKV向量的计算公式

【2】、计算位置间相关性

  完成 Query、Key、Value 向量的生成后,模型会使用每个位置的 Query 向量与所有位置的 Key 向量进行相关性评分。

评分计算

  评分函数 采用向量点积形式。由于在高维空间中,点积的数值可能过大,会影响 softmax() 函数的稳定性,因此在实际计算中对结果进行了缩放。最终的评分函数为:

\[score(i, j) = \frac{q_{i} \cdot k_{j}}{\sqrt{d_{k}}} \]

  其中 \(d_{k}\) 是 key 向量的维度,用于缩放点积的幅度。这个分数越大,表示第 i 个位置越应该关注第 j 个位置的信息。

对于整个序列,可以通过矩阵运算一次性计算所有位置之间的评分。

【3】、计算注意力权重

  在得到每个位置与所有位置之间的相关性评分后,模型会使用 softmax() 函数进行归一化,确保每个位置对所有位置的关注程度之和为 1,从而形成一个有效的加权分布。

计算注意力权重

对于整个序列,模型要做的是对之前得到的注意力评分矩阵的 每一行 进行 softmax() 归一化。

【4】、加权汇总生成输出

  最后,模型会根据注意力权重对所有位置的 Value 向量进行加权求和,得到每个位置融合全局信息后的新表示。

加权汇总生成输出

对于整个序列,同样可以通过矩阵运算一次性计算所有位置的输出。

  综上所述,可得整个自注意力机制的完整的计算公式如下:

自注意力机制的完整的计算公式

  对应 原始论文( https://arxiv.org/pdf/1706.03762 )中的公式如下:

\[Attention(Q, K, V) = softmax(\frac{Q K^{T}}{\sqrt(d_{k})}) \]

  自注意力机制通过 Query、Key 和 Value 向量计算每个位置与其他位置之间的依赖关系,使模型能够有效捕捉序列中的全局信息。然而,自然语言本身具有高度的语义复杂性,一个句子往往同时包含多种类型的语义关系。例如,句子 “那只动物没有过马路,因为它太累了” 中就涉及多个层面的语言关系:

  • “它” 指代 “那只动物”,属于跨句的代指关系。
  • “因为” 连接前后两个分句,体现语义上的因果逻辑。
  • “过马路” 构成动词短语,属于固定的动宾结构。

  要准确理解这类句子,模型需要同时识别并建模多种层次和类型的依赖关系。但这些信息很难通过单一视角或一套注意力机制完整捕捉。

  为此,Transformer 引入了 多头注意力机制(Multi-Head Attention),其核心思想是通过多组独立的 Query、Key、Value 投影,让不同注意力头分别专注于不同的语义关系,最后将各头的输出拼接融合。

  多头注意力的计算过程如下:

【1】、分别计算各头注意力

  每个 Self-Attention Head 独立计算一套注意力输出。

分别计算各头注意力

【2】、合并多头注意力

  多个输出矩阵按维度拼接,再乘以 \(W_{0}\) 得到最终多头注意力的输出。

合并多头注意力

4.3、前馈神经网络层

  前馈神经网络(Feed-Forward Network,简称 FFN)是 Transformer 编码器中每个子层的重要组成部分,紧接在多头注意力子层之后。它通过对每个位置的表示进行 逐位置、非线性 的特征变换,进一步提升模型对复杂语义的建模能力。

前馈神经网络层

  一个标准的 FFN 子层包含两个线性变换和一个非线性激活函数,中间通常使用 ReLU激活。其计算公式如下:

\[FFN(x) = Linear_{2}(ReLU(Linear_{1}(x))) = W_{2} \cdot ReLU(W_{1}x+b_{1})+b_{2} \]

  计算图如下:

前馈神经网络层的计算图

4.4、残差连接与层归一化

  在 Transformer 的每个编码器层中,每个子层,包括自注意力子层和前馈神经网络子层,其输出都要经过残差连接(Residual Connection)和层归一化(Layer Normalization)处理。这两者是深层神经网络中常用的结构,用于缓解模型训练中的梯度消失、收敛困难等问题,对于 Transformer 能够堆叠多层至关重要。

残差连接与层归一化

【1】、残差连接

  残差连接(Residual Connection,也称 “跳跃连接” 或 “捷径连接”)最初在计算机视觉领域被提出,用于缓解深层神经网络中的梯度消失问题。其核心思想是:将子层的输入直接与其输出相加,形成一条跨越子层的 “捷径”,其数学形式为:

\[y=x+SubLayer(x) \]

  具体计算过程如图所示:

残差连接的计算过程

残差连接确保反向传播时,梯度至少有一条稳定通路可回传,是深层网络可稳定训练的关键结构。

【2】、层归一化

  每个子层在残差连接之后都会进行 层归一化(Layer Normalization,简称 LayerNorm。它的主要作用是 规范输入序列中每个 token 的特征分布(某个 token 的表示可能在不同维度上有较大数值差异),提升模型训练的稳定性。该操作会将每个 token 的向量调整为 均值为 0、方差为 1 的规范分布。

层归一化

  我们假设某个 token 的特征向量为 \(x = \begin{bmatrix} x^{1}, x^{2}, \cdots, x^{d} \end{bmatrix}\),然后计算该向量在 所有特征维度上的平均值 \(\mu = \frac{1}{d} \sum_{i = 1}^{d}x^{i}\),接着计算 向量 各维度的标准差 \(\sigma = \sqrt{\frac{1}{d} \sum_{i = 1}^{d} (x_{i} - \mu)^{2}}\),然后将每个特征值转换为 均值为 0、方差为 1 的标准正态分布 \(\hat{x^{i}} = \frac{x^{i} - \mu}{\sigma + \epsilon}\),其中 \(\epsilon\) 是一个小的常数,用于防止出现除以 0 的情况。接着,我们还让模型可以学习在归一化后的基础上进行适当的调整,保证归一化不会限制模型的表示能力 \(LayerNorm(x^{i}) = \gamma^{i} \cdot \hat{x^{i}} + \beta^{i}\),其中 \(\gamma^{i}\) 和 \(\beta^{i}\) 为 可学习参数。

4.6、位置编码

  Transformer 模型完全摒弃了 RNN 结构,意味着它不再按顺序处理序列,而是可以并行处理所有位置的信息。尽管这带来了显著的计算效率提升,却也引发了一个问题:Transformer 无法像 RNN 那样天然地捕捉词语之间的顺序关系。换句话说,在没有额外机制的情况下,Transformer 无法区分 “猫吃鱼” 和 “鱼吃猫” 这类语序不同但词汇相同的句子。

  为了解决这一问题,Transformer 引入了一个关键机制 —— 位置编码(Positional Encoding)。该机制为 每个词引入一个表示其位置信息的向量,并将其与对应的词向量相加,作为模型输入的一部分。这样一来,模型在处理每个词时,既能获取词义信息,也能感知其在句子中的位置,从而具备对基本语序的理解能力。

位置编码

  位置编码最直接的方式是使用 绝对位置编号 来表示每个词的位置,例如第一个词用 0,第二个词用 1,依此类推。这样做虽然简单,但有一个明显的问题,越靠后的 token 位置编码就越大,若直接与词向量相加,会造成 数值倾斜,让模型更关注位置,而忽视词义。为缓解这一问题,可以考虑将位置编号归一化为 [0, 1] 区间,例如用 \(\frac{pos}{T}\) 表示位置,其中 T 是句子长度。这种方式虽然使数值范围更平稳,但也引入了一个严重的问题:相同位置的词在不同长度句子中的位置编码不再一致。

  为了解决上述问题,Transformer 使用了一种基于正弦(sin)和余弦(cos)函数的位置编码方式,具体定义如下:

\[\begin{align} & PE(pos, 2i) & = sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}}) \\ & PE(pos, 2i + 1) & = cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}}) \\ \end{align} \]

  其中,\(pos\) 是 当前词在序列中的位置,\(i\) 用于表示 位置编码向量的维度索引,\(2i\) 表示 偶数维,\(2i + 1\) 表示 奇数维,\(d_{model}\) 是 词向量的维度大小。

序列中的每个位置 \(pos\) 对应一个长度为 \(d_{model}\) 的位置编码向量。该向量的 偶数维度 通过 正弦函数 生成,奇数维度 通过 余弦函数 生成。

  Transformer 提出的这种编码方式不依赖任何可学习参数,数值稳定,并具备以下优势:

  • 所有值都在 [−1,1] 范围内,数值稳定。
  • 编码方式固定、可预计算,无需训练。
  • 相同位置的编码在不同句子中保持一致。
  • 编码之间具有数学规律,便于模型在注意力机制中感知词语之间的相对位置关系。

五、解码器

5.1、解码器结构

  Transformer 解码器的主要功能是:根据编码器的输出,逐步生成目标序列中的每一个词。其生成方式采用 自回归机制(autoregressive):每一步的输入由此前已生成的所有词组成,模型将输出一个与当前输入长度相同的序列表示。我们只取最后一个位置的输出,作为当前步的预测结果。这一过程会不断重复,直到生成特殊的结束标记 <eos>,表示序列生成完成。编码器也由多个结构相同的解码器层堆叠组成。

Transformer的解码器

  每个 Decoder Layer 都包含三个子层,分别是 Masked 自注意力子层、编码器 — 解码器注意力子层(Encoder-Decoder Attention)和 前馈神经网络子层(Feed-Forward Network)。

解码器层的结构

  各层作用如下:

  • Masked自注意力子层(Masked Self Attention):用于建模当前位置与前文词之间的依赖关系。为了在训练时模拟逐词生成的过程,引入遮盖机制(Mask),限制每个位置只能关注它前面的词。
  • 编码器-解码器注意力子层(Encoder-Decoder Attention):用于建模当前解码位置与源序列各位置之间的依赖关系。通过注意力机制,模型能够根据当前状态从编码器的输出中提取相关上下文信息(相当于 Seq2Seq 模型中的 Attention 机制)。
  • 前馈神经网络子层(Feed-Forward Network):与编码器中结构完全一致,对每个位置的表示进行非线性变换,增强模型的表达能力。

  每个子层后也都配有 残差连接 与 层归一化(Layer Normalization),结构设计与编码器保持一致,确保训练的稳定性和效率。此外,解码器在 输入端 同样需要加入位置编码(Positional Encoding),用于 提供序列中的位置信息,其计算方式与编码器中相同。在 输出端,解码器的 隐藏向量 会送入一个 线性变换层(Linear),映射为 词表大小的向量,并通过 Softmax() 生成一个概率分布,用于预测当前应输出的词。

完整的解码器结构

  torcn.nn.Transformer 模块中包含一个解码器部分,可通过属性 transformer.decoder 访问,其本质是一个 torch.nn.TransformerDecoder 实例。通过其 forward() 方法,可以基于编码器的输出(memory)和目标序列的嵌入表示,逐步生成目标序列中的各个 token。

torch.nn.TransformerDecoder.forward(
    tgt: Tensor,                                                                # 输入解码器的目标序列
    memory: Tensor,                                                             # 从编码器输出的最终隐状态
    tgt_mask: Tensor | None = None,                                             # 自注意力掩码
    memory_mask: Tensor | None = None,                                          # 交叉注意力掩码
    tgt_key_padding_mask: Tensor | None = None,                                 # 目标序列的填充掩码
    memory_key_padding_mask: Tensor | None = None,                              # 编码器记忆的填充掩码
    tgt_is_causal: bool | None = None,                                          # 是否自动生成上三角因果掩码
    memory_is_causal: bool = False                                              # 记忆因果标志
) -> Tensor                                                                     # 解码器输出张量

5.2、Masked 自注意力子层

  Masked自注意力子层的主要作用是:建模目标序列中当前位置与前文之间的依赖关系,为当前词的生成提供上下文语义支持。

  由于 Transformer 不具备像 RNN 那样的隐藏状态传递机制,无法在序列生成过程中保留上下文信息,因此在生成每一个词时,必须将此前已生成的所有词作为输入,通过自注意力机制重新建模上下文关系,以预测下一个词。

  此外,从结构上看,Transformer 编解码器都具有一个典型特性:输入多少个词,就输出多少个表示。需要注意的是,在 推理阶段,我们只使用解码器 最后一个位置的输出 作为当前步的预测结果,如下图所示:

解码器的推理阶段

  如果训练阶段也完全按照推理流程进行,就必须将每个目标序列拆分成多个训练样本,每个样本输入一段前文,只预测一个词。这种方式虽然逻辑合理,但训练效率极低,完全无法利用 Transformer 并行计算的优势。为提升效率,Transformer 采用了 并行训练策略:一次性输入完整目标序列,同时预测每个位置的词。

解码器的训练阶段

  但如果不加限制,这种方式会让模型在预测每个位置时 “看到” 后面的词,即提前访问未来信息,破坏生成任务的因果结构。

解码器提前访问未来信息

  为解决这个问题,解码器在自注意力机制中引入了 遮盖机制(Mask)。该机制会在计算注意力时,阻止模型访问当前位置之后的词,只允许它依赖自身及前文的信息。这样,即使在并行训练时,模型也只能像逐词生成一样 “看见” 它应该看到的内容,从而保持训练与推理阶段的一致性。

解码器加入遮盖机制

  Mask 机制的实现非常简单:只需将注意力得分矩阵中 当前位置对其后续位置的评分设置为 \(- \infty\)。这样,在经过 softmax() 运算后,这些位置的权重会趋近于 0。最终在加权求和时,来自未来位置的信息几乎不会参与计算,从而实现了 “当前词只能看到它前面的词” 的约束。

Mask机制的原理

5.3、编码器—解码器注意力子层

  编码器-解码器注意力子层的主要作用是:建模当前解码位置与源语言序列中各位置之间的依赖关系,帮助模型在生成目标词时有效地参考输入内容,相当于 Seq2Seq 模型中的注意力机制。编码器—解码器注意力的核心机制与前面讲过的自注意力机制完全一致,区别仅在于:

  • Query 来自解码器当前的输入表示,即当前生成状态。
  • Key 和 Value 来自编码器的输出表示,即整个源序列的上下文。

  也就是说,当前生成位置使用自己的 Query,去 “询问” 编码器输出中的哪些位置最相关。注意力机制会根据 Query 与所有 Key 的相似度,为每个源位置分配一个权重,然后用这些权重对 Value 进行加权求和,得到当前生成词所需的上下文信息。

六、训练和推理机制

  Transformer 的训练与推理都基于 自回归生成机制(Autoregressive Generation):模型逐步生成目标序列中的每一个词。然而,在实现方式上,训练与推理存在明显区别。

6.1、训练阶段

  训练时,Transformer 将目标序列整体输入解码器,并在每个位置同时进行预测。为防止模型 “看到” 后面的词,破坏因果顺序,解码器在自注意力机子层中引入了 遮盖机制(Mask),限制每个位置只能关注它前面的词。

模型的训练阶段

  这种机制让模型在结构上模拟逐词生成,但在实现上能充分利用并行计算,大幅提升训练效率。

6.2、推理推荐

  推理时,每一步都要重新输入整个已生成序列,模型需要基于全量前文重新计算注意力分布,决定下一个词的输出。整个过程必须顺序执行,无法并行。推理阶段,模型每一步都要重新输入当前已生成的全部词,通过自注意力机制建模上下文关系,预测下一个词。每步输出的是一个词的概率分布,最终生成结果也可使用不同的解码策略(如贪心搜索、束搜索等)。

模型的推理阶段

七、中英翻译案例

  我们可以从 阿里天池 中下载 中英翻译的数据集: https://tianchi.aliyun.com/dataset/174937 。

  我们可以在终端中使用 pip 安装 pandas、sklearn 机器学习库和 ntlk 自然语言处理工具包。默认是从国外的主站上下载,因此,我们可能会遇到网络不好的情况导致下载失败。我们可以在 pip 指令后通过 -i 指定国内镜像源下载。

pip install scikit-learn jieba nltk -i https://pypi.tuna.tsinghua.edu.cn/simple

  数据预处理:

import os

DATA_DRIECTORY = os.path.join(os.getcwd(), "assets", "data")
MODEL_DRIECTORY = os.path.join(os.getcwd(), "assets", "models")

UNK_TOKEN = "<unk>"
PAD_TOKEN = "<pad>"
SOS_TOKEN = "<sos>"
EOS_TOKEN = "<eos>"

SPECIAL_TOKENS = [UNK_TOKEN, PAD_TOKEN, SOS_TOKEN, EOS_TOKEN]
import os

import pandas as pd

from nltk.tokenize import TreebankWordTokenizer

from sklearn.model_selection import train_test_split

def preprocess_data(file_path, test_size):
    # 读取 TSV 格式的文件,
    # sep 指定字段的分隔符,head=None 表示没有表头, 
    # secols 为要使用的列,可以使用索引,names 表示为列取的标签名
    df = pd.read_csv(file_path, sep="\t", header=None, usecols=[0, 1], names=["en", "zh_cn"], encoding="utf-8").dropna()

    train_df, test_df = train_test_split(df, test_size=test_size)               # 划分训练集与测试集

    # 构建中文词表,这里采用按字符切分
    zh_cn_vacab_set = set([vacab for sentence in train_df["zh_cn"] for vacab in sentence if vacab.strip()])
    zh_cn_vacab_list = SPECIAL_TOKENS + list(zh_cn_vacab_set)                   # 添加特殊标记
    print("构建中文词表成功")

    # 构建英文词表,基于正则匹配规则,将输入的英文句子拆分成包含单词和独立标点符号的词元列表(Token List)
    tokenizer = TreebankWordTokenizer()

    en_vacab_set = set([vacab for sentence in train_df["en"] for vacab in tokenizer.tokenize(sentence) if vacab.strip()])
    en_vacab_list = SPECIAL_TOKENS + list(en_vacab_set)                         # 添加特殊标记
    print("构建英文词表成功")

    if not os.path.exists(DATA_DRIECTORY):
        os.makedirs(DATA_DRIECTORY)

    # 保存词表到文件中
    file_path = os.path.join(DATA_DRIECTORY, "zh_cn_vocab.txt")
    with open(file_path, "w", encoding="utf-8") as f:
        f.write("\n".join(zh_cn_vacab_list))

    print("保存中文词表到文件中成功")

    file_path = os.path.join(DATA_DRIECTORY, "en_vocab.txt")
    with open(file_path, "w", encoding="utf-8") as f:
        f.write("\n".join(en_vacab_list)) 

    print("保存英文词表到文件中成功")

    # 构建词表到索引的映射
    zh_cn_word2index = {word: index for index, word in enumerate(zh_cn_vacab_list)}
    print("构建中文词表到索引的映射成功")

    en_word2index = {word: index for index, word in enumerate(en_vacab_list)}
    print("构建英文词表到索引的映射成功")

    # 构建训练集
    train_df["zh_cn"] = train_df["zh_cn"].apply(lambda sentence: get_word2index(zh_cn_word2index, list(sentence)))
    train_df["en"] = train_df["en"].apply(lambda sentence: get_word2index(en_word2index, tokenizer.tokenize(sentence), True))

    file_path = os.path.join(DATA_DRIECTORY, "translation_train.jsonl")
    train_df.to_json(file_path, orient="records", lines=True)                   # 保存训练集
    print("保存训练集数据成功")

    # 构建测试集
    test_df["zh_cn"] = test_df["zh_cn"].apply(lambda sentence: get_word2index(zh_cn_word2index, list(sentence)))
    test_df["en"] = test_df["en"].apply(lambda sentence: get_word2index(en_word2index, tokenizer.tokenize(sentence), True))

    # 保存测试集
    file_path = os.path.join(DATA_DRIECTORY, "translation_test.jsonl")
    test_df.to_json(file_path, orient="records", lines=True)
    print("保存测试集数据成功")

def get_word2index(word2index, tokens, add_sos_eos=False):
    if add_sos_eos:
        tokens = [SOS_TOKEN] + tokens + [EOS_TOKEN]
    return [word2index.get(token, SPECIAL_TOKENS.index(UNK_TOKEN)) for token in tokens]

if __name__ == "__main__":
    test_size = 0.2
    file_path = os.path.join(DATA_DRIECTORY, "cmn.txt")

    preprocess_data(file_path, test_size)

  创建数据集:

import torch
import pandas as pd

from torch.utils.data import Dataset

class TranslationDataset(Dataset):
    def __init__(self, file_path):
        self.data = pd.read_json(file_path, orient="records", lines=True).to_dict(orient="records")

    def __len__(self):
        return len(self.data)

    def __getitem__(self, index):
        index = min(max(index, 0), len(self.data) - 1)                          # 索引边界检查

        # x.shape: [源序列长度]
        x = torch.tensor(self.data[index]["zh_cn"], dtype=torch.long)

        # y.shape: [目标序列长度]
        y = torch.tensor(self.data[index]["en"], dtype=torch.long)
        
        return x, y

  创建模型:

from torch import nn

class PositionEncoding(nn.Module):
    def __init__(self, dim_model, max_seq_len):
        super().__init__()

        # pos.shape: [max_seq_len, 1]
        pos = torch.arange(0, max_seq_len, dtype=torch.float).unsqueeze(1)      # 生成位置索引

        # _2i.shape: [dim_model / 2,]
        _2i = torch.arange(0, dim_model, 2, dtype=torch.float)                  # 生成维度索引

        div_term = torch.pow(10000, _2i / dim_model)

        sins = torch.sin(pos / div_term)
        coss = torch.cos(pos / div_term)

        pe = torch.zeros(max_seq_len, dim_model, dtype=torch.float)

        # 切片索引,将偶数维度的元素赋值为 sin 函数值,将奇数维度的元素赋值为 cos 函数值
        pe[:, 0::2] = sins
        pe[:, 1::2] = coss

        # 当我们使用模型的 to() 方法将模型移动到指定设备时,在该模型中定义的属性不会被移动到指定设备上
        # 因此,我们需要将位置编码注册为持久化缓冲区,以确保在移动模型时位置编码也会移动
        self.register_buffer('pe', pe)                                          # 注册为持久化缓冲区

    def forward(self, x):
        # x.shape: [批量大小, 序列长度, 编码器的特征维度]
        seq_len = x.shape[1]

        # part_pe.shape: [序列长度, 编码器的特征维度]
        part_pe = self.pe[ : seq_len]

        return x + part_pe

class TranslationModel(nn.Module):
    def __init__(self, vocab_sizes, dim_model, nhead, num_encoder_layers, num_decoder_layers, padding_idxs, max_seq_len):
        super().__init__()

        # 词嵌入层 [词向量数(词表大小), 词向量维度, 填充值的索引]
        self.zh_cn_embedding = nn.Embedding(vocab_sizes[0], dim_model, padding_idx=padding_idxs[0])
        self.en_cn_embedding = nn.Embedding(vocab_sizes[1], dim_model, padding_idx=padding_idxs[1])
        
        self.position_encoding = PositionEncoding(dim_model, max_seq_len)       # 位置编码

        # Transformer 模型
        self.transformer = nn.Transformer(
            dim_model,                                                          # 编码器和解码器输入的特征维度
            nhead,                                                              # 多头注意力机制中并行注意力头的数量
            num_encoder_layers,                                                 # 编码器一侧堆叠的层数
            num_decoder_layers,                                                 # 解码器一侧堆叠的层数
            batch_first = True                                                  # 是否将批量大小作为第一个维度
        )

        # 输出层 [输入维度(解码器的输出维度), 输出维度(英文词表大小)]
        self.linear = nn.Linear(dim_model, vocab_sizes[1])

    def forward(self, src, tgt, src_pad_mask, tgt_mask):
        memory = self.encoder(src, src_pad_mask)
        output = self.decoder(tgt, memory, tgt_mask, src_pad_mask)

        return output

    def encoder(self, src, src_pad_mask):
        # src.shape: [批量大小, 源序列长度]
        # src_pad_mask.shape: [批量大小, 源序列长度]
        # embed.shape: [批量大小, 源序列长度, 编码器输入的特征维度]
        embed = self.zh_cn_embedding(src)                                       # 词嵌入层

        embed = self.position_encoding(embed)                                   # 位置编码

        # memory.shape: [批量大小, 源序列长度, 编码器输出的特征维度]
        memory = self.transformer.encoder(embed, src_key_padding_mask=src_pad_mask)  # 编码器
        
        return memory

    def decoder(self, tgt, memory, tgt_mask, memory_pad_mask):
        # tgt.shape: [批量大小, 目标序列长度]
        # embed.shape: [批量大小, 目标序列长度, 解码器输入的特征维度]
        embed = self.en_cn_embedding(tgt)                                       # 词嵌入层

        embed = self.position_encoding(embed)                                   # 位置编码

        # output.shape: [批量大小, 目标序列长度, 解码器输出的特征维度]
        output = self.transformer.decoder(embed, memory, tgt_mask, memory_key_padding_mask=memory_pad_mask)

        # outputs.shape: [批量大小, 目标序列长度, 输出维度(英文词表大小)]
        outputs = self.linear(output)
        
        return outputs

  模型单轮训练:

def model_train_one_epoch(model, device, data_loader, criterion, optimizer):
    model.train()                                                               # 训练模式
    total_loss = 0.0

    for x, y in data_loader:
        # x.shape: [批量大小, 源序列长度]
        # y.shape: [批量大小, 目标序列长度]
        x, y = x.to(device), y.to(device)                                       # 指定设备

        # 解码阶段
        decoder_inputs = y[:, : -1]                                             # 获取解码器的输入列表
        decoder_targets = y[:, 1 :]                                             # 获取解码器的目标列表

        src_pad_mask = (x == SPECIAL_TOKENS.index(PAD_TOKEN))                   # 获取源序列的掩码
        tgt_mask = model.transformer.generate_square_subsequent_mask(decoder_inputs.shape[1])   # 获取目标序列的因果掩码

        # decoder_outputs.shape: [批量大小, 目标序列长度, 输出维度(英文词表大小)]
        decoder_outputs = model.forward(x, decoder_inputs, src_pad_mask, tgt_mask)  

        # 计算损失
        loss = criterion(decoder_outputs.reshape(-1, decoder_outputs.shape[-1]), decoder_targets.reshape(-1))
        optimizer.zero_grad()                                                   # 清空梯度
        loss.backward()                                                         # 反向传播
        optimizer.step()                                                        # 优化器更新参数

        # x.shape[0] 是当前批次的样本数量
        # 乘以 x.shape[0] 是为了消除批次大小不一致(尤其是最后一个不完整批次)带来的统计偏差
        total_loss += loss.item() * x.shape[0]                                  # 计算当前批次的损失

    # len(dataloader.dataset) 返回数据集的样本数量
    # len(dataloader) 返回数据加载器的批次数量
    return total_loss / len(data_loader.dataset)                                # 返回平均损失

  训练模型:

DIM_MODEL = 128
NHEAD = 4
NUM_ENCODER_LAYERS = 2
NUM_DECODER_LAYERS = 2

BATCH_SIZE = 128

LEARNING_RATE = 1e-3
EPOCHS = 10
MAX_SEQ_LEN = 128
from torch.nn.utils.rnn import pad_sequence

def collate_fn(batch):
    # batch 是一个二元组列表,[(x_tensor, y_tensor)]
    x_tensors = [item[0] for item in batch]
    y_tensors = [item[1] for item in batch]

    # pad_sequence() 方法将多个长度不同的张量(序列)填充到相同长度,并堆叠成一个批处理张量
    # padding_value 填充的值,<pad> 特殊标识的索引
    x_tensors = pad_sequence(x_tensors, batch_first=True, padding_value=SPECIAL_TOKENS.index(PAD_TOKEN))
    y_tensors = pad_sequence(y_tensors, batch_first=True, padding_value=SPECIAL_TOKENS.index(PAD_TOKEN))

    return x_tensors, y_tensors
import os

import torch

from torch.utils.data import DataLoader

def model_train(model, device, data_loader, learning_rate, epochs, pad_token_index):
    model.to(device)                                                            # 指定设备

    # ignore_index 让损失函数忽略指定的目标类别索引,不参与损失计算
    criterion = torch.nn.CrossEntropyLoss(ignore_index=pad_token_index)         # 多元交叉熵损失函数
    optimizer = torch.optim.Adam(model.parameters(), learning_rate)             # 创建优化器

    # 开始训练
    for epoch in range(epochs):
        print(f"开始第 {epoch + 1} 次训练")
        loss = model_train_one_epoch(model, device, data_loader, criterion, optimizer)
        print(f"epoch: {epoch + 1:0>2},loss: {loss:.4f}")
        
    print("训练完成,开始保存模型")
    
    if not os.path.exists(MODEL_DRIECTORY):
        os.makedirs(MODEL_DRIECTORY)

    file_path = os.path.join(MODEL_DRIECTORY, "translation_model_by_transformer.pth")
    torch.save(model.state_dict(), file_path)                                   # 保存模型

    print("模型保存完成")

if __name__ == "__main__":
    pad_token_index = SPECIAL_TOKENS.index(PAD_TOKEN)
    padding_idxs = [pad_token_index] * 2
    
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    # 加载词表
    file_path = os.path.join(DATA_DRIECTORY, "zh_cn_vocab.txt")
    with open(file_path, "r", encoding="utf-8") as f:
        zh_cn_vocab_list = [line.strip() for line in f.readlines()]

    print("加载中文词表完成")

    file_path = os.path.join(DATA_DRIECTORY, "en_vocab.txt")
    with open(file_path, "r", encoding="utf-8") as f:
        en_vocab_list = [line.strip() for line in f.readlines()]

    vocab_sizes = [len(zh_cn_vocab_list), len(en_vocab_list)]

    print("加载英文词表完成")

    file_path = os.path.join(DATA_DRIECTORY, "translation_train.jsonl")
    train_dataset = TranslationDataset(file_path)                               # 创建训练数据集
    print("创建训练集成功")

    # collate_fn 用来指定自定义的填充方式,如果一批数据中的序列长度不一样,会报错
    train_data_loader = DataLoader(train_dataset, BATCH_SIZE, True, collate_fn=collate_fn)  # 创建训练数据集加载器
    print("创建训练数据集加载器成功")

    model = TranslationModel(vocab_sizes, DIM_MODEL, NHEAD, NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS, padding_idxs, MAX_SEQ_LEN)
    print("创建模型成功")

    model_train(model, device, train_data_loader, LEARNING_RATE, EPOCHS, pad_token_index)

  在机器翻译任务中,BLEU(Bilingual Evaluation Understudy) 是一种常用的自动评估指标,用于衡量模型生成的翻译与人工参考译文之间的相似程度。其核心思想是:

  • n-gram 匹配:统计预测译文中有多少 n-gram(词或短语)同时出现在参考译文中,用于衡量翻译内容的准确性。
  • 精确率计算:将匹配到的 n-gram 数量除以预测译文中 n-gram 的总数,反映生成译文中 “正确部分” 的比例。

  此外,BLEU 还引入长度惩罚机制,防止模型通过生成过短句子获得不合理的高分。最终得到的 BLEU 分数越高,说明生成译文与参考译文越接近。

  在本案例中,我们可以使用 Python 的 NLTK 库 中的 bleu_score 模块,对模型在测试集上的翻译结果进行评估,主要参考 BLEU-4 的得分情况,作为翻译质量的衡量依据。

  模型单批次预测:

import torch

def model_predict_one_batch(model, x, en_word2index, max_seq_len):
    device = x.device
    
    model.eval()                                                                # 评估模式

    generated_token_indexes = []                                                # 存储每个时间步的预测概率最大的单词索引

    with torch.no_grad():                                                       # 禁用梯度计算
        # 编码阶段
        src_pad_mask = (x == SPECIAL_TOKENS.index(PAD_TOKEN))

        # memory.shape: [批量大小, 目标序列长度, 编码器输出的特征维度]
        memory = model.encoder(x, src_pad_mask)

        # 解码阶段
        batch_size = x.size(0)                                                  # 批量大小

        # decoder_input.shape: [批量大小, 目标序列长度(初始为 1)]
        # torch.full(张量, 值) 创建一个张量,所有元素都填充为指定值
        decoder_input = torch.full((batch_size, 1), en_word2index[SOS_TOKEN], dtype=torch.long, device=device)

        is_finished = torch.full([batch_size], False, device=device)            # 记录每个样本是否生成完成

        # 自回归生成
        for i in range(max_seq_len):
            tgt_mask = model.transformer.generate_square_subsequent_mask(decoder_input.shape[1]).to(device)

            # decoder_output.shape: [批量大小, 目标序列长度, 词汇表大小]
            decoder_output = model.decoder(decoder_input, memory, tgt_mask, src_pad_mask)   # 解码器前向传播

            # next_token_indexes.shape: [批量大小, 序列长度(1)]
            # next_token_indexes = decoder_output.argmax(dim=-1)                  # 获取每个时间步的预测概率最大的单词索引
            next_token_indexes = torch.argmax(decoder_output[:, -1, :], dim=-1, keepdim=True)   # 获取最后时间步的预测概率最大的单词索引

            generated_token_indexes.append(next_token_indexes)                  # 存储每个时间步的预测概率最大的单词索引
            decoder_input = torch.cat([decoder_input, next_token_indexes], dim=1)   # 更新解码器的输入

            # 判断是否生成了 <eos> 标记
            is_finished |= (next_token_indexes.squeeze(1) == en_word2index[EOS_TOKEN])

            # 如果所有样本都生成完成,跳出循环
            if is_finished.all():
                break

        # 处理预测结果
        # generated_tensor.shape: [批量大小, 各个序列长度维度长度之和]
        generated_tensor = torch.cat(generated_token_indexes, dim=1)            # 合并所有时间步的预测概率最大的单词索引
        generated_list = generated_tensor.tolist()

        # 去掉 <eos> 标记之后的 token id
        for index, sentence in enumerate(generated_list):
            if en_word2index[EOS_TOKEN] in sentence:
                generated_list[index] = sentence[:sentence.index(en_word2index[EOS_TOKEN])]

    return generated_list

  模型评估:

import os

import torch

from torch.utils.data import DataLoader

from nltk.translate.bleu_score import corpus_bleu

def model_evaluate(model, device, data_loader, en_word2index, max_seq_len):
    model.to(device)                                                            # 指定设备
    model.eval()                                                                # 评估模式

    # predictions: [[*, *, *], [*, *, *]]
    predictions = []
    # references: [[[*, *, *]], [[*, *, *]]]
    references = []

    for x, y in data_loader:
        # x.shape: [批量大小, 源序列长度]
        # y.shape: [批量大小, 目标序列长度]
        x, y = x.to(device), y.to(device)                                       # 指定设备

        # targets: [[<sos>, *, *, <eos>, <pad>], [<sos>, *, *, <eos>, <pad>], ...]
        targets = y.tolist()

        results = model_predict_one_batch(model, x, en_word2index, max_seq_len)

        predictions.extend(results)
        references.extend([[target[1 : target.index(en_word2index[EOS_TOKEN])]] for target in targets])

    accuracy = corpus_bleu(references, predictions)

    print(f"准确率:{accuracy:.4f}")

if __name__ == "__main__":
    pad_token_index = SPECIAL_TOKENS.index(PAD_TOKEN)
    padding_idxs = [pad_token_index] * 2
    
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    # 加载词表
    file_path = os.path.join(DATA_DRIECTORY, "zh_cn_vocab.txt")
    with open(file_path, "r", encoding="utf-8") as f:
        zh_cn_vocab_list = [line.strip() for line in f.readlines()]

    print("加载中文词表完成")

    file_path = os.path.join(DATA_DRIECTORY, "en_vocab.txt")
    with open(file_path, "r", encoding="utf-8") as f:
        en_vocab_list = [line.strip() for line in f.readlines()]

    vocab_sizes = [len(zh_cn_vocab_list), len(en_vocab_list)]

    print("加载英文词表完成")

    # 构建词表索引
    en_word2index = {word: index for index, word in enumerate(en_vocab_list)}
    print("构建英文词表索引完成")

    file_path = os.path.join(DATA_DRIECTORY, "translation_test.jsonl")
    test_dataset = TranslationDataset(file_path)                                # 创建测试数据集
    print("创建测试集成功")

    test_data_loader = DataLoader(test_dataset, BATCH_SIZE, collate_fn=collate_fn)  # 创建测试数据集加载器
    print("创建测试数据集加载器成功")

    model = TranslationModel(vocab_sizes, DIM_MODEL, NHEAD, NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS, padding_idxs, MAX_SEQ_LEN).to(device)
    print("创建模型成功")

    file_path = os.path.join(MODEL_DRIECTORY, "translation_model_by_transformer.pth")
    model.load_state_dict(torch.load(file_path))                                # 加载模型参数
    print("加载模型参数完成")

    model_evaluate(model, device, test_data_loader, en_word2index, MAX_SEQ_LEN)

  模型预测:

import os

import torch

from nltk.tokenize import TreebankWordDetokenizer

def model_predict(model, device, text, zh_cn_word2index, en_word2index, en_index2word, detokenizer, max_seq_len):
    model.to(device)                                                            # 指定设备
    tokens = list(text)                                                         # 切词
    zh_cn_indexes = [zh_cn_word2index.get(token, SPECIAL_TOKENS.index(UNK_TOKEN)) for token in tokens]  # 获取单词的索引
    x = torch.tensor([zh_cn_indexes], dtype=torch.long, device=device)          # 输入数据

    results = model_predict_one_batch(model, x, en_word2index, max_seq_len)

    tokens = [en_index2word[index] for index in results[0]]
    return detokenizer.detokenize(tokens)

def model_run_predict(model, device, word2index, en_word2index, en_index2word, max_seq_len):
    print("欢迎使用中英翻译模型,输入q或quit退出")

    detokenizer = TreebankWordDetokenizer()

    while True:
        text = input("请输入中文: \n")
        if text.strip() in ["q", "quit"]:
            break

        if text.strip() == "":
            continue

        print(f"输入的中文:")
        print(text)

        result = model_predict(model, device, text, word2index, en_word2index, en_index2word, detokenizer, max_seq_len)
        print(f"翻译结果:")
        print(result)

if __name__ == "__main__":
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    file_path = os.path.join(DATA_DRIECTORY, "zh_cn_vocab.txt")
    with open(file_path, "r", encoding="utf-8") as f:
        zh_cn_vocab_list = [line.strip() for line in f.readlines()]

    print("加载中文词表完成")

    file_path = os.path.join(DATA_DRIECTORY, "en_vocab.txt")
    with open(file_path, "r", encoding="utf-8") as f:
        en_vocab_list = [line.strip() for line in f.readlines()]

    vocab_sizes = [len(zh_cn_vocab_list), len(en_vocab_list)]

    print("加载英文词表完成")

    vocab_sizes = [len(zh_cn_vocab_list), len(en_vocab_list)]

    # 构建词表到索引的映射
    zh_cn_word2index = {word: index for index, word in enumerate(zh_cn_vocab_list)}
    print("构建中文词表到索引的映射")
    
    en_word2index = {word: index for index, word in enumerate(en_vocab_list)}
    print("构建英文词表到索引的映射")

    # 构建索引到词表的映射
    en_index2word = {index: word for index, word in enumerate(en_vocab_list)}
    print("构建英文词引到词表的映射")
    
    model = TranslationModel(vocab_sizes, DIM_MODEL, NHEAD, NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS, padding_idxs, MAX_SEQ_LEN).to(device)
    print("创建模型完成")

    file_path = os.path.join(MODEL_DRIECTORY, "translation_model_by_transformer.pth")
    model.load_state_dict(torch.load(file_path))                                # 加载模型参数
    print("加载模型参数完成")

    model_run_predict(model, device, zh_cn_word2index, en_word2index, en_index2word, MAX_SEQ_LEN)
posted @ 2026-10-12 22:57  星光映梦  阅读(2)  评论(0)    收藏  举报