Mechanistic Interpretability LLM: Python код для анализа активаций модели | AiManual
AiManual Logo Ai / Manual.
05 Фев 2026 Гайд

Зачем лезть в черный ящик: Mechanistic Interpretability для тех, кто не боится матриц

Практический гайд по интерпретации языковых моделей. Код для визуализации активаций, анализа весов и поиска нейронов-функций в трансформерах.

Вот типичная картина: вы запускаете GPT-4o или Claude 3.7, получаете гениальный ответ и думаете – «какого черта?». Как куча умножений матриц порождает смысл? Большинство довольствуется магией. Но некоторые смотрят на этот черный ящик и хотят его вскрыть отверткой. Это и есть Mechanistic Interpretability (MI) – дисциплина для параноиков, которые не верят в магию.

В 2026 году это уже не академическая забава. Когда модель стоимостью в миллионы долларов внезапно выдает опасные инструкции, понимать «почему» становится вопросом безопасности. Не говоря уже о том, что без интерпретируемости вы не сможете починить сломанное поведение. Вы будете просто тыкать в промпты наугад.

Зачем это вам? (Спойлер: не для красоты)

Представьте, что ваша fine-tuned Llama 3.3 начала врать в ответах про медицину. Вы можете:

  • Добавить больше примеров в датасет (надеяться)
  • Поиграть с гиперпараметрами (помолиться)
  • Или залезть внутрь и посмотреть, какой именно нейрон активируется на медицинских терминах и какие ассоциации он выдает

Третий вариант – это MI. Вы не просто смотрите на вход и выход. Вы ставите модель под микроскоп, замораживаете выполнение на определенном слое и спрашиваете: «Что ты сейчас думаешь, дружок?».

Важно: Mechanistic Interpretability ≠ Explainable AI (XAI). XAI часто отвечает «почему модель так решила» через приближения (типа SHAP). MI отвечает «как именно она это сделала» на уровне отдельных нейронов и внимания. Это reverse engineering, а не объяснение постфактум.

Инструментарий: что нужно кроме храбрости

В 2026 году экосистема созрела. Раньше нужно было писать тонны кода с нуля. Теперь есть библиотеки, которые делают грязную работу.

Инструмент Для чего Состояние на 05.02.2026
TransformerLens Основная работа с активациями, вмешательства Активно развивается, полная поддержка Llama 3.3, Qwen-2.5-32B, новых архитектур
Neuroscope Визуализация активаций в браузере Добавлена поддержка grouped-query attention (GQA) из последних моделей
NNSight «Отладчик» для LLM, вмешательство в реальном времени Стабильная версия, работает с моделями через Hugging Face
CircuitsVis Визуализация паттернов внимания Интеграция с Jupyter, улучшенная работа с длинными контекстами

Начнем с установки. Не пытайтесь ставить все сразу – начните с TransformerLens, это Swiss Army knife для MI.

pip install transformer_lens==1.12.0  # Актуально на 05.02.2026
pip install circuitsvis
pip install plotly  # для графиков

1 Загружаем модель и смотрим, что внутри

Первое, что удивляет новичков – модели не так уж черны. Мы можем загрузить любую модель из Hugging Face и сразу получить доступ ко всем внутренностям.

import transformer_lens
import torch
from transformer_lens import HookedTransformer

# Загружаем не самую большую модель для экспериментов
# На 2026 год Llama 3.1 8B уже считается «маленькой» для экспериментов
model = HookedTransformer.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    device="cuda" if torch.cuda.is_available() else "cpu",
    dtype=torch.bfloat16  # Современные модели используют bfloat16
)

# Смотрим, что у нас есть
print(f"Модель: {model.cfg.model_name}")
print(f"Слоев: {model.cfg.n_layers}")
print(f"Голов внимания: {model.cfg.n_heads}")
print(f"Размерность: {model.cfg.d_model}")
print(f"Словарь: {model.cfg.d_vocab} токенов")

Уже здесь видна первая странность. HookedTransformer – это обертка, которая добавляет «крючки» (hooks) в модель. Эти крючки позволяют перехватывать активации в любой точке сети. Без них вы просто видите вход и выход, как обычный пользователь API.

💡
На 2026 год большинство моделей используют grouped-query attention (GQA) вместо обычной multi-head. TransformerLens автоматически это обрабатывает, но важно понимать: теперь не все головы внимания независимы – они сгруппированы для эффективности.

