我最近正在阅读huggingface项目的bert源代码。我注意到所谓的“可学习位置编码”在实现时似乎指的是特定的 nn.Parameter 层。
def __init__(self):
super()
positional_encoding = nn.Parameter()
def forward(self, x):
x += positional_encoding
↑ 可能是这种感觉,然后进行了可学习的位置编码。不管是不是这么简单,我不确定我理解是否正确,我想请教有经验的人。
此外,我注意到一个经典的 bert 结构,其位置实际上仅在初始输入时编码一次。这是否意味着后续的 bert 层,对于彼此来说,失去了捕获位置信息的能力?
BertModel(
(embeddings): BertEmbeddings(
(word_embeddings): Embedding(30522, 768, padding_idx=0)
(position_embeddings): Embedding(512, 768)
(token_type_embeddings): Embedding(2, 768)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(encoder): BertEncoder(
(layer): ModuleList(
(0): BertLayer(...)
...
(pooler): BertPooler(...)
如果在下一个 BERT 层之前对上一层的结果进行重新位置编码,我会得到更好的结果吗?
位置嵌入的目的是什么?
在 Transformer(包括 BERT)中,不同 token 之间的唯一交互是通过自注意力层完成的。如果仔细观察这些层实现的数学运算,您会注意到这些层是排列等变:即表示
“我确实喜欢编码”
和
“我喜欢编码吗”
是相同的,因为两个句子中的单词(=标记)是相同的,只是它们的顺序不同。
正如您所看到的,这种“排列等方差”在许多情况下并不是所需的属性。
为了打破这种对称性/等变性,我们可以简单地“编码”句子中每个单词/标记的实际位置。例如:
“I_1 do_2 like_3 编码_4”
不再与
相同
“Do_1 I_2 like_3编码_4”
这就是位置编码/嵌入的目的——使自注意力层对标记的顺序敏感。
现在回答你的问题:
nn.Parameter
来实现的。位置编码只是添加到每个标记的“代码”,标记其在序列中的位置。因此,它所需要的只是一个与输入序列大小相同的张量,每个位置具有不同的值。@Shai的回答相当精彩。我也有同样的疑问,这个答案对我帮助很大。我想添加关于该主题的另一篇好论文,它提供了对位置编码的深入见解:视觉变换器的条件位置编码(arXiv 2021)。
在第 4.2 节中,表 2 CPVT-Ti plus 显示出比 CPVT-Ti 更好的性能。 CPVT-Ti plus 为第一个第五个编码器插入了 pos 嵌入(而不是仅为 CPVT-Ti 的第一个编码器)。因此,它表明您的猜测是“如果在下一个 BERT 层之前对上一层的结果进行重新位置编码,我会得到更好的结果吗?”可能是对的。