【自然语言处理】Transformer模型
目录
一、引言
二、模块导入
三、位置编码(Positional Encoding)
(一)算法设计意图
(二)核心思想
(三)数学公式
公式参数说明:
(五)代码实现解析
1. 初始化方法(__init__):预计算位置编码矩阵
2. 前向传播(forward):注入位置信息
(六)完整位置编码(Positional Encoding)类实现
(七)设计优势
四、多头自注意力(Multi-Head Self-Attention)
(一)算法设计意图
(二)模块结构与参数
(三)核心注意力计算(attention方法)
步骤解析:
(四)前向传播流程(forward方法)
步骤解析:
(五)完整多头自注意力(Multi-Head Self-Attention)类实现
(六)算法优势与意义
五、 前馈层(Feed-Forward Layer)
(一)算法设计意图
(二)模块结构与参数
(三)前向传播流程
步骤解析:
(四)完整前馈层(Feed-Forward Layer)类实现
(五)算法优势与意义
六、层归一化(Layer Normalization)
(一)算法设计意图
(二)模块结构与参数
(三)前向传播流程
步骤解析:
(四)完整层归一化(Layer Normalization)类实现
(五)算法优势与意义
七、编码器层(Encoder Layer)与编码器(Encoder)
(一)算法设计意图
(二)get_clones:层堆叠工具函数
(三)EncoderLayer:编码器的基本单元
1. 模块结构与参数
2. 前向传播流程(forward)
步骤解析:
(四)Encoder:完整编码器
1. 模块结构与参数
2. 前向传播流程(forward)
步骤解析:
(五)完整编码器层(Encoder Layer)与编码器(Encoder)类实现
(六)编码器的整体功能与意义
八、解码器层(Decoder Layer)与解码器(Decoder)
(一)算法设计意图
(二)DecoderLayer:解码器的基本单元
1. 模块结构与参数
2. 前向传播流程(forward)
步骤解析:
(三)Decoder:完整解码器
1. 模块结构与参数
2. 前向传播流程(forward)
步骤解析:
(四)完整解码器层(Decoder Layer)与解码器(Decoder)类实现
(五)解码器的核心功能与意义
九、整体 Transformer 模型
(一)算法设计意图
(二)Transformer类的整体结构与参数
参数说明:
核心组件作用:
(三)前向传播流程(forward方法)
步骤解析:
(四)Transformer 的核心功能与优势
(五)与训练流程的衔接
(六)完整整体 Transformer 模型类实现
十、模型训练与测试
(一)算法设计意图
(二)模型参数定义:为 Transformer 配置核心超参与初始化
1. 核心超参数说明
2. 模型实例化与参数初始化
3. 优化器配置
(三)模型训练函数(train_model):端到端优化 Transformer
1. 训练流程拆解
2. 关键训练逻辑解析
(四)模型测试函数(translate):自回归生成目标序列
1. 推理流程拆解
2. 关键推理逻辑解析
(五)完整模型训练与测试实现
(六)整体逻辑总结:从训练到推理的闭环
十一、完整Python代码实现
十二、总结
一、引言
Transformer 结构是由 Google 在 2017 年提出并首先应用于机器翻译的神经网络模型架构。机器翻译的目标是从源语言(Source Language)转换到目标语言(Target Language)。Transformer 结构完全通过注意力机制完成对源语言序列和目标语言序列全局依赖的建模。如今,几乎全部大语言模型都是基于 Transformer 结构的。
基于 Transformer 的编码器和解码器结构如图所示

