Тригонометрия для ML: углы, синус, косинус, вращения
С нуля: радианы, единичная окружность, синус и косинус как координаты точки, волны разной частоты и поворот вектора матрицей 2×2 — ровно та тригонометрия, на которой стоят позиционные кодировки, RoPE и YaRN.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Трансформер видит текст как набор векторов, и сам по себе не знает, какое слово стоит первым, а какое десятым. Номер позиции ему подсказывают с помощью синусов и косинусов. Так делали в статье «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 с центром в начале координат. Встаньте в точку и идите по окружности против часовой стрелки. РадианрадианEN radianУгол, при котором длина дуги равна радиусу. Полный оборот — 2π ≈ 6.283 радиана (360°), 1 радиан ≈ 57.3°. Все библиотеки (math, NumPy, PyTorch) ждут углы в радианах. — это угол, на который вы повернётесь, пройдя по дуге путь, равный радиусу, то есть 1. Угол в радианах — это просто длина пройденной дуги на окружности радиуса 1.
Длина всей окружности равна , при это . Значит, полный оборот — это радиан, а 360° и — одно и то же. Отсюда формула перевода:
Читаем: «чтобы получить радианы, умножьте градусы на и поделите на 180». Числа: , , а 1 радиан — чуть меньше шестой части оборота.
import math
print(math.radians(90)) # 1.5707963267948966 — это π/2print(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 повёрнут на радиана — никаких множителей 360 и 180.
Единичная окружность: синус и косинус
Заголовок раздела «Единичная окружность: синус и косинус»Единичная окружностьединичная окружностьEN unit circleОкружность радиуса 1 с центром в (0, 0). Точка под углом α на ней имеет координаты (cos α, sin α). — окружность радиуса 1 с центром в точке . Отложите от точки угол («альфа») против часовой стрелки — вы окажетесь в точке . Её координаты по определению называют косинусом и синусом угла:
КосинускосинусEN cosinecos α — координата x точки на единичной окружности под углом α. Для единичных векторов скалярное произведение равно косинусу угла между ними. — это «насколько точка ушла вправо» (координата ), синуссинусEN sinesin α — координата y точки на единичной окружности под углом α. Значения от −1 до 1, период 2π. — «насколько ушла вверх» (координата ). Оба числа всегда лежат между и : точка не может выйти за окружность.
Если вы помните школьное определение «синус — противолежащий катет к гипотенузе», оно то же самое: на рисунке видно прямоугольный треугольник с гипотенузой 1, и его катеты равны и . Определение через окружность лучше тем, что работает для любого угла: тупого, больше оборота, отрицательного (идём по часовой стрелке).
По теореме Пифагора (квадрат гипотенузы равен сумме квадратов катетов) сразу получаем главное тождество:
Запись — это . Проверка для 50°: .
Значения, которые стоит помнить. Достаточно представить, где окажется точка на окружности:
| Угол | 0° | 30° | 45° | 60° | 90° | 180° | 270° | 360° |
|---|---|---|---|---|---|---|---|---|
| Радианы | 0 | 0.524 | 0.785 | 1.047 | 1.571 | 3.142 | 4.712 | 6.283 |
| 1 | 0.866 | 0.707 | 0.5 | 0 | −1 | 0 | 1 | |
| 0 | 0.5 | 0.707 | 0.866 | 1 | 0 | −1 | 0 |
При 90° точка в самом верху , при 180° — слева , при 270° — внизу , при 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, а не 0a = math.radians(50)print(math.cos(a) ** 2 + math.sin(a) ** 2) # 1.0 — тождество выполняетсяЧисло 6.12e-17 вместо нуля — обычное округление float (урок 0.0.2): хранится неточно, и косинус «почти
» оказывается «почти нулём». Поэтому числа с плавающей точкой сравнивают через math.isclose или
np.allclose, а не через ==.
Любой вектор — это длина и угол. Точка лежит на расстоянии 5 от начала координат. Поделив на 5, получим точку на единичной окружности, то есть для угла . Значит, . Такая запись «длина × направление» пригодится через раздел, когда будем поворачивать векторы.
Волны: период, частота, фаза
Заголовок раздела «Волны: период, частота, фаза»Пусть угол растёт равномерно: на каждом шаге точка проходит по окружности одно и то же расстояние («тета»). На шаге угол равен . Если записывать только высоту точки, получится последовательность — волна.
Через полный оборот точка возвращается туда же, поэтому синус и косинус повторяются: . Такие функции называют периодическимипериодическая функцияEN periodic functionФункция, значения которой повторяются через период T: f(x + T) = f(x). У sin(θ·x) период 2π/θ.. Три слова описывают любую волну:
- Частота — на сколько радиан поворачивается точка за один шаг. В ML это обычно «радиан на токен».
- Период (или длина волны) — через сколько шагов волна повторяется: . При это позиции, при — , при — .
- Фаза — сдвиг волны: начинается не с нуля, а «забегает вперёд» на угол («фи»). Косинус — это синус со сдвигом на четверть оборота: .
В физике частоту называют угловойугловая частотаEN angular frequencyНа сколько радиан поворачивается точка за единицу времени или за один шаг (токен). Связь с частотой f в герцах: ω = 2πf; период T = 2π/ω., чтобы не путать с частотой в герцах (колебаниях в секунду). Для звука с частотой герц угол растёт на радиан в секунду.
import numpy as np
pos = np.arange(8) # позиции 0, 1, …, 7for 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]Посмотрите на две последние строки: у медленных волн значения почти равны . Это приближение малого угла: при малых (в радианах) и . Числа: , против . В уроке 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 года: вектор позиции длины состоит из пар с частотами
Читаем: «частота -й пары — это 10 000 в степени минус ». При получаем (секундная стрелка), дальше частоты падают в геометрической прогрессии до почти нуля (часовая). Пример для : частоты и , и позиция 2 кодируется вектором . Собрать такие векторы для всех позиций вы сможете в задании.
Углубление: спектр звука — волны в обратную сторону
Звук — это давление воздуха, которое колеблется сотни и тысячи раз в секунду. Аккорд — сумма нескольких волн разной частоты. Спектр отвечает на обратный вопрос: из каких частот и с какой силой (амплитудой) сложен сигнал? Простейший способ узнать — «прикладывать» к сигналу чистые волны и смотреть, насколько они совпадают (среднее произведение сигнала на волну):
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.5for 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 о преобразовании Фурье (ссылка в источниках).
Поворот вектора
Заголовок раздела «Поворот вектора»Теперь научимся поворачивать. Возьмём вектор длины под углом : , . Повернуть его на угол — значит оставить длину , а угол сделать :
Чтобы избавиться от и и выразить ответ через исходные , нужны школьные формулы суммы углов:
Их выводят геометрически, а мы просто проверим числами для , : слева , справа . Сошлось. Умножим обе формулы на и заменим , :
Это и есть поворот. Числа: повернём на 90°. Там , , поэтому , . Получили : длина по-прежнему , а стрелка смотрит на четверть оборота левее.
Матрица поворота
Заголовок раздела «Матрица поворота»Четыре числа удобно записать таблицей — матрицей поворотаматрица поворотаEN rotation matrixR(α) = [[cos α, −sin α], [sin α, cos α]] поворачивает вектор на плоскости на угол α, не меняя длины. Главное свойство для RoPE: R(a)ᵀR(b) = R(b − a).:
Правило умножения матрицы на вектор (подробно — в уроке 1.1.2): каждая строка матрицы умножается на вектор
поэлементно, и результаты складываются. Первая строка даёт , вторая — .
В 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)) @ vprint(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:
- Поворот не меняет длину. Вектор длины 5 остаётся длины 5.
- Повороты складываются: . Повернуть на 0.3, потом на 0.5 — то же, что сразу на 0.8.
- Обратный поворот — поворот на минус тот же угол: . Его же можно получить, отразив матрицу относительно диагонали (транспонировав, урок 1.1.2): у синусы просто меняют знак.
Косинус разности и скалярное произведение
Заголовок раздела «Косинус разности и скалярное произведение»Последний шаг — связать углы с тем, как нейросети сравнивают векторы. В уроке 1.1.1 вы подробно разберёте скалярное произведение; на плоскости оно считается так: — произведения координат, сложенные вместе. Например, . Именно так внимание в трансформере измеряет, насколько запрос «подходит» ключу (урок 4.4.2).
Возьмём два единичных вектора под углами и : , . Их скалярное произведение — это ещё одна школьная формула, косинус разности:
Читаем: «скалярное произведение двух единичных векторов равно косинусу угла между ними». Числа: , : . Векторы разной длины и дают то же, умноженное на длины: — эту формулу вы снова встретите в уроке 1.1.1.
Главное следствие. Скалярное произведение зависит не от самих углов, а только от разности углов (и от длин). Повернём оба вектора на один и тот же угол — разность не изменится, а значит, не изменится и скалярное произведение.
Идея RoPE за одну формулу
Заголовок раздела «Идея RoPE за одну формулу»Пусть токен на позиции даёт запрос , а токен на позиции — ключ . Повернём запрос на угол , а ключ — на , каждый «по своему номеру». Углы стали и , а их разность — . Абсолютные позиции и исчезли, осталось только расстояние :
Числа при и радиан на токен:
| Позиции | Скалярное произведение | ||
|---|---|---|---|
| 3 и 1 | |||
| 10 и 8 |
Векторы совсем разные, а оценка одна и та же: в обоих случаях токены стоят на расстоянии 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.5for 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. в двумерном виде. В настоящей модели вектор головы внимания длины режут на пар и каждую пару вращают со своей частотой — теми же «стрелками часов», что в синусоидальных кодировках. Подробности, код Llama и сравнение со старыми схемами — в уроке 4.5.2. А когда модель надо научить читать текст длиннее обучающего, оказывается, что самые медленные стрелки за всё обучение не сделали ни одного оборота (их период больше длины обучающих текстов). Как это чинят YaRN и NTK — урок 5.5.1.
Практика
Заголовок раздела «Практика»Соберите весь урок в код: перевод в радианы, матрица поворота, поворот вектора, «оценка RoPE» для двух позиций и синусоидальные кодировки. Тесты проверят, что поворот сохраняет длину, что повороты складываются и что оценка зависит только от расстояния между позициями.
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").
Слева вы увидите частые полосы быстрых пар, справа — почти однотонные медленные. Это «часы» в одном изображении.
Проверь себя
Заголовок раздела «Проверь себя»- Угол в радианах — длина дуги на окружности радиуса 1; полный оборот . Все библиотеки ждут радианы.
- и — координаты точки на единичной окружности; .
- — волна с частотой и периодом . Набор волн разной частоты — «часы», которыми кодируют позицию токена. При малых углах , .
- Поворот — матрица : сохраняет длины, повороты складываются, отменяет .
- Скалярное произведение единичных векторов — косинус разности углов. Поэтому после поворотов на и оно зависит только от : это идея RoPE.
Дальше: множества, сочетания и подсчёт — как работать со словарём токенов как с множеством, сравнивать документы через сходство Жаккара и считать, сколькими способами можно выбрать экспертов в MoE.