agentsight-api / src /training /train_baseline.py
Minato Namikaze
Deploy to Hugging Face Spaces
2aed081
Raw
History Blame Contribute Delete
5.51 kB
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()