Key Findings
  • Catastrophic Forgetting[1] ist ein grundlegender Mangel des Deep Learning -- wenn ein Modell neue Aufgaben lernt, vergisst es abrupt das Wissen alter Aufgaben. Dies verhindert, dass KI wie der Mensch kontinuierlich lernen kann
  • Die drei grossen Strategiekategorien des Continual Learning: Regularisierungsmethoden (EWC[3], SI[4]) schuetzen wichtige Parameter, Architekturmethoden (Progressive Networks[6]) erweitern das Netzwerk dynamisch, Replay-Methoden (ER[9], GR[10]) wiederholen Beispiele alter Aufgaben
  • Auch grosse Sprachmodelle sind von Catastrophic Forgetting betroffen[15] -- kontinuierliches Fine-Tuning von BERT/GPT fuehrt zum schnellen Verlust der Faehigkeiten frueherer Aufgaben. Experience Replay ist derzeit die wirksamste Gegenmassnahme
  • Dieser Artikel enthaelt zwei Google Colab Praxisuebungen: EWC zur Vermeidung von Vergessen bei der MNIST-Bildklassifikation sowie BERT Continual Learning fuer Multi-Task-Textklassifikation -- beides direkt im Browser ausfuehrbar

1. Warum KI vergisst: Das Wesen des Catastrophic Forgetting

Menschen koennen kontinuierlich neue Faehigkeiten erlernen, ohne altes Wissen zu vergessen -- nachdem Sie Fahrradfahren gelernt haben, fuehrt das Erlernen von Schwimmen nicht dazu, dass Sie das Radfahren vergessen. Tiefe neuronale Netze stehen jedoch vor einem fundamentalen Problem: Catastrophic Forgetting (Katastrophales Vergessen)[1][2].

Wenn ein Netzwerk, das bereits auf Aufgabe A trainiert wurde, anschliessend auf Aufgabe B trainiert wird, sinkt die Leistung bei Aufgabe A dramatisch -- nicht allmaehlich, sondern katastrophal. Der Grund: Gradient Descent aktualisiert unterschiedslos alle Parameter, um die aktuelle Aufgabe zu optimieren, und ueberschreibt dabei die fuer die alte Aufgabe entscheidenden Gewichte.

Das Wesen des Catastrophic Forgetting:

Aufgabe A Training abgeschlossen: θ* = argmin_θ L_A(θ)
                     → Parameter konvergieren zum Optimum von A

Aufgabe B anschliessendes Training: θ** = argmin_θ L_B(θ), ausgehend von θ*
                     → Parameter entfernen sich vom Optimum von A, Leistung bei A bricht ein

Grundursache: Stabilitaets-Plastizitaets-Dilemma (Stability-Plasticity Dilemma)
  - Zu stabil → kann neue Aufgaben nicht lernen (Underfitting)
  - Zu plastisch → vergisst alte Aufgaben (Catastrophic Forgetting)
  - Ziel: Eine Balance zwischen beiden finden

Konkrete Auswirkung (am Beispiel Bildklassifikation):
  Schritt 1: Training auf Ziffern 0-4 → Genauigkeit 98%
  Schritt 2: Training auf Ziffern 5-9 → Genauigkeit 5-9: 97%, aber 0-4 sinkt auf ~20%
  Ursache: Die Gradienten-Updates fuer 5-9 zerstoeren die Schluesselgewichte zur Unterscheidung von 0-4

Catastrophic Forgetting tritt nicht nur bei der Bildklassifikation auf. Es ist ebenso gravierend beim kontinuierlichen Fine-Tuning von Sprachmodellen[15]: Ein auf Sentiment-Klassifikation feinabgestimmtes BERT, das anschliessend auf Named Entity Recognition feinabgestimmt wird, verliert erheblich an Sentiment-Klassifikationsfaehigkeit. Dieses Problem wird in der Aera grosser Sprachmodelle noch kritischer -- wir moechten, dass Modelle kontinuierlich aus neuen Daten lernen, anstatt jedes Mal von Grund auf neu zu trainieren.

2. Gesamtueberblick Continual Learning: Die drei grossen Strategiekategorien

Das Forschungsziel des Continual Learning (Lebenslanges Lernen) ist es, Modelle zu befaehigen, beim sequenziellen Erlernen mehrerer Aufgaben sowohl neue Aufgaben gut zu erlernen als auch die Leistung bei alten Aufgaben aufrechtzuerhalten[12][13]. Je nach Loesungsansatz lassen sich drei Hauptkategorien unterscheiden[17]:

StrategiekategorieKernideeRepraesentative MethodenVorteileEinschraenkungen
RegularisierungsmethodenHinzufuegen eines Strafterms zur Verlustfunktion, der die Veraenderung wichtiger Parameter einschraenktEWC[3], SI[4], LwF[5]Keine Speicherung alter Daten noetig, fester SpeicherbedarfSchutzfaehigkeit nimmt mit steigender Aufgabenanzahl ab
ArchitekturmethodenVerschiedenen Aufgaben werden unterschiedliche Netzwerkstrukturen oder Subnetzwerke zugewiesenProgressive Nets[6], PackNet[7], HAT[8]Kein Vergessen (harte Isolation)Modellgroesse waechst mit der Aufgabenanzahl
Replay-MethodenSpeichern oder Generieren von Beispielen alter Aufgaben, die beim Erlernen neuer Aufgaben mittrainiert werdenER[9], GR[10], GEM[11], DER++[14]Einfach und effektiv, kombinierbar mit anderen MethodenZusaetzlicher Speicher fuer alte Beispiele erforderlich

Es gibt ausserdem drei Bewertungsszenarien fuer Continual Learning:

Die drei Szenarien des Continual Learning:

