目录
一、【Attention is all you need】代码实现
Ⅱ、位置前馈网络 PositionwiseFeedForward
② get_subsequent_mask:生成后续位置掩码
二、利用定义的Transformer架构模型完成文章标题生成任务
你曾一个人走过的路,都是最勇敢的征途
—— 25.3.19
一、【Attention is all you need】代码实现
1.Modules.py
Ⅰ、缩放点积注意力机制类 ScaledDotProductAttention
① 类的定义
继承自nn.Module,是PyTorch中定义神经网络模块的标准方式,该类实现了缩放点积注意力机制
class ScaledDotProductAttention(nn.Module):
''' Scaled Dot-Product Attention '''
② 初始化方法
temperature:缩放因子,通常为键向量维度dk的平方根,用于缩放点积结果,防止梯度消失或爆炸。
attn_dropout:Dropout概率,用于正则化,防止过拟合。
self.dropout():对 softmax 后的注意力权重进行随机 dropout,防止过拟合。
nn.Dropout():PyTorch中的一个模块,用于在训练神经网络时随机丢弃部分神经元,以防止过拟合。
参数名 | 类型 | 默认值 | 说明 |
---|---|---|---|
p | float | 0.5 | 每个神经元被丢弃的概率,取值范围为[0, 1) 。 |
inplace | bool | False | 是否在原地修改输入数据。如果为True ,会直接修改输入数据以节省内存。 |
def __init__(self, temperature, attn_dropout=0.1):
super().__init__()
self.temperature = temperature
self.dropout = nn.Dropout(attn_dropout)
③ 前向传播
代码运行流程
forward(q, k, v, mask=None)
├── 计算缩放点积
│ ├── q / self.temperature # 缩放查询向量
│ ├── k.transpose(2, 3) # 转置键向量
│ └── torch.matmul(q / self.temperature, k.transpose(2, 3)) # 计算点积
├── 应用掩码(如果mask不为None)
│ └── attn.masked_fill(mask == 0, -1e9) # 将掩码为0的位置置为极小值
├── 计算Softmax并应用Dropout
│ ├── F.softmax(attn, dim=-1) # 对点积结果进行Softmax归一化
│ └── self.dropout(attn) # 对Softmax结果应用Dropout
├── 加权求和
│ └── torch.matmul(attn, v) # 使用注意力权重对值向量加权求和
└── 返回结果
├── output # 加权求和后的输出
└── attn # 注意力权重矩阵
q:查询(Query)向量,形状为[batch_size, num_heads, seq_len, d_k]
k:键(Key)向量,形状为[batch_size, num_heads, seq_len, d_k]
v:值(Value)向量,形状为[batch_size, num_heads, seq_len, d_v]
mask:掩码矩阵,用于屏蔽某些位置的注意力得分,形状为[batch_size, seq_len, seq_len]
attn:注意力分数矩阵,通过 q
和 k
的点积计算得到
self.temperature:缩放因子,通常为键向量维度dk的平方根,用于缩放点积结果,防止梯度消失或爆炸。
output: 最终的注意力加权输出,通过 attn
和 v
的矩阵乘法计算
self.dropout():对 softmax 后的注意力权重进行随机 dropout,防止过拟合。
torch.matmul():PyTorch中的矩阵乘法函数,用于计算两个张量的矩阵乘积。它支持广播机制,可以处理不同维度的张量。
参数名 | 类型 | 说明 |
---|---|---|
input | Tensor | 第一个输入张量。 |
other | Tensor | 第二个输入张量。 |
transpose():PyTorch中的转置函数,用于交换张量的两个维度。
参数名 | 类型 | 说明 |
---|---|---|
input | Tensor | 输入张量。 |
dim0 | int | 要交换的第一个维度。 |
dim1 | int | 要交换的第二个维度。 |
masked_fill():根据布尔掩码(mask
)将张量中指定位置的元素替换为特定值(value
)。常用于屏蔽无效数据(如填充符)或控制注意力范围(如 Transformer 模型中的因果掩码)
参数名 | 类型 | 必填 | 描述 | 示例 |
---|---|---|---|---|
mask |
| 是 | 布尔掩码张量,形状需与输入张量一致或可广播。True 表示需要填充的位置。 | mask = tensor([[False, True], [True, False]]) |
value | float 或 Tensor | 是 | 填充到掩码标记位置的值(如 -1e9 或 0 )。 | value = -1e9 |
F.softmax(): 将输入张量归一化为概率分布,使每个元素值在 [0, 1]
范围内且所有元素和为 1
。广泛用于多分类任务(如语言模型的词汇概率预测)
参数名 | 类型 | 必填 | 描述 | 示例 |
---|---|---|---|---|
input | Tensor | 是 | 输入张量,通常为模型输出(如未归一化的 logits) | input = torch.tensor([1.0, 2.0, 3.0]) |
dim | int | 是 | 计算归一化的维度(如 dim=1 表示对每行计算) | dim=1 |
dtype | torch.dtype | 否 | 输出张量的数据类型,用于防止数值溢出 | dtype=torch.float32 |
def forward(self, q, k, v, mask=None):
attn = torch.matmul(q / self.temperature, k.transpose(2, 3))
if mask is not None:
attn = attn.masked_fill(mask == 0, -1e9)
attn = self.dropout(F.softmax(attn, dim=-1))
output = torch.matmul(attn, v)
return output, attn
④ 完整代码
import torch
import torch.nn as nn
import torch.nn.functional as F
__author__ = "Yu-Hsiang Huang"
class ScaledDotProductAttention(nn.Module):
''' Scaled Dot-Product Attention '''
def __init__(self, temperature, attn_dropout=0.1):
super().__init__()
self.temperature = temperature
self.dropout = nn.Dropout(attn_dropout)
def forward(self, q, k, v, mask=None):
attn = torch.matmul(q / self.temperature, k.transpose(2, 3))
if mask is not None:
attn = attn.masked_fill(mask == 0, -1e9)
attn = self.dropout(F.softmax(attn, dim=-1))
output = torch.matmul(attn, v)
return output, attn
2.SubLayers.py
Ⅰ、多头注意力机制实现类 MultiHeadAttention
① 类的定义
继承自nn.Module,是PyTorch中定义神经网络模块的标准方式,该类实现了多头注意力机制
class MultiHeadAttention(nn.Module):
''' Multi-Head Attention module '''
② 初始化方法
n_head:注意力头的数量
d_model:输入和输出的维度
d_k:每个注意力头中查询(Query)和键(Key)的维度
d_v:每个注意力头中值(Value)的维度
dropout:Dropout概率,用于正则化
self.w_qs:将输入投影到查询的线性变换层
self.w_ks:将输入投影到键的线性变换层
self.w_vs:将输入投影到值的线性变换层
self.fc:将多头注意力的输出投影回原始维度的线性变换层
self.attention:缩放点积注意力机制(ScaledDotProductAttention
)的实例
self.dropout:Dropout层,用于正则化
self.layer_norm:层归一化(Layer Normalization),用于稳定训练过程
ScaledDotProductAttention():缩放点积注意力机制类
nn.Linear():全连接层,执行线性变换 y = xW^T + b
,用于神经网络的前向传播
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
in_features | int | 输入特征维度 | 必选 |
out_features | int | 输出特征维度 | 必选 |
bias | bool | 是否添加偏置项 | True |
nn.Dropout():随机丢弃部分神经元输出(置零),防止过拟合(仅在训练模式下生效)
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
p | float | 神经元被丢弃的概率(0 ≤ p < 1) | 必选 |
inplace | bool | 是否原地修改输入张量 | False |
nn.LayerNorm():层归一化,对输入特征在指定维度进行标准化(均值0、方差1),提升训练稳定性
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
normalized_shape | int/list | 需归一化的维度(如输入最后一维) | 必选 |
eps | float | 数值稳定性系数(防止除零) | 1e-5 |
def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
super().__init__()
self.n_head = n_head
self.d_k = d_k
self.d_v = d_v
self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)
self.dropout = nn.Dropout(dropout)
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
③ 前向传播
代码运行流程
forward(q, k, v, mask=None)
├── 初始化变量
│ ├── d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
│ ├── sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1)
│ └── residual = q
├── 线性变换并分头
│ ├── q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
│ ├── k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
│ └── v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)
├── 转置用于注意力计算
│ └── q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
├── 应用掩码(如果mask不为None)
│ └── mask = mask.unsqueeze(1)
├── 计算注意力
│ └── q, attn = self.attention(q, k, v, mask=mask)
├── 转置并合并多头
│ └── q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
├── 线性变换并应用Dropout
│ └── q = self.dropout(self.fc(q))
├── 残差连接
│ └── q += residual
├── 层归一化
│ └── q = self.layer_norm(q)
└── 返回结果
├── q # 多头注意力的输出
└── attn # 注意力权重矩阵
q:查询(Query)向量,形状为[batch_size, num_heads, seq_len, d_k],
用于生成注意力权重
k:键(Key)向量,形状为[batch_size, num_heads, seq_len, d_k],
用于与 q
计算相似度得分。
v:值(Value)向量,形状为[batch_size, num_heads, seq_len, d_v],
存储待聚合的语义信息。
mask:掩码矩阵,用于屏蔽某些位置的注意力得分,形状为[batch_size, seq_len, seq_len]
d_k:每个注意力头中查询(Query)和键(Key)的维度
d_v:每个注意力头中值(Value)的维度
n_head:注意力头的数量
sz_b:批次大小(batch_size
)
len_q:查询序列长度
len_k:键序列长度
len_v:值序列长度
residual:保留残差连接的原始输入,用于后续与注意力输出相加
self.w_qs:将输入投影到查询的线性变换层
self.w_ks:将输入投影到键的线性变换层
self.w_vs:将输入投影到值的线性变换层
attn:注意力权重矩阵,表示输入序列中不同位置之间的相关性强度。它通过计算 query
和 key
的相似度,并经 Softmax 归一化后得到,最终用于对 value
进行加权求和,生成自注意力输出。
self.attention():缩放点积注意力机制(ScaledDotProductAttention
)的实例
self.dropout():Dropout层,用于正则化
self.layer_norm():层归一化(Layer Normalization),用于稳定训练过程
transpose():交换张量的两个指定维度(行列转置),常用于调整数据维度顺序
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
input | Tensor | 输入张量 | 必选 |
dim0 | int | 要交换的第一个维度索引 | 必选 |
dim1 | int | 要交换的第二个维度索引 | 必选 |
unsqueeze():在指定位置插入一个大小为1的维度,用于扩展张量维度以适应计算需求
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
input | Tensor | 输入张量 | 必选 |
dim | int | 插入新维度的位置(支持负数) | 必选 |
contiguous():确保张量在内存中连续存储,避免因转置等操作导致的非连续性问题(如后续调用 view()
需连续存储)
view():调整张量的形状(元素总数不变),常用于维度变换或展平数据,-1
会根据张量的总元素个数和前两个维度 sz_b
和 len_q
自动计算出第三个维度的大小。
参数 | 类型/可变参数 | 说明 | 默认值 |
---|---|---|---|
*shape | int | 目标形状(可用 -1 自动推导) | 必选 |
def forward(self, q, k, v, mask=None):
d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1)
residual = q
# Pass through the pre-attention projection: b x lq x (n*dv)
# Separate different heads: b x lq x n x dv
q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)
# Transpose for attention dot product: b x n x lq x dv
q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
if mask is not None:
mask = mask.unsqueeze(1) # For head axis broadcasting.
q, attn = self.attention(q, k, v, mask=mask)
# Transpose to move the head dimension back: b x lq x n x dv
# Combine the last two dimensions to concatenate all the heads together: b x lq x (n*dv)
q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
q = self.dropout(self.fc(q))
q += residual
q = self.layer_norm(q)
return q, attn
③ 完整代码
class MultiHeadAttention(nn.Module):
''' Multi-Head Attention module '''
def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
super().__init__()
self.n_head = n_head
self.d_k = d_k
self.d_v = d_v
self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)
self.dropout = nn.Dropout(dropout)
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
def forward(self, q, k, v, mask=None):
d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1)
residual = q
# Pass through the pre-attention projection: b x lq x (n*dv)
# Separate different heads: b x lq x n x dv
q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)
# Transpose for attention dot product: b x n x lq x dv
q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
if mask is not None:
mask = mask.unsqueeze(1) # For head axis broadcasting.
q, attn = self.attention(q, k, v, mask=mask)
# Transpose to move the head dimension back: b x lq x n x dv
# Combine the last two dimensions to concatenate all the heads together: b x lq x (n*dv)
q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
q = self.dropout(self.fc(q))
q += residual
q = self.layer_norm(q)
return q, attn
Ⅱ、位置前馈网络实现类 PositionwiseFeedForward
① 类的定义
继承自 PyTorch 的 nn.Module
,用于实现位置前馈神经网络(Positionwise Feed-Forward Network)
class PositionwiseFeedForward(nn.Module):
''' A two-feed-forward-layer module '''
② 初始化方法
d_in:输入特征维度,通常与模型主维度 d_model
一致(例如 512),表示每个词向量的长度
d_hid:隐藏层(中间层)的维度,扩展模型表达能力,通常设置为 d_in
的 4 倍(如 d_hid=2048
),通过非线性变换增强特征交互
dropout:随机丢弃神经元的概率(默认 0.1)
self.w_1:将输入从 d_in
维度映射到 d_hid
,激活函数(如 ReLU)引入非线性
self.w_2:将隐藏层维度压缩回 d_in
,保持输入输出维度一致,便于残差连接
self.layer_norm:对输出进行层归一化(Layer Normalization)
self.dropout:通常在激活函数(如 ReLU)后调用,随机丢弃神经元,防止过拟合
nn.Linear():全连接层,执行线性变换 y = xW^T + b
,用于神经网络的前向传播
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
in_features | int | 输入特征维度 | 必选 |
out_features | int | 输出特征维度 | 必选 |
bias | bool | 是否添加偏置项 | True |
nn.Dropout():随机丢弃部分神经元输出(置零),防止过拟合(仅在训练模式下生效)
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
p | float | 神经元被丢弃的概率(0 ≤ p < 1) | 必选 |
inplace | bool | 是否原地修改输入张量 | False |
nn.LayerNorm():层归一化,对输入特征在指定维度进行标准化(均值0、方差1),提升训练稳定性
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
normalized_shape | int/list | 需归一化的维度(如输入最后一维) | 必选 |
eps | float | 数值稳定性系数(防止除零) | 1e-5 |
def __init__(self, d_in, d_hid, dropout=0.1):
super().__init__()
self.w_1 = nn.Linear(d_in, d_hid) # position-wise
self.w_2 = nn.Linear(d_hid, d_in) # position-wise
self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
self.dropout = nn.Dropout(dropout)
③ 前向传播
代码运行流程
forward(x)
├── 1. 保存残差连接: residual = x
├── 2. 前馈网络计算:
│ ├── a. 第一层线性变换: x = self.w_1(x)
│ ├── b. 激活函数: x = F.relu(x)
│ ├── c. 第二层线性变换: x = self.w_2(x)
│ └── d. Dropout 正则化: x = self.dropout(x)
├── 3. 残差连接: x += residual
└── 4. 层归一化: x = self.layer_norm(x)
residual:原始输入 x
的副本,用于与处理后的张量相加。
x:输入到前馈神经网络的张量,通常来自自注意力层或上一层的输出。
self.w_1():将输入从 d_in
维度映射到 d_hid
,激活函数(如 ReLU)引入非线性
self.w_2():将隐藏层维度压缩回 d_in
,保持输入输出维度一致,便于残差连接
self.dropout():通常在激活函数(如 ReLU)后调用,随机丢弃神经元,防止过拟合
self.layer_norm():对输出进行层归一化(Layer Normalization)
F.relu():修正线性单元(Rectified Linear Unit)激活函数,其核心作用是为神经网络引入非线性特征,解决线性模型的表达能力局限性问题
参数名 | 类型 | 必填 | 描述 | 默认值 |
---|---|---|---|---|
input | Tensor | 是 | 输入张量,支持任意维度(如 (batch_size, channels, height, width) ) | 无 |
inplace | bool | 否 | 是否执行原地操作(直接修改输入张量,减少内存占用) |
def forward(self, x):
residual = x
x = self.w_2(F.relu(self.w_1(x)))
x = self.dropout(x)
x += residual
x = self.layer_norm(x)
return x
④ 完整代码
class PositionwiseFeedForward(nn.Module):
''' A two-feed-forward-layer module '''
def __init__(self, d_in, d_hid, dropout=0.1):
super().__init__()
self.w_1 = nn.Linear(d_in, d_hid) # position-wise
self.w_2 = nn.Linear(d_hid, d_in) # position-wise
self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
residual = x
x = self.w_2(F.relu(self.w_1(x)))
x = self.dropout(x)
x += residual
x = self.layer_norm(x)
return x
Ⅲ、完整代码
''' Define the sublayers in encoder/decoder layer '''
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
from .Modules import ScaledDotProductAttention
__author__ = "Yu-Hsiang Huang"
class MultiHeadAttention(nn.Module):
''' Multi-Head Attention module '''
def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
super().__init__()
self.n_head = n_head
self.d_k = d_k
self.d_v = d_v
self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)
self.dropout = nn.Dropout(dropout)
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
def forward(self, q, k, v, mask=None):
d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1)
residual = q
# Pass through the pre-attention projection: b x lq x (n*dv)
# Separate different heads: b x lq x n x dv
q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)
# Transpose for attention dot product: b x n x lq x dv
q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
if mask is not None:
mask = mask.unsqueeze(1) # For head axis broadcasting.
q, attn = self.attention(q, k, v, mask=mask)
# Transpose to move the head dimension back: b x lq x n x dv
# Combine the last two dimensions to concatenate all the heads together: b x lq x (n*dv)
q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
q = self.dropout(self.fc(q))
q += residual
q = self.layer_norm(q)
return q, attn
class PositionwiseFeedForward(nn.Module):
''' A two-feed-forward-layer module '''
def __init__(self, d_in, d_hid, dropout=0.1):
super().__init__()
self.w_1 = nn.Linear(d_in, d_hid) # position-wise
self.w_2 = nn.Linear(d_hid, d_in) # position-wise
self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
residual = x
x = self.w_2(F.relu(self.w_1(x)))
x = self.dropout(x)
x += residual
x = self.layer_norm(x)
return x
3.Layers.py
Ⅰ、编码层 EncoderLayer
① 类的定义
继承自 PyTorch 的 nn.Module
类,EncoderLayer
类代表了 Transformer 编码器中的一个基本层,其主要作用是对输入序列进行特征提取和转换,通过多头自注意力机制捕获序列中元素之间的依赖关系,再利用前馈神经网络进行非线性变换,从而增强模型对序列信息的理解和表达能力。
class EncoderLayer(nn.Module):
''' Compose with two layers '''
② 初始化方法
d_model:模型的主维度,表示输入/输出向量的统一维度(如词嵌入维度)
d_inner:前馈网络(Feed Forward Network, FFN)的隐藏层维度
n_head:多头注意力机制中的头数
d_k:每个注意力头中查询(Query)和键(Key)的维度
d_v:每个注意力头中值(Value)向量的维度
dropout:随机失活概率,随机屏蔽神经元的概率(默认0.1)
self.slf_attn:实现多头注意力机制,计算输入序列内部的全局依赖关系
self.pos_ffn:实现位置感知的前馈网络,对每个位置的向量独立进行非线性变换
MultiHeadAttention():创建多头注意力机制类实体
PositionwiseFeedForward():创建位置前馈网络类实体
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(EncoderLayer, self).__init__()
self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
③ 前向传播
代码运行流程
forward(enc_input, slf_attn_mask)
├── 1. 自注意力计算(Multi-Head Self-Attention)
│ ├── a. 输入参数:
│ │ ├── query = enc_input
│ │ ├── key = enc_input
│ │ ├── value = enc_input
│ │ └── mask = slf_attn_mask(可选)
│ ├── b. 多头注意力机制:
│ │ ├── 拆分头: 将 enc_input 分割为多个头(维度变换)
│ │ ├── 计算注意力权重: Q·K^T / sqrt(d_k)
│ │ ├── 应用掩码: 若提供 slf_attn_mask,抑制无效位置
│ │ ├── Softmax 归一化: 生成注意力概率分布
│ │ └── 加权求和: 注意力权重与 Value 相乘,合并多头输出
│ └── c. 输出:
│ ├── enc_output: 注意力后的特征表示(维度与输入一致)
│ └── enc_slf_attn: 注意力权重矩阵(用于可视化或分析)
├── 2. 前馈网络(Positionwise Feed-Forward Network, FFN)
│ ├── a. 线性变换: 将 enc_output 从 d_model 映射到 d_inner(升维)
│ ├── b. 激活函数: 通常为 ReLU
│ ├── c. 线性变换: 从 d_inner 映射回 d_model(降维)
│ └── d. Dropout: 随机屏蔽部分神经元(若启用)
└── 3. 最终输出:
├── enc_output: 经过自注意力和前馈网络处理的特征
└── enc_slf_attn: 保留的注意力权重
enc_input:编码器输入,形状为 (batch_size, seq_len, d_model)
,包含经过词嵌入和位置编码的特征向量
slf_attn_mask:自注意力掩码,形状为 (batch_size, seq_len, seq_len)
,用于屏蔽无效位置(如填充符或未来信息)
enc_output:编码器输出,形状与 enc_input
相同
enc_slf_attn:自注意力权重矩阵,形状为 (batch_size, n_head, seq_len, seq_len)
self.slf_attn():多头自注意力模块,接受 enc_input
作为 Q/K/V,计算注意力权重并聚合 Value 向量
self.pos_ffn():位置前馈网络,包含两个线性层和 ReLU 激活函数,公式为 FFN(x) = max(0, xW1 + b1)W2 + b2
def forward(self, enc_input, slf_attn_mask=None):
enc_output, enc_slf_attn = self.slf_attn(
enc_input, enc_input, enc_input, mask=slf_attn_mask)
enc_output = self.pos_ffn(enc_output)
return enc_output, enc_slf_attn
④ 完整代码
class EncoderLayer(nn.Module):
''' Compose with two layers '''
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(EncoderLayer, self).__init__()
self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
def forward(self, enc_input, slf_attn_mask=None):
enc_output, enc_slf_attn = self.slf_attn(
enc_input, enc_input, enc_input, mask=slf_attn_mask)
enc_output = self.pos_ffn(enc_output)
return enc_output, enc_slf_attn
Ⅱ、解码层 DecoderLayer
① 类的定义
继承自 PyTorch 的 nn.Module
类,DecoderLayer
类代表了 Transformer 解码器中的一个基本层,其主要作用是在生成目标序列时,综合考虑已生成的目标序列信息(通过自注意力层)、编码器输出的源序列信息(通过编码器 - 解码器注意力层),并利用前馈神经网络进行非线性变换,从而逐步生成高质量的目标序列。
class DecoderLayer(nn.Module):
''' Compose with three layers '''
② 初始化方法
d_model:模型的主维度,表示输入/输出向量的统一维度(如词嵌入维度)
d_inner:前馈网络(Feed Forward Network, FFN)的隐藏层维度
n_head:多头注意力机制中的头数
d_k:每个注意力头中查询(Query)和键(Key)的维度
d_v:每个注意力头中值(Value)向量的维度
dropout:随机失活概率,随机屏蔽神经元的概率(默认0.1)
self.slf_attn:多头自注意力模块,接受 enc_input
作为 Q/K/V,计算注意力权重并聚合 Value 向量
self.enc_attn:编码器-解码器注意力,使解码器关注编码器输出(即源序列的语义表示
self.pos_ffn:位置前馈网络,包含两个线性层和 ReLU 激活函数,公式为 FFN(x) = max(0, xW1 + b1)W2 + b2
MultiHeadAttention():创建多头注意力机制类实体
PositionwiseFeedForward():创建位置前馈网络类实体
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(DecoderLayer, self).__init__()
self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.enc_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
② 前向传播
代码运行流程
forward(dec_input, enc_output, slf_attn_mask, dec_enc_attn_mask)
├── 1. 掩码自注意力层(Masked Multi-Head Self-Attention)
│ ├── a. 输入参数:
│ │ ├── query = dec_input(目标序列嵌入 + 位置编码)
│ │ ├── key = dec_input
│ │ ├── value = dec_input
│ │ └── mask = slf_attn_mask(因果掩码,屏蔽未来位置)
│ ├── b. 处理流程:
│ │ ├── 拆分多头: 将输入分割为 `n_head` 个并行头(如 8 头)
│ │ ├── 计算 Q·K^T / √d_k: 点积缩放防止梯度爆炸
│ │ ├── 应用因果掩码: 将未来位置的注意力得分设为负无穷(Softmax 后权重为 0)
│ │ ├── Softmax 归一化: 生成概率分布
│ │ └── 加权求和 Value: 合并多头输出并线性投影
│ └── c. 输出:
│ ├── dec_output: 掩码自注意力后的特征(维度不变,如 512)
│ └── dec_slf_attn: 注意力权重矩阵(形状:batch × n_head × seq_len × seq_len)
├── 2. 交叉注意力层(Encoder-Decoder Multi-Head Attention)
│ ├── a. 输入参数:
│ │ ├── query = dec_output(来自步骤1的输出)
│ │ ├── key = enc_output(编码器的最终输出)
│ │ ├── value = enc_output
│ │ └── mask = dec_enc_attn_mask(屏蔽编码器填充符)
│ ├── b. 处理流程:
│ │ ├── 计算交叉注意力: Query 来自解码器,Key/Value 来自编码器
│ │ ├── 应用编码器掩码: 过滤编码器输出的无效位置(如填充符)
│ │ └── 权重聚合: 生成对齐编码器语义的特征
│ └── c. 输出:
│ ├── dec_output: 对齐编码器后的特征(维度不变)
│ └── dec_enc_attn: 解码器-编码器注意力权重(用于可视化对齐关系)
├── 3. 前馈网络层(Position-wise Feed-Forward Network, FFN)
│ ├── a. 结构:
│ │ ├── 第一层线性变换: d_model → d_inner(如 512 → 2048)
│ │ ├── ReLU 激活: 引入非线性
│ │ ├── 第二层线性变换: d_inner → d_model
│ │ └── Dropout: 随机屏蔽部分神经元(若启用)
│ └── b. 输出:
│ └── dec_output: 最终解码器层输出(维度保持 d_model)
└── 4. 返回结果:
├── dec_output: 传递给下一解码器层或输出层(如分类器)
├── dec_slf_attn: 目标序列内部依赖关系的可视化数据
└── dec_enc_attn: 源-目标序列对齐关系的分析依据
dec_input:解码器输入,目标序列的嵌入表示(含位置编码),形状为 (batch_size, seq_len, d_model)
enc_output:编码器输出,源序列的上下文表示,形状与 dec_input
相同,作为交叉注意力的 Key/Value 来源
slf_attn_mask:自注意力掩码,防止解码器关注未来位置(如生成任务中当前词不可依赖后续词),形状为 (batch_size, seq_len, seq_len)
dec_enc_attn_mask:交叉注意力掩码,屏蔽编码器输出中的无效位置(如填充符),形状与 slf_attn_mask
相同
dec_output:解码器中间输出,依次通过自注意力、交叉注意力和前馈网络处理后的特征表示
dec_slf_attn:自注意力权重矩阵,反映解码器输入序列内部的关联强度,形状为 (batch_size, n_head, seq_len, seq_len)
,用于可视化模型关注模式
dec_enc_attn:交叉注意力权重矩阵,反映解码器与编码器输出的对齐关系,形状与 dec_slf_attn
相同
self.slf_attn():通过 slf_attn_mask
将未来位置的注意力得分设为极小值(如 -1e9
),经 Softmax 后权重趋近于零,确保自回归生成时仅依赖历史信息
self.enc_attn():通过 dec_enc_attn_mask
过滤编码器输出中的无效位置(如填充符),例如机器翻译中忽略源句子的填充符
self.pos_ffn():增强局部非线性表达能力,公式为 FFN(x) = ReLU(xW1 + b1)W2 + b2
def forward(
self, dec_input, enc_output,
slf_attn_mask=None, dec_enc_attn_mask=None):
dec_output, dec_slf_attn = self.slf_attn(
dec_input, dec_input, dec_input, mask=slf_attn_mask)
dec_output, dec_enc_attn = self.enc_attn(
dec_output, enc_output, enc_output, mask=dec_enc_attn_mask)
dec_output = self.pos_ffn(dec_output)
return dec_output, dec_slf_attn, dec_enc_attn
③ 完整代码
class DecoderLayer(nn.Module):
''' Compose with three layers '''
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(DecoderLayer, self).__init__()
self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.enc_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
def forward(
self, dec_input, enc_output,
slf_attn_mask=None, dec_enc_attn_mask=None):
dec_output, dec_slf_attn = self.slf_attn(
dec_input, dec_input, dec_input, mask=slf_attn_mask)
dec_output, dec_enc_attn = self.enc_attn(
dec_output, enc_output, enc_output, mask=dec_enc_attn_mask)
dec_output = self.pos_ffn(dec_output)
return dec_output, dec_slf_attn, dec_enc_attn
Ⅲ、完整代码
''' Define the Layers '''
import torch.nn as nn
import torch
from .SubLayers import MultiHeadAttention, PositionwiseFeedForward
# Layers.py
# 后续代码...
__author__ = "Yu-Hsiang Huang"
class EncoderLayer(nn.Module):
''' Compose with two layers '''
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(EncoderLayer, self).__init__()
self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
def forward(self, enc_input, slf_attn_mask=None):
enc_output, enc_slf_attn = self.slf_attn(
enc_input, enc_input, enc_input, mask=slf_attn_mask)
enc_output = self.pos_ffn(enc_output)
return enc_output, enc_slf_attn
class DecoderLayer(nn.Module):
''' Compose with three layers '''
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(DecoderLayer, self).__init__()
self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.enc_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
def forward(
self, dec_input, enc_output,
slf_attn_mask=None, dec_enc_attn_mask=None):
dec_output, dec_slf_attn = self.slf_attn(
dec_input, dec_input, dec_input, mask=slf_attn_mask)
dec_output, dec_enc_attn = self.enc_attn(
dec_output, enc_output, enc_output, mask=dec_enc_attn_mask)
dec_output = self.pos_ffn(dec_output)
return dec_output, dec_slf_attn, dec_enc_attn
4.Models.py
Ⅰ、位置编码 PositionalEncoding
代码运行流程
PositionalEncoding 类
|
|-- __init__ 方法
| |-- 调用父类构造函数
| | |-- super(PositionalEncoding, self).__init__()
| |-- 生成并注册位置编码表
| |-- self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid))
| |-- _get_sinusoid_encoding_table 方法
| |-- get_position_angle_vec 函数
| |-- 计算每个位置在每个维度上的角度值
| |-- 生成角度值表
| |-- sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
| |-- 应用正弦和余弦函数
| |-- sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2])
| |-- sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2])
| |-- 转换为 PyTorch 张量并添加维度
| |-- return torch.FloatTensor(sinusoid_table).unsqueeze(0)
|
|-- forward 方法
| |-- 将位置编码添加到输入序列
| |-- return x + self.pos_table[:, :x.size(1)].clone().detach()
① 类的定义
继承自 PyTorch 的 nn.Module
类,用于为输入序列添加位置编码信息
class PositionalEncoding(nn.Module):
② 初始化方法
self.register_buffer():在 PyTorch 里,模型的参数(nn.Parameter
)会被优化器更新,而缓冲区则是一些不需要被优化器更新,但又需要随着模型一起保存和加载的张量。self.register_buffer()
方法可以将一个张量注册为缓冲区,使其成为模块的一部分,这样在保存和加载模型时,缓冲区也会被正确处理
参数名 | 类型 | 描述 |
---|---|---|
name | str | 缓冲区的名称,是一个字符串。之后能通过 self.name 来访问这个缓冲区。例如,若 name 设为 'pos_table' ,就可以用 self.pos_table 访问该缓冲区。 |
tensor | torch.Tensor | 需要注册为缓冲区的张量。此张量不会被优化器更新,但会和模型一同保存与加载。例如在位置编码里,预先计算好的位置编码张量就可作为该参数传入。 |
persistent | bool,可选 | 指示该缓冲区是否会被包含在 state_dict 中并在保存和加载模型时被处理,默认值为 True 。当设为 False 时,这个缓冲区不会出现在 state_dict 里,也就不会被保存和加载。不过在模型的前向传播等操作里,仍然可以使用该缓冲区。 |
self.get_sinusoid_encoding_table():编码序列信息
def __init__(self, d_hid, n_position=200):
super(PositionalEncoding, self).__init__()
# Not a parameter
self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid))
③ 编码序列信息【正弦位置编码】_get_sinusoid_encoding_table
n_position:表示序列的最大长度(即位置的数量),例如 512,
决定了生成的编码表有多少行(每个位置对应一行)
d_hid:代表隐藏层的维度。在位置编码的上下文中,它决定了每个位置编码向量的长度,即编码表中每一行的列数。
hid_j:一个循环变量,用于遍历隐藏层维度(d_hid
)的索引,位置(0 到 d_hid-1)
pos_i:一个循环变量,用于遍历位置(n_position
)的索引,位置(0
到 n_position-1
)
position:当前要计算角度向量的位置。
sinusoud_table:n_position × d_hid
的矩阵,存储所有位置的位置编码
get_position_angle_vec():内部函数,用于计算给定位置 position
在不同维度上的角度值。这些角度值后续会被用于生成正弦和余弦位置编码
self.rigister_buffer():nn.Module
类的一个方法,其作用是在模块里注册一个缓冲区(buffer)。
缓冲区:缓冲区(buffer)和模型参数(parameter)有所不同。参数是可学习的变量,会在模型训练时被优化器更新;而缓冲区则是一些不需要进行梯度更新的张量,不过在模型的保存和加载时,它们也会被保存和加载。
self.get_sinusoid_encoding_table(): 编码序列信息【正弦位置编码】
np.sin():计算输入数组(弧度制)中每个元素的正弦值,支持数组广播和多种数值类型
参数 | 类型/默认值 | 说明 |
---|---|---|
x | array_like | 输入数组(角度需转换为弧度) |
out | ndarray (可选) | 结果存储位置,需与输入广播后的形状一致 |
where | array_like (可选) | 条件过滤,True的位置执行计算 |
**kwargs | - | 其他关键字参数(如dtype ) |
np.cos():计算输入数组(弧度制)中每个元素的余弦值,功能与np.sin()
类似
参数 | 类型/默认值 | 说明 |
---|---|---|
x | array_like | 输入数组 |
out | ndarray (可选) | 同np.sin() |
where | array_like (可选) | 同np.sin() |
**kwargs | - | 同np.sin() |
np.power():对第一个数组的元素进行逐元素幂运算,基数为第一个数组,指数为第二个数组
参数 | 类型/默认值 | 说明 |
---|---|---|
x1 | array_like | 基数数组 |
x2 | array_like | 指数数组,需与x1 广播兼容 |
out | ndarray (可选) | 结果存储位置 |
**kwargs | - | 其他参数(如dtype ) |
torch.FloatTensor():创建浮点型张量,支持通过维度或数据初始化
参数 | 类型/默认值 | 说明 |
---|---|---|
data | list/维度值 | 数据或张量维度(如(2,3) ) |
dtype | - | 默认为torch.float32 |
张量.unsqueeze():在指定维度插入大小为1的新维度,常用于调整张量形状以适应模型输入
数 | 类型/默认值 | 说明 |
---|---|---|
dim | int | 插入维度的索引(如0 在第0维插入) |
def _get_sinusoid_encoding_table(self, n_position, d_hid):
''' Sinusoid position encoding table '''
# TODO: make it with torch instead of numpy
def get_position_angle_vec(position):
return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]
sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i
sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1
return torch.FloatTensor(sinusoid_table).unsqueeze(0)
④ 前向传播
将位置编码添加到输入序列:从位置编码表中截取与输入序列长度相同的部分,然后将其与输入序列相加,最后返回结果。通过 clone().detach()
确保位置编码表的数据不会影响梯度计算。
x:输入张量,通常是词嵌入(word embeddings),形状为 [batch_size, seq_len, d_model]
self.pos_table:存储预计算的位置编码表,由 _get_sinusoid_encoding_table(n_position, d_hid)
生成
clone():创建张量的深拷贝副本,保留计算图梯度(与原张量数据独立)
参数 | 类型/默认值 | 说明 |
---|---|---|
input | Tensor | 输入张量 |
memory_format | - | 内存格式(可选) |
size():返回张量或数组的维度大小(如(3,4)
表示3行4列)
参数 | 类型/默认值 | 说明 |
---|---|---|
dim | int (可选) | 指定维度索引(如dim=0 返回第0维大小) |
detach():从计算图中分离张量,阻止梯度传播,断开计算图,防止位置编码参与梯度更新
参数 | 类型/默认值 | 说明 |
---|---|---|
input | Tensor | 输入张量 |
def forward(self, x):
return x + self.pos_table[:, :x.size(1)].clone().detach()
⑤ 完整代码
class PositionalEncoding(nn.Module):
def __init__(self, d_hid, n_position=200):
super(PositionalEncoding, self).__init__()
# Not a parameter
self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid))
def _get_sinusoid_encoding_table(self, n_position, d_hid):
''' Sinusoid position encoding table '''
# TODO: make it with torch instead of numpy
def get_position_angle_vec(position):
return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]
sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i
sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1
return torch.FloatTensor(sinusoid_table).unsqueeze(0)
def forward(self, x):
return x + self.pos_table[:, :x.size(1)].clone().detach()
Ⅱ、编码器 Encoder
① 类的定义
继承自 PyTorch 的 nn.Module
类,构建一个具备自注意力机制的编码器模型,对输入序列进行特征提取与编码,将输入的原始序列转换为包含丰富语义信息的特征表示,而自注意力机制能够让模型捕捉序列中不同位置元素之间的依赖关系。
class Encoder(nn.Module):
''' A encoder model with self attention mechanism. '''
② 初始化方法
n_src_vocab:整数,源语言词汇表的大小,即源语言中不同单词的数量。在自然语言处理任务中,通常需要将文本中的单词转换为对应的整数索引,n_src_vocab
表示这些索引的取值范围。
d_word_vec:整数,词向量的维度。词向量是将单词表示为实数向量的一种方式,d_word_vec
表示每个单词对应的向量的长度。
n_layers:整数,编码器层(EncoderLayer
)的数量。在 Transformer 架构中,编码器通常由多个相同的编码器层堆叠而成,n_layers
表示堆叠的层数。
n_head:整数,多头注意力机制中的头数。多头注意力机制通过多个注意力头并行处理输入,能够捕捉输入序列中不同位置之间的多种依赖关系,n_head
表示注意力头的数量。
d_k:查询(Query)和键(Key)向量的维度。在
d_v:值(Value)向量的维度。
d_model:模型的隐藏层维度。在 Transformer 架构中,d_model
表示整个模型中各个层的隐藏单元数量,通常与词向量的维度相同。
d_inner:前馈神经网络(Feed-Forward Network)中隐藏层的维度。
pad_idx:填充标记(Padding Token)的索引
dropout:在训练过程中随机丢弃神经元的概率。
n_position:位置编码的最大长度。
scale_emb:是否对嵌入向量进行缩放
self.src_word_emb:源语言单词的嵌入层,在模型的前向传播过程中,将输入的单词索引转换为词向量。
self.position_enc:位置编码层,在模型的前向传播过程中,为输入的词向量添加位置编码。
self.dropout:丢弃层,在模型的前向传播过程中,对输入进行丢弃操作
self.layer_stack:编码器层的堆叠,在模型的前向传播过程中,依次通过每个 EncoderLayer
对输入进行编码
self.layer_norm:层归一化层
self.scale_emb:是否对嵌入向量进行缩放的标志
self.d_model:模型的隐藏层维度
PositionalEncoding():位置编码层,为输入序列中的每个元素添加位置信息,让模型能够学习到序列中元素的相对位置关系。
EncoderLayer():编码器层类,多个 EncoderLayer
实例堆叠在一起,对输入进行多次编码操作,提取输入序列的特征。
nn.Embedding():将离散索引映射为连续向量(如词嵌入),支持词表大小和向量维度定义
参数 | 类型/默认值 | 说明 |
---|---|---|
num_embeddings | int | 词表大小 |
embedding_dim | int | 向量维度 |
padding_idx | int (可选) | 填充索引(不更新梯度) |
max_norm | float (可选) | 向量最大范数限制 |
nn.Dropout():随机将输入元素置零(概率由p
控制),防止过拟合
参数 | 类型/默认值 | 说明 |
---|---|---|
p | float (0≤p<1) | 元素置零概率 |
inplace | bool (False) | 是否原地操作 |
nn.ModuleList():存储子模块的列表,支持动态增减模块(如循环网络层)
参数 | 类型/默认值 | 说明 |
---|---|---|
modules | iterable (可选) | 初始模块列表 |
nn.LayerNorm():对输入进行层标准化(沿指定维度归一化),常用于稳定训练
参数 | 类型/默认值 | 说明 |
---|---|---|
normalized_shape | int/list | 归一化的维度(如[128] ) |
eps | float (1e-5) | 数值稳定项 |
elementwise_affine | bool (True) | 是否启用可学习的缩放和偏移参数 |
def __init__(
self, n_src_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
d_model, d_inner, pad_idx, dropout=0.1, n_position=200, scale_emb=False):
super().__init__()
self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx)
self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
self.dropout = nn.Dropout(p=dropout)
self.layer_stack = nn.ModuleList([
EncoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
for _ in range(n_layers)])
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
self.scale_emb = scale_emb
self.d_model = d_model
③ 前向传播
代码运行流程
forward 方法
|
|-- 初始化自注意力矩阵列表
| |-- enc_slf_attn_list = []
|
|-- 前向传播步骤
| |-- 词嵌入
| |-- enc_output = self.src_word_emb(src_seq)
| |-- 缩放嵌入(可选)
| |-- if self.scale_emb:
| |-- enc_output *= self.d_model ** 0.5
| |-- 添加位置编码并应用 Dropout
| |-- enc_output = self.dropout(self.position_enc(enc_output))
| |-- 层归一化
| |-- enc_output = self.layer_norm(enc_output)
|
|-- 遍历编码器层
| |-- for enc_layer in self.layer_stack:
| |-- 编码器层计算
| |-- enc_output, enc_slf_attn = enc_layer(enc_output, slf_attn_mask=src_mask)
| |-- 记录自注意力矩阵(可选)
| |-- if return_attns:
| |-- enc_slf_attn_list += [enc_slf_attn]
|
|-- 返回结果
| |-- if return_attns:
| |-- return enc_output, enc_slf_attn_list
| |-- else:
| |-- return enc_output,
src_seq:源序列的输入,一般是经过分词并转换为词索引后的序列。
src_mask:源序列的掩码,是一个布尔类型的张量,用于指示哪些位置的元素需要被忽略。
return_attns:标志位,用于决定是否返回每层的自注意力矩阵。
enc_slf_attn_list:存储编码器每层的自注意力矩阵的列表
enc_output:编码器在不同阶段的输出结果。在不同的代码行中,它代表不同阶段的中间结果。
enc_slf_attn:编码器每层的自注意力矩阵,反映了该层中每个位置对其他位置的注意力权重分布。
self.src_word_emb():源语言单词的嵌入层,在类的初始化中定义。它将输入的词索引映射为对应的词向量。
self.scale_emb:标志位,用于决定是否对词嵌入的结果进行缩放操作
self.d_model:模型的隐藏层维度,也就是模型中各层的特征维度。
self.dropout():丢弃层,在模型的前向传播过程中,对输入进行丢弃操作
self.layer_norm():层归一化层
self.layer_stack:编码器层的堆叠,在模型的前向传播过程中,依次通过每个 EncoderLayer
对输入进行编码
def forward(self, src_seq, src_mask, return_attns=False):
enc_slf_attn_list = []
# -- Forward
enc_output = self.src_word_emb(src_seq)
if self.scale_emb:
enc_output *= self.d_model ** 0.5
enc_output = self.dropout(self.position_enc(enc_output))
enc_output = self.layer_norm(enc_output)
for enc_layer in self.layer_stack:
enc_output, enc_slf_attn = enc_layer(enc_output, slf_attn_mask=src_mask)
enc_slf_attn_list += [enc_slf_attn] if return_attns else []
if return_attns:
return enc_output, enc_slf_attn_list
return enc_output,
④ 完整代码
class Encoder(nn.Module):
''' A encoder model with self attention mechanism. '''
def __init__(
self, n_src_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
d_model, d_inner, pad_idx, dropout=0.1, n_position=200, scale_emb=False):
super().__init__()
self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx)
self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
self.dropout = nn.Dropout(p=dropout)
self.layer_stack = nn.ModuleList([
EncoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
for _ in range(n_layers)])
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
self.scale_emb = scale_emb
self.d_model = d_model
def forward(self, src_seq, src_mask, return_attns=False):
enc_slf_attn_list = []
# -- Forward
enc_output = self.src_word_emb(src_seq)
if self.scale_emb:
enc_output *= self.d_model ** 0.5
enc_output = self.dropout(self.position_enc(enc_output))
enc_output = self.layer_norm(enc_output)
for enc_layer in self.layer_stack:
enc_output, enc_slf_attn = enc_layer(enc_output, slf_attn_mask=src_mask)
enc_slf_attn_list += [enc_slf_attn] if return_attns else []
if return_attns:
return enc_output, enc_slf_attn_list
return enc_output,
Ⅲ、解码器 Decoder
① 类的定义
继承自 PyTorch 的 nn.Module
类,Decoder
类是一个带有自注意力机制的解码器模型。在许多序列到序列(Seq2Seq)的任务中,如机器翻译、文本生成等,通常会使用编码器 - 解码器(Encoder - Decoder)架构。编码器负责对输入序列进行编码,提取特征;而解码器则根据编码器的输出和已经生成的部分目标序列,逐步生成完整的目标序列。自注意力机制能够让解码器在生成过程中关注目标序列内部不同位置之间的关系,以及目标序列与源序列之间的关系。
class Decoder(nn.Module):
''' A decoder model with self attention mechanism. '''
② 初始化方法
n_trg_vocab:目标语言词汇表的大小,即目标语言中不同单词的数量。
d_word_vec:词向量的维度。词向量是将单词表示为实数向量的一种方式,该参数表示每个单词对应的向量的长度。
n_layers:解码器层(DecoderLayer
)的数量,用于创建 nn.ModuleList,
通过多层的堆叠让模型能够学习到更复杂的特征和语义信息。
n_head:多头注意力机制中的头数。多头注意力机制通过多个注意力头并行处理输入,能够捕捉输入序列中不同位置之间的多种依赖关系,该参数表示注意力头的数量。
d_k:查询(Query)和键(Key)向量的维度
d_v:值(Value)向量的维度。
d_model:模型的隐藏层维度
d_inner:前馈神经网络(Feed - Forward Network)中隐藏层的维度
pad_idx:填充标记(Padding Token)的索引。
n_position:位置编码的最大长度。位置编码用于为输入序列中的每个位置提供位置信息,该参数表示位置编码表的最大长度。
dropout:丢弃率,该参数表示在训练过程中随机丢弃神经元的概率。
scale_emb:是否对嵌入向量进行缩放
self.trg_word_emb:目标语言单词的嵌入层。nn.Embedding
是 PyTorch 中用于将整数索引转换为词向量的层,该变量用于将目标语言中的单词索引转换为对应的词向量
self.position_enc:位置编码层。
self.dropout:丢弃层。nn.Dropout
用于在训练过程中随机丢弃神经元,防止模型过拟合
self.layer_norm:解码器层的堆叠。nn.ModuleList
是 PyTorch 中用于存储多个模块的列表,该变量包含了指定数量的 DecoderLayer
实例,用于对输入进行多次解码操作。
self.scale_emb:是否对嵌入向量进行缩放的标志
self.d_model:模型的隐藏层维度
PositionalEncoding():用于生成位置编码表,并将其添加到输入的词向量中,为模型提供位置信息。
DecoderLayer(): 用于对输入进行解码操作
nn.Embedding():PyTorch 中用于将离散的整数索引转换为连续的向量表示(即词嵌入)的层。在自然语言处理任务中,通常会将单词映射为整数索引,而 nn.Embedding
可以将这些索引转换
参数名 | 类型 | 描述 |
---|---|---|
num_embeddings | int | 嵌入字典的大小,即词汇表中不同单词的数量。 |
embedding_dim | int | 每个嵌入向量的维度。 |
padding_idx | int,可选 | 填充标记的索引。如果指定了该参数,那么在计算过程中,填充标记对应的嵌入向量将始终为零向量,并且不会参与梯度更新。 |
max_norm | float,可选 | 嵌入向量的最大范数。如果嵌入向量的范数超过该值,将对其进行归一化处理。 |
norm_type | float,可选 | 计算范数时使用的范数类型,默认为 2 范数。 |
scale_grad_by_freq | bool,可选 | 是否根据单词的频率缩放梯度。如果设置为 True ,则频率较高的单词的梯度会相应减小。 |
sparse | bool,可选 | 是否使用稀疏梯度。如果设置为 True ,则嵌入层的梯度将是稀疏张量,适用于大规模词汇表的情况。 |
nn.Dropout():正则化技术,用于防止神经网络过拟合。在训练过程中,nn.Dropout
会以一定的概率随机将输入的某些元素置为零,从而迫使模型学习更鲁棒的特征表示。在测试阶段,nn.Dropout
会自动关闭,即不进行元素置零操作。
参数名 | 类型 | 描述 |
---|---|---|
p | float,可选 | 元素被置为零的概率,取值范围为 [0, 1],默认值为 0.5。 |
inplace | bool,可选 | 是否原地操作。如果设置为 True ,则直接在输入张量上进行修改,不创建新的张量,默认值为 False 。 |
nn.ModuleList():PyTorch 中用于存储多个 nn.Module
实例的容器。它类似于 Python 的列表,但 nn.ModuleList
会将其中的模块注册为当前模块的子模块,使得这些子模块的参数能够被正确管理
参数名 | 类型 | 描述 |
---|---|---|
modules | iterable,可选 | 一个可迭代对象,包含要存储在 nn.ModuleList 中的 nn.Module 实例。 |
nn.LayerNorm():归一化技术,用于对输入的特征进行归一化处理
参数名 | 类型 | 描述 |
---|---|---|
normalized_shape | int 或 list 或 torch.Size | 输入特征的形状,可以是整数、列表或 torch.Size 对象。如果是整数,则表示输入特征的最后一个维度的大小;如果是列表或 torch.Size 对象,则表示需要归一化的维度的形状。 |
eps | float,可选 | 为了数值稳定性而添加到分母中的一个小常数,默认值为 1e-5。 |
elementwise_affine | bool,可选 | 是否对归一化后的输出进行仿射变换(即乘以一个可学习的权重并加上一个可学习的偏置),默认值为 True 。 |
def __init__(
self, n_trg_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
d_model, d_inner, pad_idx, n_position=200, dropout=0.1, scale_emb=False):
super().__init__()
self.trg_word_emb = nn.Embedding(n_trg_vocab, d_word_vec, padding_idx=pad_idx)
self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
self.dropout = nn.Dropout(p=dropout)
self.layer_stack = nn.ModuleList([
DecoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
for _ in range(n_layers)])
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
self.scale_emb = scale_emb
self.d_model = d_model
③ 前向传播
代码运行流程
forward 方法
|
|-- 初始化
| |-- dec_slf_attn_list = []
| |-- dec_enc_attn_list = []
|
|-- 前向传播
| |-- 词嵌入
| | |-- dec_output = self.trg_word_emb(trg_seq)
| |
| |-- 缩放嵌入(可选)
| | |-- if self.scale_emb:
| | |-- dec_output *= self.d_model ** 0.5
| |
| |-- 添加位置编码并丢弃
| | |-- dec_output = self.dropout(self.position_enc(dec_output))
| |
| |-- 层归一化
| | |-- dec_output = self.layer_norm(dec_output)
| |
| |-- 解码器层循环
| | |-- for dec_layer in self.layer_stack:
| | |-- 调用解码器层
| | | |-- dec_output, dec_slf_attn, dec_enc_attn = dec_layer(
| | | |-- dec_output,
| | | |-- enc_output,
| | | |-- slf_attn_mask=trg_mask,
| | | |-- dec_enc_attn_mask=src_mask)
| | |
| | |-- 保存注意力矩阵(可选)
| | | |-- if return_attns:
| | | |-- dec_slf_attn_list.append(dec_slf_attn)
| | | |-- dec_enc_attn_list.append(dec_enc_attn)
|
|-- 返回结果
| |-- if return_attns:
| | |-- return dec_output, dec_slf_attn_list, dec_enc_attn_list
| |-- else:
| |-- return dec_output,
trg_seq:目标序列的输入,在机器翻译等任务里,就是目标语言的单词索引序列。
trg_mask:目标序列的掩码矩阵。掩码矩阵是一个布尔类型的张量,用于指示哪些位置的元素需要被关注或忽略。
enc_output:编码器(Encoder)的输出结果。编码器对输入的源序列进行编码,提取出序列的特征表示,这个输出会作为解码器的输入之一,帮助解码器生成目标序列。
src_mask:源序列的掩码矩阵。和 trg_mask
类似,用于指示源序列中哪些位置的元素需要被关注或忽略。
return_attns:一个标志位,用于决定是否返回解码器每层的自注意力矩阵和编码器 - 解码器注意力矩阵。
dec_slf_attn_list:列表,存储解码器每层的自注意力矩阵
dec_enc_attn_list:列表,存储解码器每层的编码器 - 解码器注意力矩阵
dec_output:解码器在不同阶段的输出结果
dec_slf_attn:解码器每层的自注意力矩阵
dec_enc_attn:解码器每层的编码器 - 解码器注意力矩阵
self.trg_word_emb():在类的初始化时定义的目标语言词嵌入层
self.dropout():在类初始化时定义的丢弃层
self.layer_norm():在类初始化时定义的层归一化层
self.position_enc():在类初始化时定义的位置编码层
self.layer_stack:类初始化时定义的解码器层堆叠
dec_layer:self.layer_stack
中的每一个 DecoderLayer
实例。DecoderLayer
是解码器的基本组成单元,通常包含自注意力机制、编码器 - 解码器注意力机制和前馈神经网络。
slf_attn_mask:自注意力掩码矩阵,在 dec_layer
调用时传入,即 slf_attn_mask=trg_mask
。
dec_enc_attn_mask:编码器 - 解码器注意力掩码矩阵,在 dec_layer
调用时传入,即 dec_enc_attn_mask=src_mask
。
return_attns: 一个标志位,用于决定是否返回解码器每层的自注意力矩阵和编码器 - 解码器注意力矩阵。
def forward(self, trg_seq, trg_mask, enc_output, src_mask, return_attns=False):
dec_slf_attn_list, dec_enc_attn_list = [], []
# -- Forward
dec_output = self.trg_word_emb(trg_seq)
if self.scale_emb:
dec_output *= self.d_model ** 0.5
dec_output = self.dropout(self.position_enc(dec_output))
dec_output = self.layer_norm(dec_output)
for dec_layer in self.layer_stack:
dec_output, dec_slf_attn, dec_enc_attn = dec_layer(
dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=src_mask)
dec_slf_attn_list += [dec_slf_attn] if return_attns else []
dec_enc_attn_list += [dec_enc_attn] if return_attns else []
if return_attns:
return dec_output, dec_slf_attn_list, dec_enc_attn_list
return dec_output,
④ 完整代码
class Decoder(nn.Module):
''' A decoder model with self attention mechanism. '''
def __init__(
self, n_trg_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
d_model, d_inner, pad_idx, n_position=200, dropout=0.1, scale_emb=False):
super().__init__()
self.trg_word_emb = nn.Embedding(n_trg_vocab, d_word_vec, padding_idx=pad_idx)
self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
self.dropout = nn.Dropout(p=dropout)
self.layer_stack = nn.ModuleList([
DecoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
for _ in range(n_layers)])
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
self.scale_emb = scale_emb
self.d_model = d_model
def forward(self, trg_seq, trg_mask, enc_output, src_mask, return_attns=False):
dec_slf_attn_list, dec_enc_attn_list = [], []
# -- Forward
dec_output = self.trg_word_emb(trg_seq)
if self.scale_emb:
dec_output *= self.d_model ** 0.5
dec_output = self.dropout(self.position_enc(dec_output))
dec_output = self.layer_norm(dec_output)
for dec_layer in self.layer_stack:
dec_output, dec_slf_attn, dec_enc_attn = dec_layer(
dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=src_mask)
dec_slf_attn_list += [dec_slf_attn] if return_attns else []
dec_enc_attn_list += [dec_enc_attn] if return_attns else []
if return_attns:
return dec_output, dec_slf_attn_list, dec_enc_attn_list
return dec_output,
Ⅳ、Transformer模型 Transformer
① 类的定义
继承自 PyTorch 的 nn.Module
类,处理输入序列到输出序列的转换任务,像机器翻译(将源语言句子转换为目标语言句子)、文本摘要(将长文本转换为短摘要)等。而注意力机制能让模型在处理序列时,动态地关注输入序列的不同部分,从而提升模型性能。
class Transformer(nn.Module):
''' A sequence to sequence model with attention mechanism. '''
② 初始化方法
n_src_vocab:源语言词汇表的大小,即源语言中不同单词的数量。
n_trg_vocab:目标语言词汇表的大小,即目标语言中不同单词的数量。
src_pad_idx:源语言中填充标记(padding token)的索引。
trg_pad_idx:目标语言中填充标记的索引
d_word_vec:词向量的维度,即每个单词被映射到的向量空间的维度。
d_model:模型的隐藏层维度,是整个 Transformer 模型中各层的特征维度。
d_inner:前馈神经网络(Feed - Forward Network)中隐藏层的维度。
n_layers:编码器和解码器中层的数量。
n_head:多头注意力机制(Multi - Head Attention)中的头数。
d_k:查询(Query)和键(Key)向量的维度。
d_v:值(Value)向量的维度。
dropout:Dropout 层的丢弃率。
n_position:位置编码的最大长度。位置编码用于为输入序列中的每个位置添加位置信息,使模型能够感知序列中元素的相对位置。
trg_emb_prj_weight_sharing:一个布尔值,指示是否共享目标语言词嵌入层和最终线性投影层的权重。
scale_emb_or_prj:一个字符串,取值为 'emb'
、'prj'
或 'none','emb'
表示对嵌入层的输出乘以 d_model^1/2;'prj'
表示对线性投影层的输出乘以 (d_model^1/2)^−1;'none'
表示不进行缩放。
self.src_pad_idx:存储源语言填充标记的索引
self.trg_pad_idx:存储目标语言填充标记的索引
scale_emb:个布尔值,根据 scale_emb_or_prj
和 trg_emb_prj_weight_sharing
的值确定是否对嵌入层的输出进行缩放
self.scale_prj:一个布尔值,根据 scale_emb_or_prj
和 trg_emb_prj_weight_sharing
的值确定是否对线性投影层的输出进行缩放
self.d_model:存储模型的隐藏层维度,方便在后续的代码中使用,例如在缩放操作时会用到该值。
self.encoder:编码器实例,用于对输入的源序列进行编码。
self.decoder:解码器实例,根据编码器的输出和目标序列的部分信息生成完整的目标序列。
self.trg_word_prj:一个线性层,将解码器的输出映射到目标词汇表大小,用于预测每个位置的单词。
assert:对某个条件进行测试,如果条件为 True
,程序会继续正常执行;如果条件为 False
,则会抛出 AssertionError
异常,同时可以附带一个错误信息,方便开发者定位问题。
参数 | 类型 | 描述 |
---|---|---|
condition | 布尔表达式 | 需要测试的条件,如果为 True ,程序继续执行;如果为 False ,抛出 AssertionError 异常。 |
message (可选) | 字符串 | 当 condition 为 False 时,作为 AssertionError 异常的错误信息输出。 |
parameters():PyTorch 中 nn.Module
类的一个方法,用于返回一个包含模型所有可训练参数的迭代器。
dim():PyTorch 中 torch.Tensor
类的一个方法,用于返回张量的维度数(即秩)
nn.init.xavier_uniform():PyTorch 中用于初始化张量参数的函数,它实现了 Xavier 均匀初始化方法。
参数 | 类型 | 描述 |
---|---|---|
tensor | torch.Tensor | 需要进行初始化的张量。 |
gain (可选) | 浮点数 | 缩放因子,用于调整初始化的范围。默认值为 1.0。 |
nn.Linear():PyTorch 中实现全连接层(线性层)的模块,其作用是对输入张量进行线性变换
参数名 | 类型 | 默认值 | 说明 |
---|---|---|---|
in_features | int | 无 | 输入样本的特征维度(例如,每个样本的特征数量)。 |
out_features | int | 无 | 输出样本的特征维度(例如,全连接层的神经元数量)。 |
bias | bool | True | 是否添加偏置项。设为 False 时,输出为 y = xA^T 。 |
def __init__(
self, n_src_vocab, n_trg_vocab, src_pad_idx, trg_pad_idx,
d_word_vec=512, d_model=512, d_inner=2048,
n_layers=6, n_head=8, d_k=64, d_v=64, dropout=0.1, n_position=200,
trg_emb_prj_weight_sharing=True, emb_src_trg_weight_sharing=True,
scale_emb_or_prj='prj'):
super().__init__()
self.src_pad_idx, self.trg_pad_idx = src_pad_idx, trg_pad_idx
# In section 3.4 of paper "Attention Is All You Need", there is such detail:
# "In our model, we share the same weight matrix between the two
# embedding layers and the pre-softmax linear transformation...
# In the embedding layers, we multiply those weights by \sqrt{d_model}".
#
# Options here:
# 'emb': multiply \sqrt{d_model} to embedding output
# 'prj': multiply (\sqrt{d_model} ^ -1) to linear projection output
# 'none': no multiplication
assert scale_emb_or_prj in ['emb', 'prj', 'none']
scale_emb = (scale_emb_or_prj == 'emb') if trg_emb_prj_weight_sharing else False
self.scale_prj = (scale_emb_or_prj == 'prj') if trg_emb_prj_weight_sharing else False
self.d_model = d_model
self.encoder = Encoder(
n_src_vocab=n_src_vocab, n_position=n_position,
d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,
n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,
pad_idx=src_pad_idx, dropout=dropout, scale_emb=scale_emb)
self.decoder = Decoder(
n_trg_vocab=n_trg_vocab, n_position=n_position,
d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,
n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,
pad_idx=trg_pad_idx, dropout=dropout, scale_emb=scale_emb)
self.trg_word_prj = nn.Linear(d_model, n_trg_vocab, bias=False)
for p in self.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
assert d_model == d_word_vec, \
'To facilitate the residual connections, \
the dimensions of all module outputs shall be the same.'
if trg_emb_prj_weight_sharing:
# Share the weight between target word embedding & last dense layer
self.trg_word_prj.weight = self.decoder.trg_word_emb.weight
if emb_src_trg_weight_sharing:
self.encoder.src_word_emb.weight = self.decoder.trg_word_emb.weight
③ 前向传播
代码运行流程
forward 方法
|
|-- 输入
| |-- src_seq: 源序列
| |-- trg_seq: 目标序列
|
|-- 生成掩码
| |-- 源序列掩码
| |-- src_mask = get_pad_mask(src_seq, self.src_pad_idx)
| |-- 目标序列掩码
| |-- trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)
|
|-- 编码器处理
| |-- 调用编码器对源序列进行编码
| |-- enc_output, *_ = self.encoder(src_seq, src_mask)
|
|-- 解码器处理
| |-- 调用解码器生成目标序列
| |-- dec_output, *_ = self.decoder(trg_seq, trg_mask, enc_output, src_mask)
|
|-- 线性投影
| |-- 将解码器输出映射到词汇表大小
| |-- seq_logit = self.trg_word_prj(dec_output)
|
|-- 缩放操作(可选)
| |-- if self.scale_prj:
| |-- seq_logit *= self.d_model ** -0.5
|
|-- 调整形状并返回结果
| |-- return seq_logit.view(-1, seq_logit.size(2))
src_seq:输入的源序列,通常是一个 torch.Tensor
类型的张量。它代表源语言句子中各个单词对应的索引序列。
trg_seq:输入的目标序列,同样是 torch.Tensor
类型的张量。它代表目标语言句子中各个单词对应的索引序列。
src_mask:源序列的掩码矩阵.
trg_mask:目标序列的掩码矩阵,由两部分组合而成。
enc_output:编码器的输出结果
dec_output:解码器的输出结果
self.scale_prj:一个布尔类型的标志,在类的初始化方法中定义,标识是否进行缩放操作
seq_logit:将解码器的输出 dec_output
通过 self.trg_word_prj
线性层进行映射后得到的结果。
self.d_model:模型的隐藏层维度,在类的初始化方法中定义。
get_pad_mask():用于生成填充掩码(padding mask)。在处理变长序列时,通常会将序列填充到相同的长度,填充的部分在后续的计算中是没有意义的,需要通过掩码机制将其屏蔽掉。
self.encoder():编码器模块,编码器的主要作用是对输入的源序列进行编码,将输入的源序列转换为一系列的特征表示,这些特征表示包含了源序列的语义信息。
self.decoder():解码器模块,解码器的主要作用是根据编码器的输出和已经生成的部分目标序列,逐步生成完整的目标序列。
self.trg_word_prj():线性层,用于将解码器的输出映射到目标词汇表大小的维度上。
view():PyTorch 中 torch.Tensor
类的一个方法,用于改变张量的形状。它可以在不改变张量数据的情况下,重新调整张量的维度和大小。
参数 | 类型 | 描述 |
---|---|---|
*shape | 整数或 torch.Size 对象 | 新的形状。可以传入多个整数参数,也可以传入一个 torch.Size 对象来指定新的形状。例如,tensor.view(2, 3) 表示将张量调整为形状为 (2, 3) 的二维张量;tensor.view(torch.Size([2, 3])) 也有相同的效果。 |
size():PyTorch 中 torch.Tensor
类的一个方法,用于返回张量的形状。它会返回一个 torch.Size
对象,该对象类似于元组,可以通过索引访问每个维度的大小。
参数 | 类型 | 描述 |
---|---|---|
dim (可选) | 整数 | 指定要返回大小的维度索引。如果不提供该参数,则返回整个张量的形状(即 torch.Size 对象);如果提供了该参数,则返回指定维度的大小。例如,tensor.size() 会返回整个张量的形状,而 tensor.size(1) 会返回张量第二维的大小。 |
def forward(self, src_seq, trg_seq):
src_mask = get_pad_mask(src_seq, self.src_pad_idx)
trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)
enc_output, *_ = self.encoder(src_seq, src_mask)
dec_output, *_ = self.decoder(trg_seq, trg_mask, enc_output, src_mask)
seq_logit = self.trg_word_prj(dec_output)
if self.scale_prj:
seq_logit *= self.d_model ** -0.5
return seq_logit.view(-1, seq_logit.size(2))
④ 完整代码
class Transformer(nn.Module):
''' A sequence to sequence model with attention mechanism. '''
def __init__(
self, n_src_vocab, n_trg_vocab, src_pad_idx, trg_pad_idx,
d_word_vec=512, d_model=512, d_inner=2048,
n_layers=6, n_head=8, d_k=64, d_v=64, dropout=0.1, n_position=200,
trg_emb_prj_weight_sharing=True, emb_src_trg_weight_sharing=True,
scale_emb_or_prj='prj'):
super().__init__()
self.src_pad_idx, self.trg_pad_idx = src_pad_idx, trg_pad_idx
# In section 3.4 of paper "Attention Is All You Need", there is such detail:
# "In our model, we share the same weight matrix between the two
# embedding layers and the pre-softmax linear transformation...
# In the embedding layers, we multiply those weights by \sqrt{d_model}".
#
# Options here:
# 'emb': multiply \sqrt{d_model} to embedding output
# 'prj': multiply (\sqrt{d_model} ^ -1) to linear projection output
# 'none': no multiplication
assert scale_emb_or_prj in ['emb', 'prj', 'none']
scale_emb = (scale_emb_or_prj == 'emb') if trg_emb_prj_weight_sharing else False
self.scale_prj = (scale_emb_or_prj == 'prj') if trg_emb_prj_weight_sharing else False
self.d_model = d_model
self.encoder = Encoder(
n_src_vocab=n_src_vocab, n_position=n_position,
d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,
n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,
pad_idx=src_pad_idx, dropout=dropout, scale_emb=scale_emb)
self.decoder = Decoder(
n_trg_vocab=n_trg_vocab, n_position=n_position,
d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,
n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,
pad_idx=trg_pad_idx, dropout=dropout, scale_emb=scale_emb)
self.trg_word_prj = nn.Linear(d_model, n_trg_vocab, bias=False)
for p in self.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
assert d_model == d_word_vec, \
'To facilitate the residual connections, \
the dimensions of all module outputs shall be the same.'
if trg_emb_prj_weight_sharing:
# Share the weight between target word embedding & last dense layer
self.trg_word_prj.weight = self.decoder.trg_word_emb.weight
if emb_src_trg_weight_sharing:
self.encoder.src_word_emb.weight = self.decoder.trg_word_emb.weight
def forward(self, src_seq, trg_seq):
src_mask = get_pad_mask(src_seq, self.src_pad_idx)
trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)
enc_output, *_ = self.encoder(src_seq, src_mask)
dec_output, *_ = self.decoder(trg_seq, trg_mask, enc_output, src_mask)
seq_logit = self.trg_word_prj(dec_output)
if self.scale_prj:
seq_logit *= self.d_model ** -0.5
return seq_logit.view(-1, seq_logit.size(2))
Ⅴ、辅助函数
① get_pad_mask 生成填充掩码
seq:输入序列,包含 token 的索引,其中 pad_idx
表示填充位置(如 0
或 [PAD]
)
pad_idx:填充 token 的索引值,所有等于 pad_idx
的位置会被掩码(标记为 False
)
张量.unsqueeze():在指定维度插入大小为1的新维度,常用于调整张量形状以适应模型输入
参数 | 类型/默认值 | 说明 |
---|---|---|
dim | int | 插入维度的索引(如0 在第0维插入) |
def get_pad_mask(seq, pad_idx):
return (seq != pad_idx).unsqueeze(-2)
② get_subsequent_mask:生成后续位置掩码
seq:输入序列,形状通常为 [batch_size, seq_len]
sz_b:batch size(批大小),即 seq
的第 0 维大小
len_s:序列长度(seq_len
),即 seq
的第 1 维大小
subsequent_mask:最终的后续位置掩码,形状为 [1, len_s, len_s],
确保解码时第 i
个位置只能关注到前 i
个位置(包括自己),无法看到未来信息
size():返回张量或数组的维度大小(如(3,4)
表示3行4列)
参数 | 类型/默认值 | 说明 |
---|---|---|
dim | int (可选) | 指定维度索引(如dim=0 返回第0维大小) |
torch.triu():返回矩阵的上三角部分(含对角线及以上元素)
参数 | 类型/默认值 | 说明 |
---|---|---|
input | Tensor | 输入张量 |
diagonal | int (0) | 对角线偏移量(正数上移) |
torch.ones():创建全1张量,支持形状和数据类型指定
参数 | 类型/默认值 | 说明 |
---|---|---|
size | tuple/int | 张量形状(如(2,3) ) |
dtype | dtype (可选) | 数据类型(默认torch.float32 ) |
bool():将输入转换为布尔类型(非零/非空为True
,否则False
)
参数 | 类型/默认值 | 说明 |
---|---|---|
x | 任意类型 | 输入值(如数值、字符串、列表等) |
def get_subsequent_mask(seq):
''' For masking out the subsequent info. '''
sz_b, len_s = seq.size()
subsequent_mask = (1 - torch.triu(
torch.ones((1, len_s, len_s), device=seq.device), diagonal=1)).bool()
return subsequent_mask
Ⅵ、完整代码
''' Define the Transformer model '''
import torch
import torch.nn as nn
import numpy as np
from .Layers import EncoderLayer, DecoderLayer
__author__ = "Yu-Hsiang Huang"
def get_pad_mask(seq, pad_idx):
return (seq != pad_idx).unsqueeze(-2)
def get_subsequent_mask(seq):
''' For masking out the subsequent info. '''
sz_b, len_s = seq.size()
subsequent_mask = (1 - torch.triu(
torch.ones((1, len_s, len_s), device=seq.device), diagonal=1)).bool()
return subsequent_mask
class PositionalEncoding(nn.Module):
def __init__(self, d_hid, n_position=200):
super(PositionalEncoding, self).__init__()
# Not a parameter
self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid))
def _get_sinusoid_encoding_table(self, n_position, d_hid):
''' Sinusoid position encoding table '''
# TODO: make it with torch instead of numpy
def get_position_angle_vec(position):
return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]
sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i
sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1
return torch.FloatTensor(sinusoid_table).unsqueeze(0)
def forward(self, x):
return x + self.pos_table[:, :x.size(1)].clone().detach()
class Encoder(nn.Module):
''' A encoder model with self attention mechanism. '''
def __init__(
self, n_src_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
d_model, d_inner, pad_idx, dropout=0.1, n_position=200, scale_emb=False):
super().__init__()
self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx)
self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
self.dropout = nn.Dropout(p=dropout)
self.layer_stack = nn.ModuleList([
EncoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
for _ in range(n_layers)])
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
self.scale_emb = scale_emb
self.d_model = d_model
def forward(self, src_seq, src_mask, return_attns=False):
enc_slf_attn_list = []
# -- Forward
enc_output = self.src_word_emb(src_seq)
if self.scale_emb:
enc_output *= self.d_model ** 0.5
enc_output = self.dropout(self.position_enc(enc_output))
enc_output = self.layer_norm(enc_output)
for enc_layer in self.layer_stack:
enc_output, enc_slf_attn = enc_layer(enc_output, slf_attn_mask=src_mask)
enc_slf_attn_list += [enc_slf_attn] if return_attns else []
if return_attns:
return enc_output, enc_slf_attn_list
return enc_output,
class Decoder(nn.Module):
''' A decoder model with self attention mechanism. '''
def __init__(
self, n_trg_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
d_model, d_inner, pad_idx, n_position=200, dropout=0.1, scale_emb=False):
super().__init__()
self.trg_word_emb = nn.Embedding(n_trg_vocab, d_word_vec, padding_idx=pad_idx)
self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
self.dropout = nn.Dropout(p=dropout)
self.layer_stack = nn.ModuleList([
DecoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
for _ in range(n_layers)])
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
self.scale_emb = scale_emb
self.d_model = d_model
def forward(self, trg_seq, trg_mask, enc_output, src_mask, return_attns=False):
dec_slf_attn_list, dec_enc_attn_list = [], []
# -- Forward
dec_output = self.trg_word_emb(trg_seq)
if self.scale_emb:
dec_output *= self.d_model ** 0.5
dec_output = self.dropout(self.position_enc(dec_output))
dec_output = self.layer_norm(dec_output)
for dec_layer in self.layer_stack:
dec_output, dec_slf_attn, dec_enc_attn = dec_layer(
dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=src_mask)
dec_slf_attn_list += [dec_slf_attn] if return_attns else []
dec_enc_attn_list += [dec_enc_attn] if return_attns else []
if return_attns:
return dec_output, dec_slf_attn_list, dec_enc_attn_list
return dec_output,
class Transformer(nn.Module):
''' A sequence to sequence model with attention mechanism. '''
def __init__(
self, n_src_vocab, n_trg_vocab, src_pad_idx, trg_pad_idx,
d_word_vec=512, d_model=512, d_inner=2048,
n_layers=6, n_head=8, d_k=64, d_v=64, dropout=0.1, n_position=200,
trg_emb_prj_weight_sharing=True, emb_src_trg_weight_sharing=True,
scale_emb_or_prj='prj'):
super().__init__()
self.src_pad_idx, self.trg_pad_idx = src_pad_idx, trg_pad_idx
# In section 3.4 of paper "Attention Is All You Need", there is such detail:
# "In our model, we share the same weight matrix between the two
# embedding layers and the pre-softmax linear transformation...
# In the embedding layers, we multiply those weights by \sqrt{d_model}".
#
# Options here:
# 'emb': multiply \sqrt{d_model} to embedding output
# 'prj': multiply (\sqrt{d_model} ^ -1) to linear projection output
# 'none': no multiplication
assert scale_emb_or_prj in ['emb', 'prj', 'none']
scale_emb = (scale_emb_or_prj == 'emb') if trg_emb_prj_weight_sharing else False
self.scale_prj = (scale_emb_or_prj == 'prj') if trg_emb_prj_weight_sharing else False
self.d_model = d_model
self.encoder = Encoder(
n_src_vocab=n_src_vocab, n_position=n_position,
d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,
n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,
pad_idx=src_pad_idx, dropout=dropout, scale_emb=scale_emb)
self.decoder = Decoder(
n_trg_vocab=n_trg_vocab, n_position=n_position,
d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,
n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,
pad_idx=trg_pad_idx, dropout=dropout, scale_emb=scale_emb)
self.trg_word_prj = nn.Linear(d_model, n_trg_vocab, bias=False)
for p in self.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
assert d_model == d_word_vec, \
'To facilitate the residual connections, \
the dimensions of all module outputs shall be the same.'
if trg_emb_prj_weight_sharing:
# Share the weight between target word embedding & last dense layer
self.trg_word_prj.weight = self.decoder.trg_word_emb.weight
if emb_src_trg_weight_sharing:
self.encoder.src_word_emb.weight = self.decoder.trg_word_emb.weight
def forward(self, src_seq, trg_seq):
src_mask = get_pad_mask(src_seq, self.src_pad_idx)
trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)
enc_output, *_ = self.encoder(src_seq, src_mask)
dec_output, *_ = self.decoder(trg_seq, trg_mask, enc_output, src_mask)
seq_logit = self.trg_word_prj(dec_output)
if self.scale_prj:
seq_logit *= self.d_model ** -0.5
return seq_logit.view(-1, seq_logit.size(2))
二、利用定义的Transformer架构模型完成文章标题生成任务
数据文件
通过网盘分享的文件:生成文章标题
链接: https://pan.baidu.com/s/1PraH5soeHEZJ2t0ZIJGiwg?pwd=8pwy 提取码: 8pwy
--来自百度网盘超级会员v3的分享
1.配置文件 config.py
model_path:模型保存的路径,训练完成后模型会存储在该目录下
input_max_length:输入序列的最大长度,输入序列的长度将被截断或填充到最大长度个token
output_max_length:输出序列的最大长度,生成的输出序列长度不超过最大长度个token
epoch:训练的轮数,表示模型将遍历整个数据集的次数
batch_size:每次训练时使用的样本数量,表示每次训练会处理的样本数量
optimizer:优化器的类型
learning_rate:学习率,控制模型参数更新的步长
seed:随机种子,用于确保实验的可重复性
vocab_size:词汇表的大小,表示模型可以识别的不同token的数量
vocab_path:词汇表的文件路径
train_data_path:训练数据集的路径
valid_data_path:验证数据集的路径
beam_size:束搜索(Beam Search)的宽度,用于生成序列时保留的候选序列数量
# -*- coding: utf-8 -*-
"""
配置参数信息
"""
import os
import torch
Config = {
"model_path": "output",
"input_max_length": 120,
"output_max_length": 30,
"epoch": 200,
"batch_size": 32,
"optimizer": "adam",
"learning_rate":1e-3,
"seed":42,
"vocab_size":6219,
"vocab_path":"vocab.txt",
"train_data_path": r"sample_data.json",
"valid_data_path": r"sample_data.json",
"beam_size":5
}
2.数据加载文件 loader.py
Ⅰ、代码运行流程
DataLoader运行流程树状图
├─ 1. 初始化配置
│ ├─ a. 加载词汇表 (load_vocab)
│ │ ├─ 来源路径: config["vocab_path"] [1](@ref)
│ │ └─ 构建词到索引的映射字典
│ ├─ b. 配置更新
│ │ ├─ vocab_size: 词表实际长度
│ │ ├─ pad_idx: 填充符索引([PAD])
│ │ ├─ start_idx: 起始符索引([CLS])
│ │ └─ end_idx: 结束符索引([SEP]) [6](@ref)
├─ 2. 数据加载与预处理
│ ├─ a. 读取原始数据文件 (load)
│ │ ├─ 路径: config["train_data_path"]
│ │ ├─ 逐行解析JSON格式的title和content字段
│ │ └─ 调用prepare_data处理每对(title, content)
│ ├─ b. 序列编码 (encode_sentence)
│ │ ├─ 添加特殊标记:[CLS]在头部,[SEP]在尾部 [6](@ref)
│ │ ├─ 文本转索引(未登录词用[UNK]表示)
│ │ └─ 截断/填充至max_length [1](@ref)
│ ├─ c. 张量转换
│ │ ├─ input_seq: 输入序列(无[CLS],用于模型输入)
│ │ ├─ output_seq: 输出序列(带[CLS],用于解码器输入)
│ │ └─ gold: 目标序列(带[SEP],用于计算损失) [6](@ref)
├─ 3. DataLoader封装
│ ├─ a. 创建DataGenerator实例
│ │ ├─ 实现__len__和__getitem__方法 [4,6](@ref)
│ │ └─ 存储数据为LongTensor三元组列表
│ ├─ b. 参数配置
│ │ ├─ batch_size: 批次大小
│ │ ├─ shuffle: 是否打乱顺序(默认True)
│ │ └─ 自动分批机制:按索引获取数据 [4,6](@ref)
├─ 4. 批次生成流程
│ ├─ a. 迭代器启动
│ │ ├─ 调用DataLoader的__iter__方法
│ │ └─ 创建_DataLoaderIter对象 [5](@ref)
│ ├─ b. 批次采样
│ │ ├─ 根据batch_size划分索引
│ │ └─ 若shuffle=True则随机采样 [6](@ref)
│ ├─ c. 数据组装
│ │ ├─ 调用__getitem__获取单个样本
│ │ └─ 自动拼接为(batch_input, batch_output, batch_gold) [4,6](@ref)
└─ 5. 训练循环
├─ a. 遍历DataLoader
│ ├─ 每个iteration获取一个批次
│ └─ 数据自动迁移至GPU(若可用) [4](@ref)
└─ b. 数据维度示例
├─ input_seq: (batch_size, input_max_length)
├─ output_seq: (batch_size, output_max_length)
└─ gold: (batch_size, output_max_length) [6](@ref)
Ⅱ、初始化方法 __init__
data_path:数据文件路径,指定原始语料的位置(如 data/train.txt
),用于后续加载文本数据。
config:全局配置字典,存储模型参数与训练设置(如 batch_size
、max_seq_len
),并在初始化时更新词汇相关参数。
logger:日志记录器,输出调试信息(如词汇表加载状态、数据处理进度)。
self.config:更新后的配置字典,添加或覆盖一些键值
self.logger:日志记录器,记录类初始化过程中的关键事件(如词汇表加载成功/失败)
self.path:原始数据路径,指向待处理的文本文件(如新闻语料、对话数据)
self.vocab:词汇表映射字典,通过 load_vocab(config["vocab_path"])
加载,格式为 { "word": index }
self.config["vocab_size"]:词汇表实际长度
self.config["pad_idx"]:填充符索引(如 0)
self.config["start_idx"]:起始符索引(如 101,对应 [CLS]
)
self.config["end_idx"]:结束符索引(如 102,对应 [SEP]
)
load_vocab():词汇表加载,从vocab_path加载词汇表
self.load():文本读取,从 self.path
加载原始语料。
len():返回对象的长度或元素数量,支持字符串、列表、元组、字典等可迭代对象。对于字符串,返回字符数(单字节或 Unicode 字符)
参数 | 类型 | 说明 |
---|---|---|
obj | 可迭代对象(如 str 、list 、dict ) | 必需,需计算长度的对象。 |
def __init__(self, data_path, config, logger):
self.config = config
self.logger = logger
self.path = data_path
self.vocab = load_vocab(config["vocab_path"])
self.config["vocab_size"] = len(self.vocab)
self.config["pad_idx"] = self.vocab["[PAD]"]
self.config["start_idx"] = self.vocab["[CLS]"]
self.config["end_idx"] = self.vocab["[SEP]"]
self.load()
Ⅲ、读取数据文件 load
self.data:一个列表,用于存储从文件中读取并处理后的数据
self.path:类的一个实例属性,是一个字符串类型的变量,代表要读取的文件的路径
f:文件对象,是调用 open
函数打开文件后返回的对象
line:在文件读取循环中,line
代表文件中的每一行内容,是一个字符串。
title:从文件中每一行解析出的字典中的 "title"
键对应的值,通常是一个字符串,表示某条数据的标题
content:从文件中每一行解析出的字典中的 "content"
键对应的值,通常是一个字符串,表示某条数据的具体内容。
self.preprare_data():类的一个实例方法,将输入输出转化为序列
open():打开文件并返回文件对象,用于读写操作。支持多种模式(如读取、写入、追加)和编码格式
参数 | 类型/默认值 | 说明 |
---|---|---|
file | 字符串 | 必需,文件路径(绝对或相对路径)。 |
mode | 字符串('r' ) | 文件模式:'r' (只读)、'w' (写入覆盖)、'a' (追加)、'b' (二进制模式)等。 |
encoding | 字符串(None ) | 文本模式下的编码(如 'utf-8' ),默认使用系统编码。 |
errors | 字符串(None ) | 编码错误处理方式(如 'ignore' 忽略错误)。 |
newline | 字符串(None ) | 控制换行符解析(如 '\n' )。 |
enumerate():将可迭代对象(如列表、字符串)转换为索引-元素对的迭代器,常用于 for
循环中同时获取索引和值
参数 | 类型/默认值 | 说明 |
---|---|---|
iterable | 可迭代对象(如 list 、str ) | 必需,需遍历的对象。 |
start | 整数(0 ) | 可选,索引起始值(如 start=1 从 1 开始计数)。 |
json.loads():将 JSON 格式的字符串解析为 Python 对象(如字典、列表),常用于处理 API 响应或文件中的 JSON 数据
参数 | 类型/默认值 | 说明 |
---|---|---|
json_string | 字符串 | 必需,需解析的 JSON 字符串(如 '{"name": "Alice"}' )。 |
object_hook | 函数(None ) | 可选,自定义 JSON 对象转换为 Python 对象的方式(如解析为自定义类实例)。 |
parse_float | 函数(None ) | 可选,自定义浮点数解析逻辑(如转换为 Decimal 类型)。 |
def load(self):
self.data = []
with open(self.path, encoding="utf8") as f:
for i, line in enumerate(f):
line = json.loads(line)
title = line["title"]
content = line["content"]
self.prepare_data(title, content)
return
Ⅳ、序列编码 encode_sentence
text:字符串,表示需要进行编码的文本内容
max_length:代表编码后序列的最大长度。如果编码后的 input_id
长度小于 max_length
,则会进行填充操作;如果大于 max_length
,可能需要进行截断
with_cls_token:布尔类型的参数,默认值为 True
。当它为 True
时,会在编码后的序列开头添加 [CLS]
标记对应的索引;当为 False
时,则不添加
with_sep_token:布尔类型的参数,默认值为 True
。当它为 True
时,会在编码后的序列结尾添加 [SEP]
标记对应的索引;当为 False
时,则不添加
input_id:列表,用于存储文本编码后得到的索引序列
char:循环变量,在遍历 text
时,char
依次代表 text
中的每个字符
self.padding():补齐或截断输入序列函数
列表.append():向列表末尾添加单个元素,直接修改原列表,无返回值。支持任意数据类型(如整数、字符串、列表)
参数 | 类型 | 说明 |
---|---|---|
x | 任意类型 | 必需,要添加的元素(如 list.append(42) 或 list.append([1,2]) )。 |
#文本到对应的index
#头尾分别加入[cls]和[sep]
def encode_sentence(self, text, max_length, with_cls_token=True, with_sep_token=True):
input_id = []
if with_cls_token:
input_id.append(self.vocab["[CLS]"])
for char in text:
input_id.append(self.vocab.get(char, self.vocab["[UNK]"]))
if with_sep_token:
input_id.append(self.vocab["[SEP]"])
input_id = self.padding(input_id, max_length)
return input_id
Ⅴ、补齐或截断输入序列 padding
input_id:列表,包含了经过编码后的文本索引序列
length:整数,代表期望的序列长度
self.vocab:全局配置字典,存储模型参数与训练设置(如 batch_size
、max_seq_len
),并在初始化时更新词汇相关参数
len():Python 的内置函数,用于返回对象的长度(元素个数)
参数名 | 数据类型 | 是否必需 | 说明 |
---|---|---|---|
object | 字符串、列表、元组、字典、集合等可计算长度的对象 | 是 | 要计算长度的对象,根据对象类型的不同,len() 函数返回其对应的长度值,如字符串的字符数、列表的元素个数等 |
#补齐或截断输入的序列,使其可以在一个batch内运算
def padding(self, input_id, length):
input_id = input_id[:length]
input_id += [self.vocab["[PAD]"]] * (length - len(input_id))
return input_id
Ⅵ、将输入输出转化为序列 prepare_data
title:字符串参数,代表文本数据的标题
content:字符串参数,代表文本数据的具体内容
input_seq:列表,是调用 self.encode_sentence
方法对 content
进行编码后得到的结果
output_seq:列表,是调用 self.encode_sentence
方法对 title
进行编码后得到的结果
self.encode_sentence():序列编码函数
列表.append():列表对象的一个方法,用于在列表的末尾添加一个新元素
参数名 | 数据类型 | 是否必需 | 说明 |
---|---|---|---|
object | 任意 Python 数据类型 | 是 | 要添加到列表末尾的元素。 |
torch.LongTensor():PyTorch 中的一个函数,用于创建一个 torch.LongTensor
类型的张量(Tensor)。torch.LongTensor
是 64 位整数类型的张量,常用于存储索引等整数值。
参数名 | 数据类型 | 是否必需 | 说明 |
---|---|---|---|
data | 数值、列表、元组、torch.Tensor 等 | 否 | 用于初始化张量的数据。如果传入数值,会创建一个包含该数值的标量张量;传入列表或元组,会根据其元素创建相应形状的张量;传入 torch.Tensor ,会尝试将其转换为 torch.LongTensor 类型。如果不提供该参数,则创建一个空张量。 |
size | 整数或整数元组 | 否 | 用于指定张量的形状。例如,torch.LongTensor(3, 4) 会创建一个形状为 (3, 4) 的张量。如果提供了 data 参数,size 参数通常不需要使用。 |
out | torch.Tensor | 否 | 可选参数,用于指定输出的张量对象。如果提供了该参数,生成的张量会存储在这个指定的对象中。 |
#输入输出转化成序列
def prepare_data(self, title, content):
input_seq = self.encode_sentence(content, self.config["input_max_length"], False, False) #输入序列
output_seq = self.encode_sentence(title, self.config["output_max_length"], True, False) #输出序列
gold = self.encode_sentence(title, self.config["output_max_length"], False, True) #不进入模型,用于计算loss
self.data.append([torch.LongTensor(input_seq),
torch.LongTensor(output_seq),
torch.LongTensor(gold)])
return
Ⅶ、 类内魔术方法
① 获取数据长度
self.data:类的一个实例属性,通常是一个可迭代对象
len():Python 的内置函数,用于返回对象的长度或元素个数
参数名 | 数据类型 | 是否必需 | 说明 |
---|---|---|---|
object | 字符串、列表、元组、字典、集合等可计算长度的对象 | 是 | 要计算长度的对象,根据对象类型的不同,len() 函数返回其对应的长度值,如字符数、元素个数、键值对数量等 |
def __len__(self):
return len(self.data)
② 根据索引返回值
index:代表要从 self.data
中获取元素的索引位置
self.data:类的实例属性,通常是一个可迭代对象,像列表、元组或者其他支持索引访问的数据结构
def __getitem__(self, index):
return self.data[index]
Ⅷ、加载词表
token_dict:空字典,用于存储词汇表中每个词(token)及其对应的索引
vocab_path:字符串类型的参数,代表词汇表文件的路径。该文件通常是一个文本文件,每行包含一个词。
f:文件对象,是调用 open
函数打开词汇表文件后返回的对象。
index:循环变量,是 enumerate
函数为每一行分配的索引,从 0 开始计数。
line:在文件读取循环中,line
代表词汇表文件中的每一行内容,是一个字符串。
token:字符串,是对 line
进行 strip
处理后得到的词。
open():Python 内置函数,用于打开文件并返回一个文件对象。
参数名 | 数据类型 | 是否必需 | 说明 |
---|---|---|---|
file | 字符串 | 是 | 要打开的文件的路径,可以是相对路径或绝对路径。 |
mode | 字符串 | 否 | 指定文件的打开模式,常见模式有: - 'r' :只读模式(默认),打开文件用于读取。- 'w' :写入模式,打开文件用于写入,如果文件已存在则覆盖,不存在则创建。- 'a' :追加模式,打开文件用于追加内容,如果文件不存在则创建。- 'b' :二进制模式,可与其他模式组合使用,如 'rb' 表示以二进制只读模式打开文件。- '+' :更新模式,可与其他模式组合使用,如 'r+' 表示以读写模式打开文件。 |
buffering | 整数 | 否 | 可选参数,用于设置缓冲策略: - -1 :使用系统默认缓冲(默认值)。- 0 :不使用缓冲(仅适用于二进制模式)。- 1 :使用行缓冲(仅适用于文本模式)。- 大于 1 的整数:指定缓冲区大小。 |
encoding | 字符串 | 否 | 指定文件的编码格式,常见的编码格式有 'utf-8' 、'gbk' 等。在处理文本文件时,需要指定正确的编码格式,否则可能会出现编码错误。 |
errors | 字符串 | 否 | 指定如何处理编码错误,常见的值有 'strict' (默认,遇到错误抛出异常)、'ignore' (忽略错误)、'replace' (用 ? 替换错误字符)等。 |
newline | 字符串 | 否 | 控制换行符的处理,常见的值有 None (使用系统默认换行符)、'' (不转换换行符)、'\n' (只识别 \n 为换行符)等。 |
enumerate():Python 内置函数,用于将一个可迭代对象(如列表、元组、字符串等)组合为一个索引序列,同时列出数据和数据的索引。
参数名 | 数据类型 | 是否必需 | 说明 |
---|---|---|---|
iterable | 可迭代对象(如列表、元组、字符串等) | 是 | 要进行枚举的可迭代对象。 |
start | 整数 | 否 | 可选参数,指定索引的起始值,默认值为 0。 |
字符串.strip():Python 字符串对象的方法,用于移除字符串首尾的指定字符(默认为空格、制表符、换行符等空白字符)。
参数名 | 数据类型 | 是否必需 | 说明 |
---|---|---|---|
chars | 字符串 | 否 | 可选参数,指定要移除的字符集合。如果不提供该参数,则默认移除字符串首尾的空白字符。 |
def load_vocab(vocab_path):
token_dict = {}
with open(vocab_path, encoding="utf8") as f:
for index, line in enumerate(f):
token = line.strip()
token_dict[token] = index
return token_dict
Ⅸ、封装数据
data_path:字符串类型的参数,代表数据文件的路径
config:全局配置字典,存储模型参数与训练设置(如 batch_size
、max_seq_len
),并在初始化时更新词汇相关参数。
logger:日志记录器对象,用于记录数据加载过程中的各种信息
shuffle:布尔类型的参数,默认值为 True
。当 shuffle
为 True
时,DataLoader
在每个 epoch 开始时会对数据进行洗牌,即打乱数据的顺序;当为 False
时,数据将按照原始顺序加载。
dg:DataGenerator
类的一个实例对象。DataGenerator
类通常用于生成数据,它会根据 data_path
、config
和 logger
对数据进行处理和组织,以便后续使用。
dl: DataLoader
类的一个实例对象。DataLoader
是 PyTorch 中用于批量加载数据的工具,它可以将数据分成多个批次,并且可以进行洗牌、并行加载等操作。
DataGenerator():用于生成数据,它会根据 data_path
、config
和 logger
对数据进行处理和组织,以便后续使用。
DataLoader():PyTorch 中的一个实用类,用于批量加载数据
参数名 | 数据类型 | 是否必需 | 说明 |
---|---|---|---|
dataset | Dataset 类的实例 | 是 | 要加载的数据集对象,该对象需要实现 __len__ 和 __getitem__ 方法,以便 DataLoader 可以获取数据集的长度和访问其中的元素。 |
batch_size | 整数 | 否 | 每个批次加载的数据样本数量,默认值为 1。 |
shuffle | 布尔值 | 否 | 是否在每个 epoch 开始时对数据进行洗牌(打乱顺序),默认值为 False 。设置为 True 有助于提高模型训练的泛化能力。 |
sampler | Sampler 类的实例 | 否 | 自定义的数据采样器,用于控制数据的采样方式。如果指定了 sampler ,则 shuffle 参数将被忽略。 |
batch_sampler | Sampler 类的实例 | 否 | 自定义的批次采样器,用于生成批次索引。如果指定了 batch_sampler ,则 batch_size 、shuffle 和 sampler 参数将被忽略。 |
num_workers | 整数 | 否 | 用于数据加载的子进程数量,默认值为 0,表示在主进程中进行数据加载。设置为大于 0 的值可以实现并行数据加载,提高数据加载的效率。 |
collate_fn | 可调用对象 | 否 | 用于将多个样本组合成一个批次的函数。默认情况下,DataLoader 会使用一个简单的函数将样本组合成批次,但对于一些特殊的数据类型或需要自定义处理的情况,可以提供自定义的 collate_fn 函数。 |
pin_memory | 布尔值 | 否 | 是否将数据加载到 CUDA 固定内存中,默认值为 False 。对于使用 GPU 进行训练的情况,设置为 True 可以加快数据从 CPU 到 GPU 的传输速度。 |
drop_last | 布尔值 | 否 | 是否丢弃最后一个不完整的批次(即样本数量不足 batch_size 的批次),默认值为 False 。 |
timeout | 数值 | 否 | 数据加载的超时时间,单位为秒,默认值为 0。如果在指定时间内无法获取数据,则会抛出异常。 |
worker_init_fn | 可调用对象 | 否 | 每个子进程启动时调用的函数,用于初始化子进程的状态。 |
multiprocessing_context | 字符串或 multiprocessing.context.BaseContext 类的实例 | 否 | 用于多进程数据加载的上下文,默认值为 None 。 |
#用torch自带的DataLoader类封装数据
def load_data(data_path, config, logger, shuffle=True):
dg = DataGenerator(data_path, config, logger)
dl = DataLoader(dg, batch_size=config["batch_size"], shuffle=shuffle)
return dl
Ⅹ、完整代码及测试
# -*- coding: utf-8 -*-
import json
import re
import os
import torch
import numpy as np
from torch.utils.data import Dataset, DataLoader
from collections import defaultdict
"""
数据加载
"""
class DataGenerator:
def __init__(self, data_path, config, logger):
self.config = config
self.logger = logger
self.path = data_path
self.vocab = load_vocab(config["vocab_path"])
self.config["vocab_size"] = len(self.vocab)
self.config["pad_idx"] = self.vocab["[PAD]"]
self.config["start_idx"] = self.vocab["[CLS]"]
self.config["end_idx"] = self.vocab["[SEP]"]
self.load()
def load(self):
self.data = []
with open(self.path, encoding="utf8") as f:
for i, line in enumerate(f):
line = json.loads(line)
title = line["title"]
content = line["content"]
self.prepare_data(title, content)
return
#文本到对应的index
#头尾分别加入[cls]和[sep]
def encode_sentence(self, text, max_length, with_cls_token=True, with_sep_token=True):
input_id = []
if with_cls_token:
input_id.append(self.vocab["[CLS]"])
for char in text:
input_id.append(self.vocab.get(char, self.vocab["[UNK]"]))
if with_sep_token:
input_id.append(self.vocab["[SEP]"])
input_id = self.padding(input_id, max_length)
return input_id
#补齐或截断输入的序列,使其可以在一个batch内运算
def padding(self, input_id, length):
input_id = input_id[:length]
input_id += [self.vocab["[PAD]"]] * (length - len(input_id))
return input_id
#输入输出转化成序列
def prepare_data(self, title, content):
input_seq = self.encode_sentence(content, self.config["input_max_length"], False, False) #输入序列
output_seq = self.encode_sentence(title, self.config["output_max_length"], True, False) #输出序列
gold = self.encode_sentence(title, self.config["output_max_length"], False, True) #不进入模型,用于计算loss
self.data.append([torch.LongTensor(input_seq),
torch.LongTensor(output_seq),
torch.LongTensor(gold)])
return
def __len__(self):
return len(self.data)
def __getitem__(self, index):
return self.data[index]
def load_vocab(vocab_path):
token_dict = {}
with open(vocab_path, encoding="utf8") as f:
for index, line in enumerate(f):
token = line.strip()
token_dict[token] = index
return token_dict
#用torch自带的DataLoader类封装数据
def load_data(data_path, config, logger, shuffle=True):
dg = DataGenerator(data_path, config, logger)
dl = DataLoader(dg, batch_size=config["batch_size"], shuffle=shuffle)
return dl
if __name__ == "__main__":
from config import Config
dl = load_data(Config["train_data_path"], Config, 1)
# print(dl[1])
batch_index = 0
for batch_data in dl:
if batch_index == 1:
print("第 2 个批次的数据:", batch_data)
break
batch_index += 1
3.模型效果评估 evaluate.py
Ⅰ、模型初始化
config:配置对象的字典,包含评估器所需的各种配置信息,如验证数据路径、束搜索大小等
model:模型对象实例
logger:日志记录器对象,用于记录评估过程中的信息
self.config:配置对象的字典
self.model:模型对象实例
self.logger:日志记录器对象
self.valid_data:加载验证数据后得到的 DataLoader
对象,用于提供验证数据的批次
self.reverse_vocab:将词汇表中的索引映射回对应的词的字典,用于解读模型的输出
self.translator:进行序列生成任务的对象,生成预测结果
Translator():实现序列生成任务的类,根据输入序列生成输出序列
dict():创建字典对象,支持多种初始化方式(关键字参数、键值对列表等)
参数 | 类型 | 说明 |
---|---|---|
**kwargs | 关键字参数 | 如 dict(name="Alice", age=25) |
iterable | 可迭代对象 | 如 dict([("name", "Alice"), ("age", 25)]) |
mapping | 映射对象 | 如 dict({"name": "Alice", "age": 25}) |
items():返回字典的键值对视图(dict_items
对象),用于遍历或转换为列表
class Evaluator:
def __init__(self, config, model, logger):
self.config = config
self.model = model
self.logger = logger
self.valid_data = load_data(config["valid_data_path"], config, logger, shuffle=False)
self.reverse_vocab = dict([(y, x) for x, y in self.valid_data.dataset.vocab.items()])
self.translator = Translator(self.model,
config["beam_size"],
config["output_max_length"],
config["pad_idx"],
config["pad_idx"],
config["start_idx"],
config["end_idx"])
Ⅱ、解码
seq:可迭代对象,通常为列表或元组。其中的每个元素代表一个索引值,这些索引值一般是由模型生成的输出序列经过编码后得到的,对应着词汇表中的某个词的索引。
self.reverse_vocab:字典,它的键是词汇表中的索引,值是对应的词。这个字典是通过将词汇表中的键值对反转得到的,用于将索引映射回对应的词。
idx:循环变量,在遍历 seq
的过程中,idx
依次代表 seq
中的每个元素,即每个索引值。
join():将可迭代对象中的元素用指定分隔符连接成字符串
参数 | 类型 | 说明 |
---|---|---|
sep | 字符串 | 分隔符(如 ", " ) |
iterable | 可迭代对象 | 需拼接的元素(如 ["a", "b", "c"] ) |
int():将数值或字符串转换为整数,支持指定进制
参数 | 类型/默认值 | 说明 |
---|---|---|
x | 数值/字符串 | 输入值(如 int("A", 16) 返回 10) |
base | int / 10 | 进制(2-36,或 0 表示自动检测前缀如 0x ) |
def decode_seq(self, seq):
return "".join([self.reverse_vocab[int(idx)] for idx in seq])
Ⅲ、测试模型
epoch:整数,代表当前进行评估的训练轮数。在深度学习训练中,一个 epoch
表示对整个训练数据集的一次完整遍历。
self.logger:日志记录器对象。它用于记录评估过程中的各种信息
self.model:深度学习模型对象。通常是已经训练好的模型,用于进行评估和推理。
self.stats_dict:defaultdict
对象,初始值为整数类型。它用于存储测试结果,例如可以记录不同评估指标的值。
index:循环变量,在遍历 self.valid_data
的过程中,index
代表当前批次的索引,从 0 开始计数。
batch_data:一个 DataLoader
对象,它会将验证数据分成多个批次,每个批次的数据包含输入序列、目标序列和用于计算损失的 gold
序列。
self.valid_data:DataLoader
对象。它是通过调用 load_data
函数加载验证数据后得到的,用于提供验证数据的批次。
input_seqs:列表,包含当前批次中所有样本的输入序列。
target_seqs:列表,包含当前批次中所有样本的目标序列。目标序列是模型期望生成的结果,用于与模型的预测结果进行比较。
gold:列表,用于计算损失
generate:对输入序列进行翻译或生成后得到的结果,列表,包含模型生成的输出序列。
self.translator.translate_sentence():对输入序列进行翻译或生成任务。它接受一个输入序列作为参数,使用模型和束搜索等算法生成输出序列。
self.decode_seq():将索引序列解码为文本。它接受一个索引序列作为参数,通过 self.reverse_vocab
字典将索引映射回对应的词,然后将这些词拼接成一个字符串。
logger.info():记录 INFO 级别的日志
参数 | 类型 | 说明 |
---|---|---|
msg | 字符串 | 日志消息(支持 %s 格式化)。 |
*args | 可变参数 | 填充格式化占位符的值。 |
model.eval():设置模型为评估模式(禁用 Dropout 和 BatchNorm 的训练行为)
cpu():将张量或模型从 GPU 迁移到 CPU
defaultdict():创建默认值字典,自动为缺失键生成默认值(需指定工厂函数)
参数 | 类型 | 说明 |
---|---|---|
default_factory | 工厂函数 | 如 int (默认值 0)、list (默认值空列表) |
**kwargs | 关键字参数 | 初始键值对(如 defaultdict(int, a=1, b=2) ) |
enumerate():返回索引-元素对的迭代器,常用于遍历序列时获取位置信息
参数 | 类型/默认值 | 说明 |
---|---|---|
iterable | 可迭代对象 | 如列表、元组 |
start | int / 0 | 索引起始值(如 enumerate(["a", "b"], start=1) ) |
张量.unsqueeze():在指定维度增加大小为 1 的维度(扩展张量形状)
参数 | 类型 | 说明 |
---|---|---|
dim | int | 需扩展的维度(如 unsqueeze(0) 在第 0 维扩展) |
def eval(self, epoch):
self.logger.info("开始测试第%d轮模型效果:" % epoch)
self.model.eval()
self.model.cpu()
self.stats_dict = defaultdict(int) # 用于存储测试结果
for index, batch_data in enumerate(self.valid_data):
input_seqs, target_seqs, gold = batch_data
for input_seq in input_seqs:
generate = self.translator.translate_sentence(input_seq.unsqueeze(0))
print("输入:", self.decode_seq(input_seq))
print("输出:", self.decode_seq(generate))
break
return
Ⅳ、完整代码及测试
lable:Python 列表,它包含了一系列整数值,代表了某种数据的标签信息
enumerate():返回索引和元素的迭代器
参数 | 类型 | 说明 |
---|---|---|
iterable | 可迭代对象 | 如列表、元组。 |
start | 整数 | 索引起始值(默认 0 )。 |
# -*- coding: utf-8 -*-
import torch
import collections
import io
import json
import six
import sys
import argparse
from loader import load_data
from collections import defaultdict, OrderedDict
from transformer.Translator import Translator
"""
模型效果测试
"""
class Evaluator:
def __init__(self, config, model, logger):
self.config = config
self.model = model
self.logger = logger
self.valid_data = load_data(config["valid_data_path"], config, logger, shuffle=False)
self.reverse_vocab = dict([(y, x) for x, y in self.valid_data.dataset.vocab.items()])
self.translator = Translator(self.model,
config["beam_size"],
config["output_max_length"],
config["pad_idx"],
config["pad_idx"],
config["start_idx"],
config["end_idx"])
def eval(self, epoch):
self.logger.info("开始测试第%d轮模型效果:" % epoch)
self.model.eval()
self.model.cpu()
self.stats_dict = defaultdict(int) # 用于存储测试结果
for index, batch_data in enumerate(self.valid_data):
input_seqs, target_seqs, gold = batch_data
for input_seq in input_seqs:
generate = self.translator.translate_sentence(input_seq.unsqueeze(0))
print("输入:", self.decode_seq(input_seq))
print("输出:", self.decode_seq(generate))
break
return
def decode_seq(self, seq):
return "".join([self.reverse_vocab[int(idx)] for idx in seq])
if __name__ == "__main__":
label = [2, 2, 2, 2, 0, 2, 2, 0, 0, 2, 2, 2, 2, 2, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
print([(i, l) for i, l in enumerate(label)])
4.模型训练文件 main.py
Ⅰ、代码运行流程
├── 全局设置与导入
│ ├── 导入必要的库
│ │ ├── sys
│ │ ├── torch
│ │ ├── os
│ │ ├── random
│ │ ├── numpy as np
│ │ ├── time
│ │ ├── logging
│ │ ├── json
│ │ ├── Config 从 config 模块导入
│ │ ├── Evaluator 从 evaluate 模块导入
│ │ ├── load_data 从 loader 模块导入
│ │ └── Transformer 从 transformer.Models 导入
│ ├── 日志基础配置
│ │ └── logging.basicConfig(level = logging.INFO, format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
│ ├── 获取日志记录器
│ │ └── logger = logging.getLogger(__name__)
│ └── 设置随机种子
│ ├── seed = Config["seed"]
│ ├── random.seed(seed)
│ ├── np.random.seed(seed)
│ ├── torch.manual_seed(seed)
│ └── torch.cuda.manual_seed_all(seed)
├── 定义优化器选择函数
│ └── choose_optimizer(config, model)
│ ├── 获取优化器类型和学习率
│ │ ├── optimizer = config["optimizer"]
│ │ └── learning_rate = config["learning_rate"]
│ ├── 根据优化器类型返回对应优化器
│ │ ├── 如果 optimizer 是 "adam"
│ │ │ └── return torch.optim.Adam(model.parameters(), lr=learning_rate)
│ │ └── 如果 optimizer 是 "sgd"
│ │ └── return torch.optim.SGD(model.parameters(), lr=learning_rate)
├── 定义主函数
│ └── main(config)
│ ├── 创建保存模型的目录
│ │ └── 如果目录不存在
│ │ └── os.mkdir(config["model_path"])
│ ├── 记录配置信息日志
│ │ └── logger.info(json.dumps(config, ensure_ascii=False, indent=2))
│ ├── 初始化 Transformer 模型
│ │ └── model = Transformer(config["vocab_size"], config["vocab_size"], 0, 0, d_word_vec=128, d_model=128, d_inner=256, n_layers=1, n_head=2, d_k=64, d_v=64)
│ ├── 检查 GPU 可用性并迁移模型
│ │ ├── cuda_flag = torch.cuda.is_available()
│ │ └── 如果 cuda_flag 为真
│ │ ├── logger.info("gpu可以使用,迁移模型至gpu")
│ │ └── model = model.cuda()
│ ├── 选择优化器
│ │ └── optimizer = choose_optimizer(config, model)
│ ├── 加载训练数据
│ │ └── train_data = load_data(config["train_data_path"], config, logger)
│ ├── 加载效果测试类
│ │ └── evaluator = Evaluator(config, model, logger)
│ ├── 加载损失函数
│ │ └── loss_func = torch.nn.CrossEntropyLoss(ignore_index=0)
│ ├── 训练循环
│ │ └── for epoch in range(config["epoch"])
│ │ ├── epoch 计数加 1
│ │ ├── 设置模型为训练模式
│ │ │ └── model.train()
│ │ ├── 如果 cuda_flag 为真
│ │ │ └── model.cuda()
│ │ ├── 记录 epoch 开始日志
│ │ │ └── logger.info("epoch %d begin" % epoch)
│ │ ├── 初始化训练损失列表
│ │ │ └── train_loss = []
│ │ ├── 遍历训练数据批次
│ │ │ └── for index, batch_data in enumerate(train_data)
│ │ │ ├── 如果 cuda_flag 为真
│ │ │ │ └── batch_data = [d.cuda() for d in batch_data]
│ │ │ ├── 解包批次数据
│ │ │ │ ├── input_seq, target_seq, gold = batch_data
│ │ │ ├── 前向传播
│ │ │ │ └── pred = model(input_seq, target_seq)
│ │ │ ├── 计算损失
│ │ │ │ └── loss = loss_func(pred, gold.view(-1))
│ │ │ ├── 记录损失
│ │ │ │ └── train_loss.append(float(loss))
│ │ │ ├── 反向传播
│ │ │ │ ├── loss.backward()
│ │ │ │ ├── optimizer.step()
│ │ │ │ └── optimizer.zero_grad()
│ │ ├── 记录该 epoch 平均损失日志
│ │ │ └── logger.info("epoch average loss: %f" % np.mean(train_loss))
│ │ ├── 进行评估
│ │ │ └── evaluator.eval(epoch)
│ ├── 保存模型
│ │ ├── model_path = os.path.join(config["model_path"], "epoch_%d.pth" % epoch)
│ │ └── torch.save(model.state_dict(), model_path)
└── 程序入口
└── if __name__ == "__main__":
└── main(Config)
Ⅱ、导入文件
# -*- coding: utf-8 -*-
import sys
import torch
import os
import random
import os
import numpy as np
import time
import logging
import json
from config import Config
from evaluate import Evaluator
from loader import load_data
#这个transformer是本文件夹下的代码,和我们之前用来调用bert的transformers第三方库是两回事
from transformer.Models import Transformer
from loader import load_data
Ⅲ、日志配置
logger:日志记录器对象,用于记录数据加载过程中的各种信息,比如加载数据的进度、可能出现的错误信息、数据的统计信息(如数据量大小等)。
logging.basicConfig():配置日志系统的基本参数
参数 | 类型 | 说明 |
---|---|---|
level | 整数/常量 | 日志级别(如 logging.INFO )。 |
format | 字符串 | 日志格式(如 '%(asctime)s - %(message)s' )。 |
logging.getLogger():获取日志记录器实例
参数 | 类型 | 说明 |
---|---|---|
name | 字符串 | 日志记录器名称(默认 __name__ )。 |
logging.basicConfig(level = logging.INFO,format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
Ⅳ、设置随机数种子
seed:配置文件中定义的随机数种子
random.seed() 和 np.random.seed():设置随机数生成器的种子,确保可复现性
参数 | 类型 | 说明 |
---|---|---|
seed | 整数 | 随机种子值(如 42 )。 |
torch.manual_seed() 和 torch.cuda.manual_seed_all():设置 PyTorch 的 CPU 和 GPU 随机种子
参数 | 类型 | 说明 |
---|---|---|
seed | 整数 | 随机种子值(如 42 )。 |
seed = Config["seed"]
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
Ⅴ、根据配置文件选择优化器
config:包含各种配置信息的字典
model:深度学习模型对象
optimizer:从 config
中提取出的字符串变量,用于表示优化器的类型
learning_rate:从 config
中提取出的数值变量,用于指定优化器在更新模型参数时的步长大小
torch.optim.Adam():实现 Adam 优化算法(自适应动量估计),结合动量和自适应学习率特性,适用于大多数深度学习任务
参数 | 类型/默认值 | 说明 |
---|---|---|
params | iterable | 模型参数(如 model.parameters() ) |
lr | float / 1e-3 | 学习率 |
betas | Tuple / (0.9,0.999) | 动量衰减系数(一阶和二阶动量) |
eps | float / 1e-8 | 数值稳定性修正项 |
weight_decay | float / 0 | L2 正则化系数 |
amsgrad | bool / False | 是否使用 AMSGrad 变体 |
torch.optim.SGD():实现随机梯度下降(SGD)算法,支持动量和 Nesterov 加速梯度
参数 | 类型/默认值 | 说明 |
---|---|---|
params | iterable | 模型参数 |
lr | float / 必填 | 学习率 |
momentum | float / 0 | 动量系数(0 表示无动量) |
dampening | float / 0 | 动量阻尼系数(与 momentum 配合使用) |
weight_decay | float / 0 | L2 正则化系数 |
nesterov | bool / False | 是否启用 Nesterov 动量 |
model.parameters(): 在 PyTorch 中,model.parameters()
是 nn.Module
类的方法,返回模型中所有可学习参数的生成器(如权重、偏置)。这些参数通常用于优化器的初始化(如 optim.Adam(model.parameters(), lr=0.001)
def choose_optimizer(config, model):
optimizer = config["optimizer"]
learning_rate = config["learning_rate"]
if optimizer == "adam":
return torch.optim.Adam(model.parameters(), lr=learning_rate)
elif optimizer == "sgd":
return torch.optim.SGD(model.parameters(), lr=learning_rate)
Ⅵ、主函数main
1.创建模型保存目录
config:配置文件字典
os.path.isdir():拼接多个路径组件,自动处理跨平台路径分隔符(如 Windows 的 \
和 Linux/macOS 的 /
)
参数 | 类型 | 说明 |
---|---|---|
*paths | 可变字符串 | 多个路径组件,如 os.path.join("dir1", "dir2", "file.txt") 。 |
os.mkdir():Python 的 os
模块中的一个函数,用于创建一个新的目录(文件夹)。如果目录已经存在,该函数会引发 FileExistsError
异常
参数 | 类型 | 描述 | 默认值 | 是否必需 |
---|---|---|---|---|
path | str | 要创建的目录的路径 | 无 | 是 |
mode | int | 设置目录的权限模式(Unix系统有效) | 0o777 | 否 |
* | - | 用于强制使用关键字参数 | - | - |
dir_fd | int | 目录文件描述符 | None | 否 |
#创建保存模型的目录
if not os.path.isdir(config["model_path"]):
os.mkdir(config["model_path"])
2.记录配置信息并加载模型
config:包含各种配置信息的字典
d_word_vec:词嵌入的维度
d_model:模型的隐藏层维度
d_inner:前馈神经网络(Feed Forward Network, FNN)内部隐藏层的维度
n_layers:Transformer
模型的层数
n_head:多头注意力机制(Multi-Head Attention)中的头数
d_k:多头注意力机制中,每个头的键(Key)向量和查询(Query)向量的维度
d_v:多头注意力机制中,每个头的值(Value)向量的维度
logging.info():记录 INFO 级别的日志
参数 | 类型 | 说明 |
---|---|---|
msg | 字符串 | 日志消息(支持 %s 格式化)。 |
*args | 可变参数 | 填充格式化占位符的值。 |
json.dumps():将 Python 对象转换为 JSON 字符串
参数 | 类型 | 描述 | 默认值 | 是否必需 |
---|---|---|---|---|
obj | object | 要序列化的 Python 对象 | 无 | 是 |
skipkeys | bool | 是否跳过非基本类型键(True=跳过) | False | 否 |
ensure_ascii | bool | 是否确保输出 ASCII(True=转义非ASCII字符) | True | 否 |
check_circular | bool | 是否检查循环引用 | True | 否 |
allow_nan | bool | 是否允许 NaN, Infinity 等特殊值 | True | 否 |
cls | class | 自定义编码器类 | None | 否 |
indent | int/str | 缩进空格数或缩进字符串 | None | 否 |
separators | tuple | 项目分隔符,格式为 (', ', ': ') | None | 否 |
default | function | 处理不可序列化对象的函数 | None | 否 |
sort_keys | bool | 是否按键排序 | False | 否 |
#加载模型
logger.info(json.dumps(config, ensure_ascii=False, indent=2))
model = Transformer(config["vocab_size"], config["vocab_size"], 0, 0,
d_word_vec=128, d_model=128, d_inner=256,
n_layers=1, n_head=2, d_k=64, d_v=64,
)
3.检查GPU并迁移模型
cuda_flag:是否使用GPU加速的标志
logger:日志记录器对象,用于记录数据加载过程中的各种信息,比如加载数据的进度、可能出现的错误信息、数据的统计信息(如数据量大小等)。
model:模型的实例
torch.cuda.is_available():检查 GPU 是否可用
logging.info():记录 INFO 级别的日志
参数 | 类型 | 说明 |
---|---|---|
msg | 字符串 | 日志消息(支持 %s 格式化)。 |
*args | 可变参数 | 填充格式化占位符的值。 |
cuda():将张量/模型迁移到 GPU
参数 | 类型 | 说明 |
---|---|---|
device | 整数/字符串 | 可选,指定 GPU 设备号(如 0 或 "cuda:0" )。 |
# 标识是否使用gpu
cuda_flag = torch.cuda.is_available()
if cuda_flag:
logger.info("gpu可以使用,迁移模型至gpu")
model = model.cuda()
4.加载优化器
optimizer:通过调用 choose_optimizer()
函数后得到的优化器对象
config:包含各种配置信息的字典
model:模型的实例
choose_optimizer(): 根据配置信息选择合适的优化器
#加载优化器
optimizer = choose_optimizer(config, model)
5.加载训练数据
train_data:包含训练数据的可迭代对象
config:包含各种配置信息的字典
logger:日志记录器对象,用于记录数据加载过程中的各种信息,比如加载数据的进度、可能出现的错误信息、数据的统计信息(如数据量大小等)。
load_data():加载并处理训练数据
# 加载训练数据
train_data = load_data(config["train_data_path"], config, logger)
6.加载评估器
evaluator:Evaluator
类的一个实例对象,它的主要作用是对模型的性能进行评估
config:包含各种配置信息的字典
model:模型的实例
logger:日志记录器对象,用于记录程序运行过程中的各种信息,包括配置信息、训练过程中的损失值、评估指标等。
Evaluator(): 对模型进行评估。在初始化时,它接收 config
、model
和 logger
作为参数,以便获取必要的配置信息、使用待评估的模型以及记录评估过程中的信息。
#加载效果测试类
evaluator = Evaluator(config, model, logger)
7.加载损失函数
torch.nn.CrossEntropyLoss():计算交叉熵损失,常用于分类任务
参数 | 类型 | 说明 |
---|---|---|
ignore_index | 整数 | 忽略指定类别的损失计算(如填充符 0 )。 |
weight | 张量 | 类别权重(处理类别不平衡)。 |
#加载loss
loss_func = torch.nn.CrossEntropyLoss(ignore_index=0)
8.模型训练主流程 ⭐
① Epoch循环控制
config:配置文件中定义的配置字典
epoch:当前训练轮次(从1开始计数)
range():生成整数序列
参数 | 类型 | 说明 |
---|---|---|
start | 整数 | 起始值(默认 0 )。 |
stop | 整数 | 终止值(不包含)。 |
step | 整数 | 步长(默认 1 )。 |
for epoch in range(config["epoch"]):
epoch += 1
② 模型设置训练模式
model.train():设置模型为训练模式(启用 Dropout、BatchNorm)
model.train()
③ 设备切换
cuda_flag:是否使用GPU加速的标志
batch_data:当前批次的原始数据,通常为 [input_seq, target_seq, gold]
的列表
d:代表 batch_data
中的一个张量(Tensor)
cuda():将张量/模型迁移到 GPU
参数 | 类型 | 说明 |
---|---|---|
device | 整数/字符串 | 可选,指定 GPU 设备号(如 0 或 "cuda:0" )。 |
if cuda_flag:
batch_data = [d.cuda() for d in batch_data]
④ 记录日志
logger:日志记录器,用于输出训练信息
epoch:当前训练轮次(从1开始计数)
logger.info():记录 INFO 级别的日志
参数 | 类型 | 说明 |
---|---|---|
msg | 字符串 | 日志消息(支持 %s 格式化)。 |
*args | 可变参数 | 填充格式化占位符的值。 |
logger.info("epoch %d begin" % epoch)
⑤ 初始化训练损失列表
train_loss:列表,存储当前epoch所有批次的损失值
train_loss = []
⑥ 遍历训练数据批次
index:当前批次的索引号
train_data:训练数据迭代器,每次返回一个批次的数据
batch_data:当前批次的原始数据,通常为 [input_seq, target_seq, gold]
的列表
cuda_flag:是否使用GPU加速的标志
input_seq:输入序列,形状为 [batch_size, seq_len]
target_seq:目标序列(解码器输入),形状同 input_seq
gold:真实标签,形状为 [batch_size, seq_len]
d:代表 batch_data
中的一个张量(Tensor)
enumerate():返回索引和元素的迭代器
参数 | 类型 | 说明 |
---|---|---|
iterable | 可迭代对象 | 如列表、元组。 |
start | 整数 | 索引起始值(默认 0 )。 |
cuda():将张量/模型迁移到 GPU
参数 | 类型 | 说明 |
---|---|---|
device | 整数/字符串 | 可选,指定 GPU 设备号(如 0 或 "cuda:0" )。 |
for index, batch_data in enumerate(train_data):
if cuda_flag:
batch_data = [d.cuda() for d in batch_data]
input_seq, target_seq, gold = batch_data
⑦ 前向传播、计算损失并记录、反向传播
pred:模型输出的预测结果,形状通常为 [batch_size * seq_len, vocab_size]
loss:计算得到的损失值(标量)
view():调整张量形状(类似 reshape
),不改变数据
参数 | 类型 | 说明 |
---|---|---|
*shape | 可变整数 | 新形状,可用 -1 自动推断维度(如 view(2, -1) )。 |
列表.append():向列表末尾添加元素
参数 | 类型 | 说明 |
---|---|---|
x | 任意类型 | 要添加的元素(如 [1, 2].append(3) → [1, 2, 3] )。 |
float():将数值或字符串转换为浮点数
参数 | 类型 | 说明 |
---|---|---|
x | 数值/字符串 | 可选,默认返回 0.0 ;支持 "3.14" 或 "inf" (无穷大)。 |
loss.backward():反向传播计算梯度
参数 | 类型 | 说明 |
---|---|---|
retain_graph | 布尔值 | 是否保留计算图(默认 False ,反向传播后释放)。 |
optimizer.step():根据梯度更新模型参数(如 SGD、Adam)
optimizer.zero_grad():清空所有参数的梯度,防止梯度累积
pred = model(input_seq, target_seq)
loss = loss_func(pred, gold.view(-1))
train_loss.append(float(loss))
loss.backward()
optimizer.step()
optimizer.zero_grad()
⑧ 记录损失进行评估
logger:日志记录器,用于输出训练信息
evaluator:评估器实例
epoch:当前训练轮次(从1开始计数)
train_loss:列表,存储当前epoch所有批次的损失值
logger.info():记录 INFO 级别的日志
参数 | 类型 | 说明 |
---|---|---|
msg | 字符串 | 日志消息(支持 %s 格式化)。 |
*args | 可变参数 | 填充格式化占位符的值。 |
np.mean():计算数组元素的算术平均值(支持多维数组和指定轴计算)
参数 | 类型 | 描述 | 默认值 | 是否必需 |
---|---|---|---|---|
a | array_like | 输入数组(可以是列表、元组、NumPy数组等) | 无 | 是 |
axis | int/tuple | 沿指定轴计算均值(如 0 =列,1 =行) | None | 否 |
dtype | data-type | 计算时使用的数据类型(如 np.float64 ) | 自动推断 | 否 |
out | ndarray | 结果存储的输出数组 | None | 否 |
keepdims | bool | 是否保持原数组维度(True=保留缩减的轴为1) | False | 否 |
where | array_like | 条件筛选,仅计算符合条件的元素 | True | 否 |
evaluator.eval():将模型切换到评估模式(关闭Dropout/BatchNorm等训练特定层)
参数 | 类型 | 描述 | 默认值 | 是否必需 |
---|---|---|---|---|
self | 对象 | PyTorch模型或评估器实例 | 无 | 是(隐式传递) |
logger.info("epoch average loss: %f" % np.mean(train_loss))
evaluator.eval(epoch)
⑨ 完整训练代码
#训练
for epoch in range(config["epoch"]):
epoch += 1
model.train()
if cuda_flag:
model.cuda()
logger.info("epoch %d begin" % epoch)
train_loss = []
for index, batch_data in enumerate(train_data):
if cuda_flag:
batch_data = [d.cuda() for d in batch_data]
input_seq, target_seq, gold = batch_data
pred = model(input_seq, target_seq)
loss = loss_func(pred, gold.view(-1))
train_loss.append(float(loss))
loss.backward()
optimizer.step()
optimizer.zero_grad()
logger.info("epoch average loss: %f" % np.mean(train_loss))
evaluator.eval(epoch)
9.模型保存
model_path:模型保存路径
epoch:当前训练的 epoch 编号
config:配置文件中定义的配置字典
os.path.join():拼接多个路径组件,自动处理跨平台路径分隔符(如 Windows 的 \
和 Linux/macOS 的 /
)
参数 | 类型 | 说明 |
---|---|---|
*paths | 可变字符串 | 多个路径组件,如 os.path.join("dir1", "dir2", "file.txt") 。 |
torch.save():保存 PyTorch 对象(模型、张量等)到文件
参数 | 类型 | 说明 |
---|---|---|
obj | 对象 | 要保存的对象(如模型、张量、字典)。 |
f | 字符串或文件对象 | 保存路径(如 "model.pth" )或已打开的文件对象。 |
model.state_dict():返回模型参数的字典,键为参数名,值为张量
model_path = os.path.join(config["model_path"], "epoch_%d.pth" % epoch)
torch.save(model.state_dict(), model_path)
return
Ⅶ、调用模型预测
Config:配置文件中定义的配置字典
# -*- coding: utf-8 -*-
import sys
import torch
import os
import random
import os
import numpy as np
import time
import logging
import json
from config import Config
from evaluate import Evaluator
from loader import load_data
#这个transformer是本文件夹下的代码,和我们之前用来调用bert的transformers第三方库是两回事
from transformer.Models import Transformer
logging.basicConfig(level = logging.INFO,format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
"""
模型训练主程序
"""
seed = Config["seed"]
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
def choose_optimizer(config, model):
optimizer = config["optimizer"]
learning_rate = config["learning_rate"]
if optimizer == "adam":
return torch.optim.Adam(model.parameters(), lr=learning_rate)
elif optimizer == "sgd":
return torch.optim.SGD(model.parameters(), lr=learning_rate)
def main(config):
#创建保存模型的目录
if not os.path.isdir(config["model_path"]):
os.mkdir(config["model_path"])
#加载模型
logger.info(json.dumps(config, ensure_ascii=False, indent=2))
model = Transformer(config["vocab_size"], config["vocab_size"], 0, 0,
d_word_vec=128, d_model=128, d_inner=256,
n_layers=1, n_head=2, d_k=64, d_v=64,
)
# 标识是否使用gpu
cuda_flag = torch.cuda.is_available()
if cuda_flag:
logger.info("gpu可以使用,迁移模型至gpu")
model = model.cuda()
#加载优化器
optimizer = choose_optimizer(config, model)
# 加载训练数据
train_data = load_data(config["train_data_path"], config, logger)
#加载效果测试类
evaluator = Evaluator(config, model, logger)
#加载loss
loss_func = torch.nn.CrossEntropyLoss(ignore_index=0)
#训练
for epoch in range(config["epoch"]):
epoch += 1
model.train()
if cuda_flag:
model.cuda()
logger.info("epoch %d begin" % epoch)
train_loss = []
for index, batch_data in enumerate(train_data):
if cuda_flag:
batch_data = [d.cuda() for d in batch_data]
input_seq, target_seq, gold = batch_data
pred = model(input_seq, target_seq)
loss = loss_func(pred, gold.view(-1))
train_loss.append(float(loss))
loss.backward()
optimizer.step()
optimizer.zero_grad()
logger.info("epoch average loss: %f" % np.mean(train_loss))
evaluator.eval(epoch)
model_path = os.path.join(config["model_path"], "epoch_%d.pth" % epoch)
torch.save(model.state_dict(), model_path)
return
if __name__ == "__main__":
main(Config)