🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
位置编码作为Transformer模型的核心组件,其重要性往往被低估。尽管相关研究常以可视化图表呈现,但鲜有文章深入解析其运作机制。本文将从原理层面系统剖析这一关键技术。
若您已掌握Transformer架构和自注意力机制,将更易理解本文的讨论内容。
一、序列的重要性
在自然语言处理和序列数据分析中,元素顺序起着决定性作用。例如,句子"猫坐在垫子上"一旦打乱词序,就可能产生无意义表达或完全改变原意。
Transformer之所以高效,得益于其并行处理能力,但这同时也使其缺乏对顺序的天然理解。与循环神经网络不同——后者通过内置机制处理序列顺序——Transformer既不采用循环结构,也不使用卷积操作,而是将每个数据点视为独立个体。这一特性给序列数据处理带来了显著挑战。
二、位置编码:解决方案
为了解决这一局限性,Transformer模型采用了一种称为位置编码的技术。这是让Transformer理解序列顺序的秘诀。解决问题的关键在于找到一种将位置信息直接编码到输入嵌入中的方法。
位置编码有多种类型,但本文将重点介绍两种重要且流行的方式:
- 绝对位置编码
- 旋转位置嵌入(RoPE)
三、绝对位置编码
正弦曲线位置编码因其简洁高效的特点成为主流方案,尤其在Vaswani等人提出的Transformer架构中成功应用后,迅速成为自然语言处理领域的标配技术。这项技术被收录在开创性论文《Attention Is All You Need》中。
该编码方案的核心机制是:
- 为序列各位置生成独有向量,通过正弦和余弦函数的组合实现
- 编码维度与词嵌入保持一致,支持直接向量相加
- 不同维度对应不同频率波形,形成连续频谱分布
这种设计的关键优势在于:模型能够通过简单的线性变换,轻松捕捉位置间的相对关系,因为固定位置偏移的编码可以表示为当前位置编码的线性组合。
四、从二进制到波形
为了理解绝对位置编码的直觉,让我们从基础开始:二进制如何表示数字。
二进制表示
思考我们如何用二进制计数:
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
请注意观察每一位(列)的变化规律:
最低有效位(最右侧)随每个数字交替变化(频率为1/2) 右数第二位每两个数字变化一次(频率为1/4) 右数第三位每四个数字变化一次(频率为1/8) 依此类推... 这种差异化频率的变化模式正是位置编码的核心思想。不过我们并非采用离散的二进制位表示,而是使用更平滑的正弦波和余弦波函数。
在原版Transformer论文中提出的正弦位置编码方案如下:
- 为序列中的每个位置生成一个数字向量
- 向量中的每个数值都由正弦或余弦函数计算得到
- 向量的不同维度采用不同的频率设置
位置编码和嵌入的维度保持一致,均为d_model,这使得两者可以直接相加。其中,pos表示位置索引,i代表维度索引。具体而言,位置编码的每个维度都对应一个独立的正弦波函数。
以下是实现这一功能的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]
# 原始公式pos/10000^(2i/d_model)等价于pos*(1/10000^(2i/d_model))。
# 为了数值稳定性,我使用以下版本
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 # 最大序列长度
d_model = 128 # 嵌入维度
pe = sinusoidal_positional_encoding(max_position, d_model)
plt.figure(figsize=(12, 8))
plt.imshow(pe, cmap='温度', aspect='auto', vmin=-1, vmax=1)
plt.colorbar()
plt.title('正弦位置编码')
plt.xlabel('维度')
plt.ylabel('位置')
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('特定尺寸的正弦位置编码')
plt.xlabel('位置')
plt.ylabel('值')
plt.tight_layout()
plt.show()
运行此代码时,您将看到两张图表:
1、热力图 
此图类似于我们的二进制表示表,但具有连续谱而非离散的0和1:
-
每一行代表序列中的一个标记,类似于二进制表中每一行代表一个数字。
-
每一列对应标记编码中的一个维度,类似于二进制中的位位置。
-
颜色表示在-1(蓝色)和1(红色)之间振荡的值,这是二进制中0和1的连续版本。
关键观察:
— 第一行(位置0)类似于我们的二进制“0000”,作为起始点。
— 随着行数向下移动(位置增加),我们看到颜色变化的模式,类似于二进制计数中的位翻转。
— 最左侧的列(较低维度)变化迅速,类似于二进制中的最低有效位。
— 最右侧的列(较高维度)变化较慢,类似于二进制中的最高有效位。
2、折线图
折线图聚焦于特定维度及其如何随位置变化,直接呼应了我们的二进制类比:
-
每条线代表一个特定维度,类似于追踪二进制表格中的单列(比特位)。
-
X轴显示序列中的位置,相当于我们二进制示例中的递增计数。
关键观察: — 维度0(蓝线)快速振荡,随每个位置变化。这与二进制中最右侧比特的表现完全一致——每次计数都会翻转。
— 维度21(橙线)变化更为缓慢,类似于二进制表示中更左侧的比特位翻转频率较低的现象。
相对位置 原版Transformer论文作者指出:
我们选择此函数是因为假设它能让模型通过相对位置轻松学习注意力机制,因为对于任何固定偏移量k,PE(pos+k)都可以表示为PE(pos)的线性函数
但这在实践中意味着什么?让我们来详细解析。
这一特性的核心在于一个优美的数学关系。对于每个对应于频率ω_k的正弦-余弦对,存在一个线性变换M,可以将位置移动任意固定偏移量φ。用数学术语表达:
该等式表明,我们可以将任意位置(t + φ)的位置编码表示为位置 t 处编码的线性变换。关键在于这个变换矩阵 M 与 t 无关,这意味着无论起始的绝对位置如何,变换方式都保持一致。
1、我们首先利用三角函数的加法公式展开等式右侧。
2、这一展开式为我们提供了两个方程:一个是关于sin(ω_k · (t + φ))的方程,另一个是关于cos(ω_k · (t + φ))的方程
3、通过求解这些方程,我们发现变换矩阵M为:
按回车键或点击以查看完整尺寸的图像
4、有趣的是,这个矩阵与线性代数中的旋转矩阵非常相似。
3、重要性解析
-
高效计算相对位置: 通过简单的线性变换,模型就能基于当前位置编码计算出任意相对位置的位置编码。这种机制让注意力机制能够精准捕捉词元间的相对距离关系。
-
位置模式学习优势: 虽然无法完全实现平移不变性,但这种设计显著提升了模型学习相对位置依赖模式的能力。例如,它能更自然地掌握"not"等修饰词通常影响邻近词汇的语法规律,无论这些词出现在句子的任何位置。
五、正弦编码的有效性原理
-
唯一标识性: 每个位置都获得独特的高低频成分组合,确保模型能准确区分不同位置。
-
相对位置特性: 得益于正弦函数的数学特性,任意位置编码都能表示为其他位置编码的线性函数,这极大简化了相对位置的学习过程。
-
数值稳定性: 正弦和余弦函数的值域始终保持在[-1,1]范围内,有效避免了超长序列中位置编码的数值爆炸或消失问题。
六、编码嵌入可视化方法
为深入理解绝对位置编码对输入嵌入的影响,我们设计了一个可视化方案:
- 构建一个128维的虚拟词元嵌入向量
- 分别进行三种位置编码:
- 位置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('编码嵌入的差异(绝对位置编码)', fontsize=16)
diff_50 = encoded_embeddings[1] - encoded_embeddings[0]
axs[0].plot(diff_50)
axs[0].set_title('绝对位置编码:编码嵌入(50位)-编码嵌入(0位)')
axs[0].set_xlabel('维度')
axs[0].set_ylabel('差异')
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('绝对编码:编码嵌入(99位)-编码嵌入(0位)')
axs[1].set_xlabel('维度')
axs[1].set_ylabel('差异')
axs[1].grid(True, linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
关键发现
位置编码特征
- 空间分布:位置信息主要集中在嵌入向量的前几个维度
- 衰减效应:位置编码的影响随维度增加而递减
- 语义保留:高维度区域主要保留词元语义信息
- 周期特性:差异变化呈现正弦函数的周期性规律
2、维度功能分区
- 位置感知区:低维度负责编码位置关系
- 语义存储区:高维度专注存储词元特征
- 注意力机制:这种分区结构引导模型分层处理不同信息
3、信息整合优势
- 双流处理:同时保留位置上下文和词元语义
- 动态平衡:实现位置信息与语义信息的优化配置
七、旋转位置嵌入(RoPE)
要理解旋转位置嵌入(RoPE)的精妙之处,我们需要深入自注意力层的机制。
1、序列表示
我们有一系列标记(单词或子词)w₁, w₂, ..., wₗ,该序列的长度为L。 每个标记会被转换为高维空间中的向量(称为嵌入向量),设这个空间的维度为|D|。 最终得到的是这个|D|维空间中的一组向量x₁, x₂, ..., xₗ。
2、注意力计算基础
在Transformer模型中,序列中的每个位置(m)都有一个查询向量(qₘ)和一个键向量(kₘ)。
这些向量通过两个函数 f_q 和 f_k 生成:
在计算注意力矩阵时,我们旨在编码两个关键特征:
- 词元相似性:具有相似嵌入向量的词元应获得更高的注意力分数。例如,"猫"和"狗"由于常出现在相似语境中,可能获得较高分数。
- 位置邻近性:序列中相邻较近的词通常应获得更高分数,因为它们更可能存在语义关联。
为了量化一个词对另一个词的"关注度",我们使用称为注意力分数的指标。对于任意位置m和n,其计算公式为:查询向量q_m与键向量k_n的点积。这个点积运算同时包含了词元相似性和位置信息。
在此点积运算中:(q · k = ||q|| ||k|| cos(θ))
幅度:影响词元相似性。q和k的幅度相似度(记为||q|| · ||k||)对应词元嵌入相似性。
角度:Q和K的角度(θ_m和θ_n)影响位置相似性。
该设计具有重要特性:
- 角度分量仅取决于向量在序列中的位置,与实际词元嵌入无关
- 词元相似性与角度无关
RoPE的方法
RoPE巧妙地利用了这种视角:
它并非添加独立的位置编码,而是根据序列中的位置对查询向量和键向量进行旋转。
这种旋转在保持向量模长(保留词元相似性)的同时,通过角度变化编码位置信息。
当计算旋转后向量的点积时:
- 模长仍能反映词元相似度
- 向量间的相对角度则体现位置邻近性
该方法使RoPE能将词元信息和位置信息无缝整合到单一运算中,相比传统位置编码方式更高效且可能更有效。
关于旋转机制,RoPE的核心思想是:"让注意力分数仅取决于词元间距,而非绝对位置"。
用数学语言表述即为:
该方程表达的是:位置m和n之间的注意力应仅取决于它们的内容(xₘ和xₙ)以及相距多远(m — n)。
就好比更关注"not"是否紧接在"happy"之前,而非它们是否是句中的第5和第6个词。
RoPE在编码绝对位置信息与促进相对位置关系计算之间取得了平衡:
每个词元的嵌入通过应用旋转保留了其绝对位置信息;
而注意力机制中旋转嵌入之间的交互,使模型更容易计算和利用相对位置信息。
这种方法使模型能同时感知绝对和相对位置,从而可能更细致地理解序列结构。
八、Rope 数学公式
1、二维数学公式
对于查询向量
旋转矩阵R_{𝜃,m}是一个2x2矩阵,其公式为:
其中θ∈𝑅为预设的非零常数。
我们可以利用这个矩阵对向量𝑞进行旋转,得到新的旋转后向量𝑞^。
以下是旋转操作应用于二维向量𝑞的可视化效果。初始向量以黑色显示,不同位置(不同m值)对应的向量展示如下:
在不同位置表示的2D嵌入
2、通用数学公式
我们将d维(嵌入维度)空间划分为d/2个子空间,并分别应用旋转。
“Enhanced”的嵌入向量被划分为d/2个子空间
每个子空间通过旋转矩阵进行旋转
关键点
注意随着θ值增大,其衰减过程 观察上方展示的增强查询/键向量,浅绿色子空间(θ₁)的旋转幅度将显著大于后续子空间,到末端时子空间的旋转可以忽略不计 以下是RoPE的Python实现代码:
在下方代码中,我们创建三维矩阵,每个切片包含对应位置"m"的旋转矩阵。即第一个查询/键向量与第一个切片相乘,第二个向量与第二个切片相乘,依此类推实现旋转。
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]
# 创建矩阵theta(形状: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
# 创建sin和cos值
cos_values = np.cos(m_theta)
sin_values = np.sin(m_theta)
# 使用高级索引填充旋转矩阵R
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
高效实现
考虑到旋转矩阵的稀疏性,作者提出了这种计算RoPE的高效方法:
你可以尝试用 Python 实现这个作为练习。
可视化RoPE 还记得不同的子空间以不同的速率旋转吗?这里让我们将其可视化并探讨其含义。
context_len = 100
d_model = 128
R = get_rotary_matrix(context_len, d_model)
np.random.seed(42)
# 生成虚拟嵌入
dummy_embedding = np.random.randn(d_model)
# 编码的位置
positions = [0, 50, 99]
# 创建编码嵌入
encoded_embeddings = [R[pos] @ dummy_embedding for pos in positions]
# 创建子图
fig, axs = plt.subplots(3, 1, figsize=(7, 10))
fig.suptitle('虚拟嵌入与编码嵌入的比较', fontsize=16)
# 在位置0处绘制虚拟嵌入和编码嵌入
axs[0].plot(dummy_embedding, label='虚拟嵌入')
axs[0].plot(encoded_embeddings[0], label='编码嵌入 (位置 0)')
axs[0].set_title('虚拟嵌入与编码嵌入(位置0)')
axs[0].set_xlabel('维度')
axs[0].set_ylabel('值')
axs[0].legend()
axs[0].grid(True, linestyle='--', alpha=0.7)
# 在位置50处绘制虚拟嵌入和编码嵌入
axs[1].plot(dummy_embedding, label='虚拟嵌入')
axs[1].plot(encoded_embeddings[1], label='编码嵌入 (位置 50)')
axs[1].set_title('虚拟嵌入与编码嵌入(位置50)')
axs[1].set_xlabel('维度')
axs[1].set_ylabel('值')
axs[1].legend()
axs[1].grid(True, linestyle='--', alpha=0.7)
# 在位置99处绘制虚拟嵌入和编码嵌入
axs[2].plot(dummy_embedding, label='虚拟嵌入')
axs[2].plot(encoded_embeddings[2], label='编码嵌入 (位置 99)')
axs[2].set_title('虚拟嵌入与编码嵌入(位置99)')
axs[2].set_xlabel('维度')
axs[2].set_ylabel('值')
axs[2].legend()
axs[2].grid(True, linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
正如我们上文所讨论的,
——初始子空间相较于最终子空间发生了显著旋转。
让我们观察绝对位置编码的特性,验证其在此是否同样适用:
关键点
位置信息的局部化:最显著的差异出现在嵌入向量的初始维度。这表明位置信息主要编码在这些早期维度中。(此处同样成立) 振幅递减现象:随着维度升高,差异振幅逐渐减弱。这说明后续维度受位置编码的影响较小。(此处同样成立) 词汇信息的保留:后续维度差异极小,表明这些维度主要保留词汇本身信息,几乎不受位置因素干扰。(此处同样成立)
应用启发
维度分工:嵌入维度似乎存在功能分化,前期维度侧重位置信息,后期维度聚焦词汇语义。 模型注意力机制:这种结构可能影响模型对输入不同层面的关注方式——通过早期维度捕捉位置关系,借助后期维度提取词汇特征。 信息平衡机制:该编码方案在保留词汇信息与添加上下文位置之间取得平衡,使模型能同时利用这两类信息进行处理。
九、正弦与RoPE编码实践
回顾RoFormer论文中呈现的结果
RoFormer在WMT 2014英德翻译任务中的BLEU分数优于其基线模型的版本。
旋转位置编码似乎具有更好的收敛速度。
许多模型,如Gemma 、LLama 、qwen等,都采用了RoPE编码。