Перейти к содержимому
Ступень 11.0 Школьная математика — освежаем

Тригонометрия для ML: углы, синус, косинус, вращения

С нуля: радианы, единичная окружность, синус и косинус как координаты точки, волны разной частоты и поворот вектора матрицей 2×2 — ровно та тригонометрия, на которой стоят позиционные кодировки, RoPE и YaRN.

УРОКstage-1/1-0-school-math/04-trigonometry.mdx1 ч 40 минУрок 4 из 5
После урока вы сможете
  • Переводить градусы в радианы и находить синус и косинус любого угла как координаты точки на единичной окружности
  • Читать запись sin⁡(θ⋅pos)\sin(\theta \cdot pos) как волну с частотой θ\theta и периодом 2π/θ2\pi/\theta и объяснить, зачем позиционным кодировкам волны разной частоты
  • Повернуть вектор на плоскости матрицей поворота 2×22 \times 2 и проверить, что длина при этом не меняется
  • Вывести из формулы косинуса разности, почему скалярное произведение двух повёрнутых векторов зависит только от разности углов — главную идею RoPE

Трансформер видит текст как набор векторов, и сам по себе не знает, какое слово стоит первым, а какое десятым. Номер позиции ему подсказывают с помощью синусов и косинусов. Так делали в статье «Attention Is All You Need» (2017, синусоидальные кодировки) и так делают сегодня в Llama, Qwen и DeepSeek: там позицию кодируют поворотом векторов (RoPE, урок 4.5.2). Когда контекст модели растягивают с 4 тысяч токенов до 128 тысяч, правят частоты этих поворотов (YaRN и NTK, урок 5.5.1). А распознавание речи начинается с того, что звук раскладывают на волны разной частоты (спектр, урок 9.1.5).

Всё это держится на трёх школьных идеях: точка на окружности, волна и поворот. Если вы забыли, что такое синус, — ничего страшного: начнём с нуля, и каждую формулу проверим в Python.

flowchart TD
A["Угол<br/>(радианы)"] --> B["Точка на окружности<br/>(cos, sin)"]
B --> C["Волна<br/>sin(θ·pos)"]
B --> D["Поворот вектора<br/>матрица 2×2"]
C --> E["Синусоидальные<br/>кодировки"]
D --> F["RoPE"]
C --> G["YaRN, NTK:<br/>правка частот"]
F --> G
Карта урока: от угла до позиционных кодировок

Угол показывает, насколько повернулось направление. В быту его меряют в градусах: полный оборот — 360°, прямой угол — 90°. В математике и во всех библиотеках (math, NumPy, PyTorch) углы меряют в радианах.

Представьте окружность радиуса 1 с центром в начале координат. Встаньте в точку (1,0)(1, 0) и идите по окружности против часовой стрелки. РадианрадианEN radianУгол, при котором длина дуги равна радиусу. Полный оборот — 2π ≈ 6.283 радиана (360°), 1 радиан ≈ 57.3°. Все библиотеки (math, NumPy, PyTorch) ждут углы в радианах. — это угол, на который вы повернётесь, пройдя по дуге путь, равный радиусу, то есть 1. Угол в радианах — это просто длина пройденной дуги на окружности радиуса 1.

Длина всей окружности равна 2πr2\pi r, при r=1r = 1 это 2π≈6.2832\pi \approx 6.283. Значит, полный оборот — это 2π2\pi радиан, а 360° и 2π2\pi — одно и то же. Отсюда формула перевода:

радианы=градусы⋅π180\text{радианы} = \text{градусы} \cdot \frac{\pi}{180}

Читаем: «чтобы получить радианы, умножьте градусы на π\pi и поделите на 180». Числа: 90°=π/2≈1.57190° = \pi/2 \approx 1.571, 30°=π/6≈0.52430° = \pi/6 \approx 0.524, а 1 радиан =180°/π≈57.3°= 180°/\pi \approx 57.3° — чуть меньше шестой части оборота.

