Attention mechanism 🔍
В уроке 341 ты разобрал encoder-decoder архитектуру и увидел её фундаментальное ограничение: кодировщик обязан сжать всю входную последовательность — предложение из трёх слов или абзац из ста пятидесяти — в единственный вектор фиксированной размерности, а декодировщик потом вынужден генерировать весь перевод или всё резюме, опираясь исключительно на этот один вектор. Это и есть то самое «бутылочное горлышко» (bottleneck): узкое место, через которое вынужденно проходит вся информация, независимо от того, насколько длинным и насыщенным деталями был исходный текст. Сегодняшний урок посвящён идее, которая эту проблему решает не частично, а принципиально — механизму внимания (attention mechanism).
Идея удивительно проста в формулировке и при этом невероятно мощна в последствиях: что, если вместо одного контекстного вектора на всю последовательность декодировщик на каждом шаге генерации будет заново «оглядываться» на все скрытые состояния кодировщика и сам решать, какому из них уделить больше внимания прямо сейчас? Переводя слово «машину» в конце длинного предложения, модели не нужно тащить это знание через десятки промежуточных шагов LSTM (урок 340) в единственном сжатом векторе — она может напрямую «посмотреть» на скрытое состояние, соответствующее слову «car» во входном предложении, в момент, когда это слово актуально для генерации.
Важно сказать это прямо, без преуменьшения: механизм внимания, который ты разбираешь в этом уроке, — один из важнейших уроков всего курса. Он не просто улучшает одну конкретную архитектуру seq2seq (sequence-to-sequence, «последовательность в последовательность») — он лежит в основе абсолютно всех современных больших языковых моделей. Когда ты в следующем уроке (343) встретишь трансформеры, а затем в уроке 344 — BERT, GPT и семейство современных LLM (large language models, «большие языковые модели»), ты обнаружишь, что все они построены вокруг одной и той же математической операции, которую ты сегодня выведешь с нуля: взвешенной суммы значений, где веса вычисляются через сравнение запроса с ключами. Claude, GPT-4, LLaMA, Gemini — при всех архитектурных различиях в деталях, все они опираются на self-attention (самовнимание) как на базовый строительный блок.
Есть смысл заранее обозначить, почему именно эта идея оказалась настолько плодотворной. Attention решает сразу три разные проблемы одним и тем же математическим приёмом. Во-первых, проблему бутылочного горлышка — декодировщику больше не нужно сжимать всё в один вектор. Во-вторых, проблему длинных зависимостей — слова, разделённые десятками токенов, теперь могут напрямую «видеть» друг друга за один шаг, а не через долгую цепочку рекуррентных обновлений скрытого состояния. В-третьих, что не менее важно, проблему интерпретируемости — веса внимания можно визуализировать и буквально увидеть, на какое слово входа «смотрит» модель, генерируя каждое слово выхода. Ни один из предыдущих механизмов, включая LSTM с его вентилями забывания (урок 340), не давал такой прозрачности.
Для практикующего специалиста по Data Science это не архивная историческая справка о том, как в 2014–2015 годах решили одну конкретную проблему машинного перевода. Это фундамент, без понимания которого невозможно осмысленно работать ни с одной современной архитектурой обработки естественного языка, ни с современными архитектурами компьютерного зрения (Vision Transformer), ни даже с рекомендательными системами, которые всё активнее перенимают ту же идею. Сегодняшний урок стоит того, чтобы разобрать его максимально медленно и дотошно — с полным численным примером, а не только с абстрактной формулой.
История
К 2014 году encoder-decoder архитектура (урок 341) уже показала впечатляющие результаты в машинном переводе, но исследователи упирались в стену: качество перевода стабильно ухудшалось по мере роста длины входного предложения. Модель прекрасно переводила короткие фразы из пяти-семи слов, но на предложениях из двадцати-тридцати слов качество резко падало. Причина была понятна на интуитивном уровне: кодировщик обязан был сжать всё предложение, независимо от его длины, в вектор фиксированной размерности — скажем, 512 или 1024 числа. Чем длиннее предложение, тем больше информации нужно втиснуть в тот же самый «контейнер» фиксированного размера, и неизбежно что-то теряется, обычно — детали из начала предложения, вытесненные более свежими впечатлениями от конца.
В 2014–2015 годах Дмитрий Бахданау (Dzmitry Bahdanau), Кёнхён Чо (KyungHyun Cho) и Йошуа Бенджио (Yoshua Bengio) опубликовали статью, которая предложила элегантное решение: вместо того чтобы заставлять кодировщик сжимать всю информацию в один вектор, давайте сохраним скрытые состояния кодировщика для каждой позиции входной последовательности, а декодировщику на каждом шаге генерации позволим самому, обучаемым образом, решать — какие из этих сохранённых состояний важны именно сейчас, для генерации именно этого слова. Модель сама вычисляла веса важности — attention weights — для каждой пары «текущий шаг декодера — позиция кодировщика», и эти веса менялись динамически от шага к шагу. Результат оказался разительным: качество перевода длинных предложений перестало деградировать так резко, а как побочный, но крайне ценный эффект, появилась возможность визуализировать, на какие именно слова входа опиралась модель при генерации каждого слова перевода — то есть заглянуть внутрь «чёрного ящика» нейросети почти буквально.
Идея быстро распространилась и обросла вариациями — вскоре появился и несколько иначе устроенный механизм внимания (в частности, работа Луонга и соавторов 2015 года с несколькими альтернативными способами вычисления весов), но принцип остался тем же самым. А в 2017 году произошёл качественный скачок, о котором ты подробно узнаешь в следующем уроке: команда исследователей Google опубликовала статью с провокационным названием «Attention Is All You Need» («Внимание — это всё, что нужно»), в которой предложила выбросить рекуррентные слои (RNN/LSTM, урок 340) из архитектуры вообще, оставив только механизм внимания в его самой чистой форме — self-attention. Именно эта архитектура, названная трансформером (Transformer), стала основой для всех крупных языковых моделей, которые ты знаешь сегодня. Но прежде чем перейти к трансформерам, нужно досконально разобраться в самом механизме внимания — и этим мы сейчас займёмся.
От бутылочного горлышка к вниманию на каждом шаге
Интуиция
Вспомни устройство encoder-decoder из урока 341: кодировщик читает входную последовательность токен за токеном, обновляя своё скрытое состояние на каждом шаге, и в конце возвращает единственный вектор — обычно последнее скрытое состояние — как «сжатое представление» всего смысла входа. Декодировщик получает этот единственный вектор и должен, опираясь только на него (плюс уже сгенерированные токены), произвести всю выходную последовательность.
Представь синхронного переводчика, которому запрещено смотреть в текст оригинала во время произнесения перевода — ему разрешено только один раз прочитать всю речь, составить в уме краткий конспект фиксированного объёма (скажем, ровно на одной карточке), а затем переводить исключительно по этой карточке, без права взглянуть на оригинал снова. Для короткой фразы это осуществимо. Но для длинной речи из нескольких абзацев переводчик неизбежно забудет детали, которые не поместились на карточку, — и чем длиннее речь, тем катастрофичнее потери.
Attention mechanism меняет правила игры: переводчику разрешают держать перед глазами весь текст оригинала на протяжении всего перевода, и на каждом произносимом слове он может бросить взгляд именно на ту часть оригинала, которая сейчас актуальна. Переводя начало фразы, он смотрит на начало оригинала; добравшись до конца, — на конец оригинала. Ему больше не нужно ничего сжимать в единственный конспект — вся информация остаётся доступной, и решение о том, куда смотреть прямо сейчас, принимается заново на каждом шаге.
Технически это означает следующее изменение архитектуры. Кодировщик, как и раньше, обрабатывает входную последовательность из $n$ токенов и производит скрытое состояние на каждом шаге: $h_1, h_2, \ldots, h_n$. Но теперь, в отличие от базового encoder-decoder, декодировщик получает доступ ко всем этим скрытым состояниям одновременно, а не только к последнему $h_n$. На каждом шаге генерации $t$ декодировщик вычисляет, какое взвешенное сочетание $h_1, \ldots, h_n$ ему нужно именно сейчас — и это взвешенное сочетание называется вектором контекста для данного шага, $c_t$. В отличие от урока 341, где был всего один контекстный вектор на всю генерацию, теперь у каждого шага декодера — свой собственный контекстный вектор, вычисленный заново.
Формула
Контекстный вектор шага $t$ (общая идея, до введения Q/K/V). Пусть кодировщик произвёл скрытые состояния $h_1, h_2, \ldots, h_n$ для входной последовательности из $n$ токенов. На шаге декодирования $t$ вычисляются веса важности $\alpha_{t,1}, \alpha_{t,2}, \ldots, \alpha_{t,n}$ (по одному весу на каждую позицию кодировщика), такие что
$$\sum_{i=1}^{n} \alpha_{t,i} = 1, \qquad \alpha_{t,i} \geq 0$$Контекстный вектор для шага $t$ — это взвешенная сумма всех скрытых состояний кодировщика:
$$c_t = \sum_{i=1}^{n} \alpha_{t,i} \, h_i$$
Разбор примеров
Пример 1 (в чём разница с уроком 341 на конкретном предложении). Пусть кодировщик обработал предложение «Кот спит на диване» и произвёл четыре скрытых состояния: $h_1$ (после «Кот»), $h_2$ (после «спит»), $h_3$ (после «на»), $h_4$ (после «диване»). В базовом encoder-decoder из урока 341 декодировщик получил бы только $h_4$ и должен был бы опираться исключительно на него для генерации всего перевода «The cat is sleeping on the sofa». С attention декодировщик, генерируя слово «cat», может присвоить большой вес $\alpha$ состоянию $h_1$ (связанному с «Кот») и маленькие веса остальным трём состояниям; генерируя позже слово «sofa», он присвоит большой вес состоянию $h_4$ (связанному с «диване»). Веса меняются от шага к шагу — это и есть динамическое «внимание».
Пример 2 (почему сумма весов равна единице — это не случайность, а требование интерпретации). Если бы веса $\alpha_{t,i}$ не суммировались в единицу, контекстный вектор $c_t$ мог бы иметь произвольно большую или маленькую норму в зависимости от длины предложения — для предложения из 3 слов и для предложения из 30 слов масштаб результата отличался бы просто из-за разного числа слагаемых, а не из-за содержательной разницы в том, на что «смотрит» модель. Требование $\sum_i \alpha_{t,i}=1$ превращает веса в полноценное распределение вероятностей: это позволяет интерпретировать $\alpha_{t,i}$ как «доля внимания, уделённая позиции $i$ на шаге $t$», а сам механизм вычисления таких весов, суммирующихся в единицу, — это в точности функция softmax, которую ты уже видел в контексте классификации (многоклассовая логистическая регрессия) и о которой пойдёт речь в следующем разделе.
Пример 3 (длинное предложение — где выигрыш становится наглядным). Возьмём предложение из 25 слов, например развёрнутое описание с несколькими придаточными: «Книга, которую мне подарила бабушка на день рождения два года назад, всё ещё стоит на полке в моей комнате нетронутой». В базовой архитектуре из урока 341 вся эта информация должна пройти через единственный вектор $h_{25}$ — финальное скрытое состояние LSTM после 25 шагов рекуррентного обновления, за которые ранние детали («книга», «бабушка», «подарила») неизбежно ослабевают под действием механизма забывания (урок 340). С attention декодировщик, генерируя английское слово «grandmother» где-то в середине перевода, может напрямую обратиться к скрытому состоянию, соответствующему позиции «бабушка» — второму слову входа — минуя необходимость «протащить» эту информацию через 23 промежуточных рекуррентных шага. Именно поэтому эффект от attention особенно заметен на длинных последовательностях: чем длиннее вход, тем дороже было бы полагаться на единственный сжатый вектор, и тем больше выигрыш от прямого доступа ко всем скрытым состояниям.
Почему это важно
Смена архитектурной идеи с «один вектор на всю последовательность» на «свой контекстный вектор на каждый шаг декодирования» — это не косметическое улучшение, а снятие фундаментального структурного ограничения. Урок 337 уже показывал похожий паттерн мышления: ResNet вместо того, чтобы заставлять каждый слой обязательно что-то преобразовывать, дал слою возможность «пропустить» вход без изменений через skip connection. Attention даёт декодировщику аналогичную свободу — не полагаться слепо на единственный сжатый источник информации, а гибко, по-разному на каждом шаге, комбинировать все доступные источники. Осталось только определить, как именно вычисляются веса $\alpha_{t,i}$ — и для этого нужен аппарат запросов, ключей и значений.
Запрос, ключ, значение: интуиция через поиск
Интуиция
Чтобы понять, откуда берутся веса $\alpha_{t,i}$, полезна аналогия с поисковой системой — той же, что ты используешь каждый день. Когда ты вводишь запрос в поисковике, происходит следующее: твой запрос (query) сравнивается с ключевыми словами и метаданными (key) миллионов проиндексированных страниц, и по степени совпадения запроса с ключом поисковик решает, какие страницы показать первыми, а какие вообще не показывать. Наконец, ты получаешь не сами ключевые слова страницы, а её содержимое — то есть значение (value), которое эта страница на самом деле несёт.
Внутри механизма внимания происходит математически точно то же самое, только в непрерывном, а не в бинарном «показать / не показать» виде. Декодировщик на шаге $t$ формирует запрос (query) — вектор, отражающий «что мне сейчас нужно знать, чтобы сгенерировать следующий токен». Каждая позиция $i$ кодировщика имеет свой ключ (key) — вектор, отражающий «какую информацию я, позиция $i$, могу предложить». Запрос сравнивается с каждым ключом — чаще всего через скалярное произведение, которое ты уже встречал как меру сходства векторов, — и чем больше это сходство, тем больше внимания получит соответствующая позиция. Наконец, каждая позиция $i$ несёт своё значение (value) — вектор с содержательной информацией, которая и попадёт во взвешенную сумму, формирующую итоговый вектор контекста.
Важная деталь, которая отличает эту аналогию от буквального поиска: ключ и значение для одной и той же позиции — это, как правило, разные векторы, полученные из одного и того же скрытого состояния через разные обучаемые линейные преобразования. Ключ отвечает на вопрос «насколько эта позиция релевантна запросу», а значение отвечает на вопрос «что содержательно эта позиция может дать, если она окажется релевантной». Разделение этих двух ролей — не техническая прихоть: оно позволяет модели отдельно учиться «на что откликаться» (ключ) и отдельно «что выдавать в ответ» (значение), а это две принципиально разные задачи, которые выгодно решать разными наборами весов.
Формула
Запрос, ключ и значение. Пусть $q_t$ — вектор запроса, сформированный декодировщиком на шаге $t$ (обычно — линейное преобразование текущего скрытого состояния декодировщика: $q_t = W_Q \, s_t$). Пусть для каждой позиции $i$ кодировщика заданы вектор ключа $k_i = W_K \, h_i$ и вектор значения $v_i = W_V \, h_i$, где $W_Q, W_K, W_V$ — обучаемые матрицы весов. Тогда:
$$\text{сходство}(q_t, k_i) = q_t \cdot k_i = q_t^T k_i$$— скалярное произведение запроса с $i$-м ключом, показывающее степень «совместимости» запроса на шаге $t$ с позицией $i$. Чем больше это число, тем более релевантной запросу модель считает данную позицию (до применения softmax, о котором пойдёт речь в следующем разделе).
Разбор примеров
Пример 1 (три разных вектора из одного скрытого состояния). Пусть скрытое состояние кодировщика для позиции «кот» — это вектор $h = (1, 0, 2, -1)$ размерности 4. Через три разные обучаемые матрицы $W_Q, W_K, W_V$ из этого одного вектора $h$ получаются три разных вектора: запрос $q$, ключ $k$ и значение $v$ — все они, как правило, разной или одинаковой размерности в зависимости от архитектуры, но принципиально разные по смыслу. Важно осознать: это не три случайных дублирования одного и того же вектора, а три разные проекции, каждая из которых обучается решать свою задачу — $W_Q$ учится формировать «удачные вопросы», $W_K$ — «удачные метки релевантности», $W_V$ — «удачное содержимое для выдачи».
Пример 2 (скалярное произведение как мера согласия направлений). Скалярное произведение $q \cdot k = \sum_j q_j k_j$ велико и положительно, когда векторы $q$ и $k$ направлены «похоже» — сонаправлены в пространстве признаков; близко к нулю, когда они почти ортогональны (не связаны); отрицательно, когда направлены «в противоположные стороны». Возьмём для примера $q=(1,2)$ и три варианта ключа: $k_1=(1,2)$ (сонаправлен) даёт $q\cdot k_1 = 1\cdot1+2\cdot2=5$; $k_2=(2,-1)$ (почти ортогонален, поскольку $1\cdot2+2\cdot(-1)=0$) даёт $q\cdot k_2=0$; $k_3=(-1,-2)$ (противоположен) даёт $q\cdot k_3=-5$. Модель, обучаясь на данных, настраивает $W_Q$ и $W_K$ так, чтобы запросы и ключи семантически связанных позиций (например, местоимение «она» и предшествующее ему существительное женского рода) оказывались сонаправленными и давали большое положительное скалярное произведение.
Пример 3 (почему нельзя просто сравнивать сами скрытые состояния $h_i$ напрямую, без проекций Q/K/V). Можно было бы задаться вопросом: зачем вообще вводить отдельные матрицы $W_Q$, $W_K$, $W_V$, если можно было бы напрямую сравнивать скрытые состояния декодировщика и кодировщика через $s_t \cdot h_i$? Проблема в том, что скрытое состояние $h_i$ обязано одновременно нести всю информацию о позиции $i$ — и для сравнения на релевантность, и для содержательного использования во взвешенной сумме, и вдобавок эта информация формировалась в процессе рекуррентной обработки последовательности, а не специально «под задачу вычисления релевантности». Отдельные обучаемые проекции $W_Q$, $W_K$, $W_V$ дают модели три независимые «ручки», которые она может крутить по отдельности при обучении: если оказывается, что для хорошей релевантности нужен один взгляд на $h_i$, а для хорошего содержательного значения — совершенно другой, модель может обучить именно такие разные проекции, вместо того чтобы искать компромисс в одном-единственном представлении.
Почему это важно
Разделение на запрос, ключ и значение — это архитектурное решение, которое переживёт весь остаток курса: именно эта тройка Q/K/V станет базовым строительным блоком self-attention в трансформерах (урок 343), а затем — во всех крупных языковых моделях, которые ты будешь изучать дальше. Понимание того, что «внимание» — это не мистическая способность модели «понимать важное», а конкретная, дифференцируемая, обучаемая через обратное распространение ошибки операция сравнения запроса с ключами, снимает значительную долю таинственности с самых сложных современных архитектур ИИ.
Полный вывод формулы: softmax(QK^T/√d)V с численным примером
Интуиция
У нас уже есть все содержательные кусочки: запрос $q_t$, ключи $k_i$ для каждой позиции, значения $v_i$ для каждой позиции, и скалярное произведение как мера сходства между запросом и ключом. Осталось собрать их в единую формулу, которая на выходе даёт контекстный вектор — и разобраться с двумя техническими деталями, без которых формула работает плохо на практике: как превратить произвольные скалярные произведения (числа любого знака и величины) в аккуратные, суммирующиеся в единицу веса, и почему перед этим превращением нужно поделить на $\sqrt{d}$.
Первая деталь решается уже знакомой тебе функцией softmax: она принимает вектор произвольных вещественных чисел и превращает его в вектор неотрицательных чисел, суммирующихся в единицу — то есть ровно в то распределение весов $\alpha_{t,i}$, которое требовалось для контекстного вектора $c_t = \sum_i \alpha_{t,i} v_i$.
Вторая деталь — деление на $\sqrt{d}$, где $d$ — размерность векторов ключей и запросов, — на первый взгляд кажется чисто техническим трюком, но у неё есть конкретное математическое обоснование, и его стоит понять, а не просто запомнить. Когда компоненты $q$ и $k$ — случайные величины с нулевым средним и единичной дисперсией (типичное состояние после инициализации весов, урок 331), скалярное произведение $q \cdot k = \sum_{j=1}^{d} q_j k_j$ представляет собой сумму $d$ слагаемых, каждое из которых имеет дисперсию порядка единицы. Дисперсия суммы $d$ независимых слагаемых с одинаковой дисперсией растёт линейно с $d$, значит стандартное отклонение суммы растёт как $\sqrt{d}$. При большой размерности $d$ (скажем, 512 или 1024, как в реальных трансформерах) скалярные произведения оказываются существенно больше по модулю, чем при малой размерности — просто потому, что складывается больше случайных слагаемых. А softmax крайне чувствителен к масштабу входа: при больших по модулю значениях он «схлопывается» — один вход получает вес, близкий к единице, остальные — близкий к нулю, — и градиент через такой почти-бинарный softmax становится крайне маленьким (эффект, аналогичный насыщению сигмоиды, который ты разбирал применительно к функциям активации в уроке 329). Деление на $\sqrt{d}$ возвращает дисперсию скалярных произведений обратно к порядку единицы, независимо от размерности $d$, и тем самым сохраняет softmax в «здоровом», хорошо дифференцируемом режиме.
Формула
Attention (полная формула). Пусть $Q$ — матрица запросов размера $[n_q \times d]$ (по одной строке-запросу на каждый шаг декодера), $K$ — матрица ключей размера $[n_k \times d]$ (по одной строке-ключу на каждую позицию кодировщика), $V$ — матрица значений размера $[n_k \times d_v]$ (по одной строке-значению на ту же позицию), а $d$ — размерность запросов и ключей. Тогда:
$$\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^T}{\sqrt{d}}\right) V$$Разбор по шагам:
- $QK^T$ — матрица размера $[n_q \times n_k]$, где элемент $(t, i)$ равен скалярному произведению $q_t \cdot k_i$ — «сырой» сырой счёт совместимости запроса $t$ с ключом $i$.
- Деление на $\sqrt{d}$ — масштабирование, удерживающее дисперсию скалярных произведений порядка единицы независимо от размерности $d$, чтобы softmax не «схлопывался» и градиенты не затухали.
- $\text{softmax}(\cdot)$ применяется построчно — то есть по оси $n_k$ для каждой строки $t$ отдельно, превращая сырые счета в неотрицательные веса, суммирующиеся в единицу вдоль каждой строки: $\alpha_{t,i} = \dfrac{\exp(q_t \cdot k_i / \sqrt{d})}{\sum_{j=1}^{n_k} \exp(q_t \cdot k_j / \sqrt{d})}$.
- Умножение результата на $V$ — взвешенная сумма значений с весами $\alpha_{t,i}$: итоговая строка $t$ выходной матрицы равна $\sum_{i=1}^{n_k} \alpha_{t,i} v_i$, то есть в точности контекстный вектор $c_t$, определённый в первом разделе урока.
Разбор примеров
Пример 1 (полный численный расчёт: 3 токена, векторы размерности 4 — базовый случай для одного запроса). Возьмём последовательность из трёх токенов «Кот спит тихо» и предположим, что после проекций $W_K$, $W_V$ у нас уже готовы три ключа и три значения (размерность $d=4$):
$$k_1 = (1, 0, 1, 0), \quad k_2 = (0, 1, 0, 1), \quad k_3 = (1, 1, 0, 0)$$$$v_1 = (2, 0, 0, 1), \quad v_2 = (0, 2, 1, 0), \quad v_3 = (1, 1, 1, 1)$$
Пусть декодировщик на текущем шаге сформировал запрос $q = (1, 0, 1, 1)$ — интуитивно он «интересуется» позициями, чьи ключи совпадают по первому и третьему компонентам с единицами запроса.
Шаг 1 — скалярные произведения $q \cdot k_i$:
$$q \cdot k_1 = 1\cdot1+0\cdot0+1\cdot1+1\cdot0 = 1+0+1+0 = 2$$$$q \cdot k_2 = 1\cdot0+0\cdot1+1\cdot0+1\cdot1 = 0+0+0+1 = 1$$
$$q \cdot k_3 = 1\cdot1+0\cdot1+1\cdot0+1\cdot0 = 1+0+0+0 = 1$$
Получили вектор сырых счетов $(2, 1, 1)$.
Шаг 2 — деление на $\sqrt{d}=\sqrt{4}=2$:
$$(2/2, \ 1/2, \ 1/2) = (1, \ 0{,}5, \ 0{,}5)$$Шаг 3 — softmax. Вычислим экспоненты: $e^{1}\approx2{,}71828$, $e^{0{,}5}\approx1{,}64872$, $e^{0{,}5}\approx1{,}64872$. Сумма экспонент: $2{,}71828+1{,}64872+1{,}64872=6{,}01572$. Тогда веса:
$$\alpha_1 = \frac{2{,}71828}{6{,}01572} \approx 0{,}4519, \qquad \alpha_2 = \frac{1{,}64872}{6{,}01572} \approx 0{,}2741, \qquad \alpha_3 = \frac{1{,}64872}{6{,}01572} \approx 0{,}2741$$Проверка: $0{,}4519+0{,}2741+0{,}2741 = 1{,}0001$ (сходится к $1$, с точностью до округления). Обрати внимание: позиция 1 получила почти вдвое больший вес, чем позиции 2 и 3, — потому что её сырой счёт совместимости ($2$) был вдвое выше остальных ($1$ и $1$) ещё до применения softmax, а softmax сохраняет относительный порядок, только сглаживая (или заостряя) разницу в зависимости от масштаба.
Шаг 4 — взвешенная сумма значений:
$$c = 0{,}4519\cdot v_1 + 0{,}2741\cdot v_2 + 0{,}2741\cdot v_3$$Распишем по координатам. Первая координата: $0{,}4519\cdot2+0{,}2741\cdot0+0{,}2741\cdot1=0{,}9038+0+0{,}2741=1{,}1779$. Вторая координата: $0{,}4519\cdot0+0{,}2741\cdot2+0{,}2741\cdot1=0+0{,}5482+0{,}2741=0{,}8223$. Третья координата: $0{,}4519\cdot0+0{,}2741\cdot1+0{,}2741\cdot1=0+0{,}2741+0{,}2741=0{,}5482$. Четвёртая координата: $0{,}4519\cdot1+0{,}2741\cdot0+0{,}2741\cdot1=0{,}4519+0+0{,}2741=0{,}7260$.
$$c \approx (1{,}1779, \ 0{,}8223, \ 0{,}5482, \ 0{,}7260)$$Это и есть итоговый вектор контекста для данного запроса — взвешенная смесь трёх значений $v_1, v_2, v_3$, где вклад каждого значения пропорционален тому, насколько соответствующий ключ был «совместим» с запросом.
Пример 2 (та же последовательность, но другой запрос — веса меняются полностью). Возьмём те же три ключа и значения, что и в примере 1, но другой запрос: $q' = (0, 1, 0, 1)$ — обрати внимание, это ровно тот же вектор, что и $k_2$. Скалярные произведения: $q' \cdot k_1 = 0\cdot1+1\cdot0+0\cdot1+1\cdot0=0$; $q' \cdot k_2 = 0\cdot0+1\cdot1+0\cdot0+1\cdot1=2$; $q' \cdot k_3 = 0\cdot1+1\cdot1+0\cdot0+1\cdot0=1$. После деления на $\sqrt4=2$: $(0, \ 1, \ 0{,}5)$. Экспоненты: $e^0=1$, $e^1\approx2{,}71828$, $e^{0{,}5}\approx1{,}64872$. Сумма: $1+2{,}71828+1{,}64872=5{,}367$. Веса: $\alpha_1\approx1/5{,}367\approx0{,}1863$, $\alpha_2\approx2{,}71828/5{,}367\approx0{,}5065$, $\alpha_3\approx1{,}64872/5{,}367\approx0{,}3072$. На этот раз позиция 2 получила наибольший вес ($\approx0{,}51$) — что логично: запрос $q'$ совпадает именно с ключом $k_2$, а значит скалярное произведение $q'\cdot k_2$ максимально среди всех трёх. Этот пример наглядно показывает главное свойство attention: веса не фиксированы раз и навсегда для последовательности — они полностью пересчитываются для каждого нового запроса, и разные запросы «смотрят» на разные позиции.
Пример 3 (эффект масштабирования $\sqrt{d}$ — что было бы без него при большой размерности). Возьмём гипотетическую ситуацию с размерностью $d=100$ (типичный порядок для реальных моделей — там $d$ доходит до 64 на голову внимания и выше) и предположим, что сырой счёт совместимости для позиции 1 оказался равен $50$, а для позиций 2 и 3 — по $45$ (разница в 5 единиц на фоне суммы 100 компонент — вполне реалистичный случай без масштабирования). Без деления на $\sqrt{d}$ softmax от $(50, 45, 45)$: экспонента $e^{50}$ уже практически не помещается в обычные вычисления с плавающей точкой одинарной точности без переполнения, но даже если формально её посчитать, разница $e^{50}/e^{45}=e^5\approx148{,}4$ означает, что вес позиции 1 окажется примерно в 148 раз больше веса остальных двух позиций — softmax резко «схлопнется» почти в one-hot вектор (вектор из нулей с единственной единицей), и градиент по всем остальным весам станет исчезающе малым — учиться станет почти нечему. Теперь поделим те же сырые счета на $\sqrt{100}=10$: получим $(5, \ 4{,}5, \ 4{,}5)$ — разница всего в $0{,}5$, и после softmax это даёт заметно более «мягкое» распределение весов, где отношение между весами составляет $e^{0{,}5}\approx1{,}65$ раза, а не 148. Именно эта разница между «почти one-hot, градиент умер» и «мягкое, хорошо дифференцируемое распределение» и есть практическая причина, по которой деление на $\sqrt{d}$ — не опциональная деталь формулы, а необходимое условие устойчивого обучения при реалистичных размерностях векторов.
Почему это важно
Формула $\text{softmax}(QK^T/\sqrt{d})V$ — это не произвольная инженерная находка, а результат последовательных, объяснимых архитектурных решений: скалярное произведение даёт меру сходства запроса и ключа; softmax превращает сырые счета в интерпретируемое, суммирующееся в единицу распределение весов; деление на $\sqrt{d}$ удерживает softmax в режиме с содержательным градиентом независимо от размерности векторов; а финальное умножение на $V$ агрегирует содержательную информацию пропорционально вычисленной релевантности. Каждый из этих четырёх шагов ты теперь умеешь не просто процитировать, а посчитать вручную на конкретных числах — и именно эта формула, без каких-либо изменений в своей сути, станет ядром self-attention в трансформерах уже в следующем уроке.
Визуализация attention weights: механизм внимания как окно в чёрный ящик
Интуиция
Одно из самых практически ценных следствий механизма внимания — то, что веса $\alpha_{t,i}$, которые модель вычисляет для каждой пары «шаг генерации — позиция входа», можно сохранить и визуализировать в виде тепловой карты (heatmap): по одной оси — позиции входного предложения, по другой — позиции выходного (сгенерированного) предложения, а яркость каждой ячейки соответствует величине веса $\alpha$. До появления attention нейросети для обработки языка были практически непрозрачным «чёрным ящиком» — можно было оценить итоговое качество перевода, но невозможно было понять, на основании какой именно информации из входа модель приняла то или иное решение при генерации конкретного слова.
С attention эта прозрачность появляется почти бесплатно, как побочный продукт самого механизма, а не как отдельно спроектированный инструмент интерпретируемости. Визуализировав веса внимания для перевода предложения, можно буквально увидеть: генерируя слово «house», модель концентрирует вес на входном слове «дом»; генерируя определённый артикль или согласуя род прилагательного, модель может «смотреть» сразу на два входных слова — существительное и связанное с ним прилагательное. Более того, для языковых пар с различным порядком слов (например, японский и английский, где порядок субъект-объект-глагол отличается от привычного субъект-глагол-объект) визуализация attention наглядно показывает, как модель «переставляет» внимание, компенсируя разницу в синтаксисе, — то есть буквально показывает выученное моделью соответствие между грамматическими структурами двух языков, при том, что никто явно не программировал такое соответствие.
Формула
Матрица весов внимания как объект интерпретации. Для перевода входной последовательности длины $n$ в выходную последовательность длины $m$, вычисляемую decoder'ом с attention, получается матрица весов $A$ размера $[m \times n]$, где строка $t$ — это распределение весов $(\alpha_{t,1}, \ldots, \alpha_{t,n})$, использованное при генерации $t$-го выходного токена:
$$A_{t,i} = \alpha_{t,i}, \qquad \sum_{i=1}^{n} A_{t,i} = 1 \ \ \text{для каждой строки } t$$Визуализация этой матрицы как тепловой карты (тёмный цвет — малый вес, яркий — большой) — стандартный инструмент анализа поведения attention-моделей: диагональные или почти диагональные паттерны обычно означают монотонный, «слово за слово» перевод, а выраженные отклонения от диагонали показывают перестановку порядка слов или согласование на расстоянии.
Разбор примеров
Пример 1 (численный пример матрицы весов для короткого перевода). Пусть входное предложение — «Красная машина едет» (3 токена), а выходное — «The red car drives» (условно 4 токена, включая артикль). Гипотетическая, но реалистичная по форме матрица весов внимания (строки — выходные токены, столбцы — входные токены «Красная», «машина», «едет») могла бы выглядеть так:
| выход \ вход | Красная | машина | едет |
|---|---|---|---|
| The | 0.10 | 0.75 | 0.15 |
| red | 0.85 | 0.10 | 0.05 |
| car | 0.10 | 0.80 | 0.10 |
| drives | 0.05 | 0.15 | 0.80 |
Каждая строка суммируется в единицу (с точностью до округления). Чтение этой таблицы напрямую отвечает на вопрос «откуда модель взяла информацию»: генерируя «red», модель в основном (вес $0{,}85$) опиралась на входное слово «Красная»; генерируя «drives» — на «едет» (вес $0{,}80$). Любопытно, что артикль «The» опирается больше всего на «машина» (вес $0{,}75$), а не на «Красная», — это отражает то, что определённый артикль в английском связан с существительным, а не с прилагательным, и модель выучила эту грамматическую связь без явного указания.
Пример 2 (диагональный vs. недиагональный паттерн — что означает перестановка). Если языковая пара имеет схожий порядок слов (например, русский и английский для простых предложений), матрица весов внимания часто близка к диагональной — то есть выходной токен $t$ в основном опирается на входной токен примерно на той же позиции $i \approx t$, как в примере 1 выше, где веса концентрируются близко к «своей» диагонали с небольшим сдвигом. Но для языковых пар с другим порядком слов — скажем, немецкий с его рамочной конструкцией сказуемого в конце придаточного предложения, — матрица весов внимания демонстрирует выраженные внедиагональные пики: выходной токен, стоящий в начале перевода, может опираться на входной токен, стоящий ближе к концу исходного предложения. Именно такая недиагональная картина — прямое визуальное доказательство того, что attention действительно выполняет содержательную работу по перестройке структуры предложения, а не просто копирует порядок слов.
Пример 3 (attention как инструмент отладки, а не только красивая картинка). Представь, что модель перевода систематически ошибается в согласовании рода прилагательных при переводе на русский язык. Если посмотреть на матрицу весов внимания в моменты этих ошибок и увидеть, что вес при генерации прилагательного равномерно размазан по всем входным токенам (например, $(0{,}25,\,0{,}25,\,0{,}25,\,0{,}25)$ вместо выраженного пика на конкретном существительном), это прямо указывает на диагностируемую причину ошибки: модель не научилась чётко связывать прилагательное с существительным, к которому оно относится. Такая диагностика была бы совершенно недоступна в архитектуре без attention из урока 341, где единственный контекстный вектор в принципе не позволяет разложить вклад отдельных входных позиций — там можно оценить только итоговое качество перевода, но не заглянуть в то, «почему» модель ошиблась именно так.
Почему это важно
Интерпретируемость — не второстепенное удобство, а критически важное свойство для любой модели, которая применяется в ответственных областях: медицине, юриспруденции, финансах. Возможность посмотреть на веса внимания и увидеть, на какую часть входа модель реально опиралась, принимая конкретное решение, резко отличает attention-архитектуры от полностью непрозрачных «чёрных ящиков» предыдущего поколения. Этот же принцип визуализации внимания напрямую переносится и на self-attention в трансформерах — там точно так же можно построить матрицу весов и увидеть, какие токены последовательности «общаются» друг с другом внутри одной модели, без разделения на отдельные кодировщик и декодировщик.
Self-attention: внимание последовательности на саму себя
Интуиция
До сих пор мы разбирали attention в контексте encoder-decoder: запрос формировал декодировщик, а ключи и значения приходили от кодировщика — то есть внимание связывало две разные последовательности (вход и генерируемый выход). Self-attention (самовнимание) делает следующий концептуальный шаг: что если позволить последовательности «смотреть» на саму себя? То есть каждый токен последовательности формирует свой собственный запрос и сравнивает его с ключами всех токенов той же самой последовательности, включая самого себя, — и получает контекстный вектор, обогащённый информацией обо всех остальных токенах, с которыми он содержательно связан.
Зачем это может быть полезно даже без задачи перевода на другой язык? Возьми предложение «Банк одобрил кредит, потому что он посчитал риски приемлемыми». Слово «он» неоднозначно само по себе — грамматически оно могло бы относиться и к «банку», и, будь фраза чуть другой, к другому существительному мужского рода. Чтобы правильно понять смысл слова «он» в контексте всего предложения, модели нужно «посмотреть» на остальные слова той же самой последовательности — в первую очередь на «Банк» — и обогатить представление слова «он» информацией о том, к чему оно на самом деле относится. Self-attention делает это напрямую, вычисляя для позиции «он» запрос, сравнивая его с ключами всех остальных позиций предложения (включая «Банк», «одобрил», «кредит» и так далее) и формируя взвешенную сумму значений с весом, сконцентрированным на релевантных позициях.
Важно подчеркнуть принципиальное отличие self-attention от того attention, что мы разбирали выше применительно к encoder-decoder. Там запрос и ключи/значения приходили из разных источников: запрос — от декодировщика, ключи и значения — от кодировщика. В self-attention запрос, ключ и значение для каждого токена вычисляются из одной и той же последовательности — просто через три разные обучаемые матрицы $W_Q$, $W_K$, $W_V$, применённые к одним и тем же входным эмбеддингам. Математически формула остаётся ровно той же самой — $\text{softmax}(QK^T/\sqrt{d})V$, — меняется лишь источник данных, из которого вычисляются $Q$, $K$ и $V$.
Формула
Self-attention. Пусть входная последовательность из $n$ токенов представлена матрицей эмбеддингов $X$ размера $[n \times d_{model}]$ (по одной строке-вектору на каждый токен). Тогда, в отличие от encoder-decoder attention, все три матрицы вычисляются из одного и того же $X$:
$$Q = X W_Q, \qquad K = X W_K, \qquad V = X W_V$$где $W_Q, W_K, W_V$ — обучаемые матрицы проекций одинаковой формы $[d_{model} \times d]$. Сама формула внимания не меняется:
$$\text{SelfAttention}(X) = \text{softmax}\!\left(\frac{QK^T}{\sqrt{d}}\right) V$$Результат — матрица размера $[n \times d]$: для каждого из $n$ токенов входной последовательности получена новая, «обогащённая контекстом» версия его представления, учитывающая связи со всеми остальными токенами той же последовательности (включая самого себя, поскольку диагональные элементы $QK^T$, то есть $q_i \cdot k_i$, тоже участвуют в softmax наравне с остальными).
Разбор примеров
Пример 1 (self-attention на трёх токенах — тот же численный аппарат, что и выше, но источник Q/K/V один и тот же). Возьмём предложение из трёх токенов: «Банк», «одобрил», «он» (упрощённо, как будто «он» идёт сразу после «одобрил» для компактности примера). Предположим, что после применения матриц $W_Q$, $W_K$, $W_V$ к эмбеддингам всех трёх токенов получились следующие запросы, ключи и значения (размерность $d=4$, те же числа, что мы уже считали выше, чтобы переиспользовать арифметику): для токена «он» запрос $q_{\text{он}} = (1, 0, 1, 1)$ — тот же вектор, что и в первом примере предыдущего раздела. Ключи всех трёх токенов последовательности (включая ключ самого токена «он»): $k_{\text{Банк}} = (1, 0, 1, 0)$, $k_{\text{одобрил}} = (0, 1, 0, 1)$, $k_{\text{он}} = (1, 1, 0, 0)$. Заметь: это в точности $k_1, k_2, k_3$ из численного примера предыдущего раздела — но теперь их смысл другой: это не позиции входа отдельного кодировщика, а токены той же самой последовательности, к которой принадлежит и сам запрашивающий токен «он». Расчёт весов дословно повторяет пример 1 предыдущего раздела: $\alpha_{\text{Банк}}\approx0{,}4519$, $\alpha_{\text{одобрил}}\approx0{,}2741$, $\alpha_{\text{он}}\approx0{,}2741$. Интерпретация: токен «он», вычисляя своё обновлённое представление, уделяет почти половину внимания токену «Банк» — именно так self-attention и решает задачу разрешения кореференции (связывания местоимения с тем существительным, к которому оно относится), которую мы обсуждали в интуиции этого раздела.
Пример 2 (каждый токен формирует свой запрос — self-attention даёт n разных наборов весов для n токенов). Важно понимать: self-attention не вычисляет один набор весов на всё предложение — каждый из $n$ токенов последовательности выступает в роли «декодировщика» по очереди, формируя собственный запрос и получая собственный набор весов внимания к остальным токенам (включая себя). Для последовательности из 3 токенов получится матрица весов $3\times3$ — по одной строке весов на каждый токен-«запрашивающий». Токен «Банк», формируя свой собственный запрос, может обнаружить, что для его же обновлённого представления наиболее релевантен токен «одобрил» (глагол, действие которого совершает банк) — с совершенно другим набором весов, чем те, что мы посчитали выше для токена «он». Именно поэтому self-attention даёт полную матрицу $n \times n$ весов, а не единственную строку, — каждый токен последовательности одновременно и «спрашивает» (через свой запрос), и «отвечает» (через свой ключ и значение) на запросы всех остальных токенов.
Пример 3 (self-attention без учёта порядка токенов — чего формуле не хватает, и это мостик к трансформерам). Обрати внимание на важную особенность формулы $\text{softmax}(QK^T/\sqrt{d})V$: она сама по себе не содержит никакой информации о порядке токенов в последовательности — если переставить местами строки матрицы $X$ (то есть переставить порядок токенов входа), результат self-attention для каждого токена останется содержательно тем же самым, только тоже переставленным, — формула симметрична относительно перестановки позиций. Для рекуррентных сетей (урок 340) порядок обработки был встроен в саму архитектуру — LSTM физически обрабатывает токены один за другим, и переставить их означало бы полностью изменить вычисление. Self-attention же полностью «параллелен» по своей природе: все токены обрабатываются одновременно, независимо друг от друга по порядку вычисления, что даёт огромный выигрыш в скорости обучения на современных GPU (графических процессорах), но требует отдельного механизма, который явно сообщал бы модели о порядке токенов — это позиционное кодирование (positional encoding), с которым ты познакомишься в следующем уроке как с неотъемлемой частью архитектуры трансформера.
Почему это важно
Self-attention — это именно та идея, ради которой был написан весь сегодняшний урок. Encoder-decoder attention, который мы разбирали в предыдущих разделах, был важным историческим шагом и остаётся полезным инструментом в задачах с двумя разными последовательностями, но именно self-attention — идея о том, что последовательность может обогащать собственное представление, «смотря сама на себя», — стала концептуальным ядром архитектуры трансформера. В следующем уроке (343) ты увидишь, как self-attention, применённый параллельно в нескольких «головах» (multi-head attention) и дополненный позиционным кодированием, полностью заменяет рекуррентные слои LSTM/GRU (урок 340), убирая последовательную зависимость вычислений и позволяя обрабатывать целые последовательности параллельно, — и именно эта параллельность стала одной из ключевых причин, по которым стало возможным обучать модели того масштаба, что лежат в основе современных больших языковых моделей.
Практика: 30 заданий
Базовые задания (1–10)
Задание 1: В чём главное архитектурное отличие encoder-decoder с attention (этот урок) от базового encoder-decoder (урок 341)?
Задание 2: Почему сумма весов внимания $\alpha_{t,1}, \ldots, \alpha_{t,n}$ на каждом шаге $t$ должна равняться единице?
Задание 3: Что из себя представляют вектор запроса (query), вектор ключа (key) и вектор значения (value) в аналогии с поисковой системой?
Задание 4: Даны запрос $q=(2, 1)$ и ключ $k=(1, 3)$. Посчитай скалярное произведение $q \cdot k$.
Задание 5 (машинное обучение): Зачем в формуле attention нужно деление на $\sqrt{d}$ перед применением softmax?
Задание 6: Даны сырые счета совместимости (до softmax) $(3, 1, 1)$ для трёх позиций. Посчитай веса внимания после softmax (без деления на $\sqrt{d}$, размерность условно не учитываем).
Задание 7: Матрица запросов $Q$ имеет размер $[5, 64]$, матрица ключей $K$ — размер $[5, 64]$. Какая размерность у $QK^T$?
Задание 8: Чем self-attention принципиально отличается от encoder-decoder attention с точки зрения источника Q, K и V?
Задание 9 (машинное обучение): В предложении «Собака побежала за мячом, потому что он укатился» — к какому слову должен «привязаться» self-attention для слова «он»?
Задание 10: Правда или ложь: формула $\text{softmax}(QK^T/\sqrt{d})V$ сама по себе учитывает порядок токенов во входной последовательности.
Средние задания (11–20)
Задание 11: Дан запрос $q=(1,1,0,0)$ и три ключа: $k_1=(1,0,0,0)$, $k_2=(0,1,0,0)$, $k_3=(1,1,0,0)$. Посчитай все три скалярных произведения.
Задание 12: Используя сырые счета из задания 11, размерность $d=4$, посчитай веса внимания после деления на $\sqrt{d}$ и применения softmax.
Задание 13: Продолжая задание 12: значения $v_1=(1,0,0,0)$, $v_2=(0,1,0,0)$, $v_3=(1,1,1,1)$. Посчитай итоговый контекстный вектор $c$.
Задание 14: Почему в self-attention диагональные элементы матрицы $QK^T$ (то есть $q_i \cdot k_i$ для одного и того же токена $i$) участвуют в softmax наравне с остальными, а не исключаются из рассмотрения?
Задание 15 (машинное обучение): Реализуй на NumPy функцию, вычисляющую $\text{Attention}(Q,K,V) = \text{softmax}(QK^T/\sqrt{d})V$.
import numpy as np
def attention(Q, K, V):
# твой код здесь
pass
Задание 16: Проверь на примере: сырые счета $(10, 10, 10)$ для трёх позиций. Посчитай веса внимания после softmax. Что это говорит о содержательном смысле равных счетов?
Задание 17: Матрица весов внимания декодировщика для перевода 3-словного входа в 3-словный выход имеет вид: строка 1 — $(0{,}9,\,0{,}05,\,0{,}05)$, строка 2 — $(0{,}05,\,0{,}9,\,0{,}05)$, строка 3 — $(0{,}05,\,0{,}05,\,0{,}9)$. Что означает такой паттерн?
Задание 18 (машинное обучение): Даны эмбеддинги трёх токенов последовательности одинаковой размерности. Объясни, почему для self-attention нужны три отдельные обучаемые матрицы $W_Q, W_K, W_V$, а не достаточно использовать сами эмбеддинги напрямую как Q, K и V одновременно.
Задание 19: Запрос $q=(0,0,1,1)$, размерность $d=4$. Ключи: $k_1=(1,1,0,0)$, $k_2=(0,0,1,1)$. Посчитай веса внимания после softmax (без учёта $\sqrt{d}$ отдельно — раздели сначала).
Задание 20 (машинное обучение): Почему self-attention даёт матрицу весов размера $n \times n$ для последовательности из $n$ токенов, тогда как encoder-decoder attention с одним запросом на шаге $t$ даёт только одну строку весов размера $n$?
Продвинутые задания (21–30)
Задание 21: Четыре токена последовательности с ключами $k_1=(1,0)$, $k_2=(0,1)$, $k_3=(-1,0)$, $k_4=(0,-1)$ и значениями $v_1=(1,1)$, $v_2=(2,2)$, $v_3=(3,3)$, $v_4=(4,4)$. Запрос $q=(1,0)$, $d=2$. Посчитай контекстный вектор полностью.
Задание 22 (машинное обучение): В задании 21 позиция 1 (ключ $k_1$) получила наибольший вес ($\approx0{,}449$), хотя её значение $v_1=(1,1)$ — наименьшее по модулю среди всех четырёх. Объясни, почему это не противоречие.
Задание 23: Дана матрица $X$ трёх токенов (размерность эмбеддинга 2): $x_1=(1,0)$, $x_2=(0,1)$, $x_3=(1,1)$. Матрицы проекций (размер $2\times2$, единичные для простоты): $W_Q=W_K=W_V=I$ (единичная матрица). Посчитай $Q$, $K$, $V$.
Задание 24 (машинное обучение): Используя результат задания 23, посчитай self-attention для токена $x_1=(1,0)$ (то есть для запроса $q_1=(1,0)$), $d=2$.
Задание 25: Почему self-attention особенно эффективен для параллельных вычислений на GPU по сравнению с LSTM (урок 340), и как это связано с матричной формой $QK^T$?
Задание 26 (машинное обучение): Пять позиций кодировщика с сырыми счетами совместимости (до деления на $\sqrt{d}$) $(1, 1, 1, 1, 100)$. При $d=64$ посчитай веса внимания после softmax и сделай вывод об эффекте деления на $\sqrt{d}$ при экстремальном разбросе счетов.
Задание 27: Реализуй self-attention на NumPy целиком: дана матрица эмбеддингов $X$ и три случайные матрицы проекций.
import numpy as np
def self_attention(X, W_Q, W_K, W_V):
# твой код здесь
pass
np.random.seed(0)
X = np.random.randn(3, 4) # 3 токена, размерность эмбеддинга 4
W_Q = np.random.randn(4, 4)
W_K = np.random.randn(4, 4)
W_V = np.random.randn(4, 4)
result = self_attention(X, W_Q, W_K, W_V)
print(result.shape) # должно быть (3, 4)
Задание 28 (машинное обучение): Предложение «Учитель похвалил ученика, потому что он хорошо подготовился» неоднозначно: «он» может относиться и к учителю, и к ученику, но по смыслу — к ученику. Объясни, как self-attention в принципе способен научиться разрешать такую неоднозначность, если она не разрешима чисто грамматически (оба существительных — мужского рода).
Задание 29: Даны два токена с ключами $k_1=(3,4)$ и $k_2=(4,3)$ (обрати внимание: оба имеют одинаковую норму $\sqrt{3^2+4^2}=5$) и запрос $q=(1,0)$. Посчитай веса внимания после softmax при $d=2$ и объясни, почему они не равны, несмотря на равную норму ключей.
Задание 30 (машинное обучение): Сформулируй, почему self-attention считается прямой концептуальной подготовкой к архитектуре трансформера (урок 343), а не просто ещё одной вариацией механизма внимания.
Частые ошибки
-
Путают attention weights с «важностью слова самого по себе». Веса $\alpha_{t,i}$ — это релевантность позиции $i$ относительно конкретного запроса на конкретном шаге $t$, а не абсолютная, вневременная «важность» слова во всём предложении. Один и тот же токен может получить высокий вес на одном шаге генерации и низкий — на другом, что явно продемонстрировано примерами 1 и 2 в разделе про вывод формулы, где один и тот же набор ключей давал совершенно разные веса для разных запросов.
-
Забывают, что деление на $\sqrt{d}$ — не опциональная деталь, а необходимое условие устойчивого обучения при больших размерностях. Как показано в задании 26, деление на $\sqrt{d}$ смягчает эффект роста дисперсии скалярных произведений с размерностью $d$, но не решает проблему content-driven (содержательно большого) разброса между самими сырыми счетами — важно понимать, что нормализация решает конкретную, узко очерченную проблему, а не любую нестабильность softmax в принципе.
-
Считают, что ключ и значение — это один и тот же вектор, просто под разными именами. Ключ и значение вычисляются из одного и того же скрытого состояния или эмбеддинга через разные обучаемые матрицы $W_K$ и $W_V$ и решают принципиально разные задачи: ключ отвечает за сравнение на релевантность, значение — за содержательную выдачу, если релевантность подтвердилась (см. задание 22, где вес и величина значения оказались никак не связаны).
-
Путают encoder-decoder attention и self-attention как один и тот же механизм без различий. Формула $\text{softmax}(QK^T/\sqrt d)V$ действительно одна и та же в обоих случаях, но источник Q, K и V принципиально разный: в encoder-decoder attention Q приходит от декодировщика, а K и V — от кодировщика (две разные последовательности); в self-attention все три матрицы вычисляются из одной и той же последовательности.
-
Считают, что self-attention автоматически «знает» порядок токенов, потому что видит их все одновременно. Как разобрано в задании 10 и в примере 3 раздела о self-attention, формула симметрична относительно перестановки позиций входа — сама по себе она не кодирует порядок, и без дополнительного механизма (позиционного кодирования, урок 343) переставленная последовательность дала бы содержательно тот же самый, только тоже переставленный результат.
-
Игнорируют, что визуализация attention weights — не гарантированное, «идеальное» объяснение решения модели, а лишь один из инструментов интерпретации. Высокий вес внимания на конкретной позиции показывает, что модель использовала информацию оттуда при формировании контекстного вектора, но не доказывает с абсолютной строгостью, что именно эта информация была единственной причиной итогового предсказания — attention weights дают ценную, но не исчерпывающую картину внутреннего устройства модели.
Главное запомнить
-
Attention mechanism решает проблему бутылочного горлышка encoder-decoder архитектуры (урок 341): вместо одного статического контекстного вектора на всю последовательность декодировщик на каждом шаге генерации заново вычисляет свой собственный контекстный вектор как взвешенную сумму всех скрытых состояний кодировщика.
-
Механизм внимания строится вокруг трёх ролей: запрос (query) — «что мне сейчас нужно», ключ (key) — «насколько эта позиция релевантна запросу», значение (value) — «что содержательно выдать, если релевантность подтвердилась»; ключ и значение — разные проекции одного и того же входа через разные обучаемые матрицы $W_K$ и $W_V$.
-
Полная формула внимания: $\text{Attention}(Q,K,V) = \text{softmax}(QK^T/\sqrt{d})V$ — сначала скалярные произведения запроса с ключами дают меру совместимости, затем деление на $\sqrt{d}$ обеспечивает численную стабильность softmax, затем softmax превращает счета в распределение весов, суммирующееся в единицу, а умножение на $V$ даёт взвешенную сумму значений.
-
Деление на $\sqrt{d}$ необходимо потому, что скалярное произведение $q\cdot k$ — сумма $d$ слагаемых, чья дисперсия растёт линейно с размерностью $d$, а без нормализации при больших $d$ softmax «схлопывается» почти в one-hot вектор и градиент затухает — эффект, аналогичный насыщению сигмоиды (урок 329).
-
Веса внимания можно визуализировать как тепловую карту (матрица весов $[m \times n]$), что даёт прямую интерпретируемость: буквально видно, на какое слово входа «смотрит» модель при генерации каждого слова выхода — недиагональные паттерны показывают перестановку порядка слов между языками, а размазанные веса могут указывать на ошибки модели.
-
Self-attention (самовнимание) — внимание последовательности на саму себя: каждый токен формирует собственный запрос и сравнивает его с ключами всех токенов той же последовательности, включая самого себя, получая обогащённое контекстом представление; формула остаётся той же самой, меняется только источник Q, K, V — все три матрицы вычисляются из одного и того же входа.
-
Self-attention для последовательности из $n$ токенов даёт полную матрицу весов $n \times n$ (в отличие от одной строки весов на шаг в encoder-decoder attention), поскольку каждый токен одновременно выступает и «запрашивающим», и «отвечающим» на запросы всех остальных.
-
Self-attention симметричен относительно перестановки позиций входа и сам по себе не кодирует порядок токенов — в отличие от LSTM (урок 340), где порядок обработки встроен в саму рекуррентную структуру; это требует отдельного механизма позиционного кодирования, с которым ты познакомишься в следующем уроке.
-
Отсутствие последовательной зависимости в self-attention (все токены обрабатываются параллельно через одну матричную операцию $QK^T$) даёт огромный выигрыш в скорости вычислений на GPU по сравнению с пошаговой обработкой LSTM/GRU — это одна из ключевых причин, по которым архитектуры на основе self-attention смогли масштабироваться до размеров современных больших языковых моделей.
-
Self-attention — прямая концептуальная подготовка к архитектуре трансформера (урок 343): трансформер строится буквально как последовательность слоёв self-attention (в многоголовом варианте, multi-head attention), полностью заменяя рекуррентные слои, а формула $\text{softmax}(QK^T/\sqrt d)V$, разобранная сегодня, остаётся неизменной основой всех современных больших языковых моделей — от BERT и GPT до Claude и LLaMA.
Связь с темами курса
Сегодняшний урок напрямую продолжает урок 341, где ты разобрал базовую encoder-decoder архитектуру и её фундаментальное ограничение — единственный контекстный вектор фиксированного размера как узкое место для всей последовательности. Attention mechanism решает это ограничение не косметически, а структурно: вместо статического сжатия всей информации в один вектор декодировщик получает возможность на каждом шаге заново обращаться ко всем скрытым состояниям кодировщика. Формула, которую ты сегодня вывел с нуля и разобрал на нескольких полных численных примерах — $\text{softmax}(QK^T/\sqrt d)V$, — опирается на функцию softmax, уже знакомую тебе по многоклассовой классификации, и на понятие скалярного произведения как меры сходства векторов, а обсуждение затухания градиента при плохо масштабированном softmax напрямую перекликается с проблемой насыщения функций активации (урок 329) и затухающего градиента, которую в контексте LSTM (урок 340) решают вентили, а в контексте ResNet (урок 337) — skip connections.
Раздел про self-attention — прямой мостик к следующему уроку (343), где архитектура трансформера будет построена буквально вокруг идеи, разобранной сегодня: несколько параллельных self-attention (multi-head attention), дополненных позиционным кодированием для компенсации отсутствия встроенного понятия порядка токенов, полностью заменяют рекуррентные слои LSTM/GRU. Урок 344 пойдёт ещё дальше и покажет, как self-attention лежит в основе BERT (использующего только кодировщик трансформера) и GPT (использующего только декодировщик), а также современных больших языковых моделей в целом — то есть практически всей современной практической работы с текстом, которую ты встретишь в реальных Data Science проектах.
Интересные факты
-
Статья Бахданау, Чо и Бенджио 2014–2015 годов, впервые предложившая attention mechanism для машинного перевода, не использовала слово «attention» в самом распространённом сегодня смысле «единого блока архитектуры» — механизм был встроен именно как решение конкретной проблемы качества перевода на длинных предложениях, и лишь позже, с распространением идеи, за ним закрепилось это ставшее теперь повсеместным название.
-
Название статьи 2017 года «Attention Is All You Need» («Внимание — это всё, что нужно») — намеренно провокационная формулировка: на момент публикации подавляющее большинство архитектур для последовательностей опирались на рекуррентные или свёрточные слои, а идея о том, что можно полностью выбросить и то и другое, оставив только механизм внимания, казалась многим исследователям слишком радикальной, пока не была подтверждена результатами.
-
Визуализация attention weights в ранних работах по машинному переводу с attention использовалась не только как красивая иллюстрация в статьях, но и как практический инструмент отладки: исследователи буквально просматривали тепловые карты весов для проблемных переводов, чтобы понять, на какую (неправильную) часть входа модель опиралась, и это стало одним из первых массово используемых инструментов интерпретируемости в области глубокого обучения для текста.
-
Идея self-attention — внимания последовательности на саму себя — оказалась настолько универсальной, что вышла далеко за пределы обработки текста: архитектура Vision Transformer (ViT) применяет ровно тот же механизм self-attention к фрагментам («патчам») изображений вместо токенов текста, показав, что задачи компьютерного зрения тоже можно эффективно решать без единой свёртки — вопреки десятилетиям доминирования CNN-архитектур (уроки 335–338).
-
Сложность self-attention растёт квадратично с длиной последовательности $n$ (поскольку матрица весов имеет размер $n \times n$), и именно эта квадратичная сложность стала предметом отдельного активного направления исследований — разреженного внимания (sparse attention), линейного внимания (linear attention) и других приёмов, ищущих способы сохранить выразительность self-attention для очень длинных последовательностей (тысячи и десятки тысяч токенов) без квадратичного роста вычислений.
Лайфхаки
-
Когда встречаешь новую архитектуру, использующую слово «attention», в первую очередь задай себе вопрос из этого урока: откуда берутся Q, K и V — из одной последовательности (self-attention) или из двух разных (encoder-decoder / cross-attention)? Этот единственный вопрос сразу проясняет структурную роль механизма в конкретной архитектуре.
-
Если формула $\text{softmax}(QK^T/\sqrt d)V$ кажется абстрактной, возвращайся к численному примеру этого урока (3 токена, размерность 4) и пересчитывай её вручную с другими числами — механическое повторение расчёта на конкретных цифрах закрепляет понимание формулы значительно надёжнее, чем многократное чтение символьной записи.
-
Работая с готовыми библиотеками (например,
torch.nn.MultiheadAttentionв PyTorch), не поленись хотя бы один раз реализовать базовый attention с нуля на NumPy, как в заданиях 15 и 27 этого урока, — это единственный надёжный способ убедиться, что понимание формулы не осталось поверхностным, «на уровне картинки», а закрепилось на уровне конкретных вычислений. -
Если анализируешь поведение обученной языковой модели и подозреваешь конкретную ошибку (неправильное согласование, неверное разрешение местоимения), первым делом попробуй визуализировать веса внимания для проблемного момента — как показано в задании 17 и разделе про интерпретируемость, размазанные или явно «неправильные» веса часто прямо указывают на источник ошибки.
-
Не путай два родственных, но разных навыка: умение процитировать формулу $\text{softmax}(QK^T/\sqrt d)V$ и умение объяснить, зачем нужен каждый из четырёх её элементов (скалярное произведение, деление на $\sqrt d$, softmax, умножение на V). Экзаменационные и рабочие вопросы по этой теме почти всегда проверяют именно второе — способность объяснить «зачем», а не просто воспроизвести формулу по памяти.
-
Перед тем как переходить к следующему уроку про трансформеры, убедись, что ты можешь без подглядывания объяснить своими словами разницу между encoder-decoder attention и self-attention — это ключевое различие, вокруг которого строится весь следующий урок, и путаница в нём на старте сделает понимание трансформера заметно труднее.
Ты только что разобрал, возможно, самый важный отдельный механизм за весь курс — не потому что он сложнее остальных (базовая формула attention на самом деле проще, чем, скажем, полный вывод обратного распространения ошибки из урока 330), а потому что он лежит в фундаменте буквально каждой современной большой языковой модели, с которой ты когда-либо взаимодействовал или будешь взаимодействовать в своей карьере в Data Science. От GPT и BERT до Claude и LLaMA — все они, при всех архитектурных различиях в деталях, обучении и масштабе, опираются на одну и ту же операцию, которую ты сегодня вывел с нуля и посчитал вручную на конкретных числах: взвешенную сумму значений, где веса определяются через сравнение запроса с ключами и нормализуются функцией softmax с масштабированием на $\sqrt{d}$. В следующем уроке эта идея, применённая параллельно в нескольких головах внимания и дополненная позиционным кодированием, соберётся в полноценную архитектуру трансформера — и ты придёшь к ней уже не как к незнакомой магии, а как к логичному развитию механизма, который ты только что полностью освоил.
Понял тему? Закрепи в боте! 🚀
Попрактикуйся на задачах и получи персональные рекомендации от AI
💪 Начать тренировку