找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2697

积分

0

好友

353

主题
发表于 3 天前 | 查看: 17| 回复: 0

Dalle’s creative interpretation of my article

位置编码(Positional Encoding)是 Transformer 模型里非常关键的一环,但经常被忽视。很多文章画了很好看的位置编码图,却没有把“为什么它能工作”讲清楚。本文尝试补上这块缺口:用更可解释的方式,从直觉到数学性质,再到代码与可视化,系统梳理两类常用位置编码。

前置要求:如果你已经熟悉 Transformer 架构与 self-attention(自注意力)机制,读起来会更顺畅。


问题:序列顺序很重要

在自然语言处理(NLP)和各种序列数据任务里,顺序决定了含义。比如句子:

“The cat sat on the mat.”

你把词语顺序打乱,很可能就变成了无意义的组合,或者语义完全不同。

Transformer 的高效来自并行计算,但并行也带来一个代价:模型本身并不天然理解“顺序”。与 RNN 不同,Transformer 既不靠递归结构,也不靠卷积去隐式编码时序关系,而是更像把每个 token 当成相互独立的点来处理。这对序列任务显然是个大麻烦:没有顺序,注意力怎么知道“谁在谁前面”?


位置编码:解决方案

为了解决上述限制,Transformer 引入了 Positional Encoding。你可以把它理解为“给 embedding 注入位置信息”的方法,让模型在并行计算时依旧能感知序列结构。

位置编码有多种形式,本文聚焦两种常见且重要的方案:

  1. Absolute Positional Encoding(绝对位置编码,本文主要讲 sinusoidal)
  2. Rotary Position Embedding(RoPE,旋转位置编码)

并建议你把它们与 Transformer 的注意力计算放在一起理解。


Absolute Positional Encoding(Sinusoidal)

绝对位置编码的方法很多,其中 sinusoidal(正弦/余弦) 因为形式优雅、效果稳定,在论文 Attention Is All You Need(Vaswani et al.)之后被广泛采用,并成为经典基线。

核心思路

Sinusoidal PE 的做法可以概括为:

  • 为序列中每个位置生成一个唯一向量(用不同频率的正弦/余弦函数)。
  • 该向量的维度与 token embedding 相同(通常记为
    $$d_{model}$$
    ),因此可以直接相加
  • 不同维度对应不同频率:从高频到低频,形成“频率谱”。
  • 有一个重要性质:相对位移(offset)可以通过线性方式表示,从而更容易学习相对位置关系。

直觉:从比特到波(From Bits to Waves)

为了建立直觉,我们从一个非常基础的表示开始:二进制。

二进制表示(Binary Representation)

我们看二进制计数:

0:  0 0 0 0    8:  1 0 0 0
1:  0 0 0 1    9:  1 0 0 1
2:  0 0 1 0    10: 1 0 1 0
3:  0 0 1 1    11: 1 0 1 1
4:  0 1 0 0    12: 1 1 0 0
5:  0 1 0 1    13: 1 1 0 1
6:  0 1 1 0    14: 1 1 1 0
7:  0 1 1 1    15: 1 1 1 1

观察每一列(每一位)的变化规律:

  • 最右边的 bit(LSB)每次加 1 都翻转(频率 1/2)
  • 倒数第二位每两个数翻转一次(频率 1/4)
  • 倒数第三位每四个数翻转一次(频率 1/8)
  • 依此类推……

也就是说:不同维度携带不同频率的信息。这正是 sinusoidal PE 的直觉来源。只不过二进制是离散的 0/1,而正弦余弦提供了“更平滑、可微、可连续”的表示。


Sinusoidal Positional Encoding 公式与示意

原始 Transformer 给出的做法是:用不同频率的 sin/cos 组合为每个位置生成一个向量(示意图如下)。

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 1

位置编码与 embedding 具有相同维度

$$d_{model}$$
,因此可以相加;其中
$$pos$$
表示位置,
$$i$$
表示维度索引(不同维度对应不同频率的正弦波)。


Python:实现 Sinusoidal PE(代码保持不改动)

下面是生成 sinusoidal positional encoding 的 Python 代码,并画出热力图与部分维度的折线图。

