理解Transformer论文中的positional encoding,和三角函数有什么关系

理解Transformer论文中的positional encoding,和三角函数有什么关系

关注这个问题有一段时间了,一直没有看到太满意的回答,来讲一下我的见解。

首先,需要明确的是,建模位置信息(无论是绝对位置还是相对位置)并不是必须用到三角函数,否则fairseq和BERT中使用的positional embedding也不会奏效了。我想,作者在这里使用正余弦函数,只是根据归纳偏置和一些经验作出的选择罢了

不妨从零构想一个位置编码的方法。首先,给定一个长为 [公式] 的文本,最简单的位置编码就是计数,即使用 [公式] 作为文本中每个字的位置编码了。当然这样的瑕疵非常明显,这个序列是没有上界的。设想一段很长的(比如含有500个字的)文本,最后一个字的位置编码非常大,这是很不合适的:1. 它比第一个字的编码大太多,和字嵌入合并以后难免会出现特征在数值上的倾斜;2. 它比一般的字嵌入的数值要大,难免会抢了字嵌入的「风头」,对模型可能有一定的干扰。

从这里,我们知道位置编码最好具有一定的值域范围,这样就有了第二个版本:使用文本长度对每个位置作归一化,得到 [公式] 。这样固然使得所有位置编码都落入 [公式] 区间,但是问题也是显著的:不同长度文本的位置编码步长是不同的,在较短的文本中紧紧相邻的两个字的位置编码差异,会和长文本中相邻数个字的两个字的位置编码差异一致。这显然是不合适的,我们关注的位置信息,最核心的就是相对次序关系,尤其是上下文中的次序关系,如果使用这种方法,那么在长文本中相对次序关系会被「稀释」。

再重新审视一下位置编码的需求:1. 需要体现同一个单词在不同位置的区别;2. 需要体现一定的先后次序关系,并且在一定范围内的编码差异不应该依赖于文本长度,具有一定不变性。我们又需要值域落入一定数值区间内的编码,又需要保证编码与文本长度无关,那么怎么做呢?一种思路是使用有界的周期性函数。在前面的两种做法里面,我们为了体现某个字在句子中的绝对位置,使用了一个单调的函数,使得任意后续的字符的位置编码都大于前面的字,如果我们放弃对绝对位置的追求,转而要求位置编码仅仅关注一定范围内的相对次序关系,那么使用一个sin/cos函数就是很好的选择,因为sin/cos函数的周期变化规律非常稳定,所以编码具有一定的不变性。简单的构造可以使用下面的形式

[公式]

其中 [公式] 用来调节位置编码函数的波长,当 [公式] 比较大时,波长比较长,相邻字的位置编码之间的差异比较小。

这样的做法还是有一些简陋,周期函数的引入是为了复用位置编码函数的值域,但是这种 [公式] 的映射,还是太单调:如果 [公式] 比较大,相邻字符之间的位置差异体现得不明显;如果 [公式] 比较小,在长文本中还是可能会有一些不同位置的字符的编码一样,这是因为 [公式] 空间的表现范围有限。既然字嵌入的维度是 [公式] ,自然也可以使用一个 [公式] 维向量来表示某个位置编码—— [公式] 的表示范围要远大于[公式] (醉酒的鸟儿永远飞不回家)。

显然,在不同维度上应该用不同的函数操纵位置编码,这样高维的表示空间才有意义。可以为位置编码的每一维赋予不同的 [公式] ;甚至在一些维度将 [公式] 替换为 [公式] …一种构造方法就是论文中的方法了

[公式]

[公式]

这里不同维度上 [公式] 的波长从 [公式][公式] 都有;区分了奇偶数维度的函数形式。这使得每一维度上都包含了一定的位置信息,而各个位置字符的位置编码又各不相同。这里可以顺便为

的疑问作一个可能的解释:为什么官方代码tensor2tensor的最初版本只是简单地分了两段,却没有什么性能差异呢?因为 [公式] 的交替使用只是为了使编码更「丰富」,在哪些维度上使用 [公式] ,哪些使用 [公式] ,不是很重要,都是模型可以调整适应的。

当然,我觉得深究这里的三角函数形式的位置编码没有太大意义。因为至少现在看来:1. 这个函数形式很可能是基于经验得到的,并且应该有不少可以替代的方法;2. 谷歌后期的作品BERT已经换用位置嵌入(positional embedding)了,这可能说明编码的方案有一定的问题(猜测)。

Swin Transformer使用的是Learned Positional Encoding,如果要将其替换为Sinusoidal Positional Encoding,需要进行一些修改。 首先,可以定义一个Sinusoidal Positional Encoding的函数,如下所示: ```python import math import torch import torch.nn as nn class SinusoidalPositionalEmbedding(nn.Module): def __init__(self, d_model, max_len=512): super().__init__() self.d_model = d_model self.max_len = max_len 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() * (-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).transpose(0, 1) self.register_buffer('pe', pe) def forward(self, x): x = x * math.sqrt(self.d_model) seq_len = x.size(1) pe = self.pe[:seq_len, :] pe = pe.repeat(x.size(0), 1, 1) x = x + pe.to(x.device) return x ``` 然后,在Swin Transformer的构造函数,将使用Learned Positional Encoding的部分替换为Sinusoidal Positional Encoding,如下所示: ```python import torch import torch.nn as nn from einops.layers.torch import Rearrange class SwinTransformer(nn.Module): def __init__(self, img_size=224, patch_size=4, in_chans=3, num_classes=1000, embed_dim=96, depths=[2, 2, 18, 2], num_heads=[3, 6, 12, 24], window_size=7, mlp_ratio=4., qkv_bias=True, qk_scale=None, drop_rate=0., attn_drop_rate=0., drop_path_rate=0.): super().__init__() norm_layer = nn.LayerNorm self.num_classes = num_classes self.num_features = self.embed_dim = embed_dim # stochastic depth decay rule dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule # patch embedding self.patch_embed = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size) self.norm1 = norm_layer(embed_dim) # pos embedding self.pos_embed = SinusoidalPositionalEmbedding(embed_dim, max_len=(img_size//patch_size)**2+1) # swin transformer blocks self.blocks = nn.ModuleList([ SwinTransformerBlock(dim=embed_dim, num_heads=num_heads[i], window_size=window_size, shift_size=window_size // 2 if i == 0 else 0, mlp_ratio=mlp_ratio, qkv_bias=qkv_bias, qk_scale=qk_scale, drop=drop_rate, attn_drop=attn_drop_rate, drop_path=dpr[sum(depths[:i]):sum(depths[:i+1])]) for i in range(len(depths))]) # norm before classifier self.norm2 = norm_layer(embed_dim) # classification head self.head = nn.Linear(embed_dim, num_classes) if num_classes > 0 else nn.Identity() def forward_features(self, x): x = self.patch_embed(x) x = self.norm1(x) x = x.flatten(2).transpose(1, 2) x = self.pos_embed(x) for i, blk in enumerate(self.blocks): x = blk(x) x = self.norm2(x) return x def forward(self, x): x = self.forward_features(x) x = x.mean(dim=1) # global average pooling x = self.head(x) return x ``` 这样,就完成了Swin Transformer模型Positional Encoding形式的替换。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值