import math
print(math.radians(90)) # 1.5707963267948966 — это π/2
print(math.radians(180)) # 3.141592653589793 — это π
print(math.degrees(1)) # 57.29577951308232 — столько градусов в одном радиане
print(math.sin(90)) # 0.8939966636005579 — ЛОВУШКА: это синус 90 радиан, а не 90°

Последняя строка — классическая ошибка новичка: math.sin всегда считает, что угол в радианах. Синус 90° равен 1, и правильно писать math.sin(math.radians(90)).

Почему радианы удобнее градусов? В позиционных кодировках угол получается умножением: «позиция × частота». Токен на позиции 5 при частоте 0.1 повёрнут на 5⋅0.1=0.55 \cdot 0.1 = 0.5 радиана — никаких множителей 360 и 180.

Единичная окружностьединичная окружностьEN unit circleОкружность радиуса 1 с центром в (0, 0). Точка под углом α на ней имеет координаты (cos α, sin α). — окружность радиуса 1 с центром в точке (0,0)(0, 0). Отложите от точки (1,0)(1, 0) угол α\alpha («альфа») против часовой стрелки — вы окажетесь в точке PP. Её координаты по определению называют косинусом и синусом угла:

P=(cos⁡α, sin⁡α)P = (\cos\alpha,\ \sin\alpha)

КосинускосинусEN cosinecos α — координата x точки на единичной окружности под углом α. Для единичных векторов скалярное произведение равно косинусу угла между ними. — это «насколько точка ушла вправо» (координата xx), синуссинусEN sinesin α — координата y точки на единичной окружности под углом α. Значения от −1 до 1, период 2π. — «насколько ушла вверх» (координата yy). Оба числа всегда лежат между −1-1 и 11: точка не может выйти за окружность.

αP = (cos α, sin α)cos αsin α11−11−1xyУгол α = 50° ≈ 0.873 радcos 50° ≈ 0.643 — тень на ось xsin 50° ≈ 0.766 — тень на ось yКатеты cos α и sin α,гипотенуза 1, по Пифагору:cos²α + sin²α = 1Длина дуги от (1, 0) до Pравна углу в радианах: 0.873
Определение синуса и косинуса: пройдите по окружности радиуса 1 от точки (1, 0) против часовой стрелки на угол α — координаты точки, где вы остановились, и есть (cos α, sin α).

Если вы помните школьное определение «синус — противолежащий катет к гипотенузе», оно то же самое: на рисунке видно прямоугольный треугольник с гипотенузой 1, и его катеты равны cos⁡α\cos\alpha и sin⁡α\sin\alpha. Определение через окружность лучше тем, что работает для любого угла: тупого, больше оборота, отрицательного (идём по часовой стрелке).

По теореме Пифагора (квадрат гипотенузы равен сумме квадратов катетов) сразу получаем главное тождество:

cos⁡2α+sin⁡2α=1\cos^2\alpha + \sin^2\alpha = 1

Запись cos⁡2α\cos^2\alpha — это (cos⁡α)2(\cos\alpha)^2. Проверка для 50°: 0.6432+0.7662=0.413+0.587=10.643^2 + 0.766^2 = 0.413 + 0.587 = 1.

Значения, которые стоит помнить. Достаточно представить, где окажется точка на окружности:

Угол0°30°45°60°90°180°270°360°
Радианы00.5240.7851.0471.5713.1424.7126.283
cos⁡\cos10.8660.7070.50−101
sin⁡\sin00.50.7070.86610−10

При 90° точка в самом верху (0,1)(0, 1), при 180° — слева (−1,0)(-1, 0), при 270° — внизу (0,−1)(0, -1), при 360° вернулась в начало.

