""" finalize.py — Finalize training (save metrics JSON), run evaluate.py, run sample inference. Called after training completes or is interrupted with a valid checkpoint. """ import os import sys import json import csv import torch import numpy as np sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from dataset import get_dataloaders, POLYMER_CLASSES from model import build_model from infer import load_model from evaluate import evaluate _BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) PROC_DIR = os.path.join(_BASE, "data", "processed", "m2b") ASSETS_DIR = os.path.join(_BASE, "assets") def finalize_training(arch="cnn", seed=42): ckpt_path = os.path.join(PROC_DIR, f"m2b_{arch}_best.pt") log_path = os.path.join(PROC_DIR, f"m2b_{arch}_train_log.csv") if not os.path.exists(ckpt_path): print(f"[ERROR] Checkpoint not found: {ckpt_path}") return # Read history from CSV history = [] if os.path.exists(log_path): with open(log_path, "r") as f: reader = csv.DictReader(f) for row in reader: history.append({k: float(v) for k, v in row.items()}) # Load checkpoint for meta device = torch.device("cpu") ckpt = torch.load(ckpt_path, map_location=device, weights_only=True) # Get data for test eval _, _, test_loader, meta = get_dataloaders(seed=seed, augment_train=False) model = build_model(arch, n_classes=ckpt.get("n_classes", 6), input_len=ckpt.get("input_dim", 901)) model.load_state_dict(ckpt["model_state"]) model.eval() # Compute test accuracy import torch.nn as nn criterion = nn.CrossEntropyLoss() total_loss, correct, total = 0.0, 0, 0 all_true, all_pred, all_proba = [], [], [] with torch.no_grad(): for X_batch, y_batch in test_loader: logits = model(X_batch) loss = criterion(logits, y_batch) probs = torch.softmax(logits, dim=-1).numpy() preds = probs.argmax(axis=1) total_loss += loss.item() * len(y_batch) correct += (preds == y_batch.numpy()).sum() total += len(y_batch) all_true.extend(y_batch.numpy()) all_pred.extend(preds) all_proba.extend(probs) test_acc = correct / total test_loss = total_loss / total best_val_acc = ckpt.get("val_acc", max((h.get("val_acc", 0) for h in history), default=0)) best_epoch = ckpt.get("epoch", len(history)) print(f"[INFO] Test accuracy: {test_acc:.4%} (best val: {best_val_acc:.4%} @ epoch {best_epoch})") # Save metrics JSON metrics = { "arch": arch, "seed": seed, "best_epoch": int(best_epoch), "best_val_acc": float(best_val_acc), "test_acc": float(test_acc), "test_loss": float(test_loss), "n_params": sum(p.numel() for p in model.parameters() if p.requires_grad), "n_train": meta["n_train"], "n_val": meta["n_val"], "n_test": meta["n_test"], "class_names": meta["class_names"], "data_source": meta["source"], "history": history, } metrics_path = os.path.join(PROC_DIR, f"m2b_{arch}_metrics.json") with open(metrics_path, "w") as f: json.dump(metrics, f, indent=2) print(f"[INFO] Metrics saved → {metrics_path}") return metrics, np.array(all_true), np.array(all_pred), np.array(all_proba) if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument("--arch", default="cnn") parser.add_argument("--seed", type=int, default=42) args = parser.parse_args() result = finalize_training(args.arch, args.seed) if result is None: sys.exit(1) metrics, y_true, y_pred, y_proba = result print("\n[Step 2] Running full evaluation + generating plots...") eval_report = evaluate(arch=args.arch, seed=args.seed, save_preds=True) print(f"\n[Step 3] Sample inference demo...") from synthetic_spectra import generate_spectrum, POLYMER_CLASSES clf = load_model(arch=args.arch) rng = np.random.default_rng(777) sample_results = [] print(f"\n{'Polymer':>8} | {'Predicted':>9} | {'Confidence':>11} | Correct") print("─" * 55) for polymer in POLYMER_CLASSES: spectrum = generate_spectrum(polymer, rng) result = clf.predict(spectrum) correct = "✓" if result["polymer"] == polymer else "✗" sample_results.append({ "true": polymer, "pred": result["polymer"], "confidence": result["confidence"], "probabilities": result["probabilities"], "correct": result["polymer"] == polymer, }) print(f"{polymer:>8} | {result['polymer']:>9} | {result['confidence']:>10.4f} | {correct}") # Save sample inference output samples_path = os.path.join(PROC_DIR, "m2b_sample_inference.json") with open(samples_path, "w") as f: json.dump(sample_results, f, indent=2) print(f"\n[INFO] Sample inference → {samples_path}") print(f"\n{'='*60}") print(f" TRAINING SUMMARY") print(f"{'='*60}") print(f" Architecture: {args.arch.upper()}") print(f" Best Val Accuracy: {metrics['best_val_acc']:.4%}") print(f" Test Accuracy: {metrics['test_acc']:.4%}") print(f" Macro AUC: {eval_report['macro_auc']:.4f}") print(f" Epochs trained: {metrics['best_epoch']}") print(f" Parameters: {metrics['n_params']:,}") print(f"{'='*60}") print(f"\n Artifacts:") print(f" Checkpoint: {os.path.join(PROC_DIR, f'm2b_{args.arch}_best.pt')}") print(f" Metrics JSON: {os.path.join(PROC_DIR, f'm2b_{args.arch}_metrics.json')}") print(f" Confusion PNG: {os.path.join(PROC_DIR.replace('processed/m2b', 'assets'), 'm2b_confusion.png').replace('data/', '')}") print(f" Eval Report: {os.path.join(PROC_DIR, 'm2b_eval_report.json')}")