Запрос, Ключ, Значение
Три линейных отображения из одного и того же входа
После встраивания (активность 4) каждая позиция несёт 768-мерный вектор x_t. Attention начинается с создания трёх различных проекций x:
Q (query): что эта позиция хочет узнать?
K (ключ): что эта позиция предлагает другим позициям?
V (значение): какой контент эта позиция предоставляет, если на неё обратить внимание?
Каждая проекция получается из матрицы весов, полученной в результате обучения:
Q = x · W_Q # Форма W_Q: (d_model, d_k)
K = x · W_K # Форма W_K: (d_model, d_k)
V = x · W_V # Форма W_V: (d_model, d_k)
Три матрицы, все обучаются через обратное распространение. Модель учится: в этой позиции какая query лучше всего извлекает полезный прошлый контекст? Какой key хорошо рекламирует содержимое этой позиции? Какое value предоставить, если выбрано?
Аналогия с библиотекой
Представьте каталог карточек библиотеки. Вы заходите с темой в голове (ваш query). Каждая карточка содержит ключевые слова (key). Когда ваша тема совпадает с ключевыми словами на карточке, вы берёте содержание книги (value). Attention делает это для каждого токена параллельно: каждая позиция запрашивает каждую другую позицию, ранжирует совпадение и извлекает взвешенную комбинацию векторов значений.
Размерности ANDREA-120M
| Количество | Значение | Примечания |
|---|---|---|
| d_model | 768 | Размер вектора на каждой позиции |
| n_head | 12 | Параллельные головы внимания |
| d_k | 64 | Размерность на голову (= d_model / n_head) |
| T | 1024 | Длина контекста |
d_k = d_model / n_head = 768 / 12 = 64. Каждая голова видит 64-мерный срез полного 768-мерного пространства. Activity 6 (grow_a_language_model_multi_head) подробно разбирает разделение по головам.
Вычислите d_k
Почему делить на sqrt(d_k)
Матрица оценок
Как только Q и K существуют (каждый с формой (T, d_k)), внимание вычисляет матрицу оценок:
scores = Q · K^T # shape: (T, T)
scores[i, j] = насколько сильно запрос позиции i выравнивается с ключом позиции j. Каждая пара (i, j) получает один score: 1024 × 1024 = 1 048 576 scores на attention head за один forward pass.
Почему делим
Скалярные произведения двух случайных единичных векторов размерности d имеют величину порядка sqrt(d). Без масштабирования scores растут с d_k:
- d_k = 64: типичные скалярные произведения порядка 8.
- d_k = 256: типичные скалярные произведения порядка 16.
- d_k = 4096: типичные скалярные произведения порядка 64.
Большие значения приводят к пиковому softmax (одна позиция доминирует, градиенты исчезают в других местах). Обучение останавливается. Масштабирование исправляет величину:
scaled_scores = (Q · K^T) / sqrt(d_k)
Для ANDREA-120M, sqrt(d_k) = sqrt(64) = 8. Каждое значение делится на 8. Величины остаются примерно на единичном масштабе независимо от d_k. Softmax остается хорошо управляемым. Градиенты текут.
Оригинальное обоснование Васвани
Из Attention Is All You Need (2017): «Для больших значений d_k скалярные произведения растут в величине, выталкивая функцию softmax в области, где у неё чрезвычайно малые градиенты.» Делитель sqrt(d_k) противодействует этому росту.
Взгляд на код
Внутри microgpt_cuda.cu это масштабирование появляется как буквальное деление:
scores[i][j] = dot(Q[i], K[j]) * (1.0f / sqrtf(d_k));
Одно умножение float на каждый score. Дёшево. Критично.
Масштабирование при d_model = 4096
Почему позиция i не может видеть позицию j > i
Ограничение, рождённое генерацией
ANDREA генерирует один токен за раз. На этапе инференса позиция 0 производит первый токен, затем позиция 1 видит вывод позиции 0 и производит второй токен, и так далее. Модель никогда не имеет доступа к будущим токенам во время генерации.
Обучение должно отражать это. Если во время обучения позиция 5 могла бы обращать внимание на позицию 6, модель выучила бы обходной путь: «предсказывать токен 6, читая токен 6». На этапе инференса этот обходной путь исчезает (токен 6 ещё не существует). Поведение модели во время обучения и инференса катастрофически разойдётся.
Маска
Causal-маска блокирует внимание из любой позиции i на любую позицию j > i. Реализация: устанавливаем scaled_scores[i][j] = -infinity везде, где j > i. После softmax эти значения становятся exp(-inf) = 0. Маска чисто обнуляет внимание к будущим позициям.
for i in range(T):
for j in range(T):
if j > i:
scaled_scores[i][j] = -1e9 # эффективно -inf
После softmax (по строкам) каждая строка суммируется до 1, но только элементы [0, i] несут вероятностную массу. Позиция i смешивает информацию только из прошлых позиций.
Визуализация маски
Матрица оценок формы (T, T) с применённой маской выглядит как нижнетреугольная структура:
scaled_scores после маски, softmax по строкам:
строка 0: [1.0, 0, 0, 0, ...] # видит только себя
строка 1: [0.4, 0.6, 0, 0, ...] # видит позиции 0, 1
строка 2: [0.2, 0.3, 0.5, 0, ...] # видит 0, 1, 2
строка 3: [0.1, 0.2, 0.3, 0.4, ...] # видит 0, 1, 2, 3
...
Строго нижнетреугольное распределение вероятностей по строкам. Будущее остаётся невидимым.
Почему трансформеру только с декодером это нужно
Модели только с декодером, такие как ANDREA, GPT и LLaMA, имеют одну общую цель: предсказывать следующий токен на основе предыдущих. Causal mask делает эту цель обучаемой параллельно: каждая позиция вычисляет своё предсказание следующего токена одновременно, и ни одна позиция не жульничает, заглядывая вперёд.
Маска и тип
От оценок к выходу
Softmax: От оценок к вероятностям
Маскированные, масштабированные оценки всё ещё принимают значения из области действительных чисел. Softmax преобразует каждую строку в распределение вероятностей:
A[i][j] = exp(scaled_scores[i][j]) / sum_k exp(scaled_scores[i][k])
В результате получаются три свойства:
- A[i][j] >= 0 для всех (i, j).
- sum_j A[i][j] = 1 для каждой строки i.
- Большие сырые оценки производят большие вероятности (монотонно).
Вектор вероятностей строки i говорит модели: насколько сильно позиция i должна учитывать каждую предыдущую позицию при вычислении своего выхода?
Взвешенная сумма V
Итоговый выход внимания для позиции i:
output[i] = sum_j A[i][j] · V[j]
Каждый вектор значений V[j] взвешивается вероятностью внимания A[i][j], затем суммируется. Выход позиции i объединяет векторы значений из каждой предыдущей позиции, взвешенные по релевантности.
В матричном виде, все позиции одновременно:
Attention(Q, K, V) = softmax(mask(Q · K^T / sqrt(d_k))) · V
Одна строка. Весь механизм внимания. Vaswani et al. написали эту строку в 2017 году; трансформеры с тех пор фундаментально не изменились.
Форма выхода на голову
Выход одной головы внимания: форма (T, d_k). Для ANDREA-120M: (1024, 64). Все 12 голов вычисляются параллельно; их выходы конкатенируются в (1024, 768) и подаются в финальную линейную проекцию (W_O), затем в MLP блока трансформера.
Активность 6 (grow_a_language_model_multi_head) охватывает разделение на несколько голов. Активность 7 (grow_a_language_model_transformer_block) охватывает всё, что окружает внимание: остаточные соединения, нормализацию слоёв, MLP.