Continual Learning Evaluation › Code Implementation

Michael BrenndoerferFebruary 23, 202613 min read
Language AI HandbookAll chapters
Part 46 of 67Continual Learning
Chapter 356Continual Learning Evaluation › Code Implementation
Reading section 11 of 14 · Code Implementation · 13 minSection 11 of 14 · 13 min

Book contents

Section 11 of 14 · 13 min read

Continual Learning Evaluation › Code ImplementationOne section · Sections in this chapter11 / 14 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

This section builds a framework to evaluate a continual learning scenario from scratch, computing the accuracy matrix and all derived metrics.

We will simulate a simple continual learning run on Split MNIST: 5 sequential binary classification tasks, each distinguishing a different pair of digit classes. The "model" is represented by a simulated accuracy matrix that reproduces the forgetting pattern of a naive gradient descent baseline and a replay-based method. Using simulation rather than a full MNIST training loop keeps the focus on the evaluation machinery rather than the training mechanics, while still producing realistic-looking metric values.

Setup and Task Construction

In[3]:
Code
import random

import numpy as np

# Fix random seeds for reproducibility
np.random.seed(42)
random.seed(42)

# Number of tasks
N = 5

# Simulate the accuracy matrix for a naive fine-tuning baseline
# After training on task i, accuracy on task j follows a forgetting pattern:
# - diagonal: high accuracy right after training (~92-96%)
# - off-diagonal: decays over subsequent tasks (~random baseline = 50% for binary)


def simulate_accuracy_matrix(
    n_tasks, diag_acc=0.93, forget_rate=0.70, noise_std=0.03
):
    """Simulate a lower-triangular accuracy matrix for a continual learning model.

    After training on task i, performance on earlier tasks j < i decays
    according to a forgetting rate. We add Gaussian noise to reflect
    real run-to-run variation.
    """
    R = np.zeros((n_tasks, n_tasks))
    for i in range(n_tasks):
        for j in range(i + 1):
            if i == j:
                # Diagonal: accuracy right after training on task j
                R[i, j] = diag_acc + np.random.normal(0, noise_std)
            else:
                # Off-diagonal: forgetting after (i - j) subsequent tasks
                # Performance decays toward chance (0.50 for binary)
                steps_later = i - j
                floor_acc = 0.50  # binary chance
                retained_above_chance = (diag_acc - floor_acc) * (
                    1 - forget_rate
                ) ** steps_later
                R[i, j] = (
                    floor_acc
                    + retained_above_chance
                    + np.random.normal(0, noise_std)
                )
    # Binary-task accuracy should not fall below the chance baseline.
    R = np.clip(R, 0.50, 1.0)
    return R


# Simulate naive baseline accuracy matrix
R_naive = simulate_accuracy_matrix(N, diag_acc=0.93, forget_rate=0.70)

# Simulate a replay-based method with much slower forgetting
R_replay = simulate_accuracy_matrix(N, diag_acc=0.92, forget_rate=0.04)

# Isolated training baselines (each task trained from scratch)
b = np.full(N, 0.95)
Out[4]:
Console
Naive fine-tuning accuracy matrix R (rows=after training task i, cols=task j):
     T1      T2      T3      T4      T5
T1:  0.945    ---      ---      ---      ---  
T2:  0.625  0.949    ---      ---      ---  
T3:  0.584  0.622  0.923    ---      ---  
T4:  0.559  0.562  0.615  0.946    ---  
T5:  0.500  0.500  0.546  0.572  0.878

Isolated training baselines b: [0.95 0.95 0.95 0.95 0.95]

The matrix makes forgetting concrete. Right after training on each task, the model achieves around 93% accuracy (the diagonal). But once subsequent tasks overwrite the weights, performance on earlier tasks collapses toward the 50% binary chance level. The bottom-left corner of the matrix contains the most damning evidence: by the time the model has learned all five tasks, its accuracy on the first task has fallen to near chance.

Visualizing the accuracy matrix as a heatmap makes the forgetting pattern immediately apparent. The diagonal glows bright while the off-diagonal entries fade toward chance.

