AI/ML Explained: Continual Learning & Catastrophic Forgetting

Day 52 · 2026-07-09
For: engineers with coding experience, non-AI background

Catastrophic Forgetting & Stability-PlasticityCatastrophic Forgetting

MechanismNeural NetsCore Tension
One-line analogy

A neural net's weights are a piece of globally shared mutable state. Learning a new task B means an in-place update—directly overwriting that state. No version isolation, no transactions: the weights task A relies on get silently rewritten by B's gradients, and A is catastrophically forgotten. This isn't "memory is full"—it's a write-write conflict with no isolation mechanism.

Problem it solves + how it works

Catastrophic forgetting means: when a network learns several tasks in sequence, learning the later ones sharply erases what it learned earlier. McCloskey & Cohen documented it back in 1989. The root cause: task A's knowledge is distributed across one specific setting of all the weights, while SGD on task B optimizes only B's loss—it neither knows nor cares which weights are A's "load-bearing walls"; it just pushes wherever lowers B's loss.

This gives the field's central tension—the stability-plasticity dilemma: weights too stable (can't change) → can't learn new tasks (no plasticity); weights too plastic (freely changed) → learning the new forgets the old. It's isomorphic to system trade-offs you know: a read-only replica is forever consistent but can't be written; a write-anywhere cache has high throughput but no consistency. Continual learning is about finding a compromise on this spectrum:

Stability ←──────────────────────→ Plasticity

freeze all· EWC reg.· replay· isolation· free fine-tune
remembers, won't learn                      learns fast, forgets all

The next 3 concepts are three mainstream remedies on this spectrum: constrain weights (regularization), rehearse old data (replay), give each task dedicated parameters (isolation).

