【多模态大模型学习】位置编码的学习记录

news2025/4/26 3:06:14

【多模态大模型学习】位置编码的学习记录

  • 0.前言
  • 1. sinusoidal编码
    • 1.0 数学知识——复数
      • 1.0.1 复数乘法、共轭复数
      • 1.0.2 复数的指数表示
    • 1.1 sinusoidal编码来历
    • 1.2 代码实现
  • 2. Rotary Positional Embedding (RoPE) ——旋转位置编码
    • 2.1 RoPE来历
    • 2.2 代码实现
      • 2.2.1 GPT-J风格的1D-RoPE实现
      • 2.2.2 GPT-NeoX style的1D-RoPE
  • 3. 二维旋转位置编码(2D-RoPE)
    • 3.1 ECCV的2D-RoPE论文中的实现
    • 3.2 qwen2-vl的实现
  • 4. qwen2-vl提出的M-RoPE
  • 5. qwen2.5-vl的位置编码
  • 6.很好的参考资料
  • 7.TODO

0.前言

  本文是近期位置编码相关内容的学习记录,之前遇到位置编码的内容都是直接跳过的,在看了近期一些模型还有苏建林老师的博客内容后发现位置编码也是一个很重要的内容。
  直接从最新的看会很迷惑这些位置编码的代码是在做什么神奇的操作。。。。。。以及为什么是这样,所以本文从最早的开始记录,也是我学习的过程。

1. sinusoidal编码

  这部分推荐看苏建林老师的博客,想不明白的地方可以截图不停地追问通义千问。

1.0 数学知识——复数

1.0.1 复数乘法、共轭复数

  在数学中,复数可以被表示为 a + b i a + bi a+bi 的形式,其中 a a a b b b 是实数, i i i 是虚数单位(满足 i 2 = − 1 i^2 = -1 i2=1)。复数可以在二维平面上用向量表示,横轴代表实部,纵轴代表虚部。
  假设我们有两个二维向量 [ x 1 , y 1 ] [x_1, y_1] [x1,y1] [ x 2 , y 2 ] [x_2, y_2] [x2,y2],我们可以将它们视为两个复数 z 1 = x 1 + y 1 i z_1 = x_1 + y_1i z1=x1+y1i z 2 = x 2 + y 2 z_2 = x_2 + y_2 z2=x2+y2
  复数的乘法遵循特定的规则:

z 1 ⋅ z 2 = ( x 1 + y 1 i ) ⋅ ( x 2 + y 2 i ) = x 1 x 2 − y 1 y 2 + ( x 1 y 2 + x 2 y 1 ) i z_1 \cdot z_2 = (x_1 + y_1i) \cdot (x_2 + y_2i) = x_1x_2 - y_1y_2 + (x_1y_2 + x_2y_1)i z1z2=(x1+y1i)(x2+y2i)=x1x2y1y2+(x1y2+x2y1)i

  如果我们想要计算两个复数的内积,并且只关心结果的实部,那么我们可以使用共轭的概念。给定一个复数 z = a + b i z = a + bi z=a+bi,其共轭定义为 z ∗ = a − b i z^* = a - bi z=abi。互为共轭的两个复数相乘,结果为模长平方。
z ⋅ z ∗ = ( a + b i ) ⋅ ( a − b i ) = a ( a ) + a ( − b i ) + ( b i ) a + ( b i ) ( − b i ) = a 2 − a b i + a b i − b 2 i 2 = a 2 + b 2 z \cdot z^* = (a + bi) \cdot (a - bi) = a(a) + a(-bi) + (bi)a + (bi)(-bi) = a^2 - abi + abi - b^2i^2 = a^2 + b^2 zz=(a+bi)(abi)=a(a)+a(bi)+(bi)a+(bi)(bi)=a2abi+abib2i2=a2+b2

  使用共轭可以帮助我们“消去”虚部,使得最终结果成为实数。对于两个复数 z 1 z_1 z1 z 2 z_2 z2,它们的共轭为
z 1 ⋅ z 2 ∗ = ( x 1 + y 1 i ) ⋅ ( x 2 − y 2 i ) = x 1 x 2 + y 1 y 2 + ( x 2 y 1 − x 1 y 2 ) i z_1 \cdot z_2^* = (x_1 + y_1i) \cdot (x_2 - y_2i) = x_1x_2 + y_1y_2 + (x_2y_1 - x_1y_2)i z1z2=(x1+y1i)(x2y2i)=x1x2+y1y2+(x2y1x1y2)i

它们的内积可以定义为:

⟨ z 1 , z 2 ⟩ = x 1 x 2 + y 1 y 2 = Re [ z 1 ⋅ z 2 ∗ ] \langle z_1, z_2 \rangle =x_1x_2 + y_1y_2= \text{Re}[z_1 \cdot z_2^*] z1,z2=x1x2+y1y2=Re[z1z2]

换句话说,给定两个复数 z 1 = x 1 + y 1 i z_1 = x_1 + y_1i z1=x1+y1i z 2 = x 2 + y 2 i z_2 = x_2 + y_2i z2=x2+y2i,它们作为二维向量的内积可以通过公式 ⟨ z 1 , z 2 ⟩ = x 1 x 2 + y 1 y 2 \langle z_1, z_2 \rangle = x_1x_2 + y_1y_2 z1,z2=x1x2+y1y2 来计算。

1.0.2 复数的指数表示

  复数还有指数表示形式,它基于欧拉公式(Euler’s formula),将复数与三角函数和指数函数联系起来。欧拉公式表述为:

e i θ = cos ⁡ ( θ ) + i sin ⁡ ( θ ) e^{i\theta} = \cos(\theta) + i\sin(\theta) eiθ=cos(θ)+isin(θ)

这里, e e e 是自然对数的底数,而 θ \theta θ 是以弧度为单位的角度。

  对于任意一个非零复数 z = a + b i z = a + bi z=a+bi,可以将其转换为极坐标形式(polar form)来表示,即通过它的模(magnitude)和辐角(argument)来描述:

  • (或绝对值): r = ∣ z ∣ = a 2 + b 2 r = |z| = \sqrt{a^2 + b^2} r=z=a2+b2
  • 辐角(或幅角): θ = arg ⁡ ( z ) \theta = \arg(z) θ=arg(z),是实轴正方向到从原点到复数点连线之间的夹角。

因此,任何非零复数都可以写成:

z = r ( cos ⁡ ( θ ) + i sin ⁡ ( θ ) ) z = r(\cos(\theta) + i\sin(\theta)) z=r(cos(θ)+isin(θ))

利用欧拉公式,这个表达式可以简化为指数形式:

z = r e i θ z = re^{i\theta} z=reiθ

这里, r r r 代表复数的长度或大小,而 e i θ e^{i\theta} eiθ描述了该复数的方向。

