Spaces:
Sleeping
Sleeping
| import os | |
| import sys | |
| import torch | |
| import torch.nn as nn | |
| import torch.optim as optim | |
| import json | |
| import argparse | |
| from sklearn.metrics import f1_score, recall_score | |
| script_dir = os.path.dirname(os.path.abspath(__file__)) | |
| project_root = os.path.join(script_dir, "..", "..") | |
| sys.path.insert(0, project_root) | |
| from src.data.preprocessor import StepPreprocessor | |
| from src.data.dataset import get_dataloader | |
| from src.models.baseline_model import VanillaBaselineModel | |
| def train_baseline_epoch(model, loader, optimizer): | |
| model.train() | |
| device = next(model.parameters()).device | |
| # Add pos_weight to handle trajectory-level class imbalance | |
| # (Without this, it predicts 0 for everything since most trajectories are clean) | |
| pos_weight = torch.tensor([5.0]).to(device) | |
| bce = nn.BCEWithLogitsLoss(pos_weight=pos_weight) | |
| total_loss = 0 | |
| for batch in loader: | |
| device = next(model.parameters()).device | |
| # Squeeze DataLoader dimension | |
| input_ids = batch["input_ids"].squeeze(0).to(device) | |
| attention_mask = batch["attention_mask"].squeeze(0).to(device) | |
| # Clamp input_ids | |
| vocab_size = model.encoder.config.vocab_size | |
| input_ids = torch.clamp(input_ids, min=0, max=vocab_size - 1) | |
| # For the baseline, we only care if the overall trajectory is hallucinated | |
| # hal_label shape is (N_steps,). If ANY step is hallucinated, the trajectory is 1.0. | |
| hal_labels = batch["hal_label"].squeeze(0).float().to(device) | |
| trajectory_label = hal_labels.max().unsqueeze(0) # Shape: (1,) | |
| optimizer.zero_grad() | |
| # Forward pass (Baseline predicts 1 score for the whole trajectory) | |
| logits = model(input_ids, attention_mask) | |
| # The model now outputs a single logit for the trajectory (shape: 1,) | |
| traj_logit = logits[0].unsqueeze(0) | |
| loss = bce(traj_logit, trajectory_label) | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| optimizer.step() | |
| total_loss += loss.item() | |
| return total_loss / len(loader) if len(loader) > 0 else 0 | |
| def evaluate_baseline(model, test_samples, preprocessor): | |
| model.eval() | |
| hal_preds, hal_true = [], [] | |
| device = next(model.parameters()).device | |
| with torch.no_grad(): | |
| for sample in test_samples: | |
| steps = preprocessor.encode_trajectory(sample) | |
| if not steps: | |
| continue | |
| input_ids = [] | |
| attention_masks = [] | |
| for step in steps: | |
| input_ids.append(step["encoding"]["input_ids"].squeeze(0)) | |
| attention_masks.append(step["encoding"]["attention_mask"].squeeze(0)) | |
| input_ids = torch.stack(input_ids).to(device) | |
| attention_masks = torch.stack(attention_masks).to(device) | |
| vocab_size = model.encoder.config.vocab_size | |
| input_ids = torch.clamp(input_ids, min=0, max=vocab_size - 1) | |
| logits = model(input_ids, attention_masks) | |
| traj_logit = logits[0] | |
| prob = torch.sigmoid(traj_logit).item() | |
| hal_preds.append(1 if prob > 0.5 else 0) | |
| hal_true.append(1 if sample.get("is_hallucination") else 0) | |
| f1 = f1_score(hal_true, hal_preds, average="macro", zero_division=0) | |
| rec = recall_score(hal_true, hal_preds, average="macro", zero_division=0) | |
| return {"judgment_f1": f1, "judgment_recall": rec} | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--epochs", type=int, default=50) | |
| parser.add_argument("--lr", type=float, default=3e-4) # Higher LR since encoder is frozen | |
| args = parser.parse_args() | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| print(f"Using device: {device}") | |
| splits_dir = os.path.join(project_root, "data", "splits") | |
| preprocessor = StepPreprocessor() | |
| train_loader = get_dataloader(os.path.join(splits_dir, "train.json"), preprocessor, batch_size=1, shuffle=True) | |
| with open(os.path.join(splits_dir, "val.json"), "r") as f: | |
| val_samples = json.load(f) | |
| print("Initializing Vanilla Baseline Model...") | |
| model = VanillaBaselineModel() | |
| model.to(device) | |
| optimizer = optim.Adam(model.parameters(), lr=args.lr) | |
| best_val_f1 = 0.0 | |
| patience = 5 | |
| epochs_no_improve = 0 | |
| for epoch in range(1, args.epochs + 1): | |
| print(f"\n--- Epoch {epoch}/{args.epochs} ---") | |
| avg_train_loss = train_baseline_epoch(model, train_loader, optimizer) | |
| print(f"Train Loss: {avg_train_loss:.4f}") | |
| metrics = evaluate_baseline(model, val_samples, preprocessor) | |
| print(f"Validation Metrics: Judgment F1: {metrics['judgment_f1']*100:.1f}% | Judgment Recall: {metrics['judgment_recall']*100:.1f}%") | |
| if metrics['judgment_f1'] > best_val_f1: | |
| best_val_f1 = metrics['judgment_f1'] | |
| epochs_no_improve = 0 | |
| torch.save(model.state_dict(), os.path.join(project_root, "src", "models", "baseline_model.pth")) | |
| print(" [*] New best baseline saved!") | |
| else: | |
| epochs_no_improve += 1 | |
| if epochs_no_improve >= patience: | |
| print(f"Early stopping triggered after {epoch} epochs.") | |
| break | |
| if __name__ == "__main__": | |
| main() | |