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 下载源列表:
- 阿里云 https://mirrors.aliyun.com/pypi/simple
- 清华大学 https://pypi.tuna.tsinghua.edu.cn/simple
- 中国科学技术大学 https://pypi.mirrors.ustc.edu.cn/simple
安装完 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 的整体结构延续了 Seq2Seq 模型中 “编码器-解码器” 的设计理念,其中,编码器(Encoder)负责 对输入序列进行理解和表示,而 解码器(Decoder)则 根据编码器的输出逐步生成目标序列。
与基于 RNN 的 Seq2Seq 模型一样,Transformer 的解码器采用 自回归方式生成目标序列。不同之处在于,每一步的输入是此前已生成的全部词,模型会输出一个与输入长度相同的序列,但我们 只取最后一个位置的结果作为当前预测。这个过程不断重复,直到生成结束标记 <eos>。
此外,Transformer 的编码器和解码器模块分别由多个结构相同的层堆叠而成。通过层层堆叠,模型能够逐步提取更深层次的语义特征,从而增强对复杂语言现象的建模能力。标准的 Transformer 模型通常包含 6个编码器层和 6 个解码器层。

在 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)堆叠而成。

每个 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 向量加权汇总,形成新的表示。
三个向量的计算公式如下:

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

评分函数 采用向量点积形式。由于在高维空间中,点积的数值可能过大,会影响 softmax() 函数的稳定性,因此在实际计算中对结果进行了缩放。最终的评分函数为:
其中 \(d_{k}\) 是 key 向量的维度,用于缩放点积的幅度。这个分数越大,表示第 i 个位置越应该关注第 j 个位置的信息。
对于整个序列,可以通过矩阵运算一次性计算所有位置之间的评分。
【3】、计算注意力权重
在得到每个位置与所有位置之间的相关性评分后,模型会使用 softmax() 函数进行归一化,确保每个位置对所有位置的关注程度之和为 1,从而形成一个有效的加权分布。

对于整个序列,模型要做的是对之前得到的注意力评分矩阵的 每一行 进行
softmax()归一化。
【4】、加权汇总生成输出
最后,模型会根据注意力权重对所有位置的 Value 向量进行加权求和,得到每个位置融合全局信息后的新表示。

对于整个序列,同样可以通过矩阵运算一次性计算所有位置的输出。
综上所述,可得整个自注意力机制的完整的计算公式如下:

对应 原始论文( https://arxiv.org/pdf/1706.03762 )中的公式如下:
自注意力机制通过 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激活。其计算公式如下:
计算图如下:

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

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

残差连接确保反向传播时,梯度至少有一条稳定通路可回传,是深层网络可稳定训练的关键结构。
【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)函数的位置编码方式,具体定义如下:
其中,\(pos\) 是 当前词在序列中的位置,\(i\) 用于表示 位置编码向量的维度索引,\(2i\) 表示 偶数维,\(2i + 1\) 表示 奇数维,\(d_{model}\) 是 词向量的维度大小。
序列中的每个位置 \(pos\) 对应一个长度为 \(d_{model}\) 的位置编码向量。该向量的 偶数维度 通过 正弦函数 生成,奇数维度 通过 余弦函数 生成。
Transformer 提出的这种编码方式不依赖任何可学习参数,数值稳定,并具备以下优势:
- 所有值都在
[−1,1]范围内,数值稳定。 - 编码方式固定、可预计算,无需训练。
- 相同位置的编码在不同句子中保持一致。
- 编码之间具有数学规律,便于模型在注意力机制中感知词语之间的相对位置关系。
五、解码器
5.1、解码器结构
Transformer 解码器的主要功能是:根据编码器的输出,逐步生成目标序列中的每一个词。其生成方式采用 自回归机制(autoregressive):每一步的输入由此前已生成的所有词组成,模型将输出一个与当前输入长度相同的序列表示。我们只取最后一个位置的输出,作为当前步的预测结果。这一过程会不断重复,直到生成特殊的结束标记 <eos>,表示序列生成完成。编码器也由多个结构相同的解码器层堆叠组成。

每个 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。最终在加权求和时,来自未来位置的信息几乎不会参与计算,从而实现了 “当前词只能看到它前面的词” 的约束。

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)

浙公网安备 33010602011771号