Когда нейросеть перестает понимать, что говорит
Представьте: вы просите модель объяснить квантовую механику. Она генерирует связный, научно звучащий текст. Но если вы знаете, какие 17 нейронов нужно «подкрутить», ответ превратится в бессвязную окрошку из терминов. Модель все еще говорит красиво, но смысл исчез. Это не баг — это ключ к пониманию того, как работают LLM.
В прошлой статье мы смотрели, как Llama 3.2 3B думает внутри. Сегодня мы пойдем дальше — найдем те самые «несущие» измерения (load-bearing dims), которые держат семантику на плаву. И аккуратно их выключим.
Важно: это не про взлом модели. Это про понимание ее архитектуры. Манипулируя внутренними представлениями, мы видим, какие нейроны действительно важны, а какие — шум.
Почему это вообще работает?
Трансформеры — не черный ящик. Это высокоразмерные карты признаков. Некоторые измерения в этих картах кодируют конкретные понятия: «научность», «формальность», «причинно-следственная связь». Если найти и зашумлять именно их — модель теряет способность обрабатывать соответствующие концепции. При этом синтаксис и беглость часто сохраняются. Потому что за синтаксис отвечают другие нейроны.
1 Собираем лабораторию для вскрытия
Для работы нужен не просто Python, а специальный набор инструментов. Не пытайтесь делать это в Colab — будете ждать вечность.
# Основные зависимости
pip install torch transformers datasets
pip install numpy scipy scikit-learn
pip install matplotlib seaborn tqdm
# Для работы с активациями
pip install einops
# Для запуска модели локально
# (я использую трансформеры, но можно и llama.cpp)
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp && make
2 Загружаем модель и готовимся к слежке
Мы будем использовать Llama 3.2 3B Instruct — она достаточно мала для экспериментов, но достаточно умна, чтобы демонстрировать интересное поведение. Загружаем с хуками для перехвата активаций.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
import numpy as np
model_name = "meta-llama/Llama-3.2-3B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto"
)
# Словарь для хранения активаций
activations = {}
def get_activation_hook(name):
"""Хук для захвата активаций из конкретного слоя"""
def hook(module, input, output):
# Сохраняем скрытые состояния (последний токен)
if isinstance(output, tuple):
hidden_states = output[0] # (batch, seq_len, hidden_size)
else:
hidden_states = output
# Берем активации для последнего токена в последовательности
activations[name] = hidden_states[:, -1, :].detach().cpu()
return hook
# Регистрируем хуки для всех слоев
for i, layer in enumerate(model.model.layers):
layer.register_forward_hook(get_activation_hook(f"layer_{i}"))
Теперь при каждом forward pass активации каждого слоя будут сохраняться в наш словарь. Это дает нам моментальный снимок того, что «видит» модель на каждом шаге.
Методология: как искать иголку в стоге нейронов
В Llama 3.2 3B скрытая размерность — 3072. Это 3072 измерения на каждом слое. Наша задача — найти те 10-20, которые критичны для семантики. Как?
- Собираем датасет активаций для разных типов запросов (научные, бытовые, логические)
- Вычисляем вариативность каждого измерения по датасету
- Коррелируем активации с метриками качества ответов
- Применяем perturbation к самым вариативным измерениям
| Метод поиска | Что ищет | Плюсы | Минусы |
|---|---|---|---|
| Variance analysis | Измерения с наибольшей дисперсией | Быстро, просто | Много шума |
| PCA + clustering | Основные компоненты вариации | Убирает корреляции | Теряется интерпретируемость |
| Gradient-based | Нейроны, влияющие на loss | Прямая связь с качеством | Вычислительно тяжело |
3 Собираем активации и вычисляем важность
Создаем набор промптов, покрывающий разные домены. Кормим их модели и сохраняем активации.
prompts = [
"Explain quantum entanglement in simple terms.",
"What is the capital of France?",
"Write a Python function to calculate factorial.",
"Describe the process of photosynthesis.",
"Solve: 2x + 5 = 15",
"What are the main themes in Shakespeare's Hamlet?",
# Добавляем еще 50+ промптов разных типов
]
all_activations = []
for prompt in prompts:
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model(**inputs)
# Собираем активации для каждого слоя
layer_acts = []
for i in range(len(model.model.layers)):
act = activations[f"layer_{i}"]
layer_acts.append(act.numpy())
all_activations.append(layer_acts)
# Преобразуем в numpy массив: (промпты, слои, размерность)
all_activations = np.array(all_activations) # shape: (n_prompts, n_layers, hidden_dim)
Теперь вычисляем, какие измерения больше всего меняются между разными типами запросов:
# Вычисляем вариативность по промптам для каждого слоя
layer_variability = []
for layer_idx in range(all_activations.shape[1]):
# Активации для этого слоя по всем промптам
layer_acts = all_activations[:, layer_idx, :] # (n_prompts, hidden_dim)
# Дисперсия по промптам для каждого измерения
variances = np.var(layer_acts, axis=0)
layer_variability.append(variances)
# Находим топ-20 самых вариативных измерений для каждого слоя
top_indices_per_layer = []
for variances in layer_variability:
top_indices = np.argsort(variances)[-20:] # 20 самых вариативных
top_indices_per_layer.append(top_indices)
Проверка гипотезы: perturbation analysis
Теперь самое интересное. Мы возьмем эти «подозрительные» измерения и добавим к ним шум. Если гипотеза верна — семантика сломается, а беглость останется.
def perturb_activations(model, tokenizer, prompt, layer_idx, dim_indices, noise_scale=2.0):
"""Добавляем шум к конкретным измерениям в конкретном слое"""
# Хук для модификации активаций
def perturbation_hook(module, input, output):
hidden_states = output[0] if isinstance(output, tuple) else output
# Добавляем шум только к выбранным измерениям
noise = torch.randn_like(hidden_states) * noise_scale
# Обнуляем шум для всех измерений, кроме целевых
mask = torch.zeros_like(hidden_states)
for dim in dim_indices:
mask[:, :, dim] = 1.0
noise = noise * mask
return (hidden_states + noise,) if isinstance(output, tuple) else hidden_states + noise
# Регистрируем хук
handle = model.model.layers[layer_idx].register_forward_hook(perturbation_hook)
# Генерируем ответ
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(**inputs, max_length=200)
# Убираем хук
handle.remove()
return tokenizer.decode(outputs[0], skip_special_tokens=True)
Внимание: noise_scale — критический параметр. Слишком маленький — эффекта не будет. Слишком большой — модель начнет генерировать мусор. Начинайте с 1.0-2.0.
4 Эксперимент: ломаем научные объяснения
Возьмем промпт про квантовую механику и добавим шум к топ-15 самым вариативным измерениям в слоях 10-15 (примерно середина сети).
# Целевые измерения из нашего анализа
critical_dims = top_indices_per_layer[12][:15] # Слой 12, топ-15
original_prompt = "Explain quantum entanglement in simple terms."
# Оригинальный ответ
original_response = generate_response(model, tokenizer, original_prompt)
# Ответ с perturbation
perturbed_response = perturb_activations(
model, tokenizer,
original_prompt,
layer_idx=12,
dim_indices=critical_dims,
noise_scale=2.5
)
Результаты обычно выглядят так:
| Тип | Ответ | Оценка |
|---|---|---|
| Оригинал | "Quantum entanglement is a phenomenon where two particles become linked..." | Корректно, связно |
| После perturbation | "Quantum entanglement particles simple link together science physics theory explains..." | Бессвязный набор терминов |
Модель все еще использует правильные слова. Они грамматически связаны. Но смысловая связь потеряна. Это и есть «катастрофическая потеря семантики при сохранении беглости».
Глубже в кроличью нору: что мы нашли?
После десятков экспериментов с Llama 3.2 3B вырисовывается картина:
- Слои 8-16 — критичны для семантической интеграции. Их perturbation ломает смысл сильнее всего.
- Слои 20+ — больше связаны с синтаксисом и стилем. Шум здесь делает текст грамматически странным.
- ~3% измерений отвечают за 80% семантической вариации. Остальные — либо шум, либо служебные функции.
Самое интересное: найденные «несущие» измерения часто соответствуют конкретным концепциям. Один и тот же нейрон может активироваться на «научность» в разных контекстах. Это подтверждает гипотезу о том, что трансформеры развивают что-то вроде концептуальных осей в скрытом пространстве.
Ошибки, которые сломают ваш эксперимент
Я потратил неделю, наступая на эти грабли. Не повторяйте.
- Шум одинаковой амплитуды для всех измерений. Некоторые нейроны работают в диапазоне [-0.1, 0.1], другие — в [-5, 5]. Нормализуйте шум относительно стандартного отклонения каждого измерения.
- Perturbation только в одном слое. Семантика распределена по сети. Нужно бить по нескольким слоям одновременно.
- Использование только вариативности как метрики. Самые вариативные измерения не всегда самые важные. Добавьте gradient-based анализ.
- Короткие промпты. Нужны минимум 50 токенов, чтобы активации стабилизировались.
- Игнорирование batch эффектов. При обработке нескольких промптов параллельно активации влияют друг на друга.
Что это значит для разработчиков?
Понимание «несущих» измерений дает практические преимущества:
- Целевая дообучка. Вместо тонкой настройки всей модели можно адаптировать только критичные нейроны.
- Обнаружение аномалий. Мониторинг активаций этих измерений покажет, когда модель «сошла с ума».
- Сжатие моделей. Если 3% измерений несут смысл, может быть, остальные можно проредить?
- Защита от jailbreak. Понимая, какие нейроны отвечают за безопасность, можно их усилить.
Но есть и темная сторона. Зная критические нейроны, можно целенаправленно ломать модели. Или создавать adversarial примеры, которые выглядят безобидно для человека, но сбивают LLM с толку. Это уже не теория — инструменты для такого анализа становятся доступнее.
Что дальше?
Методология работает для Llama 3.2 3B. Будет ли она работать для 70B моделей? Скорее всего, да, но масштабирование нетривиально. В больших моделях «несущие» измерения могут быть распределены иначе — более диффузно.
Следующий шаг — автоматическое обнаружение семантических осей без ручного анализа. Представьте: загружаете модель, запускаете скрипт, и через час получаете карту «что где живет». Это уже не фантастика — инструменты вроде TransformerLens движутся в этом направлении.
А пока — экспериментируйте с маленькими моделями. Они проще, быстрее, и на них можно отточить методику. Когда поймете, как ломать семантику в 3B модели, вы будете готовы к большим целям.
И помните: мы не ломаем модели ради разрушения. Мы разбираем их, чтобы понять, как они работают. А понимание — первый шаг к улучшению.