Spaces:
Sleeping
Sleeping
| """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() | |