Перейти к содержимому
Ступень 44.4 Механизм внимания

Scaled dot-product self-attention

Разбираем формулу softmax(QKᵀ/√d_k)V по шагам с размерностями, выясняем, зачем делить на √d_k и как причинная маска не даёт модели подсматривать в будущее, и пишем всё на NumPy.

УРОКstage-4/4-4-attention/02-self-attention.mdx1 ч 40 минУрок 2 из 3
После урока вы сможете
  • Пройти формулу softmax(QK⊤/dk)V\mathrm{softmax}(QK^\top/\sqrt{d_k})V по шагам и назвать форму каждого промежуточного тензора
  • Объяснить, почему без деления на dk\sqrt{d_k} softmax «насыщается» и обучение замедляется, и показать это численно
  • Реализовать причинное self-attention на NumPy и сверить его с F.scaled_dot_product_attention в PyTorch

В прошлом уроке внимание было мягким поиском: запрос сравнивается с ключами, веса считаются через softmax, ответ — смесь значений. Теперь запишем ту формулу, которая стоит в каждом слое GPT, Qwen и DeepSeek:

Attention(Q,K,V)=softmax ⁣(QK⊤dk)V\mathrm{Attention}(Q, K, V) = \mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) V

Это масштабированное скалярное вниманиемасштабированное скалярное внимание (SDPA)EN scaled dot-product attention (SDPA)softmax(Q Kᵀ / √d_k) V — формула внимания трансформера (Vaswani и др., 2017). В PyTorch — F.scaled_dot_product_attention. из статьи «Attention Is All You Need». В PyTorch это одна строка, но у каждого символа есть причина. После урока вы сможете назвать форму каждого тензора, объяснить, зачем корень из dkd_k и что делает причинная маска, и напишете всё сами на NumPy. В модуле 4.6 эта функция станет сердцем вашей GPT.

На вход слою приходит матрица XX размера T×dmodelT \times d_{\text{model}}: TT токенов, по строке-эмбеддингу на токен (урок 4.3). Возьмём сквозной пример — китайскую идиому «карп прыгает через Врата дракона» в коротком русском варианте: «Карп прыгает через врата». Для простоты считаем, что каждое слово — один токен, так что T=4T = 4.

В самовниманиесамовниманиеEN self-attention запросы, ключи и значения получаются из одной матрицы XX тремя обучаемыми матрицами:

Q=XWQ,K=XWK,V=XWVQ = X W_Q, \qquad K = X W_K, \qquad V = X W_V

WQW_Q и WKW_K имеют размер dmodel×dkd_{\text{model}} \times d_k, WVW_V — dmodel×dvd_{\text{model}} \times d_v. Строка ii матрицы QQ — запрос токена ii, строка jj матрицы KK — его ключ, строка jj матрицы VV — его значение.

Зачем три разные матрицы? Один и тот же токен играет три роли:

  • запрос — «что я ищу». Для «прыгает»: «кто совершает действие?»;
  • ключ — «по каким признакам меня можно найти». У «Карп»: «существительное, живое, подлежащее»;
  • значение — «что я передам тому, кто на меня посмотрел». У «Карп»: смысл слова (рыба, сказочный сюжет).

Почему бы не сравнивать сами эмбеддинги, xi⋅xjx_i \cdot x_j? По двум причинам. Во-первых, такие оценки симметричны: насколько «прыгает» интересуется «Карпом», настолько и наоборот, а реальные связи несимметричны. Во-вторых, xi⋅xi=∥xi∥2x_i \cdot x_i = \lVert x_i \rVert^2 обычно больше всех остальных произведений, и токен смотрел бы в основном на себя. Отдельные WQW_Q и WKW_K снимают оба ограничения, а WVW_V позволяет передавать не те признаки, по которым токен нашли.

ШагОперацияФормаСмысл
1Q=XWQ, K=XWK, V=XWVQ = XW_Q,\ K = XW_K,\ V = XW_V(T,dk), (T,dk), (T,dv)(T, d_k),\ (T, d_k),\ (T, d_v)три роли каждого токена
2S=QK⊤S = QK^\top(T,T)(T, T)SijS_{ij} — насколько токен ii интересуется токеном jj
3S/dkS / \sqrt{d_k}(T,T)(T, T)возвращаем оценкам разумный масштаб
4маска(T,T)(T, T)будущие позиции получают −∞-\infty
5A=softmax(S)A = \mathrm{softmax}(S) по строкам(T,T)(T, T)строка ii — распределение внимания токена ii
6Y=AVY = AV(T,dv)(T, d_v)строка ii — взвешенное среднее значений
flowchart LR
X["X (T, d_model)"] --> Q["Q = X·W_Q"]
X --> K["K = X·W_K"]
X --> V["V = X·W_V"]
Q --> S["S = Q·Kᵀ (T, T)"]
K --> S
S --> SC["÷ √d_k"]
SC --> M["маска: будущее = −∞"]
M --> SM["softmax по строкам"]
SM --> Y["Y = A·V (T, d_v)"]
V --> Y
Scaled dot-product self-attention: шесть шагов

