2026 г.

Глава 3. Механизм внимания и архитектура трансформера

Учебник «Большие языковые модели»

У нас есть последовательность векторов — эмбеддинги токенов из главы 2. Теперь центральный вопрос: как построить сеть, в которой представление каждого токена обогащается информацией обо всём контексте? Слово «ключ» должно «узнать», что рядом стоит «из-под земли», причём «рядом» может означать и за пятьдесят слов. Ответ индустрии на этот вопрос — архитектура трансформер, предложенная в статье с говорящим названием «Attention Is All You Need» (Васвани и др., 2017). Архитектура задаёт способ вычисления, но способности конкретной модели определяют также данные, цель обучения и последующее дообучение.

3.1. Чем плохи рекуррентные сети

До 2017 года последовательности обрабатывали рекуррентными сетями (RNN, LSTM): сеть читает токены по одному, поддерживая вектор скрытого состояния — «конспект» прочитанного. У классической однонаправленной RNN без внимания есть два ограничения. Во-первых, узкое горло: весь прочитанный контекст, хоть тысяча слов, должен уместиться в один вектор фиксированного размера; связи между далёкими токенами доходят ослабленными — сигнал от начала фразы к концу проходит через длинную цепочку преобразований. Во-вторых, и это оказалось решающим для масштабирования, — последовательность вычислений: нельзя обработать токен номер 100, не обработав предыдущие 99. А значит, нельзя загрузить работой десятки тысяч параллельных ядер GPU так же эффективно, как большими матричными операциями трансформера.

3.2. Внимание: мягкий поиск по таблице

Механизм полного внимания решает обе проблемы одним приёмом: пусть каждый токен напрямую сравнит себя со всеми доступными ему позициями и назначит им веса.

Удобная интуиция — «мягкая» ассоциативная таблица. В обычной хеш-таблице запрос (key) либо совпадает с ключом, либо нет, и мы получаем одно значение. Внимание устраивает нечёткий поиск: запрос сравнивается со всеми ключами, каждому назначается вес «насколько подходит», и результат — взвешенная сумма всех значений.

Формально. Каждый токен i с текущим представлением xi ∈ ℝd порождает три вектора тремя обучаемыми матрицами:

qi = WQxi,    ki = WKxi,    vi = WVxi

Запрос (query) — «что я ищу», ключ (key) — «что во мне можно найти», значение (value) — «что я отдам, если меня выберут». Сходство запроса токена i с ключом токена j измеряется скалярным произведением, а веса внимания получаются нормировкой сходств функцией softmax:

aij = softmaxj ( qi · kj / √dk ) = exp(qi·kj/√dk) / Σm exp(qi·km/√dk)

Веса aij неотрицательны и в сумме дают единицу — это распределение внимания токена i по всем токенам. Новое представление токена — взвешенная сумма значений:

zi = Σj aij vj

В матричной записи для всей последовательности сразу (строки Q, K, V — запросы, ключи и значения всех токенов):

Attention(Q, K, V) = softmax(QKT / √dk) V

Деление на √dk (корень из размерности ключей) — техническая, но важная деталь: скалярные произведения случайных векторов растут как √dk, и без нормировки softmax при больших размерностях «насыщается» — почти весь вес достаётся одному токену, градиенты исчезают, обучение стопорится.

Заметьте, чего мы добились. Путь между любыми двумя токенами — один шаг, как бы далеко они ни стояли: проблема дальних связей снята. Все строки матрицы QKT вычисляются независимо — это одно большое матричное умножение, идеальная пища для GPU: проблема параллелизма снята. Цена — квадратичность: матрица внимания имеет размер n×n, и удвоение длины контекста вчетверо увеличивает объём этих вычислений. Оптимизированные реализации вроде FlashAttention не обязаны хранить всю матрицу в памяти, но число пар для обычного полного внимания остаётся квадратичным. При пошаговой генерации с KV-кэшем картина вычислений отличается; к ней вернёмся в главе 7.

3.3. Причинная маска и многоголовое внимание

Языковая модель предсказывает следующий токен, поэтому при обучении токену нельзя подглядывать вперёд. Это обеспечивает причинная маска (causal mask): в матрице сходств все элементы выше диагонали (позиции j > i) заменяются на −∞ перед softmax, и вес будущих токенов обнуляется. Токен видит только прошлое. Именно поэтому генерация получается авторегрессионной. В рассматриваемых LLM этот принцип реализован архитектурой decoder-only, в отличие от двунаправленных кодирующих моделей типа BERT. Эти термины не синонимы: декодер модели encoder–decoder тоже может генерировать авторегрессионно.

Далее, одного «взгляда» на контекст мало: токену может быть одновременно важно синтаксическое согласование с одним словом, смысловая связь с другим, кореференция с третьим. Поэтому внимание делают многоголовым (multi-head): в классическом варианте вместо одного внимания размерности d параллельно работают h «голов» размерности dk = d/h, каждая со своими матрицами WQ, WK, WV. Их результаты конкатенируются и проецируются обратно в размерность d. У некоторых голов обнаруживаются устойчивые синтаксические или позиционные паттерны, но интерпретация не однозначна, а часть голов бывает избыточной. Современные варианты MQA и GQA разделяют ключи и значения между группами голов, уменьшая KV-кэш.

3.4. Блок трансформера

Внимание перемешивает информацию между токенами, но почти не перерабатывает её. Поэтому в блоке трансформера за вниманием следует полносвязная сеть (feed-forward network, FFN), применяемая к каждой позиции независимо: два линейных слоя с нелинейностью между ними, внутренняя размерность часто в несколько раз больше d. Если внимание — это «коммуникация», то FFN — «вычисление». Исследования находят фактические ассоциации и в FFN, и в других компонентах; знания распределены по сети, поэтому буквального «хранилища фактов» здесь нет. В части крупных моделей FFN устроена как смесь экспертов (MoE): маршрутизатор активирует для токена небольшое подмножество экспертов, чтобы число параметров росло быстрее, чем вычисления.

