大家好,我是CuddleSabe,目前大四在读,深圳准入职算法工程师,研究主要方向为多模态(VQA、ImageCaptioning等),欢迎各位佬来讨论!
我最近在有序地计划整理CV入门实战系列及NLP入门实战系列。在这两个专栏中,我将会带领大家一步步进行经典网络算法的实现,欢迎各位读者(da lao)订阅
因为Self-Attention相对于传统的RNN在输入计算时没有输入先后顺序,而是采用并行化的思想来加快运算,这样Self-Attention在前一个token结果还没有出来的时候便可以同时处理下一个token。但是这样在并行化提升运算速度的同时也会带来一个问题,那便是丧失了序列的顺序性。因此为了不损失顺序性,在将序列输入之前还需要结合位置编码(Positional Encoding)。
这里我们先考虑最简单的一种Encoding方式:用token的序号来进行编码。
即:北京欢迎您,我爱它。
P E i = i PE_{i}=i PEi=i
那么这种最简单的Encoding结果为(不考虑标点符号):
token | Encoded |
---|---|
‘北’ | 0 |
‘京’ | 1 |
‘欢’ | 2 |
‘迎’ | 3 |
‘您’ | 4 |
‘我’ | 5 |
‘爱’ | 6 |
‘它’ | 7 |
可以看到,这种编码方式的最明显缺点是:
对于特长序列,编码长度会不可控。
那么再看一种编码方式
P E i = i l e n PE_{i}=\frac{i}{len} PEi=leni
那么这样编码结果为(不考虑标点符号):
token | Encoded |
---|---|
‘北’ | 0 .000 |
‘京’ | 0.125 |
‘欢’ | 0.250 |
‘迎’ | 0.375 |
‘您’ | 0.500 |
‘我’ | 0.625 |
‘爱’ | 0.750 |
‘它’ | 0.875 |
这样编码结果限制为0到1, | |
但同时也会带来一个新的问题,那就是编码结果的尺度随着不同序列长度变化也在变化。 | |
例如,10个单词和100个单词的句子经过编码后,相邻单词的位置编码差距不是同一个数量级,这种尺度不统一的话,便无法进行训练。 |
因此,一种良好的位置编码方式需要达到以下两点:
1.位置编码长度不会随着序列长度而变化,而是限制在一定范围
2.位置序列的变化尺度需要统一
数学基础好的同学可能已经猜到了,满足这两条的便是———三角函数!
公式如下,如果想要详细解释可查看这篇文章:
地址:Positional Encoding详细解释
公式:
P E p o s , 2 i = s i n ( p o s / 1000 0 2 i / d ) PE_{pos,2i}=sin(pos/10000^{2i/d}) PEpos,2i=sin(pos/100002i/d)
P E p o s , 2 i + 1 = c o s ( p o s / 1000 0 2 i / d ) PE_{pos,2i+1}=cos(pos/10000^{2i/d}) PEpos,2i+1=cos(pos/100002i/d)
这里d为Embedding时的d_model参数,也是Embedding时一个单词的特征数,详细请看我的另一篇文章:
PyTorch文字处理及Embedding
我们已知公式如下,那么我们如果使用PyTorch实现呢?
P E p o s , 2 i = s i n ( p o s / 1000 0 2 i / d ) PE_{pos,2i}=sin(pos/10000^{2i/d}) PEpos,2i=sin(pos/100002i/d)
P E p o s , 2 i + 1 = c o s ( p o s / 1000 0 2 i / d ) PE_{pos,2i+1}=cos(pos/10000^{2i/d}) PEpos,2i+1=cos(pos/100002i/d)
这里我们需要将公式进行变换
我们设
T 2 i = 1 / 1000 0 2 i / d T_{2i}=1/10000^{2i/d} T2i=1/100002i/d和 T 2 i + 1 = 1 / 1000 0 2 i / d T_{2i+1}=1/10000^{2i/d} T2i+1=1/100002i/d
那么进一步,两边取对数
l n T = − 2 i d l n 10000 lnT=-\frac{2i}{d}ln10000 lnT=−d2iln10000
进而
T = e − 2 i d l n 10000 T=e^{-\frac{2i}{d}ln10000} T=e−d2iln10000
至于这里为什么不直接使用初始公式进行计算而是推导到现在,
个人理解是因为可以避免10000的幂作为分母而带来的计算机的计算误差。
class PositionalEncoding(nn.Module):
"实现PE功能"
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
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) # [1, max_len, d_model]
self.register_buffer('pe', pe)
def forward(self, x):
x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False) # 这里是Embedding与Positional Encoding相结合
return self.dropout(x)
将pe画出,得到以下图像