| """ |
| Evaluate saved ModernBERT model on the held-out test split. |
| ========================================================= |
| Loads the saved test split from splits/test.csv and evaluates |
| the model on it. |
| |
| Usage: |
| python eval_bert.py --model modernbert/best --splits-dir splits |
| """ |
|
|
| import csv |
| import argparse |
| from collections import defaultdict |
| from pathlib import Path |
|
|
| import numpy as np |
| import torch |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification |
| from sklearn.metrics import classification_report, accuracy_score, confusion_matrix |
|
|
| from config import CLASSES, BINARY_CLASSES |
|
|
| csv.field_size_limit(10_000_000) |
|
|
| MAX_TOKENS = 8192 |
| BATCH_SIZE = 4 |
|
|
|
|
| |
|
|
| def run_inference(model, tokenizer, device, texts, id2label): |
| all_preds, all_confs, all_probs = [], [], [] |
| for i in range(0, len(texts), BATCH_SIZE): |
| batch = texts[i : i + BATCH_SIZE] |
| enc = tokenizer( |
| batch, |
| truncation=True, |
| max_length=MAX_TOKENS, |
| padding="longest", |
| return_tensors="pt", |
| ).to(device) |
| with torch.no_grad(): |
| logits = model(**enc).logits |
| probs = torch.softmax(logits, dim=-1).float().cpu().numpy() |
| for prob in probs: |
| best_idx = prob.argmax() |
| all_preds.append(id2label[best_idx]) |
| all_confs.append(prob[best_idx]) |
| all_probs.append(prob) |
| if (i // BATCH_SIZE) % 50 == 0: |
| print(f" {i + len(batch):,} / {len(texts):,}", end="\r") |
| print() |
| return all_preds, all_confs, all_probs |
|
|
|
|
| |
|
|
| def evaluate(model_dir, splits_dir): |
| |
| splits_dir = Path(splits_dir) |
| X_test, y_test = [], [] |
| with open(splits_dir / "test.csv") as f: |
| for row in csv.DictReader(f): |
| X_test.append(row["text"]) |
| y_test.append(row["label"]) |
|
|
| |
| all_labels_set = set(y_test) |
| if all_labels_set.issubset(set(BINARY_CLASSES)): |
| classes = BINARY_CLASSES |
| print("Detected binary classification mode") |
| else: |
| classes = CLASSES |
| print("Detected multiclass classification mode") |
|
|
| id2label = {i: cls for i, cls in enumerate(classes)} |
|
|
| counts = defaultdict(int) |
| for l in y_test: |
| counts[l] += 1 |
| print(f"Test set: {len(X_test):,} examples") |
| for cls in classes: |
| print(f" {cls:<25}: {counts[cls]:>5}") |
|
|
| |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| print(f"\nDevice: {device}") |
| print(f"Loading model from {model_dir}...") |
| tokenizer = AutoTokenizer.from_pretrained(model_dir) |
| model = AutoModelForSequenceClassification.from_pretrained( |
| model_dir, dtype=torch.bfloat16 |
| ).to(device) |
| model.eval() |
|
|
| |
| print(f"\nRunning inference on {len(X_test):,} test examples...") |
| pred_names, confidences, _ = run_inference(model, tokenizer, device, X_test, id2label) |
|
|
| |
| acc = accuracy_score(y_test, pred_names) |
| report = classification_report(y_test, pred_names, labels=classes, zero_division=0) |
|
|
| print(f"\n--- Test Set Results (acc={acc:.3f}) ---") |
| print(report) |
|
|
| |
| cm = confusion_matrix(y_test, pred_names, labels=classes) |
| print("Confusion matrix (rows=true, cols=pred):") |
| header = "".join(f"{c[:8]:>10}" for c in classes) |
| print(f"{'':>25}{header}") |
| for i, cls in enumerate(classes): |
| row = "".join(f"{cm[i,j]:>10}" for j in range(len(classes))) |
| print(f" {cls:<25}{row}") |
|
|
| |
| print("\nMean confidence by true class:") |
| class_confs = defaultdict(list) |
| for true, conf in zip(y_test, confidences): |
| class_confs[true].append(conf) |
| for cls in classes: |
| if class_confs[cls]: |
| print(f" {cls:<25}: {np.mean(class_confs[cls]):.3f}") |
|
|
| |
| misclassified_path = Path(model_dir) / "misclassified.csv" |
| with open(misclassified_path, "w", newline="") as f: |
| writer = csv.writer(f) |
| writer.writerow(["true_label", "pred_label", "confidence", "text"]) |
| for text, true, pred, conf in zip(X_test, y_test, pred_names, confidences): |
| if true != pred: |
| writer.writerow([true, pred, f"{conf:.4f}", text]) |
| print(f"\nMisclassified examples saved to {misclassified_path}") |
|
|
|
|
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--model", default="modernbert/best") |
| parser.add_argument("--splits-dir", default="splits") |
| args = parser.parse_args() |
|
|
| evaluate(args.model, args.splits_dir) |
|
|