Out[5]:
Visualization
Heatmap showing accuracy matrix where off-diagonal entries fade quickly toward 0.5 for naive fine-tuning.
Accuracy matrix for the naive fine-tuning baseline across 5 sequential tasks. Each row shows model accuracy on all tasks evaluated after completing a given task. The bright diagonal (high accuracy right after training) contrasts sharply with the dark lower-left region, revealing catastrophic forgetting of earlier tasks.
Heatmap showing accuracy matrix where off-diagonal entries stay near 0.9 for replay method.
Accuracy matrix for the replay-based method across the same 5-task sequence. The near-uniform brightness off-diagonal shows that replay substantially preserves old task performance, with only slight degradation visible in the bottom-left corners.

Computing Average Accuracy

In[6]:
Code
def average_accuracy(R):
    """Average accuracy across all tasks after training on all N tasks."""
    N = R.shape[0]
    return np.mean([R[N - 1, j] for j in range(N)])


avg_naive = average_accuracy(R_naive)
avg_replay = average_accuracy(R_replay)
Out[7]:
Console
Average accuracy (naive fine-tuning): 0.599
Average accuracy (replay method):     0.878

The replay method achieves higher average accuracy. But average accuracy alone does not tell us whether the gap comes from better retention of old tasks, faster learning of new tasks, or both. That is why we need BWT and FWT. The numerical difference between methods on average accuracy often understates the true behavioral difference, because some of the naive model's forgetting is partially offset by the fact that its most recently trained task performs well.

Computing Backward Transfer

In[8]:
Code
def backward_transfer(R):
    """Backward transfer: how much final accuracy differs from peak accuracy on each task."""
    N = R.shape[0]
    bwt = np.mean([R[N - 1, j] - R[j, j] for j in range(N - 1)])
    return bwt


bwt_naive = backward_transfer(R_naive)
bwt_replay = backward_transfer(R_replay)
Out[9]:
Console
BWT (naive fine-tuning): -0.412
BWT (replay method):     -0.055

The naive method shows strongly negative BWT, confirming catastrophic forgetting. The replay method's BWT is much closer to zero, showing that replaying past examples substantially preserves old task performance. The magnitude of the BWT difference between the two methods is much larger than the average accuracy difference, which illustrates exactly why BWT is a more diagnostic metric than average accuracy for understanding forgetting behavior.

Computing Forward Transfer

In[10]:
Code
def forward_transfer(R, b):
    """Forward transfer: how much prior tasks help learning new tasks, vs. isolated training."""
    N = R.shape[0]
    fwt = np.mean([R[j, j] - b[j] for j in range(1, N)])
    return fwt


fwt_naive = forward_transfer(R_naive, b)
fwt_replay = forward_transfer(R_replay, b)
Out[11]:
Console
FWT (naive fine-tuning): -0.026
FWT (replay method):     -0.023

Both methods show slightly negative FWT in this simulation. This is typical for Split MNIST: the tasks are designed to be distinct enough that prior training does not transfer positively. The replay method's FWT is closer to zero because replaying diverse examples helps maintain useful general features that modestly benefit new task learning, even when the tasks themselves are not semantically related.

Computing Forgetting and Intransigence

In[12]:
Code
def forgetting_measure(R):
    """Average worst-case forgetting: max accuracy ever achieved minus final accuracy, per task."""
    N = R.shape[0]
    forgetting_per_task = []
    for j in range(N - 1):
        # Maximum accuracy achieved on task j at any point during training
        max_acc = max(R[k, j] for k in range(j, N))
        final_acc = R[N - 1, j]
        forgetting_per_task.append(max_acc - final_acc)
    return np.mean(forgetting_per_task)


def intransigence(R, b):
    """Intransigence: how much accumulated training hurts learning new tasks."""
    N = R.shape[0]
    intr = np.mean([b[j] - R[j, j] for j in range(N)])
    return intr


forget_naive = forgetting_measure(R_naive)
forget_replay = forgetting_measure(R_replay)
intr_naive = intransigence(R_naive, b)
intr_replay = intransigence(R_replay, b)
Out[13]:
Console
Forgetting (naive fine-tuning): 0.412
Forgetting (replay method):     0.055
Intransigence (naive):  0.022
Intransigence (replay): 0.028

