位置编码(Positional Encoding):为序列注入位置信息的方法详解

Transformer 模型中另一个至关重要的组件——位置编码。这是理解 Transformer 如何处理序列数据的关键一步。

一、 为什么需要位置编码?

首先,我们回顾一下自注意力机制的工作原理。在计算一个词的表示时,它会关注序列中所有其他词,并给它们分配一个权重。这个过程是并行的,并且不关心词与词之间的顺序。举个例子,考虑这两个句子:

  1. “The cat sat on the mat.”
  2. “The mat sat on the cat.”

如果模型只依赖自注意力机制,它可能会认为这两个句子是相似的,因为它们包含完全相同的词汇。显然,这是错误的,因为词的顺序完全改变了句子的含义。

核心问题: RNN 和 LSTM 等模型天生具有顺序性(第 t 步的隐藏状态依赖于第 t-1 步),而 Transformer 的核心组件(自注意力、前馈网络)都是置换不变的——它们不关心输入的顺序。

解决方案: 我们必须以一种明确的方式,将单词在序列中的位置信息注入到模型中。这就是位置编码的作用。

二、 位置编码的两种主要方法

位置编码需要满足几个特性:

  1. 确定性:同一个位置在任何序列中都应该有相同的编码。
  2. 独特性:不同位置的编码应该尽可能不同,以利于模型区分。
  3. 有界性:编码值应该在一定范围内,避免过大或过小。
  4. 可学习性或可计算性:编码应该易于计算或由模型自己学习。

目前主流的位置编码方法有两种:

2.1 正弦/余弦编码

这是 Transformer 论文(“Attention is All You Need”)中提出的方法,也是最经典、最著名的方法。

1、核心思想
不使用可学习的嵌入向量,而是使用一组固定的、预先计算好的正弦和余弦函数来生成位置编码。这种方法巧妙地将位置信息编码到了不同频率的信号中。

2、数学公式
对于序列中的第 pos 个位置,其位置编码 PE(pos) 是一个维度与词嵌入 d_model 相同的向量。向量的每个维度 i 的计算公式如下:
P E ( p o s , 2 i ) = sin ⁡ ( p o s 1000 0 2 i / d m o d e l ) PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i)=sin(100002i/dmodelpos)
P E ( p o s , 2 i + 1 ) = cos ⁡ ( p o s 1000 0 2 i / d m o d e l ) PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i+1)=cos(100002i/dmodelpos)
其中:

  • pos:单词在序列中的位置(从 0 开始)。
  • i:编码向量中的维度索引(从 0 开始)。
  • d_model:词向量和位置编码的维度(论文中为 512)。

3、公式解析与直觉
这个公式看起来很复杂,但我们可以分解它:

  • 奇偶维度分开:对于偶数索引 2i,使用 sin 函数;对于奇数索引 2i+1,使用 cos 函数。这保证了每个位置的编码都是唯一的。
  • 频率变化:分母 10000^(2i/d_model) 是关键。随着维度索引 i 的增加,分母会变得非常大,这意味着 sin/cos 函数的频率会降低
    • 低维度 (i 较小):频率高,编码值变化剧烈。这些维度主要编码局部位置信息(如相邻位置的区别)。
    • 高维度 (i 较大):频率低,编码值变化平缓。这些维度主要编码全局位置信息(如开头和结尾的区别)。
  • 相对位置关系:这种编码方案有一个非常优美的特性:任意两个位置之间的位置编码差,可以被这两个位置的编码线性表示。这意味着模型可以非常容易地学习到相对位置关系,这对于理解语言至关重要。

2.2 可学习的位置编码

这是一种更简单、更直接的方法。
1、核心思想
创建一个可训练的嵌入查找表。就像学习词嵌入一样,模型可以自己学习最优的位置表示。

  • 我们创建一个 max_sequence_length x d_model 的矩阵。
  • 矩阵的第 i 行就是位置 i 的位置编码。
  • 这个矩阵的参数是可学习的,会在模型训练过程中被优化。

2、优缺点

  • 优点
    • 简单直观:实现起来非常简单,就是加一个嵌入层。
    • 灵活性强:模型可以根据数据自己学习最适合的位置表示,不一定局限于正弦/余弦的特定模式。
  • 缺点
    • 泛化能力差:模型在训练时见过的最大长度是 max_sequence_length。如果测试时遇到更长的序列,它就无法处理。而正弦编码因为是一个公式,可以处理任意长度的序列。
    • 需要额外参数:会增加模型的参数量。

2.3 两种编码方式对比

