Перейти к содержимому
Ступень 44.4 Механизм внимания

Внимание интуитивно

Почему перевод через один вектор «душит» длинные предложения и как внимание превращает поиск по словарю в мягкую обучаемую операцию — от идеи Богданова (2014) до трансформера (2017).

УРОКstage-4/4-4-attention/01-attention-intuition.mdx1 ч 10 минУрок 1 из 3
После урока вы сможете
  • Объяснить, в чём «бутылочное горлышко» seq2seq-моделей и почему из-за него падает качество перевода длинных предложений
  • Описать внимание как мягкий поиск по словарю (запрос, ключи, значения, веса через softmax) и посчитать его вручную
  • Рассказать путь идеи от Bahdanau (2014) до Transformer (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, ещё и не замечает порядка слов. Чтобы понять смысл слова, модель должна посмотреть на контекст и подмешать к вектору информацию из нужных соседей. Это и делает внимание.

В этом уроке формул с матрицами почти не будет. Разберёмся, откуда взялась идея и почему внимание — это «мягкий поиск по словарю». В следующем уроке превратим интуицию в формулу softmax(QK⊤/d)V\mathrm{softmax}(QK^\top/\sqrt{d})V и реализуем её, а в третьем — сделаем многоголовой.

Как был устроен нейросетевой перевод в 2014 году

Заголовок раздела «Как был устроен нейросетевой перевод в 2014 году»

Внимание в нынешнем виде придумали не для чат-ботов, а для машинного переводамашинный переводEN machine translation. В 2014 году две группы (Суцкевер, Виньялс и Ле из Google и группа Кёнхёна Чо из Монреаля) предложили схему seq2seqseq2seq (последовательность в последовательность)EN sequence-to-sequence (seq2seq)Схема «энкодер читает вход, декодер генерирует выход» (Sutskever и др.; Cho и др., 2014). Без внимания всё входное предложение сжимается в один вектор контекста., «последовательность в последовательность». В ней две рекуррентные сетирекуррентная нейросетьEN recurrent neural network (RNN) (о них — в уроке 3.4.3):

  1. Энкодер читает исходное предложение по одному токену и обновляет своё скрытое состояние hth_t. Последнее состояние считается «смыслом всего предложения». Обозначим его cc.
  2. Декодер — это языковая модель из урока 4.1.1, которой дали подсказку cc. Она генерирует перевод токен за токеном, пока не выдаст «конец предложения».

Такую модель вы сами обучали для транслитерации в уроке 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"]
seq2seq без внимания: всё предложение проходит через один вектор c

Всё, что декодер знает об исходной фразе, должно пройти через вектор cc фиксированного размера: в базовой модели без внимания из статьи Bahdanau и соавторов это 1000 чисел. Фраза из трёх слов и абзац из шестидесяти сжимаются в одинаковое количество чисел. Представьте, что вас просят пересказать главу «Войны и мира» одной SMS, а потом перевести главу на китайский, глядя только в эту SMS. Короткую фразу вы переведёте, длинную — нет.

Так и вышло на практике. Качество перевода обычных encoder–decoder-моделей падало с ростом длины предложения. Симптомы проблемы видны и в статье Суцкевера: если перевернуть исходное предложение (подавать «ковре на сидит Кошка»), качество заметно растёт. Начало фразы оказывается рядом с началом перевода, и информации о нём проще «дожить» до декодера. Такой трюк лечит симптом, но не причину: узким местом остаётся вектор cc.

Решение придумал Дмитрий Богданов (Dzmitry Bahdanau): выпускник Белорусского государственного университета, в 2014 году стажёр в лаборатории Йошуа Бенджио в Монреале. Позже он рассказал Андрею Карпатому, откуда пришла идея: из школьных упражнений по переводу. Когда человек переводит, его взгляд бегает между исходным текстом и переводом. Он не держит всю фразу в голове, а для каждого нового слова заново находит нужное место в оригинале.

Отсюда идея: не сжимать предложение в один вектор, а сохранить векторы всех слов h1,…,hnh_1, \dots, h_n. На каждом шаге декодер сам решает, какие из них нужны прямо сейчас. Энкодер у Bahdanau двунаправленный: одна RNN читает фразу слева направо, другая справа налево, и hjh_j склеивает оба состояния. Поэтому hjh_j описывает слово jj вместе с его окружением с обеих сторон. В статье модель называлась RNNsearch («поиск»), а слово «внимание» (attention) появилось благодаря Бенджио — в абзаце, который объяснял интуицию механизма.

На шаге tt декодер делает четыре действия:

  1. Оценивает каждое слово источника: etj=score(st−1,hj)e_{tj} = \mathrm{score}(s_{t-1}, h_j), где st−1s_{t-1} — текущее состояние декодера. Получается число «насколько слово jj важно сейчас».
  2. Нормирует оценки через softmax (урок 1.3.2): αtj=softmax(et)j\alpha_{tj} = \mathrm{softmax}(e_t)_j. Это веса вниманиявеса вниманияEN attention weightsРезультат softmax от оценок внимания. Неотрицательны, в сумме по ключам дают 1; показывают, «куда смотрит» запрос.: все положительные, в сумме 1.
  3. Усредняет: вектор контекста ct=∑jαtjhjc_t = \sum_j \alpha_{tj} h_j. Теперь он свой для каждого шага.
  4. Генерирует следующее слово, глядя на ctc_t.

Функция оценки у Bahdanau — маленькая нейросеть, её называют аддитивным вниманиемаддитивное внимание (внимание Bahdanau)EN additive attention (Bahdanau attention)Оценка сходства маленькой сетью v^T tanh(W q + U k). Предложено Bahdanau, Cho, Bengio (2014) для перевода.:

etj=v⊤tanh⁡(Wst−1+Uhj)e_{tj} = v^\top \tanh(W s_{t-1} + U h_j)

Что здесь написано: оба вектора переводятся матрицами WW и UU в общее пространство, складываются, проходят через tanh⁡\tanh, и вектор vv сворачивает результат в одно число. Важная деталь: никто не говорит модели, на какое слово смотреть. Всё обучается вместе с переводом, обычным обратным распространением ошибки. Модель сама выясняет, что для слова «cat» полезно смотреть на «Кошка».

Считать аддитивную оценку руками не будем. Уже через год Луонг и соавторы показали, что не хуже работает простое скалярное произведение состояний декодера и энкодера. Его и использует трансформер, и его мы посчитаем ниже.

Если нарисовать веса αtj\alpha_{tj} таблицей (строки — слова перевода, столбцы — слова источника), получится выравниваниевыравнивание слов (в переводе)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.050.050.91
's (показатель «чей»)0.050.050.91
new (новая)0.910.050.05
book (книга)0.050.910.05

Первое слово перевода смотрит на последнее слово источника. Модели без внимания пришлось бы «помнить» слово «брата» внутри вектора cc, а с вниманием она просто находит его, когда оно нужно.

Теперь отвлечёмся от перевода. Внимание — это общий способ доставать информацию по сходству. Проще всего понять его через обычный словарь Python.

d = {"кот": 4, "собака": 20, "рыба": 1} # ключ -> значение (масса, кг)
d["кот"] # 4

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

  • Только точное совпадение. Запрос «котёнок» даст KeyError, хотя «кот» явно ближе, чем «рыба».
  • Нельзя обучать. Если чуть-чуть изменить запрос, ответ либо не изменится, либо скачком станет другим. Производной нет, градиентный спуск не работает.

Сделаем всё «гладким». Ключи kik_i и запрос qq теперь векторы, у каждого ключа есть значение viv_i:

  1. Считаем сходство запроса с каждым ключом: si=s(q,ki)s_i = s(q, k_i). Самый простой вариант — скалярное произведение q⋅kiq \cdot k_i (урок 1.1.1): оно большое, когда векторы смотрят в одну сторону.
  2. Превращаем сходства в веса: α=softmax(s)\alpha = \mathrm{softmax}(s).
  3. Возвращаем взвешенное среднее значений:
Attention(q,K,V)=∑i=1nαivi,αi=es(q,ki)∑j=1nes(q,kj)\mathrm{Attention}(q, K, V) = \sum_{i=1}^{n} \alpha_i v_i, \qquad \alpha_i = \frac{e^{s(q, k_i)}}{\sum_{j=1}^{n} e^{s(q, k_j)}}

Что здесь написано: ответ — это смесь всех значений; чем больше ключ похож на запрос, тем больше вклад его значения. Сумма весов равна 1, поэтому ответ всегда лежит «между» значениями. Это запрос, ключ, значениезапрос, ключ, значение (Q, K, V)EN query, key, value (QKV)Три роли во внимании. Запрос сравнивается с ключами, по сходству получаются веса, ответ — взвешенная сумма значений. В трансформере все три получаются из токена обучаемыми матрицами W_Q, W_K, W_V., три роли, на которых построен весь трансформер. (Слово «ключ» из примера про многозначность получило ещё одно значение.)

Числовой пример. Ключи — двумерные векторы: «кот» =(1,0)= (1, 0), «собака» =(0.8,0.6)= (0.8, 0.6), «рыба» =(0,1)= (0, 1). Значения — масса: 4, 20 и 1 кг. Запрос q=β⋅(1,0)q = \beta \cdot (1, 0) похож на «кота», а множитель β\beta задаёт «резкость» поиска. Оценки si=q⋅ki=β⋅(1,0.8,0)s_i = q \cdot k_i = \beta \cdot (1, 0.8, 0):

β\betaвес «кот»вес «собака»вес «рыба»ответ, кг
00.3330.3330.3338.33 — просто среднее
10.4570.3740.1689.49
50.7270.2680.0058.27
501.0000.0000.0004.00 — как d["кот"]

Мягкий поиск — это непрерывная шкала от «усредни всё подряд» до «точное совпадение ключа». Множитель β\beta работает как обратная температуратемпература (сэмплирования)EN temperatureЛогиты делятся на T перед softmax. T < 1 — распределение острее, T > 1 — площе, T → 0 — greedy. из урока о сэмплировании. Нейросеть сама выучивает, насколько резко искать: для этого ей достаточно сделать векторы запросов и ключей длиннее или короче. Почему длину векторов при этом нужно держать под контролем, вы увидите в следующем уроке (деление на d\sqrt{d}).

В 1964 году статистики Э. Надарая и Дж. Ватсон независимо предложили ядерную регрессиюядерная регрессия Надарая–ВатсонаEN Nadaraya–Watson kernel regressionСтатистический метод 1964 года — предсказание как взвешенное среднее известных ответов с весами по близости. Необучаемый прообраз внимания; с гауссовым ядром веса — это softmax.. Чтобы предсказать yy в точке xx, нужно взять известные точки (xi,yi)(x_i, y_i) и усреднить их yiy_i с весами, которые тем больше, чем ближе xix_i к xx:

y^(x)=∑iK(x−xi)∑jK(x−xj) yi\hat y(x) = \sum_{i} \frac{K(x - x_i)}{\sum_j K(x - x_j)}\, y_i

С гауссовым ядром K(u)=e−u2/2K(u) = e^{-u^2/2} веса — это ровно softmax от оценок −(x−xi)2/2-(x - x_i)^2/2. Запрос — xx, ключи — xix_i, значения — yiy_i. Китайское издание 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 уже содержит информацию о нужном существительном. Так внимание решает проблему многозначности, с которой мы начали.

В трансформере каждый токен играет все три роли сразу: из его эмбеддинга тремя обучаемыми матрицами WQW_Q, WKW_K, WVW_V получаются запрос, ключ и значение. Как именно — в следующем уроке.

ГодРаботаЧто нового
1964Надарая; ВатсонЯдерная регрессия — взвешенное среднее по сходству, прообраз внимания
2014Sutskever, Vinyals, Le; Cho и соавт.seq2seq: энкодер и декодер на RNN, один вектор контекста
2014Bahdanau, Cho, BengioАддитивное внимание: декодер ищет по всем состояниям энкодера
2015Luong, Pham, ManningВнимание через скалярное произведение — проще и быстрее аддитивного
2015Xu и соавт., «Show, Attend and Tell»Внимание к областям картинки при генерации подписи
2016Cheng, Dong, LapataВнимание внутри одной последовательности (intra-attention) — прообраз self-attention
2016Wu и соавт., Google GNMTПеревод на LSTM с вниманием в продакшене (в работающем сервисе) Google Translate
2017Lin и соавт.Эмбеддинг предложения из нескольких «взглядов» self-attention — прообраз многоголовости
2017Vaswani и соавт., «Attention Is All You Need»Трансформер: только внимание, без рекуррентности

У RNN две беды. Первая — последовательность: состояние hth_t нельзя посчитать, пока не готово ht−1h_{t-1}. Текст из 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
В RNN путь от x1 до x4 — три шага. В self-attention — один.

Результат статьи: базовая модель обучалась 12 часов на 8 GPU P100, а большая версия установила рекорд англо-немецкого перевода WMT 2014 — 28.4 BLEU. Через год на этой архитектуре построили GPT и BERT, ещё через несколько лет — все современные LLM. У подхода есть цена: если каждый токен сравнивается с каждым, то на nn токенов нужно n2n^2 сравнений. Об этой квадратичной стоимости — в уроке 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.33
beta= 1 веса=[0.46 0.37 0.17] ответ=9.49
beta= 5 веса=[0.73 0.27 0. ] ответ=8.27
beta= 50 веса=[1. 0. 0.] ответ=4.00
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] -> книга
контексты: (4, 4)