1. Task-Incremental Learning (Task-IL):
   Die aktuelle Aufgabe ist zur Inferenzzeit bekannt → am einfachsten
   Beispiel: "Dies sind Daten von Aufgabe B, verwende den Klassifikationskopf von B"

2. Domain-Incremental Learning (Domain-IL):
   Die Aufgabenstruktur bleibt gleich, aber die Datenverteilung aendert sich → mittlere Schwierigkeit
   Beispiel: Dieselbe 10-Klassen-Klassifikation, aber der Bildstil wechselt von Skizze zu Foto

3. Class-Incremental Learning (Class-IL):
   Die Aufgabenidentitaet ist zur Inferenzzeit unbekannt, es muss zwischen allen gelernten Klassen unterschieden werden → am schwierigsten
   Beispiel: Zuerst 0-4 lernen, dann 5-9, beim Test alle Ziffern 0-9 unterscheiden

Schwierigkeitsranking: Task-IL < Domain-IL < Class-IL
In der Praxis kommt Class-IL den realen Anforderungen am naechsten

3. Regularisierungsmethoden: EWC und Wissensdestillation

Elastic Weight Consolidation (EWC)

EWC[3] ist die einflussreichste Regularisierungsmethode im Continual Learning, inspiriert von der synaptischen Konsolidierung in den Neurowissenschaften -- wichtige synaptische Verbindungen sollten geschuetzt werden, weniger wichtige koennen frei aktualisiert werden.

Die zentrale Frage lautet: Wie misst man die "Wichtigkeit" jedes Parameters fuer die alte Aufgabe? Die Antwort von EWC ist die Fisher-Informationsmatrix:

EWC-Verlustfunktion:

L_total(θ) = L_B(θ) + (λ/2) Σ_i F_i (θ_i - θ*_A,i)²

Dabei gilt:
  L_B(θ):     Verlust der neuen Aufgabe B
  θ*_A:       Optimale Parameter nach Abschluss des Trainings auf Aufgabe A
  F_i:        Diagonalelement der Fisher-Informationsmatrix (Wichtigkeit von Parameter i fuer Aufgabe A)
  λ:          Regularisierungsstaerke (steuert die Balance zwischen Stabilitaet und Plastizitaet)

Fisher-Informationsmatrix (Diagonalapproximation):
  F_i = E_{x~D_A} [(∂ log p(y|x,θ) / ∂θ_i)²]

Intuitive Erklaerung:
  F_i gross → Parameter i ist sehr wichtig fuer Aufgabe A → starke Einschraenkung seiner Veraenderung
  F_i klein → Parameter i ist unwichtig fuer Aufgabe A → kann frei aktualisiert werden, um Aufgabe B zu lernen

Geometrische Perspektive:
  Um das Optimum θ*_A von Aufgabe A existiert ein "Tal niedriger Verluste"
  Die Fisher-Matrix beschreibt die Form dieses Tals
  EWC lenkt die Optimierung von Aufgabe B entlang der Ausdehnungsrichtung des Tals
  → Findet Parameter, die gleichzeitig fuer A und B geeignet sind

Synaptic Intelligence (SI)

SI[4] ist die Online-Alternative zu EWC. Waehrend EWC nach jeder Aufgabe die Fisher-Matrix berechnen muss, akkumuliert SI die Wichtigkeit jedes Parameters waehrend des Trainings in Echtzeit -- es verfolgt den Beitrag des "zurueckgelegten Pfads" jedes Parameters zur Verlustreduzierung.

Learning without Forgetting (LwF)

LwF[5] geht einen anderen Weg -- es schuetzt nicht die Parameter, sondern die Ausgaben. Vor dem Erlernen einer neuen Aufgabe werden zunaechst "Soft Labels" erzeugt, indem die Daten der neuen Aufgabe durch das alte Modell gefuehrt werden. Beim Erlernen der neuen Aufgabe wird dann gleichzeitig ein Wissensdestillations-Verlust verwendet, um die Ausgabeverteilung der alten Aufgabe beizubehalten. Der groesste Vorteil: Es muessen keinerlei Daten der alten Aufgabe gespeichert werden.

4. Architekturmethoden: Progressive Networks und dynamische Erweiterung

Die Philosophie der Architekturmethoden lautet: Anstatt im begrenzten Parameterraum muehsam alte und neue Aufgaben auszubalancieren, wird jeder Aufgabe dedizierte Netzwerkkapazitaet zugewiesen.

Progressive Neural Networks

Die von Rusu et al.[6] vorgeschlagenen Progressive Networks sind der direkteste Ansatz -- bei jeder neuen Aufgabe "waechst" eine neue Netzwerkspalte (Column) daneben, und durch laterale Verbindungen (Lateral Connections) kann die neue Aufgabe die von alten Aufgaben gelernten Merkmale wiederverwenden:

Progressive Neural Networks:

Aufgabe 1:  [Column 1] ← normales Training
Aufgabe 2:  [Column 1] (eingefroren) ←─ laterale Verbindung ──→ [Column 2] ← nur diese wird trainiert
Aufgabe 3:  [Column 1] (eingefroren) ←─┐                [Column 2] (eingefroren) ←─┐
                              └─ laterale Verbindung ──→                      └─→ [Column 3]

Vorteil: Absolut null Vergessen (alte Spalten sind eingefroren)
Nachteil: Parameterzahl waechst linear (T Aufgaben = T-fache Parameter)

PackNet und HAT

PackNet[7] und HAT[8] versuchen, Multitasking in einem Netzwerk fester Groesse zu realisieren:

MethodeStrategieMechanismusBesonderheit
PackNet[7]Iteratives PruningTraining → unwichtige Gewichte entfernen → Kapazitaet fuer naechste Aufgabe freigebenJede Aufgabe hat ein dediziertes duennes Subnetzwerk
HAT[8]Harte AufmerksamkeitsmaskenLernen einer binaeren Maske fuer jede Aufgabe zum Schutz belegter NeuronenMasken sind gradientenoptimierbar, automatische Kapazitaetszuweisung

