前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >位置编码

位置编码

作者头像
西西嘛呦
发布2022-05-10 09:24:29
2.3K0
发布2022-05-10 09:24:29
举报

为什么需要位置编码

在transformer中使用了位置编码,为什么需要位置编码。因为对于transformer中的注意力机制而言,交换两个单词,并不会影响注意力的计算,也就是说这里的注意力是对单词位置不敏感的,而单词之间的位置信息往往是很重要的,因此考虑使用位置编码。

绝对位置编码

三角函数位置编码

transformer使用的位置编码。基本公式:

p_{k,2i} = sin(\frac{k}{10000^{\frac{2i}{d}}}) \\ p_{k,2i+1} = cos(\frac{k}{10000^{\frac{2i+1}{d}}})
p_{k}

表示序列中第k个单词,2i及2i+1是其的两个分量,也就是说,第k个位置编码是由两部分构成的。假设句子长度为512,那么位置编码向量维度就是512×2。那么为什么会使用这种位置编码表示呢?首先三角函数有以下性质:

sin(\alpha+\beta) = sin\alpha\cos\beta+cos\alpha\sin\beta \\ cos(\alpha+\beta) = cos\alpha\cos\beta-sin\alpha\sin\beta

那么:

p_{m}=[p_{m,2i},p_{m, 2i+1}] \\ p_{m}=[sin(\frac{m}{10000^{\frac{2i}{d}}}), cos(\frac{m}{10000^{\frac{2i}{d}}})] \\ p_{m+k}=[p_{m+k,2i},p_{m+k, 2i+1}] \\ p_{m+k}=[sin(\frac{m+k}{10000^{\frac{2i}{d}}}), cos(\frac{m+k}{10000^{\frac{2i}{d}}})] \\

我们把

\frac{1}{10000^{\frac{2i}{d}}}

记为a,则有:

p_{m+k} = [sinamcosak+cosamsinak, cosamcosak-sinamsinak] \\ p_{m+k}= \left[ \begin{matrix} cosak&sinak\\ -sinak&cosak\\ \end{matrix} \right] \left[ \begin{matrix} sinam \\ cosam \\ \end{matrix} \right] = \left[ \begin{matrix} cosak&sinak\\ -sinak&cosak\\ \end{matrix} \right]p_{m}

也就是说第m+k个位置的位置编码可以由第m个位置表示。另有:

P_{t+k} = R_{k}P_{t+k} \\ P_{t+k1+k2} = R_{k1+k2}P_{t}=R_{k1}R_{k2}P_{t} \\ 则有:\\ R_{k1+k2} =R_{k1}R_{k2} \\ R_{k1-k2} =R_{k1}R_{-k2} \\ 因为:\\ -sin\alpha=sin-\alpha, cos\alpha=cos-\alpha \\ 所以:\\ R_{-k2}=(R_{k2})^{T} \\ 最终:\\ R_{k1-k2}=R_{k1}(R_{k2})^{T} 或者 R_{k2-k1}=R_{k2}(R_{k1})^{T}

参考实现:

class PositionalEncoding(nn.Module):
    "Implement the PE function."

    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        # Compute the positional encodings once in log space.
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) *
                             -(math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)

    def forward(self, x):
        x = x + Variable(self.pe[:, :x.size(1)],
                         requires_grad=False)
        return self.dropout(x)

可学习的位置编码

这一种位置编码就是Bert模型采用的。为什么bert不用transformer的三角函数编码,因为bert训练采用了更大的预料,使用可学习的位置编码效果可能更好。

递归式位置编码

这里摘录苏剑林的文章: 原则上来说,RNN模型不需要位置编码,它在结构上就自带了学习到位置信息的可能性(因为递归就意味着我们可以训练一个“数数”模型),因此,如果在输入后面先接一层RNN,然后再接Transformer,那么理论上就不需要加位置编码了。同理,我们也可以用RNN模型来学习一种绝对位置编码,比如从一个向量

p_{0}

出发,通过递归格式

p_{k+1}=f(p_{k})

来得到各个位置的编码向量。

ICML 2020的论文《Learning to Encode Position for Transformer with Continuous Dynamical Model》把这个思想推到了极致,它提出了用微分方程(ODE)