Q @ H.T считает все оценки сразу: строка — шаг перевода, столбец — слово источника. Это уже почти формула из следующего урока, не хватает только обучаемых матриц и деления на d\sqrt{d}.

Попробуйте сами:

  1. Запрос «котёнок» q=5⋅(0.9,0.1)q = 5 \cdot (0.9, 0.1). Посчитайте веса и ответ (должно получиться около 0.64, 0.35, 0.01 и 9.57 кг). Почему «собака» забирает треть веса, хотя спрашивали про котёнка? Посмотрите на скалярное произведение ключей «кот» и «собака»: в этом игрушечном пространстве они просто похожи.
  2. Уменьшите тройки в Q до единиц. Веса станут примерно 0.21 / 0.21 / 0.58. Почему выравнивание «размылось»? Свяжите ответ с множителем β\beta из таблицы выше.
  3. Реализуйте ядерную регрессию Надарая–Ватсона: для точек xi=0,1,…,9x_i = 0, 1, \dots, 9 и yi=sin⁡xiy_i = \sin x_i предскажите y^(4.5)\hat y(4.5), взяв оценки −(x−xi)2/2-(x - x_i)^2 / 2 (функцию soft_lookup придётся чуть изменить: сходство здесь — не скалярное произведение). Должно получиться около −0.59-0.59, хотя sin⁡4.5≈−0.98\sin 4.5 \approx -0.98: ядро слишком широкое и усредняет соседей. С оценками −(x−xi)2⋅2-(x - x_i)^2 \cdot 2 получится около −0.84-0.84. Точнее, чем −0.86-0.86 (среднее синусов двух ближайших точек, 4 и 5), не выйдет даже с очень узким ядром: точка 4.5 лежит ровно между ними.
