TT Lab
开始
学习 学习路径 课程

Transformer — 手算一遍注意力

用旋转把位置放进去

在 TT Lab 中继续学习

目标

只用标准库亲手实现旋转位置编码(RoPE)。把偶数维度每两个分成一对,每一对以不同的速度按位置旋转,并用数字确认这样旋转后的 query 与 key 的点积只取决于两个位置的差。并排做出相加的方式(绝对位置编码),比较在同样间隔下值的抖动,并测量旋转不改变长度,以及距离变远时哪些成分会留下。

为什么重要

加上位置向量,把点积展开后,会留下像 q·P[n] 和 P[m]·k 这样只含一侧位置的项。这些项无法归结为差,所以即使间隔完全相同,在句子前部测得的值和在后部测得的值也不同。语言中重要的多半是前面隔几格的词,而分数里却混进了绝对位置。 旋转从运算本身解决了这个问题。把两个向量朝同一方向一起转,夹角不变,所以各自按自己的位置旋转后再做点积,值只取决于差。这不是近似,而是等式,所以可以用数字确认。 本实验不调用模型。这个 Pod 的系统 Python 里没有 numpy,也没有互联网。只用 math 来做,只使用在这里测出来的数字。所以“某个模型用什么底数”这样的话,这里不说。 评分器不会相信你写下的说明。它会真正导入你的模块,每次用不同的维度和位置检验函数,并与评分器另外计算的值在容许误差之内对照。输入每次运行都会变,所以无法把值背下来填进去。

步骤

  1. 在 /root/work/tf-rope/rope.py 中创建 DIM = 64、THETA_BASE = 10000.0、demo_vectors()、dot(a, b)、thetas(d, base=THETA_BASE)。thetas 返回每一对不同的旋转速度。
  2. 增加 rotate_pair(x0, x1, angle),把平面上的一个点逆时针旋转 angle 弧度。
  3. 增加 apply_rope(vec, pos, base=THETA_BASE),把向量分成对,每一对旋转 pos * theta_i,返回新向量。
  4. 创建 rope_score(q, k, m, n, base=THETA_BASE),把 m 号位置的 query 和 n 号位置的 key 各自旋转后再做点积。
  5. 创建 sin_pos(pos, d, base=THETA_BASE) 和 add_score(q, k, m, n, base=THETA_BASE),测量相加的方式下的同一个分数。
  6. 创建 offset_scan(q, k, offset, starts),在多个起始位置上测量同样的间隔。返回的值是 (시작자리, 회전 점수, 더하는 점수)(占位符依次为起始位置、旋转分数、相加分数)对的列表。
  7. 创建 turns(d, delta, base=THETA_BASE) 和 slow_pairs(d, delta, base=THETA_BASE),测量每个间隔下各对转了几圈,以及还没有超过一圈的对有几个。
  8. 把测得的值记录到 /root/work/tf-rope/rope_report.json 和 /root/work/tf-rope/rope_report.md 中。

参考

每一对不同的旋转速度

在 /root/work/tf-rope/rope.py 中创建 DIM = 64、THETA_BASE = 10000.0、demo_vectors()、dot(a, b)、thetas(d, base=THETA_BASE)。thetas 是长度为 d // 2 的列表,第 i 个值是 base ** (-2 * i / d)。demo_vectors() 返回长度为 DIM 的两个列表,q[j] = math.cos(0.7 * j + 0.3),k[j] = math.sin(0.4 * j + 1.1)。

不是每个维度,而是每一对一个——64 维就是 32 个。i 为 0 时指数为 0,所以第一个值是 1.0,越往后按几何级数变得越小。如果用整数做除法,指数会全部变成 0,值全都成了 1.0,所以要像 -2.0 * i / d 这样用实数。dot 用 zip 配对相乘再相加,一行就够了。

在平面上旋转一次

增加 rotate_pair(x0, x1, angle)。返回把点 (x0, x1) 按 angle 弧度逆时针旋转后的两个元素的元组。rotate_pair(1.0, 0.0, math.pi / 2) 是 (0.0, 1.0)。

是 (x0*cos - x1*sin, x0*sin + x1*cos)。两个符号中只要反一个,就成了顺时针,后面步骤的值会全部不同。角度为 0 时必须与原来的点完全相同,不管给什么角度,到原点的距离都不能变——因为 cos 和 sin 的平方之和是 1。

把向量按位置旋转

增加 apply_rope(vec, pos, base=THETA_BASE)。像 (vec[0], vec[1])、(vec[2], vec[3]) 这样把相邻的配成对,把第 i 对旋转 pos * thetas(len(vec), base)[i],返回新列表。传入的列表保持原样。

