| import math |
| import numpy as np |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| import random |
| from tqdm import tqdm |
| from torch.utils.data import Dataset, DataLoader |
| from transformers import AutoTokenizer, AutoModel, AdamW, get_linear_schedule_with_warmup |
| from torch.cuda.amp import GradScaler |
| from contextlib import nullcontext |
| from typing import List, Dict, Optional |
| from pairadigm import Pairadigm |
| import json |
|
|
| |
| def set_seeds(seed: int = 42): |
| np.random.seed(seed) |
| torch.manual_seed(seed) |
| random.seed(seed) |
| torch.cuda.manual_seed_all(seed) |
| torch.backends.cudnn.deterministic = True |
| torch.backends.cudnn.benchmark = False |
|
|
| class RewardModel(nn.Module): |
| """ |
| Unified class for training and using a reward model for text scoring. |
| |
| This class handles: |
| - Model initialization and configuration |
| - Dataset creation and management |
| - Training loop with pairwise comparisons |
| - Scoring individual texts or batches |
| - Score normalization |
| """ |
| |
| def __init__( |
| self, |
| model_name: str = "roberta-large", |
| dropout: float = 0.1, |
| max_length: int = 384, |
| device: Optional[str] = None, |
| Pairadigm: Optional['Pairadigm'] = None, |
| seed: int = 42 |
| ): |
| """ |
| Initialize the reward model trainer. |
| |
| Args: |
| model_name: HuggingFace model identifier |
| dropout: Dropout rate for reward head |
| max_length: Maximum sequence length for tokenization |
| device: Device to use ('cuda', 'cpu', or None for auto-detect) |
| """ |
| super().__init__() |
| |
| set_seeds(seed) |
|
|
| self.model_name = model_name |
| self.max_length = max_length |
| if device: |
| self.device = device |
| elif torch.cuda.is_available(): |
| self.device = 'cuda' |
| elif torch.backends.mps.is_available(): |
| self.device = 'mps' |
| else: |
| self.device = 'cpu' |
| print(f"Model using device: {self.device}") |
| self.device_type = self.device.type if isinstance(self.device, torch.device) else self.device.split(':')[0] |
| self.use_amp = self.device_type == 'cuda' |
|
|
| |
| self.tokenizer = AutoTokenizer.from_pretrained(model_name) |
| |
| |
| self.model = self._build_model(dropout) |
| self.model.to(self.device) |
|
|
| |
| self.pairadigm = Pairadigm |
| |
| |
| self.optimizer = None |
| self.scheduler = None |
| |
| self.scaler = GradScaler(self.device_type, enabled=self.device_type == 'cuda') |
| |
| self.training_history = [] |
| |
| def _autocast_context(self): |
| if self.device_type == "cuda": |
| return torch.amp.autocast(device_type="cuda", dtype=torch.float16) |
| if self.device_type == "cpu": |
| return torch.amp.autocast(device_type="cpu", dtype=torch.bfloat16) |
| return nullcontext() |
|
|
| def _build_model(self, dropout: float): |
| class _EncoderWithHead(nn.Module): |
| |
| def __init__(inner_self, model_name, dropout): |
| super().__init__() |
| |
| inner_self.encoder = AutoModel.from_pretrained(model_name) |
| hidden_size = inner_self.encoder.config.hidden_size |
| inner_self.dropout = nn.Dropout(dropout) |
|
|
| inner_self.valence_head = nn.Linear(hidden_size, 1) |
| inner_self.arousal_dominance_head = nn.Linear(hidden_size, 2) |
|
|
| nn.init.xavier_uniform_(inner_self.valence_head.weight, gain=1.0) |
| nn.init.zeros_(inner_self.valence_head.bias) |
| nn.init.xavier_uniform_(inner_self.arousal_dominance_head.weight, gain=1.0) |
| nn.init.zeros_(inner_self.arousal_dominance_head.bias) |
|
|
| def forward(inner_self, input_ids, attention_mask): |
| outputs = inner_self.encoder(input_ids=input_ids, attention_mask=attention_mask) |
| |
| pooled_output = outputs.last_hidden_state[:, 0, :] |
| pooled_output = inner_self.dropout(pooled_output) |
|
|
| valence = inner_self.valence_head(pooled_output) |
| arousal_dominance = inner_self.arousal_dominance_head(pooled_output) |
|
|
| return valence, arousal_dominance |
|
|
| return _EncoderWithHead(self.model_name, dropout) |
| |
| class _PairwiseDataset(Dataset): |
| """Internal dataset class for pairs with V gold scores and A/D probabilities.""" |
| |
| def __init__(self, pairs, tokenizer, max_length): |
| self.pairs = pairs |
| self.tokenizer = tokenizer |
| self.max_length = max_length |
| |
| def __len__(self): |
| return len(self.pairs) |
| |
| def __getitem__(self, idx): |
| text_A, text_B, V_A, V_B, label_A, label_D = self.pairs[idx] |
| |
| encoding_A = self.tokenizer( |
| text_A, |
| max_length=self.max_length, |
| padding='max_length', |
| truncation=True, |
| return_tensors='pt' |
| ) |
| |
| encoding_B = self.tokenizer( |
| text_B, |
| max_length=self.max_length, |
| padding='max_length', |
| truncation=True, |
| return_tensors='pt' |
| ) |
| |
| return { |
| 'input_ids_A': encoding_A['input_ids'].squeeze(0), |
| 'attention_mask_A': encoding_A['attention_mask'].squeeze(0), |
| 'input_ids_B': encoding_B['input_ids'].squeeze(0), |
| 'attention_mask_B': encoding_B['attention_mask'].squeeze(0), |
| 'V_A': torch.tensor(V_A, dtype=torch.float), |
| 'V_B': torch.tensor(V_B, dtype=torch.float), |
| 'label_A': torch.tensor(label_A, dtype=torch.float), |
| 'label_D': torch.tensor(label_D, dtype=torch.float) |
| } |
| |
| def fit( |
| self, |
| train_loader: DataLoader, |
| eval_loader: DataLoader, |
| epochs: int = 5, |
| learning_rate: float = 2e-5, |
| weight_decay: float = 0.01, |
| warmup_steps: int = 100, |
| log_interval: int = 50, |
| early_stopping_patience: int = 3, |
| accumulation_steps: int = 1, |
| max_steps_per_epoch: int = None |
| ): |
| """ |
| Train the reward model on pairwise comparison data with optional early stopping and gradient accumulation. |
| |
| Args: |
| train_loader: DataLoader with training pairs (mini-batch size) |
| eval_loader: DataLoader for evaluation (required for early stopping) |
| epochs: Number of training epochs |
| learning_rate: Learning rate for optimizer |
| warmup_steps: Number of warmup steps for scheduler |
| log_interval: Log metrics every N steps |
| early_stopping_patience: Number of epochs with no improvement on eval loss before stopping early. |
| Set to None or 0 to disable early stopping. |
| accumulation_steps: Number of gradient accumulation steps (effective_batch_size = mini_batch_size * accumulation_steps) |
| Returns: |
| The model (self.model) restored to the best-performing weights observed on eval data. |
| """ |
| self.model.train() |
| |
| |
| self.optimizer = AdamW( |
| self.model.parameters(), |
| lr=learning_rate, |
| weight_decay=weight_decay |
| ) |
|
|
| steps_per_epoch = math.ceil(len(train_loader) / accumulation_steps) |
| if max_steps_per_epoch is not None: |
| steps_per_epoch = math.ceil(max_steps_per_epoch / accumulation_steps) |
|
|
| total_optimization_steps = steps_per_epoch * epochs |
|
|
| self.scheduler = get_linear_schedule_with_warmup( |
| self.optimizer, |
| num_warmup_steps=warmup_steps, |
| num_training_steps=total_optimization_steps |
| ) |
|
|
| |
| best_state = None |
| best_eval_loss = float('inf') |
| epochs_without_improve = 0 |
| use_early_stopping = bool(early_stopping_patience and eval_loader is not None and early_stopping_patience > 0) |
| |
| for epoch in range(epochs): |
| |
| epoch_loss = 0 |
| grad_norms = [] |
|
|
| progress_bar = tqdm(train_loader, desc=f"Epoch {epoch + 1}/{epochs}") |
| grad_norm = 0.0 |
| |
| for step, batch in enumerate(progress_bar): |
| |
| loss = self._training_step(batch, accumulation_steps) |
| epoch_loss += loss |
|
|
| |
| is_accum_step = (step + 1) % accumulation_steps == 0 |
| is_last_batch = (step + 1) == len(train_loader) |
| is_max_step = max_steps_per_epoch is not None and (step + 1) == max_steps_per_epoch |
|
|
| if is_accum_step or is_last_batch or is_max_step: |
| self.scaler.unscale_(self.optimizer) |
| grad_norm = torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0) |
| grad_norms.append(grad_norm.item()) |
| |
| scale_before = self.scaler.get_scale() |
| self.scaler.step(self.optimizer) |
| self.scaler.update() |
| if self.scaler.get_scale() >= scale_before: |
| self.scheduler.step() |
| self.optimizer.zero_grad(set_to_none=True) |
| |
| if (step + 1) % log_interval == 0: |
| avg_loss = epoch_loss / (step + 1) |
| progress_bar.set_postfix({'loss': f'{avg_loss:.4f}', 'grad_norm': f'{grad_norm:.4f}'}) |
| |
| |
| if is_max_step: |
| break |
|
|
| |
| actual_steps = step + 1 |
| avg_epoch_loss = epoch_loss / actual_steps |
| epoch_metrics = { |
| 'epoch': epoch + 1, |
| 'train_loss': avg_epoch_loss, |
| 'train_grad_norm': float(np.nanmean([g for g in grad_norms if np.isfinite(g)])) if grad_norms else float('nan'), |
| 'grad_norms': grad_norms |
| } |
| |
| print(f"Epoch {epoch + 1} - Train Loss: {avg_epoch_loss:.4f}") |
| |
| |
| if eval_loader: |
| eval_metrics = self.evaluate(eval_loader) |
| epoch_metrics.update(eval_metrics) |
|
|
| print(f"Epoch {epoch + 1} - Eval Loss: {eval_metrics['eval_loss']:.4f} | Accuracy: {eval_metrics['eval_accuracy']:.2%}") |
|
|
| print(f" Loss - V (MSE): {eval_metrics['eval_loss_V']:.4f} | A: {eval_metrics['eval_loss_A']:.4f} | D: {eval_metrics['eval_loss_D']:.4f}") |
|
|
| print(f" Acc - V (EMOBANK): {eval_metrics['eval_accuracy_V']:.2%} | A: {eval_metrics['eval_accuracy_A']:.2%} | D: {eval_metrics['eval_accuracy_D']:.2%}") |
| |
| |
| current_eval_loss = eval_metrics.get('eval_loss', float('inf')) |
| if current_eval_loss < best_eval_loss: |
| best_eval_loss = current_eval_loss |
| |
| best_state = {k: v.cpu().clone() for k, v in self.model.state_dict().items()} |
| epochs_without_improve = 0 |
| print(f" New best model found (eval_loss improved to {best_eval_loss:.4f}).") |
| else: |
| epochs_without_improve += 1 |
| print(f" No improvement for {epochs_without_improve} epoch(s).") |
| |
| else: |
| |
| best_state = {k: v.cpu().clone() for k, v in self.model.state_dict().items()} |
| |
| self.training_history.append(epoch_metrics) |
|
|
| |
| if use_early_stopping and epochs_without_improve >= early_stopping_patience: |
| print(f"Early stopping triggered after {epoch + 1} epochs (no improvement in eval loss for {early_stopping_patience} epochs).") |
| break |
| |
| |
| if best_state is not None: |
| |
| device_state = {k: v.to(self.device) for k, v in best_state.items()} |
| self.model.load_state_dict(device_state) |
| print("Best model weights restored based on eval data.") |
| |
| return self.model |
| |
| def _training_step(self, batch, accumulation_steps: int = 1) -> float: |
| """Single training step: MSE for V (pointwise) and binary cross-entropy for A/D (pairwise). |
| |
| Args: |
| batch: Dictionary containing input_ids, attention_masks, |
| V_A/V_B (gold scores), label_A/label_D (binary decisions: 1.0 = item1 wins, 0.0 = item2 wins) |
| accumulation_steps: Number of steps to accumulate gradients over |
| |
| Returns: |
| Loss value for this step |
| """ |
| batch = {key: value.to(self.device) for key, value in batch.items()} |
| |
| with self._autocast_context(): |
| valence_A, ad_A = self.model( |
| batch['input_ids_A'], |
| batch['attention_mask_A'] |
| ) |
| valence_B, ad_B = self.model( |
| batch['input_ids_B'], |
| batch['attention_mask_B'] |
| ) |
|
|
| |
| pred_V_A = valence_A.squeeze(-1) |
| pred_V_B = valence_B.squeeze(-1) |
| |
| |
| loss_V = (F.mse_loss(pred_V_A, batch['V_A']) + \ |
| F.mse_loss(pred_V_B, batch['V_B'])) / 2.0 |
| |
| |
| |
| |
| |
| |
| |
| label_A = batch['label_A'] |
| logit_A = ad_A[:, 0] - ad_B[:, 0] |
| loss_A = F.binary_cross_entropy_with_logits(logit_A, label_A) |
| |
| |
| |
| |
| |
| label_D = batch['label_D'] |
| logit_D = ad_A[:, 1] - ad_B[:, 1] |
| loss_D = F.binary_cross_entropy_with_logits(logit_D, label_D) |
| |
| |
| |
| |
| total_loss = (loss_V + loss_A + loss_D) / accumulation_steps |
| |
| self.scaler.scale(total_loss).backward() |
| return total_loss.item() * accumulation_steps |
| |
| def evaluate(self, eval_loader: DataLoader) -> Dict[str, float]: |
| """Evaluate the model: MSE for V (pointwise), Bradley-Terry for A/D (pairwise).""" |
| self.model.eval() |
| |
| total_loss = 0 |
| losses_by_dim = {'V': 0, 'A': 0, 'D': 0} |
| |
| total_correct_V = 0 |
| total_correct_A = 0 |
| total_correct_D = 0 |
| total_count_V = 0 |
| total_count_A = 0 |
| total_count_D = 0 |
| |
| |
| |
| with torch.no_grad(): |
| for batch in tqdm(eval_loader, desc="Evaluating"): |
| batch = {key: value.to(self.device) for key, value in batch.items()} |
| |
| valence_A, ad_A = self.model( |
| batch['input_ids_A'], |
| batch['attention_mask_A'] |
| ) |
| valence_B, ad_B = self.model( |
| batch['input_ids_B'], |
| batch['attention_mask_B'] |
| ) |
|
|
| |
| pred_V_A = valence_A.squeeze(-1) |
| pred_V_B = valence_B.squeeze(-1) |
| |
| |
| loss_V = (F.mse_loss(pred_V_A, batch['V_A']) + \ |
| F.mse_loss(pred_V_B, batch['V_B'])) / 2.0 |
| |
| |
| label_A = batch['label_A'] |
| logit_A = ad_A[:, 0] - ad_B[:, 0] |
| loss_A = F.binary_cross_entropy_with_logits(logit_A, label_A) |
|
|
| |
| |
| |
| |
| |
| |
| label_D = batch['label_D'] |
| logit_D = ad_A[:, 1] - ad_B[:, 1] |
| loss_D = F.binary_cross_entropy_with_logits(logit_D, label_D) |
|
|
| |
| |
| |
| |
| batch_loss = loss_V + loss_A + loss_D |
| total_loss += batch_loss.item() |
| losses_by_dim['V'] += loss_V.item() |
| losses_by_dim['A'] += loss_A.item() |
| losses_by_dim['D'] += loss_D.item() |
| |
| |
| V_A_gold = batch['V_A'] |
| V_B_gold = batch['V_B'] |
| correct_V = ((V_A_gold > V_B_gold) & (pred_V_A > pred_V_B)) | \ |
| ((V_A_gold < V_B_gold) & (pred_V_A < pred_V_B)) |
| non_tie_V = (V_A_gold != V_B_gold) |
| total_correct_V += correct_V[non_tie_V].sum().item() |
| total_count_V += non_tie_V.sum().item() |
| |
| |
| correct_A = ((label_A == 1.0) & (ad_A[:, 0] > ad_B[:, 0])) | \ |
| ((label_A == 0.0) & (ad_B[:, 0] > ad_A[:, 0])) |
| correct_D = ((label_D == 1.0) & (ad_A[:, 1] > ad_B[:, 1])) | \ |
| ((label_D == 0.0) & (ad_B[:, 1] > ad_A[:, 1])) |
| |
| total_correct_A += correct_A.sum().item() |
| total_correct_D += correct_D.sum().item() |
| total_count_A += label_A.shape[0] |
| total_count_D += label_D.shape[0] |
| |
| self.model.train() |
| |
| accuracy_V = total_correct_V / max(total_count_V, 1) |
| accuracy_A = total_correct_A / max(total_count_A, 1) |
| accuracy_D = total_correct_D / max(total_count_D, 1) |
| accuracy_overall = (accuracy_V + accuracy_A + accuracy_D) / 3 |
| |
| return { |
| 'eval_loss': total_loss / len(eval_loader), |
| 'eval_loss_V': losses_by_dim['V'] / len(eval_loader), |
| 'eval_loss_A': losses_by_dim['A'] / len(eval_loader), |
| 'eval_loss_D': losses_by_dim['D'] / len(eval_loader), |
| 'eval_accuracy': accuracy_overall, |
| 'eval_accuracy_V': accuracy_V, |
| 'eval_accuracy_A': accuracy_A, |
| 'eval_accuracy_D': accuracy_D |
| } |
| |
| def score_text(self, text: str) -> Dict[str, float]: |
| """ |
| Score a single text item across all three dimensions (V, A, D). |
| |
| Args: |
| text: Text to score |
| |
| Returns: |
| Dictionary with 'valence', 'arousal', 'dominance' scores |
| """ |
| self.model.eval() |
| |
| encoding = self.tokenizer( |
| text, |
| max_length=self.max_length, |
| padding='max_length', |
| truncation=True, |
| return_tensors='pt' |
| ) |
| |
| with torch.no_grad(): |
| valence, arousal_dominance = self.model( |
| encoding['input_ids'].to(self.device), |
| encoding['attention_mask'].to(self.device) |
| ) |
| |
| self.model.train() |
| return { |
| 'valence': valence[0, 0].item(), |
| 'arousal': arousal_dominance[0, 0].item(), |
| 'dominance': arousal_dominance[0, 1].item() |
| } |
| |
| def score_batch(self, texts: List[str], batch_size: int = 32) -> Dict[str, np.ndarray]: |
| """ |
| Score multiple texts efficiently across all three dimensions. |
| |
| Args: |
| texts: List of texts to score |
| batch_size: Batch size for processing |
| |
| Returns: |
| Dictionary with 'valence', 'arousal', 'dominance' arrays |
| """ |
| self.model.eval() |
| scores_V = [] |
| scores_A = [] |
| scores_D = [] |
| |
| for i in range(0, len(texts), batch_size): |
| batch_texts = texts[i:i + batch_size] |
| encodings = self.tokenizer( |
| batch_texts, |
| max_length=self.max_length, |
| padding='max_length', |
| truncation=True, |
| return_tensors='pt' |
| ) |
| |
| with torch.no_grad(): |
| valence, arousal_dominance = self.model( |
| encodings['input_ids'].to(self.device), |
| encodings['attention_mask'].to(self.device) |
| ) |
| |
| scores_V.extend(valence[:, 0].cpu().numpy()) |
| scores_A.extend(arousal_dominance[:, 0].cpu().numpy()) |
| scores_D.extend(arousal_dominance[:, 1].cpu().numpy()) |
| |
| self.model.train() |
| return { |
| 'valence': np.array(scores_V), |
| 'arousal': np.array(scores_A), |
| 'dominance': np.array(scores_D) |
| } |
| |
| def normalize_scores( |
| self, |
| scores: np.ndarray, |
| scale_min: float = 0.0, |
| scale_max: float = 1.0 |
| ) -> np.ndarray: |
| """ |
| Normalize raw reward scores to a desired scale. |
| This normalizes within the provided scores (relative scaling). |
| For consistent scaling across splits, use normalize_scores_with_params instead. |
| |
| Args: |
| scores: Raw scores to normalize |
| scale_min: Minimum value of output scale |
| scale_max: Maximum value of output scale |
| |
| Returns: |
| Normalized scores |
| """ |
| score_min = scores.min() |
| score_max = scores.max() |
| |
| if score_max == score_min: |
| return np.full_like(scores, (scale_min + scale_max) / 2) |
| |
| normalized = (scores - score_min) / (score_max - score_min) |
| normalized = normalized * (scale_max - scale_min) + scale_min |
| |
| return normalized |
| |
| def test_model(self, test_loader: DataLoader) -> Dict[str, float]: |
| """Evaluate on the test set: MSE for V, Bradley-Terry for A/D.""" |
| print("\n" + "="*60) |
| print("Running Test Evaluation (V, A, D)") |
| print("="*60) |
| |
| self.model.eval() |
| total_loss = 0 |
| losses_by_dim = {'V': 0, 'A': 0, 'D': 0} |
| |
| total_correct_V = 0 |
| total_correct_A = 0 |
| total_correct_D = 0 |
| total_count_V = 0 |
| total_count_A = 0 |
| total_count_D = 0 |
| |
| eps = 1e-7 |
| |
| with torch.no_grad(): |
| for batch in tqdm(test_loader, desc="Testing"): |
| batch = {key: value.to(self.device) for key, value in batch.items()} |
| |
| valence_A, ad_A = self.model( |
| batch['input_ids_A'], |
| batch['attention_mask_A'] |
| ) |
| valence_B, ad_B = self.model( |
| batch['input_ids_B'], |
| batch['attention_mask_B'] |
| ) |
|
|
| |
| pred_V_A = valence_A.squeeze(-1) |
| pred_V_B = valence_B.squeeze(-1) |
| |
| |
| loss_V = (F.mse_loss(pred_V_A, batch['V_A']) + \ |
| F.mse_loss(pred_V_B, batch['V_B'])) / 2.0 |
| |
| |
| label_A = batch['label_A'] |
| logit_A = ad_A[:, 0] - ad_B[:, 0] |
| loss_A = F.binary_cross_entropy_with_logits(logit_A, label_A) |
| |
| |
| |
| |
| |
| label_D = batch['label_D'] |
| logit_D = ad_A[:, 1] - ad_B[:, 1] |
| loss_D = F.binary_cross_entropy_with_logits(logit_D, label_D) |
| |
| |
| |
| |
| batch_loss = loss_V + loss_A + loss_D |
| total_loss += batch_loss.item() |
| losses_by_dim['V'] += loss_V.item() |
| losses_by_dim['A'] += loss_A.item() |
| losses_by_dim['D'] += loss_D.item() |
| |
| |
| V_A_gold = batch['V_A'] |
| V_B_gold = batch['V_B'] |
| correct_V = ((V_A_gold > V_B_gold) & (pred_V_A > pred_V_B)) | \ |
| ((V_A_gold < V_B_gold) & (pred_V_A < pred_V_B)) |
| non_tie_V = (V_A_gold != V_B_gold) |
| total_correct_V += correct_V[non_tie_V].sum().item() |
| total_count_V += non_tie_V.sum().item() |
| |
| |
| correct_A = ((label_A == 1.0) & (ad_A[:, 0] > ad_B[:, 0])) | \ |
| ((label_A == 0.0) & (ad_B[:, 0] > ad_A[:, 0])) |
| correct_D = ((label_D == 1.0) & (ad_A[:, 1] > ad_B[:, 1])) | \ |
| ((label_D == 0.0) & (ad_B[:, 1] > ad_A[:, 1])) |
| |
| total_correct_A += correct_A.sum().item() |
| total_correct_D += correct_D.sum().item() |
| total_count_A += label_A.shape[0] |
| total_count_D += label_D.shape[0] |
| |
| test_loss = total_loss / len(test_loader) |
| test_loss_V = losses_by_dim['V'] / len(test_loader) |
| test_loss_A = losses_by_dim['A'] / len(test_loader) |
| test_loss_D = losses_by_dim['D'] / len(test_loader) |
| |
| accuracy_V = total_correct_V / max(total_count_V, 1) |
| accuracy_A = total_correct_A / max(total_count_A, 1) |
| accuracy_D = total_correct_D / max(total_count_D, 1) |
| accuracy_overall = (accuracy_V + accuracy_A + accuracy_D) / 3 |
| |
| print(f"\nTest Results:") |
| print(f" Total Loss: {test_loss:.4f} | Accuracy: {accuracy_overall:.2%}") |
| print(f" Loss - V (MSE): {test_loss_V:.4f} | A: {test_loss_A:.4f} | D: {test_loss_D:.4f}") |
| print(f" Acc - V (EMOBANK): {accuracy_V:.2%} | A: {accuracy_A:.2%} | D: {accuracy_D:.2%}") |
| print("\n" + "="*60 + "\n") |
| |
| results = { |
| 'test_loss': test_loss, |
| 'test_loss_V': test_loss_V, |
| 'test_loss_A': test_loss_A, |
| 'test_loss_D': test_loss_D, |
| 'test_accuracy': accuracy_overall, |
| 'test_accuracy_V': accuracy_V, |
| 'test_accuracy_A': accuracy_A, |
| 'test_accuracy_D': accuracy_D |
| } |
| |
| return results |
| |
| def save(self, path: str): |
| """Save model and training state.""" |
| torch.save({ |
| 'model_state_dict': self.model.state_dict(), |
| 'optimizer_state_dict': self.optimizer.state_dict() if self.optimizer else None, |
| 'scheduler_state_dict': self.scheduler.state_dict() if self.scheduler else None, |
| 'training_history': self.training_history, |
| 'config': { |
| 'model_name': self.model_name, |
| 'max_length': self.max_length |
| } |
| }, path) |
| print(f"Model saved to {path}") |
| |
| def load(self, path: str): |
| """Load model and training state.""" |
| checkpoint = torch.load(path, map_location=self.device) |
| self.model.load_state_dict(checkpoint['model_state_dict']) |
| |
| if checkpoint['optimizer_state_dict'] and self.optimizer: |
| self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) |
| |
| if checkpoint['scheduler_state_dict'] and self.scheduler: |
| self.scheduler.load_state_dict(checkpoint['scheduler_state_dict']) |
| |
| self.training_history = checkpoint.get('training_history', []) |
| print(f"Model loaded from {path}") |
|
|
| def push_to_hub(self, repo_id: str, private: bool = True): |
| """ |
| Push the trained model to HuggingFace Hub. |
| |
| Args: |
| repo_id: Repository ID in format "username/repo-name" |
| private: Whether to make the repository private (default True) |
| """ |
| from huggingface_hub import create_repo, upload_folder |
| import os |
| |
| |
| temp_dir = f"./temp_model_{repo_id.split('/')[-1]}" |
| os.makedirs(temp_dir, exist_ok=True) |
| |
| try: |
| |
| self.model.encoder.save_pretrained(os.path.join(temp_dir, "encoder")) |
| self.tokenizer.save_pretrained(temp_dir) |
| torch.save( |
| { |
| "model_state_dict": self.model.state_dict(), |
| "config": { |
| "model_name": self.model_name, |
| "max_length": self.max_length, |
| }, |
| }, |
| os.path.join(temp_dir, "reward_model.pth"), |
| ) |
| |
| |
| metadata = { |
| 'model_name': self.model_name, |
| 'max_length': self.max_length, |
| 'training_history': self.training_history |
| } |
| with open(os.path.join(temp_dir, "training_metadata.json"), 'w') as f: |
| json.dump(metadata, f, indent=2) |
| |
| |
| create_repo(repo_id, private=private, exist_ok=True) |
| upload_folder(repo_name=repo_id, folder_path=temp_dir, repo_type="model") |
| |
| print(f"Model successfully pushed to HuggingFace Hub: https://huggingface.co/{repo_id}") |
| |
| finally: |
| |
| import shutil |
| if os.path.exists(temp_dir): |
| shutil.rmtree(temp_dir) |