Продолжение идеи DTG-MA
Если вы помните мою предыдущую статью про DTG-MA, то знаете, что там была идея не «лечить» катастрофическое забывание регуляризациями, а архитектурно запретить нейросети портить уже выученные задачи. Вот что получилось, когда я пошел от этой идеи дальше. Эта технология решает коммерческую задачу в моем стартапе (делюсь с вами решением). Все полноценные тесты и библиотека на pyton есть в git (Split/Permuted MNIST + CIFAR-100, FCDCNN, ResNet-18 + FCD, GPT-2 + FCD-LoRA) можно подключать к своим проектам и использовать.
Суть
Frozen Core Decomposition (FCD) — это архитектурный подход, который математически гарантирует, что нейросеть не забудет старые задачи. Вместо регуляризаций и штрафов мы просто замораживаем общую базу нейросети после первой задачи и даём моделям учиться только через крошечные задачи-специфичные векторы (по 16-32 параметра на задачу).
Результат: забывание < 1%, расход памяти O(k × количество_задач) вместо линейного роста с размером сети, и это работает с CNN, LLM, и любыми другими архитектурами.
Почему нейросети вообще забывают
Проблема, которая стоит в начале, проста как день: одни и те же параметры используются для разных задач.
Представьте, что вы учили сеть распознавать кошек. Какие-то веса стали очень важны — они захватили паттерны усов, ушей, глаз кошек. Потом вы даёте ей новую задачу: распознавать собак. Стандартный SGD спокойно переписывает эти веса, потому что они помешали точности на собаках. И вот кошки забыты.
Это катастрофическое забывание.
Как другие методы пытались это лечить
Метод | Подход | Проблема |
|---|---|---|
EWC | Штраф Fisher за изменение важных весов | Мягкие ограничения → всё равно забывает (10-50% потери) |
LwF | Knowledge Distillation из старой модели | Нужно хранить старые модели или данные |
Replay | Буфер старых примеров на каждой итерации | Требует памяти для данных, нарушает приватность |
PNN | Новый столбец параметров на каждую задачу | O(T × N) память, становится невыносимо тяжело |
PackNet | Маски + pruning к новым задачам | Деградирует при 5+ задачах |
Все эти методы работают через компромиссы. Либо мягкие ограничения (которые иногда ломаются), либо требование хранить прошлое.
Шаг назад: DTG-MA и полное переосмысление
В своей предыдущей статье я представил Dynamic Task-Graph Masked Attention (DTG-MA) — подход, который говорит:
Не пытайтесь штрафовать и регуляризировать. Просто архитектурно запретите нейросети менять веса старых задач.
DTG-MA делал это на уровне графа вычислений:
Для каждой задачи строилась своя подсеть
Маски внимания перекрывали пути между задачами
Градиенты физически не могли попасть туда, куда им нельзя
Результат: забывание → 0%, но...
Цена: параметры растут с каждой новой задачей.
Логичный вопрос: а можно ли сделать то же самое, но без раздувания модели?
Ключевая идея: факторизация весов вместо маскирования
Вместо того чтобы хранить отдельные веса для каждой задачи, мы используем Tucker-стиль тензорную факторизацию.
Как это устроено (и почему это работает)
Вместо обычной матрицы весов слоя:
W ∈ ℝ^(входы × выходы)
Мы генерируем веса как комбинацию разложенных компонент:
W_t = U · (S ×₃ v_t) · V
Где:
U ∈ ℝ^(входы × r) — общая матрица признаков на входе (замораживается)
V ∈ ℝ^(r × выходы) — общая матрица признаков на выходе (замораживается)
S ∈ ℝ^(r × r × k) — «ядро» (core tensor), хранит общее пространство (замораживается)
v_t ∈ ℝ^k — крошечный задача-специфичный вектор (обучается)
Пример размеров (Split MNIST)
Компонента | Размер | Параметры | Обучается? |
|---|---|---|---|
U (784 → 32) | 784 × 32 | 25,088 | ❌ Заморозка после задачи 0 |
V (32 → 10) | 32 × 10 | 320 | ❌ Заморозка после задачи 0 |
S (ядро) | 32 × 32 × 16 | 16,384 | ❌ Заморозка после задачи 0 |
v₀ | 16 | 16 | ✅ Обучается |
v₁ | 16 | 16 | ✅ Обучается |
... | ... | ... | ... |
v₁₉ | 16 | 16 | ✅ Обучается |
Всего для 20 задач: | 41,792 | Только 320 параметров обучаемых |
Заморозка ядра — главный трюк
Обучение устроено в три этапа:
Этап 1: Первая задача (Task 0)
Обучаем всё: U, V, S и v₀ обычным способом.
L = CrossEntropy(model(x), y)
optimizer.step() # обновляются U, V, S, v₀
Этап 2: Заморозка (один раз)
После первой задачи:
U.requires_grad = False
V.requires_grad = False
S.requires_grad = False
v₀.requires_grad = False # Тоже замораживаем
Этап 3: Все остальные задачи
Для задачи t > 0:
# Инициализируем v_t ортогонально предыдущим
v_t = gram_schmidt_orthogonal(v₀, ..., v_{t-1})
# Обучаем ТОЛЬКО v_t
while training:
loss = CrossEntropy(...) + λ_sep * separation_loss(v_t, [v₀, ..., v_{t-1}])
optimizer.step() # обновляется ТОЛЬКО v_t
# Замораживаем v_t
v_t.requires_grad = False
Почему забывание исчезает (математика)
Вот почему это работает не эмпирически, а по конструкции:
Вес для старой задачи p был:
W_p = U · (S ×₃ v_p) · V
Когда мы обучаем новую задачу t+1:
Параметр | Заморожен? | Может ли градиент туда попасть? |
|---|---|---|
U | ✅ Да | ❌ Нет |
V | ✅ Да | ❌ Нет |
S | ✅ Да | ❌ Нет |
v_p (p < t) | ✅ Да | ❌ Нет |
v_t | ✅ Да (после обучения) | ❌ Нет |
Поскольку ни один параметр в формуле W_p не обновляется, вес W_p остаётся абсолютно неизменным.
Это не эмпирическое совпадение. Это математическое следствие архитектуры.
Ключевое отличие: EWC и другие регуляризации говорят: «пожалуйста, не меняй эти веса». FCD говорит: «тебе физически невозможно изменить эти веса».
Сравнение с DTG-MA: в чём разница?
Критерий | DTG-MA | FCD |
|---|---|---|
Блокирует что? | Пути вычислений | Пути изменения весов |
На каком уровне? | Граф (маски внимания) | Параметризация (факторизация) |
Рост памяти | O(T × N) | O(k × T) |
Для каких архитектур? | Transformer-ориентирован | Любые слои (Dense, Conv, RNN) |
Сложность реализации | Высокая (изменяет архитектуру) | Низкая (drop-in адаптер) |
Аналогия:
DTG-MA: запрещаем ходить по определённым дорогам
FCD: запрещаем менять фундамент здания
Практические результаты
Split MNIST (5 задач, 2 класса каждая)
Метод | Финальная точность | Забывание | Параметры на задачу |
|---|---|---|---|
FCD (наш) | 96.1% ± 0.4% | 0.2% ± 0.2% | 16 |
HAT | 82.9% ± 4.1% | 19.3% ± 5.1% | ~100K (маски) |
PackNet | 56.6% ± 2.6% | 40.6% ± 2.3% | маски |
DER++ | 56.6% ± 2.2% | 52.5% ± 2.9% | ~100K (буфер) |
EWC | 57.0% ± 3.4% | 52.2% ± 4.3% | ~2× параметров |
Fine-tuning (базовый) | 55.8% ± 1.9% | 54.0% ± 2.4% | 0 (но забывает!) |
Что здесь интересно:
Забывание 0.2% vs 52.5% — это разница между архитектурной защитой и попыткой её обойти.
И это при том, что мы добавляем всего 16 параметров на задачу, в то время как другие методы либо добавляют полные маски (~100K), либо требуют буферов данных.
Permuted MNIST (10 задач)
Это жёстче — каждая задача это случайная перестановка пикселей MNIST:
Метод | Точность | Забывание |
|---|---|---|
FCD | 82.2% | 0.2% |
EWC | 65.7% | 2.6% |
HAT | 49.2% | 35.5% |
Fine-tuning | 30.3% | 68.6% |
Split CIFAR-100 (10 задач по 10 классов)
Более сложный датасет:
Метод | Точность | Забывание |
|---|---|---|
FCD + ResNet-18 адаптер | 59.8% | 0.3% |
ResNet-18 + Fine-tuning | 16.2% | 61.3% |
Это 200× меньше забывания при 3.7× выше точности.
FCDCNN — CNN обученная с нуля
Чтобы показать, что метод работает не только с pretrained моделями:
Метод | Точность | Забывание |
|---|---|---|
FCD + CNN (с нуля) | 57.6% | 1.3% |
Fine-tuning CNN | 17.1% | 72.7% |
56× меньше забывания — метод работает даже когда backbone обучается с нуля.
Масштабируемость: когда задач больше чем размер вектора
Что если T > k? (т.е. задач больше, чем размерность v_t)
Количество задач T | k = 16 | Точность | Забывание |
|---|---|---|---|
5 | 16 | 98.4% | 0.0% |
10 | 16 | 97.6% | 0.5% |
16 | 16 | 97.6% | 0.9% |
17 | 16 | 97.4% | 1.1% |
20 | 16 | 96.6% | 1.3% |
Ключевой момент: метод изящно деградирует. При k=16 и 20 задачах точность падает на 1.8%, а забывание только 1.3%. Это не обрыв — это плавное расширение границ.
Почему? Потому что даже когда T > k, вектора всё равно находят почти-ортогональные направления в k-мерном пространстве (это свойство концентрации меры в высоких размерностях).
Большие модели:
GPT-2 + FCD-LoRA для continual learning
Тестировали на трёх последовательных задачах:
Sentiment Analysis (отзывы)
Topic Classification (классификация по темам)
Question Answering (ответы на вопросы)
Конфигурация | Забывание | Обучаемые параметры | % от модели |
|---|---|---|---|
Hard FCD (core freezing) | 0% | 576 | < 0.01% |
Soft FCD (только separation loss) | 5.4% | 848K | 0.7% |
Стандартный LoRA | ~30% | 768K | 0.6% |
Что произошло:
Стандартный LoRA добавляет матрицы B и A, которые обновляются для каждой новой задачи. Это позволяет максимально использовать параметры, но ломает задачу 1.
FCD-LoRA заменяет LoRA-веса на Tucker-факторизованные, где core замораживается. Результат: 0% забывания (hard FCD) или приемлемые 5.4% с полной ёмкостью (soft FCD).
Ограничения (они есть, не буду скрывать)
1. Нужен ID задачи при инференсе
model.set_task(task_id=0) # "Сеточка, перед тобой задача про кошек"
output = model(x)
Это task-aware setting. Если вы не знаете, какая это задача, метод не поможет.
2. Размерность k ограничивает число идеально ортогональных задач
Если k = 16, то идеально ортогональными могут быть максимум 16 задач. После этого деградация.
Но она плавная (1.3% забывания на 20-й зада��е), а не обрыв.
3. Нужно хранить v_t для каждой задачи
Если у вас 1000 задач, нужно хранить 1000 × 16-байтных векторов. Это пустяк по сравнению с полной сетью, но всё равно оверхед O(T×k).
Таблица сравнения со всеми методами
Метод | Забывание | Память на задачу | Задачи-осведомлен? | Нужны старые данные? | Легко реализовать? |
|---|---|---|---|---|---|
Fine-tuning | 30-80% | O(N) | ❌ | ❌ | ✅ Да |
EWC | 10-50% | O(2N) | ❌ | ❌ | ✅ Да |
LwF | 5-30% | O(N) | ❌ | ✅ | ⚠️ Условно |
Replay | 1-5% | O(buffer) | ❌ | ✅ (буфер) | ⚠️ Условно |
PNN | ~0% | O(T × N) | ✅ | ❌ | ❌ Сложно |
PackNet | ~0% | O(N) | ✅ | ❌ | ❌ Очень сложно |
FCD | < 1% | O(T × k) | ✅ | ❌ | ✅ Да |
Как это реализуется
Есть полная реализация на PyTorch 2.0+:
https://github.com/infosave2007/fcd
Главные компоненты:
1. Tucker-факторизация
def factorized_weight(U, S, v, V):
"""
W_t = U · (S ×₃ v) · V
"""
# S ×₃ v — mode-3 контракция
M = torch.einsum('ijk,k->ij', S, v) # (r, r)
# U · M
P = U @ M # (d_in, r)
# P · V
W = P @ V # (d_in, d_out)
return W
2. Separation loss (для ортогональности)
def separation_loss(v_t, v_prev_list):
"""
Штраф за то, чтобы новый вектор был ортогонален старым
"""
loss = 0
for v_p in v_prev_list:
cos_sim = F.cosine_similarity(v_t.unsqueeze(0), v_p.unsqueeze(0))
loss += cos_sim ** 2
return loss
3. Обучающий цикл
def continual_learning_protocol():
U, V, S = init_tensors()
v_list = []
for task_id in range(num_tasks):
# Инициализируем v_t ортогонально
v_t = gram_schmidt_orthogonal(v_list)
v_t.requires_grad = True
# Обучение
for epoch in range(epochs):
for x, y in dataloader[task_id]:
W_t = factorized_weight(U, S, v_t, V)
logits = linear_layer(x, W_t)
loss = ce_loss(logits, y)
loss += λ_sep * separation_loss(v_t, v_list)
loss.backward()
optimizer.step()
# Заморозка
v_t.requires_grad = False
v_list.append(v_t)
if task_id == 0:
U.requires_grad = False
V.requires_grad = False
S.requires_grad = False
Почему FCD — логичное продолжение DTG-MA
DTG-MA: Как запретить нейросети менять уже выученные веса?
Ответ: маски на графе вычисления.
FCD: Как сделать это эффективно и универсально?
Ответ: заморозить общую основу, учиться через крошечные задачи-специфичные параметры.
Оба подхода исходят из одной революционной идеи:
Катастрофическое забывание — это не проблема оптимизации (которую нужно регуляризировать), а архитектурная проблема.
Если архитектура позволяет градиентам менять критичные веса — забывание неизбежно.
Когда это имеет смысл использовать
✅ Хорошо подходит для:
Robotic continual learning — робот учится выполнять задачи одну за другой
Многоязычные модели — добавляем язык за языком без переобучения
Edge ML — нужна модель, которая учится на устройстве без переполнения памяти
Personalization — система адаптируется к юзеру, не забывая про других
Domain incremental learning — новые домены приходят один за другим
❌ Плохо подходит для:
Task-agnostic setting — если вы не знаете, какая это задача при инференсе
Задач совсем мало (T < 3) — не имеет смысла
Очень большой k — если вам нужна очень высокая ёмкость на задачу
Абляция: что важно в методе?
Конфигурация | Точность | Забывание |
|---|---|---|
Полный метод | 96.1% | 0.2% |
Без separation loss | 95.8% | 0.2% |
Без core freezing | 93.2% | 6.7% |
Никаких тактик | 92.2% | 8.1% |
Главный вывод: Core freezing — это 99% успеха. Separation loss добавляет косметику.
Заключение
Frozen Core Decomposition — это не очередной «лосс с штрафом».
Это переосмысление того, как должны выглядеть параметры модели, если мы хотим, чтобы она:
Училась всю жизнь
Не теряла память
Не раздувалась экспоненциально
Ключевая магия: вместо того, чтобы штрафовать изменения весов (мягко) или запрещать пути в графе (для трансформеров), мы просто делаем невозможным для градиентов менять старые веса.
Результат:
< 1% забывания (не 10%, не 5%, а < 1%)
O(k × T) памяти вместо O(T × N)
Универсальность — работает с CNN, RNN, LLM, любыми слоями
Простота — drop-in адаптер, легко добавить к существующему коду
Если вам интересна идея управляемого, математически гарантированного continual learning — рекомендую попробовать.
Ресурсы
Repo: https://github.com/infosave2007/fcd
Научное изложение: doi 18006952
Автор: Олег Кириченко — urevich55@gmail.com
Если вам полезен этот проект: Поддержать через Tribute 🙏