import math
for deg in [0, 30, 90, 180]:
a = math.radians(deg) # сначала переводим в радианы!
print(deg, round(math.cos(a), 3), round(math.sin(a), 3))
# 0 1.0 0.0
# 30 0.866 0.5
# 90 0.0 1.0
# 180 -1.0 0.0
print(math.cos(math.pi / 2)) # 6.123233995736766e-17, а не 0
a = math.radians(50)
print(math.cos(a) ** 2 + math.sin(a) ** 2) # 1.0 — тождество выполняется

Число 6.12e-17 вместо нуля — обычное округление float (урок 0.0.2): π\pi хранится неточно, и косинус «почти π/2\pi/2» оказывается «почти нулём». Поэтому числа с плавающей точкой сравнивают через math.isclose или np.allclose, а не через ==.

Любой вектор — это длина и угол. Точка (3,4)(3, 4) лежит на расстоянии 5 от начала координат. Поделив на 5, получим точку (0.6,0.8)(0.6, 0.8) на единичной окружности, то есть (cos⁡α,sin⁡α)(\cos\alpha, \sin\alpha) для угла α≈53.1°\alpha \approx 53.1°. Значит, (3,4)=5⋅(cos⁡53.1°,sin⁡53.1°)(3, 4) = 5 \cdot (\cos 53.1°, \sin 53.1°). Такая запись «длина × направление» пригодится через раздел, когда будем поворачивать векторы.

Пусть угол растёт равномерно: на каждом шаге точка проходит по окружности одно и то же расстояние θ\theta («тета»). На шаге pospos угол равен θ⋅pos\theta \cdot pos. Если записывать только высоту точки, получится последовательность sin⁡(θ⋅0),sin⁡(θ⋅1),sin⁡(θ⋅2),…\sin(\theta \cdot 0), \sin(\theta \cdot 1), \sin(\theta \cdot 2), \ldots — волна.

Через полный оборот 2π2\pi точка возвращается туда же, поэтому синус и косинус повторяются: sin⁡(α+2π)=sin⁡α\sin(\alpha + 2\pi) = \sin\alpha. Такие функции называют периодическимипериодическая функцияEN periodic functionФункция, значения которой повторяются через период T: f(x + T) = f(x). У sin(θ·x) период 2π/θ.. Три слова описывают любую волну:

  • Частота θ\theta — на сколько радиан поворачивается точка за один шаг. В ML это обычно «радиан на токен».
  • Период (или длина волны) — через сколько шагов волна повторяется: T=2π/θT = 2\pi / \theta. При θ=1\theta = 1 это 6.286.28 позиции, при θ=0.1\theta = 0.1 — 62.862.8, при θ=0.01\theta = 0.01 — 628628.
  • Фаза — сдвиг волны: sin⁡(θ⋅pos+φ)\sin(\theta \cdot pos + \varphi) начинается не с нуля, а «забегает вперёд» на угол φ\varphi («фи»). Косинус — это синус со сдвигом на четверть оборота: cos⁡α=sin⁡(α+π/2)\cos\alpha = \sin(\alpha + \pi/2).

В физике частоту называют угловойугловая частотаEN angular frequencyНа сколько радиан поворачивается точка за единицу времени или за один шаг (токен). Связь с частотой f в герцах: ω = 2πf; период T = 2π/ω., чтобы не путать с частотой в герцах (колебаниях в секунду). Для звука с частотой ff герц угол растёт на 2πf2\pi f радиан в секунду.

θ = 1: период 2π/1 ≈ 6.3 позицииθ = 0.3: период ≈ 20.9 позицииθ = 0.1: период ≈ 62.8 — за 40 позиций меньше оборота010203040позиция pos →
Значения sin(θ·pos) в целых позициях 0…40 (точки). Быстрая волна различает соседние позиции, медленная почти не меняется на коротком отрезке, зато различает далёкие. Так устроены «стрелки часов» в позиционных кодировках.
import numpy as np
pos = np.arange(8) # позиции 0, 1, …, 7
for theta in [1.0, 0.1, 0.01]:
wave = np.sin(theta * pos) # волна сразу для всех позиций
print(theta, round(2 * np.pi / theta, 1), np.round(wave, 2))
# 1.0 6.3 [ 0. 0.84 0.91 0.14 -0.76 -0.96 -0.28 0.66]
# 0.1 62.8 [0. 0.1 0.2 0.3 0.39 0.48 0.56 0.64]
# 0.01 628.3 [0. 0.01 0.02 0.03 0.04 0.05 0.06 0.07]

