Внимание интуитивно
Почему перевод через один вектор «душит» длинные предложения и как внимание превращает поиск по словарю в мягкую обучаемую операцию — от идеи Богданова (2014) до трансформера (2017).
Зачем это нужно
Заголовок раздела «Зачем это нужно»Внутри любой большая языковая модельбольшая языковая модель (БЯМ, LLM)EN large language model (LLM)В китайском «большая модель» («да мосин») — разговорное общее название для LLM и вообще foundation models. работают десятки слоёв вниманиямеханизм вниманияEN attention. У DeepSeek-V3 их 61, и в каждом 128 «голов» внимания (что такое головы — в уроке 3). У Qwen3-235B-A22B — 94 слоя по 64 головы. Всё остальное в трансформере (нормализации, полносвязные слои) обрабатывает каждый токен по отдельности. Внимание — единственное место, где токены «разговаривают» друг с другом.
Зачем им разговаривать? В уроке 4.3 каждый токен получил свой эмбеддинг, векторное представлениеэмбеддинг, векторное представлениеEN embedding. Но этот вектор не знает соседей: «ключ» в «ключ от двери» и в «скрипичный ключ» получает один и тот же вектор. Это проблема многозначностимногозначность (полисемия)EN polysemyОдно слово с несколькими значениями («ключ», «коса», «лук»). Статический эмбеддинг даёт им один вектор на все значения.. А среднее таких векторов, как мы видели в уроке 4.3.2, ещё и не замечает порядка слов. Чтобы понять смысл слова, модель должна посмотреть на контекст и подмешать к вектору информацию из нужных соседей. Это и делает внимание.
В этом уроке формул с матрицами почти не будет. Разберёмся, откуда взялась идея и почему внимание — это «мягкий поиск по словарю». В следующем уроке превратим интуицию в формулу и реализуем её, а в третьем — сделаем многоголовой.
Перевод через бутылочное горлышко
Заголовок раздела «Перевод через бутылочное горлышко»Как был устроен нейросетевой перевод в 2014 году
Заголовок раздела «Как был устроен нейросетевой перевод в 2014 году»Внимание в нынешнем виде придумали не для чат-ботов, а для машинного переводамашинный переводEN machine translation. В 2014 году две группы (Суцкевер, Виньялс и Ле из Google и группа Кёнхёна Чо из Монреаля) предложили схему seq2seqseq2seq (последовательность в последовательность)EN sequence-to-sequence (seq2seq)Схема «энкодер читает вход, декодер генерирует выход» (Sutskever и др.; Cho и др., 2014). Без внимания всё входное предложение сжимается в один вектор контекста., «последовательность в последовательность». В ней две рекуррентные сетирекуррентная нейросетьEN recurrent neural network (RNN) (о них — в уроке 3.4.3):
- Энкодер читает исходное предложение по одному токену и обновляет своё скрытое состояние . Последнее состояние считается «смыслом всего предложения». Обозначим его .
- Декодер — это языковая модель из урока 4.1.1, которой дали подсказку . Она генерирует перевод токен за токеном, пока не выдаст «конец предложения».
Такую модель вы сами обучали для транслитерации в уроке 4.1.5.
flowchart LR A["Кошка"] --> H1["h1"] B["сидит"] --> H2["h2"] C["на"] --> H3["h3"] D["ковре"] --> H4["h4"] H1 --> H2 --> H3 --> H4 H4 ==>|"один вектор c"| E["Декодер"] E --> O["The cat sits on the rug"]
Где тесно
Заголовок раздела «Где тесно»Всё, что декодер знает об исходной фразе, должно пройти через вектор фиксированного размера: в базовой модели без внимания из статьи Bahdanau и соавторов это 1000 чисел. Фраза из трёх слов и абзац из шестидесяти сжимаются в одинаковое количество чисел. Представьте, что вас просят пересказать главу «Войны и мира» одной SMS, а потом перевести главу на китайский, глядя только в эту SMS. Короткую фразу вы переведёте, длинную — нет.
Так и вышло на практике. Качество перевода обычных encoder–decoder-моделей падало с ростом длины предложения. Симптомы проблемы видны и в статье Суцкевера: если перевернуть исходное предложение (подавать «ковре на сидит Кошка»), качество заметно растёт. Начало фразы оказывается рядом с началом перевода, и информации о нём проще «дожить» до декодера. Такой трюк лечит симптом, но не причину: узким местом остаётся вектор .
Идея: пусть декодер сам ищет нужное слово
Заголовок раздела «Идея: пусть декодер сам ищет нужное слово»Решение придумал Дмитрий Богданов (Dzmitry Bahdanau): выпускник Белорусского государственного университета, в 2014 году стажёр в лаборатории Йошуа Бенджио в Монреале. Позже он рассказал Андрею Карпатому, откуда пришла идея: из школьных упражнений по переводу. Когда человек переводит, его взгляд бегает между исходным текстом и переводом. Он не держит всю фразу в голове, а для каждого нового слова заново находит нужное место в оригинале.
Отсюда идея: не сжимать предложение в один вектор, а сохранить векторы всех слов . На каждом шаге декодер сам решает, какие из них нужны прямо сейчас. Энкодер у Bahdanau двунаправленный: одна RNN читает фразу слева направо, другая справа налево, и склеивает оба состояния. Поэтому описывает слово вместе с его окружением с обеих сторон. В статье модель называлась RNNsearch («поиск»), а слово «внимание» (attention) появилось благодаря Бенджио — в абзаце, который объяснял интуицию механизма.
На шаге декодер делает четыре действия:
- Оценивает каждое слово источника: , где — текущее состояние декодера. Получается число «насколько слово важно сейчас».
- Нормирует оценки через softmax (урок 1.3.2): . Это веса вниманиявеса вниманияEN attention weightsРезультат softmax от оценок внимания. Неотрицательны, в сумме по ключам дают 1; показывают, «куда смотрит» запрос.: все положительные, в сумме 1.
- Усредняет: вектор контекста . Теперь он свой для каждого шага.
- Генерирует следующее слово, глядя на .
Функция оценки у Bahdanau — маленькая нейросеть, её называют аддитивным вниманиемаддитивное внимание (внимание Bahdanau)EN additive attention (Bahdanau attention)Оценка сходства маленькой сетью v^T tanh(W q + U k). Предложено Bahdanau, Cho, Bengio (2014) для перевода.:
Что здесь написано: оба вектора переводятся матрицами и в общее пространство, складываются, проходят через , и вектор сворачивает результат в одно число. Важная деталь: никто не говорит модели, на какое слово смотреть. Всё обучается вместе с переводом, обычным обратным распространением ошибки. Модель сама выясняет, что для слова «cat» полезно смотреть на «Кошка».
Считать аддитивную оценку руками не будем. Уже через год Луонг и соавторы показали, что не хуже работает простое скалярное произведение состояний декодера и энкодера. Его и использует трансформер, и его мы посчитаем ниже.
Что получилось
Заголовок раздела «Что получилось»Если нарисовать веса таблицей (строки — слова перевода, столбцы — слова источника), получится выравниваниевыравнивание слов (в переводе)EN word alignmentСоответствие «слово перевода ↔ слово оригинала». Модель с вниманием выучивает его сама — его видно в таблице весов внимания.: какое слово перевода «смотрит» на какое слово оригинала. Для английского и французского таблица почти диагональная, но там, где порядок слов разный, диагональ ломается. Знаменитый пример из статьи: «European Economic Area» → «zone économique européenne», прилагательные идут в обратном порядке, и модель это выучила сама. Отсюда название статьи: «совместно учимся выравнивать и переводить».
Цифры из статьи (англо-французский перевод WMT’14, предложения без неизвестных слов): модель без внимания RNNencdec-50 — 26.71 BLEU, с вниманием RNNsearch-50 — 34.16 BLEU. И главное: у модели с вниманием качество не проседало даже на предложениях из 50 слов и длиннее.
Посмотрим на выравнивание на игрушечном примере с английским: в этой фразе слово, которое в русском стоит последним, в английском идёт первым. «Новая книга брата» по-английски — «brother’s new book», дословно «брата · (притяжательный показатель) · новая · книга». Веса внимания посчитаны кодом из раздела «Практика» (векторы придуманы вручную, в настоящей модели они обучаются):
| Шаг перевода | «новая» | «книга» | «брата» |
|---|---|---|---|
brother (брат) | 0.05 | 0.05 | 0.91 |
's (показатель «чей») | 0.05 | 0.05 | 0.91 |
new (новая) | 0.91 | 0.05 | 0.05 |
book (книга) | 0.05 | 0.91 | 0.05 |
Первое слово перевода смотрит на последнее слово источника. Модели без внимания пришлось бы «помнить» слово «брата» внутри вектора , а с вниманием она просто находит его, когда оно нужно.
Внимание как мягкий поиск по словарю
Заголовок раздела «Внимание как мягкий поиск по словарю»Теперь отвлечёмся от перевода. Внимание — это общий способ доставать информацию по сходству. Проще всего понять его через обычный словарь Python.
Жёсткий поиск
Заголовок раздела «Жёсткий поиск»d = {"кот": 4, "собака": 20, "рыба": 1} # ключ -> значение (масса, кг)d["кот"] # 4Посмотрим на это как на сравнение запроса «кот» с каждым ключом: совпал — вес 1, не совпал — вес 0, и возвращается значение с весом 1. (На самом деле словарь находит ключ по хешу, не перебирая все ключи, но результат тот же.) У такого поиска два недостатка для нейросети:
- Только точное совпадение. Запрос «котёнок» даст
KeyError, хотя «кот» явно ближе, чем «рыба». - Нельзя обучать. Если чуть-чуть изменить запрос, ответ либо не изменится, либо скачком станет другим. Производной нет, градиентный спуск не работает.
Мягкий поиск
Заголовок раздела «Мягкий поиск»Сделаем всё «гладким». Ключи и запрос теперь векторы, у каждого ключа есть значение :
- Считаем сходство запроса с каждым ключом: . Самый простой вариант — скалярное произведение (урок 1.1.1): оно большое, когда векторы смотрят в одну сторону.
- Превращаем сходства в веса: .
- Возвращаем взвешенное среднее значений:
Что здесь написано: ответ — это смесь всех значений; чем больше ключ похож на запрос, тем больше вклад его значения. Сумма весов равна 1, поэтому ответ всегда лежит «между» значениями. Это запрос, ключ, значениезапрос, ключ, значение (Q, K, V)EN query, key, value (QKV)Три роли во внимании. Запрос сравнивается с ключами, по сходству получаются веса, ответ — взвешенная сумма значений. В трансформере все три получаются из токена обучаемыми матрицами W_Q, W_K, W_V., три роли, на которых построен весь трансформер. (Слово «ключ» из примера про многозначность получило ещё одно значение.)
Числовой пример. Ключи — двумерные векторы: «кот» , «собака» , «рыба» . Значения — масса: 4, 20 и 1 кг. Запрос похож на «кота», а множитель задаёт «резкость» поиска. Оценки :
| вес «кот» | вес «собака» | вес «рыба» | ответ, кг | |
|---|---|---|---|---|
| 0 | 0.333 | 0.333 | 0.333 | 8.33 — просто среднее |
| 1 | 0.457 | 0.374 | 0.168 | 9.49 |
| 5 | 0.727 | 0.268 | 0.005 | 8.27 |
| 50 | 1.000 | 0.000 | 0.000 | 4.00 — как d["кот"] |
Мягкий поиск — это непрерывная шкала от «усредни всё подряд» до «точное совпадение ключа». Множитель работает как обратная температуратемпература (сэмплирования)EN temperatureЛогиты делятся на T перед softmax. T < 1 — распределение острее, T > 1 — площе, T → 0 — greedy. из урока о сэмплировании. Нейросеть сама выучивает, насколько резко искать: для этого ей достаточно сделать векторы запросов и ключей длиннее или короче. Почему длину векторов при этом нужно держать под контролем, вы увидите в следующем уроке (деление на ).
Внимание старше нейросетей
Заголовок раздела «Внимание старше нейросетей»В 1964 году статистики Э. Надарая и Дж. Ватсон независимо предложили ядерную регрессиюядерная регрессия Надарая–ВатсонаEN Nadaraya–Watson kernel regressionСтатистический метод 1964 года — предсказание как взвешенное среднее известных ответов с весами по близости. Необучаемый прообраз внимания; с гауссовым ядром веса — это softmax.. Чтобы предсказать в точке , нужно взять известные точки и усреднить их с весами, которые тем больше, чем ближе к :
С гауссовым ядром веса — это ровно softmax от оценок . Запрос — , ключи — , значения — . Китайское издание d2l разбирает этот пример сразу после введения в главу о внимании (раздел 10.2). Отличие от современного внимания одно, но важное: здесь ничего не обучается. В нейросети запросы, ключи и значения получаются обучаемыми преобразованиями, и модель сама решает, что считать «похожим».
Откуда берутся запросы, ключи и значения
Заголовок раздела «Откуда берутся запросы, ключи и значения»Между двумя последовательностями. У Bahdanau запрос — состояние декодера (переводимого предложения), ключи и значения — состояния энкодера (исходного). Такой вариант называют перекрёстным вниманиемперекрёстное внимание (cross-attention)EN cross-attentionЗапросы из одной последовательности, ключи и значения — из другой (декодер смотрит на выход энкодера). Ср. self-attention, где все три из одной последовательности.: «спрашивает» одна последовательность, «отвечает» другая.
Внутри одной последовательности. Если запросы, ключи и значения берутся из одного и того же текста, получается самовниманиесамовниманиеEN self-attention (self-attention): каждый токен «спрашивает» остальные токены своего же предложения. Классический пример:
- «Кошка не перепрыгнула через забор, потому что она устала» — «она» должна смотреть на «кошку».
- «Кошка не перепрыгнула через забор, потому что он был слишком высоким» — «он» смотрит на «забор».
В русском подсказывает род. В английском и «она», и «он» становятся одним словом it: «The cat didn’t jump over the fence because it was tired» и «…because it was too high». Модель должна понять по смыслу: устать может кошка, высоким бывает забор. После слоя self-attention вектор токена it уже содержит информацию о нужном существительном. Так внимание решает проблему многозначности, с которой мы начали.
В трансформере каждый токен играет все три роли сразу: из его эмбеддинга тремя обучаемыми матрицами , , получаются запрос, ключ и значение. Как именно — в следующем уроке.
От Bahdanau к трансформеру: 2014 → 2017
Заголовок раздела «От Bahdanau к трансформеру: 2014 → 2017»| Год | Работа | Что нового |
|---|---|---|
| 1964 | Надарая; Ватсон | Ядерная регрессия — взвешенное среднее по сходству, прообраз внимания |
| 2014 | Sutskever, Vinyals, Le; Cho и соавт. | seq2seq: энкодер и декодер на RNN, один вектор контекста |
| 2014 | Bahdanau, Cho, Bengio | Аддитивное внимание: декодер ищет по всем состояниям энкодера |
| 2015 | Luong, Pham, Manning | Внимание через скалярное произведение — проще и быстрее аддитивного |
| 2015 | Xu и соавт., «Show, Attend and Tell» | Внимание к областям картинки при генерации подписи |
| 2016 | Cheng, Dong, Lapata | Внимание внутри одной последовательности (intra-attention) — прообраз self-attention |
| 2016 | Wu и соавт., Google GNMT | Перевод на LSTM с вниманием в продакшене (в работающем сервисе) Google Translate |
| 2017 | Lin и соавт. | Эмбеддинг предложения из нескольких «взглядов» self-attention — прообраз многоголовости |
| 2017 | Vaswani и соавт., «Attention Is All You Need» | Трансформер: только внимание, без рекуррентности |
Почему трансформер победил
Заголовок раздела «Почему трансформер победил»У RNN две беды. Первая — последовательность: состояние нельзя посчитать, пока не готово . Текст из 1000 токенов — это 1000 шагов подряд, и тысячи ядер видеокарты в основном ждут. Вторая — длинный путь: информация от первого слова к тысячному проходит через 999 преобразований и по дороге затухает. С градиентами то же самое: это затухание во времени из урока 3.4.3.
Авторы трансформера задали вопрос: а если оставить только внимание? Тогда каждый токен смотрит на все остальные напрямую. Путь между любыми двумя словами — один шаг, а все позиции считаются одновременно большими матричными умножениями, которые видеокарты любят больше всего.
flowchart LR subgraph R["RNN: цепочка, шаги по очереди"] r1["x1"] --> r2["x2"] --> r3["x3"] --> r4["x4"] end subgraph S["Self-attention: каждый с каждым, все сразу"] a1["x1"] --- a2["x2"] a1 --- a3["x3"] a1 --- a4["x4"] a2 --- a3 a2 --- a4 a3 --- a4 end
Результат статьи: базовая модель обучалась 12 часов на 8 GPU P100, а большая версия установила рекорд англо-немецкого перевода WMT 2014 — 28.4 BLEU. Через год на этой архитектуре построили GPT и BERT, ещё через несколько лет — все современные LLM. У подхода есть цена: если каждый токен сравнивается с каждым, то на токенов нужно сравнений. Об этой квадратичной стоимости — в уроке 3.
Практика: мягкий поиск руками
Заголовок раздела «Практика: мягкий поиск руками»Запустите локально (python soft_lookup.py). Нужен только NumPy.
import numpy as np
np.set_printoptions(precision=2, suppress=True)
def softmax(z): """Устойчивый softmax по последней оси (урок 1.3).""" z = z - z.max(axis=-1, keepdims=True) e = np.exp(z) return e / e.sum(axis=-1, keepdims=True)
def soft_lookup(q, K, V): """Мягкий поиск: сходство запроса с ключами -> веса -> взвешенное среднее значений.""" scores = K @ q # (n,) скалярное произведение запроса с каждым ключом w = softmax(scores) # (n,) веса, сумма = 1 return w @ V, w # w @ V = сумма w_i * v_i — взвешенное среднее значений
# 1) Мягкий словарь: от «среднего» до «точного совпадения»K = np.array([[1.0, 0.0], # «кот» [0.8, 0.6], # «собака» [0.0, 1.0]]) # «рыба»V = np.array([4.0, 20.0, 1.0]) # значения: масса, кгfor beta in [0.0, 1.0, 5.0, 50.0]: out, w = soft_lookup(beta * np.array([1.0, 0.0]), K, V) print(f"beta={beta:4.0f} веса={w} ответ={out:.2f}")
# 2) Выравнивание «новая книга брата» -> «brother's new book»src = ["новая", "книга", "брата"]# признаки: [свойство, предмет, человек, родительный падеж]H = np.array([[1, 0, 0, 0], # новая [0, 1, 0, 0], # книга [0, 0, 1, 1]], # брата dtype=float) # состояния энкодера: и ключи, и значенияtgt = ["brother", "'s", "new", "book"]Q = np.array([[0, 0, 3, 0], # brother ищет «человека» [0, 0, 0, 3], # 's ищет родительный падеж [3, 0, 0, 0], # new ищет свойство [0, 3, 0, 0]], # book ищет предмет dtype=float) # запросы декодера на каждом шагеA = softmax(Q @ H.T) # (4, 3): строка — куда смотрит шаг переводаfor word, row in zip(tgt, A): print(f"{word:>7}", row, "->", src[row.argmax()])C = A @ H # векторы контекста c_t, свой для каждого шагаprint("контексты:", C.shape)Ожидаемый вывод:
beta= 0 веса=[0.33 0.33 0.33] ответ=8.33beta= 1 веса=[0.46 0.37 0.17] ответ=9.49beta= 5 веса=[0.73 0.27 0. ] ответ=8.27beta= 50 веса=[1. 0. 0.] ответ=4.00brother [0.05 0.05 0.91] -> брата 's [0.05 0.05 0.91] -> брата new [0.91 0.05 0.05] -> новая book [0.05 0.91 0.05] -> книгаконтексты: (4, 4)Q @ H.T считает все оценки сразу: строка — шаг перевода, столбец — слово источника. Это уже почти формула
из следующего урока, не хватает только обучаемых матриц и деления на .
Попробуйте сами:
- Запрос «котёнок» . Посчитайте веса и ответ (должно получиться около 0.64, 0.35, 0.01 и 9.57 кг). Почему «собака» забирает треть веса, хотя спрашивали про котёнка? Посмотрите на скалярное произведение ключей «кот» и «собака»: в этом игрушечном пространстве они просто похожи.
- Уменьшите тройки в
Qдо единиц. Веса станут примерно 0.21 / 0.21 / 0.58. Почему выравнивание «размылось»? Свяжите ответ с множителем из таблицы выше. - Реализуйте ядерную регрессию Надарая–Ватсона: для точек и предскажите
, взяв оценки (функцию
soft_lookupпридётся чуть изменить: сходство здесь — не скалярное произведение). Должно получиться около , хотя : ядро слишком широкое и усредняет соседей. С оценками получится около . Точнее, чем (среднее синусов двух ближайших точек, 4 и 5), не выйдет даже с очень узким ядром: точка 4.5 лежит ровно между ними.
Проверь себя
Заголовок раздела «Проверь себя»- В seq2seq без внимания всё предложение проходит через один вектор. Это бутылочное горлышко, и качество перевода длинных фраз падает.
- Bahdanau (2014) сохранил векторы всех слов и дал декодеру на каждом шаге искать нужные: оценки → softmax → взвешенное среднее. Модель сама выучила выравнивание слов.
- Внимание — мягкий поиск по словарю: запрос сравнивается с ключами, веса через softmax, ответ — смесь значений. Резкость поиска задаётся масштабом оценок.
- Self-attention берёт запросы, ключи и значения из одного текста. Трансформер (2017) оставил только внимание и выиграл за счёт параллельности и короткого пути между токенами. Цена — сравнений.
Дальше: превращаем интуицию в формулу , разбираемся, зачем делить на , добавляем причинную маску и пишем всё на NumPy — Scaled dot-product self-attention.