1.1 sinusoidal编码来历

  之所以要位置编码,在没有掩码的情况下,attention函数 f ( x ) f(x) f(x)是对称的,比如对于输入的Q序列里面的两个向量 x m x_m xm x n x_n xn调换位置( f 1 = { x 1 , . . . , x m , . . . , x n , . . . } f_1=\{x_1,...,x_m,...,x_n,...\} f1={x1,...,xm,...,xn,...} f 2 = { x 1 , . . . , x n , . . . , x m , . . . } f_2=\{x_1,...,x_n,...,x_m,...\} f2={x1,...,xn,...,xm,...}),有 f 1 = f 2 f_1=f_2 f1=f2,从结果上区分不出输入是 x m x_m xm还是 x n x_n xn
  所以要让attention的 Q ⋅ K Q \cdot K QK这个乘法过程中, Q Q Q K K K分别带上位置信息,每个位置的向量加上一个和位置信息相关的向量 p p p,变成例如 { x 1 + p 1 , . . . , x m + p m , . . . , x n + p n } \{x_1+p_1,...,x_m+p_m,...,x_n+p_n\} {x1+p1,...,xm+pm,...,xn+pn} p m p_m pm是位置编码向量。
  在只考虑m,n这两个位置的位置编码情况下,泰勒展开后发现只有 p m T H p n p_m^T\mathcal{H} p_n pmTHpn这一项同时包含 p m p_m pm p n p_n pn。在最简单的情况下,取 H = I \mathcal{H}=\mathcal{I} H=I,此时 p m T H p n = p m T p n = ⟨ p m , p n ⟩ p_m^T\mathcal{H} p_n=p_m^Tp_n=\langle p_m,p_n\rangle pmTHpn=pmTpn=pm,pn。希望这一项能够表示m和n的相对位置,最好能有一个函数 g ( ⋅ ) g(\cdot) g()使得
⟨ p m , p n ⟩ = g ( m − n ) \langle p_m,p_n\rangle=g(m-n) pm,pn=g(mn)
  为了方便理解,先考虑2维的情况,假如 Q Q Q是2维的,借助复数作为工具进行计算,有 ⟨ p m , p n ⟩ = Re [ p m ⋅ p n ∗ ] \langle p_m,p_n\rangle= \text{Re}[p_m \cdot p_n^*] pm,pn=Re[pmpn]
  假设有复数 q m − n q_{m-n} qmn让上式成立, p m ⋅ p n ∗ = q m − n p_m \cdot p_n^*=q_{m-n} pmpn=qmn。用复数的指数形式表示,假设 p m = r m e i ϕ m p_m=r_me^{i \phi_m} pm=rmeiϕm, p n ∗ = r n e − i ϕ n p_n^*=r_ne^{-i \phi_n} pn=rneiϕn, q m − n = R m − n e i Φ m − n q_{m-n}=R_{m-n}e^{i \Phi{m-n}} qmn=RmneiΦmn

解方程:
r m r n e i ( ϕ m − ϕ n ) = R m − n e i Φ m − n r_mr_ne^{i(\phi_m-\phi_n)}=R_{m-n}e^{i\Phi_{m-n}} rmrnei(ϕmϕn)=RmneiΦmn
整理后有:
{ r m r n = R m − n ϕ m − ϕ n = Φ m − n \left\{ \begin{array}{l} r_m r_n = R_{m-n} \\ \phi_m - \phi_n = \Phi_{m-n} \end{array} \right. {rmrn=Rmnϕmϕn=Φmn

  • 解第一个条件 对于 r m r n = R m − n r_m r_n = R_{m-n} rmrn=Rmn
    n = m n = m n=m 时,可以得到 r m 2 = R 0 r_m^2 = R_0 rm2=R0。这意味着 r m r_m rm 是一个常数(因为 R 0 R_0 R0 是一个固定值),为了简化,设 r m = 1 r_m = 1 rm=1

  • 解第二个条件,对于 ϕ m − ϕ n = Φ m − n \phi_m - \phi_n = \Phi_{m-n} ϕmϕn=Φmn
    首先,令 n = 0 n = 0 n=0,则有 ϕ m − ϕ 0 = Φ m \phi_m - \phi_0 = \Phi_m ϕmϕ0=Φm,如果我们假设 ϕ 0 = 0 \phi_0 = 0 ϕ0=0(不失一般性,因为角度是相对的),那么 ϕ m = Φ m \phi_m = \Phi_m ϕm=Φm。接着,令 n = m − 1 n = m - 1 n=m1,则有 ϕ m − ϕ m − 1 = Φ 1 \phi_m - \phi_{m-1} = \Phi_1 ϕmϕm1=Φ1,这里 Φ 1 \Phi_1 Φ1 是一个固定的相位差。由于 Φ m − n \Phi_{m-n} Φmn 表示的是相对位置信息,因此 Φ 1 \Phi_1 Φ1实际上是一个常数。这意味着 { ϕ m } \{\phi_m\} {ϕm} 形成了一个等差数列,其中每一项之间的差值为 Φ 1 \Phi_1 Φ1。用数学语言来说,就是存在一个常数 θ \theta θ(在这里 θ = Φ 1 \theta = \Phi_1 θ=Φ1)使得 ϕ m = m θ \phi_m = m\theta ϕm=mθ

  最终,通过解方程可以得到隐向量维度是2维的情况下,位置编码的一个解,通过欧拉公式表示为cos和sin的形式:
p m = e i m θ ⇔ p m = ( cos ⁡ ( m θ ) sin ⁡ ( m θ ) ) p_m = e^{im\theta} \quad \Leftrightarrow \quad p_m = \begin{pmatrix} \cos(m\theta) \\ \sin(m\theta) \end{pmatrix} pm=eimθpm=(cos(mθ)sin(mθ))
Q Q Q向量的隐向量维度是 d d d维时,位置 m m m Q m Q_m Qm对应的编码向量 p m = ( cos ⁡ ( m θ 0 ) sin ⁡ ( m θ 0 ) cos ⁡ ( m θ 1 ) sin ⁡ ( m θ 1 ) . . . cos ⁡ ( m θ d / 2 − 1 ) sin ⁡ ( m θ d / 2 − 1 ) ) p_m=\begin{pmatrix} \cos(m\theta_0) \\ \sin(m\theta_0) \\ \cos(m\theta_1) \\ \sin(m\theta_1) \\ ... \\ \cos(m\theta_{d/2-1}) \\ \sin(m\theta_{d/2-1}) \end{pmatrix} pm= cos(mθ0)sin(mθ0)cos(mθ1)sin(mθ1)...cos(mθd/21)sin(mθd/21)
  这里面需要注意的是, d d d指的是隐向量的维度, m m m指的是向量是在第 m m m个。在《Attention is All You Need》,位置编码的计算公式如下:
{ p k , 2 i = sin ⁡ ( k 1000 0 2 i / d ) p k , 2 i + 1 = cos ⁡ ( k 1000 0 2 i / d ) \begin{cases} p_{k,2i} = \sin\left(\frac{k}{10000^{2i/d}}\right) \\ p_{k,2i+1} = \cos\left(\frac{k}{10000^{2i/d}}\right) \end{cases} {pk,2i=sin(100002i/dk)pk,2i+1=cos(100002i/dk)
这里, p k , 2 i p_{k,2i} pk,2i p k , 2 i + 1 p_{k,2i+1} pk,2i+1 分别表示位置 k k k 的编码向量的第 2 i 2i 2i 2 i + 1 2i+1 2i+1 个分量, k k k 是位置索引(对应上面的推导的 m m m), i i i 是向量维度的索引, d d d 是向量的总维度。对应位置的位置编码会和在attention运算前 Q Q Q K K K相加。

1.2 代码实现

  看了下代码,之前不少多模态模型的位置编码都是学习式的,而且是直接位置编码和 q q q k k k相加。《Attention is all you need》有一份别人实现的pytorch代码里面是sinusoidal编码,并且完全遵循了上面公式的实现方式,sinusoidal编码也是和 q q q k k k相加:

#https://github.com/jadore801120/attention-is-all-you-need-pytorch/blob/master/transformer/Models.py
class PositionalEncoding(nn.Module):

    def __init__(self, d_hid, n_position=200):
        super(PositionalEncoding, self).__init__()

        # Not a parameter
        self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid))

    def _get_sinusoid_encoding_table(self, n_position, d_hid):
        ''' Sinusoid position encoding table '''
        # TODO: make it with torch instead of numpy

        def get_position_angle_vec(position):
            return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]

        sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
        sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2])  # dim 2i
        sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2])  # dim 2i+1

        return torch.FloatTensor(sinusoid_table).unsqueeze(0)

    def forward(self, x):
        return x + self.pos_table[:, :x.size(1)].clone().detach()  # 直接相加