Code
# "See" catastrophic forgetting: learn A, then B, then re-test A
import torch, torch.nn as nn
net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(), nn.Linear(64, 2))
opt = torch.optim.Adam(net.parameters(), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()

def make_task(seed):                       # two different binary tasks
    g = torch.Generator().manual_seed(seed)
    X = torch.randn(400, 20, generator=g)
    y = (X @ torch.randn(20, generator=g) > 0).long()
    return X, y

def train(X, y):
    for _ in range(200):
        opt.zero_grad(); loss_fn(net(X), y).backward(); opt.step()
def acc(X, y): return (net(X).argmax(1) == y).float().mean().item()

Xa, ya = make_task(1); Xb, yb = make_task(2)
train(Xa, ya); print("after A, acc on A:", acc(Xa, ya))   # ~0.95
train(Xb, yb); print("after B, acc on A:", acc(Xa, ya))   # drops to ~0.55 → forgot
Pitfall + where you'd use it
Pitfall: "Forgetting is just insufficient capacity—scale up the params." Wrong. Catastrophic forgetting happens in massively over-parameterized nets too: there's enough capacity to hold A and B at once, but SGD has no reason to seek the "both-satisfying" solution—it just follows the current task's gradient. The problem is in the objective, not the capacity.
📌 Super-individual scenario: you keep fine-tuning an open model on business data, and after the second batch it regresses on the first batch's task—that's catastrophic forgetting. Understanding it tells you whether to "mix the data" or "add a constraint," rather than blindly enlarging the model or tweaking lr.
Takeaway + question
💡 Catastrophic forgetting is fundamentally "shared mutable state + unisolated in-place writes"; stability-plasticity is the core trade-off it forces.
🤔 Biological brains learn new things while retaining old memories, almost never "catastrophically forgetting." What isolation mechanism pulls that off? (Hint: hippocampus vs. neocortex, a dual system.)

Elastic Weight Consolidation (EWC)Elastic Weight Consolidation

RegularizationFisher Info
One-line analogy

EWC puts a soft write-lock on each weight, with lock strength set by "how important this weight is to the old task." Like giving hot rows / load-bearing indexes in a database high-priority protection: not "no writes allowed," but "want to change these critical weights? Sure—but it'll cost you dearly." Changing irrelevant weights is nearly free; changing load-bearing ones incurs a steep penalty—learning gets elastically pulled back.

Problem it solves + how it works

Replay needs old data; isolation needs extra parameters. EWC (Kirkpatrick et al. 2017) aims for zero old data, zero new parameters—fighting forgetting purely by constraining weights. The key question: how do you know which weights are task A's "load-bearing walls"? The answer is Fisher information—it measures "if I nudge weight i slightly, how much does task A's output change." Big change = load-bearing = lock it tight; near-zero change = irrelevant = let it go.

When learning task B, EWC adds a quadratic penalty on top of B's loss:

L(θ) = L_B(θ) + Σ_i (λ/2) · F_i · (θ_i − θ*_A,i)²

Symbol by symbol: L_B(θ) is the new task B's loss (to be minimized); θ*_A,i is weight i's value after learning A (the "old-memory anchor"); F_i is weight i's Fisher information (importance); λ tunes "how much you care about not forgetting A." Intuition: hang a spring on each weight pulling it toward "its value under task A," θ*_A,i; the spring's stiffness is F_i·λ—load-bearing weights get very stiff springs (barely movable), irrelevant weights get soft springs (free for B to use). So B can only optimize in "directions that don't disturb A." This is an explicit knob on the stability-plasticity spectrum: bigger λ = more stable, smaller = more plastic.

Code
# EWC core: after A, estimate Fisher; when learning B, add quadratic penalty
def estimate_fisher(net, X, y, loss_fn):
    fisher = {n: torch.zeros_like(p) for n, p in net.named_parameters()}
    for i in range(len(X)):
        net.zero_grad()
        loss_fn(net(X[i:i+1]), y[i:i+1]).backward()
        for n, p in net.named_parameters():
            fisher[n] += p.grad.pow(2) / len(X)   # squared grad ≈ Fisher diagonal
    return fisher

# after A: star = weight snapshot (θ*_A), fisher = importance
star = {n: p.clone().detach() for n, p in net.named_parameters()}
fisher = estimate_fisher(net, Xa, ya, loss_fn)

def ewc_penalty(net, lam=1000.):     # add to total loss when learning B
    return lam * sum((fisher[n] * (p - star[n]).pow(2)).sum()
                     for n, p in net.named_parameters())
# total = loss_fn(net(Xb), yb) + ewc_penalty(net)  → A's accuracy preserved
Pitfall + where you'd use it
Pitfall: "EWC is the perfect, once-and-for-all fix." No. Its weakness is that penalties accumulate and Fisher is an approximation: as tasks pile up, a stack of quadratic constraints locks the weights ever tighter, plasticity dries up, and by task 10 it can barely move; also Fisher takes only the diagonal, ignoring inter-weight correlations, so the estimate is biased. Honest conclusion: EWC pays off when tasks are few and old data can't be stored; over long task sequences, replay is usually more robust.
📌 Decision-support scenario: think of λ as a "conservatism knob." When injecting new-domain knowledge while fearing loss of general ability, the EWC mindset (constrain the critical params from wandering) is more controllable than "blind full-parameter fine-tuning"—it's also the intuition behind "only touch some layers / add regularization" in a lot of LLM continued pre-training.
Takeaway + question
💡 EWC = hang a spring on each weight by "importance (Fisher)," pulling it back to the old task's value—a quadratic penalty buying zero old data, zero new params.
🤔 Fisher information approximates weight importance via "squared gradient." How does that resemble your intuition for "which DB table is hot, the one that crashes the whole site if touched"?

Experience & Generative ReplayExperience / Generative Replay

ReplayGenerative Model
One-line analogy

Replay is like a database's WAL (write-ahead log) replay: to keep old and new consistent, you re-run old transactions alongside. Experience replay stores a small set of old samples and feeds old+new mixed. Generative replay is cleverer—instead of storing raw old data, it trains a generator to synthesize "fake old data," effectively replacing a full backup with a compressed snapshot generator, balancing privacy and storage.

Problem it solves + how it works

Forgetting's root cause is "not seeing A's data while learning B." The most direct fix: rehearse A while learning B. Experience replay keeps a small memory buffer of A's samples and mixes a few old ones into each batch, so the gradient lowers both old and new losses at once and the weights don't drift. Crude but extremely effective—it's nearly the strongest baseline in continual learning.

But storing old data has two hard constraints: storage blowup (tasks grow without bound) and privacy/compliance (medical or user data can't be retained). Generative replay (Shin et al. 2017, Deep Generative Replay) offers an elegant fix: train a pair of cooperating models—a generator that learns to produce "data that looks like old tasks," and a solver that does the real classify/predict work. When learning a new task, the generator first emits a batch of synthetic old samples (labeled by the old solver), then trains on a mix with the new data. The inspiration is the hippocampus "replaying" experiences during sleep to consolidate memory. Flow:

Generator G──synth. old──▶mixed batch◀──new data──new data


train Solver──▶update G (learns new dist.)
↑ next task, G can generate both "old+new," rolling forward—no raw data stored
Code
# Experience replay: the minimal, strongest baseline. Keep a buffer, mix in
import random
buffer = []                                  # stores (x, y) old samples

def reservoir_add(buf, sample, cap=200):    # reservoir sampling: uniform retention
    if len(buf) < cap: buf.append(sample)
    else:
        j = random.randint(0, len(buf))      # random replace, stay representative
        if j < cap: buf[j] = sample

def train_with_replay(net, opt, loss_fn, Xb, yb, k=32):
    for i in range(200):
        xb, yb_ = Xb, yb                     # current task batch
        if buffer:                           # mix in old samples, joint loss
            old = random.sample(buffer, min(k, len(buffer)))
            ox = torch.stack([o[0] for o in old])
            oy = torch.tensor([o[1] for o in old])
            xb = torch.cat([xb, ox]); yb_ = torch.cat([yb_, oy])
        opt.zero_grad(); loss_fn(net(xb), yb_).backward(); opt.step()
# while learning A, reservoir_add into buffer; learning B auto-rehearses A → forgetting eased
Pitfall + where you'd use it
Pitfall: "Generative replay perfectly reproduces old data, so it equals full replay." No. The generator is itself continually learning and degrades generation-over-generation (quality drift): its synthetic "old samples" look less and less real, errors compound down the task chain, and over long sequences old tasks collapse anyway. Generative replay's effectiveness is capped by the generator's fidelity—the more complex the old task (high-res images, long text), the less you can count on it.
📌 Super-individual scenario: to make a local model keep absorbing new knowledge without forgetting old, experience replay is actually the least fussy—just keep a representative small sample of old data and mix it into the next training round; simpler and more robust than fancy regularization. It also explains why LLM continued training industry-wide "mixes old and new data in proportion" rather than feeding only new data.
Takeaway + question
💡 Replay = rehearse the old while learning the new; generative replay swaps storage for a "generator that synthesizes old data," capped by fidelity. Mixing old data is continual learning's strongest and plainest baseline.
🤔 Generative replay fights forgetting by "synthesizing fake old memories." Human memory is itself reconstructive, not playback—every recall rewrites. Is this "imprecise replay" a bug or a feature?

Parameter IsolationParameter Isolation

IsolationPackNet / Progressive
One-line analogy

Parameter isolation is a partitioning / sharding idea: since shared mutable state causes write conflicts, give each task its own dedicated block of parameters and set it read-only once learned. Other tasks use other shards—physically no write conflict, and old tasks are structurally protected—just like microservices deployed independently, never overwriting each other.

Problem it solves + how it works

Regularization and replay are "soft" protections—old knowledge can still be slowly eroded. Parameter isolation gives a "hard" guarantee: any weight allocated to an old task is frozen and never updated again, killing forgetting at the root. Two representative approaches:

① PackNet (Mallya & Lazebnik 2018)—"pack" multiple tasks into fixed capacity. Large nets are highly redundant; many weights can be pruned with almost no accuracy loss. Recipe: learn task A → prune away a batch of unimportant weights → freeze the remaining "A weights" → use the just-freed spare weights to learn task B → prune, freeze again... Like carving new partitions on a fixed disk for new tasks until space runs out. Zero storage growth, but the task count is capped by a hard capacity limit.

② Progressive Networks (Rusu et al. 2016)—add a new column per task. Each new task adds a fresh column of parameters, with all old columns frozen; the new column reads features learned by old columns via lateral connections (enabling forward transfer), but can't modify the old ones. Truly zero forgetting and it reuses old knowledge—at the cost of parameters growing linearly with the number of tasks, like spinning up a new replica per task that may only read the old replicas.

PackNet (fixed capacity · prune to free space)
A weights·frozenB weights·frozenC weights·spare

Progressive (column per task · read old laterally)
col A·frozen─lateral→col B·frozen─lateral→col C·training
old columns read-only, features borrowable; new column's gradient can't reach old ones
Code
# PackNet-style: after A, prune+freeze, hand "off" weights to B
import torch

def prune_and_freeze(weight, keep=0.5):
    # keep top-`keep` by magnitude as task A's "locked partition"
    flat = weight.abs().flatten()
    thresh = flat.kthvalue(int(len(flat) * (1 - keep))).values
    taskA_mask = weight.abs() >= thresh    # True = belongs to A, frozen
    weight.data *= taskA_mask                     # prune the unimportant (zero, leave for B)
    return taskA_mask

def masked_grad_hook(mask):
    # zero the "A partition" gradient on backward → A weights never updated by B
    def hook(grad): return grad * (~mask)
    return hook

W = net[0].weight
maskA = prune_and_freeze(W, keep=0.5)   # call after learning A
W.register_hook(masked_grad_hook(maskA))  # from now, learning B hard-protects A's partition
# training B: only ~50% spare weights update, A's accuracy stays exactly the same
Pitfall + where you'd use it
Pitfall: "Parameter isolation is zero-forgetting, so it's the best." Its zero-forgetting has costs: (1) you must know which task is current (pick the right partition/column at inference), so it fails when task boundaries are fuzzy; (2) PackNet has a hard capacity limit, Progressive Nets grow linearly; (3) transfer is one-way only (new borrows old)—later data can't feed back to improve an already-learned old task. It fits scenarios with clear, limited tasks where old tasks must never degrade at all.
📌 Personal-project scenario: train a separate LoRA adapter per client/domain, freeze the backbone, each adapter is an independent "partition"—exactly parameter isolation's practical variant in the LLM era: zero mutual interference, load on demand, mount whichever adapter the task needs.
Takeaway + question
💡 Parameter isolation gives old tasks a hard guarantee via "partition + freeze read-only": PackNet prunes to free space within fixed capacity, Progressive Nets add a column per task. Zero forgetting, at the cost of needing task identity and limited capacity/params.
🤔 The three remedies (reg / replay / isolation) map neatly onto three concurrency-control ideas you know: locking, log replay, data sharding. Which one is most like "optimistic locking," which most like "pessimistic locking"?

Further ReadingFurther Reading

Deep QuestionsDeep Questions

1. Regularization (EWC), replay, and isolation all "constrain optimization to not destroy the old solution." If they're one thing done three ways, what is that one thing?
The shared core is: in parameter space, protect the "good-solution region" of old tasks, so new-task optimization proceeds only within a subspace that doesn't break the old solution. The three schools just characterize and protect that subspace differently: EWC uses a Fisher quadratic penalty to approximate the old solution as an ellipsoid and penalize leaving it; replay/GEM sample the old constraint from old data (real or synthetic)—GEM is most direct, requiring the new gradient's inner product with old-sample gradients be non-negative ("don't move in directions that raise old loss"); isolation is most brutal, freezing the old solution's dimensions into constants that never enter optimization. Hence their different spots on the stability-plasticity spectrum: isolation is most stable (hard constraint, zero forgetting, limited plasticity), EWC in the middle (soft constraint, plastic but accumulates rigidity), replay most flexible (data-driven, strong but needs storage/generation). With this unifying view, choosing a method isn't memorizing 3 tricks but asking: "in my scenario, at what cost should I characterize the old solution's protected subspace?"—the same thinking as choosing a consistency protocol in distributed systems.
2. Some argue "big model + mixed-data full retraining" makes continual learning a pseudo-problem—you retrain from scratch each time anyway. Does that hold?
Partly, with hard boundaries. Where it holds: when you can get all historical data and the compute budget allows, "periodic full retraining" (with old+new mixed) is indeed industry's most robust approach, sidestepping forgetting—which is why many production models skip fancy CL algorithms and just retrain once enough data accrues. Experience replay's success essentially validates the "mix the data" path. Where it breaks: (1) data can't be kept—privacy/compliance/expired licenses make old data physically vanish, leaving only "no-old-data" methods like EWC or generative replay; (2) compute won't allow it—edge devices and robots must learn online in deployment, with no chance to "re-melt and retrain"; (3) real-time—need to adapt to new distributions in minutes (recommendation, risk control), can't wait for a retraining cycle; (4) cost—retraining a frontier model for a bit of incremental knowledge is uneconomical. So continual learning isn't a pseudo-problem but is pushed into harsher-constraint scenarios: data can't be kept, compute is limited, adaptation must be online. For the super-individual, local small-model continual fine-tuning falls exactly within these boundaries.
3. Catastrophic forgetting barely happens in the human brain. Which biological mechanisms map onto this issue's three schools, and what's the lesson for AI?
Strikingly one-to-one. (1) Complementary Learning Systems (hippocampus + neocortex) ↔ replay: the hippocampus rapidly encodes new experiences and, during sleep, repeatedly "replays" them to the neocortex for slow consolidation—the direct inspiration for generative replay (Shin et al. explicitly cite the hippocampus). (2) Synaptic consolidation ↔ EWC: important synapses become "harder to overwrite" via molecular mechanisms—EWC's Fisher penalty is its mathematical incarnation, and "consolidation" is literally in the paper title. (3) Functional partitioning / neurogenesis ↔ parameter isolation: different brain regions handle different functions—akin to allocating new capacity per task. The lesson: the brain relies not on a single mechanism but on all three in concert; most AI methods use just one, while the strongest CL systems tend to be hybrid (replay+regularization). A deeper point: brain memory is lossy reconstruction (recall rewrites), and it actively forgets the unimportant—"smart forgetting" may be a feature, not a flaw, a dimension most CL methods haven't taken seriously.
4. If "BigCat working long-term with AI" is a continual-learning system, which of the three schools should you (the human) play?
An interesting mismatch: today's AI tools have almost no real continual learning—each conversation is stateless, weights don't update. "Memory" is outsourced to the outer system (the context/memory architecture from Day 8), not the model's internals. This means you play the "replay" school—re-feeding "old context" to a stateless model via shared memory / an operating manual is human-powered experience replay; the vendor's "periodic retraining of new versions" is full retraining. Lessons: (1) don't expect today's model to "remember" you—crucial memory should be structured and persisted outside (isolation mindset: one dedicated memory partition per project); (2) "rehearse" periodically—important preferences/conclusions must be re-injected, or a new session equals forgetting; (3) beware your own forgetting: over-outsourcing lets your brain's "load-bearing weights" (core judgment) get quietly eroded by daily convenience unless you add "EWC-style protection" (deliberate practice, don't outsource core thinking).
5. Can the "stability-plasticity dilemma" be fully solved, or is it an essential trade-off like CAP?
Leaning toward: an essential trade-off, but its Pareto frontier can be raised—like CAP can't be fully satisfied at once, yet engineering can make both C and A great for a given partition tolerance. Under fixed capacity and fixed information, learning the new must rewrite some representation, and rewriting damages old tasks—an information-theoretic tension that can't vanish. But many ways raise the frontier: (1) add capacity (isolation)—trade params for having both, at the cost of scale; (2) add information (replay)—keep old data to turn "sequential" back into "joint" learning, at the cost of storage; (3) better representations—if different tasks' representations are naturally orthogonal/sparse (don't share weights), conflict shrinks, the hope of modular/sparse architectures; (4) forward transfer—ideally learning the new even improves the old (GEM's aim), no longer zero-sum. Pragmatic answer: the principle can't be removed, but via capacity, information, representation, and transfer, real systems can push it to "acceptable" or even "near win-win"—the same mature stance you take toward CAP: don't eliminate the trade-off, tune it to the optimum for the concrete scenario.