Посмотрите на две последние строки: у медленных волн значения почти равны θ⋅pos\theta \cdot pos. Это приближение малого угла: при малых xx (в радианах) sin⁡x≈x\sin x \approx x и cos⁡x≈1−x2/2\cos x \approx 1 - x^2/2. Числа: sin⁡0.1=0.0998\sin 0.1 = 0.0998, cos⁡0.1=0.99500\cos 0.1 = 0.99500 против 1−0.005=0.9951 - 0.005 = 0.995. В уроке 5.5.1 это приближение объяснит, почему «сжатие» частот в 4 раза делает соседние токены неразличимыми в 16 раз сильнее.

Одна волна плохо кодирует позицию: быстрая повторяется каждые 6 токенов, медленная почти не отличает соседей. Решение — взять много волн разной частоты, как стрелки часов: секундная различает соседние мгновения, часовая — далёкие. Так устроены синусоидальные позиционные кодировкисинусоидальное позиционное кодированиеEN sinusoidal positional encodingИз статьи 2017 года: PE(pos, 2i) = sin(pos·θ_i), PE(pos, 2i+1) = cos(pos·θ_i), θ_i = 10000^(−2i/d). Прибавляется к эмбеддингам один раз на входе. 2017 года: вектор позиции длины dd состоит из d/2d/2 пар (sin⁡,cos⁡)(\sin, \cos) с частотами

θi=10000−2i/d,i=0,1,…,d/2−1\theta_i = 10000^{-2i/d}, \qquad i = 0, 1, \ldots, d/2 - 1

Читаем: «частота ii-й пары — это 10 000 в степени минус 2i/d2i/d». При i=0i = 0 получаем 100000=110000^0 = 1 (секундная стрелка), дальше частоты падают в геометрической прогрессии до почти нуля (часовая). Пример для d=4d = 4: частоты 11 и 10000−1/2=0.0110000^{-1/2} = 0.01, и позиция 2 кодируется вектором (sin⁡2,cos⁡2,sin⁡0.02,cos⁡0.02)=(0.909,−0.416,0.020,1.000)(\sin 2, \cos 2, \sin 0.02, \cos 0.02) = (0.909, -0.416, 0.020, 1.000). Собрать такие векторы для всех позиций вы сможете в задании.

Углубление: спектр звука — волны в обратную сторону

Звук — это давление воздуха, которое колеблется сотни и тысячи раз в секунду. Аккорд — сумма нескольких волн разной частоты. Спектр отвечает на обратный вопрос: из каких частот и с какой силой (амплитудой) сложен сигнал? Простейший способ узнать — «прикладывать» к сигналу чистые волны и смотреть, насколько они совпадают (среднее произведение сигнала на волну):

import numpy as np
t = np.arange(1000) / 1000 # 1 секунда, 1000 отсчётов
signal = np.sin(2 * np.pi * 3 * t) + 0.5 * np.sin(2 * np.pi * 10 * t) # 3 Гц с амплитудой 1 и 10 Гц с амплитудой 0.5
for f in [1, 2, 3, 4, 10]:
match = 2 * np.mean(signal * np.sin(2 * np.pi * f * t)) # насколько сигнал «похож» на волну f Гц
print(f, round(match, 3))
# 1 -0.0
# 2 -0.0
# 3 1.0
# 4 0.0
# 10 0.5