每一对的角度不同——如果所有对都用第 0 对的角度,就只是整体转了一下,位置信息只放进了一层。pos 为 0 时所有角度都是 0,必须与原向量相同,不管给哪个位置,向量的范数都不能变。别忘了接收 base 并传给 thetas——只用默认值的话,用别的底数调用时会悄悄出错。

旋转后的 query 与 key 的分数

创建 rope_score(q, k, m, n, base=THETA_BASE)。把 m 号位置的 query 和 n 号位置的 key 各自旋转后再做点积,返回得到的实数。不除以 √d。

dot(apply_rope(q, m, base), apply_rope(k, n, base)) 一行。只旋转一侧的话,绝对位置会原样留下,后面步骤的性质就崩了。m 和 n 相同时,必须得到与没有旋转的 dot(q, k) 相同的值——因为是朝同一方向一起旋转,夹角不变。

并排放上相加的方式

创建 sin_pos(pos, d, base=THETA_BASE) 和 add_score(q, k, m, n, base=THETA_BASE)。sin_pos 是长度为 d 的位置向量,把 angle = pos / (base ** ((2 * (j // 2)) / d)) 设好后,第 j 个值在 j 为偶数时是 math.sin(angle),为奇数时是 math.cos(angle)。add_score 把位置向量分别加到 q 和 k 上之后再做点积。

这边是生成值再加上去的方式。只加在一侧的话,比较就不成立。把点积展开,除了 q·k,还会出现 q·P[n] 和 P[m]·k,这两项只含一侧位置,无法归结为差——下一步会用数字看到它的结果。

间隔相同,分数就相同

创建 offset_scan(q, k, offset, starts)。对 starts 中的每个位置 s,把 query 放在 s + offset,把 key 放在 s,用两种方式测量分数。返回的值是 (s, 회전 점수, 더하는 점수)(占位符依次为旋转分数与相加分数)对的列表,顺序与 starts 相同。做完之后请亲自打印 offset_scan(q, k, 2, [3, 10, 100, 4000]),亲眼确认旋转一侧的四个值相同,而相加一侧在抖动。

直接调用前面做的 rope_score 和 add_score,五行就够了。把 query 和 key 放反,间隔的符号就会反过来,得到不同的值。旋转一侧的四个值,应该到小数点后十二位都相同才算正常——并不是完全相同,会留下极小的差别,那就是浮点误差。请与相加一侧的分散程度在数量级上比较一下。

离得远了,哪些对会留下

创建 turns(d, delta, base=THETA_BASE) 和 slow_pairs(d, delta, base=THETA_BASE)。turns 是长度为 d // 2 的列表,第 i 个值是 delta * theta_i / (2 * math.pi),也就是那一对转的圈数。slow_pairs 是该值小于 1.0 的对的个数(整数)。

一圈是 2π 弧度。忘了除的话,得到的不是圈数而是角度,个数会完全不同。超过一圈的对,会把那个距离和减去一圈的距离写成同样的角度,所以无法区分二者。用间隔 1、16、256、4096 调用,看看剩下的对的数量是怎样减少的。

把测得的内容留成记录

用 demo_vectors() 的 q·k 测量,在 /root/work/tf-rope/rope_report.json 中写入 dim、base、offset、starts、rope_scores、rope_spread、add_scores、add_spread、norm_before、norm_max_gap、deltas、slow_pairs、turns_first、turns_last,在 /root/work/tf-rope/rope_report.md 中用 ## 무엇을 쟀나(韩文,意为“测量了什么”)、## 같은 간격이면 같은 점수다(韩文,意为“间隔相同,分数就相同”)、## 더하는 방식은 왜 다른가(韩文,意为“相加的方式为什么不同”)、## 멀어지면 어느 성분이 남는가(韩文,意为“离得远了,哪些成分会留下”)四节来写。间隔是 2,起始位置是 3、10、100、4000,测量范数的位置是 0、1、7、100、4096,数圈数的间隔是 1、16、256、4096。

数字不要手写,要用实际运行你的代码得到的值来填。rope_scores、add_scores 从 offset_scan 返回的对中取出。rope_spread、add_spread 分别是最大值减最小值,norm_max_gap 是在五个位置测得的差的绝对值中最大的一个。turns_first 是每个间隔下第 0 对的圈数,turns_last 是最后一对的圈数。报告正文中要用数字写出旋转一侧的第一个分数、相加一侧的分散程度、q 的范数——评分器会检查正文中有没有这三个值。