Avoiding Catastrophic Forgetting

AIMachine LearningFine-TuningLLM
Share on LinkedIn

You fine-tuned a 7B model on ten thousand internal support tickets — domain accuracy jumped twelve points, and suddenly it forgot how to follow JSON schema instructions it handled perfectly last week. That is catastrophic forgetting: not mystical AI amnesia, but gradient descent happily destroying general-purpose representations to minimize loss on your narrow distribution. Every fine-tuning run trades off adaptation against preservation. The job is to measure both sides and use techniques that move the Pareto frontier — not to hope a single epoch count works forever.

Why forgetting happens

Pretrained models encode broad linguistic and reasoning structure in shared weights. Fine-tuning on specialized data applies updates ΔW that improve domain loss L_domain but increase loss on general tasks L_general.

Full fine-tuning updates all weights — high capacity to forget. Even adapter methods shift activations passing through frozen layers.

More epochs, higher learning rate, smaller diverse data → more forgetting risk.

Evaluation harness (non-negotiable)

Before training, freeze baseline scores:

eval_suites = {
    "domain_support": load_jsonl("eval/support_golden.jsonl"),
    "general_instr": load_jsonl("eval/mmlu_style_instr.jsonl"),
    "json_format": load_jsonl("eval/structured_output.jsonl"),
}

def evaluate(model, suite):
    # generation + automatic metrics or LLM judge
    ...

baseline = {name: evaluate(base_model, data) for name, data in eval_suites.items()}

After each epoch checkpoint:

for name, data in eval_suites.items():
    score = evaluate(model, data)
    delta = score - baseline[name]
    log(name, score, delta)  # alert if general_instr delta < -0.03

Domain-only eval greenlights while production general behavior regresses.

Mitigation strategies

1. Parameter-efficient fine-tuning (LoRA/QLoRA)

from peft import LoraConfig, get_peft_model

config = LoraConfig(
    r=16,              # lower r = less capacity, often less forgetting
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
)
model = get_peft_model(base_model, config)

Start r=8 or 16; increase only if underfitting domain eval.

2. Replay mixing

Combine domain data with general instruction sample:

batch = concat(
    sample(domain_dataset, k=6),
    sample(general_instruction_pool, k=2),
)

Replay ratio 10–30% often stabilizes general metrics. Use diverse general pool — not duplicate of pretraining corpus necessarily, but varied task formats.

3. Conservative optimization

4. Elastic Weight Consolidation (EWC)

Penalize changes to weights important for prior tasks:

[ L = L_{\text{domain}} + \lambda \sum_i F_i (\theta_i - \theta_i^*)^2 ]

Fisher information F from general task gradients — research-heavy; replay often simpler in practice.

5. Multi-stage fine-tuning

General instruction tune → domain tune with replay → optional DPO with same replay — each stage eval both suites.

When some forgetting is acceptable

Narrow classifiers inside enterprise with controlled prompts may tolerate general regression if guardrailed. Customer-facing assistants and coding agents cannot.

Define acceptance thresholds upfront: "json_format exact match ≥ 95% of baseline."

Production monitoring post-deploy

Rollback adapter weights without redeploying base model — keep previous LoRA artifact versioned.

Building a general capability eval suite

Before any fine-tune, establish baseline scores on tasks the model must retain:

GENERAL_EVAL = [
    {"prompt": "Summarize this in 3 bullet points: ...", "metric": "format_compliance"},
    {"prompt": "Translate to French: Hello world", "metric": "exact_match", "expected": "Bonjour le monde"},
    {"prompt": "Is this JSON valid? {...}", "metric": "classification"},
    {"prompt": "Write a Python function to reverse a string", "metric": "code_exec"},
]

def eval_general(model, suite):
    scores = {}
    for item in suite:
        output = model.generate(item["prompt"])
        scores[item["metric"]] = score_fn(item, output)
    return scores

Run this suite before fine-tune (baseline), after each epoch, and after deploy. Alert if any metric drops >5% from baseline.

Replay buffer design

Mix general examples into domain training data:

# 80% domain, 20% general replay
train_dataset = concatenate([
    domain_dataset.select(range(int(len(domain_dataset) * 0.8))),
    general_replay_dataset.select(range(int(len(domain_dataset) * 0.2))),
]).shuffle()

General replay examples should cover: instruction following, JSON output, code generation, summarization, translation. Source from the model's original instruction-tuning dataset if available — those examples are proven to maintain general capability.

For LoRA fine-tunes, even 500–1000 replay examples significantly reduce forgetting vs domain-only training.

Layer freezing strategy

When using full fine-tune (not LoRA), freeze early layers:

for name, param in model.named_parameters():
    if "layers.0" in name or "layers.1" in name:  # first 2 of 32 layers
        param.requires_grad = False

Early layers capture general language features; later layers capture task-specific patterns. Freezing early layers preserves general capability while allowing domain adaptation in later layers.

LoRA naturally limits forgetting by only updating low-rank adapters — prefer LoRA over full fine-tune when general capability retention matters.

Failure modes

Production checklist

Evaluate base model capabilities after every fine-tune epoch — forgetting shows up on general benchmarks before your domain metrics degrade.

Resources

Frequently asked questions

What is catastrophic forgetting in fine-tuning?

When a model adapts strongly to a narrow fine-tuning dataset, weights shift to minimize new task loss and degrade performance on previously learned capabilities — general reasoning, formatting, or earlier fine-tunes. The model did not unlearn data; optimization overwrote representations critical for other tasks.

Does LoRA prevent catastrophic forgetting?

LoRA reduces forgetting by updating low-rank adapters while freezing most base weights — preserving general capabilities better than full fine-tuning. It does not eliminate forgetting; aggressive LoRA rank, high learning rate, or too many epochs on narrow data can still damage base behavior.

How do I detect forgetting during fine-tuning?

Maintain a holdout eval set of general tasks (MMLU subset, instruction following, prior domain prompts) and track metrics each epoch. If general scores drop while domain scores rise, reduce learning rate, epochs, or add replay mixing.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →