В предыдущей статье я обратил внимание на интересное поведение Weight Decay, здесь я рассматриваю его более подробно.
В индустрии ML принято считать: если мы берем обученную модель и делаем Fine-Tuning на новой задаче, старые веса постепенно перезаписываются. А если добавить Weight Decay (L2-регуляризацию), то процесс забывания лишнего должен идти еще быстрее.
Я проверил это утверждение экспериментально. Результаты оказались контринтуитивными: при определенных настройках Weight Decay работает ровно наоборот — защищает старую структуру от разрушения.
Ниже — описание эксперимента и выводы для тех, кто занимается обучением и безопасностью моделей.
Спойлер: При переобучении нейросети Weight Decay ~10^-3 создаёт парадоксальный эффект:
- Модель без предобучения забывает старые паттерны (accuracy падает к 57%)
- Модель с предобучением сохраняет их (accuracy остаётся 83%)
- Разница в 25% — это структурная память, устойчивая к регуляризации
Практический вывод: Стандартный fine-tuning (WD~10^-4) не удаляет, а маскирует нежелательную информацию из моделей.
Для изучения влияния эффекта Weight Decay на обучение модели, пришлось очень тщательно подойти к постановке эксперимента. Нам нужно определить как переобучение модели влияет на сохранность старых данных.
Для этого в экспериментальной модели мы сначала обучаем задаче A определять какой клетке принадлежат координаты (чет/нечет). Затем переобучаем модель принципиально другой задаче B z = (x^2 - y^2) / 5. Но при этом, мы вырезаем из обучающих данных, круг с радиусом R (1.5), то есть модель их не видит. И этот диапазон закрывается обобщением модели. При переобучении используем разные уровни Weight Decay.
Мы не можем напрямую проверить, помнит ли модель Parity, она обучена выдавать Saddle. Для понимания, что происходит в модели после переобучения, мы берем зонд, который смотрит на внутреннее представление, эмбеддинги (вторая отдельная модель). Зонд тренируется решать задачу классификации A. При этом используются вектора первой модели для задачи В как входные данные. Важно то, что в обучении участвуют только координаты внутри круга радиусом R (1.5), то есть те, которые модель при обучении задаче B не видела. Поскольку первая модель передает точную информацию о положении точек в пространстве, зонд легко использует эти данные для вычисления правильного ответа.
Контрольная модель не обучается задаче A, остальные этапы сохраняются.
Вырезание центральной области создает слепую зону, где веса нейросети не получают прямых команд на изменение под новую задачу. Это необходимо для чистоты эксперимента: если бы модель обучалась на всём пространстве, алгоритм просто принудительно перезаписал бы старые знания новыми. Изолированный центр позволяет увидеть реальную картину: сохраняется ли структура старой памяти в глубине нейросети сама по себе, или же она разрушается под давлением новой задачи с периферии и механизма забывания.
Попробую объяснить проще. Обе задачи зависят от одних и тех же входов — координат x и y.
Фактически слепое пятно моделирует этапное обучение LLM:
Pre-training: в модель загружают весь интернет, создавая широкую базу знаний.
Fine-tuning: Модель начинают учить узкой задаче — например, «быть полезным ассистентом» или «писать код».
Образно говоря, цель эксперимента: это попытка выяснить, если после месяца обучения кодингу мы попросим модель написать стих (заглянем в слепую зону), сможет ли она достать этот навык из глубины весов претрейна, или он был стёрт специализацией?
Подробнее об эксперименте в спойлере:
Скрытый текстДве задачи с разной топологией:
Две задачи, требующие принципиально разной организации весов:
Задача A: «Шахматная доска» (Parity)
Label = (floor(x) + floor(y)) % 2
Высокочастотная, разрывная функция. Требует от нейросети построения множества четких границ («стен») между классами 0 и 1. Точка (0.7, 1.3) → ⌊0⌋ + ⌊1⌋ = 1 → класс 1
Это воспоминание, которое пытаемся стереть.
Задача B: «Седло» (Saddle)
z = (x^2 - y^2) / 5
Низкочастотная, гладкая функция. Это классическая регрессия, требующая непрерывного преобразования координат.
Это новая задача, которую навязываем модели.
Задачи геометрически ортогональны. Для решения Parity нужно квантовать пространство, а для Saddle нужно его интерполировать.
Пространство x, y разделено на две зоны:
Пончик (Donut, R > 1.5): Здесь модель получает градиенты во время переобучения.
Слепое пятно (Blind Spot, R < 1.5): Здесь задача B не учится, только проверяется, что осталось в голове у модели.
Эксперимент проходит в три этапа для двух групп моделей:
Группа 1: CONTROL (Tabula Rasa)
Чистый эксперимент, имитирующий обучение с нуля.
Imprinting: Пропуск. Веса инициализируются случайно (Kaiming init).
Adaptation (Обучение): Учим задачу Saddle только на «Пончике».
Ожидание: Модель выучит форму седла по краям и интерполирует её в центр (так как функция гладкая).
Test: Замораживаем модель. Запускаем «Зонд» в центр.
Группа 2: IMPRINTING (Память)
Эксперимент на выживание структуры.
Imprinting (Запечатление): Учим задачу Parity на всём пространстве (включая центр).
Веса модели формируют сложную «решетку» для разделения классов. Точность ~99%.
Adaptation (Переобучение): Меняем задачу на Saddle. Учим только на «Пончике». Включаем Weight Decay.
Оптимизатор должен снизить Loss по седлу на периферии, не имея данных о центре.
Test: Замораживаем модель, зондируем центр.
Что измеряется
Ученик (зонд) — это отдельная, маленькая нейросеть (обычно линейный классификатор или 2-слойный MLP), которая ставится поверх замороженного учителя (основной модели).
Алгоритм проверки:
Берем тестовые данные X_center из слепого пятна.
Прогоняем их через учителя: V = Teacher(X_center).
Учитель в этот момент не учится (weights frozen). Мы просто снимаем его внутреннее представление (эмбеддинги) или выходной вектор.
Подаем вектора на вход ученику.
Ученик пытается предсказать Задача А (Parity).
Логика: Ученик никогда не видит координат x, y. Он видит только то, как учитель обработал эти координаты.
Интерпретация точности Ученика:
Если Accuracy ~50-60%: Учитель выдал хаос. Вектора V не содержат информации о шахматном порядке. Структура разрушена.
Если Accuracy ~90-95% (при WD=0): Учитель идеально закодировал координаты (Generalization). Ученик использовал это, чтобы выучить Parity заново.
Если Accuracy ~80-85% (при WD=1e-3, где Control упал): Это структурная память. Учитель выдал вектора, которые уже сгруппированы в кластеры по старой памяти.
Я прогнал обучение с разными значениями Weight Decay (WD) по 20 проходов и замерил, насколько хорошо в слепом пятне сохраняется структура старой задачи.
Вот что получилось:
|
Weight Decay (Давление) |
Точность Control (Обучение с нуля) |
Точность Imprint (Память) |
Гистерезис |
Интерпретация режима |
|
0.0 |
93.4% |
89.6% |
-3.8% |
Переобучение: Оба выучили координаты идеально. |
|
1e-4 |
91.0% |
90.2% |
-0.8% |
Плато: Давление слишком слабое. |
|
3e-4 |
80.1% |
88.2% |
+8.1% |
Начало разрыва: Control начинает терять качество. |
|
6e-4 |
65.5% |
88.1% |
+22.6% |
Фазовый переход: Резкое падение Control. |
|
1e-3 |
57.3% (Шум) |
82.8% (Высокая) |
+25.4% |
ПИК (Sweet Spot): Максимальный гистерезис. |
|
2e-3 |
54.8% |
75.1% |
+20.3% |
Угасание: Память всё еще сильна. |
|
1e-2 |
54.5% |
58.6% |
+4.2% |
Стирание: Регуляризация уничтожает всё. |
Результаты:
Зона "Бесплатного обучения" (WD < 1e-4):
Обе модели показывают точность >90%. Это не память, это способность нейросети обобщать координаты. Без штрафа за веса (WD=0) Control-модель идеально интерполирует пространство даже там, где не видела данных.
Зона "Структурного Гистерезиса" (WD = 6e-4 ... 2e-3):
Это и есть открытие. При давлении WD ~ 10^-3:
Control (57.3%): Оптимизатор сдается. Ему дешевле занулить веса в центре, чем выстраивать сложную структуру без данных.
Imprinting (82.8%): Оптимизатор ленится. Ему дешевле оставить старую структуру, чем сносить её.
Итог: Разница в 25.4% — это чистый вклад прошлого опыта, который невозможно объяснить случайностью.
Зона "Стирания" (WD > 1e-2):
Давление становится настолько сильным, что любая структура разрушается. Только здесь наступает реальное забывание.
Чтобы убедиться, что это не ошибка метрик, я визуализировал скрытое пространство (Latent Space) моделей в режиме WD = 1e-3 с помощью t-SNE.
У Control-группы: Гладкая линия. Модель выучила непрерывную функцию, в центре нет никаких границ классов.
У Imprinting-группы: Четкие кластеры. Модель сохранила осколки старой задачи. Она думает категориями шахматной доски, даже пытаясь изображать седло.
Код эксперимента, (можно уменьшить количество проходов с 20, на моем i5 заняло около трёх часов)
Скрытый текстimport torch import torch.nn as nn import torch.optim as optim import numpy as np # ========================================== # 1. КОНФИГУРАЦИЯ # ========================================== N_RUNS_PER_WD = 20 # Увеличили для надежности (займет время!) N_SAMPLES = 5000 BLIND_RADIUS = 1.5 OUTPUT_B_DIM = 20 # Детальная сетка Weight Decay (логарифмическая шкала + точки интереса) WD_VALUES = [ 0.0, 1e-5, 5e-5, 1e-4, 3e-4, 6e-4, 1e-3, # Ожидаемый "Sweet Spot" 2e-3, 4e-3, 7e-3, 1e-2, 2e-2 # Зона полной смерти ] # ========================================== # 2. ФУНКЦИИ (Генерация данных) # ========================================== def task_A_Parity(x): grid_x = torch.floor(x[:, 0]); grid_y = torch.floor(x[:, 1]) return ((grid_x + grid_y) % 2 == 0).float().unsqueeze(1) def task_B_Saddle(x): return ((x[:, 0]**2 - x[:, 1]**2) / 5.0).unsqueeze(1) def get_full_data(n): X = (torch.rand(n, 2) * 6 - 3); return X, task_A_Parity(X) def get_donut_data(n): X_list = [] while len(X_list) < n: batch = (torch.rand(n, 2) * 6 - 3) R = torch.norm(batch, dim=1) mask = (R > BLIND_RADIUS) & (R < 3.5) X_list.append(batch[mask]) X = torch.cat(X_list)[:n]; return X, task_B_Saddle(X) def get_center_data(n): X_list = [] while len(X_list) < n: batch = (torch.rand(n, 2) * 6 - 3) R = torch.norm(batch, dim=1) mask = R < BLIND_RADIUS X_list.append(batch[mask]) X = torch.cat(X_list)[:n]; return X, task_A_Parity(X) # ========================================== # 3. МОДЕЛЬ # ========================================== class Teacher(nn.Module): def __init__(self): super().__init__() self.core = nn.Sequential( nn.Linear(2, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, 32) ) self.head_a = nn.Linear(32, 1); self.head_b = nn.Linear(32, OUTPUT_B_DIM) def forward_A(self, x): return torch.sigmoid(self.head_a(self.core(x))) def forward_B(self, x): return self.head_b(self.core(x)) # ========================================== # 4. ЛОГИКА ОБУЧЕНИЯ И ТЕСТА # ========================================== def run_comparison(wd_value): def train_and_probe(scenario): teacher = Teacher() # 1. IMPRINTING if scenario == 'IMPRINTING': X_full, Y_par = get_full_data(N_SAMPLES) opt = optim.Adam(teacher.parameters(), lr=0.01) crit = nn.BCELoss() for _ in range(300): loss = crit(teacher.forward_A(X_full), Y_par) opt.zero_grad(); loss.backward(); opt.step() # 2. ADAPTATION (Weight Decay применяется здесь) X_donut, Y_sad = get_donut_data(N_SAMPLES) opt = optim.Adam(teacher.parameters(), lr=0.001, weight_decay=wd_value) crit = nn.MSELoss() for _ in range(800): # Важно: ограничиваем среднее, чтобы дать свободу скрытым измерениям loss = crit(teacher.forward_B(X_donut).mean(dim=1, keepdim=True), Y_sad) opt.zero_grad(); loss.backward(); opt.step() # 3. PROBE (Test в центре) X_cen, Y_par_cen = get_center_data(1000) teacher.eval() with torch.no_grad(): V = teacher.forward_B(X_cen) # Проверка на "смерть" нейронов if V.std().item() < 0.001: return 0.5 student = nn.Sequential(nn.Linear(OUTPUT_B_DIM, 64), nn.ReLU(), nn.Linear(64, 1), nn.Sigmoid()) opt_s = optim.Adam(student.parameters(), lr=0.01) crit_s = nn.BCELoss() for _ in range(300): loss = crit_s(student(V.detach()), Y_par_cen) opt_s.zero_grad(); loss.backward(); opt_s.step() with torch.no_grad(): return ((student(V) > 0.5) == Y_par_cen).float().mean().item() return train_and_probe('CONTROL'), train_and_probe('IMPRINTING') # ========================================== # 5. ЗАПУСК И СБОР СТАТИСТИКИ # ========================================== print(f"ЗАПУСК ДЕТАЛЬНОГО СКАНИРОВАНИЯ: {len(WD_VALUES)} точек, по {N_RUNS_PER_WD} прогонов.") print("="*105) print(f"{'Weight Decay':<12} | {'Control':<10} | {'Imprint':<10} | {'LEAKAGE':<10} | {'Std Dev':<10} | {'Status':<25}") print("-" * 105) stats = [] for wd in WD_VALUES: c_list, i_list = [], [] for i in range(N_RUNS_PER_WD): c, m = run_comparison(wd) c_list.append(c); i_list.append(m) # Небольшой индикатор прогресса внутри точки if i % 5 == 0: print(".", end="", flush=True) print("", end="\r") # Очистка строки точек mc, mi = np.mean(c_list), np.mean(i_list) std_i = np.std(i_list) # Разброс Imprinting важнее всего leak = mi - mc stats.append((wd, leak)) # Автоматическая классификация режима if mc > 0.85 and mi > 0.85: status = "Overfitting (Both High)" elif mc < 0.60 and mi > 0.75: status = ">>> MEMORY ZONE <<<" elif mc < 0.60 and mi < 0.60: status = "Erasure (Both Low)" else: status = "Transition" # Рисуем "звездочки" утечки bar = "*" * int(leak * 100 / 2.5) # 1 звезда на 2.5% print(f"{wd:<12.1e} | {mc:.1%} | {mi:.1%} | +{leak:.1%} {bar:<10} | +/-{std_i:.1%} | {status}") print("="*105) best = max(stats, key=lambda x: x[1]) print(f"ПИКОВАЯ УТЕЧКА: +{best[1]:.2%} при WD={best[0]}")
Почему Weight Decay помогает сохранить память? Оптимизатор делает то, что от него требуется, оптимизирует бюджет (Loss + Weight Decay).
В случае Control: Строить сложную структуру в центре с нуля — дорого (штраф WD). Проще сделать плоскую заглушку.
В случае Imprinting: Структура уже построена. Менять веса стоит ресурсов. Дешевле оставить всё как есть, если это не противоречит новой задаче на периферии.
Weight Decay в этом режиме работает как стабилизатор инерции.
Нейросеть помнит то, что пытались стереть: выводы эксперимента
1. Иллюзия «Unlearning» и безопасность
Гипотеза: Нейросеть сохраняет память о старой задаче даже там, где новая задача не даёт градиентов (в слепых зонах). Память прячется не в выходах, а в избыточных степенях свободы (Null Space). Её можно извлечь с помощью зонда, даже если сама модель выдает мусор.
Практика: Если вы делаете Fine-tuning для «цензурирования» или забывания вредных данных, стандартный подход не работает. Обычный FT (с WD ~1e-4) просто загоняет старые паттерны в подполье, но не стирает их. Чтобы реально удалить информацию, нужен либо Re-initialization (сброс весов), либо экстремально агрессивный Weight Decay, который разрушит саму структуру весов.
2. Ловушка структурного гистерезиса
Гипотеза: Две модели могут иметь одинаковый Loss и одинаковые ответы на стандартные запросы, но быть функционально разными. Это структурный гистерезис: различия видны только через историю обучения.
Практика: Валидация невалидна. У вас могут быть два чекпоинта с одинаковой точностью. Но первый, обученный с нуля — это чистый лист, а второй дообученный может выдать старый паттерн на редком запросе.
3. Weight Decay — больше чем регуляризатор
Гипотеза: WD работает как селектор,. очищающий структуру в модели, которая учится с нуля (Control), но парадоксальным образом консервирует структуру весов в уже обученной модели.
Практика: Настройка WD зависит от цели.
Если нужен Transfer Learning (перенос знаний) — умеренный WD полезен, он не дает старой структуре раствориться.
Если нужна полная адаптация (Tabula Rasa) — стандартный WD может мешать, так как он недостаточно силен, чтобы стереть структуру прошлого, но достаточно силен, чтобы исказить новое обучение.
4. Геометрия задач (Совместимость)
Гипотеза: Новая задача сохраняет старую структуру лучше, если они геометрически совместимы. Задачи, которые разделяют координаты (как в нашем эксперименте: одна работала с четностью, другая с формой), консервируют память сильнее, чем задачи, которые смешивают всё в кучу.
Практика: Чем ортогональнее новая задача старой, тем сложнее стереть предыдущие знания обычным дообучением.
Высокая точность на новой задаче не означает, что модель забыла старую. Она просто её скрывает.
Источник