特性正弦/余弦编码可学习位置编码
来源论文原始方法,基于数学函数一种常见的替代方案
核心思想使用不同频率的 sin/cos 波生成编码创建一个可训练的嵌入查找表
优点1. 泛化能力强:可处理任意长度的序列。
2. 捕捉相对位置:数学特性优美。
3. 无额外参数
1. 实现简单
2. 灵活性高,模型可自学习最优表示。
缺点1. 固定模式,可能不如学习到的表示灵活。
2. 数学实现稍复杂。
1. 泛化能力弱,受限于训练时见过的最大长度。
2. 增加模型参数量。
适用场景长文本任务,或对序列长度不敏感的任务。计算资源充足,且序列长度相对固定的任务。

三、 位置编码在模型中的使用

位置编码的使用流程非常简单:

  1. 获取词嵌入:首先,将输入序列中的每个单词通过词嵌入层转换为一个 d_model 维的向量。假设输入序列长度为 seq_len,我们得到一个 seq_len x d_model 的矩阵 E
  2. 获取位置编码:根据输入序列的长度,生成一个同样大小的 seq_len x d_model 的位置编码矩阵 PE
  3. 相加:将词嵌入矩阵 E 和位置编码矩阵 PE 对应元素相加。
    X = E + P E X = E + PE X=E+PE
  4. 输入模型:将相加后的结果 X 输入到 Transformer 的编码器和解码器中。

关键点:这里的加法是逐元素相加。位置编码并没有改变词嵌入本身,而是为它附加了位置信息。Transformer 的后续层(自注意力、前馈网络)会在处理这些相加后的向量时,同时利用到词的语义信息和它们的位置信息。

四、 用 Python 实现正弦/余弦位置编码 (基于PyTorch)

4.1 完整代码

下面我们用 PyTorch 来实现论文中的正弦/余弦位置编码。

import torch
import torch.nn as nn
import math
import matplotlib.pyplot as plt
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000):
        """
        初始化位置编码层
        :param d_model: 词嵌入的维度
        :param max_len: 支持的最大序列长度
        """
        super(PositionalEncoding, self).__init__()
        
        # 创建一个足够大的位置编码矩阵
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        
        # 计算分母项 10000^(2i/d_model)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        
        # 填充位置编码矩阵
        # 使用广播机制,将 position 和 div_term 扩展到合适的形状
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        
        # pe 的形状是 (max_len, d_model),我们需要 (1, max_len, d_model)
        # 这样可以在 batch 维度上进行广播
        pe = pe.unsqueeze(0).transpose(0, 1) # (max_len, 1, d_model)
        
        # 将 pe 注册为 buffer,这样它就不会被视为模型的可训练参数
        # 但会在模型被保存和加载时一同保存
        self.register_buffer('pe', pe)
    def forward(self, x):
        """
        前向传播
        :param x: 输入张量,形状为 (seq_len, batch_size, d_model)
        :return: 加上位置编码后的张量
        """
        # x.shape[0] 是序列长度
        # self.pe[:x.size(0), :] 会取到对应长度的位置编码
        x = x + self.pe[:x.size(0), :]
        return x
# --- 演示 ---
if __name__ == '__main__':
    # 1. 创建参数
    d_model = 128  # 词嵌入维度
    seq_len = 50   # 序列长度
    # 2. 实例化位置编码层
    pos_encoder = PositionalEncoding(d_model)
    # 3. 创建一个随机的词嵌入输入
    # 形状: (seq_len, batch_size, d_model)
    # batch_size=1
    embeddings = torch.randn(seq_len, 1, d_model)
    # 4. 将词嵌入输入到位置编码层
    encoded_embeddings = pos_encoder(embeddings)
    print("原始词嵌入形状:", embeddings.shape)
    print("加上位置编码后形状:", encoded_embeddings.shape)
    print("是否相等:", torch.allclose(embeddings, encoded_embeddings - pos_encoder.pe[:seq_len, :]))
    # 5. 可视化前两个维度的位置编码
    plt.figure(figsize=(10, 8))
    plt.plot(pos_encoder.pe[:seq_len, :, 0].numpy(), label="Dimension 0 (Sin)")
    plt.plot(pos_encoder.pe[:seq_len, :, 1].numpy(), label="Dimension 1 (Cos)")
    plt.title("Sinusoidal Positional Encoding (First 2 Dimensions)")
    plt.xlabel("Position")
    plt.ylabel("Encoding Value")
    plt.legend()
    plt.grid(True)
    plt.show()

4.2 执行结果

运行这段代码,你会生成一个图表,清晰地展示了位置编码中 sincos 部分随位置变化的规律,这与我们前面的理论分析完全吻合。截图如下:
在这里插入图片描述

总结:在现代 Transformer 的实践中,两种方法都有应用。正弦编码因其出色的泛化能力和在长序列任务上的稳定表现,仍然是许多模型(如标准 Transformer、BERT)的首选。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据知道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值