2. Rotary Positional Embedding (RoPE) ——旋转位置编码

  • 运算前含有绝对位置的信息,运算后的结果含有相对位置的信息

2.1 RoPE来历

  RoPE是苏建林老师在博客里面提出来的一种位置编码方式,提出的背景、证明等可以参考其博客空间。这部分如果有比较晦涩难懂的地方也是可以直接截博客里面的图片问通义千问,通义千问可以在看图之后进行非常仔细的解答。
  RoPE用绝对编码的方式,在计算 Q Q Q K K K的内积时,又让结果能带入 Q Q Q K K K的相对信息。对于位置为 m m m q m q_m qm和位置为n的 k n k_n kn分别乘以绝对位置编码 e i m θ e^{im\theta} eimθ e i n θ e^{in\theta} einθ,得到 q m e i m θ q_me^{im\theta} qmeimθ q n e i n θ q_ne^{in\theta} qneinθ,在进行内积运算,会发现运算结果含有相对信息
⟨ q m e i m θ , k n e i n θ ⟩ = Re ⁡ [ ( q m e i m θ ) ( k n e i n θ ) ∗ ] = Re ⁡ [ q m k n ∗ e i ( m − n ) θ ] \langle q_m e^{im\theta}, k_n e^{in\theta} \rangle = \operatorname{Re} \left[ (q_m e^{im\theta}) (k_n e^{in\theta})^* \right] = \operatorname{Re} \left[ q_m k_n^* e^{i(m-n)\theta} \right] qmeimθ,kneinθ=Re[(qmeimθ)(kneinθ)]=Re[qmknei(mn)θ]
  最简单的情况下假如 Q Q Q向量的隐向量维度 d = 2 d=2 d=2,这个操作对于位置 m m m q m q_m qm向量进行了一个旋转操作
q m e i m θ = ( cos ⁡ m θ − sin ⁡ m θ sin ⁡ m θ cos ⁡ m θ ) ( q m 0 q m 1 ) q_m e^{im\theta} = \begin{pmatrix} \cos m\theta & -\sin m\theta \\ \sin m\theta & \cos m\theta \end{pmatrix} \begin{pmatrix} q_m^0 \\ q_m^1 \end{pmatrix} qmeimθ=(cosmθsinmθsinmθcosmθ)(qm0qm1)
  通用的情况下,对于位置在 m m m(可以说position_id=m)的 Q Q Q向量 q m q_m qm,它的旋转位置编码的计算过程为:
( q 0 q 1 q 2 q 3 ⋮ q d − 2 q d − 1 ) ⊗ ( cos ⁡ m θ 0 cos ⁡ m θ 0 cos ⁡ m θ 1 cos ⁡ m θ 1 ⋮ cos ⁡ m θ d / 2 − 1 cos ⁡ m θ d / 2 − 1 ) + ( − q 1 q 0 − q 3 q 2 ⋮ − q d − 1 q d − 2 ) ⊗ ( sin ⁡ m θ 0 sin ⁡ m θ 0 sin ⁡ m θ 1 sin ⁡ m θ 1 ⋮ sin ⁡ m θ d / 2 − 1 sin ⁡ m θ d / 2 − 1 ) \begin{pmatrix} q_0 \\ q_1 \\ q_2 \\ q_3 \\ \vdots \\ q_{d-2} \\ q_{d-1} \end{pmatrix} \otimes \begin{pmatrix} \cos m\theta_0 \\ \cos m\theta_0 \\ \cos m\theta_1 \\ \cos m\theta_1 \\ \vdots \\ \cos m\theta_{d/2-1} \\ \cos m\theta_{d/2-1} \end{pmatrix} + \begin{pmatrix} -q_1 \\ q_0 \\ -q_3 \\ q_2 \\ \vdots \\ -q_{d-1} \\ q_{d-2} \end{pmatrix} \otimes \begin{pmatrix} \sin m\theta_0 \\ \sin m\theta_0 \\ \sin m\theta_1 \\ \sin m\theta_1 \\ \vdots \\ \sin m\theta_{d/2-1} \\ \sin m\theta_{d/2-1} \end{pmatrix} q0q1q2q3qd2qd1 cosmθ0cosmθ0cosmθ1cosmθ1cosmθd/21cosmθd/21 + q1q0q3q2qd1qd2 sinmθ0sinmθ0sinmθ1sinmθ1sinmθd/21sinmθd/21

2.2 代码实现

  • RoPE是相乘进行的位置编码,不是相加

2.2.1 GPT-J风格的1D-RoPE实现

  看代码这部分比较让人头大,如果是完全按照上面公式来实现的是一目了然的,首先看这种实现方式,被称为GPT-J。在Meta官方实现的llama代码里面,可以找到这种实现方式。当然,这里也不是使用提到的乘法方式,而是使用了复数运算。一个复数对应2个实数,所以如果是 q q q转为了复数,维度只有 d / 2 d/2 d/2,最后变成实数时回到 d d d维。以及需要注意 q 0 q_0 q0 q 1 q_1 q1对应的是 m θ 0 m\theta_0 mθ0,所以freqs_cis的维度只有 q q q k k k的一半就够了。

# https://github.com/meta-llama/llama/blob/main/llama/model.py

# 下面这个函数是要预先把从0到最大长度的位置编码需要使用的角度算好
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0):
    """
    Precompute the frequency tensor for complex exponentials (cis) with given dimensions.

    This function calculates a frequency tensor with complex exponentials using the given dimension 'dim'
    and the end index 'end'. The 'theta' parameter scales the frequencies.
    The returned tensor contains complex values in complex64 data type.

    Args:
        dim (int): Dimension of the frequency tensor.
        end (int): End index for precomputing frequencies.
        theta (float, optional): Scaling factor for frequency computation. Defaults to 10000.0.

    Returns:
        torch.Tensor: Precomputed frequency tensor with complex exponentials.
    """
    ## 因为维度为2i、2i+1的mθ相同,所以是(0, dim, 2)
    freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)) # 算θ值
    t = torch.arange(end, device=freqs.device)  # end是最大长度,对应一个个位置m
    freqs = torch.outer(t, freqs).float()  #这个是算m*θ行数为t,列数为dim//2,每行对应一个q向量
    freqs_cis = torch.polar(torch.ones_like(freqs), freqs)  # 变成复数形式,幅度为1,角度为freqs
    return freqs_cis

# 在每个attention block中有
xq = xq.view(bsz, seqlen, self.n_local_heads, self.head_dim)
xk = xk.view(bsz, seqlen, self.n_local_kv_heads, self.head_dim)
xv = xv.view(bsz, seqlen, self.n_local_kv_heads, self.head_dim)
xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis)
......
score = torch.matmul(xq,xk.transpose(2,3)) # 位置编码后直接计算attention分数