5. Experience-Replay-Methoden: Speicherpuffer und Generative Replay

Experience-Replay-Methoden (Erfahrungswiedergabe) sind inspiriert von der Gedaechtniskonsolidierung in der Kognitionswissenschaft -- Menschen "spielen" im Schlaf Erlebnisse des Tages ab, um Erinnerungen zu festigen. Beim Continual Learning mischen Replay-Methoden beim Erlernen neuer Aufgaben Beispiele alter Aufgaben bei[9].

Experience Replay (ER)

Der direkteste Ansatz: Es wird ein Speicherpuffer (Memory Buffer) fester Groesse gepflegt, der eine kleine Anzahl repraesentativer Beispiele jeder alten Aufgabe speichert. Beim Erlernen einer neuen Aufgabe enthaelt jede Mini-Batch eine Mischung aus neuen Aufgabendaten und aus dem Puffer abgerufenen alten Daten:

Experience-Replay-Ablauf:

Speicherpuffer M (feste Groesse, z.B. 200 Beispiele)

Aufgabe t lernen:
  for each mini-batch:
    batch_new = sample(D_t)           # Daten der neuen Aufgabe
    batch_old = sample(M)             # Alte Daten aus dem Puffer abrufen
    loss = L(batch_new) + L(batch_old)  # Gemeinsamer Verlust
    θ aktualisieren

  Nach Abschluss der Aufgabe:
    Repraesentative Beispiele von D_t dem Puffer M hinzufuegen (mit Reservoir Sampling oder Herding)

Reservoir Sampling:
  Das n-te Beispiel wird mit Wahrscheinlichkeit |M| / n in den Puffer aufgenommen,
  sodass jedes bisher gesehene Beispiel die gleiche Auswahlwahrscheinlichkeit hat

Zentrale Erkenntnis (Rolnick et al., 2019):
  Bereits 1-5 Beispiele pro Klasse reduzieren das Vergessen erheblich
  → Minimaler Speicheraufwand erzielt bereits signifikante Schutzwirkung gegen Vergessen

Generative Replay (GR)

Shin et al.[10] schlugen eine elegante Alternative vor: Anstatt echte alte Daten zu speichern, wird ein generatives Modell (z.B. GAN oder VAE) trainiert, um virtuelle Beispiele alter Aufgaben zu erzeugen. Dies ist besonders wertvoll in datenschutzsensiblen Szenarien -- medizinische Daten duerfen nicht gespeichert werden, aber ein generatives Modell kann ihre Verteilung rekonstruieren.

GEM und DER++

GEM[11] (Gradient Episodic Memory) verwendet Beispiele aus dem Speicher zur Berechnung von Gradientenbeschraenkungen: Die Gradienten-Updates der neuen Aufgabe duerfen den Verlust der alten Aufgabe bei den gespeicherten Beispielen nicht erhoehen. DER++[14] kombiniert Experience Replay mit Wissensdestillation -- es werden nicht nur die Labels alter Daten abgespielt, sondern auch die Soft-Outputs (Logits) des alten Modells, um reichhaltigere Informationen in Form von "dunklem Wissen" zu bewahren.

6. Continual Learning fuer Text-KI: Kontinuierliches Fine-Tuning von Sprachmodellen

Continual Learning fuer grosse Sprachmodelle ist ein aktuelles Forschungsfrontgebiet[16]. Wenn Unternehmen BERT oder GPT kontinuierlich an neue Aufgaben oder Domaenen anpassen moechten, kann Catastrophic Forgetting die bestehenden Faehigkeiten erheblich beeintraechtigen:

Continual-Learning-Szenarien fuer Sprachmodelle:

1. Kontinuierliches Task-Fine-Tuning (Continual Task Fine-tuning):
   BERT → Sentiment-Analyse → NER → QA → Textzusammenfassung
   Problem: Spaeteres Fine-Tuning zerstoert die Faehigkeiten frueherer Aufgaben

2. Kontinuierliche Domaenenanpassung (Continual Domain Adaptation):
   Allgemeines LLM → Finanzdomaene → Rechtsdomaene → Medizindomaene
   Problem: Wissen der neuen Domaene ueberschreibt das Fachwissen der alten Domaene

3. Kontinuierliches Pre-Training (Continual Pre-training):
   Basismodell → kontinuierliche Aufnahme neuer Dokumente/neuen Wissens
   Problem: Neues Wissen kann die grundlegenden Sprachverstaendnisfaehigkeiten beschaedigen

Besondere Herausforderungen beim Vergessen von Sprachmodellen:
  - Extrem hoher Grad an Parameterfreigabe (alle Aufgaben teilen denselben Transformer)
  - Schwerwiegendere Interferenz im Repraesentationsraum (viele semantische Ueberlappungen)
  - Aufgabenkoepfe koennen separiert werden, aber die zugrunde liegenden Repraesentationen sind schwer zu isolieren

Die Forschung von Scialom et al.[15] zeigt, dass Experience Replay derzeit die wirksamste Methode fuer Continual Learning bei Sprachmodellen ist -- das Beimischen einer kleinen Anzahl alter Aufgabenbeispiele beim Erlernen neuer Aufgaben reduziert das Vergessen erheblich. Dies ist effektiver als Regularisierungsmethoden wie EWC im NLP-Bereich, da die Parameterverteilung der Wichtigkeit bei Sprachaufgaben gleichmaessiger ist und die Unterscheidungskraft der Regularisierungsbeschraenkungen begrenzt ist.

7. Hands-on Lab 1: EWC zur Vermeidung von Vergessen bei der MNIST-Bildklassifikation (Google Colab)

Das folgende Experiment vergleicht auf Split MNIST drei Strategien: (1) Naives Fine-Tuning (Naive), (2) EWC-Regularisierung, (3) Experience Replay (ER) und veranschaulicht das Phaenomen des Catastrophic Forgetting sowie dessen Abschwaesung.

# ============================================================
# Lab 1: Continual Learning — EWC vs Experience Replay vs Naives Fine-Tuning (Split MNIST)
# Umgebung: Google Colab (CPU genuegt)
# ============================================================
# --- 0. Installation ---
!pip install -q torch torchvision matplotlib

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import numpy as np
import matplotlib.pyplot as plt
import copy

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Device: {device}")

# --- 1. Datenvorbereitung: Split MNIST ---
# Aufgabe A: Ziffern 0-4, Aufgabe B: Ziffern 5-9
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])

train_data = torchvision.datasets.MNIST('./data', train=True, download=True, transform=transform)
test_data = torchvision.datasets.MNIST('./data', train=False, transform=transform)

def filter_by_labels(dataset, labels):
    """Daten nach bestimmten Labels filtern"""
    mask = torch.zeros(len(dataset.targets), dtype=torch.bool)
    for l in labels:
        mask |= (dataset.targets == l)
    indices = mask.nonzero(as_tuple=True)[0]
    return torch.utils.data.Subset(dataset, indices)

task_a_labels = [0, 1, 2, 3, 4]
task_b_labels = [5, 6, 7, 8, 9]

train_a = filter_by_labels(train_data, task_a_labels)
train_b = filter_by_labels(train_data, task_b_labels)
test_a = filter_by_labels(test_data, task_a_labels)
test_b = filter_by_labels(test_data, task_b_labels)

loader_a = torch.utils.data.DataLoader(train_a, batch_size=128, shuffle=True)
loader_b = torch.utils.data.DataLoader(train_b, batch_size=128, shuffle=True)
test_loader_a = torch.utils.data.DataLoader(test_a, batch_size=256)
test_loader_b = torch.utils.data.DataLoader(test_b, batch_size=256)

print(f"Task A (digits 0-4): {len(train_a)} train, {len(test_a)} test")
print(f"Task B (digits 5-9): {len(train_b)} train, {len(test_b)} test")

# --- 2. Einfaches CNN-Modell ---
class SimpleCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
        self.pool = nn.MaxPool2d(2)
        self.fc1 = nn.Linear(64 * 7 * 7, 256)
        self.fc2 = nn.Linear(256, 10)  # Alle 10 Klassen teilen sich die Ausgabe

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(x.size(0), -1)
        x = F.relu(self.fc1(x))
        return self.fc2(x)

def evaluate(model, loader):
    model.eval()
    correct, total = 0, 0
    with torch.no_grad():
        for x, y in loader:
            x, y = x.to(device), y.to(device)
            pred = model(x).argmax(dim=1)
            correct += (pred == y).sum().item()
            total += y.size(0)
    return correct / total

# --- 3. EWC-Implementierung ---
class EWC:
    def __init__(self, model, dataloader, device, n_samples=200):
        self.params = {n: p.clone().detach() for n, p in model.named_parameters() if p.requires_grad}
        self.fisher = self._compute_fisher(model, dataloader, device, n_samples)

    def _compute_fisher(self, model, dataloader, device, n_samples):
        """Fisher-Informationsmatrix berechnen (Diagonalapproximation)"""
        fisher = {n: torch.zeros_like(p) for n, p in model.named_parameters() if p.requires_grad}
        model.eval()
        count = 0
        for x, y in dataloader:
            if count >= n_samples:
                break
            x, y = x.to(device), y.to(device)
            model.zero_grad()
            output = model(x)
            loss = F.cross_entropy(output, y)
            loss.backward()
            for n, p in model.named_parameters():
                if p.requires_grad and p.grad is not None:
                    fisher[n] += p.grad.data.pow(2) * x.size(0)
            count += x.size(0)
        fisher = {n: f / count for n, f in fisher.items()}
        return fisher

    def penalty(self, model):
        """EWC-Regularisierungsterm"""
        loss = 0
        for n, p in model.named_parameters():
            if p.requires_grad and n in self.fisher:
                loss += (self.fisher[n] * (p - self.params[n]).pow(2)).sum()
        return loss

# --- 4. Experience-Replay-Speicherpuffer ---
class ReplayBuffer:
    def __init__(self, capacity=200):
        self.capacity = capacity
        self.buffer_x = []
        self.buffer_y = []

    def add_from_loader(self, loader, n_samples):
        """Zufaellige Stichproben aus dem Loader in den Puffer aufnehmen"""
        all_x, all_y = [], []
        for x, y in loader:
            all_x.append(x)
            all_y.append(y)
        all_x = torch.cat(all_x)
        all_y = torch.cat(all_y)
        indices = torch.randperm(len(all_x))[:n_samples]
        self.buffer_x.append(all_x[indices])
        self.buffer_y.append(all_y[indices])

    def sample(self, batch_size):
        all_x = torch.cat(self.buffer_x)
        all_y = torch.cat(self.buffer_y)
        indices = torch.randperm(len(all_x))[:batch_size]
        return all_x[indices], all_y[indices]