Совпадение нашлось ровно на 3 Гц (амплитуда 1) и 10 Гц (0.5). Быстрый алгоритм, делающий это сразу для всех частот, — преобразование Фурье (np.fft). Спектр, посчитанный по коротким кусочкам звука, — спектрограмма; с неё начинается распознавание речи (урок 9.1.5). Наглядно эта идея показана в видео 3Blue1Brown о преобразовании Фурье (ссылка в источниках).

Теперь научимся поворачивать. Возьмём вектор v=(x,y)v = (x, y) длины rr под углом α\alpha: x=rcos⁡αx = r\cos\alpha, y=rsin⁡αy = r\sin\alpha. Повернуть его на угол θ\theta — значит оставить длину rr, а угол сделать α+θ\alpha + \theta:

x′=rcos⁡(α+θ),y′=rsin⁡(α+θ)x' = r\cos(\alpha + \theta), \qquad y' = r\sin(\alpha + \theta)

Чтобы избавиться от rr и α\alpha и выразить ответ через исходные x,yx, y, нужны школьные формулы суммы углов:

cos⁡(α+θ)=cos⁡αcos⁡θ−sin⁡αsin⁡θ,sin⁡(α+θ)=sin⁡αcos⁡θ+cos⁡αsin⁡θ\cos(\alpha + \theta) = \cos\alpha\cos\theta - \sin\alpha\sin\theta, \qquad \sin(\alpha + \theta) = \sin\alpha\cos\theta + \cos\alpha\sin\theta

Их выводят геометрически, а мы просто проверим числами для α=50°\alpha = 50°, θ=30°\theta = 30°: слева cos⁡80°=0.174\cos 80° = 0.174, справа 0.643⋅0.866−0.766⋅0.5=0.557−0.383=0.1740.643 \cdot 0.866 - 0.766 \cdot 0.5 = 0.557 - 0.383 = 0.174. Сошлось. Умножим обе формулы на rr и заменим rcos⁡α=xr\cos\alpha = x, rsin⁡α=yr\sin\alpha = y:

x′=xcos⁡θ−ysin⁡θ,y′=xsin⁡θ+ycos⁡θx' = x\cos\theta - y\sin\theta, \qquad y' = x\sin\theta + y\cos\theta

Это и есть поворот. Числа: повернём (3,4)(3, 4) на 90°. Там cos⁡90°=0\cos 90° = 0, sin⁡90°=1\sin 90° = 1, поэтому x′=3⋅0−4⋅1=−4x' = 3 \cdot 0 - 4 \cdot 1 = -4, y′=3⋅1+4⋅0=3y' = 3 \cdot 1 + 4 \cdot 0 = 3. Получили (−4,3)(-4, 3): длина по-прежнему 16+9=5\sqrt{16 + 9} = 5, а стрелка смотрит на четверть оборота левее.

Четыре числа cos⁡θ,−sin⁡θ,sin⁡θ,cos⁡θ\cos\theta, -\sin\theta, \sin\theta, \cos\theta удобно записать таблицей — матрицей поворотаматрица поворотаEN rotation matrixR(α) = [[cos α, −sin α], [sin α, cos α]] поворачивает вектор на плоскости на угол α, не меняя длины. Главное свойство для RoPE: R(a)ᵀR(b) = R(b − a).:

R(θ)=(cos⁡θ−sin⁡θsin⁡θcos⁡θ),R(θ)(xy)=(xcos⁡θ−ysin⁡θxsin⁡θ+ycos⁡θ)R(\theta) = \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{pmatrix}, \qquad R(\theta)\begin{pmatrix} x \\ y \end{pmatrix} = \begin{pmatrix} x\cos\theta - y\sin\theta \\ x\sin\theta + y\cos\theta \end{pmatrix}

Правило умножения матрицы на вектор (подробно — в уроке 1.1.2): каждая строка матрицы умножается на вектор поэлементно, и результаты складываются. Первая строка (cos⁡θ,−sin⁡θ)(\cos\theta, -\sin\theta) даёт x′x', вторая — y′y'. В NumPy это оператор @ из урока 0.3.2.

