Заглядываем внутрь трансформера: logit lens, индукционные головы, patching
Вводный урок модуля об интерпретируемости и безопасности. Раскладываем остаточный поток на вклады голов, читаем промежуточные слои через logit lens, находим индукционные головы и доказываем их роль абляцией и activation patching. Практика — модель в браузере, своя GPT из 4.6 и GPT-2 small в TransformerLens.
Модуль 9.3: что внутри и можно ли этому доверять
Заголовок раздела «Модуль 9.3: что внутри и можно ли этому доверять»К этому моменту вы умеете собрать LLM от токенизатора до агента. Последний модуль ступени задаёт неудобные вопросы: что модель на самом деле вычисляет, как заставить её вести себя так, как мы хотим, как проверить, что она не опасна, и что можно и нельзя делать с данными и моделями по закону. Пять уроков идут от устройства модели к обществу:
| Урок | Вопрос | Главные инструменты |
|---|---|---|
| 9.3.1 (этот) | Что происходит внутри трансформера? | logit lens, паттерны внимания, абляция, activation patching, зонды |
| 9.3.2 | Почему нейроны многозначны и как это распутать? | суперпозиция, разреженные автоэнкодеры, графы атрибуции |
| 9.3.3 | Чего мы хотим от модели и как этого добиться? | спецификации, RLHF/DPO, Constitutional AI, масштабируемый надзор |
| 9.3.4 | Как проверить, что модель не опасна? | оценки опасных возможностей, red teaming, guard-модели, политики лабораторий |
| 9.3.5 | Что говорят этика и право? | смещения, авторское право, персональные данные, регулирование ИИ |
Интерпретируемость идёт первой: выравнивание и оценки безопасности смотрят на модель по её ответам, а она пытается заглянуть внутрь. Здесь создатели моделей делятся результатами охотнее всего: Anthropic публикует интерактивные статьи серии Transformer Circuits, Google DeepMind выложила открытые разреженные автоэнкодеры для Gemma, а сообщество сделало бесплатный практикум ARENA.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В уроке 4.6.2 вы написали GPT своими руками. Но код описывает только архитектуру. Алгоритм, по которому обученная модель ставит запятую или вспоминает столицу, нашёл градиентный спуск, и записан он в сотнях тысяч чисел, которые нельзя просто прочитать.
Механистическая интерпретируемостьмеханистическая интерпретируемостьEN mechanistic interpretabilityОбратная разработка обученной нейросети — поиск внутри неё понятных алгоритмов (схем) и доказательство вмешательством, что модель действительно их использует. пытается провести обратную разработку (reverse engineering) этих алгоритмов: найти внутри модели понятные шаги и доказать, что модель действительно использует именно их. Это нужно по трём причинам:
- Отладка: какой компонент виноват в ошибке?
- Безопасность: если модель научится обманывать, по ответам это может быть не видно, а по внутренним состояниям — возможно.
- Наука: понять, как устроены обучение в контексте, память фактов, рассуждения.
Поле молодое: полностью «прочитать» большую модель пока не умеет никто. Но несколько механизмов поняты по-настоящему, и инструменты для их поиска вы освоите в этом уроке.
Остаточный поток: всё складывается
Заголовок раздела «Остаточный поток: всё складывается»Напоминание
Заголовок раздела «Напоминание»В уроке 3.4.2 и уроке 4.5.1 мы назвали вектор, который течёт сквозь модель, остаточным потокомостаточный потокEN residual streamВектор шириной d, который течёт сквозь все блоки. Каждый подслой читает из него и дописывает поправку, так что выход = вход + сумма всех поправок.. Каждый подслой читает из него и прибавляет свою поправку. Если развернуть все блоки, итоговый вектор последней позиции — это сумма:
Словами: эмбеддинг плюс вклад каждой головы каждого слоя плюс вклад каждого MLP. В уроке 4.4.3 мы проверили, что выход многоголового внимания — тоже сумма вкладов отдельных голов: склейка голов и умножение на равны сумме .
Главное следствие: прямая атрибуция логитов
Заголовок раздела «Главное следствие: прямая атрибуция логитов»Логиты получаются так: , где — матрица LM-головы, а — финальная нормализация. Вычитание среднего и умножение на в ней линейны, нелинейно только деление на стандартное отклонение . Если считать константой (она одна на всю позицию), логит токена раскладывается на слагаемые:
где — столбец для токена , а пробегает все головы и MLP. Точнее, — этот столбец, умноженный поэлементно на и центрированный: так линейные части нормализации «вшивают» в , а сдвиг даёт слагаемое, одинаковое для всех компонентов, и в сумму вкладов не входит. Каждое слагаемое отвечает на вопрос «насколько компонент сам, напрямую, толкает модель к токену ». Это прямая атрибуция логитовпрямая атрибуция логитов (DLA)EN direct logit attributionВклад компонента в логит токена t — скалярное произведение его выхода со столбцом W_U для t (при замороженном масштабе финальной нормализации). Видит только прямые вклады. (direct logit attribution, DLA).
Обычно смотрят не на один логит, а на разность двух: «Paris, а не Rome». Разность убирает всё, что одинаково поднимает оба токена. Посчитаем на игрушечных числах с (для простоты без нормализации):
import numpy as np
# Вклады трёх «авторов» в остаточный поток последней позиции (d = 4, без нормализации)parts = { "эмбеддинг": np.array([1.0, 0.0, 0.5, 0.0]), "голова 1": np.array([0.0, 2.0, 0.0, 0.5]), "MLP 2": np.array([0.5, 0.5, 1.0, 0.0]),}x = sum(parts.values()) # итоговый поток = сумма вкладовu_paris = np.array([0.0, 1.0, 0.5, 0.0]) # столбцы W_U для двух токеновu_rome = np.array([0.5, 0.0, 0.0, 1.0])u = u_paris - u_rome # направление «Paris, а не Rome»
print(f"logit(Paris) - logit(Rome) = {x @ u:.2f}")for name, p in parts.items(): print(f" вклад «{name}»: {p @ u:+.2f}")logit(Paris) - logit(Rome) = 2.00 вклад «эмбеддинг»: -0.25 вклад «голова 1»: +1.50 вклад «MLP 2»: +0.75Три вклада в сумме дают ровно 2.00 — линейность работает. Голова 1 сделала большую часть работы, эмбеддинг даже немного мешал. В настоящей модели таких слагаемых сотни, и DLA за один прогон показывает, какие головы напрямую пишут ответ.
Но запомните слово «напрямую». Голова может не писать ответ сама, а готовить данные для другой головы. DLA такой вклад не увидит, и в практике вы встретите именно этот случай.
Logit lens: что модель думает на полпути
Заголовок раздела «Logit lens: что модель думает на полпути»Раз все слои пишут в одно и то же пространство, можно взять остаточный поток после промежуточного слоя и применить к нему финальную нормализацию и LM-голову, как будто модель на этом слое закончилась. Получится распределение «что бы модель предсказала, если бы остановилась здесь». Этот приём назвали logit lenslogit lens («логит-линза»)EN logit lensПрименить финальную нормализацию и LM-голову к остаточному потоку промежуточного слоя — «что модель предсказала бы, если бы остановилась здесь» (nostalgebraist, 2020).. Его описал автор под ником nostalgebraist в посте «interpreting GPT: the logit lens» на LessWrong (2020). На GPT-2 он увидел, что первые слои угадывают самые частые слова вроде «the» и «of», а правильный ответ появляется в верхушке списка только на определённой глубине и дальше держится.
Посмотрим на вашу модель из урока 4.6.2 (полный скрипт — в разделе «Практика»). Средний loss на валидации, если предсказывать по потоку после каждого блока:
эмбеддинги: loss 12.82 после блока 0: loss 3.24 после блока 1: loss 2.94 после блока 2: loss 2.47 после блока 3: loss 1.64Предсказание уточняется слой за слоем: 3.24 → 2.94 → 2.47 → 1.64. Каждый блок добавляет в поток то, что приближает ответ. Странное число в первой строке тоже поучительно. Loss 12.82 намного хуже случайного угадывания (). Причина — weight tying из урока 4.6.2: у LM-головы та же матрица, что у эмбеддингов, поэтому «голый» эмбеддинг символа сильнее всего похож на самого себя. Модель уверенно предсказывает, что текущий символ повторится, а символы почти никогда не повторяются подряд.
Ограничения метода:
- Ранние слои могут хранить «черновик» ответа в другом базисе, и финальная LM-голова его не прочитает. Поэтому Белроуз и соавторы (2023) предложили tuned lens: для каждого слоя обучают маленькое линейное преобразование, которое переводит поток в «язык» последнего слоя.
- Logit lens показывает, что закодировано в потоке. Почему оно там оказалось и какой компонент это сделал, он не говорит.
Паттерны внимания и индукционные головы
Заголовок раздела «Паттерны внимания и индукционные головы»Что видно в паттерне
Заголовок раздела «Что видно в паттерне»Паттерн вниманияпаттерн вниманияEN attention patternМатрица весов внимания одной головы размера T×T; строка i показывает, как позиция-запрос i распределила внимание между позициями 0…i. — это матрица весов одной головы из урока 4.4.2: строка показывает, как позиция-запрос распределила внимание между позициями . Если нарисовать её как картинку, у разных голов проступают узнаваемые узоры:
- голова предыдущего токена — яркая полоса сразу под диагональю: каждая позиция смотрит на соседа слева;
- «голова начала» — яркий первый столбец: всё внимание уходит на первый токен (часто это способ «никуда не смотреть»);
- голова дубликатов — смотрит на прошлые появления того же токена;
- индукционная голова — самая интересная, о ней дальше.
Индукция: как модель продолжает уже виденное
Заголовок раздела «Индукция: как модель продолжает уже виденное»В уроке 4.4.3 мы коротко упомянули индукционные головыиндукционная головаEN induction headПара голов, которая находит в контексте прошлое появление текущего токена и копирует токен, шедший после него ([A][B] … [A] → [B]). Механизм in-context learning (Olsson и др., 2022). (Olsson и соавт., Anthropic, 2022). Теперь разберём механизм целиком. Задача: в тексте уже было «… Гарри Поттер …», и снова встретилось «Гарри». Хорошая модель должна предсказать «Поттер». Общее правило: .
Одной головой это не сделать. Чтобы найти позицию , нужно знать, что перед ней стоял , а внимание видит только содержимое позиций, но не их соседей. Поэтому работают две головы в разных слоях:
flowchart TD P["Слой 0, голова предыдущего токена:<br/>позиция B смотрит на соседа слева<br/>и пишет в свой поток «передо мной был A»"] Q["Слой 1, индукционная голова:<br/>запрос со второго A — «кто стоял после A?»"] K["ключ позиции B: «передо мной был A»<br/>совпадение — внимание на B"] O["копирует токен B в поток второго A"] R["предсказание: B"] P -->|"метка в остаточном потоке"| K Q --> K --> O --> R
Здесь полезен язык из «A Mathematical Framework for Transformer Circuits». У каждой головы две независимые части:
- QK-цепь решает, куда смотреть: какие пары «запрос — ключ» получат большой вес.
- OV-цепь решает, что переносить: какой вектор попадёт в поток позиции-запроса.
В индукционной схеме OV-цепь первой головы переносит «кто я» соседу справа, а QK-цепь второй головы сравнивает «кто я» запроса с «кто был до тебя» ключа. Ключи второй головы читают то, что записала первая. Такое сцепление голов называют K-композицией. Совокупность компонентов, которые вместе реализуют один алгоритм, называют схемойсхема (цепь) в нейросетиEN circuitНабор компонентов (головы, нейроны, признаки) и связей между ними, которые вместе реализуют один алгоритм, например индукцию. (circuit).
Как найти индукционную голову
Заголовок раздела «Как найти индукционную голову»Самый надёжный тест — повторённые случайные токены: длиной . В случайной последовательности нет ни грамматики, ни смысла, поэтому вторую половину можно предсказать единственным способом — скопировать из первой. Индукционная голова на позиции второй половины смотрит на позицию : туда, где в первый раз стоял следующий токен. На картинке паттерна это яркая полоса, сдвинутая на влево от диагонали. Индукционный счёт головы — средний вес на этой полосе:
Числа: при позиция должна смотреть на позицию . У идеальной индукционной головы счёт равен 1, у головы, размазывающей внимание равномерно, — около , то есть меньше 0.1.
Что показали Olsson и соавторы
Заголовок раздела «Что показали Olsson и соавторы»- Индукционные головы появляются скачком, в узком окне в начале обучения — авторы называют это фазовым переходом. В тот же момент на кривой loss виден небольшой «бугорок», и резко улучшается обучение в контексте (их метрика: насколько loss на 500-м токене контекста ниже, чем на 50-м). В их моделях это происходило примерно после 2.5–5 млрд токенов.
- У однослойных моделей фазового перехода нет: схеме нужны минимум две головы в разных слоях.
- Для маленьких моделей только из внимания доказательства причинные (выключаешь головы — пропадает обучение в контексте). Для больших моделей с MLP — корреляционные. Авторы прямо пишут об этой разнице.
В GPT-2 small индукционными головами обычно называют 5.1, 5.5, 6.9, 7.2 и 7.10 (номер слоя, точка, номер головы), а головой предыдущего токена — 4.11. Эти номера вы проверите сами в «🖥 На вашей машине».
А вот вашей модели из 4.6.2 повезло меньше. Скрипт из практики даёт на повторённых фрагментах текста:
loss на 1-м повторе 1.73, на 2-м 1.82блок 0: индукционный счёт голов [0. 0. 0. 0.]блок 1: индукционный счёт голов [0. 0. 0. 0.]блок 2: индукционный счёт голов [0. 0. 0. 0.]блок 3: индукционный счёт голов [0. 0. 0. 0.]Второй повтор модель предсказывает не лучше первого: копировать из контекста она не умеет, индукционных голов нет. Это не ошибка, а масштаб. Наша модель видела около 8 млн символов, на сотни раз меньше, чем модели Olsson к моменту фазового перехода. Она выучила статистику соседних символов и слов, а до индукции не доросла.
Корреляция или причина: абляция и activation patching
Заголовок раздела «Корреляция или причина: абляция и activation patching»Паттерн — ещё не доказательство
Заголовок раздела «Паттерн — ещё не доказательство»Паттерн показывает, куда смотрит голова, но не важно ли это: её выход может никто не читать. Чтобы доказать роль компонента, в него нужно вмешаться и посмотреть, изменится ли поведение — как в контролируемом эксперименте.
Абляция
Заголовок раздела «Абляция»Самый простой способ — выключить компонент (об абляциях как о разделе статьи — в уроке 9.2.2) и измерить, насколько вырос loss. Варианты:
- zero-ablation — заменить выход головы нулём. Просто, но модель никогда не видела здесь нулей, и падение качества может объясняться «шоком», а не потерей функции;
- mean-ablation — заменить выход средним по многим текстам: голова перестаёт различать примеры, а масштаб остаётся привычным. Обычно это честнее.
В практике вы увидите эффект, который DLA пропускает: голова предыдущего токена ничего не пишет в выход, её прямой вклад в логиты нулевой. Но если её выключить, индукционная голова теряет метки и ломается. Причинный метод видит композицию, наблюдательный — нет.
Activation patching
Заголовок раздела «Activation patching»Абляция грубая. Чтобы узнать, где именно (слой, позиция) лежит информация для ответа, придумали activation patchingподмена активаций (activation patching)EN activation patchingПричинный метод. Активацию в одной точке (слой, позиция) из «чистого» прогона подставляют в «испорченный» и меряют, какую долю правильного ответа это восстанавливает. (подмену активаций). Возьмём два почти одинаковых промпта:
- чистый (clean): «When John and Mary went to the shops, John gave the bag to» → правильный ответ « Mary»;
- испорченный (corrupted): «When John and Mary went to the shops, Mary gave the bag to» → теперь ответ « John».
Отличается одно слово, длина в токенах одинаковая. Метрика — разность логитов : на чистом промпте она положительная, на испорченном отрицательная. Запускаем испорченный промпт, но в одной точке (слой , позиция ) подставляем активацию из чистого прогона. Если метрика вернулась к «чистому» значению, вся нужная информация была в этой точке. Доля восстановления:
Словами: какую часть пути от испорченного ответа к чистому прошла метрика после подмены. Пример: , , после подмены . Тогда : эта точка объясняет почти три четверти эффекта.
Пройдя по всем слоям и позициям, получают карту «где живёт ответ». Метод прославили две работы:
- ROME (Meng, Bau, Andonian, Belinkov, NeurIPS 2022). Подменяя активации (авторы называют это causal tracing), они нашли, что для фактов вроде «Эйфелева башня находится в …» решающую роль играют MLP средних слоёв на последнем токене названия объекта. Затем они научились править факт изменением весов одного такого MLP (Rank-One Model Editing).
- IOI (Wang и соавт., Redwood Research, 2022). Для задачи с Джоном и Мэри выше в GPT-2 small нашли схему из 26 голов в 7 функциональных группах. Головы, которые переносят имя в ответ, назвали name movers.
Линейные зонды и их пределы
Заголовок раздела «Линейные зонды и их пределы»Линейный зондлинейный зондEN linear probeЛогистическая или линейная регрессия на замороженных активациях модели. Показывает, что свойство линейно извлекаемо, но не доказывает, что модель его использует. мы упоминали в уроке 2.2.2: на скрытых состояниях замороженной модели учат логистическую регрессию предсказывать свойство — часть речи, истинность утверждения, клетку на доске. Если зонд справляется, информация линейно закодирована в активациях. Подвохов два:
- Зонд может выучить задачу сам. Хьюитт и Лян (EMNLP 2019) дают тому же зонду бессмысленную контрольную разметку (случайную, но согласованную метку для каждого слова). Если зонд хорошо учит и её, высокая точность говорит о силе зонда, а не о модели. Разницу точностей они назвали селективностью.
- Извлекаемо — не значит используется. Зонд корреляционный: модель может решать по другим признакам. Проверка та же — вмешаться: сдвинуть активацию вдоль направления зонда и посмотреть, меняется ли поведение.
Поучительная история — Othello-GPT. Модель учили только предсказывать ходы в игре «Отелло», без доски. Исходная работа нашла в ней представление доски, но извлекалось оно нелинейными зондами. Нил Нанда затем показал, что представление линейно, если кодировать клетки не как «чёрная/белая», а как «моя/чужая» (относительно игрока, который сейчас ходит). Вывод: неудача линейного зонда может означать, что вы ищете не те признаки, а не что их нет. Идея «понятия — это направления в пространстве активаций» станет центральной в следующем уроке.
| Метод | Что показывает | Тип доказательства | Главная ловушка |
|---|---|---|---|
| DLA | кто напрямую пишет ответ | наблюдение | не видит косвенных вкладов |
| Logit lens | что закодировано на глубине | наблюдение | ранние слои в «другом базисе» |
| Паттерн внимания | куда смотрит голова | наблюдение | «смотрит» ≠ «важно» |
| Линейный зонд | извлекаема ли информация | корреляция | извлекаемо ≠ используется |
| Абляция | нужен ли компонент | вмешательство | выводит модель из привычного режима |
| Activation patching | где лежит нужная информация | вмешательство | зависит от пары промптов и метрики |
Практика
Заголовок раздела «Практика»В браузере: найдите индукционную голову
Заголовок раздела «В браузере: найдите индукционную голову»В задании — двухслойный трансформер только из внимания, по три головы в слое. Веса заданы заранее, головы перемешаны.
По паттернам найдите голову предыдущего токена и индукционную, через logit lens посмотрите, на какой глубине появляется
предсказание, а абляцией докажите, что схема держится ровно на двух головах. После решения откройте load_model: там
QK- и OV-цепи из раздела выше записаны числами.
import numpy as np
# ── Ваша часть ─────────────────────────────────────────────────────────────────
def prev_token_score(pattern):
"""Насколько каждая голова смотрит на ПРЕДЫДУЩИЙ токен.
pattern — веса внимания одного слоя, форма (H, T, T): pattern[h, i, j] — сколько
внимания позиция-запрос i отдаёт позиции-ключу j в голове h.
Вернуть массив (H,): среднее pattern[h, i, i-1] по позициям i = 1 … T-1.
"""
# TODO
...
def induction_score(pattern, period):
"""Индукционный счёт: последовательность — два одинаковых куска длины period.
Позиция i во втором куске (i = period … T-1) видит тот же токен, что на позиции i - period,
а индукционная голова смотрит на токен ПОСЛЕ него — на позицию i - period + 1.
Вернуть массив (H,): среднее pattern[h, i, i - period + 1] по i = period … T-1.
"""
# TODO
...
def loss_on_repeats(logits, tokens, period):
"""Средняя cross-entropy предсказания следующего токена на ВТОРОМ куске.
logits — (T, V), tokens — (T,). Позиция i предсказывает tokens[i + 1].
Берём позиции i = period … T-2 (у последней позиции нет «следующего» токена).
Считайте устойчиво: вычтите максимум строки перед exp (урок 1.3.2).
"""
# TODO
...
def logit_lens_losses(model, tokens, period):
"""Logit lens: loss на втором куске, если «остановить» модель после 0, 1, …, N_LAYERS слоёв.
run(model, tokens) возвращает (logits, cache); cache["resid"][k] — остаточный поток (T, D_MODEL)
после k слоёв (k = 0 — только эмбеддинги). Логиты «на глубине k» = cache["resid"][k] @ model["W_U"].
Вернуть список из N_LAYERS + 1 чисел (loss_on_repeats на каждой глубине).
"""
# TODO
...
def ablation_effects(model, tokens, period):
"""Абляция: насколько вырастет loss на втором куске, если выключить одну голову.
run(model, tokens, ablate=(слой, голова)) обнуляет выход этой головы.
Вернуть словарь {(слой, голова): loss_с_абляцией - loss_без_абляции} для всех N_LAYERS * N_HEADS голов.
"""
# TODO
...
# ── Готовая модель (писать не нужно) ───────────────────────────────────────────
# Двухслойный трансформер только из внимания: без MLP и без нормализации, по 3 головы в слое.
# Остаточный поток шириной D_MODEL = 72; каждая голова читает из него и дописывает свой вклад.
V, T_MAX, D_HEAD, N_LAYERS, N_HEADS = 12, 24, 24, 2, 3
D_MODEL = 72
def softmax(x):
x = x - x.max(axis=-1, keepdims=True)
e = np.exp(x)
return e / e.sum(axis=-1, keepdims=True)
def run(model, tokens, ablate=None):
"""Прямой проход. Возвращает (logits (T, V), cache).
cache["pattern"][l] — веса внимания слоя l, (H, T, T);
cache["head_out"][l] — вклад каждой головы в поток, (H, T, D_MODEL);
cache["resid"][k] — остаточный поток после k слоёв, (T, D_MODEL).
ablate=(l, h) — обнулить выход головы h в слое l.
"""
T = len(tokens)
x = model["W_E"][tokens] + model["W_pos"][:T] # эмбеддинг токена + позиции
cache = {"resid": [x], "pattern": [], "head_out": []}
mask = np.tril(np.ones((T, T), dtype=bool)) # причинная маска
for l in range(N_LAYERS):
q = np.einsum("td,hde->hte", x, model["W_Q"][l]) # (H, T, D_HEAD)
k = np.einsum("td,hde->hte", x, model["W_K"][l])
v = np.einsum("td,hde->hte", x, model["W_V"][l])
scores = q @ k.transpose(0, 2, 1) / np.sqrt(D_HEAD) # (H, T, T)
pattern = softmax(np.where(mask, scores, -np.inf))
out = np.einsum("hts,hse,hed->htd", pattern, v, model["W_O"][l]) # вклад каждой головы
if ablate is not None and ablate[0] == l:
out[ablate[1]] = 0.0
x = x + out.sum(axis=0) # головы ДОПИСЫВАЮТ в поток
cache["pattern"].append(pattern)
cache["head_out"].append(out)
cache["resid"].append(x)
return x @ model["W_U"], cache
def make_repeated(period, seed=0):
"""Случайный кусок из period разных токенов, повторённый дважды: A B C … A B C …"""
rng = np.random.default_rng(seed)
half = rng.choice(V, size=period, replace=False)
return np.concatenate([half, half])
def load_model(seed=2055):
"""Веса заданы заранее. Не заглядывайте сюда, пока не найдёте головы по их поведению!"""
rng = np.random.default_rng(seed)
TOK, POS, PREV, OUT, JUNK = 0, 12, 36, 48, slice(60, 72) # подпространства потока
eps = 0.02
W_E, W_pos = eps * rng.normal(size=(V, D_MODEL)), eps * rng.normal(size=(T_MAX, D_MODEL))
W_U = eps * rng.normal(size=(D_MODEL, V))
shp = (N_LAYERS, N_HEADS)
W_Q, W_K = eps * rng.normal(size=shp + (D_MODEL, D_HEAD)), eps * rng.normal(size=shp + (D_MODEL, D_HEAD))
W_V, W_O = eps * rng.normal(size=shp + (D_MODEL, D_HEAD)), eps * rng.normal(size=shp + (D_HEAD, D_MODEL))
junk = rng.normal(size=(D_HEAD, 12)) / 8
for v in range(V):
W_E[v, TOK + v] += 1.0
W_U[OUT + v, v] += 6.0
for t in range(T_MAX):
W_pos[t, POS + t] += 1.0
W_Q[0, 0, POS + t, t] += 4.0 # слой 0, голова A: ищет позицию t-1
if t + 1 < D_HEAD:
W_K[0, 0, POS + t, t + 1] += 10.0
W_Q[0, 1, POS + t, t] += 4.0 # слой 0, голова B: смотрит на себя
W_K[0, 1, POS + t, t] += 10.0
W_Q[0, 2, POS + t, 0] += 4.0 # слой 0, голова C: смотрит на начало
W_K[0, 2, POS, 0] += 10.0
for v in range(V):
W_V[0, :, TOK + v, v] += 1.0
W_O[0, 0, v, PREV + v] += 1.0 # пишет «кто был до меня»
W_Q[1, 0, TOK + v, v] += 4.0 # слой 1, голова A: «мой токен» ↔
W_K[1, 0, PREV + v, v] += 10.0 # «кто был до тебя»
W_V[1, 0, TOK + v, v] += 1.0
W_O[1, 0, v, OUT + v] += 1.0 # копирует токен в выход
W_Q[1, 1, TOK + v, v] += 4.0 # слой 1, голова B: такой же токен
W_K[1, 1, TOK + v, v] += 10.0
W_V[1, 1, TOK + v, v] += 1.0
W_K[1, 0, POS, :V] -= 10.0 # 1A не смотрит на позицию 0
W_O[0, 1, :, JUNK] += junk
W_O[0, 2, :, JUNK] += junk[::-1]
W_O[1, 1, :, JUNK] += junk
for l in range(N_LAYERS): # перемешиваем головы: A, B, C → номера 0–2
p = rng.permutation(N_HEADS)
W_Q[l], W_K[l], W_V[l], W_O[l] = W_Q[l][p], W_K[l][p], W_V[l][p], W_O[l][p]
return {"W_E": W_E, "W_pos": W_pos, "W_Q": W_Q, "W_K": W_K, "W_V": W_V, "W_O": W_O, "W_U": W_U}
# ── Проверьте себя кнопкой «Запустить» ─────────────────────────────────────────
model = load_model()
P = 8
runs = [run(model, make_repeated(P, s)) for s in range(20)]
if prev_token_score(runs[0][1]["pattern"][0]) is not None:
for l in range(N_LAYERS):
prev = np.mean([prev_token_score(c["pattern"][l]) for _, c in runs], axis=0)
print(f"слой {l}: счёт «предыдущий токен» {prev.round(2)}")
if induction_score(runs[0][1]["pattern"][1], P) is not None:
for l in range(N_LAYERS):
ind = np.mean([induction_score(c["pattern"][l], P) for _, c in runs], axis=0)
print(f"слой {l}: индукционный счёт {ind.round(2)}")
tokens = make_repeated(P, 0)
if loss_on_repeats(runs[0][0], tokens, P) is not None:
print("loss на втором куске:", round(loss_on_repeats(runs[0][0], tokens, P), 3), " ln V =", round(np.log(V), 3))
lens = logit_lens_losses(model, tokens, P)
if lens is not None:
print("logit lens по глубине:", [round(x, 2) for x in lens])
eff = ablation_effects(model, tokens, P)
if eff is not None:
for (l, h), d in sorted(eff.items()):
print(f"выключили голову {l}.{h}: loss {d:+.2f}")
Посмотрите на вывод абляции. Головы с большим эффектом — ровно те две, что вы нашли по паттернам. Обратите внимание на голову предыдущего токена: в выход она не пишет ничего, а без неё loss на повторе вырастает больше чем на 2 ната.
Локально: своя GPT из урока 4.6.2
Заголовок раздела «Локально: своя GPT из урока 4.6.2»Скрипт работает на CPU за полминуты. Положите его рядом с model.py, gpt_ru.pt и input.txt из урока 4.6.2. Хуки
(register_forward_hook) — стандартный механизм PyTorch: функция, которую модуль вызывает после своего forward,
передавая ей вход и выход. Так можно подсмотреть любую промежуточную активацию, не меняя код модели.
"""look_inside.py — logit lens и индукционный тест для GPT из урока 4.6.2. Запуск: python look_inside.py"""import torchimport torch.nn.functional as F
from model import GPT
torch.manual_seed(0)ckpt = torch.load("gpt_ru.pt", weights_only=False) # чекпоинт из урока 4.6.2model = GPT(ckpt["cfg"])model.load_state_dict(ckpt["model"])model.eval()cfg = model.cfgstoi = {ch: i for i, ch in enumerate(ckpt["chars"])}text = open("input.txt", encoding="utf-8").read()data = torch.tensor([stoi[c] for c in text if c in stoi]) # только символы из словаря моделиval = data[int(0.9 * len(data)):] # валидационная часть, как при обучении
# ── 1. Logit lens: применяем ln_f и lm_head к потоку после каждого блока ──starts = torch.arange(64) * cfg.block_size # 64 непересекающихся окнаx = torch.stack([val[s:s + cfg.block_size] for s in starts])y = torch.stack([val[s + 1:s + 1 + cfg.block_size] for s in starts])resid = {} # номер блока -> его выход (B, T, C)hooks = [blk.register_forward_hook(lambda mod, inp, out, i=i: resid.__setitem__(i, out)) for i, blk in enumerate(model.blocks)]with torch.no_grad(): model(x) resid[-1] = model.wte(x) + model.wpe(torch.arange(cfg.block_size)) # «после 0 блоков» for h in hooks: h.remove() for i in range(-1, cfg.n_layer): logits = model.lm_head(model.ln_f(resid[i])) # та же голова, что в конце модели loss = F.cross_entropy(logits.flatten(0, 1), y.flatten()) print(f"{'эмбеддинги' if i < 0 else f'после блока {i}':>14}: loss {loss.item():.2f}")
# ── 2. Индукционный тест: фрагмент текста, повторённый дважды ──L = 60idx = torch.randint(len(val) - L, (64,))seq = torch.stack([val[s:s + L] for s in idx])rep = torch.cat([seq, seq], dim=1) # A B C … A B C … (длина 120 ≤ 128)qk = {} # выход c_attn = [Q | K | V] каждого блокаhooks = [blk.attn.c_attn.register_forward_hook(lambda mod, inp, out, i=i: qk.__setitem__(i, out)) for i, blk in enumerate(model.blocks)]with torch.no_grad(): logits, _ = model(rep)for h in hooks: h.remove()nll = F.cross_entropy(logits[:, :-1].transpose(1, 2), rep[:, 1:], reduction="none") # (64, 119)print(f"\nloss на 1-м повторе {nll[:, :L - 1].mean():.2f}, на 2-м {nll[:, L:].mean():.2f}")
# ── 3. Индукционный счёт голов: внимание с позиции t на t - L + 1 ──T, H, d = rep.shape[1], cfg.n_head, cfg.n_embd // cfg.n_headmask = torch.tril(torch.ones(T, T, dtype=torch.bool))for i in range(cfg.n_layer): q, k, _ = qk[i].split(cfg.n_embd, dim=2) # как в CausalSelfAttention.forward q = q.view(-1, T, H, d).transpose(1, 2) # (B, H, T, d) k = k.view(-1, T, H, d).transpose(1, 2) att = (q @ k.transpose(-2, -1) / d ** 0.5).masked_fill(~mask, float("-inf")).softmax(-1) stripe = att.diagonal(offset=1 - L, dim1=-2, dim2=-1)[..., 1:] # запросы t = L … 2L-1 print(f"блок {i}: индукционный счёт голов {stripe.mean(dim=(0, 2)).numpy().round(2)}") эмбеддинги: loss 12.82 после блока 0: loss 3.24 после блока 1: loss 2.94 после блока 2: loss 2.47 после блока 3: loss 1.64
loss на 1-м повторе 1.73, на 2-м 1.82блок 0: индукционный счёт голов [0. 0. 0. 0.]блок 1: индукционный счёт голов [0. 0. 0. 0.]блок 2: индукционный счёт голов [0. 0. 0. 0.]блок 3: индукционный счёт голов [0. 0. 0. 0.]Это вывод для нашей модели (val loss 1.71 после 2000 шагов). У вас числа будут немного другими, но картина та же.
Две детали кода. Внимание в model.py считает F.scaled_dot_product_attention, а он не возвращает веса. Поэтому мы
перехватываем выход c_attn, то есть , и , и пересчитываем паттерн сами по формуле из урока 4.4.2. А
diagonal(offset=1 - L) вынимает из матрицы ровно элементы — ту самую индукционную
полосу.
Проверь себя
Заголовок раздела «Проверь себя»- Остаточный поток — сумма вкладов эмбеддинга, голов и MLP, поэтому предсказание раскладывается на слагаемые (DLA), а промежуточные слои можно «прочитать» через финальную LM-голову (logit lens).
- Индукционная схема — две головы в разных слоях: голова предыдущего токена оставляет метку, индукционная ищет по ней и копирует. Её находят по полосе со сдвигом на повторённых случайных токенах. Появляется она скачком, а у маленькой модели из 4.6.2 её ещё нет.
- Паттерны, DLA и зонды — наблюдения. Доказательство роли даёт вмешательство: абляция (лучше средним, а не нулём) и activation patching с долей восстановления .
- Инструменты: TransformerLens (с версии 4.0 —
TransformerBridge) и nnsight. Практикум следующего уровня — ARENA, глава 1.
Дальше: урок 9.3.2 «Суперпозиция и разреженные автоэнкодеры». Почему отдельный нейрон часто отвечает сразу за несколько несвязанных понятий и как Anthropic научилась извлекать из модели миллионы понятных признаков.