Механизм внимания: на что смотреть
Внимание — поворотная идея в NLP: вместо того чтобы сжимать всё в один вектор, модель на каждом шаге решает, на какие слова входа смотреть и насколько сильно.
Механизм внимания (attention) — способ для модели на каждом шаге вычислять взвешенную сумму всех элементов входа, где веса показывают, насколько каждый элемент важен сейчас.
Откуда взялась идея
В прошлом уроке мы упёрлись в горлышко: один context-вектор не вмещает длинное предложение. Внимание убирает это горлышко. Идея человеческая: переводя слово, мы не держим в голове всю фразу одинаково — мы смотрим на релевантную часть оригинала. Переводя «cats», мы смотрим на «котов», а не на «я». Внимание даёт модели ровно эту способность: на каждом шаге сфокусироваться на нужных словах входа.
Как это работает по шагам
Для текущего шага декодера (его называют query — запрос) модель сравнивает его с каждым словом входа (keys — ключи) и получает «оценку совпадения» для каждого. Затем оценки превращают в веса через softmax (так, чтобы они были положительны и в сумме давали 1). Наконец, берут взвешенную сумму представлений слов входа (values — значения) с этими весами. Результат — вектор, в котором преобладают самые релевантные слова.
1. оценки = насколько query похож на каждый key
2. веса = softmax(оценки) # положительные, сумма = 1
3. итог = сумма( вес_i * value_i ) # взвешенная смесь входных слов
Считаем веса внимания руками
Пусть декодер генерирует слово и его query сравнивается с тремя словами входа. У нас есть три «оценки совпадения». Превратим их в веса через softmax и посмотрим, на что модель смотрит сильнее всего.
import math
input_words = ["я", "люблю", "котов"]
# оценки совпадения query с каждым словом входа (чем больше — тем релевантнее)
scores = [1.0, 2.0, 5.0]
# softmax: exp каждой оценки, делённый на сумму всех exp
exps = [math.exp(s) for s in scores]
total = sum(exps)
weights = [e / total for e in exps]
for word, w in zip(input_words, weights):
bar = "#" * int(w * 40)
print("%-7s вес %.3f %s" % (word, w, bar))
print("Сумма весов:", round(sum(weights), 3))
Вывод:
я вес 0.017 люблю вес 0.047 # котов вес 0.936 ##################################### Сумма весов: 1.0
Softmax превратил сырые оценки в веса: модель почти всё внимание (0.936) отдала слову «котов» и почти проигнорировала «я». Если бы это был перевод и мы генерировали «cats», такое распределение идеально: смотрим именно на «котов». Веса в сумме ровно 1 — это и есть распределение внимания (отдельные значения в столбце округлены до трёх знаков, поэтому визуально не дают ровно единицу).
Взвешенная сумма — итог внимания
Дальше этими весами взвешивают сами слова входа и складывают. Посмотрим на одномерном примере, как получается итоговый «вектор внимания».
import math
values = [10.0, 20.0, 50.0] # «значения» слов входа (упрощённо — числа)
scores = [1.0, 2.0, 5.0]
exps = [math.exp(s) for s in scores]
total = sum(exps)
weights = [e / total for e in exps]
context = sum(w * v for w, v in zip(weights, values))
print("Веса:", [round(w, 3) for w in weights])
print("Вектор внимания (взвешенная сумма):", round(context, 2))
Вывод:
Веса: [0.017, 0.047, 0.936] Вектор внимания (взвешенная сумма): 47.92
Итог 47.92 близок к 50 — значению самого релевантного слова, потому что оно получило почти весь вес. Так внимание собирает «нужную» информацию из всех слов входа, а не из одного сжатого вектора. Декодер на каждом шаге строит свой context, глядя туда, куда надо.
Почему это так важно
- Снимает горлышко seq2seq: доступны все слова входа, а не один вектор.
- Работает с длинными предложениями: можно «дотянуться» до далёкого слова напрямую.
- Даёт интерпретируемость: по весам видно, на что модель смотрела.
- Это прямой предшественник self-attention — сердца трансформера (следующий раздел).
Итог
- Внимание = взвешенная сумма всех слов входа, веса задают важность.
- Веса считаются softmax от оценок совпадения query с ключами.
- Softmax даёт положительные веса с суммой 1 — распределение внимания.
- Это убирает узкое место seq2seq и ведёт прямо к трансформерам.