콘텐츠로 이동
Data Prep
상세

Continual Learning (CL)

메타 정보

항목 내용
분류 Learning Paradigm / Lifelong Learning
핵심 논문 "Overcoming Catastrophic Forgetting in Neural Networks" (Kirkpatrick et al., PNAS 2017 - EWC), "Progressive Neural Networks" (Rusu et al., arXiv 2016), "Gradient Episodic Memory for Continual Learning" (Lopez-Paz & Ranzato, NeurIPS 2017 - GEM), "Dark Experience for General Continual Learning" (Buzzega et al., NeurIPS 2020 - DER++), "PackNet: Adding Multiple Tasks to a Single Network by Iterative Pruning" (Mallya & Lazebnik, CVPR 2018), "A Comprehensive Survey of Continual Learning" (Wang et al., IEEE TPAMI 2024)
주요 저자 James Kirkpatrick (EWC), Andrei Rusu (Progressive Nets), David Lopez-Paz (GEM), Pietro Buzzega (DER++), Gido van de Ven, Andreas Tolias
핵심 개념 새로운 태스크를 순차적으로 학습하면서 이전 지식을 유지하는 학습 패러다임
관련 분야 Transfer Learning, Meta-Learning, Multi-Task Learning, Curriculum Learning, Knowledge Distillation

정의

Continual Learning(CL)은 모델이 순차적으로 도착하는 태스크/데이터를 학습하면서, 이전에 학습한 지식을 catastrophic forgetting(파국적 망각) 없이 유지하는 것을 목표로 하는 학습 패러다임이다.

기존 학습 방식과의 비교

항목 정적 학습 (Static) Multi-Task Learning Continual Learning
데이터 접근 전체 데이터 동시 전체 태스크 동시 순차적 (이전 데이터 접근 불가)
태스크 수 고정 고정 증가
메모리 전체 저장 전체 저장 제한적
주요 과제 과적합 태스크 간 간섭 망각 vs 가소성
현실 반영도 낮음 중간 높음

핵심 문제: Catastrophic Forgetting

Catastrophic Forgetting의 메커니즘:

[Task A 학습]
+------------------------------------------+
|  파라미터가 Task A의 최적해로 수렴        |
|  theta_A* = argmin L_A(theta)            |
+------------------------------------------+
          |
          v
[Task B 학습 (Task A 데이터 없이)]
+------------------------------------------+
|  파라미터가 Task B 방향으로 이동          |
|  theta_B* = argmin L_B(theta)            |
|                                          |
|  --> Task A 성능이 급격히 하락!          |
|  --> theta_B*는 theta_A*에서 멀어짐      |
+------------------------------------------+

핵심 딜레마: Stability-Plasticity Tradeoff
- Stability: 이전 지식을 안정적으로 유지
- Plasticity: 새로운 지식을 유연하게 습득
- 둘 다 완벽하게 달성하는 것은 이론적으로 어려움

CL 시나리오 분류

CL 문제는 태스크 정보의 가용성에 따라 세 가지 시나리오로 구분된다:

Continual Learning 시나리오:

+---------------------------------------------+
| 1. Task-Incremental Learning (Task-IL)      |
| - 추론 시 태스크 ID 제공됨                  |
| - 태스크별 독립적 출력 헤드 사용 가능       |
| - 난이도: 가장 쉬움                         |
| - 예: "이것은 Task 2의 입력이다" 정보 제공  |
+---------------------------------------------+
          |
          v
+---------------------------------------------+
| 2. Domain-Incremental Learning (Domain-IL)  |
| - 태스크 ID 미제공, 출력 공간 동일          |
| - 입력 분포만 변화                          |
| - 난이도: 중간                              |
| - 예: 다른 환경의 자율주행 데이터           |
+---------------------------------------------+
          |
          v
+---------------------------------------------+
| 3. Class-Incremental Learning (Class-IL)    |
| - 태스크 ID 미제공, 출력 공간 확장          |
| - 새로운 클래스가 지속적으로 추가           |
| - 난이도: 가장 어려움                       |
| - 예: 새로운 객체 카테고리 추가             |
+---------------------------------------------+
시나리오 태스크 ID (학습) 태스크 ID (추론) 출력 공간 대표 벤치마크
Task-IL 제공 제공 태스크별 분리 Permuted MNIST
Domain-IL 제공 미제공 공유 Rotated MNIST
Class-IL 제공 미제공 확장 Split CIFAR-100

