| """ |
| CLARA Trainer Module |
| |
| Handles training, validation, and evaluation of CLARA models. |
| """ |
|
|
| import os |
| import json |
| import torch |
| import torch.nn as nn |
| from torch.optim import AdamW |
| from torch.optim.lr_scheduler import CosineAnnealingLR |
| from torch.utils.data import DataLoader |
| from tqdm.auto import tqdm |
| from typing import Dict, Optional, List |
| import numpy as np |
| from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score |
|
|
|
|
| class CLARATrainer: |
| """Trainer for CLARA model""" |
| |
| def __init__( |
| self, |
| model: nn.Module, |
| train_loader: DataLoader, |
| val_loader: DataLoader, |
| output_dir: str, |
| learning_rate: float = 2e-5, |
| weight_decay: float = 0.01, |
| warmup_ratio: float = 0.1, |
| max_grad_norm: float = 1.0, |
| device: Optional[str] = None, |
| label_smoothing: float = 0.1, |
| early_stopping_patience: int = 10 |
| ): |
| """ |
| Args: |
| model: CLARA model |
| train_loader: Training dataloader |
| val_loader: Validation dataloader |
| output_dir: Directory to save checkpoints and logs |
| learning_rate: Learning rate |
| weight_decay: Weight decay for AdamW |
| warmup_ratio: Ratio of warmup steps |
| max_grad_norm: Maximum gradient norm for clipping |
| device: Device to train on ('cuda' or 'cpu') |
| label_smoothing: Label smoothing factor |
| early_stopping_patience: Patience for early stopping |
| """ |
| self.model = model |
| self.train_loader = train_loader |
| self.val_loader = val_loader |
| self.output_dir = output_dir |
| self.max_grad_norm = max_grad_norm |
| self.early_stopping_patience = early_stopping_patience |
| |
| |
| if device is None: |
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| else: |
| self.device = torch.device(device) |
| |
| self.model = self.model.to(self.device) |
| |
| |
| self.optimizer = AdamW( |
| self.model.parameters(), |
| lr=learning_rate, |
| weight_decay=weight_decay |
| ) |
| |
| |
| self.criterion = nn.CrossEntropyLoss(label_smoothing=label_smoothing) |
| |
| |
| self.best_val_f1 = 0.0 |
| self.best_epoch = 0 |
| self.epochs_without_improvement = 0 |
| self.history = { |
| 'train_loss': [], |
| 'train_acc': [], |
| 'train_f1': [], |
| 'val_loss': [], |
| 'val_acc': [], |
| 'val_f1': [] |
| } |
| |
| |
| os.makedirs(output_dir, exist_ok=True) |
| |
| def train_epoch(self) -> Dict[str, float]: |
| """Train for one epoch""" |
| self.model.train() |
| |
| total_loss = 0.0 |
| all_preds = [] |
| all_labels = [] |
| |
| pbar = tqdm(self.train_loader, desc="Training") |
| for batch in pbar: |
| |
| pixel_values = batch['pixel_values'].to(self.device) |
| input_ids = batch['input_ids'].to(self.device) |
| attention_mask = batch['attention_mask'].to(self.device) |
| labels = batch['label'].to(self.device) |
| |
| |
| outputs = self.model(pixel_values, input_ids, attention_mask) |
| loss = self.criterion(outputs['logits'], labels) |
| |
| |
| self.optimizer.zero_grad() |
| loss.backward() |
| |
| |
| torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.max_grad_norm) |
| |
| self.optimizer.step() |
| |
| |
| total_loss += loss.item() |
| preds = torch.argmax(outputs['logits'], dim=-1) |
| all_preds.extend(preds.cpu().numpy()) |
| all_labels.extend(labels.cpu().numpy()) |
| |
| |
| pbar.set_postfix({'loss': loss.item()}) |
| |
| |
| avg_loss = total_loss / len(self.train_loader) |
| accuracy = accuracy_score(all_labels, all_preds) |
| f1 = f1_score(all_labels, all_preds, average='weighted') |
| |
| return { |
| 'loss': avg_loss, |
| 'accuracy': accuracy, |
| 'f1': f1 |
| } |
| |
| def validate(self) -> Dict[str, float]: |
| """Validate model""" |
| self.model.eval() |
| |
| total_loss = 0.0 |
| all_preds = [] |
| all_labels = [] |
| |
| with torch.no_grad(): |
| for batch in tqdm(self.val_loader, desc="Validation"): |
| |
| pixel_values = batch['pixel_values'].to(self.device) |
| input_ids = batch['input_ids'].to(self.device) |
| attention_mask = batch['attention_mask'].to(self.device) |
| labels = batch['label'].to(self.device) |
| |
| |
| outputs = self.model(pixel_values, input_ids, attention_mask) |
| loss = self.criterion(outputs['logits'], labels) |
| |
| |
| total_loss += loss.item() |
| preds = torch.argmax(outputs['logits'], dim=-1) |
| all_preds.extend(preds.cpu().numpy()) |
| all_labels.extend(labels.cpu().numpy()) |
| |
| |
| avg_loss = total_loss / len(self.val_loader) |
| accuracy = accuracy_score(all_labels, all_preds) |
| f1 = f1_score(all_labels, all_preds, average='weighted') |
| |
| return { |
| 'loss': avg_loss, |
| 'accuracy': accuracy, |
| 'f1': f1 |
| } |
| |
| def train( |
| self, |
| num_epochs: int, |
| scheduler: Optional[any] = None |
| ) -> Dict[str, List[float]]: |
| """ |
| Train model for multiple epochs |
| |
| Args: |
| num_epochs: Number of epochs to train |
| scheduler: Optional learning rate scheduler |
| |
| Returns: |
| Training history |
| """ |
| print(f"Training on device: {self.device}") |
| print(f"Total epochs: {num_epochs}") |
| print(f"Train batches: {len(self.train_loader)}") |
| print(f"Val batches: {len(self.val_loader)}") |
| print("-" * 50) |
| |
| |
| if scheduler is None: |
| scheduler = CosineAnnealingLR(self.optimizer, T_max=num_epochs) |
| |
| for epoch in range(num_epochs): |
| print(f"\nEpoch {epoch + 1}/{num_epochs}") |
| print("-" * 50) |
| |
| |
| train_metrics = self.train_epoch() |
| |
| |
| val_metrics = self.validate() |
| |
| |
| scheduler.step() |
| |
| |
| print(f"Train Loss: {train_metrics['loss']:.4f} | " |
| f"Train Acc: {train_metrics['accuracy']:.4f} | " |
| f"Train F1: {train_metrics['f1']:.4f}") |
| print(f"Val Loss: {val_metrics['loss']:.4f} | " |
| f"Val Acc: {val_metrics['accuracy']:.4f} | " |
| f"Val F1: {val_metrics['f1']:.4f}") |
| |
| |
| self.history['train_loss'].append(train_metrics['loss']) |
| self.history['train_acc'].append(train_metrics['accuracy']) |
| self.history['train_f1'].append(train_metrics['f1']) |
| self.history['val_loss'].append(val_metrics['loss']) |
| self.history['val_acc'].append(val_metrics['accuracy']) |
| self.history['val_f1'].append(val_metrics['f1']) |
| |
| |
| if val_metrics['f1'] > self.best_val_f1: |
| self.best_val_f1 = val_metrics['f1'] |
| self.best_epoch = epoch + 1 |
| self.epochs_without_improvement = 0 |
| |
| |
| checkpoint_path = os.path.join(self.output_dir, "best_model.pt") |
| self.model.save_pretrained( |
| checkpoint_path, |
| epoch=epoch + 1, |
| best_val_f1=self.best_val_f1, |
| optimizer_state_dict=self.optimizer.state_dict() |
| ) |
| print(f"✅ Saved best model with F1: {self.best_val_f1:.4f}") |
| else: |
| self.epochs_without_improvement += 1 |
| |
| |
| if self.epochs_without_improvement >= self.early_stopping_patience: |
| print(f"\n⚠️ Early stopping triggered after {epoch + 1} epochs") |
| print(f"Best F1: {self.best_val_f1:.4f} at epoch {self.best_epoch}") |
| break |
| |
| |
| history_path = os.path.join(self.output_dir, "training_history.json") |
| with open(history_path, 'w') as f: |
| json.dump(self.history, f, indent=2) |
| |
| print("\n" + "=" * 50) |
| print(f"Training completed!") |
| print(f"Best F1: {self.best_val_f1:.4f} at epoch {self.best_epoch}") |
| print(f"Checkpoints saved to: {self.output_dir}") |
| print("=" * 50) |
| |
| return self.history |
| |
| def evaluate(self, test_loader: DataLoader) -> Dict[str, any]: |
| """ |
| Evaluate model on test set |
| |
| Args: |
| test_loader: Test dataloader |
| |
| Returns: |
| Dictionary with evaluation metrics |
| """ |
| self.model.eval() |
| |
| all_preds = [] |
| all_labels = [] |
| all_probs = [] |
| |
| with torch.no_grad(): |
| for batch in tqdm(test_loader, desc="Evaluating"): |
| |
| pixel_values = batch['pixel_values'].to(self.device) |
| input_ids = batch['input_ids'].to(self.device) |
| attention_mask = batch['attention_mask'].to(self.device) |
| labels = batch['label'].to(self.device) |
| |
| |
| outputs = self.model(pixel_values, input_ids, attention_mask) |
| probs = torch.softmax(outputs['logits'], dim=-1) |
| preds = torch.argmax(probs, dim=-1) |
| |
| all_preds.extend(preds.cpu().numpy()) |
| all_labels.extend(labels.cpu().numpy()) |
| all_probs.extend(probs.cpu().numpy()) |
| |
| |
| accuracy = accuracy_score(all_labels, all_preds) |
| f1_weighted = f1_score(all_labels, all_preds, average='weighted') |
| f1_macro = f1_score(all_labels, all_preds, average='macro') |
| precision = precision_score(all_labels, all_preds, average='weighted') |
| recall = recall_score(all_labels, all_preds, average='weighted') |
| |
| |
| f1_per_class = f1_score(all_labels, all_preds, average=None) |
| precision_per_class = precision_score(all_labels, all_preds, average=None) |
| recall_per_class = recall_score(all_labels, all_preds, average=None) |
| |
| results = { |
| 'accuracy': accuracy, |
| 'f1_weighted': f1_weighted, |
| 'f1_macro': f1_macro, |
| 'precision': precision, |
| 'recall': recall, |
| 'f1_per_class': f1_per_class.tolist(), |
| 'precision_per_class': precision_per_class.tolist(), |
| 'recall_per_class': recall_per_class.tolist(), |
| 'predictions': all_preds, |
| 'labels': all_labels, |
| 'probabilities': all_probs |
| } |
| |
| |
| print("\n" + "=" * 50) |
| print("EVALUATION RESULTS") |
| print("=" * 50) |
| print(f"Accuracy: {accuracy:.4f}") |
| print(f"Weighted F1: {f1_weighted:.4f}") |
| print(f"Macro F1: {f1_macro:.4f}") |
| print(f"Precision: {precision:.4f}") |
| print(f"Recall: {recall:.4f}") |
| print("\nPer-class F1 scores:") |
| for i, f1 in enumerate(f1_per_class): |
| print(f" Class {i}: {f1:.4f}") |
| print("=" * 50) |
| |
| return results |
|
|