上一篇我们详细解析了多头注意力机制(Multi-Head Attention)。本篇将继续填充 Transformer 骨架中剩余的组件,使其成为一个真正可运行的完整架构。
3.1.2 Transformer 架构解析 (续)
(3) 前馈神经网络 (Feed-Forward Networks)
在每个 Encoder 和 Decoder 层中,多头注意力子层之后都跟着一个逐位置前馈网络 (Position-wise Feed-Forward Network, FFN)。
如果说注意力层的作用是从整个序列中“动态地聚合”相关信息,那么前馈网络的作用就是从这些聚合后的信息中提取更高阶的特征。
核心特性:“逐位置”
这个名字的关键在于“逐位置”。它意味着这个前馈网络会独立地作用于序列中的每一个词元向量。
换句话说,对于一个长度为 seq_len 的序列,这个 FFN 实际上会被调用 seq_len 次,每次处理一个词元。重要的是,所有位置共享的是同一组网络权重。这种设计既保持了对每个位置进行独立加工的能力,又大大减少了模型的参数量。
结构与公式
这个网络的结构非常简单,由两个线性变换和一个 ReLU 激活函数组成 :
FFN(x)=max(0,xW1+b1)W2+b2FFN(x)=max(0,xW_{1}+b_{1})W_{2}+b_{2}FFN(x)=max(0,xW1+b1)W2+b2
其中:
-
xxx 是注意力子层的输出。
-
W1,b1,W2,b2W_{1},b_{1},W_{2},b_{2}W1,b1,W2,b2 是可学习的参数。
💡 深度解析:
通常,第一个线性层的输出维度d_ffd\_ffd_ff 会远大于输入的维度 d_modeld\_modeld_model(例如 d_ff=4×d_modeld\_ff=4 \times d\_modeld_ff=4×d_model),经过 ReLU 激活后再通过第二个线性层映射回 d_modeld\_modeld_model 维度 。
这种**“先扩大再缩小”的模式,也被称为瓶颈结构 (Bottleneck Structure)**,被认为有助于模型学习更丰富的特征表示。
代码实现:PositionwiseFeedForward
在我们的 PyTorch 骨架中,我们可以用以下代码来实现这个模块 :
class PositionwiseFeedForward(nn.Module):
"""位置前馈网络模块"""
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.dropout = nn.Dropout(dropout)
self.linear2 = nn.Linear(d_ff, d_model)
self.relu = nn.ReLU()
def forward(self, x):
# x 形状: (batch_size, seq_len, d_model)
# 1. 线性变换 1 (扩大维度)
x = self.linear1(x)
# 2. ReLU 激活
x = self.relu(x)
# 3. Dropout
x = self.dropout(x)
# 4. 线性变换 2 (恢复维度)
x = self.linear2(x)
# 最终输出形状: (batch_size, seq_len, d_model)
return x
(4) 残差连接与层归一化 (Add & Norm)
在 Transformer 的每个编码器和解码器层中,所有子模块(如多头注意力和前馈网络)都被一个 Add & Norm 操作包裹 。这个看似微小的细节,却是训练深层 Transformer 能够收敛的关键。
这个操作由两个部分组成:
-
残差连接 (Add):该操作将子模块的输入 xxx 直接加到该子模块的输出 Sublayer(x)Sublayer(x)Sublayer(x) 上。
-
公式:Output=x+Sublayer(x)Output=x+Sublayer(x)Output=x+Sublayer(x)
-
作用:这一结构解决了深度神经网络中的梯度消失 (Vanishing Gradients) 问题。在反向传播时,梯度可以绕过子模块直接向前传播,从而保证了即使网络层数很深,模型也能得到有效的训练 。
-
-
层归一化 (Norm):该操作对单个样本的所有特征进行归一化,使其均值为 0,方差为 1。
- 作用:这解决了模型训练过程中的内部协变量偏移 (Internal Covariate Shift) 问题,使每一层的输入分布保持稳定,从而加速模型收敛并提高训练的稳定性 。
(5) 位置编码 (Positional Encoding)
我们已经了解,Transformer 的核心是自注意力机制,它通过计算序列中任意两个词元之间的关系来捕捉依赖。然而,这种计算方式有一个固有的问题:它本身不包含任何关于词元顺序或位置的信息。
对于自注意力来说,“agent learns” 和 “learns agent” 这两个序列是完全等价的,因为它只关心词元之间的关系,而忽略了它们的排列。为了解决这个问题,Transformer 引入了 位置编码 (Positional Encoding) 。
核心思想
为输入序列中的每一个词元嵌入向量,都额外加上一个能代表其绝对位置和相对位置信息的“位置向量”。
这个位置向量不是通过学习得到的,而是通过一个固定的数学公式直接计算得出。这样一来,即使两个词元(例如,两个都叫 agent 的词元)自身的嵌入是相同的,但由于它们在句子中的位置不同,它们最终输入到 Transformer 模型中的向量就会因为加上了不同的位置编码而变得独一无二 。
原论文中提出的位置编码使用正弦和余弦函数来生成,其公式如下 :
PE(pos,2i)=sin(pos100002i/dmodel)PE_{(pos,2i)}=sin(\frac{pos}{10000^{2i/d_{model}}})PE(pos,2i)=sin(100002i/dmodelpos)
PE(pos,2i+1)=cos(pos100002i/dmodel)PE_{(pos,2i+1)}=cos(\frac{pos}{10000^{2i/d_{model}}})PE(pos,2i+1)=cos(100002i/dmodelpos)
其中:
-
pospospos 是词元在序列中的位置(例如,0, 1, 2, …)
-
iii 是位置向量中的维度索引(从 0 到 dmodel/2d_{model}/2dmodel/2)
-
dmodeld_{model}dmodel 是词嵌入向量的维度
代码实现:PositionalEncoding
现在,我们来实现 Positional Encoding 模块,并完成我们 Transformer 骨架代码的最后一部分 。
class PositionalEncoding(nn.Module):
"""为输入序列的词嵌入向量添加位置编码"""
def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000):
super().__init__()
self.dropout = nn.Dropout(p=dropout)
# 创建一个足够长的位置编码矩阵
# pe 的大小为 (max_len, d_model)
pe = torch.zeros(max_len, d_model)
# 创建位置索引向量 (0, 1, ..., max_len-1)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
# 计算分母中的除数项
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
# 偶数维度使用 sin
pe[:, 0::2] = torch.sin(position * div_term)
# 奇数维度使用 cos
pe[:, 1::2] = torch.cos(position * div_term)
# 增加一个 batch 维度: (1, max_len, d_model)
pe = pe.unsqueeze(0)
# 将 pe 注册为 buffer,这样它就不会被视为模型参数(不参与梯度更新),
# 但会随模型保存和加载(state_dict),也会随模型移动设备(to(device))
self.register_buffer('pe', pe)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
x: 包含词嵌入的输入张量,形状为 (batch_size, seq_len, d_model)
"""
# x.size(1) 是当前输入的序列长度
# 将位置编码截取到与输入相同的长度,并加到输入向量上
x = x + self.pe[:, :x.size(1)]
return self.dropout(x)
💡 注解:
代码中使用 register_buffer 是一个很重要的 PyTorch 技巧。因为位置编码是固定的数学公式,不需要训练(即不是 Parameter),但它是模型状态的一部分,需要随模型一起保存到 .pth 文件中,也需要随 model.cuda() 一起迁移到 GPU 上。register_buffer 完美满足了这些需求。

被折叠的 条评论
为什么被折叠?



