Transformer 中如何把位置编码值添加到输入嵌入中

举报
汪子熙 发表于 2025/07/01 20:27:14 2025/07/01
【摘要】 笔者最近在学习 Transformer 架构设计,其中有一个章节谈到了 Transformer 的位置编码。Transformer 模型中的自注意力机制(Self-Attention)与传统的 RNN 模型不同,后者通过递归计算来保持输入序列的顺序,而 Transformer 模型并没有递归结构。Transformer 使用的是多头自注意力机制,可以并行处理序列的所有元素。在这种架构中,序列...

笔者最近在学习 Transformer 架构设计,其中有一个章节谈到了 Transformer 的位置编码。

Transformer 模型中的自注意力机制(Self-Attention)与传统的 RNN 模型不同,后者通过递归计算来保持输入序列的顺序,而 Transformer 模型并没有递归结构。Transformer 使用的是多头自注意力机制,可以并行处理序列的所有元素。在这种架构中,序列中每个元素对其他元素的注意力分数是通过点积计算得到的,然而,序列中的顺序信息会丢失。因此,为了能够利用序列的顺序,必须要为 Transformer 模型提供一个额外的手段,使其知道每个词在序列中的相对或绝对位置。这就引入了位置编码。

位置编码是 Transformer 中用来向模型注入位置信息的技术,目的是使模型能够区分同一个词在不同位置的情况。为实现这一点,位置编码向输入的词嵌入中添加一个额外的向量,注入位置信息,这些向量就是所谓的“位置编码值”。

位置编码的数学描述

在 Transformer 中,位置编码是通过给每个输入的位置生成一个唯一的向量,并将其与对应的输入嵌入进行逐元素加法(Element-wise Addition)的方式实现的。具体来说,每个位置编码都是一个与词嵌入维度相同的向量,代表序列中相应位置的特征。

论文 Attention is All You Need 提出的经典位置编码公式如下:

对于位置 pospos 和维度 ii,位置编码值 PE(pos,i)PE(pos, i) 被定义为:

PE(pos,2i)=sin(pos100002i/dmodel)PE(pos, 2i) = \sin(\frac{pos}{10000^{2i/d_{model}}})

PE(pos,2i+1)=cos(pos100002i/dmodel)PE(pos, 2i+1) = \cos(\frac{pos}{10000^{2i/d_{model}}})

其中,dmodeld_{model} 是嵌入向量的维度。公式通过正弦和余弦的组合来生成位置信息,且不同维度有不同的频率。这样,模型可以通过这种连续、平滑的函数捕获序列元素的相对和绝对位置信息。

使用正弦和余弦的优点在于,任何两个位置编码之间的点积结果可以反映它们的相对位置。换句话说,这种设计方式使模型具备了一定的相对位置敏感性。通过这样一种可区分的方式,Transformer 在编码序列中每个词的位置信息时,具有更好的泛化能力。

位置编码添加到输入嵌入中的过程

生成的位置编码向量与输入的词嵌入逐元素相加,这样就能在输入中融入位置信息。这个操作可以被看作是向每个输入向量注入某种“标签”,从而明确其在序列中的位置。

举个例子,假设我们有一个输入序列 ["hello", "world"],并且每个词的词嵌入是一个 4 维的向量:

  • hello 对应的嵌入为:[0.5, 1.0, 0.3, 0.7]
  • world 对应的嵌入为:[0.8, 0.6, 0.4, 0.9]

我们假设使用的模型嵌入维度是 4,那么对于位置编码,我们可以这样计算:

  • 位置编码第 0 位:[sin(0), cos(0), sin(0), cos(0)] = [0, 1, 0, 1]
  • 位置编码第 1 位:[sin(1), cos(1), sin(1/10000^1), cos(1/10000^1)] ≈ [0.8415, 0.5403, 0.0001, 1.0]

因此,位置编码后新的输入嵌入为:

  • hello[0.5+0, 1.0+1, 0.3+0, 0.7+1] = [0.5, 2.0, 0.3, 1.7]
  • world[0.8+0.8415, 0.6+0.5403, 0.4+0.0001, 0.9+1.0] = [1.6415, 1.1403, 0.4001, 1.9]

这样经过位置编码后的输入就包含了词的语义信息以及位置信息。

Python 代码实现

接下来,我们通过代码来展示位置编码的实现以及它是如何与词嵌入结合的。使用 Python 和 numpy 库可以很方便地实现位置编码的计算。

import numpy as np
import matplotlib.pyplot as plt

def get_positional_encoding(seq_len, d_model):
    positional_encoding = np.zeros((seq_len, d_model))
    for pos in range(seq_len):
        for i in range(0, d_model, 2):
            positional_encoding[pos, i] = np.sin(pos / (10000 ** ((2 * i) / d_model)))
            if i + 1 < d_model:
                positional_encoding[pos, i + 1] = np.cos(pos / (10000 ** ((2 * i) / d_model)))
    return positional_encoding

# 设置序列长度和嵌入维度
seq_len = 10
d_model = 16

# 生成位置编码
pos_encoding = get_positional_encoding(seq_len, d_model)