핵심 방법론

CL 방법론은 크게 세 가지 패밀리로 분류된다:

Continual Learning 방법론 분류:

+---------------------------------------------+
| 1. Regularization-Based                     |
| - 중요 파라미터의 변화를 제한               |
| - 추가 메모리 불필요 (파라미터 수준)        |
| - EWC, SI, MAS, LwF                        |
+---------------------------------------------+
          |
          v
+---------------------------------------------+
| 2. Replay-Based                             |
| - 이전 데이터를 버퍼에 저장/생성하여 재학습 |
| - 메모리 버퍼 필요                          |
| - ER, GEM, A-GEM, DER++, GDumb             |
+---------------------------------------------+
          |
          v
+---------------------------------------------+
| 3. Architecture-Based                       |
| - 태스크별 전용 파라미터 할당               |
| - 모델 크기 증가                            |
| - Progressive Nets, PackNet, DEN            |
+---------------------------------------------+

1. Regularization-Based Methods

EWC (Elastic Weight Consolidation)

항목 내용
논문 Kirkpatrick et al., PNAS 2017
핵심 Fisher Information Matrix로 중요 파라미터 식별 후 변화 억제
직관 이전 태스크에 중요한 가중치일수록 더 강하게 고정
장점 단순하고 구현 용이, 추가 데이터 저장 불필요
단점 Fisher 근사의 부정확성, 태스크 수 증가 시 누적 제약

EWC 손실 함수:

\[ L(\theta) = L_B(\theta) + \frac{\lambda}{2} \sum_i F_i (\theta_i - \theta_{A,i}^*)^2 \]
  • \(L_B(\theta)\): 새 태스크 B의 손실
  • \(F_i\): Fisher Information Matrix의 대각 원소 (파라미터 \(i\)의 중요도)
  • \(\theta_{A,i}^*\): 태스크 A 학습 후 최적 파라미터
  • \(\lambda\): 정규화 강도 (stability-plasticity 균형 조절)
EWC의 작동 원리:

파라미터 공간에서의 최적화 경로:

    theta_2 (파라미터 2)
    ^
    |     * theta_A* (Task A 최적해)
    |    /|
    |   / |  <-- Fisher가 큰 방향: 이동 억제
    |  /  |
    | /   |
    |/ _ _|_ _ _ _ _ _ _> theta_1 (파라미터 1)
    |     ^
    |     |
    |     Fisher가 작은 방향: 자유롭게 이동 가능
    |

    --> Task B 학습 시 중요 파라미터 방향의 이동만 제한
    --> 덜 중요한 방향으로는 자유롭게 이동하여 새 태스크 학습

SI (Synaptic Intelligence)

항목 내용
논문 Zenke et al., ICML 2017
핵심 온라인으로 파라미터 중요도를 축적 (path integral)
vs EWC EWC는 학습 후 Fisher 계산, SI는 학습 중 실시간 추적
장점 별도 중요도 계산 단계 불필요
\[ L(\theta) = L_{current}(\theta) + c \sum_k \Omega_k (\theta_k - \theta_k^*)^2 \]
  • \(\Omega_k\): 파라미터 \(k\)의 누적 중요도 (경로 적분으로 계산)

LwF (Learning without Forgetting)

항목 내용
논문 Li & Hoiem, IEEE TPAMI 2018
핵심 Knowledge Distillation을 CL에 적용
방식 새 데이터에 대한 이전 모델의 출력을 soft target으로 사용
장점 이전 데이터 저장 불필요, 파라미터 중요도 계산 불필요
단점 태스크 간 유사도가 낮으면 효과 감소

2. Replay-Based Methods

ER (Experience Replay)

항목 내용
핵심 이전 태스크의 샘플을 고정 크기 메모리 버퍼에 저장
버퍼 관리 Reservoir Sampling으로 균등 분포 유지
학습 현재 태스크 배치 + 버퍼 배치를 함께 학습
장점 단순하지만 강력한 베이스라인
단점 개인정보 문제 (원본 데이터 저장), 메모리 제한

GEM / A-GEM

항목 GEM A-GEM
논문 Lopez-Paz & Ranzato, NeurIPS 2017 Chaudhry et al., ICLR 2019
핵심 이전 태스크 손실 증가 방지 제약 GEM의 효율적 근사
제약 모든 이전 태스크에 대해 gradient projection 랜덤 샘플된 이전 배치 1개로 근사
복잡도 O(태스크 수) QP 문제 O(1) 단일 projection
성능 정확하지만 느림 빠르지만 다소 부정확

GEM의 제약 조건:

\[ \langle g, g_k \rangle \geq 0, \quad \forall k < t \]
  • \(g\): 현재 태스크의 gradient
  • \(g_k\): 이전 태스크 \(k\)의 메모리 기반 gradient
  • 현재 업데이트가 이전 태스크의 손실을 증가시키지 않도록 보장

DER++ (Dark Experience Replay++)

항목 내용
논문 Buzzega et al., NeurIPS 2020
핵심 입력 + 레이블뿐 아니라 모델의 logit 출력도 함께 저장
직관 "dark knowledge" (soft output)가 hard label보다 풍부한 정보 제공
손실 cross-entropy (현재) + MSE (버퍼 logit) + cross-entropy (버퍼 label)
성능 단순 ER 대비 일관된 개선

DER++ 손실 함수:

\[ L = L_{ce}(f_\theta(x), y) + \alpha \cdot \|f_\theta(x_{buf}) - z_{buf}\|^2 + \beta \cdot L_{ce}(f_\theta(x_{buf}), y_{buf}) \]
  • \((x, y)\): 현재 태스크 샘플
  • \((x_{buf}, y_{buf}, z_{buf})\): 버퍼의 (입력, 레이블, 저장된 logit)
  • \(\alpha\): logit matching 강도
  • \(\beta\): 레이블 matching 강도

3. Architecture-Based Methods

Progressive Neural Networks

항목 내용
논문 Rusu et al., arXiv 2016 (DeepMind)
핵심 새 태스크마다 새 네트워크 칼럼 추가 + lateral connection
장점 망각 완전 방지 (이전 칼럼 동결)
단점 태스크 수에 비례하여 모델 크기 선형 증가
적용 RL 환경에서의 태스크 전이에 효과적
Progressive Neural Networks 구조:

Task 1          Task 2          Task 3
Column          Column          Column

[Layer 3]       [Layer 3]       [Layer 3]
    |               |  <---         |  <--- <---
[Layer 2]       [Layer 2]       [Layer 2]
    |               |  <---         |  <--- <---
[Layer 1]       [Layer 1]       [Layer 1]
    |               |               |
  Input           Input           Input

<--- : Lateral Connection (이전 칼럼에서 지식 전이)

- 이전 칼럼: frozen (수정 불가)
- 새 칼럼: 자유롭게 학습
- Lateral connection: 이전 지식 재활용

PackNet

항목 내용
논문 Mallya & Lazebnik, CVPR 2018
핵심 프루닝으로 태스크별 서브네트워크 할당
과정 (1) 학습 -> (2) 프루닝 -> (3) 재학습 -> (4) 마스킹 고정
장점 단일 네트워크 내에서 여러 태스크 수용
단점 수용 가능한 태스크 수가 네트워크 용량에 제한
PackNet의 파라미터 할당:

하나의 네트워크 내부:

+-----------------------------------------+
|  [Task 1 전용]  [Task 2 전용]  [공유]   |
|  ############   oooooooooooo   ........  |
|  ############   oooooooooooo   ........  |
|  ############   oooooooooooo   ........  |
|  ############   oooooooooooo   [미사용]  |
+-----------------------------------------+

# = Task 1 파라미터 (고정됨)
o = Task 2 파라미터 (고정됨)
. = 공유 파라미터
  = 미사용 (향후 태스크용)

각 태스크 학습 후 프루닝으로 사용 파라미터 축소
--> 남은 용량을 다음 태스크에 할당

방법론 비교

성능 비교 (Split CIFAR-100, Class-IL, Buffer=2000)

방법 유형 평균 정확도 (%) 망각률 (%) 추가 메모리
Fine-tuning (baseline) - ~8-12 ~80+ 없음
EWC Regularization ~20-25 ~50-60 Fisher 행렬
SI Regularization ~19-24 ~55-65 중요도 행렬
LwF Regularization ~18-22 ~60-70 없음
ER Replay ~40-45 ~20-30 버퍼 2000
A-GEM Replay ~20-25 ~40-50 버퍼 2000
GDumb Replay ~35-42 N/A 버퍼 2000
DER++ Replay ~45-52 ~15-25 버퍼 2000 + logit
ER-ACE Replay ~48-55 ~12-20 버퍼 2000
PackNet Architecture ~50-60 (Task-IL) ~0 마스크
Progressive Nets Architecture ~55-65 (Task-IL) 0 추가 칼럼

주: 정확한 수치는 실험 설정(backbone, epoch, buffer 관리 등)에 따라 달라진다. 위 범위는 여러 논문에서 보고된 대략적 경향이다.

장단점 비교

방법 유형 장점 단점 적합 상황
Regularization 추가 메모리 불필요, 단순 Class-IL에서 성능 약함 메모리 제약 환경, 단순 태스크
Replay 강력한 성능, 범용적 데이터 저장 필요, 프라이버시 문제 대부분의 CL 시나리오
Architecture 망각 없음, 이론적 보장 모델 크기 증가, 확장성 제한 소수 태스크, 자원 충분

평가 지표

지표 수식 의미
Average Accuracy (AA) \(AA = \frac{1}{T} \sum_{i=1}^{T} a_{T,i}\) T개 태스크 학습 후 모든 태스크의 평균 정확도
Backward Transfer (BWT) \(BWT = \frac{1}{T-1} \sum_{i=1}^{T-1} (a_{T,i} - a_{i,i})\) 이후 학습이 이전 태스크에 미치는 영향 (음수 = 망각)
Forward Transfer (FWT) \(FWT = \frac{1}{T-1} \sum_{i=2}^{T} (a_{i-1,i} - \bar{b}_i)\) 이전 학습이 새 태스크 학습에 미치는 영향 (양수 = 전이)
Forgetting \(F_i = \max_{t \in \{1,...,T-1\}} a_{t,i} - a_{T,i}\) 태스크 \(i\)에서의 최대 성능 대비 최종 성능 하락
  • \(a_{t,i}\): 태스크 \(t\)까지 학습한 후 태스크 \(i\)에서의 정확도
  • \(\bar{b}_i\): 태스크 \(i\)의 랜덤 초기화 성능

주요 벤치마크

벤치마크 유형 설명
Permuted MNIST Domain-IL MNIST 픽셀을 태스크마다 다르게 permutation
Rotated MNIST Domain-IL MNIST를 태스크마다 다른 각도로 회전
Split MNIST Task-IL / Class-IL MNIST 10개 클래스를 5개 태스크로 분할
Split CIFAR-10 Class-IL CIFAR-10을 5개 태스크 (2 클래스/태스크)
Split CIFAR-100 Class-IL CIFAR-100을 10-20개 태스크로 분할
Split Mini-ImageNet Class-IL Mini-ImageNet 100 클래스를 분할
CORe50 복합 50개 객체, 11개 세션의 실제 연속 학습
Split TinyImageNet Class-IL TinyImageNet 200 클래스 분할

Python 구현 예시

EWC (Elastic Weight Consolidation)

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from copy import deepcopy


class EWC:
    """Elastic Weight Consolidation 구현.

    이전 태스크에 중요한 파라미터의 변화를 억제하여
    catastrophic forgetting을 완화한다.
    """

    def __init__(self, model: nn.Module, lambda_ewc: float = 400.0):
        self.model = model
        self.lambda_ewc = lambda_ewc
        self.saved_params = {}    # 태스크별 최적 파라미터
        self.fisher_diags = {}    # 태스크별 Fisher 대각 행렬
        self.task_count = 0

    def compute_fisher(self, dataloader: DataLoader, 
                       num_samples: int = 2000) -> dict:
        """Fisher Information Matrix의 대각 원소를 경험적으로 추정.

        F_i = E[(d log p(y|x, theta) / d theta_i)^2]
        """
        fisher = {}
        for name, param in self.model.named_parameters():
            fisher[name] = torch.zeros_like(param)

        self.model.eval()
        count = 0

        for inputs, targets in dataloader:
            if count >= num_samples:
                break

            inputs, targets = inputs.cuda(), targets.cuda()
            self.model.zero_grad()

            outputs = self.model(inputs)
            loss = nn.functional.cross_entropy(outputs, targets)
            loss.backward()

            for name, param in self.model.named_parameters():
                if param.grad is not None:
                    fisher[name] += param.grad.data ** 2

            count += inputs.size(0)

        # 평균
        for name in fisher:
            fisher[name] /= count

        return fisher

    def register_task(self, dataloader: DataLoader):
        """현재 태스크 학습 완료 후 호출. 파라미터와 Fisher 저장."""
        self.fisher_diags[self.task_count] = self.compute_fisher(dataloader)
        self.saved_params[self.task_count] = {
            name: param.data.clone()
            for name, param in self.model.named_parameters()
        }
        self.task_count += 1

    def penalty(self) -> torch.Tensor:
        """EWC 정규화 항 계산.

        L_ewc = (lambda/2) * sum_i F_i * (theta_i - theta_i*)^2
        """
        if self.task_count == 0:
            return torch.tensor(0.0).cuda()

        loss = torch.tensor(0.0).cuda()

        for task_id in range(self.task_count):
            for name, param in self.model.named_parameters():
                fisher = self.fisher_diags[task_id][name]
                old_param = self.saved_params[task_id][name]
                loss += (fisher * (param - old_param) ** 2).sum()

        return (self.lambda_ewc / 2.0) * loss


def train_with_ewc(model, ewc, dataloader, optimizer, epochs=10):
    """EWC를 적용한 학습 루프."""
    model.train()

    for epoch in range(epochs):
        total_loss = 0.0

        for inputs, targets in dataloader:
            inputs, targets = inputs.cuda(), targets.cuda()

            outputs = model(inputs)
            ce_loss = nn.functional.cross_entropy(outputs, targets)
            ewc_loss = ewc.penalty()
            loss = ce_loss + ewc_loss

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_loss += loss.item()

        avg_loss = total_loss / len(dataloader)
        if (epoch + 1) % 5 == 0:
            print(f"Epoch {epoch+1}/{epochs} - Loss: {avg_loss:.4f}")

Experience Replay with Reservoir Sampling

import torch
import numpy as np
from collections import defaultdict


class ReservoirBuffer:
    """Reservoir Sampling 기반 Experience Replay 버퍼.

    스트림 데이터에서 균등 확률로 샘플링하여
    고정 크기 버퍼를 유지한다.
    """

    def __init__(self, buffer_size: int = 2000):
        self.buffer_size = buffer_size
        self.buffer_x = []
        self.buffer_y = []
        self.n_seen = 0

    def add(self, x: torch.Tensor, y: torch.Tensor):
        """Reservoir Sampling으로 버퍼에 추가.

        i번째 샘플이 버퍼에 남을 확률 = buffer_size / i
        """
        batch_size = x.size(0)

        for i in range(batch_size):
            self.n_seen += 1

            if len(self.buffer_x) < self.buffer_size:
                self.buffer_x.append(x[i].cpu())
                self.buffer_y.append(y[i].cpu())
            else:
                # Reservoir sampling: 확률 buffer_size/n_seen으로 교체
                idx = np.random.randint(0, self.n_seen)
                if idx < self.buffer_size:
                    self.buffer_x[idx] = x[i].cpu()
                    self.buffer_y[idx] = y[i].cpu()

    def sample(self, batch_size: int):
        """버퍼에서 랜덤 배치 샘플링."""
        indices = np.random.choice(
            len(self.buffer_x), 
            size=min(batch_size, len(self.buffer_x)),
            replace=False
        )
        x = torch.stack([self.buffer_x[i] for i in indices]).cuda()
        y = torch.stack([self.buffer_y[i] for i in indices]).cuda()
        return x, y

    def __len__(self):
        return len(self.buffer_x)


def train_with_replay(model, buffer, dataloader, optimizer, 
                      epochs=10, replay_batch=64):
    """Experience Replay를 적용한 학습 루프."""
    model.train()

    for epoch in range(epochs):
        total_loss = 0.0

        for inputs, targets in dataloader:
            inputs, targets = inputs.cuda(), targets.cuda()

            # 현재 태스크 손실
            outputs = model(inputs)
            loss = nn.functional.cross_entropy(outputs, targets)

            # 리플레이 손실 (버퍼가 비어있지 않으면)
            if len(buffer) > 0:
                buf_x, buf_y = buffer.sample(replay_batch)
                buf_outputs = model(buf_x)
                replay_loss = nn.functional.cross_entropy(
                    buf_outputs, buf_y
                )
                loss = loss + replay_loss

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            # 현재 배치를 버퍼에 추가
            buffer.add(inputs, targets)

            total_loss += loss.item()

        avg_loss = total_loss / len(dataloader)
        if (epoch + 1) % 5 == 0:
            print(f"Epoch {epoch+1}/{epochs} - Loss: {avg_loss:.4f}")

