ravel / scripts /evaluate_hfm.py
minhy112's picture
Upload RAVEL revision project without data or checkpoints
ea8bfa1 verified
Raw
History Blame Contribute Delete
8.39 kB
#!/usr/bin/env python3
"""Evaluate HFM checkpoint with all notebook post-hoc variants."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
from transformers import CLIPProcessor, DebertaV2Tokenizer
import sys
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from src.hfm_pipeline import (
HFMLoader,
build_classification_outputs,
create_dataloaders,
evaluate_ablation,
estimate_max_length,
evaluate_all_variants,
evaluate_raw,
gather_logits_labels,
load_checkpoint,
resolve_device,
save_summary_files,
summarize_splits,
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Evaluate CLARA HFM checkpoint")
parser.add_argument(
"--checkpoint",
default="outputs/hfm/clara_hfm.pt",
help="Path to trained checkpoint",
)
parser.add_argument("--data-root", default="data/HFM", help="Path to HFM root folder")
parser.add_argument(
"--text-dir",
default=None,
help="Path to HFM text folder (default: <data-root>/text)",
)
parser.add_argument("--output-dir", default="results/hfm", help="Evaluation output directory")
parser.add_argument("--batch-size", type=int, default=None, help="Override batch size")
parser.add_argument("--max-length", type=int, default=None, help="Override token max length")
parser.add_argument("--num-workers", type=int, default=None)
parser.add_argument("--top2-eps", type=float, default=0.03)
parser.add_argument(
"--classification-mode",
choices=["raw", "bias_temp"],
default="raw",
help="Which predictions are used for classification report/confusion matrix",
)
parser.add_argument(
"--ablation-full-mode",
choices=["raw", "bias_temp"],
default="raw",
help="Metric source for Full row in ablation table",
)
parser.add_argument("--device", default="auto", help="auto|cuda|cpu")
return parser.parse_args()
def main() -> None:
args = parse_args()
text_dir = args.text_dir or str(Path(args.data_root) / "text")
device = resolve_device(args.device)
model, cfg, ckpt_meta = load_checkpoint(args.checkpoint, device)
cfg["image_root"] = args.data_root
cfg["text_dir"] = text_dir
if args.batch_size is not None:
cfg["batch_size"] = args.batch_size
if args.num_workers is not None:
cfg["num_workers"] = args.num_workers
if args.max_length is not None:
cfg["max_length"] = args.max_length
loader = HFMLoader(cfg["text_dir"], cfg["image_root"])
all_samples = loader.load()
train_samples = loader.get_split("train")
val_samples = loader.get_split("val")
test_samples = loader.get_split("test")
if not val_samples or not test_samples:
raise RuntimeError("Need both val and test splits for full evaluation.")
split_stats = summarize_splits(train_samples, val_samples, test_samples)
print("Split stats:")
print(json.dumps(split_stats, indent=2))
clip_processor = CLIPProcessor.from_pretrained(cfg["vision_model_id"])
tokenizer = DebertaV2Tokenizer.from_pretrained(cfg["text_model_id"])
max_length = cfg.get("max_length")
if not max_length:
max_length = estimate_max_length(
all_samples,
percentile=cfg["max_length_percentile"],
sample_size=cfg["max_length_sample_size"],
)
cfg["max_length"] = int(max_length)
pin_memory = bool(cfg["pin_memory"] and device.type == "cuda")
_, val_loader, test_loader = create_dataloaders(
train_samples=train_samples,
val_samples=val_samples,
test_samples=test_samples,
clip_processor=clip_processor,
tokenizer=tokenizer,
batch_size=cfg["batch_size"],
max_length=cfg["max_length"],
num_workers=cfg["num_workers"],
pin_memory=pin_memory,
weighted_train_sampler=False,
)
result = evaluate_all_variants(
model=model,
val_loader=val_loader,
test_loader=test_loader,
device=device,
top2_eps=args.top2_eps,
)
y_pred_raw, y_true_raw, _ = evaluate_raw(model, test_loader, device)
if args.classification_mode == "raw":
cls_outputs = build_classification_outputs(y_true=y_true_raw, y_pred=y_pred_raw)
else:
logits_test, y_true_bt = gather_logits_labels(model, test_loader, device)
bias = float(result["tuning"]["bias_temp"]["bias"])
tau = float(result["tuning"]["bias_temp"]["tau"])
classes = logits_test.shape[1]
neutral_idx = 1 if classes > 1 else 0
scale = np.ones(classes, dtype=np.float32)
scale[neutral_idx] = 1.0 / tau
adjusted = logits_test * scale[None, :]
adjusted[:, neutral_idx] += bias
y_pred_bt = adjusted.argmax(axis=-1)
cls_outputs = build_classification_outputs(y_true=y_true_bt, y_pred=y_pred_bt)
ablation = evaluate_ablation(model=model, loader=test_loader, device=device)
if args.ablation_full_mode == "bias_temp":
full_row = next((row for row in ablation["rows"] if row["variant"] == "Full"), None)
bias_temp_row = next((row for row in result["summary"] if row["variant"] == "Bias+Temp"), None)
if full_row is not None and bias_temp_row is not None:
full_row["accuracy"] = float(bias_temp_row["accuracy"])
full_row["f1_macro"] = float(bias_temp_row["f1_macro"])
full_row = next((row for row in ablation["rows"] if row["variant"] == "Full"), None)
ablation["full_is_highest"] = (
all(full_row["f1_macro"] > row["f1_macro"] for row in ablation["rows"] if row["variant"] != "Full")
if full_row is not None
else False
)
payload = {
"checkpoint": str(Path(args.checkpoint).resolve()),
"checkpoint_epoch": ckpt_meta.get("epoch"),
"best_val_f1": ckpt_meta.get("best_val_f1"),
"config": cfg,
"classification_mode": args.classification_mode,
"ablation_full_mode": args.ablation_full_mode,
"classification": cls_outputs,
"ablation": ablation,
**result,
}
json_path, csv_path = save_summary_files(args.output_dir, payload)
out_dir = Path(args.output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
report_path = out_dir / "classification_report.txt"
report_path.write_text(cls_outputs["classification_report_text"], encoding="utf-8")
cm_path = out_dir / "confusion_matrix.csv"
cm = cls_outputs["confusion_matrix"]
names = cls_outputs["label_names"]
with cm_path.open("w", encoding="utf-8") as f:
f.write("," + ",".join(names) + "\n")
for i, row in enumerate(cm):
f.write(names[i] + "," + ",".join(str(x) for x in row) + "\n")
ablation_csv = out_dir / "ablation_summary.csv"
with ablation_csv.open("w", encoding="utf-8") as f:
f.write("variant,accuracy,f1_macro\n")
for row in ablation["rows"]:
f.write(f"{row['variant']},{row['accuracy']:.6f},{row['f1_macro']:.6f}\n")
print("\nEvaluation summary:")
for row in payload["summary"]:
print(
f"- {row['variant']:<14} | Acc={row['accuracy']:.4f} | "
f"F1-Macro={row['f1_macro']:.4f}"
)
print(
f"Best variant: {payload['best_variant']} "
f"(F1-Macro={payload['best_f1_macro']:.4f})"
)
print(f"\nClassification report ({args.classification_mode}):")
print(cls_outputs["classification_report_text"])
print(f"Confusion matrix ({args.classification_mode}):")
for row in cls_outputs["confusion_matrix"]:
print(row)
print("\nAblation summary (test):")
for row in ablation["rows"]:
print(
f"- {row['variant']:<18} | Acc={row['accuracy']:.4f} | "
f"F1-Macro={row['f1_macro']:.4f}"
)
print(f"Full highest by F1-Macro: {ablation['full_is_highest']}")
print(f"Saved JSON: {json_path}")
print(f"Saved CSV: {csv_path}")
print(f"Saved classification report: {report_path}")
print(f"Saved confusion matrix: {cm_path}")
print(f"Saved ablation summary: {ablation_csv}")
if __name__ == "__main__":
main()