Матрицу SS называют оценками вниманияоценки внимания (attention scores)EN attention scores (logits)Сходство запроса с каждым ключом до softmax, в трансформере — q·k / √d_k. Могут быть любыми числами; именно их маскируют значением −∞. (scores), матрицу AA — весами вниманиявеса вниманияEN attention weightsРезультат softmax от оценок внимания. Неотрицательны, в сумме по ключам дают 1; показывают, «куда смотрит» запрос.. Обратите внимание на форму (T,T)(T, T): каждый токен сравнивается с каждым. Это та самая квадратичная стоимость, к которой мы вернёмся в уроке 3.

Возьмём три первых токена — «Карп прыгает через» — и маленькие векторы dk=dv=2d_k = d_v = 2. Пусть проекции уже сделаны:

Q=(200211),K=(100111),V=(10001055)Q = \begin{pmatrix} 2 & 0 \\ 0 & 2 \\ 1 & 1 \end{pmatrix}, \quad K = \begin{pmatrix} 1 & 0 \\ 0 & 1 \\ 1 & 1 \end{pmatrix}, \quad V = \begin{pmatrix} 10 & 0 \\ 0 & 10 \\ 5 & 5 \end{pmatrix}

Шаг 2. SijS_{ij} — скалярное произведение запроса ii и ключа jj. Например, S02=2⋅1+0⋅1=2S_{02} = 2 \cdot 1 + 0 \cdot 1 = 2:

S=QK⊤=(202022112)S = QK^\top = \begin{pmatrix} 2 & 0 & 2 \\ 0 & 2 & 2 \\ 1 & 1 & 2 \end{pmatrix}

Шаг 3. Делим на 2≈1.414\sqrt{2} \approx 1.414:

S2≈(1.4101.4101.411.410.710.711.41)\frac{S}{\sqrt 2} \approx \begin{pmatrix} 1.41 & 0 & 1.41 \\ 0 & 1.41 & 1.41 \\ 0.71 & 0.71 & 1.41 \end{pmatrix}

Шаги 4–5. Без маски первая строка дала бы веса (0.446, 0.108, 0.446)(0.446,\ 0.108,\ 0.446): «Карп» почти половину внимания отдал бы слову «через», которое стоит после него. Для языковой модели это подглядывание в ответ. С маской:

A=(1000.1960.80400.2480.2480.503)A = \begin{pmatrix} 1 & 0 & 0 \\ 0.196 & 0.804 & 0 \\ 0.248 & 0.248 & 0.503 \end{pmatrix}

Вторая строка — softmax(0,1.41)=(0.196,0.804)\mathrm{softmax}(0, 1.41) = (0.196, 0.804), третья — softmax(0.71,0.71,1.41)\mathrm{softmax}(0.71, 0.71, 1.41).

Шаг 6. Выход — веса, умноженные на значения:

Y=AV=(1001.968.0455)Y = AV = \begin{pmatrix} 10 & 0 \\ 1.96 & 8.04 \\ 5 & 5 \end{pmatrix}

Первый токен видит только себя и получает своё же значение. Второй на 80% взял значение «прыгает» и на 20% — «Карп». Новый вектор каждого токена — это смесь информации из тех токенов, на которые он посмотрел. Ровно этот пример проверяет один из тестов задания ниже.

В настоящем коде на вход приходит тензор (B,T,dmodel)(B, T, d_{\text{model}}): BB текстов сразу. Оператор @ в NumPy и PyTorch умножает две последние оси и транслирует (broadcasting) остальные, поэтому формулы не меняются. Только транспонировать KK нужно по двум последним осям: np.swapaxes(K, -1, -2) или k.transpose(-2, -1).

