位置编码如何加入?

访客 自然语言处理 2

本文目录导读:

  1. 核心思路:为什么是“加”而不是“拼接”?
  2. 主要加入方式
  3. 总结与对比
  4. 一句话回答你的问题:

这是一个很核心的问题,位置编码的加入方式取决于模型架构,但最主流的方法(尤其是Transformer中)是 “直接加到输入 embedding 上”

下面为你详细拆解几种常见的加入方式及其原理。

核心思路:为什么是“加”而不是“拼接”?

在Transformer中,位置编码通常直接加到词向量(Word Embedding)上,而不是拼接。

  • 加法输入 = word_embedding + position_encoding
  • 拼接输入 = concat(word_embedding, position_encoding)

为什么用加法? 假设词向量的维度是512维。

  1. 维度不变:加法不改变维度,而拼接会变成1024维,导致模型参数剧增,计算量翻倍。
  2. 信息融合:模型本质上是在做向量空间中的旋转、缩放等线性变换,通过加法,模型可以很容易地学习到“哪个维度携带着位置信息,哪个维度携带着语义信息”,相当于每个向量既包含了“是什么词”又包含了“在哪里”这两种信息。

主要加入方式

根据位置编码的类型不同,加入方式略有区别,主要有以下几种:


绝对位置编码(Absolute Position Encoding)

这是最经典、最广泛使用的方式,来自《Attention Is All You Need》论文。

  • 方式:生成一个与词向量维度相同的正弦/余弦函数值向量,然后直接加到词向量上。 [ PE{(pos, 2i)} = sin(pos / 10000^{2i/d{model}}) ] [ PE{(pos, 2i+1)} = cos(pos / 10000^{2i/d{model}}) ]

    • pos 是位置索引(0, 1, 2, ...)
    • i 是维度索引
    • d_model 是向量维度
  • 代码实现(PyTorch风格):

    import torch
    import math
    def create_sinusoidal_encoding(seq_len, d_model):
        position = torch.arange(seq_len).unsqueeze(1)  # (seq_len, 1)
        div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))
        pe = torch.zeros(seq_len, d_model)
        pe[:, 0::2] = torch.sin(position * div_term)  # 偶数维度用sin
        pe[:, 1::2] = torch.cos(position * div_term)  # 奇数维度用cos
        return pe  # (seq_len, d_model)
    # 假设词向量 shape: (batch, seq_len, d_model)
    word_embeddings = torch.randn(2, 10, 512)  # batch=2, seq_len=10, dim=512
    pos_encoding = create_sinusoidal_encoding(10, 512).unsqueeze(0) # (1, 10, 512)
    # 关键步骤:直接相加
    final_input = word_embeddings + pos_encoding
  • 优点:无需训练、有界、可外推(理论上可以处理比训练时更长的序列)。

  • 缺点:模型难以学到“相对位置”关系(比如词A和词B之间的距离)。


可学习位置编码(Learnable Position Encoding)

这是BERT、GPT等主流预训练模型使用的方式。

  • 方式:定义一个大小(max_seq_len, d_model)的可训练矩阵(nn.Embedding),模型在训练过程中,会自动调整每个位置对应的向量值,加入方式依然是直接加到词向量上

  • 代码实现

    import torch.nn as nn
    class LearnablePositionalEncoding(nn.Module):
        def __init__(self, max_len, d_model):
            super().__init__()
            # nn.Embedding 是一个可训练的查找表
            self.pos_embedding = nn.Embedding(max_len, d_model)
        def forward(self, x):
            # x shape: (batch, seq_len, d_model)
            seq_len = x.size(1)
            # 生成位置索引 [0, 1, 2, ..., seq_len-1]
            positions = torch.arange(seq_len, device=x.device)
            # 获取对应位置的可学习向量
            pos_vectors = self.pos_embedding(positions)  # (seq_len, d_model)
            # 关键步骤:直接相加
            return x + pos_vectors.unsqueeze(0)  # 广播到batch维度
  • 优点:模型可以自适应地学习最佳的位置表示,非常灵活,对于固定长度的任务(如BERT的512长度),效果很好。

  • 缺点:不能外推(extrapolate)到比训练时更长的序列(训练时最大长度是512,就不能处理513个token)。


相对位置编码(Relative Position Encoding)

这种方式不直接加到输入层,而是修改注意力分数的计算方式

  • 核心思想:在计算 Query 和 Key 的注意力分数时,不去管它们在绝对位置0和1,而是关心它们之间的相对距离(相隔3个词”)。

  • 加入方式

    • Attention Score 修正:在原始的 Q * K^T 结果上,加上一个基于相对位置 (i-j) 的偏置项 b_{i,j}
    • 公式Attention(Q, K, V) = softmax( (Q * K^T) / sqrt(d_k) + R ) * VR 是相对位置矩阵。
  • 代表模型:Transformer-XL、T5、DeBERTa,T5甚至更进一步,直接使用偏置,不生成位置向量。

  • 优点:处理长序列(外推能力)极强;更符合逻辑(句子中“词与词的关系”往往取决于它们的相对距离而非绝对位置)。

  • 缺点:实现相对复杂,计算量稍大。


旋转位置编码(Rotary Position Encoding, RoPE)

这是目前大模型(如LLaMA, Mistral, Qwen)最流行的方法。

  • 方式:既不直接加到输入,也不直接改注意力分数,而是在计算 QK 向量时,通过旋转矩阵对它们进行旋转变换,从而隐式地编码位置信息。
  • 优点:同时具备绝对位置编码的易用性和相对位置编码的外推能力(理论上可以无限长),能天然地引入相对距离信息。
  • 加入方式(简化理解):Q' = rotate(Q, position)K' = rotate(K, position)Attention = softmax( Q' * K'^T / sqrt(d) ),旋转操作就是在高维空间里对向量进行角度旋转。

总结与对比

编码类型 加入方式 是否可学习 外推能力 代表模型 核心公式/操作
绝对位置 (Sinusoidal) 直接加到输入 较好 原始Transformer input += PE(pos)
绝对位置 (可学习) 直接加到输入 (固定长度) BERT, GPT-2 input += Embedding(pos)
相对位置 修改注意力分数 T5, Transformer-XL score = QK^T + b_{i-j}
旋转位置 (RoPE) 旋转Q/K向量 最强 LLaMA, Mistral, Qwen Q = rotate(Q, pos)

一句话回答你的问题:

最常见的做法是:在Transformer模型的Embedding层之后、多头注意力层之前,直接位置编码矩阵相加到词向量矩阵上。

  • 如果你在用BERT/GPT:用 可学习的Embedding 相加。
  • 如果你在用LLaMA/Qwen:用 RoPE 方法,它不在Embedding层加,而是在计算Attention时对Q/K进行旋转操作。

标签: 绝对位置编码 相对位置编码

抱歉,评论功能暂时关闭!