dp_{t}/d_{t=h(p_{t},t)}

的方式来建模位置编码,该方案称之为FLOATER。显然,FLOATER也属于递归模型,函数

h(p_{t},t)

可以通过神经网络来建模,因此这种微分方程也称为神经微分方程,关于它的工作最近也逐渐多了起来。 理论上来说,基于递归模型的位置编码也具有比较好的外推性,同时它也比三角函数式的位置编码有更好的灵活性(比如容易证明三角函数式的位置编码就是FLOATER的某个特解)。但是很明显,递归形式的位置编码牺牲了一定的并行性,可能会带速度瓶颈。

相对位置编码

直接去看苏剑林的文章:https://zhuanlan.zhihu.com/p/352898810 旋转位置编码:

import math

import torch
import torch.nn as nn
import torch.nn.functional as F

context_outputs = torch.rand((32, 512, 768))
last_hidden_state = context_outputs  # 这里的context_outputs是bert的输出
# # last_hidden_state:[batch_size, seq_len, hidden_size]
batch_size = last_hidden_state.size()[0]
seq_len = last_hidden_state.size()[1]

hidden_size = 768
ent_type_size = 10
inner_dim = 64
# self.ent_type_size表示的是实体的总数, inner_dim自定义为64
# outputs:(batch_size, seq_len, ent_type_size*inner_dim*2)=[32, 512, 10*64*2]
outputs = nn.Linear(hidden_size, ent_type_size * inner_dim * 2)(last_hidden_state)
# 得到10个[32, 512, 64*2]
outputs = torch.split(outputs, inner_dim * 2, dim=-1)
# [32, 512, 10, 64*2]
outputs = torch.stack(outputs, dim=-2)
# qw和kw都是:[32, 512, 10, 64]
qw, kw = outputs[..., :inner_dim], outputs[..., inner_dim:]
"""这下面就是旋转位置编码主代码"""


def sinusoidal_position_embedding(batch_size, seq_len, output_dim):
    """这里是最初得正余弦位置编码"""
    # position_ids:[512, 1]
    position_ids = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(-1)
    # [32],从0-31
    indices = torch.arange(0, output_dim // 2, dtype=torch.float)
    # 10000^(-[0,...,31]/64)
    indices = torch.pow(10000, -2 * indices / output_dim)
    # [512, 32]
    embeddings = position_ids * indices
    # torch.Size([512, 32, 2])
    embeddings = torch.stack([torch.sin(embeddings), torch.cos(embeddings)], dim=-1)
    # [32, 512, 32, 2]
    embeddings = embeddings.repeat((batch_size, *([1] * len(embeddings.shape))))
    # [32, 512, 64]
    embeddings = torch.reshape(embeddings, (batch_size, seq_len, output_dim))
    return embeddings


pos_emb = sinusoidal_position_embedding(batch_size,
                                        seq_len,
                                        output_dim=inner_dim)
# 取奇数位,奇数位是cos
# repeat_interleave重复张量得元素
# torch.Size([32, 512, 1, 64])
cos_pos = pos_emb[..., None, 1::2].repeat_interleave(2, dim=-1)
# torch.Size([32, 512, 1, 64])
# 偶数位是sin
sin_pos = pos_emb[..., None,::2].repeat_interleave(2, dim=-1)

# torch.Size([32, 512, 10, 32, 2])
# 重新排列
qw2 = torch.stack([-qw[..., 1::2], qw[..., ::2]], -1)
# [32, 512, 10, 64]
qw2 = qw2.reshape(qw.shape)
# [32, 512, 10, 64] * [32, 512, 1, 64] + [32, 512, 10, 64] * [32, 512, 1, 64]
qw = qw * cos_pos + qw2 * sin_pos  # 这就是旋转位置编码得最终结果
kw2 = torch.stack([-kw[..., 1::2], kw[...,::2]], -1)
kw2 = kw2.reshape(kw.shape)
kw = kw * cos_pos + kw2 * sin_pos
 
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-04-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么需要位置编码
  • 绝对位置编码
    • 三角函数位置编码
      • 可学习的位置编码
        • 递归式位置编码
        • 相对位置编码
        相关产品与服务
        对象存储
        对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档