prism-backend / src /evaluate_transformer_models.py
Penguindrum920's picture
Prepare PRISM backend for Hugging Face Spaces
7bc5fde verified
Raw
History Blame Contribute Delete
10.6 kB
"""Evaluate transformer models (BioGPT, PubMedBERT, Clinical-T5) on the leak-free split."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import joblib
import numpy as np
import pandas as pd
import torch
from sklearn.metrics import (
accuracy_score,
auc,
classification_report,
confusion_matrix,
precision_recall_fscore_support,
roc_curve,
)
from sklearn.preprocessing import label_binarize
from torch.utils.data import DataLoader
sys.path.append(str(Path(__file__).parent))
from data_preprocessing import DataPreprocessor # type: ignore
from document_manager import DocumentManager # type: ignore
from models.medical_transformers import ( # type: ignore
BioMistralClassifier as BioGPTForTabular,
ClinicalT5Classifier as ClinicalT5ForTabular,
PubMedBERTClassifier as PubMedBERTForTabular,
)
from models.transformer_models import TabularDataset # type: ignore
ROOT = Path(__file__).resolve().parents[1]
EVAL_DIR = ROOT / "evaluation_results" / "model_metrics"
CLASS_REPORT_DIR = EVAL_DIR / "classification_reports"
CONF_MATRIX_DIR = EVAL_DIR / "confusion_matrices"
ROC_DIR = EVAL_DIR / "roc_curves"
SUMMARY_PATH = EVAL_DIR / "model_metrics_summary_transformers.csv"
LATEST_JSON = EVAL_DIR / "transformer_metrics_latest.json"
LEAK_FREE_SPLIT_PATH = ROOT / "evaluation_results" / "leak_free_split.npz"
LEAK_FREE_META_PATH = ROOT / "evaluation_results" / "leak_free_split_meta.joblib"
CLASS_NAMES = ["HC", "PD", "SWEDD", "PRODROMAL"]
for directory in (CLASS_REPORT_DIR, CONF_MATRIX_DIR, ROC_DIR):
directory.mkdir(parents=True, exist_ok=True)
def _load_or_create_leak_free_split() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, list[str]]:
if LEAK_FREE_SPLIT_PATH.exists() and LEAK_FREE_META_PATH.exists():
split = np.load(LEAK_FREE_SPLIT_PATH)
meta = joblib.load(LEAK_FREE_META_PATH)
feature_names = meta.get("feature_names") if isinstance(meta, dict) else None
if feature_names is None:
raise ValueError("Leak-free metadata missing feature names")
print("Loaded cached leak-free split artifacts.")
return split["X_train"], split["X_test"], split["y_train"], split["y_test"], feature_names
preprocessor = DataPreprocessor()
file_paths = [
ROOT / "PPMI_Curated_Data_Cut_Public_20240129.csv",
ROOT / "PPMI_Curated_Data_Cut_Public_20241211.csv",
ROOT / "PPMI_Curated_Data_Cut_Public_20250321.csv",
ROOT / "PPMI_Curated_Data_Cut_Public_20250714.csv",
]
X_train, X_test, y_train, y_test = preprocessor.prepare_data(
file_paths,
test_size=0.2,
use_patient_split=True,
)
feature_names = preprocessor.get_feature_names()
LEAK_FREE_SPLIT_PATH.parent.mkdir(parents=True, exist_ok=True)
np.savez(
LEAK_FREE_SPLIT_PATH,
X_train=X_train,
X_test=X_test,
y_train=y_train,
y_test=y_test,
)
joblib.dump({"feature_names": feature_names}, LEAK_FREE_META_PATH)
print("Saved new leak-free split artifacts.")
return X_train, X_test, y_train, y_test, feature_names
def _prepare_batch(batch, device):
if len(batch) == 3:
data, targets, contexts = batch
contexts = list(contexts)
else:
data, targets = batch
contexts = None
data = data.to(device)
targets = targets.to(device)
return data, targets, contexts
def _build_context_cache(features: np.ndarray, feature_names: list[str], doc_manager: DocumentManager) -> list[str]:
cache = []
total = len(features)
for idx, row in enumerate(features):
feature_desc = {name: float(val) for name, val in zip(feature_names, row)}
query_parts = []
for symptom_key, col in {
"tremor": "sym_tremor",
"rigidity": "sym_rigid",
"bradykinesia": "sym_brady",
"postural instability": "sym_posins",
}.items():
if feature_desc.get(col, 0) > 0:
query_parts.append(f"{symptom_key} severity:{feature_desc[col]:.2f}")
moca = feature_desc.get("moca", 30)
if moca < 26:
query_parts.append("cognitive impairment")
age = feature_desc.get("age", 0)
if age:
query_parts.append(f"age {int(age)}")
if feature_desc.get("fampd", 0) > 0:
query_parts.append("family history Parkinson's disease")
query = "Parkinson's disease " + " ".join(query_parts)
passages = doc_manager.extract_relevant_passages(query, top_k=2)
if not passages:
cache.append("")
else:
combined = []
for passage in passages:
title = passage.get("doc_title") or passage.get("doc_id") or "document"
combined.append(f"From '{title}' {passage['text'][:300]}...")
cache.append(" ".join(combined))
if (idx + 1) % 250 == 0 or idx + 1 == total:
print(f" Cached RAG context for {idx + 1}/{total} samples")
return cache
def _save_outputs(model_name: str, report: str, cm: np.ndarray, y_test: np.ndarray, y_prob: np.ndarray) -> None:
(CLASS_REPORT_DIR / f"{model_name}.txt").write_text(
f"{model_name} Classification Report (leak-free split)\n" + "-" * 60 + "\n" + report
)
cm_df = pd.DataFrame(cm, index=CLASS_NAMES, columns=CLASS_NAMES)
cm_df.to_csv(CONF_MATRIX_DIR / f"{model_name}_confusion_matrix.csv")
y_bin = label_binarize(y_test, classes=range(len(CLASS_NAMES)))
for idx, class_name in enumerate(CLASS_NAMES):
fpr, tpr, _ = roc_curve(y_bin[:, idx], y_prob[:, idx])
roc_df = pd.DataFrame({"fpr": fpr, "tpr": tpr})
roc_df.to_csv(ROC_DIR / f"{model_name}_class_{class_name}_roc.csv", index=False)
def main() -> None:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Evaluating transformers on {device} (leak-free split)")
X_train, X_test, y_train, y_test, feature_names = _load_or_create_leak_free_split()
X_train = np.asarray(X_train)
X_test = np.asarray(X_test)
y_train = np.asarray(y_train)
y_test = np.asarray(y_test)
docs_path = ROOT / "medical_docs"
doc_manager = DocumentManager(docs_dir=str(docs_path))
print("Building RAG contexts for test set...")
test_contexts = _build_context_cache(X_test, feature_names, doc_manager)
test_dataset = TabularDataset(X_test, y_test, feature_names, contexts=test_contexts)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
model_dir = ROOT / "models" / "saved"
model_configs = {
"BioGPT": {
"builder": lambda: BioGPTForTabular(
input_dim=X_train.shape[1],
num_classes=len(CLASS_NAMES),
dropout=0.15,
train_decoder_layers=6,
),
"checkpoints": [
model_dir / "biogpt_transformer.pth",
model_dir / "biogpt.pth",
model_dir / "biomistral.pth",
],
},
"PubMedBERT": {
"builder": lambda: PubMedBERTForTabular(
input_dim=X_train.shape[1],
num_classes=len(CLASS_NAMES),
dropout=0.15,
freeze_bert=False,
),
"checkpoints": [
model_dir / "pubmedbert_transformer.pth",
model_dir / "pubmedbert.pth",
],
},
"Clinical-T5": {
"builder": lambda: ClinicalT5ForTabular(
input_dim=X_train.shape[1],
num_classes=len(CLASS_NAMES),
dropout=0.15,
freeze_encoder=False,
),
"checkpoints": [
model_dir / "clinical_t5_transformer.pth",
model_dir / "clinicalt5_transformer.pth",
model_dir / "clinical_t5.pth",
],
},
}
summary_rows = []
for pretty_name, cfg in model_configs.items():
checkpoint_path = next((path for path in cfg["checkpoints"] if path.exists()), None)
if checkpoint_path is None:
expected = ", ".join(path.name for path in cfg["checkpoints"])
print(f"[WARN] Skipping {pretty_name}: no checkpoint found ({expected})")
continue
print(f"\nEvaluating {pretty_name}...")
model = cfg["builder"]().to(device)
state = torch.load(checkpoint_path, map_location=device)
model.load_state_dict(state)
model.eval()
all_targets = []
all_preds = []
all_prob = []
with torch.no_grad():
for batch in test_loader:
data, targets, contexts = _prepare_batch(batch, device)
outputs = model(data, contexts)
probs = torch.softmax(outputs, dim=1)
preds = torch.argmax(probs, dim=1)
all_targets.extend(targets.cpu().numpy())
all_preds.extend(preds.cpu().numpy())
all_prob.append(probs.cpu().numpy())
y_true = np.array(all_targets)
y_pred = np.array(all_preds)
y_prob = np.vstack(all_prob)
accuracy = accuracy_score(y_true, y_pred)
precision, recall, f1, _ = precision_recall_fscore_support(
y_true, y_pred, average="weighted", zero_division=0
)
report = classification_report(
y_true, y_pred, target_names=CLASS_NAMES, zero_division=0
)
cm = confusion_matrix(y_true, y_pred)
_save_outputs(pretty_name, report, cm, y_true, y_prob)
summary_rows.append(
{
"model": pretty_name,
"accuracy": accuracy,
"precision": precision,
"recall": recall,
"f1": f1,
}
)
print(
f"{pretty_name} -> accuracy {accuracy:.4f}, precision {precision:.4f}, recall {recall:.4f}, f1 {f1:.4f}"
)
if summary_rows:
summary_df = pd.DataFrame(summary_rows)
summary_df.to_csv(SUMMARY_PATH, index=False)
LATEST_JSON.write_text(json.dumps(summary_rows, indent=2))
print(f"Saved transformer summary to {SUMMARY_PATH}")
else:
print("No transformer metrics were generated; check checkpoints.")
if __name__ == "__main__":
main()