import numpy as np
def R(theta):
"""Матрица поворота на угол theta (в радианах)."""
c, s = np.cos(theta), np.sin(theta)
return np.array([[c, -s],
[s, c]])
v = np.array([3.0, 4.0])
print(R(np.pi / 2) @ v) # [-4. 3.] — поворот на 90°
w = R(np.radians(30)) @ v
print(w.round(3), np.linalg.norm(w)) # [0.598 4.964] 5.0 — длина не изменилась
print(np.allclose(R(0.3) @ R(0.5), R(0.8))) # True — два поворота = один на сумму углов
print(np.allclose(R(-0.7) @ R(0.7), np.eye(2))) # True — поворот назад отменяет поворот

Три свойства из этого кода понадобятся в RoPE:

  1. Поворот не меняет длину. Вектор длины 5 остаётся длины 5.
  2. Повороты складываются: R(a) R(b)=R(a+b)R(a)\,R(b) = R(a + b). Повернуть на 0.3, потом на 0.5 — то же, что сразу на 0.8.
  3. Обратный поворот — поворот на минус тот же угол: R(−θ)R(-\theta). Его же можно получить, отразив матрицу относительно диагонали (транспонировав, урок 1.1.2): у R(−θ)R(-\theta) синусы просто меняют знак.

Косинус разности и скалярное произведение

Заголовок раздела «Косинус разности и скалярное произведение»

Последний шаг — связать углы с тем, как нейросети сравнивают векторы. В уроке 1.1.1 вы подробно разберёте скалярное произведение; на плоскости оно считается так: a⋅b=axbx+aybya \cdot b = a_x b_x + a_y b_y — произведения координат, сложенные вместе. Например, (1,2)⋅(3,4)=3+8=11(1, 2) \cdot (3, 4) = 3 + 8 = 11. Именно так внимание в трансформере измеряет, насколько запрос qq «подходит» ключу kk (урок 4.4.2).

Возьмём два единичных вектора под углами α\alpha и β\beta: a=(cos⁡α,sin⁡α)a = (\cos\alpha, \sin\alpha), b=(cos⁡β,sin⁡β)b = (\cos\beta, \sin\beta). Их скалярное произведение — это ещё одна школьная формула, косинус разности:

a⋅b=cos⁡αcos⁡β+sin⁡αsin⁡β=cos⁡(α−β)a \cdot b = \cos\alpha\cos\beta + \sin\alpha\sin\beta = \cos(\alpha - \beta)

Читаем: «скалярное произведение двух единичных векторов равно косинусу угла между ними». Числа: α=60°\alpha = 60°, β=30°\beta = 30°: 0.5⋅0.866+0.866⋅0.5=0.866=cos⁡30°0.5 \cdot 0.866 + 0.866 \cdot 0.5 = 0.866 = \cos 30°. Векторы разной длины rar_a и rbr_b дают то же, умноженное на длины: a⋅b=rarbcos⁡(α−β)a \cdot b = r_a r_b \cos(\alpha - \beta) — эту формулу вы снова встретите в уроке 1.1.1.

Главное следствие. Скалярное произведение зависит не от самих углов, а только от разности углов (и от длин). Повернём оба вектора на один и тот же угол — разность не изменится, а значит, не изменится и скалярное произведение.

До поворота: q под 20°, k под 65°45°qkОба повёрнуты на 70°: 90° и 135°45°qk
Повернём оба вектора на один угол — угол между ними (45°) останется прежним, а с ним и скалярное произведение. На этом свойстве держится RoPE.

Пусть токен на позиции mm даёт запрос qq, а токен на позиции nn — ключ kk. Повернём запрос на угол mθm\theta, а ключ — на nθn\theta, каждый «по своему номеру». Углы стали αq+mθ\alpha_q + m\theta и αk+nθ\alpha_k + n\theta, а их разность — (αq−αk)+(m−n)θ(\alpha_q - \alpha_k) + (m - n)\theta. Абсолютные позиции mm и nn исчезли, осталось только расстояние m−nm - n:

