Механизм внимания: на что смотреть

Внимание — поворотная идея в 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 и ведёт прямо к трансформерам.
Проверьте себя
1. Что вычисляет механизм внимания на каждом шаге?
AОдно случайное слово входа
BВзвешенную сумму всех слов входа, где веса отражают их важность для текущего шага
CДлину входного предложения
DМатрицу ошибок
2. Зачем оценки совпадения пропускают через softmax?
AЧтобы сделать их отрицательными
BЧтобы превратить их в положительные веса, дающие в сумме 1 (распределение внимания)
CЧтобы удалить самые большие оценки
DЧтобы перевести их в текст
3. Какую проблему seq2seq решает механизм внимания?
AСлишком быстрое обучение
BБутылочное горлышко единственного context-вектора на длинных предложениях
CОтсутствие токенизации
DНевозможность считать косинус
Поддержать проект