# 可视化位置编码
plt.figure(figsize=(10, 6))
plt.pcolormesh(pos_encoding, cmap='viridis')
plt.xlabel('Embedding Dimension')
plt.ylabel('Position in Sequence')
plt.title('Positional Encoding')
plt.colorbar()
plt.show()

上面的代码中,我们定义了一个 get_positional_encoding 函数,它根据给定的序列长度和嵌入维度生成位置编码矩阵。在可视化部分,可以看到位置编码在不同维度和位置的变化情况,生成了一种独特的斑纹图案,表示位置编码值随位置和维度的不同而变化。

这种编码方式使 Transformer 模型能够更好地捕捉序列元素之间的关系。

位置编码的真实世界应用

可以借助一个真实的案例来更好理解位置编码的作用。假设我们正在处理一个翻译系统,将英语翻译为德语。在这种任务中,词与词之间的顺序对翻译的结果有至关重要的影响。例如,对于句子 “The cat sat on the mat”,如果顺序被打乱为 “The mat sat on the cat”,意义就完全变了。

Transformer 模型没有内置的顺序感知能力,而是通过位置编码来注入序列信息。这样一来,当模型在自注意力机制中计算每个词对其他词的注意力权重时,位置编码让模型“知道”当前词在句中的位置。这使得模型在进行翻译时,能够保持输入句子的顺序结构,从而生成符合语法和上下文的译文。

这种顺序感知对很多 NLP 任务来说至关重要。例如,在文本分类任务中,句子中某些词的位置对表达整体意思可能也很重要。又比如在生成式任务中,生成文本时需要参考先前生成的词,并以此来决定下一个生成的词。在这些情况下,位置编码的存在使得 Transformer 在没有递归机制的情况下,也能够具备对序列中词与词之间位置的敏感度。

为什么选择正弦和余弦?

关于位置编码的设计,正弦和余弦的组合有其独特的优势。首先,这种方式提供了一种连续的编码,使得任何两个位置之间的编码都有一定的平滑变化,这非常适合 Transformer 模型在学习不同位置之间的关系时进行插值。另外,这样的设计也使得模型对不同位置的编码具有周期性,这对某些特定类型的数据,比如时间序列数据,可以捕获一定的周期性特征。

在与其他编码方法相比时,正弦和余弦的组合还具有较好的泛化性,因为它们通过简单的数学函数生成,并且与维度无关。此外,它们提供了对不同尺度上位置关系的有效捕获。例如,对于相邻位置之间的小距离变化,低频成分可以起到作用,而对于更长距离的变化,高频成分可以提供足够的信息。

这样的编码方式不仅仅适用于 NLP 领域,也可以在其他需要处理序列数据的领域中发挥作用。例如,在处理图像数据的 Vision Transformer (ViT) 模型中,位置编码可以被用于标示图像中不同位置的特征块(Patch),帮助模型理解空间布局。

实际中的代码集成

在实际的 Transformer 模型实现中,位置编码一般是作为一个层直接集成在输入嵌入的处理过程中。以下是一个基于 PyTorch 的位置编码实现示例,展示了如何将位置编码值加到输入嵌入中。

import torch
import torch.nn as nn

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000):
        super(PositionalEncoding, self).__init__()
        # 创建一个位置编码矩阵,大小为 (max_len, d_model)
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-torch.log(torch.tensor(10000.0)) / d_model))
        
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        
        pe = pe.unsqueeze(0).transpose(0, 1)  # 调整形状以方便后续计算
        self.register_buffer('pe', pe)

    def forward(self, x):
        x = x + self.pe[:x.size(0), :]
        return x

# 示例用法
d_model = 16
max_len = 100
seq_len = 10
batch_size = 32

# 创建位置编码层
pos_encoder = PositionalEncoding(d_model, max_len)

# 随机生成一些输入嵌入
embedding = torch.randn(seq_len, batch_size, d_model)

# 将位置编码添加到输入嵌入中
encoded_embedding = pos_encoder(embedding)

在这个代码中,PositionalEncoding 是一个 PyTorch 模块,用于生成并将位置编码添加到输入的嵌入张量中。在创建位置编码时,我们使用 torch.exp() 函数计算位置编码中的指数部分。位置编码矩阵 pe 是通过先生成 sincos 值,再按每个位置将其组合得到的。

通过调用位置编码层,输入嵌入会被自动加上相应的位置信息,使得每个词嵌入不仅包含了词语的语义信息,还包含了它在句子中的位置信息。

总结

Transformer 中的位置编码是通过将位置编码值加到输入嵌入中来实现的,位置编码使用正弦和余弦函数生成,旨在为模型提供序列位置信息。这种编码方式在保留位置感知的同时,不增加模型的复杂度,并且能够很好地在不同的序列长度上泛化。

通过这种方式,Transformer 在处理自然语言处理任务时,虽然没有 RNN 那样的递归结构,却依然具备了对输入序列顺序的敏感度。结合多头自注意力机制,Transformer 模型可以有效地捕捉序列中不同词之间的依赖关系,从而在机器翻译、文本分类、文本生成等任务中取得了巨大的成功。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。