2 Первый вскрытие: что происходит при генерации?

Давайте сделаем простейший эксперимент: прогоним промпт через модель и сохраним активации со всех слоев. Это как сделать МРТ мозга во время мышления.

prompt = "Столица Франции – это"
tokens = model.to_tokens(prompt)

# Словарь для хранения активаций
activations = {}

# Функция-крючок, которая будет вызываться во время forward pass
def save_activation_hook(activation, hook):
    # hook.name содержит информацию о месте (например, 'blocks.0.attn.hook_z')
    activations[hook.name] = activation.detach().cpu()

# Регистрируем крючки на всех слоях для внимания и MLP
hook_locations = [
    f"blocks.{layer}.attn.hook_z" for layer in range(model.cfg.n_layers)
] + [
    f"blocks.{layer}.mlp.hook_post" for layer in range(model.cfg.n_layers)
]

# Запускаем модель с крючками
with model.hooks(fwd_hooks=[(loc, save_activation_hook) for loc in hook_locations]):
    logits = model(tokens)
    # Получаем предсказание
    next_token = model.tokenizer.decode(logits[0, -1].argmax().item())
    
print(f"Следующий токен: {next_token}")
print(f"Сохранено {len(activations)} тензоров активаций")

Теперь в activations у нас лежит примерно 2 * n_layers тензоров. Для Llama 3.1 8B это 32 слоя * 2 = 64 тензора. Каждый тензор имеет размерность [batch, position, features].

И вот первый инсайт: вы можете увидеть, как информация течет через сеть. Ранние слои (0-5) обычно работают с синтаксисом и базовой семантикой. Средние слои (6-20) строят представления. Поздние слои (21-31) готовят ответ.

3 Ищем нейроны-функции: кто отвечает за столицы?

Одна из ключевых идей MI: некоторые нейроны в MLP слоях кодируют конкретные понятия. Есть нейрон для «столичности», нейрон для «французскости» и т.д. Найдем их.

Сначала создадим датасет пар «страна – столица»:

country_capital_pairs = [
    ("Франция", "Париж"),
    ("Германия", "Берлин"),
    ("Италия", "Рим"),
    ("Япония", "Токио"),
    ("Россия", "Москва"),
    ("США", "Вашингтон"),
    ("Китай", "Пекин"),
    ("Индия", "Нью-Дели"),
]

# Токенизируем
activations_by_neuron = {}

for country, capital in country_capital_pairs:
    prompt = f"Столица {country} – это"
    tokens = model.to_tokens(prompt)
    
    # Нас интересует активация на последнем токене (после «это»)
    target_position = -1
    
    # Запускаем и сохраняем активации MLP слоев
    with model.hooks(fwd_hooks=[
        (f"blocks.{{layer}}.mlp.hook_post", save_activation_hook) 
        for layer in range(model.cfg.n_layers)
    ]):
        model(tokens)
    
    # Для каждой пары сохраняем активации
    activations_by_neuron[(country, capital)] = activations

Теперь проанализируем, какие нейроны наиболее активны на столицах:

import numpy as np

# Соберем статистику по нейронам
neuron_activations = {}

for layer in range(model.cfg.n_layers):
    key = f"blocks.{layer}.mlp.hook_post"
    # Размерность активаций: [batch=1, position, d_mlp]
    # d_mlp обычно 4 * d_model для Llama
    
    # Соберем активации для всех примеров
    all_acts = []
    for (country, capital), acts in activations_by_neuron.items():
        # Берем активацию на последнем токене
        act = acts[key][0, -1, :].numpy()  # [d_mlp]
        all_acts.append(act)
    
    # Преобразуем в массив [n_examples, d_mlp]
    all_acts = np.stack(all_acts, axis=0)
    
    # Для каждого нейрона считаем среднюю активацию
    mean_activation = np.mean(all_acts, axis=0)
    std_activation = np.std(all_acts, axis=0)
    
    # Найдем нейроны с наибольшим средним
    top_neuron_indices = np.argsort(mean_activation)[-10:]  # Топ-10
    
    print(f"\nСлой {layer}: топ нейроны для столиц")
    for idx in top_neuron_indices[::-1]:  # От большего к меньшему
        print(f"  Нейрон {idx}: mean={mean_activation[idx]:.3f}, std={std_activation[idx]:.3f}")

Если вам повезет, вы найдете нейроны, которые сильно активируются на всех столицах. В реальности все сложнее – понятия распределены по многим нейронам. Но иногда находятся удивительно чистые «мононейроны».

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