Forgetting confirms the large retention gap between the two methods. Intransigence is near zero for both, which is expected: naive fine-tuning suffers from forgetting but not from rigidity, and replay can still learn new tasks freely because it does not impose weight change penalties. This is a clean example of the asymmetry between forgetting and intransigence: a method can have severe forgetting (overwriting old knowledge) without having any intransigence (blocking new learning), and vice versa.

Summary of All Metrics

In[14]:
Code
metrics = {
    "Method": ["Naive Fine-tuning", "Replay"],
    "Avg Acc": [avg_naive, avg_replay],
    "BWT": [bwt_naive, bwt_replay],
    "FWT": [fwt_naive, fwt_replay],
    "Forgetting": [forget_naive, forget_replay],
    "Intransigence": [intr_naive, intr_replay],
}
Out[15]:
Console
Method                Avg Acc           BWT               FWT               Forgetting        Intransigence
----------------------------------------------------------------------------------------------------
Naive Fine-tuning     0.599             -0.412            -0.026            0.412             0.022
Replay                0.878             -0.055            -0.023            0.055             0.028

The metric table gives a complete picture of each method's behavior. Naive fine-tuning achieves high accuracy right after training each task but loses most of it by the end. The replay method sacrifices almost nothing to retention while achieving similar immediate learning. Both methods have near-zero intransigence on these short 5-task sequences. Average accuracy shows replay's practical advantage, while BWT and forgetting identify retention as the reason for that advantage.

Plotting all five metrics side by side makes the contrast between methods clear at a glance.

Out[16]:
Visualization
Grouped bar chart comparing five continual learning metrics for two methods, showing replay dominates on BWT and forgetting.
Comparison of five continual learning metrics for naive fine-tuning versus replay-based method on a simulated 5-task Split MNIST sequence. Naive fine-tuning achieves similar initial learning (low intransigence) but collapses on retention (strongly negative BWT, high forgetting), while replay maintains near-zero BWT and much higher final average accuracy.
Out[17]:
Visualization
Line chart of per-task accuracy over time for naive fine-tuning, showing steep drops after each new task.
Per-task accuracy trajectories for naive fine-tuning across the 5-task sequence. Each colored line traces a single task's test accuracy as subsequent tasks are trained. Task accuracy drops sharply after each new task overwrites the weights, collapsing toward the 50% binary chance level by the final task.
Line chart of per-task accuracy over time for replay method, showing flat trajectories indicating retention.
Per-task accuracy trajectories for the replay-based method across the same 5-task sequence. Each line remains nearly flat throughout training, confirming that replay prevents catastrophic forgetting and maintains consistent accuracy across all tasks.

The trajectory plots tell a story that the summary metrics cannot fully convey. For naive fine-tuning, each task's accuracy is highest at the moment of its own training, then falls precipitously as subsequent tasks overwrite the shared weights. The drop is not gradual. Task 1's accuracy falls most steeply because it is followed by four additional tasks, each contributing further degradation. Task 4's accuracy falls less because only one task follows. The shape of the trajectories reflects the geometric decay built into the simulation, but real networks show similar patterns.

For the replay method, the trajectories are almost horizontal. The periodic rehearsal of stored examples continuously refreshes the model's memory of earlier tasks, preventing the cascade of forgetting visible in the naive baseline. The residual downward slope visible in some task trajectories for the replay method reflects the imperfect coverage of past distributions by a finite replay buffer.

Key Parameters

The key parameters governing this evaluation framework are:

  • N: Number of tasks. Longer sequences stress-test forgetting more severely because each task has more subsequent training to degrade its performance. A method that performs well on 5-task sequences may fail on 20-task sequences if its forgetting mitigation does not scale.
  • Evaluation protocol: Task-IL versus class-IL versus domain-IL determines what task information the model can use during inference, fundamentally changing the difficulty of the problem and the ranking of methods.
  • Baseline bjb_j: How the isolated training baseline is computed affects FWT and intransigence interpretation. The baseline should use the same architecture and training budget as the continual learner, with no other tasks present, to ensure a fair comparison.
  • Evaluation frequency: End-of-task evaluation (standard) versus per-update tracking (for dynamics analysis). Fine-grained tracking reveals when during training forgetting occurs and how rapidly it progresses.