Ответ
Механизм внимания (Attention) в Transformer основан на операции Scaled Dot-Product Attention. Его цель — вычислить контекстное представление для каждого элемента последовательности как взвешенную сумму всех элементов, где веса определяются их взаимной релевантностью.
Формула внимания:
Attention(Q, K, V) = softmax( (Q * K^T) / sqrt(d_k) ) * V
Компоненты:
- Q (Query), K (Key), V (Value): Линейные проекции исходных эмбеддингов. Каждый токен получает свои векторы запроса, ключа и значения.
- *Q K^T:** Скалярное произведение запроса с каждым ключом измеряет схожесть (совместимость).
- Масштабирование на
sqrt(d_k): Стабилизирует градиенты, предотвращая слишком малые значения градиента послеsoftmaxпри большой размерности ключей. - Softmax: Преобразует оценки совместимости в вероятностное распределение весов (сумма = 1).
- Умножение на 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
Выглядит страшно, но если по-простому, то это просто умный способ посчитать взвешенную сумму. Сейчас объясню на пальцах, ёпта.
Из чего это говно собрано:
-
Q (Запрос), K (Ключ), V (Значение): Это не какие-то магические сущности. Берётся наше исходное представление слова (эмбеддинг) и прогоняется через три разных слоя-проекции. Получается три вектора для каждого слова.
Query— это как вопрос: "на кого мне обратить внимание?".Key— это заявление о себе: "вот что я из себя представляю".Value— это, собственно, информация, которую я несу. УмножениеQueryнаKey— это попытка найти ответ на вопрос "насколько ты мне релевантен?". -
*Q K^T:** Берём запрос одного слова и скалярно умножаем на ключи ВСЕХ слов в последовательности. Получаем очки совместимости. Чем больше очко — тем больше внимания нужно уделить.
-
Делим на
sqrt(d_k): А это, блядь, очень важный технический момент. Размерность ключей (d_k) может быть большой, и тогда скалярные произведения улетают в космос, в стратосферу. Послеsoftmaxэто приводит к тому, что почти все веса становятся нулевыми, кроме одного — градиенты становятся хуйовыми, и сеть не учится. Масштабированиеsqrt(d_k)возвращает дисперсию этих очков к нормальной, стабилизирует всё это дело. Без этого — пиздец. -
Softmax: Берём эти масштабированные очки и пропускаем через
softmax. Теперь это не просто числа, а нормализованные веса (все в сумме дают 1). По сути, получаем распределение вероятностей: "на слово номер один я должен потратить 60% внимания, на второе — 5%, на третье — 0.1% и т.д." -
Умножаем на 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 контекстных представлений, склеиваем их в один длинный вектор и пропускаем через финальный линейный слой. Получается овердохуища выразительная мощность. Без этого был бы просто очередной, хуй с горы, слабый механизм.