Продолжение идеи 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

Тестировали на трёх последовательных задачах:

  1. Sentiment Analysis (отзывы)

  2. Topic Classification (классификация по темам)

  3. 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 🙏