Проверь себя6 вопросов
1 Почему seq2seq без внимания плохо переводит длинные предложения?
2 Оценки запроса с тремя ключами: [2,0,0][2, 0, 0], значения — [10,4,−2][10, 4, -2]. Чему равен результат мягкого поиска? Округлите до двух знаков.
3 Какие утверждения о мягком поиске верны?Можно выбрать несколько ответов
4 Что служит запросом во внимании Bahdanau на шаге перевода tt?
5 Что главное сделали авторы «Attention Is All You Need» по сравнению с RNN-переводчиками с вниманием?
6 Как называется вариант внимания, в котором запросы, ключи и значения берутся из одной и той же последовательности? (англ. термин)
  • В seq2seq без внимания всё предложение проходит через один вектор. Это бутылочное горлышко, и качество перевода длинных фраз падает.
  • Bahdanau (2014) сохранил векторы всех слов и дал декодеру на каждом шаге искать нужные: оценки → softmax → взвешенное среднее. Модель сама выучила выравнивание слов.
  • Внимание — мягкий поиск по словарю: запрос сравнивается с ключами, веса через softmax, ответ — смесь значений. Резкость поиска задаётся масштабом оценок.
  • Self-attention берёт запросы, ключи и значения из одного текста. Трансформер (2017) оставил только внимание и выиграл за счёт параллельности и короткого пути между токенами. Цена — n2n^2 сравнений.

Дальше: превращаем интуицию в формулу softmax(QK⊤/dk)V\mathrm{softmax}(QK^\top/\sqrt{d_k})V, разбираемся, зачем делить на dk\sqrt{d_k}, добавляем причинную маску и пишем всё на NumPy — Scaled dot-product self-attention.