def apply_rotary_emb(
    xq: torch.Tensor,
    xk: torch.Tensor,
    freqs_cis: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor]:
    """
    Apply rotary embeddings to input tensors using the given frequency tensor.

    This function applies rotary embeddings to the given query 'xq' and key 'xk' tensors using the provided
    frequency tensor 'freqs_cis'. The input tensors are reshaped as complex numbers, and the frequency tensor
    is reshaped for broadcasting compatibility. The resulting tensors contain rotary embeddings and are
    returned as real tensors.

    Args:
        xq (torch.Tensor): Query tensor to apply rotary embeddings.
        xk (torch.Tensor): Key tensor to apply rotary embeddings.
        freqs_cis (torch.Tensor): Precomputed frequency tensor for complex exponentials.

    Returns:
        Tuple[torch.Tensor, torch.Tensor]: Tuple of modified query tensor and key tensor with rotary embeddings.     

    """
    # 把q向量看成复数,2个2个一组看成一个复数,例如(q0,q1)->(qc_0)
    xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
    xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))
    freqs_cis = reshape_for_broadcast(freqs_cis, xq_)
    xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3) # 做乘法
    xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)
    return xq_out.type_as(xq), xk_out.type_as(xk)

  上面的代码是用复数乘法实现的,可能不是特别直观,考虑最简单的 d = 2 d=2 d=2的情形,这种情况下令 q = ( q 0 , q 1 ) q=(q_0,q_1) q=(q0,q1),这两个向量要旋转的角度是 θ 0 \theta_0 θ0
  首先,apply_rotary_emb()函数里面的view_as_complex是让 q 0 q_0 q0 q 1 q_1 q1组成了一个复数 q c = q 0 + i ⋅ q 1 q_c={q_0+i \cdot q_1} qc=q0+iq1
  假设 freqs_cis 对应于这个位置和频率分量的旋转因子为 e i θ 0 = cos ⁡ ( θ 0 ) + i sin ⁡ ( θ 0 ) e^{i\theta_0} = \cos(\theta_0) + i\sin(\theta_0) eiθ0=cos(θ0)+isin(θ0),即[ c o s ( θ 0 ) cos(\theta_0) cos(θ0), s i n ( θ 0 ) sin(\theta_0) sin(θ0)],注意预先计算的函数precompute_freqs_cis()里面最后也是以复数形式表示的,这个cos和sin变成了一个复数,也就是freqs_cis[0] = c o s ( θ 0 ) + i ⋅ s i n ( θ 0 ) cos(\theta_0) + i \cdot sin(\theta_0) cos(θ0)+isin(θ0)

  对 q 0 q_0 q0 q 1 q_1 q1进行旋转,需要执行复数乘法xq_ * freqs_cis
q m e i m θ 0 = ( q 0 + i ⋅ q 1 ) × ( cos ⁡ ( θ 0 ) + i ⋅ sin ⁡ ( θ 0 ) ) q_me^{im\theta_0} = (q0 + i \cdot q1) \times (\cos(\theta_0) + i \cdot \sin(\theta_0)) qmeimθ0=(q0+iq1)×(cos(θ0)+isin(θ0))

根据复数乘法公式:

( a + b i ) × ( c + d i ) = ( a c − b d ) + i ( a d + b c ) (a + bi) \times (c + di) = (ac - bd) + i(ad + bc) (a+bi)×(c+di)=(acbd)+i(ad+bc)

上述表达式展开为:

( q 0 ⋅ cos ⁡ ( θ 0 ) − q 1 ⋅ sin ⁡ ( θ 0 ) ) + i ( q 0 ⋅ sin ⁡ ( θ 0 ) + q 1 ⋅ cos ⁡ ( θ 0 ) ) (q0 \cdot \cos(\theta_0) - q1 \cdot \sin(\theta_0)) + i(q0 \cdot \sin(\theta_0) + q1 \cdot \cos(\theta_0)) (q0cos(θ0)q1sin(θ0))+i(q0sin(θ0)+q1cos(θ0))

因此,旋转后的结果是一个新的复数,其实部和虚部分别为:

  • 实部: q 0 ⋅ cos ⁡ ( θ 0 ) − q 1 ⋅ sin ⁡ ( θ 0 ) q0 \cdot \cos(\theta_0) - q1 \cdot \sin(\theta_0) q0cos(θ0)q1sin(θ0)
  • 虚部: q 0 ⋅ sin ⁡ ( θ 0 ) + q 1 ⋅ cos ⁡ ( θ 0 ) q0 \cdot \sin(\theta_0) + q1 \cdot \cos(\theta_0) q0sin(θ0)+q1cos(θ0)

这和1D-RoPE的结果是一致的:

( q 0 ⋅ cos ⁡ ( θ 0 ) − q 1 ⋅ sin ⁡ ( θ 0 ) , q 0 ⋅ sin ⁡ ( θ 0 ) + q 1 ⋅ cos ⁡ ( θ 0 ) ) (q0 \cdot \cos(\theta_0) - q1 \cdot \sin(\theta_0), q0 \cdot \sin(\theta_0) + q1 \cdot \cos(\theta_0)) (q0cos(θ0)q1sin(θ0),q0sin(θ0)+q1cos(θ0))

2.2.2 GPT-NeoX style的1D-RoPE

  在eleuther的官方实现以及transformer的llama代码里面,使用的是另一种风格的旋转位置编码的代码,没有用到复数计算。重点关注forward、rotate_half以及embedding函数。

# https://github.com/huggingface/transformers/blob/main/src/transformers/models/llama/modeling_llama.py

class LlamaRotaryEmbedding(nn.Module):
    def __init__(self, config: LlamaConfig, device=None):
        super().__init__()
        # BC: "rope_type" was originally "type"
        if hasattr(config, "rope_scaling") and config.rope_scaling is not None:
            self.rope_type = config.rope_scaling.get("rope_type", config.rope_scaling.get("type"))
        else:
            self.rope_type = "default"
        self.max_seq_len_cached = config.max_position_embeddings
        self.original_max_seq_len = config.max_position_embeddings

        self.config = config
        self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type]

        inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device)
        self.register_buffer("inv_freq", inv_freq, persistent=False)
        self.original_inv_freq = self.inv_freq

    def _dynamic_frequency_update(self, position_ids, device):
        """
        dynamic RoPE layers should recompute `inv_freq` in the following situations:
        1 - growing beyond the cached sequence length (allow scaling)
        2 - the current sequence length is in the original scale (avoid losing precision with small sequences)
        """
        seq_len = torch.max(position_ids) + 1
        if seq_len > self.max_seq_len_cached:  # growth
            inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device, seq_len=seq_len)
            self.register_buffer("inv_freq", inv_freq, persistent=False)  # TODO joao: may break with compilation
            self.max_seq_len_cached = seq_len

        if seq_len < self.original_max_seq_len and self.max_seq_len_cached > self.original_max_seq_len:  # reset
            # This .to() is needed if the model has been moved to a device after being initialized (because
            # the buffer is automatically moved, but not the original copy)
            self.original_inv_freq = self.original_inv_freq.to(device)
            self.register_buffer("inv_freq", self.original_inv_freq, persistent=False)
            self.max_seq_len_cached = self.original_max_seq_len

    @torch.no_grad()
    def forward(self, x, position_ids):
        if "dynamic" in self.rope_type:
            self._dynamic_frequency_update(position_ids, device=x.device)

        # Core RoPE block
        inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1)
        position_ids_expanded = position_ids[:, None, :].float()
        # Force float32 (see https://github.com/huggingface/transformers/pull/29285)
        device_type = x.device.type
        device_type = device_type if isinstance(device_type, str) and device_type != "mps" else "cpu"
        with torch.autocast(device_type=device_type, enabled=False):
            freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2)
            emb = torch.cat((freqs, freqs), dim=-1)
            cos = emb.cos()
            sin = emb.sin()

        # Advanced RoPE types (e.g. yarn) apply a post-processing scaling factor, equivalent to scaling attention
        cos = cos * self.attention_scaling
        sin = sin * self.attention_scaling

        return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)  # 这个更像原始的RoPE,没有变成复数,就是分开了cos和sin