(R(mθ) q)⋅(R(nθ) k)=rqrkcos⁡(αq−αk+(m−n)θ)\big(R(m\theta)\,q\big) \cdot \big(R(n\theta)\,k\big) = r_q r_k \cos\big(\alpha_q - \alpha_k + (m - n)\theta\big)

Числа при q=k=(1,0)q = k = (1, 0) и θ=1\theta = 1 радиан на токен:

Позиции m,nm, nR(m) qR(m)\,qR(n) kR(n)\,kСкалярное произведение
3 и 1(−0.990, 0.141)(-0.990,\ 0.141)(0.540, 0.841)(0.540,\ 0.841)−0.416=cos⁡2-0.416 = \cos 2
10 и 8(−0.839, −0.544)(-0.839,\ -0.544)(−0.146, 0.989)(-0.146,\ 0.989)−0.416=cos⁡2-0.416 = \cos 2

Векторы совсем разные, а оценка одна и та же: в обоих случаях токены стоят на расстоянии 2. Проверим и на векторах произвольной формы:

import numpy as np
def R(theta):
c, s = np.cos(theta), np.sin(theta)
return np.array([[c, -s], [s, c]])
q, k, theta = np.array([0.6, 0.8]), np.array([1.0, -0.5]), 0.5
for m, n in [(3, 1), (10, 8), (100, 98)]:
score = (R(m * theta) @ q) @ (R(n * theta) @ k) # поворачиваем «по номеру позиции» и сравниваем
print(m, n, round(score, 4))
# 3 1 -0.8176
# 10 8 -0.8176
# 100 98 -0.8176

Это и есть ротационные позиционные эмбеддингиротационные позиционные эмбеддинги (RoPE)EN rotary position embedding (RoPE)RoPE предложен в Китае (Су Цзяньлинь и соавторы, компания Zhuiyi). Сейчас используется почти во всех LLM. в двумерном виде. В настоящей модели вектор головы внимания длины dd режут на d/2d/2 пар и каждую пару вращают со своей частотой θi\theta_i — теми же «стрелками часов», что в синусоидальных кодировках. Подробности, код Llama и сравнение со старыми схемами — в уроке 4.5.2. А когда модель надо научить читать текст длиннее обучающего, оказывается, что самые медленные стрелки за всё обучение не сделали ни одного оборота (их период 2π/θi2\pi/\theta_i больше длины обучающих текстов). Как это чинят YaRN и NTK — урок 5.5.1.

Соберите весь урок в код: перевод в радианы, матрица поворота, поворот вектора, «оценка RoPE» для двух позиций и синусоидальные кодировки. Тесты проверят, что поворот сохраняет длину, что повороты складываются и что оценка зависит только от расстояния между позициями.

Повороты, RoPE на плоскости и синусоидальные кодировкиnumpy · нужен интернет7 тестов
import numpy as np


def deg2rad(deg):
    """Переведите градусы в радианы.

    Пример: deg2rad(180) → π ≈ 3.1416, deg2rad(90) → π/2 ≈ 1.5708.
    """
    # TODO: умножьте на π и поделите на 180
    ...


def rotation_matrix(theta):
    """Матрица поворота 2×2 на угол theta (в радианах) против часовой стрелки:

        [[cos θ, −sin θ],
         [sin θ,  cos θ]]

    Верните np.array формы (2, 2).
    """
    # TODO
    ...


def rotate(v, theta):
    """Поверните вектор v = (x, y) на угол theta. Верните np.array формы (2,).

    Пример: rotate([3, 4], π/2) → [-4, 3] (длина осталась 5).
    """
    # TODO: матрица поворота @ вектор (не забудьте превратить v в массив float)
    ...


