Skip to Content
🌐 Русский03 · Внимание

03 · Внимание: как токены разговаривают друг с другом

🌐 English · Русский · Eesti

Вот задача, которую решает внимание (attention). По устройству из урока 01 каждая позиция должна предсказать свой следующий символ — но символ сам по себе почти бесполезен как контекст. Букве n в an… нужно знать, что перед ней была a; второй n в ann… нужно знать, что она — вторая n. Внимание — это механизм, который позволяет каждой позиции дотянуться назад, спросить «кто из тех, что были до меня, важен прямо сейчас?» и втянуть ровно ту информацию, которая ей нужна — одними лишь скалярными произведениями и софтмаксом. Эта единственная идея — буква «T» в GPT и причина, по которой эта архитектура захватила мир.

Прежде чем нырнуть. Внимание почти целиком построено из скалярного произведения (dot product) из 00 · Основы §3 — перемножь два списка чисел позиция за позицией, сложи и читай результат как меру похожести — плюс софтмакс (§5), чтобы превратить эти оценки в веса, дающие в сумме 1. Когда урок говорит, что q, k и v — это «обученное линейное отображение эмбеддинга», это значит: возьми вектор токена и умножь на матрицу обученных ручек — одна и та же операция, три разных набора ручек, три разных назначения. Делитель √d — просто ручка громкости, удерживающая оценки в комфортном диапазоне.

Теория

Self-attention спрашивает для каждого токена-запроса q_i: «насколько внимательно мне слушать значение v_j каждого предыдущего токена?» Рецепт:

  1. Спроецируй каждый токен в три вектора: запрос (query) q, ключ (key) k, значение (value) v. Каждый — обученное линейное отображение эмбеддинга токена.
  2. Оцени запрос против каждого ключа: score[i][j] = (q_i · k_j) / sqrt(d_head). Корень удерживает дисперсию стабильной при росте размерности головы.
  3. Примени причинную маску (causal mask) — токен i может видеть только токены j ≤ i. Прогони (замаскированную) строку через софтмакс, чтобы веса внимания давали в сумме 1.
  4. Возьми взвешенную сумму векторов значений: output_i = Σ_j softmax(score)[i][j] * v_j.

Весь слой делает это для каждой головы параллельно; каждая голова получает свой срез эмбеддинга и обучает собственные проекции запроса/ключа/значения. Выходы голов конкатенируются и проецируются обратно в размерность эмбеддинга через W_o.

Песочница ниже — лаборатория общения токенов для одной головы и одного выбранного тобой токена-запроса. Смотри, как запрос рассылает лучи оценок к каждому видимому ключу, как стена причинной маски перекрывает будущие токены, как сырые оценки превращаются в веса софтмакса (сумма — 1), и как взвешенные значения стекаются в смеситель, образуя output_i. Несколько вещей, которые картинка показывает явно:

  • Оценки и софтмакс — разные стадии. attention_logits (сырые q·k/√d) и attention_softmax (веса) снимаются с настоящей модели по отдельности — лучи сначала показывают оценки, а потом меняют подписи на веса.
  • Замаскированные будущие токены никогда не попадают в софтмакс. Оценку q_i·k_j для будущего ключа j > i вычислить можно (и лаборатория показывает стену маски над этими позициями исключительно чтобы правило было видно), но маска применяется до софтмакса — модель никогда не допускает будущий токен к участию, и веса нормируются только по j ≤ i.
  • Полоски Q/K/V — это метки, а не величины. Три цветные полоски под каждым токеном лишь помечают, какая проекция — запрос, ключ и значение; их длина условна. Реальные числа живут в панели разбора q·k.
  • Лучи значений — это взвешенная сумма. Каждое видимое значение стекает в смеситель с меткой wⱼ·vⱼ; смеситель складывает их в output_i = Σ wⱼ·vⱼ.
  • Многоголовость настоящая, а не для красоты. Каждое кольцо в нижнем ряду показывает собственное софтмакс-распределение этой головы по тем же ключам — попереключай головы (или смотри на мини-столбики), и увидишь, что разные головы фокусируются на разных позициях.
  • Это одна выбранная голова. output_i здесь — выход именно этой головы. Полный слой гоняет все головы параллельно, затем конкатенирует их выходы и проецирует их через W_o.

Аннотированный код

Блок внимания живёт в src/microgpt_annotated.py, подраздел attention-multihead:

def gpt(token_id, pos_id, keys, values): # ...embedding + rmsnorm above this point... for li in range(n_layer): x_residual = x x = rmsnorm(x) q = linear(x, state_dict[f'layer{li}.attn_wq']) k = linear(x, state_dict[f'layer{li}.attn_wk']) v = linear(x, state_dict[f'layer{li}.attn_wv']) keys[li].append(k) values[li].append(v) x_attn = [] for h in range(n_head): hs = h * head_dim q_h = q[hs:hs+head_dim] k_h = [ki[hs:hs+head_dim] for ki in keys[li]] v_h = [vi[hs:hs+head_dim] for vi in values[li]] attn_logits = [sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5 for t in range(len(k_h))] attn_weights = softmax(attn_logits) head_out = [sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h))) for j in range(head_dim)] x_attn.extend(head_out) x = linear(x_attn, state_dict[f'layer{li}.attn_wo']) x = [a + b for a, b in zip(x, x_residual)] # ...MLP block follows...

Заметь: причинная структура встроена прямо в сигнатуру вызова — gpt() вызывается по одному разу на позицию, и KV-кэш (keys, values) растёт на одну строку с каждым вызовом. Порт на TypeScript в src/inference/model.ts вместо этого считает всю последовательность длины T за один вызов с явным циклом j ≤ i — та же математика, другой поток управления.

Песочница

Выбери строку (≤6 символов), голову (0–3) и токен-запрос i. Нажми Play и смотри фазы: токены → Q/K/V → оценки → маска → софтмакс → взвешенная сумма значений → многоголовость. Тумблеры показывают/прячут векторы Q/K/V, сырые оценки, веса софтмакса, замаскированное будущее и обзор всех голов. Кликни кнопку inspect q·k (или подпись луча) — увидишь поразмерный разбор скалярного произведения.

Два цвета несут две стадии, сверху вниз: оранжевые лучи — веса внимания от запроса к каждому видимому ключу (верхняя дорожка), а зелёные лучи — взвешенные значения wⱼ·vⱼ, текущие от каждого чипа Vⱼ вниз в выходной смеситель (нижняя дорожка). Будущий токен (j > i) замаскирован: его чип сереет, и он не получает ни оранжевого ребра, ни зелёного луча.

Попробуй сам.

  1. Загрузи anna, выбери запросом последнюю a и посмотри на оранжевые веса. Какой из более ранних символов больше всего волнует эту голову? Теперь попереключай головы (0–3) — согласны ли они между собой? (Не должны: каждая голова выучила собственную привычку.)
  2. Поставь запрос на первый символ. В его строке софтмакса ровно один видимый ключ — он сам. Каким обязан быть его вес внимания, даже до того, как ты посмотришь?
  3. Кликни кнопку inspect q·k и найди, какие размерности скалярного произведения вносят наибольший вклад. Мера похожести — это сумма поразмерных согласий; вот они, одно за другим.
  4. Понаблюдай за стеной маски. Посеревшие будущие токены не получают оранжевого луча вообще — убедись, что веса по видимому прошлому всё равно дают в сумме 1.
Last updated on