本文最后更新于:2025年2月6日 晚上
位置编码–最初的sin/cos编码
1 1D 序列的sin/cos编码
1.1 介绍
众所周知,Transformers模型本身没有关于位置的inductive bias,所以需要额外注入位置信息。在最初的《Attention is All You Need》文章中,作者提出了首个流传至今的位置编码方式: sin/cos位置编码。
假设模型的输入embedding为x ∈ R B × T × d x\in \mathbb{R}^{B\times T\times d} x ∈ R B × T × d ,那么一维序列的位置编码公式可写作
PE t , 2 i = sin ( t 1000 0 2 i / d ) PE t , 2 i + 1 = cos ( t 1000 0 2 i / d ) \text{PE}_{t,2i} = \sin\left(\frac{t}{10000^{2i/d}}\right) \quad \text{PE}_{t,2i+1}=\cos\left(\frac{t}{10000^{2i/d}}\right)
PE t , 2 i = sin ( 1 0 0 0 0 2 i / d t ) PE t , 2 i + 1 = cos ( 1 0 0 0 0 2 i / d t )
其中,t t t 是时间维度T T T 的索引,i i i 是channel维度d d d 的索引,观察公式可得,位置编码在偶数和奇数位置上是不同的,且不仅与token位置t t t 有关,还与channel维度有关。
这种位置编码计算完成之后是一系列确定的值,所以我们也称这种位置编码为绝对位置编码。
1.2 代码实现
由公式可以知道,只要我们有channel大小和位置,就能够把这一系列位置编码算出来。在代码实现中,要考虑如何并行计算,即完全使用张量操作来完成。
我们可以设计一个函数get_1d_sincos_pos_embed(embed_dim: int, pos: np.array)
,输入是两个参数,embed_dim
代表channel大小或者embedding大小,pos
代表一系列的位置id,是一个一维的数组,假设一共有M M M 个位置,这个函数返回一个M × D M\times D M × D 的Tensor.
首先,我们应该确定输入的embed_dim
是否能被2整除,否则将无法实现奇偶数的计算。
1 assert embed_dim % 2 == 0
注意到,公式中无论是奇数的encoding还是偶数的encoding,分母的指数均为2 i / d 2i/d 2 i / d ,所有我们可以先创建有关于2 i / d 2i/d 2 i / d 的数据。
1 2 3 omega = np.arange(embed_dim // 2 , dtype=np.float64) omega /= embed_dim / 2. omega = 1. / 10000 **omega
第一行code创建了i ∈ [ 0 , d / 2 − 1 ] i\in [0,d/2-1] i ∈ [ 0 , d / 2 − 1 ] ,第二行code完成了
ω = i d / 2 = 2 i d \omega = \frac{i}{d/2} = \frac{2i}{d}
ω = d / 2 i = d 2 i
第三行则变为
ω = 1 1000 0 ω = 1 1000 0 2 i / d \omega = \frac{1}{10000^{\omega}} = \frac{1}{10000^{2i/d}}
ω = 1 0 0 0 0 ω 1 = 1 0 0 0 0 2 i / d 1
我们现在就有了完整的缩放因子,接下来将算出来的ω \omega ω 乘到位置上去。
1 2 pos = pos.reshape(-1 ) out = np.einsum('m,d->md' , pos, omega)
在得到的ω \omega ω 中,我们实际上得到的是一个一维向量
[ 1 ( 1000 0 0 / d ) 1 ( 1000 0 2 / d ) ⋮ 1 ( 1000 0 2 i / d ) ] i ∈ [ 0 , d / 2 − 1 ] \begin{bmatrix}
\frac{1}{(10000^{0/d})} \\
\frac{1}{(10000^{2/d})} \\
\vdots \\
\frac{1}{(10000^{2i/d})}
\end{bmatrix}
\quad
i\in[0,d/2-1]
⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ ( 1 0 0 0 0 0 / d ) 1 ( 1 0 0 0 0 2 / d ) 1 ⋮ ( 1 0 0 0 0 2 i / d ) 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ i ∈ [ 0 , d / 2 − 1 ]
位置也可以写作一个一维向量
[ 0 1 ⋮ M − 1 ] \begin{bmatrix}
0 \\
1 \\
\vdots \\
M-1
\end{bmatrix}
⎣ ⎢ ⎢ ⎢ ⎢ ⎡ 0 1 ⋮ M − 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎤
则外积可得
[ 0 1 ⋮ M ] ⊗ [ 1 ( 1000 0 0 / d ) 1 ( 1000 0 2 / d ) ⋮ 1 ( 1000 0 2 i / d ) ] = [ 0 ⋅ 1 ( 1000 0 0 / d ) 0 ⋅ 1 ( 1000 0 2 / d ) ⋯ 0 ⋅ 1 ( 1000 0 2 i / d ) 1 ⋅ 1 ( 1000 0 0 / d ) 1 ⋅ 1 ( 1000 0 2 / d ) ⋯ 1 ⋅ 1 ( 1000 0 2 i / d ) ⋮ ⋮ ⋱ ⋮ ( M − 1 ) ⋅ 1 ( 1000 0 0 / d ) ( M − 1 ) ⋅ 1 ( 1000 0 2 / d ) ⋯ ( M − 1 ) ⋅ 1 ( 1000 0 2 i / d ) ] \begin{bmatrix}
0 \\
1 \\
\vdots \\
M
\end{bmatrix}\otimes
\begin{bmatrix}
\frac{1}{(10000^{0/d})} \\
\frac{1}{(10000^{2/d})} \\
\vdots \\
\frac{1}{(10000^{2i/d})}
\end{bmatrix} =
\begin{bmatrix}
0\cdot \frac{1}{(10000^{0/d})} & 0 \cdot \frac{1}{(10000^{2/d})} &\cdots & 0\cdot \frac{1}{(10000^{2i/d})} \\
1\cdot \frac{1}{(10000^{0/d})} & 1 \cdot \frac{1}{(10000^{2/d})} &\cdots & 1\cdot \frac{1}{(10000^{2i/d})} \\
\vdots & \vdots & \ddots & \vdots \\
(M-1)\cdot \frac{1}{(10000^{0/d})} & (M-1) \cdot \frac{1}{(10000^{2/d})} &\cdots & (M-1)\cdot \frac{1}{(10000^{2i/d})}
\end{bmatrix}
⎣ ⎢ ⎢ ⎢ ⎢ ⎡ 0 1 ⋮ M ⎦ ⎥ ⎥ ⎥ ⎥ ⎤ ⊗ ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ ( 1 0 0 0 0 0 / d ) 1 ( 1 0 0 0 0 2 / d ) 1 ⋮ ( 1 0 0 0 0 2 i / d ) 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ = ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ 0 ⋅ ( 1 0 0 0 0 0 / d ) 1 1 ⋅ ( 1 0 0 0 0 0 / d ) 1 ⋮ ( M − 1 ) ⋅ ( 1 0 0 0 0 0 / d ) 1 0 ⋅ ( 1 0 0 0 0 2 / d ) 1 1 ⋅ ( 1 0 0 0 0 2 / d ) 1 ⋮ ( M − 1 ) ⋅ ( 1 0 0 0 0 2 / d ) 1 ⋯ ⋯ ⋱ ⋯ 0 ⋅ ( 1 0 0 0 0 2 i / d ) 1 1 ⋅ ( 1 0 0 0 0 2 i / d ) 1 ⋮ ( M − 1 ) ⋅ ( 1 0 0 0 0 2 i / d ) 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤
这样我们就拿到了M × d / 2 M\times d/2 M × d / 2 大小的矩阵,里面包含了每个位置,每个channel位置的编码。即得到了
t 1000 0 2 i / d i ∈ [ 0 , 2 / d − 1 ] , t ∈ [ 0 , M − 1 ] \frac{t}{10000^{2i/d}}\quad i\in[0,2/d-1], t\in[0,M-1]
1 0 0 0 0 2 i / d t i ∈ [ 0 , 2 / d − 1 ] , t ∈ [ 0 , M − 1 ]
接下来是针对cos和sin的不同处理,最后得到一个channel大小是D D D 的完整的tensor
1 2 3 emb_sin = np.sin(out) emb_cos = np.cos(out) emb = np.concatenate([emb_sin, emb_cos], axis=1 )
我们立马就能注意到,这个实现方法与公式并不一样,首先是i i i 只索引到了d / 2 d/2 d / 2 ,并且没有将sin,cos项插入到奇偶位置,而是一个放在前面,另一个放在后面。这个实现方式是在后来tensor2tensor的代码仓库中发现的,之后的开源项目中几乎都使用的是这个版本的位置编码。
完整的code如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def get_1d_sincos_pos_embed (embed_dim, pos ): assert embed_dim % 2 == 0 omega = np.arange(embed_dim // 2 , dtype=np.float64) omega /= embed_dim / 2. omega = 1. / 10000 **omega pos = pos.reshape(-1 ) out = np.einsum('m,d->md' , pos, omega) emb_sin = np.sin(out) emb_cos = np.cos(out) emb = np.concatenate([emb_sin, emb_cos], axis=1 ) return emb
1.3 原版代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def get_angles (pos, i, d_model ): angle_rates = 1 / np.power(10000 , (2 * (i//2 )) / np.float32(d_model)) return pos * angle_ratesdef positional_encoding (position, d_model ): angle_rads = get_angles(np.arange(position)[:, np.newaxis], np.arange(d_model)[np.newaxis, :], d_model) angle_rads[:, 0 ::2 ] = np.sin(angle_rads[:, 0 ::2 ]) angle_rads[:, 1 ::2 ] = np.cos(angle_rads[:, 1 ::2 ]) pos_encoding = angle_rads[np.newaxis, ...] return tf.cast(pos_encoding, dtype=tf.float32)
在原版代码中,他们首先计算了一个get_angles
ω = t 1000 0 2 ⋅ ( i / / 2 ) / d \omega = \frac{t}{10000^{2\cdot(i//2)/d}}
ω = 1 0 0 0 0 2 ⋅ ( i / / 2 ) / d t
其中i//2
因为整除得到[ 0 , 0 , 1 , 1 , 2 , 2 , 3 , … ] [0, 0, 1, 1, 2, 2, 3,\dots] [ 0 , 0 , 1 , 1 , 2 , 2 , 3 , … ] ,再乘2就是[ 0 , 0 , 2 , 2 , 4 , 4 , 6 , … ] [0,0,2,2,4,4,6,\dots] [ 0 , 0 , 2 , 2 , 4 , 4 , 6 , … ] ,最后得到序列
[ 0 1000 0 0 / d , 1 1000 0 0 / d , 2 1000 0 2 / d , 3 1000 0 2 / d , 4 1000 0 4 / d , … ] \left[\frac{0}{10000^{0/d}}, \frac{1}{10000^{0/d}}, \frac{2}{10000^{2/d}}, \frac{3}{10000^{2/d}}, \frac{4}{10000^{4/d}},\dots\right]
[ 1 0 0 0 0 0 / d 0 , 1 0 0 0 0 0 / d 1 , 1 0 0 0 0 2 / d 2 , 1 0 0 0 0 2 / d 3 , 1 0 0 0 0 4 / d 4 , … ]
再通过下面的sin/cos和替换操作,得到
[ sin ( 0 1000 0 0 / d ) , cos ( 1 1000 0 0 / d ) , sin ( 2 1000 0 2 / d ) , cos ( 3 1000 0 2 / d ) , sin ( 4 1000 0 4 / d ) , … ] \left[\sin\left(\frac{0}{10000^{0/d}}\right),\cos\left(\frac{1}{10000^{0/d}}\right), \sin\left(\frac{2}{10000^{2/d}}\right), \cos\left(\frac{3}{10000^{2/d}}\right), \sin\left(\frac{4}{10000^{4/d}}\right),\dots\right]
[ sin ( 1 0 0 0 0 0 / d 0 ) , cos ( 1 0 0 0 0 0 / d 1 ) , sin ( 1 0 0 0 0 2 / d 2 ) , cos ( 1 0 0 0 0 2 / d 3 ) , sin ( 1 0 0 0 0 4 / d 4 ) , … ]
可以发现这个代码最后出来的结果是符合原来的公式的,即奇偶位置是sin/cos交替,并且无论奇偶位置都是2 i 2i 2 i 在指数位置。
2 2D sin/cos编码
当使用Transformer类模型处理图像数据的时候,我们可能会用到二维的位置编码,但其实idea很简单,就是分别在图像的高和宽上应用1D的位置编码。
我们首先创建二维的grid
1 2 3 grid_h = np.arange(grid_size, dtype=np.float32) grid_w = np.arange(grid_size, dtype=np.float32) grid = np.meshgrid(grid_w, grid_h)
其中grid_size
是长或者宽,meshgrid
之后我们得到的是二维grid的坐标list[np.array, np.array]
,其中每个np.array
是二维数组。np.meshgrid(X,Y)
返回两个坐标索引,第一个是X的,第二个是Y的,shape是l e n ( Y ) × l e n ( X ) len(Y)\times len(X) l e n ( Y ) × l e n ( X ) 。
接下来stack,reshape,并分割
1 2 3 4 5 6 7 8 9 10 11 12 13 grid = np.stack(grid, axis=0 ) grid = grid.reshape([2 , 1 , grid_size, grid_size]) pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)def get_2d_sincos_pos_embed_from_grid (embed_dim, grid ): assert embed_dim % 2 == 0 emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2 , grid[0 ]) emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2 , grid[1 ]) emb = np.concatenate([emb_h, emb_w], axis=1 ) return emb
这里将grid坐标stack起来得到2 × s × s 2\times s\times s 2 × s × s 大小的tensor,然后新增了一个维度,得到2 × 1 × s × s 2\times 1\times s\times s 2 × 1 × s × s ,在接下来的处理中,我们都只计算高和宽一半的embed_dim
,然后将grid[0]
和grid[1]
分别送入1D的encoding中,1D的函数里会直接展平grid进行计算,最后将得到的encoding concatenate起来得到完整的encoding。
笔者注: 这里我认为命名有问题,np.meshgrid
返回的第一个是x坐标,即图像宽度的位置id,第二个是y坐标,即图像高度的位置id,所以grid[0]
对应emb_w
,grid[1]
对应emb_h
。但可能并没有什么影响?(transpose一张图的位置信息并不影响学习其位置关系)
References