def rope_score(q, k, m, n, theta=1.0):
    """«Оценка внимания» после RoPE на плоскости.

    Запрос q стоит на позиции m, ключ k — на позиции n. Поверните q на угол m·theta,
    k — на угол n·theta и верните их скалярное произведение (число float).
    Пример: rope_score([1, 0], [1, 0], 3, 1) → cos 2 ≈ −0.416.
    """
    # TODO
    ...


def sinusoidal_encoding(num_positions, d, base=10000.0):
    """Синусоидальные позиционные кодировки формы (num_positions, d), d — чётное.

    Для пары i = 0, 1, …, d/2 − 1 частота θ_i = base ** (−2i/d), и
        PE[pos, 2i]     = sin(pos · θ_i)
        PE[pos, 2i + 1] = cos(pos · θ_i)
    Пример для d = 4: строка позиции 2 ≈ [0.909, −0.416, 0.020, 1.000].
    """
    # TODO: сначала массив нулей нужной формы, потом заполните чётные и нечётные столбцы
    ...


# Проверьте себя кнопкой «Запустить»:
print(deg2rad(90))                                  # ≈ 1.5708
print(rotate([3, 4], deg2rad(90)))                  # ≈ [-4.  3.]
print(rope_score([1, 0], [1, 0], 3, 1))             # ≈ -0.416
print(rope_score([1, 0], [1, 0], 10, 8))            # то же число: расстояние снова 2
print(sinusoidal_encoding(3, 4))

Задание для Jupyter (по желанию). Постройте в matplotlib (урок 0.3.3) картинку кодировок: plt.imshow(sinusoidal_encoding(100, 64), aspect="auto"). Слева вы увидите частые полосы быстрых пар, справа — почти однотонные медленные. Это «часы» в одном изображении.

Проверь себя6 вопросов
1 Сколько радиан в угле 90°? Округлите до двух знаков.
2 Где окажется точка на единичной окружности, если от (1,0)(1, 0) пройти угол π\pi радиан против часовой стрелки?
3 Пара в позиционной кодировке поворачивается на θ=0.05\theta = 0.05 радиана за токен. Через сколько токенов она сделает полный оборот? Округлите до целого.
4 Выберите все верные утверждения о матрице поворота R(θ)R(\theta).Можно выбрать несколько ответов
5 RoPE на плоскости: q=k=(1,0)q = k = (1, 0), θ=1\theta = 1. Запрос стоит на позиции 7, ключ — на позиции 4. Чему равно скалярное произведение повёрнутых векторов? Округлите до двух знаков.
6 Почему в Python math.sin(90) не равно 1?
  • Угол в радианах — длина дуги на окружности радиуса 1; полный оборот 2π≈6.2832\pi \approx 6.283. Все библиотеки ждут радианы.
  • cos⁡α\cos\alpha и sin⁡α\sin\alpha — координаты точки на единичной окружности; cos⁡2α+sin⁡2α=1\cos^2\alpha + \sin^2\alpha = 1.
  • sin⁡(θ⋅pos)\sin(\theta \cdot pos) — волна с частотой θ\theta и периодом 2π/θ2\pi/\theta. Набор волн разной частоты — «часы», которыми кодируют позицию токена. При малых углах sin⁡x≈x\sin x \approx x, cos⁡x≈1−x2/2\cos x \approx 1 - x^2/2.
  • Поворот — матрица R(θ)R(\theta): сохраняет длины, повороты складываются, R(−θ)R(-\theta) отменяет R(θ)R(\theta).
  • Скалярное произведение единичных векторов — косинус разности углов. Поэтому после поворотов на mθm\theta и nθn\theta оно зависит только от m−nm - n: это идея RoPE.

Дальше: множества, сочетания и подсчёт — как работать со словарём токенов как с множеством, сравнивать документы через сходство Жаккара и считать, сколькими способами можно выбрать экспертов в MoE.