Скалярное произведение — сумма dkd_k слагаемых: q⋅k=∑i=1dkqikiq \cdot k = \sum_{i=1}^{d_k} q_i k_i. Пусть компоненты qq и kk независимы, со средним 0 и дисперсией 1 (примерно так и есть в начале обучения, когда вход нормализован). Напомним из модуля 1.3: дисперсиядисперсияEN varianceVar[X] = E[(X − E[X])²] — средний квадрат отклонения от матожидания. — средний квадрат отклонения от среднего, а корень из неё, стандартное отклонение, — типичный размах значений; E[⋅]\mathbb{E}[\cdot] — среднее значение (матожидание). Тогда у каждого слагаемого среднее 0 и дисперсия E[qi2] E[ki2]=1\mathbb{E}[q_i^2]\,\mathbb{E}[k_i^2] = 1. Дисперсии независимых слагаемых складываются:

Var(q⋅k)=dk,стандартное отклонение=dk\mathrm{Var}(q \cdot k) = d_k, \qquad \text{стандартное отклонение} = \sqrt{d_k}

Что здесь написано: чем длиннее векторы, тем сильнее разброс оценок. У Llama 3 и Qwen размер головы dk=128d_k = 128. При единичной дисперсии компонент стандартное отклонение оценок было бы 128≈11.3\sqrt{128} \approx 11.3, а отдельные оценки доходили бы до ±25\pm 25 и дальше. После деления на dk\sqrt{d_k} дисперсия снова равна 1 при любом dkd_k.

Из урока 1.3.2 вы знаете: разница логитов на 1 — это разница вероятностей в ee раз. Разница в 20 — в полмиллиарда раз. Softmax превращается почти в one-hot: весь вес у одного токена.

Плохо не только то, что внимание становится «жёстким», но и то, что оно перестаёт учиться. Производные softmax:

∂pi∂zi=pi(1−pi),∂pi∂zj=−pipj\frac{\partial p_i}{\partial z_i} = p_i (1 - p_i), \qquad \frac{\partial p_i}{\partial z_j} = -p_i p_j

Если p=(0.99,0.01,… )p = (0.99, 0.01, \dots), то p1(1−p1)≈0.0099p_1(1 - p_1) \approx 0.0099, а при p1=0.25p_1 = 0.25 — уже 0.1875, в 19 раз больше. Хуже всего ключам, на которые голова почти не смотрит. Градиент по оценке ziz_i равен pi(gi−∑jpjgj)p_i\big(g_i - \sum_j p_j g_j\big), где gjg_j — градиент loss по весу pjp_j. Если pi≈0p_i \approx 0, то и этот градиент почти 0. Голова «залипает» на текущем фаворите: даже если другой токен был бы полезнее, сигнал «посмотри туда» до его оценки почти не доходит, и модель плохо учится, куда смотреть. Это та же беда, что и у сигмоиды в насыщении.

Возьмём случайные запросы и ключи с единичной дисперсией компонент, по 16 ключей на запрос, и посмотрим на разброс оценок, средний максимальный вес и норму якобиана softmax (насколько сильно градиент проходит насквозь):

import numpy as np
rng = np.random.default_rng(0)
T = 16 # сколько ключей видит каждый запрос
def softmax(z):
z = z - z.max(axis=-1, keepdims=True)
e = np.exp(z)
return e / e.sum(axis=-1, keepdims=True)
def jac_norm(p):
"""Норма якобиана softmax: diag(p) - p pᵀ. Чем она меньше, тем слабее градиент."""
# Якобиан (урок 1.2.3) — таблица всех производных ∂p_i/∂z_j, формулы см. выше. Для каждой из n строк p:
# первый einsum кладёт p на диагональ матрицы (diag(p)), второй строит матрицу p_i * p_j (внешнее произведение)
J = np.einsum("ni,ij->nij", p, np.eye(p.shape[-1])) - np.einsum("ni,nj->nij", p, p)
return np.linalg.norm(J, axis=(1, 2)).mean()
print(" d_k std(q·k) max вес: без / с √d градиент: без / с √d")
for d_k in [4, 16, 64, 128, 512]:
q = rng.standard_normal((2000, d_k)) # 2000 случайных запросов
k = rng.standard_normal((2000, T, d_k)) # у каждого — 16 случайных ключей
s = np.einsum("nd,ntd->nt", q, k) # скалярные произведения, (2000, 16)
p_raw, p_sc = softmax(s), softmax(s / np.sqrt(d_k))
print(f"{d_k:4d} {s.std():9.2f} {p_raw.max(-1).mean():11.2f} / {p_sc.max(-1).mean():.2f}"
f" {jac_norm(p_raw):13.3f} / {jac_norm(p_sc):.3f}")
d_k std(q·k) max вес: без / с √d градиент: без / с √d
4 2.00 0.43 / 0.24 0.322 / 0.297
16 4.05 0.68 / 0.25 0.279 / 0.304
64 8.04 0.85 / 0.25 0.175 / 0.305
128 11.26 0.89 / 0.25 0.136 / 0.304
512 22.61 0.95 / 0.25 0.072 / 0.305