import numpy as np
import matplotlib.pyplot as plt

def sinusoidal_positional_encoding(max_position, d_model):
    position = np.arange(max_position)[:, np.newaxis]
    # The original formula pos / 10000^(2i/d_model) is equivalent to pos * (1 / 10000^(2i/d_model)).
    # I use the below version for numerical stability
    div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))

    pe = np.zeros((max_position, d_model))
    pe[:, 0::2] = np.sin(position * div_term)
    pe[:, 1::2] = np.cos(position * div_term)

    return pe

max_position = 100  # Maximum sequence length
d_model = 128        # Embedding dimension

pe = sinusoidal_positional_encoding(max_position, d_model)

plt.figure(figsize=(12, 8))
plt.imshow(pe, cmap='coolwarm', aspect='auto', vmin=-1, vmax=1)
plt.colorbar()
plt.title('Sinusoidal Positional Encoding')
plt.xlabel('Dimension')
plt.ylabel('Position')
plt.tight_layout()
plt.show()

plt.figure(figsize=(12, 6))
dimensions = [0, 21]
for d in dimensions:
    plt.plot(pe[:, d], label=f'Dim {d}')
plt.legend()
plt.title('Sinusoidal Positional Encoding for Specific Dimensions')
plt.xlabel('Position')
plt.ylabel('Value')
plt.tight_layout()
plt.show()

运行后你会看到两张图。


可视化 1:Heat map(热力图)

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 2

把它类比二进制表格,会更容易读:

  1. 每一行对应序列中的一个 token(类似二进制表格中的“一个数”)。
  2. 每一列对应 embedding 的一个维度(类似二进制的“某一位”)。
  3. 颜色在 -1(蓝)到 1(红)之间连续变化,相当于离散 0/1 的连续版本。

关键观察

  • 第 0 行(position=0)像二进制的 “0000”,是起点。
  • 往下走(位置增大),颜色变化呈现出类似“翻转频率”的规律。
  • 左侧维度变化更快(高频,类比低位 bit)。
  • 右侧维度变化更慢(低频,类比高位 bit)。

可视化 2:Line Plot(某些维度的折线)

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 3

折线图是在追踪单个维度随位置变化的曲线,更像“看某一列 bit 如何翻转”。

关键观察

  • Dimension 0(蓝线)振荡很快,几乎每个位置都变,像二进制最右边的 bit。
  • Dimension 21(橙线)变化慢得多,像更高位的 bit。

相对位置(Relative Positioning)为何“可线性表示”?

原始 Transformer 论文中有一句经典说明:

We chose this function because we hypothesized it would allow the model to easily learn to attend by relative positions, since for any fixed offset k, PE(pos+k) can be represented as a linear function of PE(pos).

直观来说:对于某个固定偏移量(例如往后移动 φ),你希望能用同一个线性变换把当前位置的编码转换成“偏移后”的编码,而不依赖起点位置 t。

对应的数学关系在文中用图展示(以某个频率

$$\omega_k$$
的 sin/cos 对为例):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 4

这说明:

$$PE(t+\phi)$$
可以通过一个与 t 无关的矩阵
$$M$$
作用在
$$PE(t)$$
上得到。

证明过程(按原文图示保留)

1)使用三角函数和角公式展开:

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 5

2)得到两条方程(sin 与 cos):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 6

3)解出矩阵

$$M$$

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 7

4)它看起来非常像线性代数里的旋转矩阵,这也为后面的 RoPE 做了铺垫。

这件事为什么重要?

  • 更容易表示相对距离:注意力可以更轻松地关注 token 间的相对偏移。
  • 更容易学位置相关模式:例如“not”常修饰附近词,它更依赖相对邻近关系,而不是绝对位置。

为什么 Sinusoidal Encoding 有效?

结合上面的直觉与性质,可以总结为:

  • Uniqueness(可区分性):每个位置都有一组高低频混合的独特模式,模型能区分不同位置。
  • Relative position friendly(相对位置友好):固定 offset 可以用线性关系表达,利于学习相对位置模式。
  • Bounded range(数值稳定):sin/cos 输出范围稳定在 [-1, 1],长序列下不易数值爆炸。