DER++ (Dark Experience Replay++)

import torch
import torch.nn as nn
import numpy as np


class DERPlusPlusBuffer:
    """DER++ 버퍼: 입력, 레이블, logit을 함께 저장."""

    def __init__(self, buffer_size: int = 2000, num_classes: int = 100):
        self.buffer_size = buffer_size
        self.buffer_x = []
        self.buffer_y = []
        self.buffer_logits = []  # dark knowledge 저장
        self.n_seen = 0

    def add(self, x, y, logits):
        batch_size = x.size(0)
        for i in range(batch_size):
            self.n_seen += 1
            if len(self.buffer_x) < self.buffer_size:
                self.buffer_x.append(x[i].cpu())
                self.buffer_y.append(y[i].cpu())
                self.buffer_logits.append(logits[i].detach().cpu())
            else:
                idx = np.random.randint(0, self.n_seen)
                if idx < self.buffer_size:
                    self.buffer_x[idx] = x[i].cpu()
                    self.buffer_y[idx] = y[i].cpu()
                    self.buffer_logits[idx] = logits[i].detach().cpu()

    def sample(self, batch_size):
        indices = np.random.choice(
            len(self.buffer_x),
            size=min(batch_size, len(self.buffer_x)),
            replace=False
        )
        x = torch.stack([self.buffer_x[i] for i in indices]).cuda()
        y = torch.stack([self.buffer_y[i] for i in indices]).cuda()
        logits = torch.stack(
            [self.buffer_logits[i] for i in indices]
        ).cuda()
        return x, y, logits

    def __len__(self):
        return len(self.buffer_x)


def train_with_der_pp(model, buffer, dataloader, optimizer,
                      alpha=0.5, beta=0.5, epochs=10):
    """DER++ 학습 루프.

    L = L_ce(current) + alpha * MSE(logit matching) + beta * L_ce(buffer)

    Args:
        alpha: logit matching 가중치
        beta: buffer cross-entropy 가중치
    """
    model.train()

    for epoch in range(epochs):
        total_loss = 0.0

        for inputs, targets in dataloader:
            inputs, targets = inputs.cuda(), targets.cuda()

            # 현재 태스크 forward
            outputs = model(inputs)
            loss = nn.functional.cross_entropy(outputs, targets)

            if len(buffer) > 0:
                buf_x, buf_y, buf_logits = buffer.sample(64)
                buf_outputs = model(buf_x)

                # Logit matching (dark knowledge)
                logit_loss = nn.functional.mse_loss(
                    buf_outputs, buf_logits
                )

                # Buffer cross-entropy
                ce_buf_loss = nn.functional.cross_entropy(
                    buf_outputs, buf_y
                )

                loss = loss + alpha * logit_loss + beta * ce_buf_loss

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            # 현재 배치를 logit과 함께 버퍼에 저장
            with torch.no_grad():
                current_logits = model(inputs)
            buffer.add(inputs, targets, current_logits)

            total_loss += loss.item()

        avg_loss = total_loss / len(dataloader)
        if (epoch + 1) % 5 == 0:
            print(f"Epoch {epoch+1}/{epochs} - Loss: {avg_loss:.4f}")

CL 평가 프레임워크

import torch
import numpy as np