# --- 5. Trainingsfunktion ---
def train_task(model, loader, optimizer, epochs, ewc=None, ewc_lambda=0,
               replay_buffer=None, replay_batch=32):
    model.train()
    for epoch in range(epochs):
        total_loss = 0
        for x, y in loader:
            x, y = x.to(device), y.to(device)
            optimizer.zero_grad()

            output = model(x)
            loss = F.cross_entropy(output, y)

            # EWC-Regularisierung
            if ewc is not None:
                loss += ewc_lambda * ewc.penalty(model)

            # Experience Replay
            if replay_buffer is not None and len(replay_buffer.buffer_x) > 0:
                rx, ry = replay_buffer.sample(replay_batch)
                rx, ry = rx.to(device), ry.to(device)
                r_output = model(rx)
                loss += F.cross_entropy(r_output, ry)

            loss.backward()
            optimizer.step()
            total_loss += loss.item()

# --- 6. Experiment: Vergleich der drei Strategien ---
n_epochs = 5
results = {}

# Strategie 1: Naives Fine-Tuning (Naive)
print("\n=== Strategy 1: Naive Fine-tuning ===")
model_naive = SimpleCNN().to(device)
opt = torch.optim.Adam(model_naive.parameters(), lr=1e-3)

train_task(model_naive, loader_a, opt, n_epochs)
acc_a_after_a = evaluate(model_naive, test_loader_a)
print(f"After Task A: Acc_A={acc_a_after_a:.4f}")

train_task(model_naive, loader_b, opt, n_epochs)
acc_a_after_b = evaluate(model_naive, test_loader_a)
acc_b_after_b = evaluate(model_naive, test_loader_b)
print(f"After Task B: Acc_A={acc_a_after_b:.4f}, Acc_B={acc_b_after_b:.4f}")
print(f"Forgetting: {acc_a_after_a - acc_a_after_b:.4f}")
results['Naive'] = (acc_a_after_a, acc_a_after_b, acc_b_after_b)

# Strategie 2: EWC
print("\n=== Strategy 2: EWC (λ=400) ===")
model_ewc = SimpleCNN().to(device)
opt = torch.optim.Adam(model_ewc.parameters(), lr=1e-3)

train_task(model_ewc, loader_a, opt, n_epochs)
acc_a_after_a = evaluate(model_ewc, test_loader_a)
print(f"After Task A: Acc_A={acc_a_after_a:.4f}")

# Fisher-Matrix berechnen
ewc = EWC(model_ewc, loader_a, device)

train_task(model_ewc, loader_b, opt, n_epochs, ewc=ewc, ewc_lambda=400)
acc_a_after_b = evaluate(model_ewc, test_loader_a)
acc_b_after_b = evaluate(model_ewc, test_loader_b)
print(f"After Task B: Acc_A={acc_a_after_b:.4f}, Acc_B={acc_b_after_b:.4f}")
print(f"Forgetting: {acc_a_after_a - acc_a_after_b:.4f}")
results['EWC'] = (acc_a_after_a, acc_a_after_b, acc_b_after_b)

# Strategie 3: Experience Replay
print("\n=== Strategy 3: Experience Replay (buffer=200) ===")
model_er = SimpleCNN().to(device)
opt = torch.optim.Adam(model_er.parameters(), lr=1e-3)
buffer = ReplayBuffer(capacity=200)

train_task(model_er, loader_a, opt, n_epochs)
acc_a_after_a = evaluate(model_er, test_loader_a)
print(f"After Task A: Acc_A={acc_a_after_a:.4f}")

# Beispiele von Aufgabe A in den Puffer aufnehmen
buffer.add_from_loader(loader_a, n_samples=200)

train_task(model_er, loader_b, opt, n_epochs, replay_buffer=buffer)
acc_a_after_b = evaluate(model_er, test_loader_a)
acc_b_after_b = evaluate(model_er, test_loader_b)
print(f"After Task B: Acc_A={acc_a_after_b:.4f}, Acc_B={acc_b_after_b:.4f}")
print(f"Forgetting: {acc_a_after_a - acc_a_after_b:.4f}")
results['Replay'] = (acc_a_after_a, acc_a_after_b, acc_b_after_b)

# --- 7. Visualisierung der Ergebnisse ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Linkes Diagramm: Genauigkeit bei Aufgabe A (Grad des Vergessens)
strategies = list(results.keys())
acc_before = [results[s][0] for s in strategies]
acc_after = [results[s][1] for s in strategies]

x_pos = np.arange(len(strategies))
width = 0.35
bars1 = axes[0].bar(x_pos - width/2, acc_before, width, label='After Task A', color='#0077b6')
bars2 = axes[0].bar(x_pos + width/2, acc_after, width, label='After Task B', color='#e63946')

axes[0].set_ylabel('Task A Accuracy')
axes[0].set_title('Catastrophic Forgetting: Task A Performance', fontsize=13)
axes[0].set_xticks(x_pos)
axes[0].set_xticklabels(strategies)
axes[0].legend()
axes[0].set_ylim(0, 1.05)
axes[0].grid(True, alpha=0.3, axis='y')

for bar in bars1:
    axes[0].text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01,
                 f'{bar.get_height():.2f}', ha='center', va='bottom', fontsize=10)
for bar in bars2:
    axes[0].text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01,
                 f'{bar.get_height():.2f}', ha='center', va='bottom', fontsize=10)

# Rechtes Diagramm: Vergessensgrad
forgetting = [results[s][0] - results[s][1] for s in strategies]
colors = ['#e63946' if f > 0.3 else '#b8922e' if f > 0.1 else '#2a9d8f' for f in forgetting]
bars = axes[1].bar(strategies, forgetting, color=colors, edgecolor='white', linewidth=1.5)

axes[1].set_ylabel('Forgetting (↓ better)')
axes[1].set_title('Amount of Forgetting on Task A', fontsize=13)
axes[1].grid(True, alpha=0.3, axis='y')

for bar, f in zip(bars, forgetting):
    axes[1].text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01,
                 f'{f:.3f}', ha='center', va='bottom', fontsize=12, fontweight='bold')

plt.tight_layout()
plt.show()

# --- 8. Genauigkeitsanalyse pro Klasse ---
print("\n=== Per-class Accuracy After Task B ===")
print(f"{'Class':<8} {'Naive':>8} {'EWC':>8} {'Replay':>8}")
print("-" * 36)

models = {'Naive': model_naive, 'EWC': model_ewc, 'Replay': model_er}
for digit in range(10):
    test_digit = filter_by_labels(test_data, [digit])
    loader_digit = torch.utils.data.DataLoader(test_digit, batch_size=256)
    accs = []
    for name in ['Naive', 'EWC', 'Replay']:
        acc = evaluate(models[name], loader_digit)
        accs.append(acc)
    marker = " ← Task A" if digit < 5 else ""
    print(f"  {digit:<6} {accs[0]:>8.1%} {accs[1]:>8.1%} {accs[2]:>8.1%}{marker}")

print("\nLab 1 Complete!")

8. Hands-on Lab 2: BERT Continual Learning fuer Multi-Task-Textklassifikation (Google Colab)

Das folgende Experiment zeigt Catastrophic Forgetting bei Sprachmodellen: BERT wird nacheinander auf zwei Textklassifikationsaufgaben feinabgestimmt, das Vergessen wird beobachtet und mittels Experience Replay abgeschwaeacht.

# ============================================================
# Lab 2: BERT Continual Learning — Catastrophic Forgetting und Abschwaeachung bei Multi-Task-Textklassifikation
# Umgebung: Google Colab (GPU empfohlen, CPU moeglich aber langsamer)
# ============================================================
# --- 0. Installation ---
!pip install -q transformers datasets torch

import torch
import torch.nn.functional as F
from transformers import BertTokenizer, BertForSequenceClassification, AdamW
from datasets import load_dataset
import numpy as np
import matplotlib.pyplot as plt
import random

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Device: {device}")

# --- 1. Zwei Textklassifikationsaufgaben laden ---
print("\n--- Loading Datasets ---")

# Aufgabe A: SST-2 Sentiment-Klassifikation (positiv/negativ)
sst2 = load_dataset("glue", "sst2")
print(f"Task A (SST-2): {len(sst2['train'])} train, {len(sst2['validation'])} val")

# Aufgabe B: MRPC semantische Aequivalenzbeurteilung (aequivalent/nicht aequivalent)
mrpc = load_dataset("glue", "mrpc")
print(f"Task B (MRPC):  {len(mrpc['train'])} train, {len(mrpc['validation'])} val")

# --- 2. Tokenizer ---
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

def tokenize_sst2(examples):
    return tokenizer(examples['sentence'], truncation=True, padding='max_length', max_length=64)

def tokenize_mrpc(examples):
    return tokenizer(examples['sentence1'], examples['sentence2'],
                     truncation=True, padding='max_length', max_length=128)

sst2_tok = sst2.map(tokenize_sst2, batched=True)
mrpc_tok = mrpc.map(tokenize_mrpc, batched=True)

for ds in [sst2_tok, mrpc_tok]:
    ds.set_format("torch", columns=["input_ids", "attention_mask", "label"])

# Teilmengen verwenden (Colab-freundlich)
sst2_train = sst2_tok["train"].shuffle(seed=42).select(range(1000))
sst2_val = sst2_tok["validation"]
mrpc_train = mrpc_tok["train"].shuffle(seed=42).select(range(800))
mrpc_val = mrpc_tok["validation"]

# --- 3. Trainings- und Evaluierungswerkzeuge ---
def make_loader(dataset, batch_size=16, shuffle=True):
    return torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)

def evaluate_task(model, loader):
    model.eval()
    correct, total = 0, 0
    with torch.no_grad():
        for batch in loader:
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['label'].to(device)
            outputs = model(input_ids=input_ids, attention_mask=attention_mask)
            preds = outputs.logits.argmax(dim=-1)
            correct += (preds == labels).sum().item()
            total += labels.size(0)
    return correct / total

def train_epoch(model, loader, optimizer, replay_data=None, replay_ratio=0.3):
    model.train()
    total_loss = 0
    for batch in loader:
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        labels = batch['label'].to(device)

        outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=labels)
        loss = outputs.loss

        # Experience Replay
        if replay_data is not None and len(replay_data) > 0:
            n_replay = max(1, int(input_ids.size(0) * replay_ratio))
            indices = random.sample(range(len(replay_data)), min(n_replay, len(replay_data)))

            r_ids = torch.stack([replay_data[i]['input_ids'] for i in indices]).to(device)
            r_mask = torch.stack([replay_data[i]['attention_mask'] for i in indices]).to(device)
            r_labels = torch.tensor([replay_data[i]['label'] for i in indices]).to(device)

            r_outputs = model(input_ids=r_ids, attention_mask=r_mask, labels=r_labels)
            loss = loss + r_outputs.loss

        optimizer.zero_grad()
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        optimizer.step()
        total_loss += loss.item()

    return total_loss / len(loader)

# --- 4. Replay-Speicher erstellen ---
def create_replay_buffer(dataset, n_samples=100):
    """Replay-Puffer durch Stichprobenziehung aus dem Datensatz erstellen"""
    indices = random.sample(range(len(dataset)), min(n_samples, len(dataset)))
    buffer = []
    for i in indices:
        item = dataset[i]
        buffer.append({
            'input_ids': item['input_ids'],
            'attention_mask': item['attention_mask'],
            'label': item['label'].item() if isinstance(item['label'], torch.Tensor) else item['label']
        })
    return buffer

# --- 5. Experiment 1: Naives sequenzielles Fine-Tuning (zeigt Vergessen) ---
print("\n" + "="*60)
print("Experiment 1: Naive Sequential Fine-tuning")
print("="*60)