Два инженерных элемента делают глубокую башню из таких блоков обучаемой. Остаточные связи (residual connections): выход каждого подслоя прибавляется к его входу, xx + Attention(x), xx + FFN(x). Сеть учит не преобразование целиком, а поправку к тождественному отображению; градиенты текут по «магистрали» остаточных связей сквозь всю глубину. Нормализация слоя (LayerNorm/RMSNorm) стабилизирует масштаб значений. Ниже показан распространённый pre-norm-вариант, где нормализация стоит перед подслоем:

x = x + MultiHeadAttention(Norm(x))     # общение токенов
x = x + FFN(Norm(x))                    # переработка на месте

Языковая модель — это эмбеддинги токенов на входе, затем L одинаковых по устройству блоков (в современных моделях L — десятки), затем финальная нормализация и выходная проекция в логиты — по числу на каждый токен словаря; softmax превращает их в распределение вероятностей следующего токена. Вопрос о том, как модель узнаёт порядок токенов (само внимание к порядку безразлично — перестановка входа лишь переставляет выход), решается позиционным кодированием; один из широко используемых вариантов — поворотные позиционные эмбеддинги RoPE, при которых пары координат запросов и ключей поворачиваются на угол, пропорциональный позиции, и скалярное произведение начинает зависеть от относительного расстояния между токенами.

3.5. Внимание в тридцать строк

Весь механизм легко пощупать руками. Ниже учебная реализация причинного многоголового внимания на NumPy: без батчей, смещений и dropout, но с выходной проекцией:

import numpy as np

def softmax(x):
    e = np.exp(x - x.max(axis=-1, keepdims=True))
    return e / e.sum(axis=-1, keepdims=True)

def causal_attention(X, Wq, Wk, Wv, Wo, n_heads):
    """X: (n, d) -- представления n токенов."""
    n, d = X.shape
    dk = d // n_heads
    Q, K, V = X @ Wq, X @ Wk, X @ Wv          # (n, d) каждая

    mask = np.triu(np.ones((n, n)), k=1) * -1e9   # будущее -> -inf

    heads, weights = [], []
    for h in range(n_heads):
        s = slice(h*dk, (h+1)*dk)
        scores = Q[:, s] @ K[:, s].T / np.sqrt(dk)  # (n, n)
        A = softmax(scores + mask)                  # веса внимания
        heads.append(A @ V[:, s])                   # (n, dk)
        weights.append(A)
    joined = np.concatenate(heads, axis=1)           # (n, d)
    return joined @ Wo, np.stack(weights)            # (n, d), (h, n, n)

# Игрушечный прогон
rng = np.random.default_rng(0)
n, d, h = 6, 32, 4
X = rng.normal(size=(n, d))
Wq, Wk, Wv, Wo = (rng.normal(size=(d, d)) * 0.1 for _ in range(4))
out, A = causal_attention(X, Wq, Wk, Wv, Wo, h)
print(out.shape, A.shape)                           # (6, 32) (4, 6, 6)

Распечатайте матрицу A[0] для одной головы и убедитесь: она нижнетреугольная (причинность), каждая строка суммируется в единицу (распределение), первая строка — в точности [1, 0, …, 0]: первому токену не на что смотреть, кроме себя.

3.6. Где живут параметры

Полезно один раз посчитать параметры «на салфетке». Для классического плотного блока с обычным многоголовым вниманием: четыре матрицы внимания (WQ, WK, WV и выходная проекция) — 4d2; FFN с внутренней размерностью 4d — примерно 8d2. Итого ≈12d2 на блок, и для модели из L блоков со словарём V:

N ≈ 12 L d2 + V d

Это приближение не учитывает смещения, нормы, отдельную выходную матрицу эмбеддингов, GQA, gated-FFN и MoE; для конкретной архитектуры коэффициенты могут отличаться. Подставим d = 4096, L = 32, V = 128 000: получаем 12·32·40962 ≈ 6,4 млрд плюс ~0,5 млрд в эмбеддингах — модель класса «7B». В этой оценке 4d2 параметров внимания вдвое меньше, чем 8d2 параметров FFN. Из этого соотношения нельзя заключить, где именно модель хранит знания.

3.7. Итоги

Трансформер строится из одинаковых блоков «внимание + FFN» с остаточными связями и нормализацией. Внимание — это мягкий поиск: каждый токен запросом опрашивает ключи всех остальных и собирает взвешенную сумму их значений; формула softmax(QKT/√dk)V — центральная операция архитектуры. Причинная маска запрещает подглядывать в будущее, многоголовость даёт несколько обучаемых проекций, а позиционное кодирование сообщает модели порядок токенов. При обучении позиции можно обрабатывать параллельно, что хорошо соответствует GPU; авторегрессионная генерация при этом остаётся последовательной. Цена обычного полного внимания — квадратичное по длине число пар позиций.

Мы построили машину. В следующей главе — чем и как её кормят: предобучение, функция потерь, данные и законы масштабирования.

Упражнения

1. В коде из раздела 3.5 распечатайте матрицу весов A[0] и проверьте её свойства: треугольность, суммы строк. Уберите маску — что изменится?

2. Уберите деление на √dk и увеличьте d до 512. Посмотрите на строки матрицы A: насколько «острым» стало распределение внимания?

3. Посчитайте по формуле раздела 3.6 приблизительное число параметров модели с d = 8192, L = 64, V = 128 000. К какому классу моделей она относится?

404 Not Found

404 Not Found


nginx/1.24.0 (Ubuntu)

Связь с редакцией