class CLEvaluator:
    """Continual Learning 평가 지표 계산기.

    Average Accuracy, Backward Transfer, Forward Transfer,
    Forgetting을 체계적으로 측정한다.
    """

    def __init__(self, num_tasks: int):
        self.num_tasks = num_tasks
        # accuracy_matrix[i][j] = 태스크 i까지 학습 후 태스크 j 정확도
        self.accuracy_matrix = np.zeros((num_tasks, num_tasks))

    def record(self, trained_up_to: int, task_id: int, accuracy: float):
        """정확도 기록.

        Args:
            trained_up_to: 현재까지 학습한 태스크 수 (0-indexed)
            task_id: 평가 대상 태스크 (0-indexed)
            accuracy: 정확도 (0~1)
        """
        self.accuracy_matrix[trained_up_to][task_id] = accuracy

    def average_accuracy(self) -> float:
        """Average Accuracy: 마지막 태스크까지 학습 후 전체 평균."""
        T = self.num_tasks
        return np.mean(self.accuracy_matrix[T-1, :T])

    def backward_transfer(self) -> float:
        """Backward Transfer: 이후 학습이 이전 태스크에 미치는 영향.

        BWT < 0: 망각 발생
        BWT > 0: 이후 학습이 이전 태스크에 도움 (드묾)
        """
        T = self.num_tasks
        bwt = 0.0
        for i in range(T - 1):
            bwt += self.accuracy_matrix[T-1, i] - self.accuracy_matrix[i, i]
        return bwt / (T - 1)

    def forward_transfer(self, random_baselines: np.ndarray) -> float:
        """Forward Transfer: 이전 학습이 새 태스크 초기 성능에 미치는 영향.

        Args:
            random_baselines: 각 태스크의 랜덤 초기화 성능
        """
        T = self.num_tasks
        fwt = 0.0
        for i in range(1, T):
            fwt += self.accuracy_matrix[i-1, i] - random_baselines[i]
        return fwt / (T - 1)

    def forgetting(self) -> float:
        """Average Forgetting: 각 태스크의 최대 성능 대비 최종 성능 하락."""
        T = self.num_tasks
        forget = 0.0
        for i in range(T - 1):
            max_acc = np.max(self.accuracy_matrix[:T, i])
            forget += max_acc - self.accuracy_matrix[T-1, i]
        return forget / (T - 1)

    def print_summary(self):
        """전체 평가 결과 출력."""
        print("=" * 50)
        print("Continual Learning Evaluation Summary")
        print("=" * 50)
        print(f"Average Accuracy:    {self.average_accuracy():.4f}")
        print(f"Backward Transfer:   {self.backward_transfer():.4f}")
        print(f"Average Forgetting:  {self.forgetting():.4f}")
        print("-" * 50)
        print("Accuracy Matrix (row=trained_up_to, col=task):")
        print(np.round(self.accuracy_matrix, 3))

최신 동향 (2024-2025)

LLM과 Continual Learning

주제 설명
Continual Pre-training 기존 LLM에 새 도메인 지식 추가 (의료, 법률 등)
Continual Instruction Tuning 새로운 instruction 유형 순차 학습
Continual RLHF 사용자 피드백 기반 지속적 정렬
Prompt-based CL L2P, DualPrompt 등 프롬프트 풀 활용

주요 연구 방향

방향 대표 연구
Online CL 데이터를 한 번만 볼 수 있는 환경 (single-pass)
Task-Free CL 태스크 경계 없이 연속 학습
Multimodal CL 비전-언어 모델의 연속 학습
Federated CL 분산 환경에서의 연속 학습
CL for Foundation Models LoRA, Adapter 기반 효율적 CL

실무 적용 가이드

어떤 CL 방법을 선택할 것인가

상황 추천 방법 이유
메모리 제약 심한 환경 EWC / SI 데이터 저장 불필요
범용 CL (Class-IL) DER++ / ER-ACE 가장 강력한 성능
프라이버시 중요 Generative Replay / LwF 원본 데이터 미저장
소수 태스크 + 자원 풍부 Progressive Nets 망각 완전 방지
LLM 연속 학습 LoRA + Replay 효율적 파라미터 관리
온라인 스트리밍 ER + MIR (Maximally Interfered Retrieval) 단일 패스 대응

라이브러리

라이브러리 설명 링크
Avalanche CL 전용 프레임워크 (ContinualAI) https://github.com/ContinualAI/avalanche
Mammoth DER++ 저자들의 CL 벤치마크 https://github.com/aimagelab/mammoth
continuum 다양한 CL 시나리오 데이터 로더 https://github.com/Continvvm/continuum

참고 자료

자료 링크
EWC 논문 https://arxiv.org/abs/1612.00796
Progressive Nets 논문 https://arxiv.org/abs/1606.04671
GEM 논문 https://arxiv.org/abs/1706.08840
DER++ 논문 https://arxiv.org/abs/2004.07211
PackNet 논문 https://arxiv.org/abs/1711.05769
SI 논문 https://arxiv.org/abs/1703.04200
LwF 논문 https://arxiv.org/abs/1606.09282
CL Survey (Wang et al., TPAMI 2024) https://arxiv.org/abs/2302.00487
Online CL Survey (2025) https://arxiv.org/abs/2501.04897
Avalanche 문서 https://avalanche.continualai.org/
ContinualAI Wiki https://wiki.continualai.org/