Chapter 3: 大语言模型基础 Part 4:Transformer 的骨血——前馈网络与位置编码

上一篇我们详细解析了多头注意力机制(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 能够收敛的关键。

这个操作由两个部分组成:

  1. 残差连接 (Add):该操作将子模块的输入 xxx 直接加到该子模块的输出 Sublayer(x)Sublayer(x)Sublayer(x) 上。

    • 公式Output=x+Sublayer(x)Output=x+Sublayer(x)Output=x+Sublayer(x)

    • 作用:这一结构解决了深度神经网络中的梯度消失 (Vanishing Gradients) 问题。在反向传播时,梯度可以绕过子模块直接向前传播,从而保证了即使网络层数很深,模型也能得到有效的训练 。

  2. 层归一化 (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 完美满足了这些需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值