左侧和右侧分别对应编码器(Encoder)和解码器(Decoder)结构,它们均由若干个基本的 Transformer 块(Block) 组成(对应图中的灰色框)。这里 N× 表示进行了 N 次堆叠。每个 Transformer 块都接收一个向量序列 作为输入,并输出一个等长的向量序列作为输出
。这里的
和
分别对应文本序列中一个词元(Token)的表示。
是当前 Transformer 块对输入
进一步整合其上下文语义后对应的输出。在从输入
到输出
的语义抽象过程中,主要涉及如下几个模块:
- 注意力层:使用多头注意力(Multi-Head Attention)机制整合上下文语义。多头注意力并行运行多种独立注意力机制,进而从多维度捕捉输入序列信息。它使序列中任意两个词之间的依赖关系可以直接被建模(而非基于传统的循环结构),从而更好地解决文本的长程依赖问题。
- 位置感知前馈网络层(Position-wise Feed-Forward Network):通过全连接层对输入文本序列中的每个单词表示进行更复杂的变换。
- 残差连接:对应图中的
Add部分。它是一条分别作用在上述两个子层中的直连通路,用于连接两个子层的输入与输出,使信息流动更高效,有利于模型的优化。 - 层归一化:对应图中的
Norm部分。它作用于上述两个子层的输出表示序列,对表示序列进行层归一化操作,同样起到稳定优化的作用。
图中包含 “输入→词元嵌入表示→位置编码→多头注意力→位置感知前馈网络→Add & Norm” 的编码器流程,以及解码器侧的 “掩码多头注意力→多头交叉注意力→位置感知前馈网络→Add & Norm” 流程,最终输出概率经Linear层和Softmax输出。
接下来依次介绍各个模块的具体功能和实现方法。
二、模块导入
在写Transformer模型的代码前,应先导入模块
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
import time
import numpy as np
import string
from torchtext.legacy import data
三、位置编码(Positional Encoding)
(一)算法设计意图
位置编码(Positional Encoding)是 Transformer 模型中用于为序列元素注入位置信息的关键模块。由于 Transformer 的自注意力机制是 “并行化” 处理序列的(不依赖递归或卷积),本身不包含对序列顺序的感知,因此需要通过位置编码告诉模型 “每个元素在序列中的位置”,从而理解语言的时序关系。
(二)核心思想
Transformer 采用正弦余弦函数生成位置编码,其核心设计基于两点:
- 能为不同位置生成唯一的编码,确保模型能区分不同位置的元素;
- 能通过函数的周期性表达相对位置关系(即位置
pos和pos+k的编码差异是固定的),让模型学习到 “距离” 的概念。
(三)数学公式
对于序列中位置为pos(从 0 开始)、维度为i(从 0 开始)的元素,位置编码的计算公式如下:
-
当
i为偶数时(0, 2, 4...):
-
当
i为奇数时(1, 3, 5...):
公式参数说明:
pos:序列中元素的位置索引(例如句子中第 1 个词pos=0,第 2 个词pos=1,以此类推);i:位置编码的维度索引(例如d_model=512时,i的范围是 0~511);d_model:模型的特征维度(即每个元素的嵌入向量维度,例如 512);10000:一个固定的缩放因子,用于控制正弦 / 余弦函数的周期(周期随维度i增大而变长)。
(五)代码实现解析
以下结合提供的PositionalEncoder类代码,逐行解释算法的实现逻辑:
1. 初始化方法(__init__):预计算位置编码矩阵
def __init__(self, d_model, max_seq_len=80):super().__init__()self.d_model = d_model # 模型维度(如512)# 初始化位置编码矩阵:shape为 [max_seq_len, d_model]pe = torch.zeros(max_seq_len, d_model)# 遍历每个位置pos(0到max_seq_len-1)for pos in range(max_seq_len):# 遍历每个偶数维度i(步长为2,处理i和i+1)for i in range(0, d_model, 2):# 偶数维度i:用正弦函数计算pe[pos, i] = math.sin(pos / (10000 ** (i/d_model)))# 奇数维度i+1:用余弦函数计算(确保i+1不超过d_model)if i+1 < d_model:pe[pos, i+1] = math.cos(pos / (10000 ** (i/d_model)))# 增加批次维度:shape变为 [1, max_seq_len, d_model](适配批量输入)pe = pe.unsqueeze(0)# 注册为非参数缓冲区(不参与梯度更新,但会被保存到模型中)self.register_buffer('pe', pe)
核心逻辑:
- 预计算一个最大长度为
max_seq_len的位置编码矩阵pe,避免每次推理时重复计算,提升效率; - 对每个位置
pos和维度i,按正弦 / 余弦公式填充pe矩阵,确保不同位置、不同维度的编码唯一; - 通过
unsqueeze(0)增加批次维度,使得pe可以与批量输入(shape 为[batch_size, seq_len, d_model])直接相加。
2. 前向传播(forward):注入位置信息
def forward(self, x):# 对输入嵌入向量进行缩放(避免嵌入向量与位置编码量级差异过大)x = x * math.sqrt(self.d_model)# 获取输入序列的实际长度(输入x的shape为 [batch_size, seq_len, d_model])seq_len = x.size(1)# 将位置编码与输入向量相加(仅取前seq_len个位置的编码)# .requires_grad_(False):确保位置编码不参与梯度计算# .cuda():将位置编码移到GPU(若使用GPU训练)x = x + self.pe[:, :seq_len].requires_grad_(False).cuda()return x
核心逻辑:
- 输入
x是序列元素的嵌入向量(如词嵌入),先通过x * sqrt(d_model)缩放,确保嵌入向量与位置编码的量级相当(避免位置编码被嵌入向量 “淹没”); - 从预计算的
pe中截取与输入序列长度seq_len匹配的部分(self.pe[:, :seq_len]),与输入x逐元素相加,最终输出 “包含位置信息的嵌入向量”。
(六)完整位置编码(Positional Encoding)类实现
class PositionalEncoder(nn.Module):def __init__(self, d_model, max_seq_len=80):super().__init__()self.d_model = d_modelpe = torch.zeros(max_seq_len, d_model)for pos in range(max_seq_len):for i in range(0, d_model, 2):pe[pos, i] = math.sin(pos / (10000 ** (i/d_model)))pe[pos, i+1] = math.cos(pos / (10000 ** (i/d_model)))pe = pe.unsqueeze(0)self.register_buffer('pe', pe)def forward(self, x):x = x * math.sqrt(self.d_model)seq_len = x.size(1)x = x + self.pe[:, :seq_len].requires_grad_(False).cuda()return x
(七)设计优势
-
相对位置可感知:对于任意固定偏移
k,位置pos和pos+k的编码满足三角函数的平移性质(如sin(a+k) = sin(a)cos(k) + cos(a)sin(k)),模型可通过学习系数捕捉相对位置关系。 -
可扩展性:预计算的
max_seq_len可覆盖训练数据中的最长序列,而公式本身支持任意长度的pos(即使推理时序列长度超过max_seq_len,仍可直接计算编码)。 -
轻量化:位置编码是固定的(非学习参数),无需额外训练,节省计算资源。
综上,位置编码通过正弦和余弦函数为序列元素注入位置信息,解决了 Transformer 对 “顺序” 不敏感的问题。其核心是利用三角函数的周期性和唯一性,既保证不同位置的编码可区分,又能表达相对位置关系,是 Transformer 模型能处理时序数据的关键设计之一。
四、多头自注意力(Multi-Head Self-Attention)
(一)算法设计意图
在 Transformer 中,自注意力机制用于捕捉序列中元素的依赖关系,但单一注意力头的视角有限(如仅关注语法结构或局部语义)。多头自注意力通过将注意力 “拆分” 为多个并行的子注意力头,让每个头在不同的特征子空间中学习依赖关系,最终整合所有头的信息,从而从多维度、多视角捕捉序列的复杂依赖(如长程语义关联、局部结构等),大幅提升模型对序列信息的建模能力。
(二)模块结构与参数
class MultiHeadAttention(nn.Module):def __init__(self, heads, d_model, dropout=0.1):super().__init__()self.d_model = d_model # 模型整体维度(如512)self.d_k = d_model // heads # 每个注意力头的维度self.h = heads # 注意力头的数量# 对Q、K、V分别做线性变换的层self.q_linear = nn.Linear(d_model, d_model)self.v_linear = nn.Linear(d_model, d_model)self.k_linear = nn.Linear(d_model, d_model)self.dropout = nn.Dropout(dropout) # 防止过拟合的Dropoutself.out = nn.Linear(d_model, d_model) # 整合多头输出的线性层
heads:注意力头的数量(如 8 头),需满足d_model % heads == 0;d_model:模型的整体特征维度(如 Transformer 的默认 512);d_k:每个注意力头的特征维度(由d_model // heads计算得到);- 线性层
q_linear、k_linear、v_linear:将输入的查询(Q)、键(K)、值(V)映射到多头所需的子空间; out:将多头的输出拼接后,映射回d_model维度,保证特征维度的一致性。
(三)核心注意力计算(attention方法)
def attention(self, q, k, v, d_k, mask=None, dropout=None):# 1. 计算注意力得分:Q·K^T / √d_kscores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k)# 2. 应用掩码(如Decoder的自注意力掩码,防止关注未来token)if mask is not None:mask = mask.unsqueeze(1) # 扩展维度以适配scoresscores = scores.masked_fill(mask == 0, -1e9) # 掩码位置设为极小值# 3. Softmax归一化得分,得到注意力权重scores = F.softmax(scores, dim=-1)# 4. 应用Dropoutif dropout is not None:scores = dropout(scores)# 5. 注意力权重与V相乘,得到最终输出output = torch.matmul(scores, v)return output
步骤解析:
- 得分计算:
Q·K^T衡量 Q 和 K 的相似度,除以√d_k是为了缩放方差(避免Q·K^T值过大导致 Softmax 后梯度消失)。 - 掩码处理:在 Decoder 的自注意力中,需通过掩码(
mask)避免模型 “看到” 未来的 token(如翻译时不能提前参考后面的词),将掩码位置的得分设为-1e9,确保 Softmax 后这些位置的权重趋近于 0。 - Softmax 与 Dropout:Softmax 将得分归一化为注意力权重;Dropout 随机置零部分权重,防止过拟合。
- 加权求和:注意力权重与
V相乘,得到当前头的注意力输出(即 “加权后的信息聚合”)。
(四)前向传播流程(forward方法)
def forward(self, q, k, v, mask=None):bs = q.size(0) # 批次大小# 1. 线性变换 + 多头维度拆分k = self.k_linear(k).view(bs, -1, self.h, self.d_k)q = self.q_linear(q).view(bs, -1, self.h, self.d_k)v = self.v_linear(v).view(bs, -1, self.h, self.d_k)# 2. 转置维度:将“头维度”提前(便于多头并行计算)k = k.transpose(1, 2)q = q.transpose(1, 2)v = v.transpose(1, 2)# 3. 调用attention方法计算每个头的输出scores = self.attention(q, k, v, self.d_k, mask, self.dropout)# 4. 拼接多头输出 + 最终线性变换concat = scores.transpose(1, 2).contiguous().view(bs, -1, self.d_model)output = self.out(concat)return output
步骤解析:
- 线性变换与维度拆分:通过
q_linear、k_linear、v_linear对输入进行线性变换后,将特征拆分为h个头部(维度变为[bs, h, seq_len, d_k])。 - 维度转置:将 “头维度” 从第 3 维提前到第 2 维(
transpose(1, 2)),使每个头的计算可以并行进行。 - 多头注意力计算:调用
attention方法,得到每个头的输出(维度为[bs, h, seq_len, d_k])。 - 拼接与线性变换:将所有头的输出拼接(
transpose+view),再通过out线性层映射回d_model维度,保证输出与输入维度一致。
(五)完整多头自注意力(Multi-Head Self-Attention)类实现
class MultiHeadAttention(nn.Module):def __init__(self, heads, d_model, dropout=0.1):super().__init__()self.d_model = d_modelself.d_k = d_model // headsself.h = headsself.q_linear = nn.Linear(d_model, d_model)self.v_linear = nn.Linear(d_model, d_model)self.k_linear = nn.Linear(d_model, d_model)self.dropout = nn.Dropout(dropout)self.out = nn.Linear(d_model, d_model)def attention(self, q, k, v, d_k, mask=None, dropout=None):scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k)if mask is not None:mask = mask.unsqueeze(1)scores = scores.masked_fill(mask == 0, -1e9)scores = F.softmax(scores, dim=-1)if dropout is not None:scores = dropout(scores)output = torch.matmul(scores, v)return outputdef forward(self, q, k, v, mask=None):bs = q.size(0)k = self.k_linear(k).view(bs, -1, self.h, self.d_k)q = self.q_linear(q).view(bs, -1, self.h, self.d_k)v = self.v_linear(v).view(bs, -1, self.h, self.d_k)k = k.transpose(1, 2)q = q.transpose(1, 2)v = v.transpose(1, 2)scores = self.attention(q, k, v, self.d_k, mask, self.dropout)concat = scores.transpose(1, 2).contiguous().view(bs, -1, self.d_model)output = self.out(concat)return output
(六)算法优势与意义
- 多视角建模:每个头在不同子空间学习注意力,可同时捕捉序列的语法、语义、局部 / 长程等多种依赖关系;
- 并行高效:多头计算可完全并行,结合 Transformer 的 “无循环” 设计,大幅提升长文本处理的效率;
- 灵活性:通过调整
heads数量,可在 “建模能力” 和 “计算效率” 之间做权衡(头数越多,建模能力越强,但计算开销也越大)。
综上,MultiHeadAttention是 Transformer 的核心模块,通过 “多头并行 + 子空间注意力” 的设计,让模型能高效且全面地捕捉序列元素的复杂依赖关系,是大语言模型实现长文本理解的关键技术之一。
五、 前馈层(Feed-Forward Layer)
(一)算法设计意图
在 Transformer 的每个模块中,前馈网络层负责对输入的序列表示进行复杂的非线性变换。它通过 “升维→非线性激活→降维” 的过程,让模型能在高维空间中学习更丰富的特征模式,从而捕捉文本中的复杂语义关系(如长程依赖、隐式逻辑等)。
(二)模块结构与参数
class FeedForward(nn.Module):def __init__(self, d_model, d_ff=2048, dropout=0.1):super().__init__()self.linear_1 = nn.Linear(d_model, d_ff) # 从d_model到d_ff的线性层self.dropout = nn.Dropout(dropout) # 防止过拟合的Dropout层self.linear_2 = nn.Linear(d_ff, d_model) # 从d_ff回到d_model的线性层
d_model:模型的基础特征维度(如 Transformer 默认的 512),需与多头注意力层的输出维度一致;d_ff:前馈层中间层的维度(默认 2048),通常远大于d_model,使模型能在高维空间中充分变换特征;dropout:丢弃率,用于正则化,防止模型过拟合。
(三)前向传播流程
def forward(self, x):# 1. 线性变换 + ReLU激活 + Dropoutx = self.dropout(F.relu(self.linear_1(x)))# 2. 线性变换,将维度从d_ff映射回d_modelx = self.linear_2(x)return x
步骤解析:
-
升维与非线性激活:输入
x(shape 为[batch_size, seq_len, d_model])先通过linear_1线性层,将维度从d_model提升到d_ff;再通过ReLU激活函数引入非线性,让模型能学习复杂的特征模式;最后通过dropout随机丢弃部分神经元,防止过拟合。 -
降维与输出:经过
linear_2线性层,将维度从d_ff降回d_model,保证前馈层的输出维度与输入一致,从而能与残差连接、层归一化等模块无缝衔接。
(四)完整前馈层(Feed-Forward Layer)类实现
class FeedForward(nn.Module):def __init__(self, d_model, d_ff=2048, dropout=0.1):super().__init__()self.linear_1 = nn.Linear(d_model, d_ff)self.dropout = nn.Dropout(dropout)self.linear_2 = nn.Linear(d_ff, d_model)def forward(self, x):x = self.dropout(F.relu(self.linear_1(x)))x = self.linear_2(x)return x
(五)算法优势与意义
- 非线性表达能力:通过
ReLU激活函数,使模型能学习输入的非线性关系(如文本中词与词的隐式语义关联); - 高维特征变换:
d_ff远大于d_model的设计,让模型在高维空间中充分挖掘特征,提升对复杂模式的捕捉能力; - 模块化与可扩展性:前馈层是独立模块,可灵活调整
d_ff和dropout参数,平衡模型性能与计算开销。
在 Transformer 架构中,前馈层通常与多头注意力层、残差连接、层归一化共同组成一个完整的 “Transformer 块”,为模型提供了 “全局依赖建模(注意力)+ 非线性特征变换(前馈)” 的双重能力,是大语言模型实现强大语义理解的核心支撑之一。
六、层归一化(Layer Normalization)
(一)算法设计意图
在深层神经网络中,内部协变量偏移(即每层输入的分布随训练不断变化)会导致训练不稳定、收敛缓慢甚至梯度消失。层归一化通过对每个样本的特征维度进行归一化,将其分布调整为 “均值 0、方差 1” 的标准形式,再通过可学习的参数恢复表达能力,从而稳定每层的输入分布,加速训练收敛。
(二)模块结构与参数
class Norm(nn.Module):def __init__(self, d_model, eps=1e-6):super().__init__()self.size = d_model # 特征维度(如Transformer的d_model=512)# 可学习的缩放参数(初始为全1)self.alpha = nn.Parameter(torch.ones(self.size))# 可学习的偏移参数(初始为全0)self.bias = nn.Parameter(torch.zeros(self.size))self.eps = eps # 极小值,防止除以0
d_model:输入的特征维度(需与 Transformer 的模型维度一致,如 512);eps:用于避免 “除以 0” 的小常数(默认 1e-6);alpha、bias:可学习的参数,分别用于对归一化后的结果进行缩放和偏移,确保模型不会因归一化丢失表达能力。
(三)前向传播流程
def forward(self, x):# 1. 计算最后一维(特征维度)的均值和标准差mean = x.mean(dim=-1, keepdim=True) # 形状:[batch_size, seq_len, 1]std = x.std(dim=-1, keepdim=True) # 形状:[batch_size, seq_len, 1]# 2. 层归一化:(x - 均值) / (标准差 + eps)normalized = (x - mean) / (std + self.eps)# 3. 缩放(alpha)+ 偏移(bias),恢复表达能力norm = self.alpha * normalized + self.biasreturn norm
步骤解析:
- 统计特征分布:对输入
x(形状通常为[batch_size, seq_len, d_model])的 ** 最后一个维度(特征维度)** 计算均值和标准差,且保持维度(keepdim=True)以支持广播运算。 - 归一化操作:将输入减去均值、除以(标准差 + eps),得到 “均值 0、方差 1” 的标准化结果。
- 缩放与偏移:通过可学习的
alpha(缩放)和bias(偏移)调整归一化结果,让模型能在标准化的基础上恢复复杂的特征表达。
(四)完整层归一化(Layer Normalization)类实现
class Norm(nn.Module):def __init__(self, d_model, eps=1e-6):super().__init__()self.size = d_modelself.alpha = nn.Parameter(torch.ones(self.size))self.bias = nn.Parameter(torch.zeros(self.size))self.eps = epsdef forward(self, x):norm = self.alpha * (x - x.mean(dim=-1, keepdim=True)) / (x.std(dim=-1, keepdim=True) + self.eps) + self.biasreturn norm
(五)算法优势与意义
- 不依赖批次:与 “批量归一化(BatchNorm)” 不同,层归一化仅依赖单个样本的特征维度统计,不依赖批次大小或序列长度,因此在 NLP 任务中更灵活(如处理变长序列时无兼容性问题)。
- 稳定训练:通过固定每层输入的分布,缓解 “内部协变量偏移”,使梯度更稳定,训练收敛更快,同时减少深层网络的梯度消失风险。
- 保留表达能力:可学习的
alpha和bias让模型能在归一化后 “自适应” 地恢复特征的多样性,避免因过度归一化丢失关键信息。
在 Transformer 架构中,层归一化通常与残差连接配合使用(即 “Add & Norm” 结构),是构建稳定、高效大语言模型的核心技术之一。它确保了每一层的输入分布稳定,为模型的深层堆叠和复杂语义建模提供了基础。
七、编码器层(Encoder Layer)与编码器(Encoder)
(一)算法设计意图
Transformer 模型中的编码器(Encoder) 包括编码器的基本单元(EncoderLayer)、层堆叠工具(get_clones)和完整编码器(Encoder)。编码器的核心功能是将输入序列(如源语言文本)转换为包含全局上下文信息的向量表示,为后续的解码器提供语义基础。
(二)get_clones:层堆叠工具函数
def get_clones(module, N):return nn.ModuleList([module.clone() for _ in range(N)])
- 功能:复制同一个模块(
module)N次,返回一个nn.ModuleList(PyTorch 中用于管理多个子模块的容器)。 - 作用:Transformer 的编码器由
N个结构相同但参数独立的EncoderLayer堆叠而成(如原论文中N=6),此函数用于高效创建这些层,确保每层可独立训练且结构一致。
(三)EncoderLayer:编码器的基本单元
EncoderLayer是编码器的核心模块,每个层包含 “多头自注意力” 和 “前馈网络” 两个核心子层,配合残差连接和层归一化,形成完整的特征变换单元。
1. 模块结构与参数
class EncoderLayer(nn.Module):def __init__(self, d_model, heads, dropout=0.1):super().__init__()self.norm_1 = Norm(d_model) # 第一个层归一化(用于注意力子层后)self.norm_2 = Norm(d_model) # 第二个层归一化(用于前馈子层后)self.attn = MultiHeadAttention(heads, d_model, dropout=dropout) # 多头自注意力self.ff = FeedForward(d_model, dropout=dropout) # 前馈网络self.dropout_1 = nn.Dropout(dropout) # 注意力输出的Dropoutself.dropout_2 = nn.Dropout(dropout) # 前馈网络输出的Dropout
d_model:模型特征维度(如 512),贯穿整个编码器的维度;heads:多头注意力的头数(如 8);dropout:防止过拟合的丢弃率;- 核心组件:
MultiHeadAttention(捕捉序列内的全局依赖)、FeedForward(非线性特征变换)、Norm(层归一化,稳定训练)、Dropout(正则化)。
2. 前向传播流程(forward)
def forward(self, x, mask):# 第一步:多头自注意力 + 残差连接 + 层归一化attn_output = self.attn(x, x, x, mask) # 自注意力:Q=K=V=x,mask过滤无效位置(如padding)attn_output = self.dropout_1(attn_output) # 对注意力输出做Dropoutx = x + attn_output # 残差连接:输入x + 注意力输出x = self.norm_1(x) # 层归一化:稳定特征分布# 第二步:前馈网络 + 残差连接 + 层归一化ff_output = self.ff(x) # 前馈网络:非线性特征变换ff_output = self.dropout_2(ff_output) # 对前馈输出做Dropoutx = x + ff_output # 残差连接:当前x + 前馈输出x = self.norm_2(x) # 层归一化:再次稳定分布return x
步骤解析:
- 多头自注意力子层:输入序列
x同时作为查询(Q)、键(K)、值(V),通过自注意力计算每个位置与序列中所有位置的依赖关系(如 “词 A 与词 B 的语义关联”),输出融合了全局上下文的特征。 - 残差连接(Add):
x + attn_output直接将输入与子层输出相加,缓解深层网络的梯度消失问题,让信息更高效地流动。 - 层归一化(Norm):对残差连接后的结果做归一化(均值 0、方差 1),并通过可学习参数调整,稳定每层输入的分布,加速训练收敛。
- 前馈网络子层:通过 “升维→ReLU→降维” 的非线性变换,进一步挖掘注意力输出中的复杂特征模式(如语义的深层抽象)。
- 整个流程形成 “注意力建模依赖→前馈增强表达” 的闭环,每个
EncoderLayer都会让序列特征更 “精炼” 地包含上下文信息。
(四)Encoder:完整编码器
Encoder类将多个EncoderLayer堆叠,并结合 “词嵌入” 和 “位置编码”,形成从原始输入到上下文特征的完整转换流程。
1. 模块结构与参数
class Encoder(nn.Module):def __init__(self, vocab_size, d_model, N, heads, dropout):super().__init__()self.N = N # 堆叠的EncoderLayer数量(如6)self.embed = nn.Embedding(vocab_size, d_model) # 词嵌入层:将词汇索引映射为向量self.pe = PositionalEncoder(d_model, dropout=dropout) # 位置编码器:注入位置信息self.layers = get_clones(EncoderLayer(d_model, heads, dropout), N) # N个EncoderLayer堆叠self.norm = Norm(d_model) # 最终的层归一化:统一输出分布
vocab_size:输入词汇表的大小(如源语言词典大小);d_model:模型特征维度(与EncoderLayer一致);N:EncoderLayer的堆叠数量(控制模型深度);- 核心组件:
nn.Embedding(词向量映射)、PositionalEncoder(位置信息注入)、layers(多层EncoderLayer堆叠)、norm(最终归一化)。
2. 前向传播流程(forward)
def forward(self, src, mask):x = self.embed(src) # 1. 词嵌入:将输入序列(词汇索引)转换为向量(shape: [batch, seq_len, d_model])x = self.pe(x) # 2. 注入位置编码:词向量 + 位置编码,让模型感知序列顺序for i in range(self.N): # 3. 经过N个EncoderLayer堆叠x = self.layers[i](x, mask) # 每层逐步增强上下文信息return self.norm(x) # 4. 最终归一化:输出稳定的上下文特征序列
步骤解析:
- 词嵌入(Embed):将输入的离散词汇索引(如 “[1, 5, 3]”)映射为连续的向量表示(
d_model维度),是计算机 “理解” 文本的基础。 - 位置编码(Positional Encoding):通过正弦余弦函数生成位置向量,与词向量相加,解决 Transformer “并行处理不感知顺序” 的问题,让模型知道 “每个词在序列中的位置”。
- 多层
EncoderLayer堆叠:N个EncoderLayer依次处理特征,每一层都在前一层的基础上进一步整合全局上下文(如第一层可能捕捉局部短语,深层可能捕捉句子级语义)。 - 最终归一化:确保输出的特征序列分布稳定,便于后续解码器使用。
(五)完整编码器层(Encoder Layer)与编码器(Encoder)类实现
def get_clones(module, N):return nn.ModuleList([module.clone() for _ in range(N)])class EncoderLayer(nn.Module):def __init__(self, d_model, heads, dropout=0.1):super().__init__()self.norm_1 = Norm(d_model)self.norm_2 = Norm(d_model)self.attn = MultiHeadAttention(heads, d_model, dropout=dropout)self.ff = FeedForward(d_model, dropout=dropout)self.dropout_1 = nn.Dropout(dropout)self.dropout_2 = nn.Dropout(dropout)def forward(self, x, mask):attn_output = self.attn(x, x, x, mask)attn_output = self.dropout_1(attn_output)x = x + attn_outputx = self.norm_1(x)ff_output = self.ff(x)ff_output = self.dropout_2(ff_output)x = x + ff_outputx = self.norm_2(x)return xclass Encoder(nn.Module):def __init__(self, vocab_size, d_model, N, heads, dropout):super().__init__()self.N = Nself.embed = nn.Embedding(vocab_size, d_model)self.pe = PositionalEncoder(d_model, dropout=dropout)self.layers = get_clones(EncoderLayer(d_model, heads, dropout), N)self.norm = Norm(d_model)def forward(self, src, mask):x = self.embed(src)x = self.pe(x)for i in range(self.N):x = self.layers[i](x, mask)return self.norm(x)
(六)编码器的整体功能与意义
编码器的核心作用是将输入序列(如 “我爱自然语言处理”)转换为包含全局上下文信息的向量序列。例如,对于 “他说苹果很好吃”,编码器会让 “苹果” 的向量同时包含 “他说” 的主语信息和 “很好吃” 的评价信息,为解码器(如翻译任务中的目标语言生成)提供准确的语义基础。
其设计优势在于:
- 全局依赖建模:通过多头自注意力,序列中任意两个位置的依赖关系可直接被捕捉(无需像 RNN 那样逐步传递),解决长文本的 “长程依赖” 问题;
- 并行高效:无循环结构,所有位置的计算可并行进行,训练和推理速度远超 RNN;
- 深层堆叠能力:残差连接和层归一化让模型可堆叠多层(如
N=6),通过深度提升语义建模能力。
综上,这段代码完整实现了 Transformer 编码器的核心逻辑,是大语言模型、机器翻译等任务中 “理解输入语义” 的关键组件。
八、解码器层(Decoder Layer)与解码器(Decoder)
(一)算法设计意图
解码器的核心功能是基于编码器输出的源序列上下文信息,生成符合逻辑的目标序列(如机器翻译中的目标语言、文本生成中的续写内容等),同时保证生成序列的时序合理性(如避免 “提前看到” 未来的 token)。
(二)DecoderLayer:解码器的基本单元
DecoderLayer是解码器的核心模块,每个层包含三个核心子层(比编码器多一个交叉注意力层),配合残差连接和层归一化,实现 “目标序列自建模→关联源序列上下文→增强特征表达” 的完整流程。
1. 模块结构与参数
class DecoderLayer(nn.Module):def __init__(self, d_model, heads, dropout=0.1):super().__init__()# 三个层归一化(分别对应三个子层)self.norm_1 = Norm(d_model)self.norm_2 = Norm(d_model)self.norm_3 = Norm(d_model)# 三个Dropout层(分别用于三个子层的输出)self.dropout_1 = nn.Dropout(dropout)self.dropout_2 = nn.Dropout(dropout)self.dropout_3 = nn.Dropout(dropout)# 第一个注意力层:掩码多头自注意力(处理目标序列内部依赖)self.attn_1 = MultiHeadAttention(heads, d_model, dropout=dropout)# 第二个注意力层:多头交叉注意力(关联目标序列与源序列上下文)self.attn_2 = MultiHeadAttention(heads, d_model, dropout=dropout)# 前馈网络(增强非线性特征表达)self.ff = FeedForward(d_model, dropout=dropout)
- 核心差异:与
EncoderLayer相比,DecoderLayer多了一个attn_2(交叉注意力层),且第一个自注意力层需要掩码(trg_mask),这是解码器适配 “序列生成任务” 的关键设计。 - 参数说明:
d_model(模型维度)、heads(注意力头数)、dropout(丢弃率)与编码器保持一致,确保维度兼容。
2. 前向传播流程(forward)
def forward(self, x, e_outputs, src_mask, trg_mask):# 第一步:掩码多头自注意力 + 残差连接 + 层归一化attn_output_1 = self.attn_1(x, x, x, trg_mask) # 自注意力:Q=K=V=x,trg_mask防止关注未来tokenattn_output_1 = self.dropout_1(attn_output_1) # Dropout正则化x = x + attn_output_1 # 残差连接:输入x + 自注意力输出x = self.norm_1(x) # 层归一化:稳定特征分布# 第二步:多头交叉注意力 + 残差连接 + 层归一化attn_output_2 = self.attn_2(x, e_outputs, e_outputs, src_mask) # 交叉注意力:Q=x,K=V=编码器输出e_outputsattn_output_2 = self.dropout_2(attn_output_2) # Dropout正则化x = x + attn_output_2 # 残差连接:当前x + 交叉注意力输出x = self.norm_2(x) # 层归一化:稳定分布# 第三步:前馈网络 + 残差连接 + 层归一化ff_output = self.ff(x) # 前馈网络:非线性特征变换ff_output = self.dropout_3(ff_output) # Dropout正则化x = x + ff_output # 残差连接:当前x + 前馈输出x = self.norm_3(x) # 层归一化:最终稳定分布return x
步骤解析:
-
掩码多头自注意力(
attn_1):- 输入
x是解码器当前的目标序列特征(如已生成的部分翻译结果),Q=K=V=x,即对目标序列做 “自注意力”,捕捉内部依赖(如 “他吃了____” 中,“吃了” 与后续宾语的关联)。 - 关键:
trg_mask(目标掩码)用于遮挡 “未来的 token”(如生成第 i 个词时,不能关注 i+1 及以后的词),确保生成过程符合时序逻辑(避免 “预知未来”)。掩码会将未来位置的注意力得分设为-1e9,使 Softmax 后权重趋近于 0。
- 输入
-
多头交叉注意力(
attn_2):- 这是解码器与编码器交互的核心层:
Q是解码器经过自注意力后的特征(x),K=V是编码器的输出(e_outputs,即源序列的上下文特征)。 - 作用:让解码器 “关注源序列中与当前目标序列相关的信息”。例如,翻译 “他喜欢苹果” 为英文时,当解码器生成 “likes”,交叉注意力会引导它关注源序列中的 “喜欢”,生成 “apple” 时关注源序列中的 “苹果”。
src_mask(源掩码)用于过滤源序列中的无效位置(如 padding 填充的词),避免模型关注无意义的信息。
- 这是解码器与编码器交互的核心层:
-
前馈网络(
ff):与编码器的前馈网络功能一致,通过 “升维→ReLU→降维” 的非线性变换,进一步挖掘交叉注意力输出中的复杂特征模式(如语义的深层抽象),增强模型的表达能力。 -
残差连接与层归一化:每个子层后都通过 “残差连接(输入 + 输出)” 缓解梯度消失,通过 “层归一化” 稳定特征分布,确保深层网络的训练稳定性 —— 这与编码器的设计逻辑一致,但因多了一个子层,故需要三个归一化步骤。
(三)Decoder:完整解码器
Decoder类将多个DecoderLayer堆叠,并结合 “词嵌入” 和 “位置编码”,形成从目标序列输入到最终生成特征的完整转换流程,同时接收编码器的输出作为上下文参考。
1. 模块结构与参数
class Decoder(nn.Module):def __init__(self, vocab_size, d_model, N, heads, dropout):super().__init__()self.N = N # 堆叠的DecoderLayer数量(如原论文中N=6)self.embed = nn.Embedding(vocab_size, d_model) # 目标序列的词嵌入层self.pe = PositionalEncoder(d_model, dropout=dropout) # 位置编码器:注入目标序列的位置信息self.layers = get_clones(DecoderLayer(d_model, heads, dropout), N) # N个DecoderLayer堆叠self.norm = Norm(d_model) # 最终的层归一化:统一输出分布
vocab_size:目标序列的词汇表大小(如目标语言词典大小);d_model:模型维度(与编码器、DecoderLayer保持一致,如 512);N:DecoderLayer的堆叠数量(控制模型深度,通常与编码器的N相同);- 核心组件:
nn.Embedding(目标词向量映射)、PositionalEncoder(目标序列位置信息注入)、layers(多层DecoderLayer堆叠)、norm(最终归一化)。
2. 前向传播流程(forward)
def forward(self, trg, e_outputs, src_mask, trg_mask):x = self.embed(trg) # 1. 词嵌入:将目标序列(词汇索引)转换为向量(shape: [batch, trg_seq_len, d_model])x = self.pe(x) # 2. 注入位置编码:目标词向量 + 位置编码,让模型感知目标序列的顺序for i in range(self.N): # 3. 经过N个DecoderLayer堆叠x = self.layers[i](x, e_outputs, src_mask, trg_mask) # 每层结合编码器输出,逐步增强生成特征return self.norm(x) # 4. 最终归一化:输出稳定的目标序列特征
步骤解析:
-
目标序列词嵌入(
embed):将输入的目标序列(如翻译任务中的 “部分生成的英文句子”)从离散词汇索引(如 “[3, 7, 2]”)映射为连续的向量表示(d_model维度),作为解码器的初始输入。 -
目标序列位置编码(
pe):与编码器类似,通过正弦余弦函数生成位置向量,与目标词向量相加,让模型感知目标序列的时序顺序(如 “我吃苹果” 与 “苹果吃我” 的区别)。 -
多层
DecoderLayer堆叠:N个DecoderLayer依次处理特征,每一层都在前一层的基础上:- 通过掩码自注意力优化目标序列内部的依赖关系;
- 通过交叉注意力关联源序列的上下文信息;
- 通过前馈网络增强特征表达。深层堆叠使生成的特征越来越 “精准”(如从模糊的语义到具体的词选择)。
-
最终归一化:确保输出的目标序列特征分布稳定,便于后续通过线性层 + Softmax 生成最终的词汇概率(如预测下一个词)。
(四)完整解码器层(Decoder Layer)与解码器(Decoder)类实现
class DecoderLayer(nn.Module):def __init__(self, d_model, heads, dropout=0.1):super().__init__()self.norm_1 = Norm(d_model)self.norm_2 = Norm(d_model)self.norm_3 = Norm(d_model)self.dropout_1 = nn.Dropout(dropout)self.dropout_2 = nn.Dropout(dropout)self.dropout_3 = nn.Dropout(dropout)self.attn_1 = MultiHeadAttention(heads, d_model, dropout=dropout)self.attn_2 = MultiHeadAttention(heads, d_model, dropout=dropout)self.ff = FeedForward(d_model, dropout=dropout)def forward(self, x, e_outputs, src_mask, trg_mask):attn_output_1 = self.attn_1(x, x, x, trg_mask)attn_output_1 = self.dropout_1(attn_output_1)x = x + attn_output_1x = self.norm_1(x)attn_output_2 = self.attn_2(x, e_outputs, e_outputs, src_mask)attn_output_2 = self.dropout_2(attn_output_2)x = x + attn_output_2x = self.norm_2(x)ff_output = self.ff(x)ff_output = self.dropout_3(ff_output)x = x + ff_outputx = self.norm_3(x)return xclass Decoder(nn.Module):def __init__(self, vocab_size, d_model, N, heads, dropout):super().__init__()self.N = Nself.embed = nn.Embedding(vocab_size, d_model)self.pe = PositionalEncoder(d_model, dropout=dropout)self.layers = get_clones(DecoderLayer(d_model, heads, dropout), N)self.norm = Norm(d_model)def forward(self, trg, e_outputs, src_mask, trg_mask):x = self.embed(trg)x = self.pe(x)for i in range(self.N):x = self.layers[i](x, e_outputs, src_mask, trg_mask)return self.norm(x)
(五)解码器的核心功能与意义
解码器的核心作用是基于 “已生成的目标序列部分” 和 “编码器提供的源序列上下文”,生成下一个合理的 token,是生成式任务(机器翻译、文本摘要、对话生成等)的核心组件。其设计优势在于:
-
时序合理性保障:通过掩码自注意力(
trg_mask)严格限制模型只能关注 “已生成的部分”,避免生成逻辑混乱(如翻译时先输出 “苹果” 再输出 “吃”)。 -
源 - 目标关联建模:通过交叉注意力层,解码器能动态关注源序列中与当前生成步骤相关的信息,确保生成结果与源序列语义一致(如 “苹果” 不会被翻译成 “banana”)。
-
深层特征增强:多层堆叠的
DecoderLayer配合前馈网络,能从简单的词匹配逐步抽象到复杂的语义生成(如处理歧义、长句依赖等)。
综上,这段代码完整实现了 Transformer 解码器的核心逻辑,是连接 “源序列理解” 与 “目标序列生成” 的关键桥梁,也是大语言模型实现文本生成能力的基础。
九、整体 Transformer 模型
(一)算法设计意图
Transformer 模型的完整架构整合了之前介绍的编码器(Encoder)、解码器(Decoder)和输出层,形成一个端到端的序列转换模型(如机器翻译、文本生成等任务)。Transformer 的核心是通过 “编码器 - 解码器” 结构,结合自注意力和交叉注意力机制,实现对源序列的理解和目标序列的生成。
(二)Transformer类的整体结构与参数
class Transformer(nn.Module):def __init__(self, src_vocab, trg_vocab, d_model, N, heads, dropout):super().__init__()# 编码器:处理源序列(如输入语言),输出包含全局上下文的特征self.encoder = Encoder(src_vocab, d_model, N, heads, dropout)# 解码器:基于编码器输出和目标序列前缀,生成目标序列(如输出语言)self.decoder = Decoder(trg_vocab, d_model, N, heads, dropout)# 输出层:将解码器输出的特征映射到目标词汇表,用于预测下一个词self.out = nn.Linear(d_model, trg_vocab)
参数说明:
src_vocab:源序列的词汇表大小(如中文词汇表大小);trg_vocab:目标序列的词汇表大小(如英文词汇表大小);d_model:模型的核心特征维度(贯穿编码器、解码器的统一维度,如 512);N:编码器和解码器中堆叠的层数(如原论文中N=6,控制模型深度);heads:多头注意力的头数(如 8 头,控制注意力的多视角建模能力);dropout:全局丢弃率(用于正则化,防止过拟合)。
核心组件作用:
self.encoder:负责将源序列(如 “我爱自然语言处理”)转换为包含全局上下文的特征序列(e_outputs),捕捉源序列中词与词的依赖关系;self.decoder:接收编码器的输出(e_outputs)和目标序列前缀(如 “I love”),生成融合源上下文和目标序列内部依赖的特征序列(d_output);self.out:线性层,将解码器输出的d_model维度特征映射到trg_vocab维度(目标词汇表大小),最终通过 Softmax 可得到每个位置的词概率分布(用于预测下一个词)。
(三)前向传播流程(forward方法)
def forward(self, src, trg, src_mask, trg_mask):# 1. 编码器处理源序列,得到上下文特征e_outputse_outputs = self.encoder(src, src_mask)# 2. 解码器结合e_outputs和目标序列前缀,生成特征d_outputd_output = self.decoder(trg, e_outputs, src_mask, trg_mask)# 3. 输出层映射到目标词汇表,得到最终预测output = self.out(d_output)return output
步骤解析:
-
编码器处理源序列(
e_outputs):- 输入
src是源序列的词汇索引(如[1, 5, 3, 9],形状为[batch_size, src_seq_len]); src_mask是源序列的掩码(形状为[batch_size, 1, src_seq_len]),用于过滤无效位置(如 padding 填充的词),避免模型关注无意义的信息;- 编码器输出
e_outputs是源序列的上下文特征(形状为[batch_size, src_seq_len, d_model]),每个位置的特征都融合了整个源序列的语义。
- 输入
-
解码器处理目标序列(
d_output):- 输入
trg是目标序列的前缀索引(如翻译任务中 “已生成的英文部分”,形状为[batch_size, trg_seq_len]); trg_mask是目标序列的掩码(形状为[batch_size, trg_seq_len, trg_seq_len]),是一个下三角矩阵,用于遮挡 “未来的词”(如生成第 i 个词时,不能关注 i+1 及以后的词),确保生成过程符合时序逻辑;- 解码器接收
trg和编码器的e_outputs,通过 “掩码自注意力” 捕捉目标序列内部依赖,通过 “交叉注意力” 关联源序列上下文,最终输出d_output(形状为[batch_size, trg_seq_len, d_model]),每个位置的特征融合了目标前缀和源序列的相关信息。
- 输入
-
输出层预测目标词(
output):- 线性层
self.out将d_output的d_model维度特征映射到trg_vocab维度(形状为[batch_size, trg_seq_len, trg_vocab]); - 输出
output的每个位置对应目标词汇表中所有词的 “得分”,后续可通过F.softmax(output, dim=-1)得到概率分布,用于预测该位置的词(如翻译任务中 “下一个英文词”)。
- 线性层
(四)Transformer 的核心功能与优势
Transformer 的整体作用是实现 “源序列→目标序列” 的端到端转换,例如:
- 机器翻译:将中文句子(源序列)转换为英文句子(目标序列);
- 文本摘要:将长文本(源序列)转换为短摘要(目标序列);
- 代码生成:将自然语言描述(源序列)转换为代码(目标序列)。
其核心优势源于各组件的协同设计:
- 并行高效:摒弃 RNN 的循环结构,编码器和解码器的所有位置计算可并行进行(注意力机制通过矩阵运算实现全局依赖建模),训练和推理速度远超传统序列模型;
- 长程依赖建模:通过自注意力机制,序列中任意两个位置的依赖关系可直接被捕捉(无需像 RNN 那样逐步传递),解决了长文本的 “长距离依赖” 问题(如一篇文章中开头和结尾的关联);
- 多视角语义理解:多头注意力让模型能从不同子空间捕捉语义(如语法结构、语义关联、局部 / 全局依赖),结合深层堆叠的编码器 / 解码器,可建模复杂的语言规律;
- 灵活的序列转换:编码器 - 解码器结构天然适配 “不等长序列转换” 任务(如源序列长度≠目标序列长度),通过交叉注意力动态关联源和目标信息,确保生成结果与源语义一致。
(五)与训练流程的衔接
在实际训练中,output会与目标序列的真实标签(trg的偏移版本,如trg[1:])计算交叉熵损失,通过反向传播更新整个模型的参数(包括编码器、解码器、输出层的所有权重)。例如:
# 假设trg是目标序列(如[<sos>, w1, w2, ..., wn, <eos>])
# 解码器输入是trg[:-1](去掉最后一个<eos>),真实标签是trg[1:](去掉第一个<sos>)
preds = model(src, trg[:-1], src_mask, trg_mask)
loss = F.cross_entropy(preds.reshape(-1, trg_vocab), trg[1:].reshape(-1))
loss.backward() # 反向传播更新参数
(六)完整整体 Transformer 模型类实现
class Transformer(nn.Module):def __init__(self, src_vocab, trg_vocab, d_model, N, heads, dropout):super().__init__()self.encoder = Encoder(src_vocab, d_model, N, heads, dropout)self.decoder = Decoder(trg_vocab, d_model, N, heads, dropout)self.out = nn.Linear(d_model, trg_vocab)def forward(self, src, trg, src_mask, trg_mask):e_outputs = self.encoder(src, src_mask)d_output = self.decoder(trg, e_outputs, src_mask, trg_mask)output = self.out(d_output)return output
综上,Transformer类是整个模型的 “总装”,它通过编码器理解源序列、解码器生成目标序列、输出层预测具体词汇,实现了从 “输入” 到 “输出” 的端到端序列转换。其设计融合了自注意力、交叉注意力、残差连接、层归一化等技术,解决了传统序列模型的效率和长程依赖问题,成为现代大语言模型(如 GPT、BERT)的核心架构基础。
十、模型训练与测试
(一)算法设计意图
Transformer 模型的参数配置、训练流程与推理(翻译)逻辑,是从 “模型定义” 到 “实际应用” 的闭环。核心围绕 “端到端序列转换”(以机器翻译为例,源语言为英文、目标语言为法文),涵盖参数初始化、训练优化、自回归生成三大模块。
(二)模型参数定义:为 Transformer 配置核心超参与初始化
这部分是模型训练前的 “准备工作”,包括超参数设置、模型实例化、参数初始化与优化器配置,直接影响模型性能与训练稳定性。
1. 核心超参数说明
d_model = 512 # 模型核心特征维度(贯穿编码器/解码器的统一维度)
heads = 8 # 多头注意力的头数(原论文配置,平衡多视角建模与计算效率)
N = 6 # 编码器/解码器的堆叠层数(原论文配置,层数越深语义建模能力越强)
src_vocab = len(EN_TEXT.vocab) # 源语言(英文)词汇表大小(如10000,取决于数据集)
trg_vocab = len(FR_TEXT.vocab) # 目标语言(法文)词汇表大小(同上)
- 超参设计依据:
d_model=512和heads=8是 Transformer 原论文的经典配置,确保每个注意力头的维度d_k = d_model//heads = 64,既避免单头维度过小导致的表达能力不足,也避免过大导致的计算冗余;N=6通过多层堆叠实现 “渐进式语义抽象”(浅层捕捉局部短语,深层捕捉句子级逻辑)。
2. 模型实例化与参数初始化
model = Transformer(src_vocab, trg_vocab, d_model, N, heads)
for p in model.parameters():if p.dim() > 1:nn.init.xavier_uniform_(p) # 对高维参数(如线性层权重)做Xavier均匀初始化
- 参数初始化意义:神经网络训练的核心是 “梯度更新”,若参数初始值过大 / 过小,会导致梯度消失(梯度趋近于 0)或梯度爆炸(梯度无限增大)。
nn.init.xavier_uniform_( Xavier 初始化)通过让参数方差与输入 / 输出维度匹配,确保初始梯度处于合理范围,尤其适配线性层、注意力层等高维参数(如q_linear的权重矩阵);低维参数(如偏置bias)默认初始化为 0,无需额外处理。
3. 优化器配置
optim = torch.optim.Adam(model.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)
- 选择 Adam 优化器的原因:Adam 结合了动量(Momentum)和自适应学习率(RMSprop)的优势,能快速收敛且不易陷入局部最优,是 Transformer 训练的主流选择。
lr=0.0001:学习率(原论文配置),控制参数更新幅度,过大会震荡不收敛,过小会训练缓慢;betas=(0.9, 0.98):一阶动量(动量项)和二阶动量的指数衰减率,分别用于平滑梯度方向和自适应调整学习率;eps=1e-9:避免分母为 0 的极小值,增强数值稳定性。
(三)模型训练函数(train_model):端到端优化 Transformer
训练函数的核心是 “迭代更新模型参数”,通过输入源语言序列和目标语言序列,计算预测损失并反向传播,让模型学习 “源→目标” 的映射规律(如英文到法文的翻译)。
1. 训练流程拆解
def train_model(epochs, print_every=100):model.train() # 设为训练模式(启用Dropout、BatchNorm更新)start = time.time()temp = starttotal_loss = 0 # 累计损失,用于计算平均损失# 1. 迭代训练轮次(epochs)for epoch in range(epochs):# 2. 迭代训练数据批次(train_iter为DataLoader,按批次加载数据)for i, batch in enumerate(train_iter):# 3. 数据预处理:调整维度与拆分目标序列src = batch.English.transpose(0, 1) # 源序列:调整维度为[batch_size, seq_len](若原是[seq_len, batch_size])trg = batch.French.transpose(0, 1) # 目标序列:同上trg_input = trg[:, :-1] # 解码器输入:去掉目标序列最后一个词(<eos>),避免泄露未来信息targets = trg[:, 1:].contiguous().view(-1) # 真实标签:去掉目标序列第一个词(<sos>),展平为1维(适配交叉熵输入)# 4. 创建掩码:过滤无效信息,确保训练逻辑正确src_mask, trg_mask = create_masks(src, trg_input) # 自定义函数,生成两种掩码:# - src_mask:[batch_size, 1, src_seq_len],过滤源序列中的padding(值为0的位置)# - trg_mask:[batch_size, trg_seq_len, trg_seq_len],下三角掩码,过滤目标序列的“未来词”# 5. 模型前向传播:预测目标序列preds = model(src, trg_input, src_mask, trg_mask) # preds形状:[batch_size, trg_seq_len-1, trg_vocab]# 6. 计算损失:交叉熵损失(忽略padding的标签)optim.zero_grad() # 清空上一轮的梯度(避免梯度累积)# 损失计算逻辑:# - preds.view(-1, trg_vocab):展平为[batch_size*(trg_seq_len-1), trg_vocab]# - targets:展平的真实标签,长度为batch_size*(trg_seq_len-1)# - ignore_index=target_pad:忽略padding对应的标签,不计算其损失(避免无效数据干扰)loss = F.cross_entropy(preds.view(-1, preds.size(-1)), targets, ignore_index=target_pad)# 7. 反向传播与参数更新loss.backward() # 计算梯度(从损失反向传播到所有参数)optim.step() # 根据梯度更新参数(执行一次优化)# 8. 日志打印:监控训练进度total_loss += loss.data[0]if (i + 1) % print_every == 0: # 每print_every个批次打印一次loss_avg = total_loss / print_every # 平均损失(更能反映训练趋势)print("time = %dm, epoch %d, iter = %d, loss = %.3f, %ds per %d iters" % ((time.time() - start) // 60, # 总训练时间(分钟)epoch + 1, # 当前轮次(从1开始)i + 1, # 当前批次(从1开始)loss_avg, # 平均损失(越小表示模型拟合越好)time.time() - temp, # 近print_every个批次的耗时print_every))total_loss = 0 # 重置累计损失temp = time.time()
2. 关键训练逻辑解析
model.train()的作用:启用模型中的正则化组件(如 Dropout 随机丢弃神经元、BatchNorm 更新移动均值 / 方差),确保训练时的泛化能力;推理时需用model.eval()关闭这些组件。- 目标序列拆分(
trg_input与targets):假设目标序列为<sos> I love apple <eos>,则trg_input是<sos> I love apple(解码器输入,用于预测下一个词),targets是I love apple <eos>(真实标签,用于计算预测误差),避免解码器 “看到未来的词”,符合实际生成逻辑。 - 掩码的核心作用:
src_mask:过滤源序列中的 padding(如句子长度不足时填充的 0),防止模型关注无意义的 padding 词;trg_mask:通过下三角矩阵(对角线及以下为 1,以上为 0),确保解码器在生成第 i 个词时,只能关注前 i-1 个已生成的词,避免 “预知未来”。
(四)模型测试函数(translate):自回归生成目标序列
训练完成后,translate函数实现 “推理逻辑”—— 输入源语言序列(如英文句子),通过自回归生成(逐词预测)输出目标语言序列(如法文句子),是模型的实际应用环节。
1. 推理流程拆解
def translate(model, src, max_len=80, custom_string=False):model.eval() # 设为推理模式(关闭Dropout、固定BatchNorm参数)with torch.no_grad(): # 禁用梯度计算(节省内存,加速推理)# 1. 处理自定义输入(若输入是字符串,需分词并转换为词汇索引)if custom_string == True:src = tokenize_en(src) # 自定义英文分词函数(如按空格分词,处理标点)# 将分词结果转换为Tensor(形状:[1, seq_len],batch_size=1,单条输入)sentence = torch.LongTensor([[EN_TEXT.vocab.stoi[tok] for tok in sentence]]).cuda()else:sentence = src # 若输入已是Tensor(如批量推理),直接使用# 2. 创建源序列掩码(过滤padding)src_mask = (sentence != input_pad).unsqueeze(-2) # 形状:[1, 1, seq_len],unsqueeze(-2)适配注意力层输入# 3. 编码器处理源序列:得到上下文特征e_outputs = model.encoder(sentence, src_mask) # 形状:[1, seq_len, d_model]# 4. 初始化目标序列(从起始符<sos>开始)outputs = torch.zeros(max_len).type_as(sentence.data) # 存储生成的目标序列(初始为0,长度max_len)# 第一个词设为目标语言的起始符<sos>(如法文的起始符索引)outputs[0] = torch.LongTensor([FR_TEXT.vocab.stoi['<sos>']])# 5. 自回归生成:逐词预测直到<eos>或达到max_lenfor i in range(1, max_len):# 5.1 创建当前步骤的目标掩码(下三角掩码,长度为i)trg_mask = np.triu(np.ones((1, i, i)), k=1).astype('uint8') # 上三角为1(需过滤),下三角为0trg_mask = torch.from_numpy(trg_mask) == 0 # 转换为布尔值:下三角为True(保留),上三角为False(过滤)trg_mask = trg_mask.cuda() # 移到GPU(若使用GPU训练)# 5.2 解码器前向传播:预测下一个词# 取前i个已生成的词(outputs[:i])作为解码器输入,形状调整为[1, i]d_output = model.decoder(outputs[:i].unsqueeze(0), e_outputs, src_mask, trg_mask)# 通过输出层映射到词汇表,计算概率分布out = model.out(d_output) # 形状:[1, i, trg_vocab]out = F.softmax(out, dim=-1) # 转为概率分布(沿词汇表维度)# 5.3 选择概率最大的词作为下一个生成词val, ix = out[:, -1].data.topk(1) # 取最后一个位置(第i个词)的Top1概率及索引outputs[i] = ix[0][0] # 将预测索引存入outputs# 5.4 终止条件:若生成<eos>(结束符),停止生成if ix[0][0] == FR_TEXT.vocab.stoi['<eos>']:break# 6. 转换为目标语言字符串(索引→词)# 取从<sos>到<eos>的部分(outputs[:i]),转换为词并拼接return ' '.join([FR_TEXT.vocab.itos[ix] for ix in outputs[:i]])
2. 关键推理逻辑解析
model.eval()与torch.no_grad():推理时无需更新参数,model.eval()关闭 Dropout 等训练专属组件,torch.no_grad()禁用梯度计算,大幅减少内存占用和推理时间。- 自回归生成(Autoregressive Generation):Transformer 解码器的生成逻辑是 “逐词预测”—— 从
<sos>开始,每次用已生成的词预测下一个词,直到生成<eos>(结束符)或达到最大长度max_len,这是序列生成任务(翻译、摘要、对话)的通用逻辑。 - 动态掩码(
trg_mask):每次生成第 i 个词时,trg_mask是 i×i 的下三角矩阵,确保解码器只能关注前 i-1 个已生成的词,避免 “作弊”(如生成第 3 个词时看到第 4 个词)。
(五)完整模型训练与测试实现
# 模型参数定义
d_model = 512
heads = 8
N = 6
src_vocab = len(EN_TEXT.vocab) # 假设EN_TEXT为源语言词汇表
trg_vocab = len(FR_TEXT.vocab) # 假设FR_TEXT为目标语言词汇表
model = Transformer(src_vocab, trg_vocab, d_model, N, heads)
for p in model.parameters():if p.dim() > 1:nn.init.xavier_uniform_(p)optim = torch.optim.Adam(model.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)# 模型训练
def train_model(epochs, print_every=100):model.train()start = time.time()temp = starttotal_loss = 0for epoch in range(epochs):for i, batch in enumerate(train_iter): # 假设train_iter为训练数据迭代器src = batch.English.transpose(0, 1)trg = batch.French.transpose(0, 1)trg_input = trg[:, :-1]targets = trg[:, 1:].contiguous().view(-1)src_mask, trg_mask = create_masks(src, trg_input) # 假设create_masks为掩码创建函数preds = model(src, trg_input, src_mask, trg_mask)optim.zero_grad()loss = F.cross_entropy(preds.view(-1, preds.size(-1)), targets, ignore_index=target_pad) # target_pad为填充索引loss.backward()optim.step()total_loss += loss.data[0]if (i + 1) % print_every == 0:loss_avg = total_loss / print_everyprint("time = %dm, epoch %d, iter = %d, loss = %.3f, %ds per %d iters" % ((time.time() - start) // 60, epoch + 1, i + 1, loss_avg, time.time() - temp, print_every))total_loss = 0temp = time.time()# 模型测试(翻译)
def translate(model, src, max_len=80, custom_string=False):model.eval()if custom_string == True:src = tokenize_en(src) # 假设tokenize_en为源语言分词函数sentence = torch.LongTensor([[EN_TEXT.vocab.stoi[tok] for tok in sentence]]).cuda()src_mask = (src != input_pad).unsqueeze(-2) # input_pad为输入填充索引e_outputs = model.encoder(src, src_mask)outputs = torch.zeros(max_len).type_as(src.data)outputs[0] = torch.LongTensor([FR_TEXT.vocab.stoi['<sos>']]) # <sos>为目标语言起始符for i in range(1, max_len):trg_mask = np.triu(np.ones((1, i, i)), k=1).astype('uint8')trg_mask = torch.from_numpy(trg_mask) == 0trg_mask = trg_mask.cuda()out = model.out(model.decoder(outputs[:i].unsqueeze(0), e_outputs, src_mask, trg_mask))out = F.softmax(out, dim=-1)val, ix = out[:, -1].data.topk(1)outputs[i] = ix[0][0]if ix[0][0] == FR_TEXT.vocab.stoi['<eos>']: # <eos>为目标语言结束符breakreturn ' '.join([FR_TEXT.vocab.itos[ix] for ix in outputs[:i]])
(六)整体逻辑总结:从训练到推理的闭环
- 参数配置:通过超参定义模型结构,初始化参数和优化器,为训练打基础;
- 训练优化:通过迭代批次数据,计算交叉熵损失并反向传播,让模型学习 “源→目标” 的映射规律;
- 推理生成:输入源序列,通过自回归逐词生成目标序列,实现实际任务(如英文到法文的翻译)。
这套流程的核心优势是 “端到端”—— 无需手动设计特征(如传统机器翻译的短语对齐),模型直接从数据中学习语言规律,且通过 Transformer 的注意力机制,能高效捕捉长程依赖和语义关联,是现代序列转换任务的主流实现方式。
十一、完整Python代码实现
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
import time
import numpy as np
import string
import os
from torch.utils.data import Dataset, DataLoader# ---------------------- 1. 基础工具函数(分词、数据生成、词汇表构建) ----------------------
def tokenize_en(text):"""英文分词:去除标点、转小写、按空格分割"""text = text.translate(str.maketrans('', '', string.punctuation))text = text.lower()return [tok for tok in text.split() if tok]def tokenize_fr(text):"""法文分词:逻辑与英文一致"""text = text.translate(str.maketrans('', '', string.punctuation))text = text.lower()return [tok for tok in text.split() if tok]def generate_sample_dataset(data_path="data", filename="train.txt"):"""生成示例英-法翻译数据集"""os.makedirs(data_path, exist_ok=True)sample_data = ["i love natural language processing\tje aime le traitement du langage naturel","deep learning is interesting\tl apprentissage profond est interessant","transformer is a powerful model\tle transformateur est un modele puissant","machine translation helps communication\tla traduction automatique aide la communication","artificial intelligence changes the world\tl intelligence artificielle change le monde","i eat an apple every day\tje mange une pomme tous les jours","the cat is sleeping on the sofa\tle chat dort sur le canape","she likes reading books\telle aime lire des livres","we will go to the park tomorrow\tnous irons au parc demain","this movie is very exciting\tce film est tres passionnant"]with open(os.path.join(data_path, filename), "w", encoding="utf-8") as f:f.write("\n".join(sample_data))print(f"示例数据集已生成:{os.path.join(data_path, filename)}")def build_vocab(sentences, min_freq=1):"""手动构建词汇表"""vocab = {'<pad>': 0, # 填充符(索引0)'<unk>': 1, # 未知词(索引1)'<sos>': 2, # 起始符(索引2)'<eos>': 3 # 结束符(索引3)}freq_dict = {}for sent in sentences:for tok in sent:freq_dict[tok] = freq_dict.get(tok, 0) + 1for tok, freq in freq_dict.items():if freq >= min_freq:vocab[tok] = len(vocab)return vocabdef load_data_and_vocab(data_path="data", filename="train.txt", min_freq=1):"""加载数据并构建词汇表"""en_sentences = []fr_sentences = []with open(os.path.join(data_path, filename), "r", encoding="utf-8") as f:for line in f:line = line.strip()if not line:continueen_text, fr_text = line.split("\t")en_sentences.append(tokenize_en(en_text))fr_sentences.append(tokenize_fr(fr_text))en_vocab = build_vocab(en_sentences, min_freq=min_freq)fr_vocab = build_vocab(fr_sentences, min_freq=min_freq)print(f"\n词汇表信息:")print(f"源语言(英文)词汇数:{len(en_vocab)}(含<sos>/<eos>/<pad>/<unk>)")print(f"目标语言(法文)词汇数:{len(fr_vocab)}")en_stoi = en_vocaben_itos = {idx: tok for tok, idx in en_stoi.items()}fr_stoi = fr_vocabfr_itos = {idx: tok for tok, idx in fr_stoi.items()}return en_sentences, fr_sentences, en_stoi, en_itos, fr_stoi, fr_itos# ---------------------- 2. 自定义数据集与批量加载 ----------------------
class TranslationDataset(Dataset):def __init__(self, en_sentences, fr_sentences, en_stoi, fr_stoi):self.en_sentences = en_sentencesself.fr_sentences = fr_sentencesself.en_stoi = en_stoiself.fr_stoi = fr_stoidef __len__(self):return len(self.en_sentences)def __getitem__(self, idx):# 英文序列:<sos> + 词索引 + <eos>en_idx = [self.en_stoi.get(tok, self.en_stoi['<unk>']) for tok in self.en_sentences[idx]]en_idx = [self.en_stoi['<sos>']] + en_idx + [self.en_stoi['<eos>']]# 法文序列:<sos> + 词索引 + <eos>fr_idx = [self.fr_stoi.get(tok, self.fr_stoi['<unk>']) for tok in self.fr_sentences[idx]]fr_idx = [self.fr_stoi['<sos>']] + fr_idx + [self.fr_stoi['<eos>']]return torch.tensor(en_idx, dtype=torch.long), torch.tensor(fr_idx, dtype=torch.long)def collate_fn(batch, pad_idx=0):"""批量处理:按长度排序+Padding"""# 按英文序列长度降序排序(减少Padding)batch.sort(key=lambda x: len(x[0]), reverse=True)en_batch, fr_batch = zip(*batch)# 计算最大长度max_en_len = max(len(en) for en in en_batch)max_fr_len = max(len(fr) for fr in fr_batch)# Paddingen_padded = []fr_padded = []for en, fr in zip(en_batch, fr_batch):en_pad = torch.full((max_en_len - len(en),), pad_idx, dtype=torch.long)en_padded.append(torch.cat([en, en_pad]))fr_pad = torch.full((max_fr_len - len(fr),), pad_idx, dtype=torch.long)fr_padded.append(torch.cat([fr, fr_pad]))return torch.stack(en_padded), torch.stack(fr_padded)def get_data_loader(en_sentences, fr_sentences, en_stoi, fr_stoi, batch_size=2, pad_idx=0):"""获取数据加载器"""dataset = TranslationDataset(en_sentences, fr_sentences, en_stoi, fr_stoi)loader = DataLoader(dataset,batch_size=batch_size,shuffle=True,collate_fn=lambda x: collate_fn(x, pad_idx=pad_idx))device = torch.device("cuda" if torch.cuda.is_available() else "cpu")print(f"\n数据加载器信息:")print(f"设备:{device} | 批次大小:{batch_size} | 总批次:{len(loader)}")return loader, device# ---------------------- 3. 掩码生成函数(修正维度) ----------------------
def create_masks(src, trg, src_pad_idx=0, trg_pad_idx=0):"""生成维度匹配的掩码:- src_mask: [batch_size, 1, seq_len] → 用于编码器自注意力(过滤Padding)- trg_mask: [batch_size, seq_len, seq_len] → 用于解码器掩码自注意力(下三角+Padding)"""# 1. 源序列掩码(过滤Padding)src_mask = (src != src_pad_idx).unsqueeze(1) # [batch_size, 1, src_seq_len]src_mask = src_mask.to(src.device)# 2. 目标序列掩码(无trg时返回None)if trg is None:return src_mask, None# 目标Padding掩码:[batch_size, 1, trg_seq_len]trg_pad_mask = (trg != trg_pad_idx).unsqueeze(1) # [batch_size, 1, trg_seq_len]# 目标下三角掩码(遮挡未来token):[1, trg_seq_len, trg_seq_len]trg_seq_len = trg.shape[1]trg_subseq_mask = torch.tril(torch.ones((1, trg_seq_len, trg_seq_len), device=trg.device)).bool()# 合并掩码:[batch_size, trg_seq_len, trg_seq_len](广播后适配注意力维度)trg_mask = trg_pad_mask & trg_subseq_mask # [batch_size, trg_seq_len, trg_seq_len]trg_mask = trg_mask.to(trg.device)return src_mask, trg_mask# ---------------------- 4. Transformer核心模型(修正维度+设备兼容) ----------------------
def get_clones(module_class, N, *args, **kwargs):"""复制N个相同模块(无clone依赖)"""return nn.ModuleList([module_class(*args, **kwargs) for _ in range(N)])class PositionalEncoder(nn.Module):def __init__(self, d_model, max_seq_len=80, dropout=0.1):super().__init__()self.d_model = d_model# 预计算位置编码(不训练)pe = torch.zeros(max_seq_len, d_model)pos = torch.arange(0, max_seq_len, dtype=torch.float).unsqueeze(1)div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))pe[:, 0::2] = torch.sin(pos * div_term)pe[:, 1::2] = torch.cos(pos * div_term)pe = pe.unsqueeze(0) # [1, max_seq_len, d_model]self.register_buffer('pe', pe)self.dropout = nn.Dropout(dropout)def forward(self, x):"""x: [batch_size, seq_len, d_model] → 适配x的设备"""x = x * math.sqrt(self.d_model) # 缩放词嵌入# 位置编码移到x的设备(避免硬编码cuda)pe = self.pe[:, :x.size(1), :].requires_grad_(False).to(x.device)x = x + pereturn self.dropout(x)class MultiHeadAttention(nn.Module):def __init__(self, heads, d_model, dropout=0.1):super().__init__()self.d_model = d_modelself.d_k = d_model // heads # 单头维度(必须整除)self.h = heads # 注意力头数# Q/K/V线性变换层self.q_linear = nn.Linear(d_model, d_model)self.k_linear = nn.Linear(d_model, d_model)self.v_linear = nn.Linear(d_model, d_model)self.out = nn.Linear(d_model, d_model)self.dropout = nn.Dropout(dropout)def attention(self, q, k, v, mask=None):"""核心注意力计算:修正掩码维度q/k/v: [batch_size, heads, seq_len_q, d_k] / [batch_size, heads, seq_len_k, d_k]scores: [batch_size, heads, seq_len_q, seq_len_k]mask: 需适配scores维度 → [batch_size, 1, seq_len_q, seq_len_k](广播到heads)"""# 计算注意力分数:Q*K^T / √d_kscores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k) # [batch_size, h, seq_q, seq_k]# 修正掩码维度:确保与scores匹配(关键修复)if mask is not None:# 根据掩码类型调整维度(适配自注意力/交叉注意力)if mask.dim() == 3:# src_mask: [batch_size,1,seq_len] → 扩展为[batch_size,1,1,seq_len](自注意力时seq_q=seq_k)if mask.size(1) == 1:mask = mask.unsqueeze(2) # [batch_size,1,1,seq_len]# trg_mask: [batch_size,seq_len,seq_len] → 扩展为[batch_size,1,seq_len,seq_len]else:mask = mask.unsqueeze(1) # [batch_size,1,seq_len,seq_len]# 掩码广播到scores维度(1→heads)scores = scores.masked_fill(mask == False, -1e9)# Softmax+Dropout+加权Vscores = F.softmax(scores, dim=-1)scores = self.dropout(scores)output = torch.matmul(scores, v) # [batch_size, h, seq_q, d_k]return output, scoresdef forward(self, q, k, v, mask=None):"""前向传播:拆分多头→计算注意力→拼接多头"""bs = q.size(0) # batch_size# 线性变换+拆分多头([batch_size, seq_len, d_model] → [batch_size, h, seq_len, d_k])q = self.q_linear(q).view(bs, -1, self.h, self.d_k).transpose(1, 2)k = self.k_linear(k).view(bs, -1, self.h, self.d_k).transpose(1, 2)v = self.v_linear(v).view(bs, -1, self.h, self.d_k).transpose(1, 2)# 计算注意力attn_output, attn_weights = self.attention(q, k, v, mask)# 拼接多头([batch_size, h, seq_q, d_k] → [batch_size, seq_q, d_model])attn_output = attn_output.transpose(1, 2).contiguous() # [batch_size, seq_q, h, d_k]attn_output = attn_output.view(bs, -1, self.d_model) # 合并h和d_k → d_model# 最终线性变换return self.out(attn_output), attn_weightsclass FeedForward(nn.Module):def __init__(self, d_model, d_ff=2048, dropout=0.1):super().__init__()self.linear1 = nn.Linear(d_model, d_ff) # 升维self.linear2 = nn.Linear(d_ff, d_model) # 降维self.dropout = nn.Dropout(dropout)def forward(self, x):"""x: [batch_size, seq_len, d_model] → 非线性变换"""x = self.dropout(F.relu(self.linear1(x)))x = self.linear2(x)return xclass Norm(nn.Module):def __init__(self, d_model, eps=1e-6):super().__init__()self.alpha = nn.Parameter(torch.ones(d_model)) # 可学习缩放self.bias = nn.Parameter(torch.zeros(d_model)) # 可学习偏移self.eps = eps # 避免分母为0def forward(self, x):"""层归一化:(x-均值)/标准差 * alpha + bias"""mean = x.mean(dim=-1, keepdim=True)std = x.std(dim=-1, keepdim=True)return self.alpha * (x - mean) / (std + self.eps) + self.biasclass EncoderLayer(nn.Module):def __init__(self, d_model, heads, dropout=0.1):super().__init__()self.norm1 = Norm(d_model) # 自注意力后归一化self.norm2 = Norm(d_model) # 前馈网络后归一化self.attn = MultiHeadAttention(heads, d_model, dropout) # 自注意力self.ff = FeedForward(d_model, dropout=dropout) # 前馈网络self.dropout1 = nn.Dropout(dropout)self.dropout2 = nn.Dropout(dropout)def forward(self, x, mask):"""前向传播:自注意力+残差→归一化→前馈+残差→归一化"""# 自注意力+残差连接attn_out, _ = self.attn(x, x, x, mask)x = x + self.dropout1(attn_out)x = self.norm1(x)# 前馈网络+残差连接ff_out = self.ff(x)x = x + self.dropout2(ff_out)x = self.norm2(x)return xclass Encoder(nn.Module):def __init__(self, vocab_size, d_model, N, heads, dropout=0.1):super().__init__()self.embed = nn.Embedding(vocab_size, d_model) # 词嵌入self.pe = PositionalEncoder(d_model, dropout=dropout) # 位置编码self.layers = get_clones(EncoderLayer, N, d_model, heads, dropout) # N层编码器self.norm = Norm(d_model) # 最终归一化def forward(self, src, src_mask):"""src: [batch_size, seq_len] → 词嵌入→位置编码→N层编码→归一化"""x = self.embed(src) # [batch_size, seq_len, d_model]x = self.pe(x) # 注入位置信息for layer in self.layers:x = layer(x, src_mask)return self.norm(x) # [batch_size, seq_len, d_model]class DecoderLayer(nn.Module):def __init__(self, d_model, heads, dropout=0.1):super().__init__()self.norm1 = Norm(d_model) # 掩码自注意力后归一化self.norm2 = Norm(d_model) # 交叉注意力后归一化self.norm3 = Norm(d_model) # 前馈网络后归一化self.attn1 = MultiHeadAttention(heads, d_model, dropout) # 掩码自注意力(目标内部)self.attn2 = MultiHeadAttention(heads, d_model, dropout) # 交叉注意力(目标-源)self.ff = FeedForward(d_model, dropout=dropout) # 前馈网络self.dropout1 = nn.Dropout(dropout)self.dropout2 = nn.Dropout(dropout)self.dropout3 = nn.Dropout(dropout)def forward(self, x, e_out, src_mask, trg_mask):"""前向传播:掩码自注意力→交叉注意力→前馈网络"""# 1. 掩码自注意力(避免关注未来token)attn1_out, _ = self.attn1(x, x, x, trg_mask)x = x + self.dropout1(attn1_out)x = self.norm1(x)# 2. 交叉注意力(关联编码器输出)attn2_out, _ = self.attn2(x, e_out, e_out, src_mask)x = x + self.dropout2(attn2_out)x = self.norm2(x)# 3. 前馈网络ff_out = self.ff(x)x = x + self.dropout3(ff_out)x = self.norm3(x)return xclass Decoder(nn.Module):def __init__(self, vocab_size, d_model, N, heads, dropout=0.1):super().__init__()self.embed = nn.Embedding(vocab_size, d_model) # 词嵌入self.pe = PositionalEncoder(d_model, dropout=dropout) # 位置编码self.layers = get_clones(DecoderLayer, N, d_model, heads, dropout) # N层解码器self.norm = Norm(d_model) # 最终归一化def forward(self, trg, e_out, src_mask, trg_mask):"""trg: [batch_size, seq_len] → 词嵌入→位置编码→N层解码→归一化"""x = self.embed(trg) # [batch_size, seq_len, d_model]x = self.pe(x) # 注入位置信息for layer in self.layers:x = layer(x, e_out, src_mask, trg_mask)return self.norm(x) # [batch_size, seq_len, d_model]class Transformer(nn.Module):def __init__(self, src_vocab, trg_vocab, d_model, N, heads, dropout=0.1):super().__init__()self.encoder = Encoder(src_vocab, d_model, N, heads, dropout) # 编码器self.decoder = Decoder(trg_vocab, d_model, N, heads, dropout) # 解码器self.out = nn.Linear(d_model, trg_vocab) # 输出层(特征→词汇表)def forward(self, src, trg, src_mask, trg_mask):"""前向传播:编码器→解码器→输出层"""e_out = self.encoder(src, src_mask) # 编码器输出:[batch_size, src_seq, d_model]d_out = self.decoder(trg, e_out, src_mask, trg_mask) # 解码器输出:[batch_size, trg_seq, d_model]return self.out(d_out) # [batch_size, trg_seq, trg_vocab]# ---------------------- 5. 模型训练函数(全流程适配) ----------------------
def train_model(model, train_loader, src_pad_idx, trg_pad_idx, device,epochs=30, print_every=2, lr=0.0001):"""训练Transformer模型"""# 初始化优化器(Adam+权重衰减)optimizer = torch.optim.Adam(model.parameters(), lr=lr, betas=(0.9, 0.98), eps=1e-9)model.train() # 训练模式(启用Dropout)start_time = time.time()total_loss = 0total_batches = len(train_loader)print(f"\n开始训练(共{epochs}轮):")for epoch in range(1, epochs + 1):epoch_loss = 0for batch_idx, (src, trg) in enumerate(train_loader, 1):# 数据移到设备src = src.to(device) # [batch_size, src_seq_len]trg = trg.to(device) # [batch_size, trg_seq_len]# 处理目标序列(避免泄露未来token)trg_input = trg[:, :-1] # 解码器输入:去掉最后一个<eos> → [batch_size, trg_seq_len-1]trg_target = trg[:, 1:].contiguous().view(-1) # 真实标签:去掉第一个<sos> → [batch_size*(trg_seq_len-1)]# 生成掩码src_mask, trg_mask = create_masks(src, trg_input, src_pad_idx, trg_pad_idx)# 前向传播outputs = model(src, trg_input, src_mask, trg_mask) # [batch_size, trg_seq_len-1, trg_vocab]outputs_flat = outputs.view(-1, outputs.size(-1)) # 展平→[batch_size*(trg_seq_len-1), trg_vocab]# 计算损失(忽略Padding标签)loss = F.cross_entropy(outputs_flat, trg_target, ignore_index=trg_pad_idx)total_loss += loss.item()epoch_loss += loss.item()# 反向传播与参数更新optimizer.zero_grad() # 清空梯度loss.backward() # 计算梯度optimizer.step() # 更新参数# 打印批次日志if batch_idx % print_every == 0 or batch_idx == total_batches:avg_loss = total_loss / batch_idxelapsed_time = time.time() - start_timeprint(f"轮次{epoch:2d}/{epochs} | 批次{batch_idx:2d}/{total_batches} "f"| 累计平均损失:{avg_loss:.4f} | 耗时:{elapsed_time:.0f}s")# 打印轮次日志avg_epoch_loss = epoch_loss / total_batchesprint(f"轮次{epoch:2d}结束 | 轮次平均损失:{avg_epoch_loss:.4f}")# 保存训练好的模型model_save_path = "transformer_translation.pth"torch.save(model.state_dict(), model_save_path)print(f"\n训练完成!模型已保存至:{model_save_path}")return model# ---------------------- 6. 模型推理函数 ----------------------
def translate_sentence(sentence, model, en_stoi, fr_stoi, fr_itos, src_pad_idx, trg_pad_idx, device="cpu", max_len=50):"""英文→法文翻译(自回归生成)"""model.eval() # 推理模式(关闭Dropout)with torch.no_grad(): # 禁用梯度计算(节省内存)# 1. 预处理输入句子(分词→索引→Tensor)en_toks = tokenize_en(sentence)en_idx = [en_stoi.get(tok, en_stoi['<unk>']) for tok in en_toks]en_idx = [en_stoi['<sos>']] + en_idx + [en_stoi['<eos>']] # 加特殊符号src_tensor = torch.tensor(en_idx, dtype=torch.long, device=device).unsqueeze(0) # [1, src_seq_len]# 2. 生成源掩码src_mask, _ = create_masks(src_tensor, None, src_pad_idx, trg_pad_idx)# 3. 编码器处理源序列e_out = model.encoder(src_tensor, src_mask) # [1, src_seq_len, d_model]# 4. 自回归生成目标序列(从<sos>开始)trg_tensor = torch.tensor([[fr_stoi['<sos>']]], dtype=torch.long, device=device) # [1, 1]for _ in range(max_len - 1):# 生成目标掩码(适配当前生成长度)_, trg_mask = create_masks(src_tensor, trg_tensor, src_pad_idx, trg_pad_idx)# 解码器处理d_out = model.decoder(trg_tensor, e_out, src_mask, trg_mask) # [1, trg_seq_len, d_model]out = model.out(d_out) # [1, trg_seq_len, trg_vocab]# 选择概率最大的词作为下一个tokennext_token_idx = out.argmax(dim=-1)[:, -1].item() # 取最后一个位置的最大概率索引trg_tensor = torch.cat([trg_tensor, torch.tensor([[next_token_idx]], device=device)], dim=1)# 终止条件:生成<eos>或达到最大长度if next_token_idx == fr_stoi['<eos>']:break# 5. 转换为法文句子(索引→词+过滤特殊符号)trg_idx = trg_tensor.squeeze(0).cpu().numpy() # 去除batch维度fr_toks = [fr_itos.get(idx, '<unk>') for idx in trg_idx]# 过滤<sos>/<eos>/<pad>/<unk>fr_toks = [tok for tok in fr_toks if tok not in ['<sos>', '<eos>', '<pad>', '<unk>']]return ' '.join(fr_toks)# ---------------------- 7. 主函数(一键执行全流程) ----------------------
def main():# 步骤1:生成示例数据集generate_sample_dataset()# 步骤2:加载数据与构建词汇表en_sentences, fr_sentences, en_stoi, en_itos, fr_stoi, fr_itos = load_data_and_vocab(data_path="data",filename="train.txt",min_freq=1 # 示例数据少,不过滤低频词)# 步骤3:获取数据加载器src_pad_idx = en_stoi['<pad>'] # 源语言Padding索引trg_pad_idx = fr_stoi['<pad>'] # 目标语言Padding索引train_loader, device = get_data_loader(en_sentences, fr_sentences, en_stoi, fr_stoi,batch_size=2, # 小批次适配CPUpad_idx=src_pad_idx)# 步骤4:初始化模型(适配小数据集)d_model = 64 # 减小维度(原128→64),降低CPU内存占用N = 2 # 2层编码器/解码器(平衡效果与速度)heads = 4 # 4头注意力(d_model必须能被heads整除:64/4=16)dropout = 0.1src_vocab_size = len(en_stoi)trg_vocab_size = len(fr_stoi)model = Transformer(src_vocab=src_vocab_size,trg_vocab=trg_vocab_size,d_model=d_model,N=N,heads=heads,dropout=dropout).to(device)print(f"\n模型初始化完成:")print(f"d_model={d_model} | 层数N={N} | 头数heads={heads} | 设备={device}")print(f"源词汇表大小:{src_vocab_size} | 目标词汇表大小:{trg_vocab_size}")# 步骤5:训练模型model = train_model(model=model,train_loader=train_loader,src_pad_idx=src_pad_idx,trg_pad_idx=trg_pad_idx,device=device,epochs=20, # 示例数据少,减少训练轮次(原30→20)print_every=2)# 步骤6:推理测试(英文→法文)print(f"\n推理测试(英文→法文):")test_sentences = ["i love deep learning","this movie is very exciting","she likes reading books"]for eng_sent in test_sentences:fr_sent = translate_sentence(sentence=eng_sent,model=model,en_stoi=en_stoi,fr_stoi=fr_stoi,fr_itos=fr_itos,src_pad_idx=src_pad_idx,trg_pad_idx=trg_pad_idx,device=device)print(f"英文:{eng_sent} → 法文:{fr_sent}")# ---------------------- 8. 执行主函数 ----------------------
if __name__ == "__main__":main()
程序运行结果如下:
示例数据集已生成:data\train.txt词汇表信息:
源语言(英文)词汇数:49(含<sos>/<eos>/<pad>/<unk>)
目标语言(法文)词汇数:52数据加载器信息:
设备:cpu | 批次大小:2 | 总批次:5模型初始化完成:
d_model=64 | 层数N=2 | 头数heads=4 | 设备=cpu
源词汇表大小:49 | 目标词汇表大小:52开始训练(共20轮):
轮次 1/20 | 批次 2/5 | 累计平均损失:3.8856 | 耗时:1s
轮次 1/20 | 批次 4/5 | 累计平均损失:4.0116 | 耗时:1s
轮次 1/20 | 批次 5/5 | 累计平均损失:3.9777 | 耗时:1s
轮次 1结束 | 轮次平均损失:3.9777
轮次 2/20 | 批次 2/5 | 累计平均损失:13.7242 | 耗时:1s
轮次 2/20 | 批次 4/5 | 累计平均损失:8.7997 | 耗时:1s
轮次 2/20 | 批次 5/5 | 累计平均损失:7.7457 | 耗时:1s
轮次 2结束 | 轮次平均损失:3.7680
轮次 3/20 | 批次 2/5 | 累计平均损失:22.8842 | 耗时:1s
轮次 3/20 | 批次 4/5 | 累计平均损失:13.2530 | 耗时:1s
轮次 3/20 | 批次 5/5 | 累计平均损失:11.3031 | 耗时:2s
轮次 3结束 | 轮次平均损失:3.5574
轮次 4/20 | 批次 2/5 | 累计平均损失:31.8322 | 耗时:2s
轮次 4/20 | 批次 4/5 | 累计平均损失:17.6916 | 耗时:2s
轮次 4/20 | 批次 5/5 | 累计平均损失:14.8090 | 耗时:2s
轮次 4结束 | 轮次平均损失:3.5059
轮次 5/20 | 批次 2/5 | 累计平均损失:40.4270 | 耗时:2s
轮次 5/20 | 批次 4/5 | 累计平均损失:21.8678 | 耗时:2s
轮次 5/20 | 批次 5/5 | 累计平均损失:18.1759 | 耗时:2s
轮次 5结束 | 轮次平均损失:3.3668
轮次 6/20 | 批次 2/5 | 累计平均损失:48.5867 | 耗时:2s
轮次 6/20 | 批次 4/5 | 累计平均损失:26.0022 | 耗时:2s
轮次 6/20 | 批次 5/5 | 累计平均损失:21.4513 | 耗时:3s
轮次 6结束 | 轮次平均损失:3.2755
轮次 7/20 | 批次 2/5 | 累计平均损失:56.7784 | 耗时:3s
轮次 7/20 | 批次 4/5 | 累计平均损失:29.9694 | 耗时:3s
轮次 7/20 | 批次 5/5 | 累计平均损失:24.5837 | 耗时:3s
轮次 7结束 | 轮次平均损失:3.1324
轮次 8/20 | 批次 2/5 | 累计平均损失:64.4394 | 耗时:3s
轮次 8/20 | 批次 4/5 | 累计平均损失:33.8021 | 耗时:3s
轮次 8/20 | 批次 5/5 | 累计平均损失:27.6441 | 耗时:3s
轮次 8结束 | 轮次平均损失:3.0604
轮次 9/20 | 批次 2/5 | 累计平均损失:72.1210 | 耗时:3s
轮次 9/20 | 批次 4/5 | 累计平均损失:37.5371 | 耗时:3s
轮次 9/20 | 批次 5/5 | 累计平均损失:30.5832 | 耗时:3s
轮次 9结束 | 轮次平均损失:2.9390
轮次10/20 | 批次 2/5 | 累计平均损失:79.4633 | 耗时:4s
轮次10/20 | 批次 4/5 | 累计平均损失:41.1580 | 耗时:4s
轮次10/20 | 批次 5/5 | 累计平均损失:33.4612 | 耗时:4s
轮次10结束 | 轮次平均损失:2.8781
轮次11/20 | 批次 2/5 | 累计平均损失:86.3909 | 耗时:4s
轮次11/20 | 批次 4/5 | 累计平均损失:44.5971 | 耗时:4s
轮次11/20 | 批次 5/5 | 累计平均损失:36.2528 | 耗时:4s
轮次11结束 | 轮次平均损失:2.7916
轮次12/20 | 批次 2/5 | 累计平均损失:93.3573 | 耗时:4s
轮次12/20 | 批次 4/5 | 累计平均损失:48.0714 | 耗时:4s
轮次12/20 | 批次 5/5 | 累计平均损失:38.9538 | 耗时:4s
轮次12结束 | 轮次平均损失:2.7010
轮次13/20 | 批次 2/5 | 累计平均损失:100.0205 | 耗时:5s
轮次13/20 | 批次 4/5 | 累计平均损失:51.2925 | 耗时:5s
轮次13/20 | 批次 5/5 | 累计平均损失:41.5632 | 耗时:5s
轮次13结束 | 轮次平均损失:2.6094
轮次14/20 | 批次 2/5 | 累计平均损失:106.4660 | 耗时:5s
轮次14/20 | 批次 4/5 | 累计平均损失:54.4476 | 耗时:5s
轮次14/20 | 批次 5/5 | 累计平均损失:44.0699 | 耗时:5s
轮次14结束 | 轮次平均损失:2.5067
轮次15/20 | 批次 2/5 | 累计平均损失:112.5525 | 耗时:5s
轮次15/20 | 批次 4/5 | 累计平均损失:57.4960 | 耗时:5s
轮次15/20 | 批次 5/5 | 累计平均损失:46.5071 | 耗时:5s
轮次15结束 | 轮次平均损失:2.4372
轮次16/20 | 批次 2/5 | 累计平均损失:118.6897 | 耗时:5s
轮次16/20 | 批次 4/5 | 累计平均损失:60.4485 | 耗时:5s
轮次16/20 | 批次 5/5 | 累计平均损失:48.8325 | 耗时:6s
轮次16结束 | 轮次平均损失:2.3254
轮次17/20 | 批次 2/5 | 累计平均损失:124.3254 | 耗时:6s
轮次17/20 | 批次 4/5 | 累计平均损失:63.2719 | 耗时:6s
轮次17/20 | 批次 5/5 | 累计平均损失:51.1066 | 耗时:6s
轮次17结束 | 轮次平均损失:2.2741
轮次18/20 | 批次 2/5 | 累计平均损失:130.0828 | 耗时:6s
轮次18/20 | 批次 4/5 | 累计平均损失:66.1580 | 耗时:6s
轮次18/20 | 批次 5/5 | 累计平均损失:53.3464 | 耗时:6s
轮次18结束 | 轮次平均损失:2.2398
轮次19/20 | 批次 2/5 | 累计平均损失:135.4422 | 耗时:6s
轮次19/20 | 批次 4/5 | 累计平均损失:68.8065 | 耗时:6s
轮次19/20 | 批次 5/5 | 累计平均损失:55.4748 | 耗时:7s
轮次19结束 | 轮次平均损失:2.1284
轮次20/20 | 批次 2/5 | 累计平均损失:140.7410 | 耗时:7s
轮次20/20 | 批次 4/5 | 累计平均损失:71.3626 | 耗时:7s
轮次20/20 | 批次 5/5 | 累计平均损失:57.5309 | 耗时:7s
轮次20结束 | 轮次平均损失:2.0561训练完成!模型已保存至:transformer_translation.pth推理测试(英文→法文):
英文:i love deep learning → 法文:je mange une pomme tous les jours
英文:this movie is very exciting → 法文:ce film est tres passionnant
英文:she likes reading books → 法文:elle aime lire des livres
十二、总结
本文详细介绍了Transformer模型的核心架构及其实现过程。Transformer基于自注意力机制,通过编码器-解码器结构实现序列转换任务。编码器由多头自注意力和前馈网络组成,解码器在此基础上增加了交叉注意力层。模型通过位置编码处理序列顺序,并结合残差连接和层归一化优化训练。文章提供了完整的Python实现代码,涵盖数据预处理、模型构建、训练及推理流程。实验结果表明,Transformer能有效完成机器翻译任务,其并行计算能力和长程依赖建模优势使其成为现代NLP任务的基础架构。
