caf324b09d
Build documentation / build (push) Failing after 0s
Deploy "method_comparison" Gradio to Spaces / deploy (push) Has been cancelled
Deploy "PEFT shop" Gradio app to Spaces / deploy (push) Has been cancelled
tests on transformers main / tests (push) Has been cancelled
tests / check_code_quality (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.11) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.12) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.11) (push) Has been cancelled
tests / tests (windows-latest, 3.12) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Secret Leaks / trufflehog (push) Has been cancelled
CI security linting / zizmor latest via Cargo (push) Has been cancelled
694 lines
24 KiB
Python
694 lines
24 KiB
Python
# Copyright 2025-present the HuggingFace Inc. team.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""
|
|
OSF Continual Learning Example
|
|
|
|
This script demonstrates OSF's ability to learn multiple tasks sequentially while preventing
|
|
catastrophic forgetting, compared to standard full fine-tuning.
|
|
|
|
Tasks:
|
|
1. ScienceQA - Science question answering
|
|
2. NumGLUE - Mathematical reasoning
|
|
3. FOMC - Financial sentiment classification
|
|
|
|
OSF Configuration:
|
|
- Task 1: effective_rank=0.3 (train 70%, freeze 30%)
|
|
- Task 2: effective_rank=0.5 (train 50%, freeze 50%)
|
|
- Task 3: effective_rank=0.7 (train 30%, freeze 70%)
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
|
|
import torch
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments
|
|
from utils import (
|
|
DataCollatorForCompletionOnly,
|
|
format_fomc_for_llama,
|
|
format_numglue_for_llama,
|
|
format_scienceqa_for_llama,
|
|
load_fomc,
|
|
load_numglue,
|
|
load_scienceqa,
|
|
)
|
|
|
|
from peft import OSFConfig, get_peft_model
|
|
|
|
|
|
def compute_accuracy_scienceqa(model, eval_dataset, tokenizer, data_collator):
|
|
"""Compute accuracy for ScienceQA (extract predicted letter)."""
|
|
model.eval()
|
|
correct = 0
|
|
total = 0
|
|
|
|
# Create a simple dataloader
|
|
from torch.utils.data import DataLoader
|
|
|
|
dataloader = DataLoader(eval_dataset, batch_size=8, collate_fn=data_collator)
|
|
|
|
with torch.no_grad():
|
|
for batch in dataloader:
|
|
input_ids = batch["input_ids"].to(model.device)
|
|
attention_mask = batch["attention_mask"].to(model.device)
|
|
labels = batch["labels"]
|
|
|
|
# Generate predictions
|
|
outputs = model.generate(
|
|
input_ids=input_ids,
|
|
attention_mask=attention_mask,
|
|
max_new_tokens=5,
|
|
pad_token_id=tokenizer.pad_token_id,
|
|
do_sample=False,
|
|
)
|
|
|
|
# Extract predictions and ground truth
|
|
for i in range(len(outputs)):
|
|
# Decode the generated text
|
|
generated_text = tokenizer.decode(outputs[i], skip_special_tokens=True)
|
|
|
|
# Extract the answer (last letter in the generated text)
|
|
# Look for single capital letters A, B, C, D
|
|
matches = re.findall(r"\b([A-D])\b", generated_text)
|
|
pred = matches[-1] if matches else "X"
|
|
|
|
# Get ground truth (find the label that's not -100)
|
|
label_ids = labels[i][labels[i] != -100]
|
|
if len(label_ids) > 0:
|
|
gt = tokenizer.decode(label_ids, skip_special_tokens=True).strip()
|
|
if pred == gt:
|
|
correct += 1
|
|
total += 1
|
|
|
|
accuracy = correct / total if total > 0 else 0.0
|
|
return accuracy
|
|
|
|
|
|
def compute_accuracy_numglue(model, eval_dataset, tokenizer, data_collator):
|
|
"""Compute accuracy for NumGLUE (extract predicted number)."""
|
|
model.eval()
|
|
correct = 0
|
|
total = 0
|
|
|
|
from torch.utils.data import DataLoader
|
|
|
|
dataloader = DataLoader(eval_dataset, batch_size=8, collate_fn=data_collator)
|
|
|
|
with torch.no_grad():
|
|
for batch in dataloader:
|
|
input_ids = batch["input_ids"].to(model.device)
|
|
attention_mask = batch["attention_mask"].to(model.device)
|
|
labels = batch["labels"]
|
|
|
|
outputs = model.generate(
|
|
input_ids=input_ids,
|
|
attention_mask=attention_mask,
|
|
max_new_tokens=20,
|
|
pad_token_id=tokenizer.pad_token_id,
|
|
do_sample=False,
|
|
)
|
|
|
|
for i in range(len(outputs)):
|
|
generated_text = tokenizer.decode(outputs[i], skip_special_tokens=True)
|
|
|
|
# Extract number from generated text
|
|
numbers = re.findall(r"-?\d+\.?\d*", generated_text)
|
|
pred = numbers[-1] if numbers else "-999"
|
|
|
|
# Get ground truth
|
|
label_ids = labels[i][labels[i] != -100]
|
|
if len(label_ids) > 0:
|
|
gt = tokenizer.decode(label_ids, skip_special_tokens=True).strip()
|
|
if pred == gt:
|
|
correct += 1
|
|
total += 1
|
|
|
|
accuracy = correct / total if total > 0 else 0.0
|
|
return accuracy
|
|
|
|
|
|
def compute_accuracy_fomc(model, eval_dataset, tokenizer, data_collator):
|
|
"""Compute accuracy for FOMC (extract predicted sentiment)."""
|
|
model.eval()
|
|
correct = 0
|
|
total = 0
|
|
|
|
from torch.utils.data import DataLoader
|
|
|
|
dataloader = DataLoader(eval_dataset, batch_size=8, collate_fn=data_collator)
|
|
|
|
valid_labels = ["Dovish", "Hawkish", "Neutral"]
|
|
|
|
with torch.no_grad():
|
|
for batch in dataloader:
|
|
input_ids = batch["input_ids"].to(model.device)
|
|
attention_mask = batch["attention_mask"].to(model.device)
|
|
labels = batch["labels"]
|
|
|
|
outputs = model.generate(
|
|
input_ids=input_ids,
|
|
attention_mask=attention_mask,
|
|
max_new_tokens=10,
|
|
pad_token_id=tokenizer.pad_token_id,
|
|
do_sample=False,
|
|
)
|
|
|
|
for i in range(len(outputs)):
|
|
generated_text = tokenizer.decode(outputs[i], skip_special_tokens=True)
|
|
|
|
# Extract sentiment label
|
|
pred = None
|
|
for label in valid_labels:
|
|
if label in generated_text:
|
|
pred = label
|
|
break
|
|
|
|
# Get ground truth
|
|
label_ids = labels[i][labels[i] != -100]
|
|
if len(label_ids) > 0:
|
|
gt = tokenizer.decode(label_ids, skip_special_tokens=True).strip()
|
|
if pred == gt:
|
|
correct += 1
|
|
total += 1
|
|
|
|
accuracy = correct / total if total > 0 else 0.0
|
|
return accuracy
|
|
|
|
|
|
def evaluate_model(model, eval_dataset, data_collator, tokenizer, task_name, task_type):
|
|
"""Evaluate model on a dataset and return loss and accuracy."""
|
|
# Compute loss
|
|
trainer = Trainer(
|
|
model=model,
|
|
data_collator=data_collator,
|
|
eval_dataset=eval_dataset,
|
|
args=TrainingArguments(
|
|
label_names=["labels"],
|
|
),
|
|
)
|
|
results = trainer.evaluate()
|
|
loss = results["eval_loss"]
|
|
|
|
# Compute accuracy based on task type
|
|
if task_type == "scienceqa":
|
|
accuracy = compute_accuracy_scienceqa(model, eval_dataset, tokenizer, data_collator)
|
|
elif task_type == "numglue":
|
|
accuracy = compute_accuracy_numglue(model, eval_dataset, tokenizer, data_collator)
|
|
elif task_type == "fomc":
|
|
accuracy = compute_accuracy_fomc(model, eval_dataset, tokenizer, data_collator)
|
|
else:
|
|
accuracy = 0.0
|
|
|
|
print(f" {task_name}: Loss = {loss:.4f}, Accuracy = {accuracy * 100:.2f}%")
|
|
return loss, accuracy
|
|
|
|
|
|
def train_with_osf(
|
|
model_name,
|
|
num_train,
|
|
num_eval,
|
|
output_dir,
|
|
num_epochs,
|
|
learning_rate,
|
|
batch_size,
|
|
gradient_accumulation_steps,
|
|
max_length,
|
|
seed,
|
|
):
|
|
"""Train using OSF with progressive rank allocation."""
|
|
print("\n" + "=" * 80)
|
|
print("TRAINING WITH OSF (Orthogonal Subspace Fine-tuning)")
|
|
print("=" * 80)
|
|
|
|
# Load tokenizer and base model
|
|
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
|
tokenizer.pad_token = tokenizer.eos_token
|
|
|
|
base_model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.bfloat16, device_map="auto")
|
|
|
|
# Load all datasets with task-specific sizes
|
|
# FOMC only has 496 samples total, so we use 350 train + 146 eval for it
|
|
print("\nLoading datasets...")
|
|
scienceqa_train, scienceqa_eval = load_scienceqa(1000, 200, seed)
|
|
numglue_train, numglue_eval = load_numglue(1000, 200, seed)
|
|
fomc_train, fomc_eval = load_fomc(350, 146, seed)
|
|
|
|
# Store original eval datasets for later
|
|
scienceqa_eval_original = scienceqa_eval
|
|
numglue_eval_original = numglue_eval
|
|
fomc_eval_original = fomc_eval
|
|
|
|
# Format datasets
|
|
scienceqa_train = scienceqa_train.map(
|
|
lambda x: format_scienceqa_for_llama(x, tokenizer, max_length),
|
|
batched=True,
|
|
remove_columns=scienceqa_train.column_names,
|
|
)
|
|
scienceqa_eval = scienceqa_eval.map(
|
|
lambda x: format_scienceqa_for_llama(x, tokenizer, max_length),
|
|
batched=True,
|
|
remove_columns=scienceqa_eval.column_names,
|
|
)
|
|
|
|
numglue_train = numglue_train.map(
|
|
lambda x: format_numglue_for_llama(x, tokenizer, max_length),
|
|
batched=True,
|
|
remove_columns=numglue_train.column_names,
|
|
)
|
|
numglue_eval = numglue_eval.map(
|
|
lambda x: format_numglue_for_llama(x, tokenizer, max_length),
|
|
batched=True,
|
|
remove_columns=numglue_eval.column_names,
|
|
)
|
|
|
|
fomc_train = fomc_train.map(
|
|
lambda x: format_fomc_for_llama(x, tokenizer, max_length), batched=True, remove_columns=fomc_train.column_names
|
|
)
|
|
fomc_eval = fomc_eval.map(
|
|
lambda x: format_fomc_for_llama(x, tokenizer, max_length), batched=True, remove_columns=fomc_eval.column_names
|
|
)
|
|
|
|
data_collator = DataCollatorForCompletionOnly(tokenizer, max_length)
|
|
|
|
# Task configurations
|
|
tasks = [
|
|
{
|
|
"name": "ScienceQA",
|
|
"train": scienceqa_train,
|
|
"eval": scienceqa_eval,
|
|
"eval_original": scienceqa_eval_original,
|
|
"effective_rank": 0.3, # Freeze 30%, train 70%
|
|
"type": "scienceqa",
|
|
},
|
|
{
|
|
"name": "NumGLUE",
|
|
"train": numglue_train,
|
|
"eval": numglue_eval,
|
|
"eval_original": numglue_eval_original,
|
|
"effective_rank": 0.5, # Freeze 50%, train 50%
|
|
"type": "numglue",
|
|
},
|
|
{
|
|
"name": "FOMC",
|
|
"train": fomc_train,
|
|
"eval": fomc_eval,
|
|
"eval_original": fomc_eval_original,
|
|
"effective_rank": 0.7, # Freeze 70%, train 30%
|
|
"type": "fomc",
|
|
},
|
|
]
|
|
|
|
# Store evaluation history: {task_name: [(loss, accuracy), ...]}
|
|
eval_history = {
|
|
"ScienceQA": [],
|
|
"NumGLUE": [],
|
|
"FOMC": [],
|
|
}
|
|
|
|
# Sequential task training
|
|
model = base_model
|
|
for task_idx, task in enumerate(tasks):
|
|
print(f"\n{'=' * 80}")
|
|
print(f"TASK {task_idx + 1}: {task['name']}")
|
|
print(f"Effective Rank: {task['effective_rank']} (preserving {task['effective_rank'] * 100:.0f}%)")
|
|
print(f"{'=' * 80}")
|
|
|
|
# Configure OSF for this task
|
|
config = OSFConfig(
|
|
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
|
|
effective_rank=task["effective_rank"],
|
|
)
|
|
|
|
# Apply OSF to the model
|
|
model = get_peft_model(model, config)
|
|
|
|
# Training arguments
|
|
training_args = TrainingArguments(
|
|
output_dir=f"{output_dir}/osf_{task['name'].lower()}",
|
|
num_train_epochs=num_epochs,
|
|
per_device_train_batch_size=batch_size,
|
|
gradient_accumulation_steps=gradient_accumulation_steps,
|
|
learning_rate=learning_rate,
|
|
logging_steps=10,
|
|
save_strategy="no",
|
|
bf16=True,
|
|
remove_unused_columns=False,
|
|
)
|
|
|
|
# Train on current task
|
|
trainer = Trainer(
|
|
model=model,
|
|
args=training_args,
|
|
train_dataset=task["train"],
|
|
data_collator=data_collator,
|
|
)
|
|
|
|
print(f"\nTraining on {task['name']}...")
|
|
trainer.train()
|
|
|
|
# Evaluate on all tasks seen so far
|
|
print(f"\nEvaluating on all tasks after training on {task['name']}:")
|
|
for eval_task_idx in range(task_idx + 1):
|
|
eval_task = tasks[eval_task_idx]
|
|
loss, accuracy = evaluate_model(
|
|
model, eval_task["eval"], data_collator, tokenizer, eval_task["name"], eval_task["type"]
|
|
)
|
|
eval_history[eval_task["name"]].append((loss, accuracy))
|
|
|
|
# Unload OSF to get the updated base model for next task (if not last task)
|
|
if task_idx < len(tasks) - 1:
|
|
print("\nUnloading OSF adapter to prepare for next task...")
|
|
model = model.unload()
|
|
|
|
# Save final model
|
|
final_model_path = f"{output_dir}/osf_final"
|
|
model.save_pretrained(final_model_path)
|
|
print(f"\nFinal OSF model saved to {final_model_path}")
|
|
|
|
return eval_history
|
|
|
|
|
|
def train_full_finetuning(
|
|
model_name,
|
|
num_train,
|
|
num_eval,
|
|
output_dir,
|
|
num_epochs,
|
|
learning_rate,
|
|
batch_size,
|
|
gradient_accumulation_steps,
|
|
max_length,
|
|
seed,
|
|
):
|
|
"""Train using standard full fine-tuning (baseline for comparison)."""
|
|
print("\n" + "=" * 80)
|
|
print("TRAINING WITH FULL FINE-TUNING (Baseline)")
|
|
print("=" * 80)
|
|
|
|
# Load tokenizer
|
|
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
|
tokenizer.pad_token = tokenizer.eos_token
|
|
|
|
# Load all datasets with task-specific sizes
|
|
# FOMC only has 496 samples total, so we use 350 train + 146 eval for it
|
|
print("\nLoading datasets...")
|
|
scienceqa_train, scienceqa_eval = load_scienceqa(1000, 200, seed)
|
|
numglue_train, numglue_eval = load_numglue(1000, 200, seed)
|
|
fomc_train, fomc_eval = load_fomc(350, 146, seed)
|
|
|
|
# Store original eval datasets
|
|
scienceqa_eval_original = scienceqa_eval
|
|
numglue_eval_original = numglue_eval
|
|
fomc_eval_original = fomc_eval
|
|
|
|
# Format datasets
|
|
scienceqa_train = scienceqa_train.map(
|
|
lambda x: format_scienceqa_for_llama(x, tokenizer, max_length),
|
|
batched=True,
|
|
remove_columns=scienceqa_train.column_names,
|
|
)
|
|
scienceqa_eval = scienceqa_eval.map(
|
|
lambda x: format_scienceqa_for_llama(x, tokenizer, max_length),
|
|
batched=True,
|
|
remove_columns=scienceqa_eval.column_names,
|
|
)
|
|
|
|
numglue_train = numglue_train.map(
|
|
lambda x: format_numglue_for_llama(x, tokenizer, max_length),
|
|
batched=True,
|
|
remove_columns=numglue_train.column_names,
|
|
)
|
|
numglue_eval = numglue_eval.map(
|
|
lambda x: format_numglue_for_llama(x, tokenizer, max_length),
|
|
batched=True,
|
|
remove_columns=numglue_eval.column_names,
|
|
)
|
|
|
|
fomc_train = fomc_train.map(
|
|
lambda x: format_fomc_for_llama(x, tokenizer, max_length), batched=True, remove_columns=fomc_train.column_names
|
|
)
|
|
fomc_eval = fomc_eval.map(
|
|
lambda x: format_fomc_for_llama(x, tokenizer, max_length), batched=True, remove_columns=fomc_eval.column_names
|
|
)
|
|
|
|
data_collator = DataCollatorForCompletionOnly(tokenizer, max_length)
|
|
|
|
tasks = [
|
|
{"name": "ScienceQA", "train": scienceqa_train, "eval": scienceqa_eval, "type": "scienceqa"},
|
|
{"name": "NumGLUE", "train": numglue_train, "eval": numglue_eval, "type": "numglue"},
|
|
{"name": "FOMC", "train": fomc_train, "eval": fomc_eval, "type": "fomc"},
|
|
]
|
|
|
|
# Store evaluation history
|
|
eval_history = {
|
|
"ScienceQA": [],
|
|
"NumGLUE": [],
|
|
"FOMC": [],
|
|
}
|
|
|
|
# Load base model once
|
|
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.bfloat16, device_map="auto")
|
|
|
|
# Sequential task training
|
|
for task_idx, task in enumerate(tasks):
|
|
print(f"\n{'=' * 80}")
|
|
print(f"TASK {task_idx + 1}: {task['name']}")
|
|
print(f"{'=' * 80}")
|
|
|
|
# Training arguments
|
|
training_args = TrainingArguments(
|
|
output_dir=f"{output_dir}/full_{task['name'].lower()}",
|
|
num_train_epochs=num_epochs,
|
|
per_device_train_batch_size=batch_size,
|
|
gradient_accumulation_steps=gradient_accumulation_steps,
|
|
learning_rate=learning_rate,
|
|
logging_steps=10,
|
|
save_strategy="no",
|
|
bf16=True,
|
|
remove_unused_columns=False,
|
|
)
|
|
|
|
# Train on current task
|
|
trainer = Trainer(
|
|
model=model,
|
|
args=training_args,
|
|
train_dataset=task["train"],
|
|
data_collator=data_collator,
|
|
)
|
|
|
|
print(f"\nTraining on {task['name']}...")
|
|
trainer.train()
|
|
|
|
# Evaluate on all tasks seen so far
|
|
print(f"\nEvaluating on all tasks after training on {task['name']}:")
|
|
for eval_task_idx in range(task_idx + 1):
|
|
eval_task = tasks[eval_task_idx]
|
|
loss, accuracy = evaluate_model(
|
|
model, eval_task["eval"], data_collator, tokenizer, eval_task["name"], eval_task["type"]
|
|
)
|
|
eval_history[eval_task["name"]].append((loss, accuracy))
|
|
|
|
# Save final model
|
|
final_model_path = f"{output_dir}/full_final"
|
|
model.save_pretrained(final_model_path)
|
|
print(f"\nFinal full fine-tuning model saved to {final_model_path}")
|
|
|
|
return eval_history
|
|
|
|
|
|
def print_results_comparison(osf_history, full_history):
|
|
"""Print comparison table of OSF vs Full Fine-tuning."""
|
|
print("\n" + "=" * 80)
|
|
print("RESULTS COMPARISON: OSF vs Full Fine-tuning")
|
|
print("=" * 80)
|
|
|
|
tasks = ["ScienceQA", "NumGLUE", "FOMC"]
|
|
|
|
# Print detailed results
|
|
print("\n" + "-" * 80)
|
|
print("DETAILED RESULTS (Accuracy %)")
|
|
print("-" * 80)
|
|
print(f"{'Task':<15} {'After Task':<15} {'OSF Acc %':<15} {'Full FT Acc %':<15} {'Difference':<15}")
|
|
print("-" * 80)
|
|
|
|
for task_idx, task in enumerate(tasks):
|
|
for eval_after_idx in range(task_idx, len(tasks)):
|
|
eval_after = tasks[eval_after_idx]
|
|
osf_acc = osf_history[task][eval_after_idx - task_idx][1] * 100
|
|
full_acc = full_history[task][eval_after_idx - task_idx][1] * 100
|
|
diff = osf_acc - full_acc
|
|
|
|
print(
|
|
f"{task:<15} {eval_after:<15} {osf_acc:<15.2f} {full_acc:<15.2f} {diff:+15.2f}{' (OSF better)' if diff > 0 else ''}"
|
|
)
|
|
|
|
# Summary statistics
|
|
print("\n" + "=" * 80)
|
|
print("SUMMARY METRICS")
|
|
print("=" * 80)
|
|
|
|
# Final average accuracy across all 3 tasks
|
|
osf_final_accs = [osf_history[task][-1][1] * 100 for task in tasks]
|
|
full_final_accs = [full_history[task][-1][1] * 100 for task in tasks]
|
|
|
|
osf_avg_final = sum(osf_final_accs) / len(osf_final_accs)
|
|
full_avg_final = sum(full_final_accs) / len(full_final_accs)
|
|
|
|
print("\n1. Average Accuracy Across All 3 Tasks (After Final Task):")
|
|
print(f" OSF: {osf_avg_final:.2f}%")
|
|
print(f" Full FT: {full_avg_final:.2f}%")
|
|
print(
|
|
f" Difference: {osf_avg_final - full_avg_final:+.2f}% {'(OSF better)' if osf_avg_final > full_avg_final else '(Full FT better)'}"
|
|
)
|
|
|
|
# Average forgetting (for tasks 1 and 2 only, since task 3 is the final task)
|
|
print("\n2. Average Forgetting (Task 1 & 2):")
|
|
print(" Forgetting = Final Accuracy - Initial Accuracy (negative is worse)\n")
|
|
|
|
osf_forgetting_vals = []
|
|
full_forgetting_vals = []
|
|
|
|
for task_idx, task in enumerate(tasks[:-1]): # Exclude last task
|
|
osf_initial_acc = osf_history[task][0][1] * 100 # Right after learning task
|
|
osf_final_acc = osf_history[task][-1][1] * 100 # After learning all tasks
|
|
osf_forgetting = osf_final_acc - osf_initial_acc
|
|
|
|
full_initial_acc = full_history[task][0][1] * 100
|
|
full_final_acc = full_history[task][-1][1] * 100
|
|
full_forgetting = full_final_acc - full_initial_acc
|
|
|
|
osf_forgetting_vals.append(osf_forgetting)
|
|
full_forgetting_vals.append(full_forgetting)
|
|
|
|
print(f" {task}:")
|
|
print(f" OSF: {osf_forgetting:+.2f}% (initial: {osf_initial_acc:.2f}% → final: {osf_final_acc:.2f}%)")
|
|
print(
|
|
f" Full FT: {full_forgetting:+.2f}% (initial: {full_initial_acc:.2f}% → final: {full_final_acc:.2f}%)"
|
|
)
|
|
print(
|
|
f" Difference: {osf_forgetting - full_forgetting:+.2f}% {'(OSF better)' if osf_forgetting > full_forgetting else '(Full FT better)'}\n"
|
|
)
|
|
|
|
osf_avg_forgetting = sum(osf_forgetting_vals) / len(osf_forgetting_vals)
|
|
full_avg_forgetting = sum(full_forgetting_vals) / len(full_forgetting_vals)
|
|
|
|
print(" Average Forgetting:")
|
|
print(f" OSF: {osf_avg_forgetting:+.2f}%")
|
|
print(f" Full FT: {full_avg_forgetting:+.2f}%")
|
|
print(
|
|
f" Difference: {osf_avg_forgetting - full_avg_forgetting:+.2f}% {'(OSF better)' if osf_avg_forgetting > full_avg_forgetting else '(Full FT better)'}"
|
|
)
|
|
|
|
print("\n" + "=" * 80)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="OSF Continual Learning Example")
|
|
parser.add_argument(
|
|
"--model_name",
|
|
type=str,
|
|
default="meta-llama/Llama-3.1-8B-Instruct",
|
|
help="Model name or path",
|
|
)
|
|
parser.add_argument("--num_train", type=int, default=1000, help="Number of training samples per task")
|
|
parser.add_argument("--num_eval", type=int, default=200, help="Number of evaluation samples per task")
|
|
parser.add_argument("--output_dir", type=str, default="./osf_continual_learning_outputs", help="Output directory")
|
|
parser.add_argument("--num_epochs", type=int, default=2, help="Number of training epochs per task")
|
|
parser.add_argument("--learning_rate", type=float, default=5e-6, help="Learning rate")
|
|
parser.add_argument("--batch_size", type=int, default=32, help="Batch size per device")
|
|
parser.add_argument("--gradient_accumulation_steps", type=int, default=1, help="Gradient accumulation steps")
|
|
parser.add_argument("--max_length", type=int, default=512, help="Maximum sequence length")
|
|
parser.add_argument("--seed", type=int, default=42, help="Random seed")
|
|
parser.add_argument(
|
|
"--run_baseline",
|
|
action="store_true",
|
|
help="Also run full fine-tuning baseline for comparison",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Create output directory
|
|
os.makedirs(args.output_dir, exist_ok=True)
|
|
|
|
# Train with OSF
|
|
osf_history = train_with_osf(
|
|
args.model_name,
|
|
args.num_train,
|
|
args.num_eval,
|
|
args.output_dir,
|
|
args.num_epochs,
|
|
args.learning_rate,
|
|
args.batch_size,
|
|
args.gradient_accumulation_steps,
|
|
args.max_length,
|
|
args.seed,
|
|
)
|
|
|
|
# Optionally train with full fine-tuning baseline
|
|
if args.run_baseline:
|
|
full_history = train_full_finetuning(
|
|
args.model_name,
|
|
args.num_train,
|
|
args.num_eval,
|
|
args.output_dir,
|
|
args.num_epochs,
|
|
args.learning_rate,
|
|
args.batch_size,
|
|
args.gradient_accumulation_steps,
|
|
args.max_length,
|
|
args.seed,
|
|
)
|
|
|
|
# Print comparison
|
|
print_results_comparison(osf_history, full_history)
|
|
else:
|
|
print("\n" + "=" * 80)
|
|
print("OSF TRAINING COMPLETE")
|
|
print("=" * 80)
|
|
print("\nTo compare with full fine-tuning baseline, run with --run_baseline flag")
|
|
|
|
# Print OSF-only summary
|
|
tasks = ["ScienceQA", "NumGLUE", "FOMC"]
|
|
print("\n" + "=" * 80)
|
|
print("OSF SUMMARY METRICS")
|
|
print("=" * 80)
|
|
|
|
# Final average accuracy
|
|
osf_final_accs = [osf_history[task][-1][1] * 100 for task in tasks]
|
|
osf_avg_final = sum(osf_final_accs) / len(osf_final_accs)
|
|
|
|
print(f"\n1. Average Accuracy Across All 3 Tasks (After Final Task): {osf_avg_final:.2f}%")
|
|
for task, acc in zip(tasks, osf_final_accs):
|
|
print(f" {task}: {acc:.2f}%")
|
|
|
|
# Average forgetting
|
|
print("\n2. Average Forgetting (Task 1 & 2):")
|
|
osf_forgetting_vals = []
|
|
for task_idx, task in enumerate(tasks[:-1]):
|
|
osf_initial_acc = osf_history[task][0][1] * 100
|
|
osf_final_acc = osf_history[task][-1][1] * 100
|
|
osf_forgetting = osf_initial_acc - osf_final_acc
|
|
osf_forgetting_vals.append(osf_forgetting)
|
|
|
|
print(f" {task}: {osf_forgetting:+.2f}% (initial: {osf_initial_acc:.2f}% → final: {osf_final_acc:.2f}%)")
|
|
|
|
osf_avg_forgetting = sum(osf_forgetting_vals) / len(osf_forgetting_vals)
|
|
print(f" Average: {osf_avg_forgetting:+.2f}%")
|
|
print("\n" + "=" * 80)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|