def rotate_half(x):
    """Rotates half the hidden dims of the input."""
    x1 = x[..., : x.shape[-1] // 2]
    x2 = x[..., x.shape[-1] // 2 :]
    return torch.cat((-x2, x1), dim=-1)


def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
    """Applies Rotary Position Embedding to the query and key tensors.

    Args:
        q (`torch.Tensor`): The query tensor.
        k (`torch.Tensor`): The key tensor.
        cos (`torch.Tensor`): The cosine part of the rotary embedding.
        sin (`torch.Tensor`): The sine part of the rotary embedding.
        position_ids (`torch.Tensor`, *optional*):
            Deprecated and unused.
        unsqueeze_dim (`int`, *optional*, defaults to 1):
            The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and
            sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note
            that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and
            k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes
            cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have
            the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2.
    Returns:
        `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding.
    """
    cos = cos.unsqueeze(unsqueeze_dim)
    sin = sin.unsqueeze(unsqueeze_dim)
    q_embed = (q * cos) + (rotate_half(q) * sin)  # 像是原始公式里面的cos和sin操作
    k_embed = (k * cos) + (rotate_half(k) * sin)
    return q_embed, k_embed

  一眼看下来,会发现不对啊,如果没有rotate_half是可以理解的,rotate_half之后,和原始公式对不上了。原来的是比如 ( q 0 , q 1 ) (q_0,q_1) (q0,q1)是在一组的,得到 ( q 0 ⋅ cos ⁡ ( θ 0 ) − q 1 ⋅ sin ⁡ ( θ 0 ) , q 0 ⋅ sin ⁡ ( θ 0 ) + q 1 ⋅ cos ⁡ ( θ 0 ) ) (q0 \cdot \cos(\theta_0) - q1 \cdot \sin(\theta_0), q0 \cdot \sin(\theta_0) + q1 \cdot \cos(\theta_0)) (q0cos(θ0)q1sin(θ0),q0sin(θ0)+q1cos(θ0))。现在的 q 0 q_0 q0 q d / / 2 q_{d//2} qd//2咋在一起了。而且神奇的是,meta版本代码训练的模型,能用transformer版本的代码加载。
  github上有一个issue解释这个问题,具体参考这个issue。首先结论是,这是2种RoPE方式,这两种方式结果肯定不一样。但是,如果加入一些转换的代码,转换后能和另一种方式能对上。
  meta的GPT-J风格的模型,要用transformer加载,需要先把 W q W_q Wq矩阵 W k W_k Wk矩阵进行一些转换,有一个permute函数专门进行这个操作。

# https://github.com/huggingface/transformers/blob/main/src/transformers/models/llama/convert_llama_weights_to_hf.py

def permute(w, n_heads, dim1=dim, dim2=dim):
    return w.view(n_heads, dim1 // n_heads // 2, 2, dim2).transpose(1, 2).reshape(dim1, dim2)

 f"model.layers.{layer_i}.self_attn.q_proj.weight": permute(
                        loaded[f"layers.{layer_i}.attention.wq.weight"], n_heads=n_heads
 )
 ........
## 

  这个函数的效果在官方论坛里面有,仔细比对前后,还真能对上。。。。把 W q W_q Wq W k W_k Wk矩阵里面元素位置变了是真没想到的。
在这里插入图片描述
在这里插入图片描述

  为什么用这种实现方式,而不是GPT-J的实现方式,官方的解答是,一方面这种方式开销小,另一方面最重要的原因是GPT-J涉及版权的问题。

3. 二维旋转位置编码(2D-RoPE)

  一维旋转位置编码1D-RoPE,或是二维的2D-RoPE,这个维度指的是有几维的位置信息,也就是position_id的维度。对于文本,是一维序列,所以只有 x x x轴上的信息,position_id =
{0,1,2,3…},也就是之前提到的 m m m,表示到底这个 q q q是在第几个。对于图片,ViT会切分为一个个patch,每个patch需要标识是在第几行、第几列,所以需要{w,h}的形式来表示。如果是视频,还有时间维度时间帧的信息,需要三维的形式{t,w,h}。
  上面的1D-RoPE扩展到高维度的方式很简单粗暴,如果要进行X-D RoPE,就把 q q q k k k向量从头到尾平均分成X份,每一份里面再按照position_id进行1D-RoPE。例如是要进行2D RoPE,就把 q q q分成 q [ 0 : d / 2 ] q[0:d/2] q[0:d/2] q [ d / 2 : ] q[d/2:] q[d/2:]这两份, k k k分成 k [ 0 : d / 2 ] k[0:d/2] k[0:d/2] k [ d / 2 : ] k[d/2:] k[d/2:]这两份,position_id是[(x,y)]的形式,就让前半段计算 q ⋅ e i x θ q \cdot e^{ix\theta} qeixθ,后半段计算前半段计算 q ⋅ e i y θ q \cdot e^{iy\theta} qeiyθ
  2D-RoPE里面难的地方可能是position_id的计算,尤其如果输入不止有图片,是图文混杂的情况下。苏神完整分析了各种可行性方案,在他的博客中可以仔细阅读这一部分——多模态位置 编码的思考。提到了一种方案,就是如果输入是图片,就用(x,y)的形式给出position_id,如果输入是文本,就让 x = y x=y x=y
  举例而言,首先,对于patch大小为 w ∗ h w*h wh的图片,如果图片在开头:

position_id第1行第h行
x1 … 1h … h
y1 … w1 … w

  如果开头是一段文本,文本的长度为 L L L,这个句子的位置编码为 { ( 1 , 1 ) , ( 2 , 2 ) , . . . , ( L , L ) } \{(1,1),(2,2),...,(L,L)\} {(1,1),(2,2),...,(L,L)},上面的图片接在这段文本后面,图片的编码变为

position_id第1行第h行
xL+1 … 1L+h … L+h
yL+1 … L+wL+1 … L+w

  苏神提到这种方式看着不完美,没有对称。因为句子的最后一个token的位置ID是 ( L , L ) (L,L) (L,L),它和图片的第一个patch的位置ID ( L + 1 , L + 1 ) (L+1,L+1) (L+1,L+1)的差距是 ( 1 , 1 ) (1,1) (1,1),但是如果图片后面再接一个句子,因为图片的最后一个token的位置ID是 ( L + h , L + w ) (L+h,L+w) (L+h,L+w),如果 w ≠ h w \neq h w=h,后面这个句子的位置ID不可能和前面图片最后一个token的差距也是 ( 1 , 1 ) (1,1) (1,1),显得不优雅,只有像下面示意这样 w = h w = h w=h时才对称。
在这里插入图片描述
  更进阶的苏神提到的位置编码的方式可以看他的博客尤其多模态编码的思考这篇,还有下面朋友们写的评论,都非常有价值。看到这里应该就可以去看多模态模型的2D-RoPE的相关的源代码了。

3.1 ECCV的2D-RoPE论文中的实现

  《Rotary Position Embedding for Vision Transformer》这篇论文在ViT中实现了2D-RoPE,里面实现了2个版本,一个是mix的2D-ROPE,一个是axial的2D-ROPE,主要看axial版本的。

#https://github.com/naver-ai/rope-vit/blob/main/models/vit_rope.py

# 计算position id
def init_t_xy(end_x: int, end_y: int):
    t = torch.arange(end_x * end_y, dtype=torch.float32)
    t_x = (t % end_x).float()
    t_y = torch.div(t, end_x, rounding_mode='floor').float()
    return t_x, t_y

# 计算RoPE的角度mθ
def compute_axial_cis(dim: int, end_x: int, end_y: int, theta: float = 100.0):
    freqs_x = 1.0 / (theta ** (torch.arange(0, dim, 4)[: (dim // 4)].float() / dim))
    freqs_y = 1.0 / (theta ** (torch.arange(0, dim, 4)[: (dim // 4)].float() / dim))

    t_x, t_y = init_t_xy(end_x, end_y)
    freqs_x = torch.outer(t_x, freqs_x)
    freqs_y = torch.outer(t_y, freqs_y)
    freqs_cis_x = torch.polar(torch.ones_like(freqs_x), freqs_x)
    freqs_cis_y = torch.polar(torch.ones_like(freqs_y), freqs_y)
    return torch.cat([freqs_cis_x, freqs_cis_y], dim=-1)

def apply_rotary_emb(xq: torch.Tensor, xk: torch.Tensor, freqs_cis: torch.Tensor):
    xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
    xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))
    freqs_cis = reshape_for_broadcast(freqs_cis, xq_)
    xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
    xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)
    return xq_out.type_as(xq).to(xq.device), xk_out.type_as(xk).to(xk.device)
    
class RoPEAttention(Attention):
    """Multi-head Attention block with rotary position embeddings."""
    def forward(self, x, freqs_cis):
        B, N, C = x.shape
        qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
        q, k, v = qkv[0], qkv[1], qkv[2]
        
        q[:, :, 1:], k[:, :, 1:] = apply_rotary_emb(q[:, :, 1:], k[:, :, 1:], freqs_cis=freqs_cis)  # 这里跳过第一个不编码,是因为self-attn的里面x[0]是[CLS] token
        attn = (q * self.scale) @ k.transpose(-2, -1)
        attn = attn.softmax(dim=-1)
        attn = self.attn_drop(attn)

        x = (attn @ v).transpose(1, 2).reshape(B, N, C)
        x = self.proj(x)
        x = self.proj_drop(x)
        
        return x

# 这个的attention块是上面的RoPEAttention
class rope_vit_models(vit_models):
    def __init__(self, rope_theta=100.0, rope_mixed=False, use_ape=False,
                 **kwargs):
        super().__init__(**kwargs)
        
        self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
		self.compute_cis = partial(compute_axial_cis, dim=embed_dim//num_heads, theta=rope_theta)
    def forward_features(self, x):      
		freqs_cis = self.compute_cis(end_x = img_size // patch_size, end_y = img_size // patch_size)
	 	self.freqs_cis = freqs_cis
	    if self.freqs_cis.shape[0] != x.shape[1] - 1:
	    	freqs_cis = self.compute_cis(end_x = W // self.patch_size, end_y = H // self.patch_size)
	    else:
	       freqs_cis = self.freqs_cis
	    freqs_cis = freqs_cis.to(x.device)
	            
	    for i , blk in enumerate(self.blocks):
	    	x = blk(x, freqs_cis=freqs_cis)

3.2 qwen2-vl的实现

  首先需要明确一下,qwen2-vl的论文里面重点是说M-RoPE的优点,但是里面也写道了qwen2-vl重新训练了ViT,ViT使用的是2D-RoPE。2D-RoPE是在纯ViT部分使用的,图片首先被CNN变成patch,然后进ViT的block,在ViT的block里面使用2D-RoPE。出了ViT之后,图片向量填充到文本向量预留的位置里面之后,对这个图文混合向量才是使用M-RoPE。
  qwen2-vl的位置编码风格是GPT-NeoX的风格的,所以会有rotate_half()函数,首先看角度生成和最后的乘法部分 q ⋅ e i θ q \cdot e^{i \theta} qeiθ,看这部分的时候会疑惑position_id的代码去哪里了,稍后再看。

# modeling_qwen2_vl.py
# 里面涉及到的是apply_rotary_pos_emb_vision函数,以q为例子
# 输入为q和位置信息rotary_pos_emb

#1. rotary_pos_emb的值为θ角
class VisionRotaryEmbedding(nn.Module):
    def __init__(self, dim: int, theta: float = 10000.0) -> None:
        super().__init__()
        inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float) / dim))
        self.register_buffer("inv_freq", inv_freq, persistent=False)

    def forward(self, seqlen: int) -> torch.Tensor:
        seq = torch.arange(seqlen, device=self.inv_freq.device, dtype=self.inv_freq.dtype)
        freqs = torch.outer(seq, self.inv_freq)
        return freqs
head_dim = config.embed_dim // config.num_heads
self.rotary_pos_emb = VisionRotaryEmbedding(head_dim // 2) # 二维的rope,所以只需要一半

# Copied from transformers.models.llama.modeling_llama.rotate_half
def rotate_half(x):
    """Rotates half the hidden dims of the input."""
    x1 = x[..., : x.shape[-1] // 2]
    x2 = x[..., x.shape[-1] // 2 :]
    return torch.cat((-x2, x1), dim=-1)

def apply_rotary_pos_emb_vision(tensor: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
    orig_dtype = tensor.dtype
    tensor = tensor.float()
    cos = freqs.cos()
    sin = freqs.sin()
    cos = cos.unsqueeze(1).repeat(1, 1, 2).unsqueeze(0).float() # 先在第1维增加一个维度,变成[seqlen,1,dim//4] repeat(1, 1, 2) 表示在第0维不重复,在第1维不重复,在第2维重复2次,变成[[0,1,2,3,0,1,2,3]]。
    sin = sin.unsqueeze(1).repeat(1, 1, 2).unsqueeze(0).float()
    output = (tensor * cos) + (rotate_half(tensor) * sin)  # 位置编码是q*e^iθ
    output = output.to(orig_dtype)
    return output

## 2. ViT的block中,q、k被加入位置信息
class VisionAttention(nn.Module):
    def __init__(self, dim: int, num_heads: int = 16) -> None:
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = dim // num_heads
        self.qkv = nn.Linear(dim, dim * 3, bias=True)
        self.proj = nn.Linear(dim, dim)

    def forward(
        self, hidden_states: torch.Tensor, cu_seqlens: torch.Tensor, rotary_pos_emb: torch.Tensor = None
    ) -> torch.Tensor:
        seq_length = hidden_states.shape[0]
        q, k, v = self.qkv(hidden_states).reshape(seq_length, 3, self.num_heads, -1).permute(1, 0, 2, 3).unbind(0)
        q = apply_rotary_pos_emb_vision(q.unsqueeze(0), rotary_pos_emb).squeeze(0)
        k = apply_rotary_pos_emb_vision(k.unsqueeze(0), rotary_pos_emb).squeeze(0)

        attention_mask = torch.full(
            [1, seq_length, seq_length], torch.finfo(q.dtype).min, device=q.device, dtype=q.dtype
        )
        for i in range(1, len(cu_seqlens)):
            attention_mask[..., cu_seqlens[i - 1] : cu_seqlens[i], cu_seqlens[i - 1] : cu_seqlens[i]] = 0

        q = q.transpose(0, 1)
        k = k.transpose(0, 1)
        v = v.transpose(0, 1)
        attn_weights = torch.matmul(q, k.transpose(1, 2)) / math.sqrt(self.head_dim)
        attn_weights = attn_weights + attention_mask
        attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(q.dtype)
        attn_output = torch.matmul(attn_weights, v)
        attn_output = attn_output.transpose(0, 1)
        attn_output = attn_output.reshape(seq_length, -1)
        attn_output = self.proj(attn_output)
        return attn_output

  position_id的代码在qwen2-vl的model里面定义,可以看到它也有置换函数,如果不置换,可以打印出来,以及取pos_id之后打印一下看看,是很规整的 ( 0 , 0 ) , ( 0 , 1 ) , . . . . ( 0 , w − 1 ) , ( 1 , 0 ) , . . . , ( h − 1 , w − 1 ) (0,0),(0,1),....(0,w-1),(1,0),...,(h-1,w-1) (0,0),(0,1),....(0,w1),(1,0),...,(h1,w1)的形式,并且是相乘的形式

class Qwen2VisionTransformerPretrainedModel(Qwen2VLPreTrainedModel):
    def __init__(self, config) -> None:
        self.spatial_merge_size = config.spatial_merge_size
        head_dim = config.embed_dim // config.num_heads
        self.rotary_pos_emb = VisionRotaryEmbedding(head_dim // 2)

	# grid_thw是一个[[t,h,w]]的形式,如果就一张图这里t=1
    def rot_pos_emb(self, grid_thw):
        pos_ids = []
        for t, h, w in grid_thw:
            hpos_ids = torch.arange(h).unsqueeze(1).expand(-1, w)
            hpos_ids = hpos_ids.reshape(
                h // self.spatial_merge_size,
                self.spatial_merge_size,
                w // self.spatial_merge_size,
                self.spatial_merge_size,
            )
            hpos_ids = hpos_ids.permute(0, 2, 1, 3)  # 可以打印一下不置换的结果
            hpos_ids = hpos_ids.flatten()

            wpos_ids = torch.arange(w).unsqueeze(0).expand(h, -1)
            wpos_ids = wpos_ids.reshape(
                h // self.spatial_merge_size,
                self.spatial_merge_size,
                w // self.spatial_merge_size,
                self.spatial_merge_size,
            )
            wpos_ids = wpos_ids.permute(0, 2, 1, 3)
            wpos_ids = wpos_ids.flatten()
            pos_ids.append(torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1)) # x,y,重复t份
        pos_ids = torch.cat(pos_ids, dim=0)
        max_grid_size = grid_thw[:, 1:].max()
        rotary_pos_emb_full = self.rotary_pos_emb(max_grid_size) 
        rotary_pos_emb = rotary_pos_emb_full[pos_ids].flatten(1) # 根据patch的形状来取position_id的(x,y)
        return rotary_pos_emb

    def forward(self, hidden_states: torch.Tensor, grid_thw: torch.Tensor) -> torch.Tensor:
        hidden_states = self.patch_embed(hidden_states)
        rotary_pos_emb = self.rot_pos_emb(grid_thw)

        for blk in self.blocks:
            hidden_states = blk(hidden_states, cu_seqlens=cu_seqlens, rotary_pos_emb=rotary_pos_emb)

4. qwen2-vl提出的M-RoPE

在这里插入图片描述
  首先明确,M-RoPE是3D-RoPE,乘法过程等是3D-RoPE的方式,自定义的部分在于position_id的计算方式上。可以回顾2D-RoPE里面苏神在多模态上讨论的不同实现方式,到底怎么排文本和图片。qwen2-vl的论文中给出了一个编排position_id的图,可以看到图片是按顺序排的,多了一个时间维度的坐标,position_id是3维的 ( t , h , w ) (t,h,w) (t,h,w)。然后对于图片后面接的文本起始编码,取图片的最后一个patch的position_id的各个维度的最大值+1。
  代码上,需要关注2个地方,一个是这个position_id如何算的,这个在get_rope_index函数中定义,函数比较长,可以看它的注释,实现的就是上图的逻辑,计算出每个位置的position_id。

Each embedding sequence contains vision embedding and text embedding or just contains text embedding.
For pure text embedding sequence, the rotary position embedding has no difference with mordern LLMs.

Examples:
	input_ids: [T T T T T], here T is for text.
	temporal position_ids: [0, 1, 2, 3, 4]
	height position_ids: [0, 1, 2, 3, 4]
	width position_ids: [0, 1, 2, 3, 4]

	For vision and text embedding sequence, we calculate 3D rotary position embedding for vision part
	and 1D rotary position embeddin for text part.

Examples:
	Assume we have a video input with 3 temporal patches, 2 height patches and 2 width patches.
	input_ids: [V V V V V V V V V V V V T T T T T], here V is for vision.
	vision temporal position_ids: [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]
	vision height position_ids: [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1]
	vision width position_ids: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
	text temporal position_ids: [3, 4, 5, 6, 7]
	text height position_ids: [3, 4, 5, 6, 7]
	text width position_ids: [3, 4, 5, 6, 7]
	Here we calculate the text start position_ids as the max vision position_ids plus 1.

  最后M-RoPE的函数里面完成嵌入3D RoPE编码, q q q k k k分别与角度相乘。不过这里面好像有一个mrope_section,看config里面好像涉及rope_scaling的内容,这个学不动了后面学学emmm

query_states, key_states = apply_multimodal_rotary_pos_emb(
	query_states, key_states, cos, sin, self.rope_scaling["mrope_section"]
)


def apply_multimodal_rotary_pos_emb(q, k, cos, sin, mrope_section, unsqueeze_dim=1):
    mrope_section = mrope_section * 2
    cos = torch.cat([m[i % 3] for i, m in enumerate(cos.split(mrope_section, dim=-1))], dim=-1).unsqueeze(
        unsqueeze_dim
    )
    sin = torch.cat([m[i % 3] for i, m in enumerate(sin.split(mrope_section, dim=-1))], dim=-1).unsqueeze(
        unsqueeze_dim
    )

    q_embed = (q * cos) + (rotate_half(q) * sin)
    k_embed = (k * cos) + (rotate_half(k) * sin)
    return q_embed, k_embed

  借助M-RoPE,论文里面提到qwen2-vl的外推能力挺好。
在这里插入图片描述

5. qwen2.5-vl的位置编码

  最近测试下来qwen2.5-vl的效果没有qwen2-vl好,可能因为里面用了窗口注意力(qwen-vl里面提到过,说这个效果不如global attention),qwen2.5能做的任务比qwen2要好。如果要使用qwen2.5,记得transformer版本安装方式为

pip install git+https://github.com/huggingface/transformers.git@9985d06add07a4cc691dc54a7e34f54205c04d40

  看上去qwen2.5-vl的位置编码,和qwen2-vl的主要区别是position_id这里面,time_id这一维度的计算方式,qwen2.5-vl里面不同的采样率,会对应不同的time_id,和绝对的时间进行对齐。
在这里插入图片描述

6.很好的参考资料

1.苏剑林老师的Transformer升级系列,在他的网站“归档”里面进行搜索,可以一章一章的看,例如:

  • Transformer升级之路:2、博采众长的旋转式位置编码
  • Transformer升级之路:4、二维位置的旋转式位置编码
  • “闭门造车”之多模态思路浅谈(三):位置编码
  • Transformer升级之路:17、多模态位置编码的简单思考

2.eleuther的博客:https://blog.eleuther.ai/rotary-embeddings/

7.TODO

  • 天池比赛最近出新的LLM比赛了
  • 强化学习很多教程云里雾里的,发现磨菇书非常不错,代码还没看:https://datawhalechina.github.io/easy-rl/#/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2309106.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vector 面试点总结

ps&#xff1a;部分内容使用“AI”查询 一、入门 1、什么是vector 动态数组容器&#xff0c;支持自动扩容、随机访问和连续内存存储。 2、怎么创建-初始化vector std::vector<int> v; // 创建空vectorstd::vector<int> v {1, 2, 3}; // 直接初始化std::vec…

正式页面开发-登录注册页面

整体路由设计&#xff1a; 登录和注册的切换是切换组件或者是切换内容&#xff08;v-if和 v-else)&#xff0c;因为点击两个之间路径是没有变化的。也就是登录和注册共用同一个路由。登录是独立的一级路由。登录之后进到首页&#xff0c;有三个大模块&#xff1a;文章分类&…

Spring项目-抽奖系统(实操项目-用户管理接口)(END)

^__^ (oo)\______ (__)\ )\/\ ||----w | || || 一&#xff1a;前言&#xff1a; 活动创建及展示博客链接&#xff1a;Spring项目-抽奖系统(实操项目-用户管理接口)(THREE)-CSDN博客 上一次完成了活动的创建和活动的展示&#xff0c;接下来就是重头戏—…

Kafka面试题及原理

1. 消息可靠性&#xff08;不丢失&#xff09; 使用Kafka在消息的收发过程都会出现消息丢失&#xff0c;Kafka分别给出了解决方案 生产者发送消息到Brocker丢失消息在Brocker中存储丢失消费者从Brocker 幂等方案&#xff1a;【分布式锁、数据库锁&#xff08;悲观锁、乐观锁…

CSS—text文本、font字体、列表list、表格table、表单input、下拉菜单select

目录 1.文本 2.字体 3.列表list a.无序列表 b.有序列表 c.定义列表 4.表格table a.内容 b.合并单元格 3.表单input a.input标签 b.单选框 c.上传文件 4.下拉菜单 1.文本 属性描述color设置文本颜色。direction指定文本的方向 / 书写方向。letter-spacing设置字符…

水果识别系统 | BP神经网络水果识别系统,含GUI界面(Matlab)

使用说明 代码下载&#xff1a;BP神经网络水果识别系统&#xff0c;含GUI界面&#xff08;Matlab&#xff09; BP神经网络水果识别系统 一、引言 1.1、研究背景及意义 在当今科技迅速发展的背景下&#xff0c;人工智能技术尤其是在图像识别领域的应用日益广泛。水果识别作为…

40岁开始学Java:Java中单例模式(Singleton Pattern),适用场景有哪些?

在Java中&#xff0c;单例模式&#xff08;Singleton Pattern&#xff09;用于确保一个类只有一个实例&#xff0c;并提供全局访问点。以下是详细的实现方式、适用场景及注意事项&#xff1a; 一、单例模式的实现方式 1. 饿汉式&#xff08;Eager Initialization&#xff09; …

李宏毅机器学习课程学习笔记04 | 浅谈机器学习-宝可梦、数码宝贝分类器

文章目录 案例&#xff1a;宝可梦、数码宝贝分类器第一步&#xff1a;需要定义一个含有未知数的function第二步&#xff1a;loss of a function如何Sample Training Examples > 如何抽样可以得到一个较好的结果如何权衡模型的复杂程度 Tradeoff of Model Complexity todo 这…

Redis详解(实战 + 面试)

目录 Redis 是单线程的&#xff01;为什么 Redis-Key(操作redis的key命令) String 扩展字符串操作命令 数字增长命令 字符串范围range命令 设置过期时间命令 批量设置值 string设置对象,但最好使用hash来存储对象 组合命令getset,先get然后在set Hash hash命令: h…

ISP CIE-XYZ色彩空间

1. 颜色匹配实验 1931年&#xff0c;CIE综合了前人实验数据&#xff0c;统一采用700nm&#xff08;红&#xff09;、546.1nm&#xff08;绿&#xff09;、435.8nm&#xff08;蓝&#xff09;​作为标准三原色波长&#xff0c;绘制了色彩匹配函数&#xff0c;如下图。选定这些波…

【强化学习笔记1】从强化学习的基本概念到近端策略优化(PPO)

好久没有更新了。最近想学习一下强化学习&#xff0c;本系列是李宏毅老师强化学习的课程笔记。 1. Policy-based Model 1.1 Actor 在policy-based model中&#xff0c;主要的目的就是训练一个actor。 对于一个episode&#xff08;例如&#xff0c;玩一局游戏&#xff09;&…

STM32中的ADC

目录 一&#xff1a;什么是ADC 二&#xff1a;ADC的用途 三&#xff1a;STM32F103ZET6的ADC 3.1ADC对应的引脚 3.2ADC时钟 3.3ADC的工作模式 ​编辑3.4ADC校准 3.5ADC转换结构和实际电压的换算 四&#xff1a;ADC配置步骤 五&#xff1a;两个重要的函数 一&#xff1a…

开启AI短剧新纪元!SkyReels-V1/A1双剑合璧!昆仑万维开源首个面向AI短剧的视频生成模型

论文链接&#xff1a;https://arxiv.org/abs/2502.10841 项目链接&#xff1a;https://skyworkai.github.io/skyreels-a1.github.io/ Demo链接&#xff1a;https://www.skyreels.ai/ 开源地址&#xff1a;https://github.com/SkyworkAI/SkyReels-A1 https://github.com/Skywork…

【uniapp】在UniApp中实现持久化存储:安卓--生成写入数据为jsontxt

在移动应用开发中&#xff0c;数据存储是一个至关重要的环节。对于使用UniApp开发的Android应用来说&#xff0c;缓存&#xff08;Cache&#xff09;是一种常见的数据存储方式&#xff0c;它能够提高应用的性能和用户体验。然而&#xff0c;缓存数据在用户清除缓存或清除应用数…

使用IDEA如何隐藏文件或文件夹

选择file -> settings 选择Editor -> File Types ->Ignored Files and Folders (忽略文件和目录) 点击号就可以指定想要隐藏的文件或文件夹

git从零学起

从事了多年java开发&#xff0c;一直在用svn进行版本控制&#xff0c;如今更换了公司&#xff0c;使用的是git进行版本控制&#xff0c;所以打算记录一下git学习的点滴&#xff0c;和大家一起分享。 百度百科&#xff1a; Git&#xff08;读音为/gɪt/&#xff09;是一个开源…

汽车低频发射天线介绍

汽车低频PKE天线是基于RFID技术的深度研究及产品开发应用的一种天线&#xff0c;在汽车的智能系统中发挥着重要作用&#xff0c;以下是关于它的详细介绍&#xff1a; 移动管家PKE低频天线结构与原理 结构&#xff1a;产品一般由一个高Q值磁棒天线和一个高压电容组成&#xff…

【Java分布式】Nacos注册中心

Nacos注册中心 SpringCloudAlibaba 也推出了一个名为 Nacos 的注册中心&#xff0c;相比 Eureka 功能更加丰富&#xff0c;在国内受欢迎程度较高。 官网&#xff1a;https://nacos.io/zh-cn/ 集群 Nacos就将同一机房内的实例划分为一个集群&#xff0c;一个服务可以包含多个集…

5G学习笔记之BWP

我们只会经历一种人生&#xff0c;我们选择的人生。 参考&#xff1a;《5G NR标准》、《5G无线系统指南:如微见著&#xff0c;赋能数字化时代》 目录 1. 概述2. BWP频域位置3. 初始与专用BWP4. 默认BWP5. 切换BWP 1. 概述 在LTE的设计中&#xff0c;默认所有终端均能处理最大2…

(十一)基于vue3+mapbox-GL实现模拟高德实时导航轨迹播放

要在 Vue 3 项目中结合 Mapbox GL 实现类似高德地图的实时导航轨迹功能,您可以按照以下步骤进行: 安装依赖: 首先,安装 mapbox-gl 和 @turf/turf 这两个必要的库: npm install mapbox-gl @turf/turf引入 Mapbox GL: 在组件中引入 mapbox-gl 并初始化地图实例: <templ…