На чем основан механизм внимания в архитектуре Transformer?

Ответ

Механизм внимания (Attention) в Transformer основан на операции Scaled Dot-Product Attention. Его цель — вычислить контекстное представление для каждого элемента последовательности как взвешенную сумму всех элементов, где веса определяются их взаимной релевантностью.

Формула внимания: Attention(Q, K, V) = softmax( (Q * K^T) / sqrt(d_k) ) * V

Компоненты:

  1. Q (Query), K (Key), V (Value): Линейные проекции исходных эмбеддингов. Каждый токен получает свои векторы запроса, ключа и значения.
  2. *Q K^T:** Скалярное произведение запроса с каждым ключом измеряет схожесть (совместимость).
  3. Масштабирование на sqrt(d_k): Стабилизирует градиенты, предотвращая слишком малые значения градиента после softmax при большой размерности ключей.
  4. Softmax: Преобразует оценки совместимости в вероятностное распределение весов (сумма = 1).
  5. Умножение на V: Выход — взвешенная сумма значений, где веса определяют, насколько каждому токену следует "уделить внимание" другим токенам.

Практическая реализация (PyTorch):

import torch
import torch.nn.functional as F

def scaled_dot_product_attention(Q, K, V, mask=None):
    """
    Q, K, V: тензоры формы [batch_size, seq_len, d_model]
    mask: тензор для маскирования (например, для padding или будущих токенов в декодере)
    """
    d_k = Q.size(-1)
    scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))

    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9) # Маскированные позиции получают очень низкий вес

    attn_weights = F.softmax(scores, dim=-1)
    output = torch.matmul(attn_weights, V)
    return output, attn_weights

Multi-Head Attention: Для повышения выразительности механизм выполняется параллельно в h разных подпространствах (головах), а их выходы конкатенируются и проецируются обратно.

Ответ 18+ 🔞

Давай разберём эту штуку, которая всех так взбудоражила. Механизм внимания в Трансформере — это, по сути, его главная фишка, его хитрая жопа. Представь, что у тебя есть предложение, и каждое слово должно понять, на какие другие слова ему смотреть. Не просто подряд, а выборочно, по смыслу. Вот этот механизм как раз это и делает.

Вот его формула, с которой все носятся: Attention(Q, K, V) = softmax( (Q * K^T) / sqrt(d_k) ) * V

Выглядит страшно, но если по-простому, то это просто умный способ посчитать взвешенную сумму. Сейчас объясню на пальцах, ёпта.

Из чего это говно собрано:

  1. Q (Запрос), K (Ключ), V (Значение): Это не какие-то магические сущности. Берётся наше исходное представление слова (эмбеддинг) и прогоняется через три разных слоя-проекции. Получается три вектора для каждого слова. Query — это как вопрос: "на кого мне обратить внимание?". Key — это заявление о себе: "вот что я из себя представляю". Value — это, собственно, информация, которую я несу. Умножение Query на Key — это попытка найти ответ на вопрос "насколько ты мне релевантен?".

  2. *Q K^T:** Берём запрос одного слова и скалярно умножаем на ключи ВСЕХ слов в последовательности. Получаем очки совместимости. Чем больше очко — тем больше внимания нужно уделить.

  3. Делим на sqrt(d_k): А это, блядь, очень важный технический момент. Размерность ключей (d_k) может быть большой, и тогда скалярные произведения улетают в космос, в стратосферу. После softmax это приводит к тому, что почти все веса становятся нулевыми, кроме одного — градиенты становятся хуйовыми, и сеть не учится. Масштабирование sqrt(d_k) возвращает дисперсию этих очков к нормальной, стабилизирует всё это дело. Без этого — пиздец.

  4. Softmax: Берём эти масштабированные очки и пропускаем через softmax. Теперь это не просто числа, а нормализованные веса (все в сумме дают 1). По сути, получаем распределение вероятностей: "на слово номер один я должен потратить 60% внимания, на второе — 5%, на третье — 0.1% и т.д."

  5. Умножаем на V: И вот финальный аккорд. Берем эти веса и умножаем на векторы Value. Выход — это контекстное представление слова. Оно уже не просто "я — слово 'кошка'", а "я — слово 'кошка' в контексте того, что рядом стоит 'пушистая' и 'спит'".

А теперь как это в коде выглядит, если не бздеть:

import torch
import torch.nn.functional as F

def scaled_dot_product_attention(Q, K, V, mask=None):
    """
    Q, K, V: тензоры формы [batch_size, seq_len, d_model]
    mask: тензор для маскирования (например, для padding или будущих токенов в декодере)
    """
    d_k = Q.size(-1)
    scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))

    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9) # Маскированные позиции получают очень низкий вес

    attn_weights = F.softmax(scores, dim=-1)
    output = torch.matmul(attn_weights, V)
    return output, attn_weights

С маской там всё просто: если нам нужно что-то проигнорировать (например, технические токены <pad> или будущие слова в декодере), мы засовываем в эти позиции score значение минус бесконечность (практически -1e9). После softmax вес в этой позиции становится ноль — и всё, на эту хуйню внимания не обращаем.

Ну и вишенка на торте — Multi-Head Attention. А это, ядрёна вошь, вообще гениально. Одна голова — хорошо, а h голов — лучше, потому что каждая может учиться смотреть на разные типы зависимостей. Одна голова может отслеживать связь подлежащего и сказуемого, другая — местоимений с их референсами, третья — просто соседние слова. Параллельно делаем h таких механизмов внимания с разными весами проекций, получаем h контекстных представлений, склеиваем их в один длинный вектор и пропускаем через финальный линейный слой. Получается овердохуища выразительная мощность. Без этого был бы просто очередной, хуй с горы, слабый механизм.