Разброс оценок растёт точно как dk\sqrt{d_k}: 2, 4, 8, 11.3, 22.6. Без масштаба при dk=512d_k = 512 один ключ из 16 забирает в среднем 95% веса, а градиент через softmax слабее в 4 раза. С делением на dk\sqrt{d_k} картина одинакова при любой размерности. Это и есть цель: поведение слоя не должно зависеть от того, насколько широкую модель вы построили.

Тонкость для любознательных: при делении на dk\sqrt{d_k} градиент к WQW_Q и WKW_K по правилу цепочки ещё и умножается на 1/dk1/\sqrt{d_k}. Так что дело не в абсолютной величине градиента (её всё равно выравнивает Adam), а в том, как он распределён: насыщенный softmax отдаёт почти весь сигнал одному «победителю».

Языковая модель предсказывает следующий токен (урок 4.1.1). При обучении мы подаём весь текст «Карп прыгает через врата» и требуем на каждой позиции предсказать следующий токен: после «Карп» — «прыгает», после «Карп прыгает» — «через» и так далее. На вход идёт настоящий текст, а не то, что сгенерировала сама модель, — это teacher forcingteacher forcing (обучение на верном префиксе)EN teacher forcingПри обучении модель на каждом шаге видит настоящий текст, а не свои предсказания. Поэтому все позиции можно обучать параллельно.. RNN считала бы эти TT предсказаний шаг за шагом, а трансформер делает их за один параллельный проход: TT обучающих примеров сразу. Это одна из главных причин, почему трансформеры учатся так быстро.

Но в self-attention без ограничений токен «Карп» видит «прыгает» и может просто скопировать ответ. Модель научится жульничать: loss на обучении упадёт почти до нуля, а при генерации, где будущего ещё нет, она ничего не сможет. Нужно правило: токен ii смотрит только на позиции j≤ij \le i. Это причинная маскапричинная (каузальная) маскаEN causal maskЗапрещает токену смотреть на будущие позиции — оценки выше диагонали заменяются на −∞ до softmax. Нужна, чтобы GPT-подобная модель училась предсказывать следующий токен на всех позициях сразу..

Перед softmax ставим −∞-\infty во все клетки выше диагонали:

Карппрыгаетчерезврата
Карпs00s_{00}−∞-\infty−∞-\infty−∞-\infty
прыгаетs10s_{10}s11s_{11}−∞-\infty−∞-\infty
черезs20s_{20}s21s_{21}s22s_{22}−∞-\infty
вратаs30s_{30}s31s_{31}s32s_{32}s33s_{33}

Так как e−∞=0e^{-\infty} = 0, будущее получает вес ровно ноль, а softmax нормирует оставшиеся позиции, и строка по-прежнему в сумме даёт 1. Матрица весов становится нижнетреугольной. В NumPy маску удобно строить так: np.tril(np.ones((T, T), dtype=bool)) — True там, куда смотреть можно.

  • Маска нулём вместо −∞-\infty. Нулевая оценка даёт вес e0=1e^0 = 1, будущее остаётся видимым. Тесты задания ловят это сразу.
  • Маска после softmax. Если обнулить веса будущего уже после softmax, строки перестанут суммироваться в 1. Маскируйте оценки, а не веса.
  • Разные соглашения о True. В нашем коде и в F.scaled_dot_product_attention булева маска означает «можно смотреть». В nn.MultiheadAttention наоборот: True значит «запрещено». Перед использованием чужой функции читайте документацию.
  • Полностью закрытая строка. Причинная маска всегда оставляет диагональ, но маска паддинга (позиций с токеном-заполнителем PAD из урока 4.2.1, на которые смотреть нельзя) может закрыть строку целиком. Тогда все оценки −∞-\infty, и softmax даст 0/0=0/0 = nan. Поэтому в коде часто пишут не −∞-\infty, а очень большое отрицательное число.
  • Маска на одну позицию шире. np.tril(..., k=1) открывает позицию i+1i + 1 — ровно тот токен, который нужно предсказать. Ничего не падает: loss на обучении подозрительно низкий, а генерация — мусор. Как поймать такой баг тестом — в уроке 11.1.2.