编码后 embedding 的差分可视化(Absolute PE)

为了更直观地看“位置信息到底注入到了哪里”,我们做一个小实验:

  • 生成一个 128 维 dummy embedding(代表一个 token)。
  • 分别在 position 0、50、99 处加上位置编码。
  • 计算差分,分离出“位置变化带来的影响”。

代码如下(保持不改动):

np.random.seed(42)

dummy_embedding = np.random.randn(d_model)

positions = [0, 50, 99]

encoded_embeddings = [dummy_embedding + pe[pos] for pos in positions]

fig, axs = plt.subplots(2, 1, figsize=(12, 10))
fig.suptitle('Difference in Encoded Embeddings (Absolute Position Encoding)', fontsize=16)

diff_50 = encoded_embeddings[1] - encoded_embeddings[0]
axs[0].plot(diff_50)
axs[0].set_title('Absolute PE: Encoded Embedding (Position 50) - Encoded Embedding (Position 0)')
axs[0].set_xlabel('Dimension')
axs[0].set_ylabel('Difference')
axs[0].grid(True, linestyle='--', alpha=0.7)

diff_99 = encoded_embeddings[2] - encoded_embeddings[0]
axs[1].plot(diff_99)
axs[1].set_title('Absolute PE: Encoded Embedding (Position 99) - Encoded Embedding (Position 0)')
axs[1].set_xlabel('Dimension')
axs[1].set_ylabel('Difference')
axs[1].grid(True, linestyle='--', alpha=0.7)

plt.tight_layout()
plt.show()

结果图:

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 8

关键观察(原意保留,表述优化)

  • 位置信息更集中在前几个维度:前段维度差异最大,说明位置影响更强。
  • 随维度增大,幅度衰减:越往后的位置差异越小,后段维度受影响更弱。
  • token 语义保存更好:后段维度变化小,意味着更偏向保留 token 本身信息。
  • 差分呈周期性:符合 sin/cos 的周期特征。

可能的含义

  1. embedding 维度可能存在“分工”:前段偏位置、后段偏语义。
  2. 注意力或许会更容易从前段维度中抽取位置关系。
  3. 这在一定程度上平衡了“语义 + 位置”的信息注入方式。

Rotary Position Embedding(RoPE)

Sinusoidal PE 很成功,但后续研究仍在探索更贴合注意力机制的编码方式。RoPE(Su et al.)在论文 RoFormer: Enhanced Transformer with Rotary Position Embedding 中提出,并被许多新模型采用。

RoPE 的基本想法

RoPE 与“加法型”位置编码不同,它做的是“旋转”:

  1. 不再把一个独立 PE 向量加到 embedding 上。
  2. 而是对 token embedding(更准确说是 Q/K)做旋转变换。
  3. 旋转角度由位置 m 与维度共同决定。
  4. 旋转会保持向量范数(norm)不变,同时把位置信息编码进角度中。

RoPE 的直觉:从 self-attention 的点积看起

理解 RoPE,最好从自注意力开始。

1)序列表示

  • 序列 token:
    $$w_1, w_2, \ldots, w_L$$
  • 每个 token 映射到
    $$|D|$$
    维 embedding:
    $$x_1, x_2, \ldots, x_L$$

图示:

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 9

2)注意力的基本计算

在 Transformer 中,每个位置 m 会生成 query

$$q_m$$
和 key
$$k_m$$
,一般由两个映射函数
$$f_q, f_k$$
得到(图示如下):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 10

注意力要同时体现两类信息:

  1. Token Similarity:语义相近的 token,注意力分数应更高(如 cat/dog)。
  2. Positional Proximity:序列上距离更近的词,往往更相关。

注意力打分通常就是点积(图示):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 11

点积可以写成:

$$q \cdot k = ||q||\,||k||\cos(\theta)$$

  • Magnitude(模长项):更多体现 token 语义相似性(embedding 相似)。
  • Angle(夹角项):可以承载位置相关性(尤其当我们把“位置”编码进角度)。

