smartlearn / src /core /fine_tuning.py
Teja Chowdary
Fix evaluate import issue - add to requirements.txt and make import conditional
7b7b8ea
Raw
History Blame Contribute Delete
18.7 kB
"""
Fine-tuning pipeline for SmartLearn AI models.
This module handles data collection, preparation, training, and evaluation.
"""
import json
import os
import pickle
from datetime import datetime
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass, asdict
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_recall_fscore_support
import torch
from torch.utils.data import Dataset, DataLoader
from transformers import (
AutoTokenizer, AutoModelForCausalLM,
TrainingArguments, Trainer,
DataCollatorForLanguageModeling
)
# Conditional import for cloud compatibility
try:
import evaluate
EVALUATE_AVAILABLE = True
except ImportError:
EVALUATE_AVAILABLE = False
print("⚠️ evaluate library not available - evaluation metrics will be limited")
@dataclass
class TrainingExample:
"""Represents a single training example."""
input_text: str
target_text: str
subject: str
difficulty: str
user_rating: Optional[float] = None
timestamp: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
@dataclass
class TrainingMetrics:
"""Training and evaluation metrics."""
accuracy: float
precision: float
recall: float
f1_score: float
loss: float
perplexity: float
training_time: float
timestamp: str
class SmartLearnDataset(Dataset):
"""Custom dataset for SmartLearn training data."""
def __init__(self, examples: List[TrainingExample], tokenizer, max_length: int = 512):
self.examples = examples
self.tokenizer = tokenizer
self.max_length = max_length
def __len__(self):
return len(self.examples)
def __getitem__(self, idx):
example = self.examples[idx]
# Combine input and target
full_text = f"Input: {example.input_text}\nOutput: {example.target_text}"
# Tokenize
encoding = self.tokenizer(
full_text,
truncation=True,
padding="max_length",
max_length=self.max_length,
return_tensors="pt"
)
return {
"input_ids": encoding["input_ids"].squeeze(),
"attention_mask": encoding["attention_mask"].squeeze(),
"labels": encoding["input_ids"].squeeze()
}
class DataCollector:
"""Collects and manages training data from user interactions."""
def __init__(self, data_dir: str = "data/training"):
self.data_dir = data_dir
os.makedirs(data_dir, exist_ok=True)
self.examples: List[TrainingExample] = []
self.load_existing_data()
def add_example(self, example: TrainingExample):
"""Add a new training example."""
self.examples.append(example)
self.save_data()
def add_batch_examples(self, examples: List[TrainingExample]):
"""Add multiple training examples at once."""
self.examples.extend(examples)
self.save_data()
def load_existing_data(self):
"""Load existing training data from disk."""
data_file = os.path.join(self.data_dir, "training_data.json")
if os.path.exists(data_file):
try:
with open(data_file, 'r') as f:
data = json.load(f)
self.examples = [TrainingExample(**ex) for ex in data]
print(f"✅ Loaded {len(self.examples)} existing training examples")
except Exception as e:
print(f"❌ Error loading training data: {e}")
self.examples = []
def save_data(self):
"""Save training data to disk."""
data_file = os.path.join(self.data_dir, "training_data.json")
try:
with open(data_file, 'w') as f:
json.dump([asdict(ex) for ex in self.examples], f, indent=2)
except Exception as e:
print(f"❌ Error saving training data: {e}")
def get_subject_data(self, subject: str) -> List[TrainingExample]:
"""Get training examples for a specific subject."""
return [ex for ex in self.examples if ex.subject.lower() == subject.lower()]
def get_difficulty_data(self, difficulty: str) -> List[TrainingExample]:
"""Get training examples for a specific difficulty level."""
return [ex for ex in self.examples if ex.difficulty.lower() == difficulty.lower()]
def get_statistics(self) -> Dict[str, Any]:
"""Get statistics about the training data."""
if not self.examples:
return {"total_examples": 0}
subjects = {}
difficulties = {}
ratings = []
for ex in self.examples:
subjects[ex.subject] = subjects.get(ex.subject, 0) + 1
difficulties[ex.difficulty] = difficulties.get(ex.difficulty, 0) + 1
if ex.user_rating:
ratings.append(ex.user_rating)
return {
"total_examples": len(self.examples),
"subjects": subjects,
"difficulties": difficulties,
"avg_rating": np.mean(ratings) if ratings else 0,
"total_ratings": len(ratings)
}
class DataPreprocessor:
"""Preprocesses training data for fine-tuning."""
def __init__(self, tokenizer, max_length: int = 512):
self.tokenizer = tokenizer
self.max_length = max_length
def preprocess_examples(self, examples: List[TrainingExample]) -> Tuple[List, List]:
"""Preprocess examples into input and target sequences."""
inputs = []
targets = []
for example in examples:
# Format input
input_text = f"Subject: {example.subject}\nDifficulty: {example.difficulty}\nQuery: {example.input_text}"
inputs.append(input_text)
# Format target
target_text = example.target_text
targets.append(target_text)
return inputs, targets
def create_training_data(self, examples: List[TrainingExample]) -> SmartLearnDataset:
"""Create training dataset from examples."""
return SmartLearnDataset(examples, self.tokenizer, self.max_length)
def split_data(self, examples: List[TrainingExample],
train_ratio: float = 0.8, val_ratio: float = 0.1) -> Tuple[List, List, List]:
"""Split data into train/validation/test sets."""
total = len(examples)
train_size = int(total * train_ratio)
val_size = int(total * val_ratio)
# Shuffle examples
np.random.shuffle(examples)
train_examples = examples[:train_size]
val_examples = examples[train_size:train_size + val_size]
test_examples = examples[train_size + val_size:]
return train_examples, val_examples, test_examples
class FineTuner:
"""Handles the fine-tuning process."""
def __init__(self, base_model: str = "microsoft/DialoGPT-medium",
output_dir: str = "models/fine_tuned"):
self.base_model = base_model
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
self.tokenizer = None
self.model = None
self.trainer = None
self.load_model()
def load_model(self):
"""Load the base model and tokenizer."""
try:
print(f"🔄 Loading base model: {self.base_model}")
self.tokenizer = AutoTokenizer.from_pretrained(self.base_model)
self.model = AutoModelForCausalLM.from_pretrained(self.base_model)
# Add padding token if not present
if self.tokenizer.pad_token is None:
self.tokenizer.pad_token = self.tokenizer.eos_token
print("✅ Model loaded successfully")
except Exception as e:
print(f"❌ Error loading model: {e}")
raise
def prepare_training_data(self, examples: List[TrainingExample]) -> Tuple[SmartLearnDataset, SmartLearnDataset]:
"""Prepare training and validation datasets."""
preprocessor = DataPreprocessor(self.tokenizer)
train_examples, val_examples, _ = preprocessor.split_data(examples)
train_dataset = preprocessor.create_training_data(train_examples)
val_dataset = preprocessor.create_training_data(val_examples)
return train_dataset, val_dataset
def setup_training(self, train_dataset: SmartLearnDataset, val_dataset: SmartLearnDataset):
"""Setup training configuration."""
training_args = TrainingArguments(
output_dir=self.output_dir,
num_train_epochs=1, # Reduced for memory
per_device_train_batch_size=1, # Minimal batch size
per_device_eval_batch_size=1,
gradient_accumulation_steps=8, # Add gradient accumulation
warmup_steps=50,
weight_decay=0.01,
logging_dir=f"{self.output_dir}/logs",
logging_steps=50,
eval_strategy="steps",
eval_steps=200,
save_steps=200, # Must be multiple of eval_steps
save_total_limit=1,
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
greater_is_better=False,
dataloader_pin_memory=False, # Disable for M3
dataloader_num_workers=0, # Single worker
)
data_collator = DataCollatorForLanguageModeling(
tokenizer=self.tokenizer,
mlm=False
)
self.trainer = Trainer(
model=self.model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=val_dataset,
data_collator=data_collator,
)
def train(self, examples: List[TrainingExample]) -> TrainingMetrics:
"""Execute the fine-tuning process."""
if not examples:
raise ValueError("No training examples provided")
print(f"🚀 Starting fine-tuning with {len(examples)} examples")
start_time = datetime.now()
# Prepare data
train_dataset, val_dataset = self.prepare_training_data(examples)
self.setup_training(train_dataset, val_dataset)
# Train
print("🔄 Training in progress...")
train_result = self.trainer.train()
# Evaluate
print("🔍 Evaluating model...")
eval_result = self.trainer.evaluate()
# Calculate metrics
end_time = datetime.now()
training_time = (end_time - start_time).total_seconds()
metrics = TrainingMetrics(
accuracy=eval_result.get("eval_loss", 0), # Simplified for now
precision=0.0, # Would need classification labels for proper calculation
recall=0.0,
f1_score=0.0,
loss=eval_result.get("eval_loss", 0),
perplexity=np.exp(eval_result.get("eval_loss", 0)),
training_time=training_time,
timestamp=datetime.now().isoformat()
)
# Save model
self.save_model()
print(f"✅ Fine-tuning completed in {training_time:.2f} seconds")
return metrics
def save_model(self):
"""Save the fine-tuned model."""
try:
model_path = os.path.join(self.output_dir, "final_model")
self.trainer.save_model(model_path)
self.tokenizer.save_pretrained(model_path)
print(f"✅ Model saved to {model_path}")
except Exception as e:
print(f"❌ Error saving model: {e}")
def load_fine_tuned_model(self, model_path: str):
"""Load a fine-tuned model."""
try:
full_path = os.path.join(self.output_dir, model_path)
self.model = AutoModelForCausalLM.from_pretrained(full_path)
self.tokenizer = AutoTokenizer.from_pretrained(full_path)
print(f"✅ Fine-tuned model loaded from {full_path}")
except Exception as e:
print(f"❌ Error loading fine-tuned model: {e}")
class ModelEvaluator:
"""Evaluates fine-tuned model performance."""
def __init__(self, model, tokenizer):
self.model = model
self.tokenizer = tokenizer
def evaluate_examples(self, test_examples: List[TrainingExample]) -> Dict[str, float]:
"""Evaluate model on test examples."""
if not test_examples:
return {}
predictions = []
targets = []
for example in test_examples:
# Generate prediction
input_text = f"Subject: {example.subject}\nDifficulty: {example.difficulty}\nQuery: {example.input_text}"
inputs = self.tokenizer(input_text, return_tensors="pt", truncation=True, max_length=512)
with torch.no_grad():
outputs = self.model.generate(
**inputs,
max_length=512,
num_return_sequences=1,
temperature=0.7,
do_sample=True,
pad_token_id=self.tokenizer.eos_token_id
)
prediction = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
predictions.append(prediction)
targets.append(example.target_text)
# Calculate metrics (simplified for now)
# In a real implementation, you'd use proper evaluation metrics
metrics = {
"num_examples": len(test_examples),
"avg_prediction_length": np.mean([len(p) for p in predictions]),
"avg_target_length": np.mean([len(t) for t in targets])
}
return metrics
class FineTuningPipeline:
"""Complete fine-tuning pipeline for SmartLearn."""
def __init__(self, base_model: str = "microsoft/DialoGPT-medium"):
self.data_collector = DataCollector()
self.fine_tuner = FineTuner(base_model)
self.evaluator = None
def collect_user_data(self, user_interactions: List[Dict[str, Any]]):
"""Collect training data from user interactions."""
examples = []
for interaction in user_interactions:
example = TrainingExample(
input_text=interaction.get("query", ""),
target_text=interaction.get("response", ""),
subject=interaction.get("subject", "general"),
difficulty=interaction.get("difficulty", "medium"),
user_rating=interaction.get("rating"),
timestamp=datetime.now().isoformat(),
metadata=interaction.get("metadata", {})
)
examples.append(example)
self.data_collector.add_batch_examples(examples)
print(f"✅ Collected {len(examples)} training examples")
def run_fine_tuning(self) -> TrainingMetrics:
"""Run the complete fine-tuning pipeline."""
examples = self.data_collector.examples
if len(examples) < 10:
raise ValueError(f"Need at least 10 training examples, got {len(examples)}")
print(f"🚀 Starting fine-tuning pipeline with {len(examples)} examples")
# Run fine-tuning
metrics = self.fine_tuner.train(examples)
# Setup evaluator
self.evaluator = ModelEvaluator(self.fine_tuner.model, self.fine_tuner.tokenizer)
return metrics
def evaluate_model(self) -> Dict[str, float]:
"""Evaluate the fine-tuned model."""
if not self.evaluator:
raise ValueError("Model not fine-tuned yet")
# Get test examples
_, _, test_examples = DataPreprocessor(self.fine_tuner.tokenizer).split_data(
self.data_collector.examples
)
return self.evaluator.evaluate_examples(test_examples)
def get_pipeline_status(self) -> Dict[str, Any]:
"""Get current status of the fine-tuning pipeline."""
data_stats = self.data_collector.get_statistics()
return {
"data_collection": data_stats,
"model_status": "fine_tuned" if self.evaluator else "not_fine_tuned",
"base_model": self.fine_tuner.base_model,
"output_directory": self.fine_tuner.output_dir
}
# Utility functions for data generation
def generate_synthetic_data(subject: str, num_examples: int = 50) -> List[TrainingExample]:
"""Generate synthetic training data for testing purposes."""
examples = []
# Template-based generation
templates = {
"mathematics": [
("Explain the concept of {topic}", "Here's a comprehensive explanation of {topic}..."),
("How do I solve {problem_type} problems?", "To solve {problem_type} problems, follow these steps..."),
("What is the difference between {concept1} and {concept2}?", "The key differences between {concept1} and {concept2} are...")
],
"computer_science": [
("How do I implement {algorithm}?", "Here's how to implement {algorithm}..."),
("Explain {concept} in programming", "In programming, {concept} refers to..."),
("What are the best practices for {topic}?", "Best practices for {topic} include...")
]
}
subject_templates = templates.get(subject.lower(), templates["mathematics"])
for i in range(num_examples):
template = subject_templates[i % len(subject_templates)]
example = TrainingExample(
input_text=template[0].format(
topic=f"topic_{i}",
problem_type=f"problem_type_{i}",
concept1=f"concept_{i}",
concept2=f"concept_{i+1}",
algorithm=f"algorithm_{i}",
concept=f"concept_{i}"
),
target_text=template[1].format(
topic=f"topic_{i}",
problem_type=f"problem_type_{i}",
concept1=f"concept_{i}",
concept2=f"concept_{i+1}",
algorithm=f"algorithm_{i}",
concept=f"concept_{i}"
),
subject=subject,
difficulty=["easy", "medium", "hard"][i % 3],
user_rating=np.random.uniform(3.5, 5.0),
timestamp=datetime.now().isoformat()
)
examples.append(example)
return examples