Без маски получается двунаправленное внимание, как в BERT: каждый токен видит весь текст. Это хорошо для понимания и классификации, но генерировать так нельзя. Все авторегрессионные LLM, то есть генерирующие текст токен за токеном (GPT, Llama, Qwen, DeepSeek), — с причинной маской. Подробнее о семействах архитектур — в уроке 4.5.1.

Строка ii матрицы AA показывает, куда смотрит токен ii. Если нарисовать её у обученной модели, увидите осмысленные картины: местоимение смотрит на существительное, глагол — на подлежащее. Но есть и странность: во многих головах огромная доля веса уходит на самый первый токен текста, даже если он ничего не значит.

Причина в softmax: веса обязаны давать в сумме 1. Если голове «нечего сказать» про текущий токен, вес всё равно нужно куда-то деть. Первый токен виден со всех позиций (маска его никогда не закрывает), и модель учится сваливать лишний вес туда. Явление назвали attention sink«сток» внимания (attention sink)EN attention sinkПервые токены получают аномально большой вес внимания независимо от смысла — туда softmax «сливает» лишнюю массу. Не путайте со сходным китайским термином из d2l-zh («схождение внимания»): там так переведено attention pooling (взвешенное усреднение значений). («сток внимания») в работе StreamingLLM (Xiao и соавт., 2023). Там же нашли практическое следствие. Если при генерации длинного текста хранить ключи и значения только последних токенов («скользящее окно»), качество рушится, как только первые токены выпадают из окна. Стоит сохранить ключи и значения четырёх первых токенов, и модель снова работает нормально. Как от стока избавились в Qwen — ниже.

Реализуйте causal_mask, softmax, attention и causal_self_attention. Тесты проверят пример из урока, масштаб 1/dk1/\sqrt{d_k}, причинность, совпадение с эталоном-циклом и работу на батче.

Причинное self-attention на NumPynumpy · нужен интернет7 тестов
import numpy as np


def causal_mask(T):
    """Причинная маска «куда можно смотреть» формы (T, T), dtype=bool.

    mask[i, j] = True, если токен i может смотреть на токен j, то есть j <= i
    (прошлое и сам токен). Будущее (j > i) — False.
    Для T = 3:
        [[ True, False, False],
         [ True,  True, False],
         [ True,  True,  True]]
    """
    # TODO: одна строка (подсказка: np.tril)
    ...


def softmax(x):
    """Устойчивый softmax по последней оси (как в уроке 1.3, но для массивов).

    Должен работать со строками, где есть -np.inf: exp(-inf) = 0, такие позиции получают вес 0.
    """
    # TODO: вычтите максимум по последней оси (keepdims=True), возьмите exp, поделите на сумму
    ...


def attention(Q, K, V, mask=None):
    """Scaled dot-product attention.

    Q: (..., T_q, d_k), K: (..., T_k, d_k), V: (..., T_k, d_v) — «...» может быть пустым или батчем.
    mask: None или bool-массив (T_q, T_k); False — туда смотреть нельзя (оценка = -inf).
    Возвращает кортеж (out, weights):
        weights = softmax(Q Kᵀ / sqrt(d_k)) — форма (..., T_q, T_k), строки в сумме дают 1;
        out = weights @ V — форма (..., T_q, d_v).
    """
    # TODO: 1) scores = Q @ Kᵀ / sqrt(d_k)  (транспонируйте две последние оси: np.swapaxes(K, -1, -2))
    #       2) если mask задана — np.where(mask, scores, -np.inf)
    #       3) weights = softmax(scores); 4) out = weights @ V
    ...


def causal_self_attention(X, W_q, W_k, W_v):
    """Причинное self-attention для последовательности эмбеддингов.

    X: (T, d_model) или (B, T, d_model); W_q, W_k: (d_model, d_k); W_v: (d_model, d_v).
    Возвращает (out, weights) как attention(): out — (..., T, d_v), weights — (..., T, T).
    """
    # TODO: Q = X @ W_q, K = X @ W_k, V = X @ W_v; T = X.shape[-2]; attention(Q, K, V, causal_mask(T))
    ...


# Проверьте себя кнопкой «Запустить» — пример из урока:
Q = np.array([[2.0, 0.0], [0.0, 2.0], [1.0, 1.0]])
K = np.array([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
V = np.array([[10.0, 0.0], [0.0, 10.0], [5.0, 5.0]])
print(causal_mask(3))
res = attention(Q, K, V, causal_mask(3))
if res is not None:
    out, w = res
    print(np.round(w, 3))    # ожидается [[1 0 0] [0.196 0.804 0] [0.248 0.248 0.503]]
    print(np.round(out, 3))  # ожидается [[10 0] [1.956 8.044] [5 5]]
import math
import torch
import torch.nn.functional as F
torch.manual_seed(0)
def attention(q, k, v, causal=True):
"""softmax(Q Kᵀ / √d_k) V с причинной маской. q, k, v: (B, T, d)."""
d_k = q.size(-1)
scores = q @ k.transpose(-2, -1) / math.sqrt(d_k) # (B, T, T)
if causal:
T = q.size(-2)
allowed = torch.ones(T, T, dtype=torch.bool).tril() # True — смотреть можно
# ~ — логическое «НЕ»: ~allowed = True там, где смотреть НЕЛЬЗЯ; туда пишем −∞
scores = scores.masked_fill(~allowed, float("-inf"))
weights = scores.softmax(dim=-1) # строки в сумме дают 1
return weights @ v, weights
B, T, d_model, d_k = 2, 4, 16, 8
x = torch.randn(B, T, d_model) # «эмбеддинги» 4 токенов
# три случайные матрицы (d_model, d_k); деление на √d_model держит выходы в разумном масштабе (инициализация, модуль 3.4)
W_q, W_k, W_v = (torch.randn(d_model, d_k) / math.sqrt(d_model) for _ in range(3))
q, k, v = x @ W_q, x @ W_k, x @ W_v # (B, T, d_k)
out, w = attention(q, k, v)
ref = F.scaled_dot_product_attention(q, k, v, is_causal=True) # встроенная версия (FlashAttention на GPU)
print("формы:", tuple(out.shape), tuple(w.shape))
print("совпадает с PyTorch:", torch.allclose(out, ref, atol=1e-6))
print(w[0].round(decimals=2))

Ожидаемый вывод:

формы: (2, 4, 8) (2, 4, 4)
совпадает с PyTorch: True
tensor([[1.0000, 0.0000, 0.0000, 0.0000],
[0.3900, 0.6100, 0.0000, 0.0000],
[0.4400, 0.3100, 0.2500, 0.0000],
[0.3500, 0.2400, 0.1500, 0.2600]])

Нули выше диагонали — работа маски. В настоящих моделях вы будете вызывать F.scaled_dot_product_attention: на GPU она выбирает быстрое ядро (FlashAttention), которое не хранит матрицу T×TT \times T целиком. Но считает она ровно то же, что ваши десять строк, и теперь вы это проверили.

Проверь себя6 вопросов
1 Компоненты qq и kk независимы, среднее 0, дисперсия 1, dk=64d_k = 64. Чему равно стандартное отклонение q⋅kq \cdot k без масштабирования?
2 Батч B=2B = 2, длина T=10T = 10, dk=64d_k = 64. Какая форма у QK⊤QK^\top?
3 Что произойдёт, если при большом dkd_k забыть делить на dk\sqrt{d_k}? Выберите все верные.Можно выбрать несколько ответов
4 Почему в причинной маске ставят −∞-\infty, а не 0?
5 В примере из урока у второго токена после масштабирования оценки (0,1.41)(0, 1.41), третья позиция замаскирована. Какой вес получит первый токен? Округлите до трёх знаков.
6 Что даёт причинная маска при обучении GPT?
  • Self-attention: Q=XWQQ = XW_Q, K=XWKK = XW_K, V=XWVV = XW_V, затем softmax(QK⊤/dk)V\mathrm{softmax}(QK^\top/\sqrt{d_k})V. Оценки и веса имеют форму (T,T)(T, T), выход — (T,dv)(T, d_v).
  • Три матрицы нужны, потому что токен играет три роли: что ищет, по чему его находят, что он отдаёт.
  • Деление на dk\sqrt{d_k} возвращает оценкам дисперсию 1. Без него softmax насыщается и плохо пропускает градиент.
  • Причинная маска (−∞-\infty выше диагонали до softmax) запрещает смотреть в будущее и позволяет учиться на всех позициях за один проход.
  • Softmax обязан раздать весь вес, отсюда attention sink. Китайские команды лечат «перегрев» оценок гейтами (Qwen) и QK-Clip (Kimi).

Дальше: одна голова умеет задавать токену только один «вопрос» за раз. Как дать ей несколько — и во что это обходится по памяти и вычислениям — в уроке «Многоголовое внимание».