4 Визуализируем внимание: кто на кого смотрит

Механизм внимания – это самое интересное в трансформерах. Он показывает, какие части контекста модель считает важными. Давайте визуализируем это для многослойного случая.

import circuitsvis as cv
import pandas as pd

# Возьмем более сложный промпт
prompt = "Илон Маск основал компанию Tesla, которая производит электромобили"
tokens = model.to_tokens(prompt)

# Получим матрицы внимания
attention_patterns = {}

def save_attention_hook(attn, hook):
    # attn имеет размерность [batch, head, query_pos, key_pos]
    attention_patterns[hook.name] = attn.detach().cpu()[0]  # Берем первый batch

# Регистрируем крючки для внимания
hook_locations = [
    f"blocks.{layer}.attn.hook_pattern" for layer in range(model.cfg.n_layers)
]

with model.hooks(fwd_hooks=[(loc, save_attention_hook) for loc in hook_locations]):
    model(tokens)

# Преобразуем токены обратно в текст для подписей
token_labels = [model.tokenizer.decode(t) for t in tokens[0]]

# Визуализируем внимание для первого слоя, всех голов
layer = 0
attn_matrix = attention_patterns[f"blocks.{layer}.attn.hook_pattern"]  # [head, query, key]

# CircuitsVis ожидает данные в определенном формате
# Преобразуем для одной головы для примера
head = 3
attention_df = pd.DataFrame(
    attn_matrix[head].numpy(),
    index=token_labels,
    columns=token_labels
)

# Это упрощенный пример. В реальности используйте cv.attention.attention_heads
# для интерактивной визуализации в Jupyter

Что вы увидите? В ранних слоях внимание часто следует за синтаксическими связями (существительное → прилагательное, глагол → дополнение). В поздних слоях появляются семантические связи («Илон Маск» → «Tesla» → «электромобили»).

На 2026 год появилась интересная особенность: в моделях с длинным контекстом (128K+) механизм внимания становится разреженным. Модель учится «фокусироваться» на релевантных частях, а не равномерно смотреть на весь контекст.

Продвинутые техники: когда простого взгляда недостаточно

Активационная патч-инженерия

Что если мы хотим понять, какие части сети ответственны за конкретное поведение? Например, за то, что модель знает, что Париж – столица Франции?

Метод: берем два промпта – один корректный («Столица Франции – это Париж»), один некорректный («Столица Франции – это Берлин»). Запускаем корректный, но подменяем активации из некорректного в определенных слоях. Смотрим, где подмена ломает правильный ответ.

# Упрощенный пример патч-инженерии
def run_with_patch(model, corrupted_tokens, clean_tokens, patch_layer):
    """Запускаем модель с подменой активаций на определенном слое"""
    
    # Сначала получаем активации от corrupted промпта
    corrupted_activations = {}
    def save_corrupted_hook(act, hook):
        if hook.layer() == patch_layer:
            corrupted_activations[hook.name] = act
    
    # Запускаем corrupted
    with model.hooks(fwd_hooks=[(f"blocks.{patch_layer}.attn.hook_z", save_corrupted_hook)]):
        model(corrupted_tokens)
    
    # Теперь запускаем clean, но подменяем активации
    def patch_hook(act, hook):
        if hook.layer() == patch_layer:
            return corrupted_activations[hook.name]
        return act
    
    with model.hooks(fwd_hooks=[(f"blocks.{patch_layer}.attn.hook_z", patch_hook)]):
        logits = model(clean_tokens)
    
    return logits

# Пример использования
clean_prompt = "Столица Франции – это Париж"
corrupted_prompt = "Столица Франции – это Берлин"

clean_tokens = model.to_tokens(clean_prompt)
corrupted_tokens = model.to_tokens(corrupted_prompt)

# Тестируем разные слои
for layer in [5, 10, 15, 20, 25]:
    logits = run_with_patch(model, corrupted_tokens, clean_tokens, layer)
    predicted_token = model.tokenizer.decode(logits[0, -1].argmax().item())
    print(f"Слой {layer}: предсказание после патча = '{predicted_token}'")

Если на слое 15 подмена активаций меняет «Париж» на «Берлин», значит этот слой критически важен для знания о столицах. Это мощный метод локализации функций.

Анализ словаря признаков

Современный тренд (2025-2026): вместо анализа отдельных нейронов анализировать «направления» в пространстве активаций. Каждое направление соответствует какому-то признаку.