这给了 RoPE 一个非常自然的切入点:

  • 保持模长不变(不破坏语义相似性)
  • 通过旋转改变角度(注入位置信息)

RoPE 的目标:让注意力更依赖相对距离

RoPE 的核心诉求可以口语化理解为:

让注意力分数更多取决于“相距多远”,而不是“各自处在第几个位置”。

数学上,文中用图表达为:

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 12

也就是:注意力在 positions m 与 n 之间的关系应主要由内容

$$x_m, x_n$$
和相对位移
$$m-n$$
决定。

这就像你更关心 “not” 是否紧挨着 “happy”,而不是它们是不是句子的第 5、6 个词。

同时,RoPE 也在“绝对位置”与“相对位置”之间做了折中:

  1. 每个 token 通过旋转仍保留一定绝对位置信息。
  2. 在点积交互时,相对角度更容易反映相对距离。
  3. 模型因此更容易同时利用绝对与相对结构。

RoPE Formulation(原文链接与说明)

推导过程原文选择跳过(避免文章过长);可参考 ROFORMER 论文,或以下解释链接(纯 URL 前后加空格以便阅读):

https://pub.towardsai.net/an-in-depth-exploration-of-rotary-position-embedding-rope-ac351a45c794


数学形式:2D 情况(原图保留)

对于 query vector:

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 13

旋转矩阵

$$R_{\theta,m}$$
是一个 2×2 矩阵(原图):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 14

其中

$$\theta \in \mathbb{R}$$
为预设的非零常数。我们用该矩阵旋转向量
$$q$$
,得到旋转后的
$$\hat{q}$$

可视化如下(黑色为原向量,不同 m 对应不同旋转后的向量):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 15

2D embedding represented at different positions


更一般的形式(d 维,原图保留)

一般情况下,将 d 维空间拆成

$$d/2$$
个二维子空间,对每个子空间分别做旋转。

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 16

拆分示意(“Enhanced” 的 embedding 被分成

$$d/2$$
个子空间):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 17

The embedding vector for “Enhanced” is divided into d/2 subspaces

每个子空间使用旋转矩阵(原图):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 18

其中参数(原图):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 19

关键观察

  • $$\theta$$
    会随子空间索引增大而衰减。
  • 前面的子空间(较小索引)旋转更明显;越到后面,旋转几乎可以忽略。

Python:RoPE 旋转矩阵(代码保持不改动)

下面是 RoPE 的 Python 实现:构造一个 3D 矩阵,每个 slice 对应位置 m 的旋转矩阵。也就是:第一个 Q/K 乘第一个 slice,第二个乘第二个……以此类推。

import numpy as np
import matplotlib.pyplot as plt

