Зачем трансформерам синус и косинус для позиционного кодирования
Пошаговое объяснение синусоидального позиционного кодирования: почему внимание не видит порядок слов, часы с разной скоростью, разобранный пример на 12 измерениях и вращение.
Почему трансформеры кодируют позицию синусом и косинусом? Пронумеровать слова 1, 2, 3 кажется куда проще, но толково ответить на этот вопрос сложнее, чем кажется.
Внимание не чувствует порядка
Как операция внимание обращается со входом как с мешком слов. Дайте трансформеру «кот ест мышь», а потом «мышь ест кота» без всякой информации о позициях, и слой внимания оба раза увидит одни и те же три вектора. Он не может понять, что шло первым, и скалярные произведения Q · K получаются одинаковыми. Это настоящая проблема: первая фраза — это ужин, а вторая — сюрприз. Позицию нужно куда-то добавить ещё до первого слоя внимания.
Очевидное решение и почему оно не работает
Возьмём фразу «cat and dog» — три токена: cat на позиции 1, «and» на позиции 2 и dog на позиции 3. Настоящие эмбеддинги слов — это не два-три числа. Боевые трансформеры используют векторы на 512, 768 и даже больше измерений. Вот cat и dog в 768 измерениях, показаны первые шесть элементов и последний, чтобы была видна форма:
cat ≈ [ 0.82, -0.14, 0.36, 0.05, -0.71, 0.29, ... , -0.18 ] (768 numbers)
dog ≈ [ 0.77, -0.09, 0.41, 0.11, -0.68, 0.24, ... , -0.15 ] (768 numbers)
Два вектора лежат близко, и это логично: оба — животные, а пространство эмбеддингов помещает слова с похожим значением рядом. Но ни в одном векторе нет ничего о том, где слово стоит в предложении. Вот этого и не хватает.
Первое, что пробует большинство, — прибавить номер позиции простым скаляром к каждому элементу эмбеддинга.
cat at position 1 → cat + 1 → [ 1.82, 0.86, 1.36, 1.05, 0.29, 1.29, ... , 0.82 ]
На позиции 1 это выглядит безобидно. А теперь представьте то же слово на позиции 500 в длинном документе:
cat at position 500 → cat + 500 → [ 500.82, 499.86, 500.36, 500.05, 499.29, 500.29, ... , 499.82 ]
Первая проблема — главная. Значения раздуваются, причём одинаково во всех измерениях. То, что делало cat узнаваемым, его особая последовательность 0.82, -0.14, 0.36 и так далее, то, что отличало «кота» от «собаки», превращается в ошибку округления рядом с 500. Сигнал тонет. Деление на максимальную длину последовательности не спасает: эта длина неизвестна, когда модель работает, а короткие предложения и очень длинные документы оказались бы в несовместимых масштабах.
Вторая проблема тоньше. Даже до раздувания одно общее смещение, добавленное ко всем измерениям, — бедный способ выразить позицию. У модели есть всего одно число, а этого мало, чтобы выстроить поведение на основе относительных позиций вроде «обрати внимание на слово двумя позициями раньше».
Значит, позиция должна быть отдельным вектором той же формы, что и эмбеддинг слова, чтобы их можно было просто сложить, и она должна вести себя прилично без внешних сведений вроде длины последовательности.
Уменьшим задачу, чтобы её было лучше видно. Забудьте про 768 измерений и представьте всего 2. Cat на позиции 1 нужен какой-то вектор [x, y], а «and» на позиции 2 — другой вектор [p, q]. Какими должны быть эти числа? Можно выбрать их вручную:
position 1 → [0.2, 0.8]
position 2 → [0.4, 0.6]
position 3 → [0.6, 0.4]
Каждая позиция получает свою пару, но выбор произволен. Ничто не связывает позицию 4 с позицией 3 так, как они связаны на числовой прямой. Спросите вектор для позиции 4 857 — и правила нет: кому-то пришлось бы заранее выбрать его для каждой позиции, которая вообще может понадобиться.
Нужна функция. Она принимает целую позицию и возвращает пару [x, y], ставит соседние позиции близко друг к другу и никогда не даёт числам взорваться, как бы велика ни была позиция. Геометрия решила это давно. Это называется окружность.
Часы для позиции
Поставьте точку на окружность и пустите её двигаться. Её координаты — (cos θ, sin θ), так что каждый угол даёт уникальную 2D-координату, а соседние углы — соседние координаты. Представление гладкое. Вместо «позиция 17» можно записать:
position 17 → (sin 17, cos 17)
Два измерения кодируют одну позицию, и это уже лучше скаляра. Значения остаются в пределах [-1, 1], так что ничего не взрывается, а позиции 17 и 18 выглядят почти одинаково.
Остаётся одна загвоздка: одни часы повторяются. После полного оборота позиция 1 и позиция 1 + 2π выглядят одинаково. При предложении из 500 токенов и часах с периодом около 6 совпадения будут повсюду.
Много часов с разной скоростью
Вся схема работает благодаря тому, что часов много и каждые идут со своей скоростью. Одни делают оборот примерно каждые 10 токенов, другие — каждые 100, третьи — каждые 1 000, четвёртые — каждые 10 000. Для каждой позиции записываются все показания вместе. Две позиции могут совпасть на быстрых часах, но не на всех часах сразу, поэтому общий набор показаний работает как отпечаток пальца. Похожий трюк использует GPS с несколькими спутниками, а штрихкод кодирует ID полосками разной ширины.
В настоящей формуле размерность эмбеддинга d даёт d/2 пар часов, и пара i идёт с частотой 1 / 10000^(2i/d). Маленькое i — быстрые часы, большое i — медленные:
PE(pos, 2i) = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))
Вот и вся формула.
Разбор на примере: «cat and dog» в 12 измерениях
Абстрактная математика даёт не так много, поэтому посчитаем. При d = 12 получается 6 пар часов, у каждой своя частота: пара 1 самая быстрая, пара 6 самая медленная:
pair 1: 10000^0/12 ≈ 1 (fastest)
pair 2: 10000^2/12 ≈ 21.54
pair 3: 10000^4/12 ≈ 464.16
pair 4: 10000^6/12 ≈ 1000
pair 5: 10000^8/12 ≈ 2154.43
pair 6: 10000^10/12 ≈ 4641.59 (slowest)
Возьмём пару 1 и cat на позиции 1: формула даёт
Столбец — это полный 12-мерный вектор PE слова, а самое интересное — в строках:
- Строка 1 сильно скачет от слова к слову: 0.84, потом 0.91, потом 0.14. Быстрые часы проходят почти целый оборот всего за два тика.
- Строка 6 почти не меняется: трижды подряд 1.0000. Медленные часы ещё толком не тикнули. Они нужны, чтобы различать далёкие друг от друга позиции, например 3 и 3000.
- Каждый элемент полностью детерминирован. Ничто в таблице не обучается.
Вход трансформера — это просто поэлементная сумма:
input = word_embedding(cat) + PE(1)
Та же форма, никакого хитрого слияния. Чтобы увидеть реальный результат, эмбеддинги слов уменьшены до 12 измерений игрушечного PE (настоящая модель использовала бы свои 768-мерные векторы, а эти числа выдуманы только для демонстрации механизма):
cat_embedding ≈ [ 0.20, 0.70, -0.10, 0.40, 0.15, -0.30, 0.55, 0.10, -0.20, 0.05, 0.30, -0.15]
dog_embedding ≈ [ 0.18, 0.68, -0.05, 0.35, 0.20, -0.25, 0.50, 0.15, -0.18, 0.08, 0.28, -0.10]
Как и в 768-мерных версиях, cat и dog поначалу близки, потому что оба — животные. Теперь прибавим к каждому слову его PE из таблицы, измерение за измерением:
cat_final = cat_embedding + PE(1)
= [1.0415, 1.2403, -0.0536, 1.3989, 0.1522, 0.7000, 0.5510, 1.1000, -0.1995, 1.0500, 0.3002, 0.8500]
dog_final = dog_embedding + PE(3)
= [0.3211, -0.3100, 0.0888, 1.3403, 0.2065, 0.7500,
0.5030, 1.1500, -0.1786, 1.0800, 0.2806, 0.9000]
Именно эти два вектора попадают в первый слой внимания. Сравните измерение 2: там cat и dog были почти одинаковыми (0.70 против 0.68), а после позиционного сигнала стали 1.24 против -0.31 — небо и земля. Это быстрые часы (пара 1, позиция 1 против позиции 3) работают как задумано. Они оставляют достаточно заметный позиционный отпечаток, чтобы «cat в начале предложения» и «dog дальше в предложении» стали явно разными векторами, хотя сами слова близки по смыслу.
Откуда модель знает, что [0.84, 0.54, ...] — это позиция 1?
Ниоткуда. Ни одно измерение в этом векторе не говорит «1», и ничто его не расшифровывает. Модели никогда не нужно восстанавливать номер позиции, это не её задача.
Задача уже. Допустим, query слова dog встречается с key слова cat в скалярном произведении. Покажет ли оценка, что слова стоят на расстоянии двух позиций? На этом слое «понимать позицию» означает ровно это. Кодировка — не шум. Это гладкая смесь синусоид с известными частотами, поэтому обученные проекции Q и K могут улавливать эти закономерности и реагировать на них. Query может в итоге означать «предпочитай key, который стоит на одну позицию раньше», и никакая переменная с названием «позиция» при этом не участвует.
Удобное сравнение — распознавание лиц. В 512-мерном эмбеддинге лица нет отдельного измерения с именем человека. Весь вектор целиком и есть этот человек. 12-мерный вектор PE(1) устроен так же: в совокупности он и есть позиция 1.
Зачем и синус, и косинус
Эту часть обычно объясняют на пальцах. Настоящая причина в том, что пара вместе даёт представление позиции, удобное для вращения. Формулы сложения углов говорят:
sin(x + Δ) = sin(x)cos(Δ) + cos(x)sin(Δ)
cos(x + Δ) = cos(x)cos(Δ) − sin(x)sin(Δ)
Зная [sin(x), cos(x)], можно вычислить [sin(x+Δ), cos(x+Δ)] с помощью фиксированного линейного преобразования, которое зависит только от Δ, а не от x. Значит, сдвиг позиции на Δ — одно и то же преобразование, откуда бы вы ни начали. Это позволяет вниманию выучить «посмотри на Δ токенов назад» как одну переиспользуемую операцию, без отдельного правила для каждой пары позиций. Одного синуса для этого мало: чтобы задать вращение полностью, нужны обе половины пары.
Что происходит при обратном распространении
Подстраиваются ли позиционные кодировки градиентным спуском во время обучения? Синусоидальные — нет. У них нет обучаемых параметров, формула фиксирована. Градиенты проходят через сумму (вход = эмбеддинг + PE) и не находят на стороне PE ничего, что можно обновить, — примерно как спрашивать, обучается ли 5 в y = x + 5.
С таблицей эмбеддингов слов иначе. Каждый раз, когда cat попадается в батче, его строка в таблице получает градиент и сдвигается.
Стоит упомянуть ещё один вариант, потому что большинство современных моделей на деле используют именно его: обучаемые позиции. Формулы нет вообще. Модель хранит обучаемую таблицу с одной строкой на позицию, до максимальной длины последовательности и шириной в размерность модели, и обучение само решает, что должно лежать в строке 17. О таком проще рассуждать, и в пределах длин, виденных при обучении, он работает хорошо, но растянуть его на более длинные последовательности, как синусоидальное PE, нельзя. Этот компромисс, как и новые подходы вроде ALiBi и RoPE, — отдельная тема.