Библиотеки вроде SAE (Sparse Autoencoders) учатся разлагать активации на разреженные признаки. Один признак может быть «столичность», другой «французскость», третий «географический объект».

# Пример загрузки предобученного SAE для Llama 3.1
# (В реальности нужно обучать свой или скачать предобученный)
try:
    # Это примерный API, детали зависят от реализации
    from sae_lens import SAE
    
    sae = SAE.from_pretrained("jbloom/llama-3.1-8B-sae")
    
    # Получаем активации для промпта
    activations = model.run_with_cache("Столица Франции – это")["blocks.15.mlp.hook_post"]
    
    # Декомпозируем на признаки
    features = sae.encode(activations)
    
    # features - разреженная матрица [batch, position, n_features]
    # Каждый столбец соответствует одному признаку
    
    # Находим наиболее активные признаки
    feature_activity = features[0, -1, :].abs().cpu().numpy()
    top_features = np.argsort(feature_activity)[-5:]
    
    print("Топ-5 признаков для 'это' в контексте столиц:")
    for feat_idx in top_features[::-1]:
        # Можно посмотреть, какие токены максимально активируют этот признак
        print(f"  Признак {feat_idx}: активность={feature_activity[feat_idx]:.3f}")
    
except ImportError:
    print("SAE Lens не установлена. pip install sae-lens")

Ошибки, которые сломают ваш анализ

Я видел, как люди тратят недели на бессмысленные эксперименты. Вот что не работает:

  1. Анализ на одном примере. Нейрон активировался на «котик» в одном промпте? Это еще ничего не значит. Нужна статистика по сотням примеров.
  2. Игнорирование распределенности. В современных моделях почти нет чистых «мононейронов». Понятия распределены по десяткам нейронов, а нейроны участвуют в десятках понятий.
  3. Забывание о контексте. Один и тот же нейрон может активироваться на «яблоко» (фрукт) и «Apple» (компания) в разных контекстах.
  4. Прямая интерпретация весов. Веса в матрицах внимания или MLP почти невозможно интерпретировать напрямую. Они оптимизированы для совместной работы, а не для человеческого чтения.
💡
Совет от практика: начните с малых моделей (Llama 3.1 8B или даже 1B). Их проще анализировать, и многие паттерны масштабируются на большие модели. Не пытайтесь сразу понять GPT-4o со смешанными экспертами – это отдельный ад.

Зачем все это? (Реальные кейсы)

«Ну и что?» – спрашивает менеджер. Вот что:

  • Дебаггинг сломанного поведения. Модель внезапно начала говорить, что 2+2=5? С помощью MI можно найти, какой нейрон «сломался» и либо починить его (через fine-tuning), либо исключить из ансамбля.
  • Поиск уязвимостей. Модели понимают инструкции, но игнорируют их? MI показывает, где именно происходит «несогласование» между пониманием цели и генерацией.
  • Улучшение эффективности. Если вы обнаружили, что 80% нейронов в некоторых слоях почти никогда не активируются – может, их можно prune без потери качества?
  • Создание более контролируемых моделей. Зная, какие части сети отвечают за креативность, можно создавать «регуляторы», как в том исследовании про Llama 3.2.

Что будет дальше? (Прогноз на 2027)

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

  • Автоматизированный MI. Инструменты, которые сами будут предлагать гипотезы и проверять их, как авто-ML для интерпретации.
  • Интерпретируемость мультимодальных моделей. Как связаны текстовые и визуальные представления в одном пространстве? Это следующая frontier.
  • MI как часть training loop. Представьте: модель обучается, а система интерпретации постоянно мониторит, какие понятия она усваивает и нет ли проблем.
  • Стандартизация метрик. Сейчас каждый исследователь придумывает свои метрики «интерпретируемости». К 2027 появятся стандартные бенчмарки.

Самый важный сдвиг: из исследовательской дисциплины MI превратится в инженерную практику. Так же, как когда-то unit-тесты из академической идеи стали обязательными для production кода.

И последний совет: не пытайтесь понять все сразу. Начните с одного слоя, одного типа анализа. Постройте интуицию. Эти модели сложны, но они не магические. Просто очень, очень сложные.

P.S. Если после этого гайда вы все еще думаете, что LLM – это просто «статистика на стероидах», посмотрите на активации нейрона, который зажигается на иронии. Статистика так не умеет.