def get_rotary_matrix(context_len: int, d_model: int) -> np.ndarray:
    """
    Generate the Rotary Matrix for ROPE
    Args:
        context_len (int): context len
        d_model (int): embedding dim
    Returns:
        np.ndarray: the rotary matrix of dimension context_len x d_model x d_model
    """
    R = np.zeros((context_len, d_model, d_model))
    positions = np.arange(1, context_len + 1)[:, np.newaxis]
    # Create matrix theta (shape: context_len x d_model // 2)
    slice_i = np.arange(0, d_model // 2)
    theta = 10000. ** (-2.0 * slice_i.astype(float) / d_model)
    m_theta = positions * theta
    # Create sin and cos values
    cos_values = np.cos(m_theta)
    sin_values = np.sin(m_theta)
    # Populate the rotary matrix R using advanced indexing
    R[:, 2*slice_i, 2*slice_i] = cos_values
    R[:, 2*slice_i, 2*slice_i+1] = -sin_values
    R[:, 2*slice_i+1, 2*slice_i] = sin_values
    R[:, 2*slice_i+1, 2*slice_i+1] = cos_values
    return R

Efficient Implementation(原图保留)

由于旋转矩阵非常稀疏,论文也给出了更高效的计算方式(原图):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 20

你可以把它当成一个练习:在不显式构造

$$d \times d$$
矩阵的情况下,直接对向量做旋转。


可视化 RoPE:不同位置的旋转效果

下面的代码生成 dummy embedding,并比较 position 0、50、99 的旋转后向量。

context_len = 100
d_model = 128
R = get_rotary_matrix(context_len, d_model)

np.random.seed(42)

# Generate dummy embedding
dummy_embedding = np.random.randn(d_model)

# Positions to encode
positions = [0, 50, 99]

# Create encoded embeddings
encoded_embeddings = [R[pos] @ dummy_embedding for pos in positions]

# Create subplots
fig, axs = plt.subplots(3, 1, figsize=(7, 10))
fig.suptitle('Comparison of Dummy Embedding and Encoded Embeddings', fontsize=16)

# Plot dummy embedding and encoded embedding at position 0
axs[0].plot(dummy_embedding, label='Dummy Embedding')
axs[0].plot(encoded_embeddings[0], label='Encoded Embedding (Position 0)')
axs[0].set_title('Dummy Embedding vs Encoded Embedding (Position 0)')
axs[0].set_xlabel('Dimension')
axs[0].set_ylabel('Value')
axs[0].legend()
axs[0].grid(True, linestyle='--', alpha=0.7)

# Plot dummy embedding and encoded embedding at position 50
axs[1].plot(dummy_embedding, label='Dummy Embedding')
axs[1].plot(encoded_embeddings[1], label='Encoded Embedding (Position 50)')
axs[1].set_title('Dummy Embedding vs Encoded Embedding (Position 50)')
axs[1].set_xlabel('Dimension')
axs[1].set_ylabel('Value')
axs[1].legend()
axs[1].grid(True, linestyle='--', alpha=0.7)

# Plot dummy embedding and encoded embedding at position 99
axs[2].plot(dummy_embedding, label='Dummy Embedding')
axs[2].plot(encoded_embeddings[2], label='Encoded Embedding (Position 99)')
axs[2].set_title('Dummy Embedding vs Encoded Embedding (Position 99)')
axs[2].set_xlabel('Dimension')
axs[2].set_ylabel('Value')
axs[2].legend()
axs[2].grid(True, linestyle='--', alpha=0.7)

plt.tight_layout()
plt.show()

效果图:

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 21

观察橙色线(旋转后的向量)在 position 0、50、99 的变化,你会再次看到:

  • 前几个子空间旋转幅度明显更大
  • 后面子空间旋转越来越弱

把它和 Absolute PE 的差分观察对照,会发现很多结论一致。

关键观察

  • 位置信息更集中在前几个维度(TRUE HERE ALSO)
  • 随维度增大,幅度衰减(TRUE HERE ALSO)
  • token 语义保存更好(TRUE HERE ALSO)

Implications

  1. 维度分工依旧存在:前段偏位置,后段偏语义。
  2. 注意力可能更容易利用这些结构化差异。
  3. 这种设计在“注入位置”与“保留语义”之间维持了平衡。

实践对比:Sinusoidal vs RoPE

从 RoFormer 论文给出的结果来看(原图保留):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 22

The RoFormer gives better BLEU scores compared to its baseline alternative Vaswani et al. [2017] on the WMT 2014 English-to-German translation task

以及训练过程曲线(原图):

Transformer位置编码详解:Sinusoidal与RoPE原理 - 图片 - 23

Rotary positional encoding 看起来拥有更好的收敛速度。

并且,很多近期模型(例如 Gemma 2、LLama 3 等)都使用了 RoPE 编码。


References(纯 URL 前后加空格)

BibTeX:

@article{paleti2024:positionalencoding,
  title   = "Positional Encoding Explained: A Deep Dive into Transformer PE",
  author  = "Paleti, Nikhil Chowdary",
  journal = "Medium",
  year    = "2024"
}

延伸阅读

如果你想继续系统整理 Transformer、位置编码、注意力机制与工程实现细节,可以到 云栈社区 查找相关主题与讨论。

另外,本文的代码示例基于 Python 生态(NumPy/Matplotlib),便于你快速复现图表与实验。




上一篇:单片机裸机多任务处理:状态机、协作调度等5种方法详解
下一篇:详解金融分布式事务架构设计:核心模式与选型指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-23 23:13 , Processed in 0.279674 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表