← Back to DevBytes

How to Prevent Catastrophic Forgetting During Fine-Tuning

What is Catastrophic Forgetting?

Catastrophic forgetting, also known as catastrophic interference, is a phenomenon in machine learning where a neural network completely and abruptly forgets previously learned information upon learning new data. When you take a pre-trained model and fine-tune it on a new, specific task, the model's weights shift to accommodate the new task. If this process is not managed carefully, the model loses its general capabilities and its performance on the original tasks degrades significantly.

For example, if you fine-tune a large language model (LLM) that was pre-trained on general internet text to exclusively answer medical questions, it might become excellent at medical queries but forget how to write Python code, translate languages, or summarize general articles. The network's weights have been overwritten by the new domain-specific gradients.

Why It Matters

Pre-training large models from scratch requires massive amounts of data, computational resources, and time. Fine-tuning is a cost-effective way to adapt these powerful foundation models to specific use cases. However, if fine-tuning leads to catastrophic forgetting, you effectively destroy the general utility of the expensive pre-trained model.

Preventing this issue is critical for several reasons:

How to Prevent Catastrophic Forgetting

There are several established techniques to mitigate catastrophic forgetting during fine-tuning. The best approach often depends on your specific architecture, available compute, and whether you have access to the original pre-training data.

1. Parameter-Efficient Fine-Tuning (PEFT) and LoRA

One of the most effective modern ways to prevent catastrophic forgetting is to simply not touch the original model weights. PEFT methods, such as Low-Rank Adaptation (LoRA), freeze the pre-trained model weights and inject trainable rank-decomposition matrices into the architecture. Because the base weights remain frozen, the original knowledge is preserved.

Here is an example using the Hugging Face peft library to apply LoRA to a causal language model:

from transformers import AutoModelForCausalLM
from peft import LoraConfig, get_peft_model

# Load the pre-trained model
model_id = "meta-llama/Llama-2-7b-hf"
model = AutoModelForCausalLM.from_pretrained(model_id)

# Define LoRA configuration
lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

# Apply LoRA to the model
model = get_peft_model(model, lora_config)

# Print trainable parameters
model.print_trainable_parameters()
# Output: trainable params: 4,194,304 || all params: 6,751,014,912 || trainable%: 0.062

2. Layer Freezing

If you are not using PEFT, a simpler approach is to freeze the early layers of the network. Early layers generally learn low-level, general features that are broadly applicable across many tasks. By freezing these layers and only training the final few layers (or a newly added classification head), you preserve the foundational knowledge while adapting the model's output to the new task.

Here is how you can freeze layers in PyTorch:

import torch
import torch.nn as nn

# Assuming 'model' is a pre-trained PyTorch model
model = ... 

# Freeze all parameters
for param in model.parameters():
    param.requires_grad = False

# Unfreeze the last few layers (e.g., the final transformer block and output head)
# Adjust the layer names based on your specific model architecture
for name, param in model.named_parameters():
    if "encoder.layer.11" in name or "classifier" in name:
        param.requires_grad = True

# Verify which parameters are trainable
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())
print(f"Trainable params: {trainable_params}/{total_params}")

3. Replay Buffers (Experience Replay)

If you have access to a subset of the original pre-training data or data from previous tasks, you can mix it with your new fine-tuning data. By interleaving old and new data, the model is continuously exposed to previous concepts, preventing the weights from drifting too far. This is known as experience replay.

Here is a conceptual example of creating a mixed DataLoader in PyTorch:

from torch.utils.data import ConcatDataset, DataLoader

# Assume 'new_task_dataset' and 'old_task_dataset' are defined PyTorch Datasets
# We mix 80% new data and 20% old data to maintain a balance

# To achieve a specific ratio, we can sample from the old dataset
from torch.utils.data import Subset
import random

old_indices = random.sample(range(len(old_task_dataset)), int(len(old_task_dataset) * 0.2))
old_subset = Subset(old_task_dataset, old_indices)

# Combine datasets
mixed_dataset = ConcatDataset([new_task_dataset, old_subset])

# Create the DataLoader
train_loader = DataLoader(mixed_dataset, batch_size=32, shuffle=True)

4. Elastic Weight Consolidation (EWC)

EWC is a more advanced technique that penalizes changes to the weights that were most important for the previous task. It calculates the Fisher Information Matrix for the pre-trained model, which identifies which weights are crucial. During fine-tuning, a penalty term is added to the loss function, heavily penalizing changes to those crucial weights while allowing less important weights to change freely.

Here is a simplified conceptual implementation of the EWC penalty in PyTorch:

import torch

def ewc_loss(model, fisher_matrix, optimal_params, lambda_ewc=1000):
    """
    Calculates the EWC penalty.
    model: current model being trained
    fisher_matrix: dict of Fisher information diagonal for each parameter
    optimal_params: dict of optimal parameters from the pre-trained model
    lambda_ewc: weighting factor for the penalty
    """
    loss = 0
    for name, param in model.named_parameters():
        if name in fisher_matrix:
            # Penalize the squared difference, weighted by Fisher information
            loss += (fisher_matrix[name] * (param - optimal_params[name]) ** 2).sum()
    
    return lambda_ewc * loss

# During training loop:
# total_loss = task_loss + ewc_loss(model, fisher_matrix, optimal_params)

Best Practices

Conclusion

Catastrophic forgetting is a significant hurdle in adapting pre-trained models, but it is entirely manageable with the right strategies. By leveraging Parameter-Efficient Fine-Tuning methods like LoRA, freezing foundational layers, utilizing replay buffers, or applying regularization techniques like EWC, developers can successfully adapt models to new domains without sacrificing their pre-trained capabilities. Choosing the right approach depends on your resource constraints and access to prior data, but prioritizing the preservation of base weights remains the most reliable way to build robust, multi-purpose AI systems.

— Ad —

Google AdSense will appear here after approval

← Back to all articles