model_naive = BertForSequenceClassification.from_pretrained(
    'bert-base-uncased', num_labels=2
).to(device)
opt = AdamW(model_naive.parameters(), lr=2e-5, weight_decay=0.01)

# Training auf Aufgabe A (SST-2)
print("\n--- Training on Task A (SST-2) ---")
loader_a = make_loader(sst2_train)
val_loader_a = make_loader(sst2_val, shuffle=False)

naive_history = {'task_a_on_a': [], 'task_a_on_b': [], 'task_b_on_b': []}

for epoch in range(3):
    loss = train_epoch(model_naive, loader_a, opt)
    acc = evaluate_task(model_naive, val_loader_a)
    naive_history['task_a_on_a'].append(acc)
    print(f"  Epoch {epoch+1}: loss={loss:.4f}, SST-2 acc={acc:.4f}")

acc_a_before = naive_history['task_a_on_a'][-1]

# Training auf Aufgabe B (MRPC)
print("\n--- Training on Task B (MRPC) ---")
loader_b = make_loader(mrpc_train)
val_loader_b = make_loader(mrpc_val, shuffle=False)

for epoch in range(3):
    loss = train_epoch(model_naive, loader_b, opt)
    acc_a = evaluate_task(model_naive, val_loader_a)
    acc_b = evaluate_task(model_naive, val_loader_b)
    naive_history['task_a_on_b'].append(acc_a)
    naive_history['task_b_on_b'].append(acc_b)
    print(f"  Epoch {epoch+1}: loss={loss:.4f}, SST-2 acc={acc_a:.4f}, MRPC acc={acc_b:.4f}")

acc_a_after_naive = naive_history['task_a_on_b'][-1]
acc_b_naive = naive_history['task_b_on_b'][-1]

# --- 6. Experiment 2: Experience Replay ---
print("\n" + "="*60)
print("Experiment 2: Experience Replay (buffer=100)")
print("="*60)

model_replay = BertForSequenceClassification.from_pretrained(
    'bert-base-uncased', num_labels=2
).to(device)
opt = AdamW(model_replay.parameters(), lr=2e-5, weight_decay=0.01)

# Training auf Aufgabe A
print("\n--- Training on Task A (SST-2) ---")
replay_history = {'task_a_on_a': [], 'task_a_on_b': [], 'task_b_on_b': []}

for epoch in range(3):
    loss = train_epoch(model_replay, loader_a, opt)
    acc = evaluate_task(model_replay, val_loader_a)
    replay_history['task_a_on_a'].append(acc)
    print(f"  Epoch {epoch+1}: loss={loss:.4f}, SST-2 acc={acc:.4f}")

# Replay-Puffer erstellen
replay_buffer = create_replay_buffer(sst2_train, n_samples=100)
print(f"\nReplay buffer: {len(replay_buffer)} samples from Task A")

# Training auf Aufgabe B + Replay
print("\n--- Training on Task B (MRPC) with Replay ---")
for epoch in range(3):
    loss = train_epoch(model_replay, loader_b, opt, replay_data=replay_buffer)
    acc_a = evaluate_task(model_replay, val_loader_a)
    acc_b = evaluate_task(model_replay, val_loader_b)
    replay_history['task_a_on_b'].append(acc_a)
    replay_history['task_b_on_b'].append(acc_b)
    print(f"  Epoch {epoch+1}: loss={loss:.4f}, SST-2 acc={acc_a:.4f}, MRPC acc={acc_b:.4f}")

acc_a_after_replay = replay_history['task_a_on_b'][-1]
acc_b_replay = replay_history['task_b_on_b'][-1]

# --- 7. Vergleichende Visualisierung ---
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# Linkes Diagramm: Genauigkeit bei Aufgabe A im Trainingsverlauf
epochs_a = list(range(1, 4))
epochs_b = list(range(4, 7))
all_epochs = epochs_a + epochs_b

naive_a_curve = naive_history['task_a_on_a'] + naive_history['task_a_on_b']
replay_a_curve = replay_history['task_a_on_a'] + replay_history['task_a_on_b']

axes[0].plot(all_epochs, naive_a_curve, 'o-', color='#e63946', linewidth=2, label='Naive')
axes[0].plot(all_epochs, replay_a_curve, 's-', color='#0077b6', linewidth=2, label='Replay')
axes[0].axvline(x=3.5, color='gray', linestyle='--', alpha=0.5)
axes[0].text(2, 0.55, 'Task A\n(SST-2)', ha='center', fontsize=10, color='gray')
axes[0].text(5, 0.55, 'Task B\n(MRPC)', ha='center', fontsize=10, color='gray')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('SST-2 Accuracy')
axes[0].set_title('Task A (SST-2) Accuracy Over Time', fontsize=13)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[0].set_ylim(0.5, 1.0)

# Mittleres Diagramm: Endvergleich
categories = ['Task A\n(SST-2)', 'Task B\n(MRPC)']
naive_scores = [acc_a_after_naive, acc_b_naive]
replay_scores = [acc_a_after_replay, acc_b_replay]

x = np.arange(len(categories))
width = 0.35

axes[1].bar(x - width/2, naive_scores, width, label='Naive', color='#e63946', alpha=0.85)
axes[1].bar(x + width/2, replay_scores, width, label='Replay', color='#0077b6', alpha=0.85)
axes[1].set_ylabel('Accuracy')
axes[1].set_title('Final Performance Comparison', fontsize=13)
axes[1].set_xticks(x)
axes[1].set_xticklabels(categories)
axes[1].legend()
axes[1].set_ylim(0.5, 1.0)
axes[1].grid(True, alpha=0.3, axis='y')

# Rechtes Diagramm: Vergessensgrad
forgetting_naive = acc_a_before - acc_a_after_naive
forgetting_replay = replay_history['task_a_on_a'][-1] - acc_a_after_replay

bars = axes[2].bar(['Naive', 'Replay'], [forgetting_naive, forgetting_replay],
                    color=['#e63946', '#0077b6'], edgecolor='white', linewidth=1.5)
axes[2].set_ylabel('Forgetting (↓ better)')
axes[2].set_title('SST-2 Forgetting After MRPC Training', fontsize=13)
axes[2].grid(True, alpha=0.3, axis='y')

for bar, f in zip(bars, [forgetting_naive, forgetting_replay]):
    axes[2].text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.005,
                 f'{f:.3f}', ha='center', va='bottom', fontsize=13, fontweight='bold')

plt.tight_layout()
plt.show()

# --- 8. Inferenz-Demo ---
print("\n=== Inference Demo ===")
test_sentences = [
    ("This movie is absolutely wonderful!", "SST-2 (Positive)"),
    ("A terrible waste of time and money.", "SST-2 (Negative)"),
    ("The film was average, nothing special.", "SST-2 (Neutral-ish)"),
]

print("\n--- Naive Model ---")
model_naive.eval()
for text, label in test_sentences:
    inputs = tokenizer(text, return_tensors='pt', truncation=True, max_length=64).to(device)
    with torch.no_grad():
        logits = model_naive(**inputs).logits
    pred = "Positive" if logits.argmax().item() == 1 else "Negative"
    conf = torch.softmax(logits, dim=-1).max().item()
    print(f"  [{pred} {conf:.1%}] {text}  (expected: {label})")

print("\n--- Replay Model ---")
model_replay.eval()
for text, label in test_sentences:
    inputs = tokenizer(text, return_tensors='pt', truncation=True, max_length=64).to(device)
    with torch.no_grad():
        logits = model_replay(**inputs).logits
    pred = "Positive" if logits.argmax().item() == 1 else "Negative"
    conf = torch.softmax(logits, dim=-1).max().item()
    print(f"  [{pred} {conf:.1%}] {text}  (expected: {label})")

print("\nLab 2 Complete!")

9. Entscheidungsrahmen: Wie Unternehmen die richtige Continual-Learning-Strategie waehlen

Basierend auf Datenverfuegbarkeit, Datenschutzanforderungen und Rechenbudget koennen Unternehmen den folgenden Rahmen verwenden, um die passende Continual-Learning-Loesung auszuwaehlen:

BedingungEmpfohlene MethodeBegruendung
Alte Daten speicherbar, ausreichend SpeicherExperience Replay (ER / DER++)[9][14]Am einfachsten und effektivsten, 200 Beispiele pro Aufgabe reduzieren das Vergessen bereits erheblich
Datenschutzanforderungen, keine Speicherung alter Daten moeglichEWC[3] + LwF[5]Nur Fisher-Matrix oder Modell-Snapshot noetig, keine Originaldaten erforderlich
Viele Aufgaben mit stetigem WachstumPackNet[7] oder HAT[8]Unterstuetzen mehrere Aufgaben bei fester Modellkapazitaet, kein zusaetzlicher Speicher noetig
Wenige Aufgaben, aber Null-Vergessen erforderlichProgressive Networks[6]Vollstaendige Isolation, garantiert null Vergessen, geeignet fuer kritische Aufgabenszenarien
Kontinuierliches Fine-Tuning von SprachmodellenExperience Replay + Lernratenanpassung[15]Am wirksamsten fuer Transformer-Architekturen, EWC hat im NLP-Bereich begrenzte Wirkung
Datenschutzanforderungen + ausreichende RechenkapazitaetGenerative Replay (GR)[10]Generiert virtuelle alte Daten, vereint Datenschutz und Schutz vor Vergessen
Entscheidungsbaum:

1. Koennen echte Daten alter Aufgaben gespeichert werden?
   ├── Ja → Experience Replay (bevorzugt DER++)
   └── Nein → 2

2. Kann die Modellkapazitaet wachsen?
   ├── Ja → Progressive Networks (null Vergessen)
   └── Nein → 3

3. Ist das Rechenbudget ausreichend?
   ├── Ja → Generative Replay (GAN/VAE erzeugen virtuelle Daten)
   └── Nein → EWC + LwF (nur Fisher-Matrix + alter Modell-Snapshot noetig)

10. Fazit und Ausblick

Catastrophic Forgetting[1] ist eines der zentralen Hindernisse auf dem Weg des Deep Learning zur echten kuenstlichen Intelligenz. Ein System, das nicht kontinuierlich lernen kann -- egal wie leistungsfaehig es ist -- bleibt ein statisches Werkzeug und kein sich weiterentwickelndes intelligentes Wesen.

Rueckblick auf die Kernlinien:

Mit Blick auf die Zukunft bewegt sich Continual Learning von der akademischen Forschung hin zur ingenieurmaessigen Praxis. Mit der Verbreitung von Foundation Models[17] muessen Unternehmen ihre Modelle kontinuierlich an neue Daten, neue Aufgaben und neue Domaenen anpassen -- anstatt jedes Mal von Grund auf neu zu trainieren. Sparse-Dynamic-Computation-Modelle (MoE) weisen verschiedenen Aufgaben natuerlich verschiedene Experten-Subnetzwerke zu und bieten auf Architekturebene Potenzial fuer Continual Learning; und parametereffizientes Fine-Tuning (LoRA Fine-Tuning, Adapter) bietet durch das Einfrieren des Hauptnetzwerks und das alleinige Trainieren kleiner Module eine leichtgewichtige, aufgabenspezifische Anpassung -- was im Kern eine Continual-Learning-Strategie darstellt. Wenn KI-Systeme lernen, wie Menschen kontinuierlich zu lernen, ohne zu vergessen, sind wir der wahren allgemeinen Intelligenz einen Schritt naeher.