Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from sklearn.metrics import accuracy_score, f1_score, mean_absolute_error, precision_score, recall_score, roc_auc_score | |
| from app.config import ( | |
| DEFAULT_ARCHIVE_MODEL_PATH, | |
| DEFAULT_EFFICIENTNET_MODEL_PATH, | |
| DEFAULT_RUNTIME_STACK_REPORT_PATH, | |
| ) | |
| from app.ml.archive_model import load_archive_model, predict_with_archive_model | |
| from app.ml.efficientnet_model import load_efficientnet_checkpoint | |
| from app.ml.features import extract_eye_features | |
| from app.ml.runtime_stack import ( | |
| DEFAULT_SOURCE_THRESHOLDS, | |
| RUNTIME_STACK_VERSION, | |
| build_runtime_stack_prediction, | |
| decision_threshold_for_source, | |
| ) | |
| from app.services.conjunctiva_roi import ConjunctivaRoiExtractor | |
| from train_efficientnet import ARCHIVE_ROOT, _balanced_group_split, _build_records, _load_image_with_fallback | |
| def main() -> None: | |
| records = _build_records(ARCHIVE_ROOT) | |
| if not records: | |
| raise RuntimeError(f"No evaluation records found in {ARCHIVE_ROOT}.") | |
| _, val_records = _balanced_group_split(records, test_size=0.2, n_splits=32) | |
| archive_model = load_archive_model(DEFAULT_ARCHIVE_MODEL_PATH) | |
| efficientnet_bundle = ( | |
| load_efficientnet_checkpoint(DEFAULT_EFFICIENTNET_MODEL_PATH) | |
| if Path(DEFAULT_EFFICIENTNET_MODEL_PATH).exists() | |
| else None | |
| ) | |
| roi_extractor = ConjunctivaRoiExtractor() | |
| runtime_rows: list[dict[str, float | int | str]] = [] | |
| full_rows: list[dict[str, float | int | str]] = [] | |
| prepared_images: list[object] = [] | |
| prepared_sources: list[str] = [] | |
| prepared_archive_predictions: list[dict[str, float]] = [] | |
| prepared_records = [] | |
| for record in val_records: | |
| image = _load_image_with_fallback(record.image_path) | |
| source_hint = record.source | |
| if record.source == "roi_original": | |
| image = roi_extractor.extract(image).image | |
| image = image.convert("RGB") | |
| archive_prediction = predict_with_archive_model( | |
| archive_model, | |
| extract_eye_features(image), | |
| source_hint=source_hint, | |
| ) | |
| prepared_records.append(record) | |
| prepared_images.append(image) | |
| prepared_sources.append(source_hint) | |
| prepared_archive_predictions.append(archive_prediction) | |
| efficientnet_predictions = _predict_efficientnet_batch(efficientnet_bundle, prepared_images) | |
| for record, source_hint, archive_prediction, efficientnet_prediction in zip( | |
| prepared_records, | |
| prepared_sources, | |
| prepared_archive_predictions, | |
| efficientnet_predictions, | |
| strict=True, | |
| ): | |
| runtime_prediction = build_runtime_stack_prediction( | |
| archive_prediction, | |
| efficientnet_prediction=efficientnet_prediction, | |
| source_hint=source_hint, # type: ignore[arg-type] | |
| ) | |
| row = { | |
| "label": int(record.label), | |
| "source": str(record.source), | |
| "risk": float(runtime_prediction["anemia_risk"]), | |
| "predicted_hb": float(runtime_prediction["predicted_hemoglobin"]), | |
| "target_hb": float(record.hb), | |
| } | |
| full_rows.append(row) | |
| if record.source == "roi_original": | |
| runtime_rows.append(row) | |
| runtime_metrics = _evaluate_rows(runtime_rows, source_aware=False) | |
| full_metrics = _evaluate_rows(full_rows, source_aware=True) | |
| report = { | |
| "primary_model": RUNTIME_STACK_VERSION, | |
| "record_count": len(records), | |
| "subject_count": len({record.subject_id for record in records}), | |
| "selected_mode": "archive_evidence_fusion_runtime", | |
| "source_thresholds": DEFAULT_SOURCE_THRESHOLDS, | |
| "metrics": runtime_metrics, | |
| "full_validation": full_metrics, | |
| } | |
| DEFAULT_RUNTIME_STACK_REPORT_PATH.write_text(json.dumps(report, indent=2), encoding="utf-8") | |
| print("\nRuntime stack metrics (ROI-gated uploads)") | |
| for key in ("accuracy", "precision", "recall", "f1", "auc", "hb_mae"): | |
| print(f"{key}: {runtime_metrics[key]:.4f}") | |
| print("\nFull validation metrics (all sources)") | |
| for key in ("accuracy", "precision", "recall", "f1", "auc", "hb_mae"): | |
| print(f"{key}: {full_metrics[key]:.4f}") | |
| def _evaluate_rows( | |
| rows: list[dict[str, float | int | str]], | |
| *, | |
| source_aware: bool, | |
| ) -> dict[str, float | int | str]: | |
| labels = np.asarray([int(row["label"]) for row in rows], dtype=np.int32) | |
| probabilities = np.asarray([float(row["risk"]) for row in rows], dtype=np.float32) | |
| predicted_hb = np.asarray([float(row["predicted_hb"]) for row in rows], dtype=np.float32) | |
| target_hb = np.asarray([float(row["target_hb"]) for row in rows], dtype=np.float32) | |
| if source_aware: | |
| predictions = np.asarray( | |
| [ | |
| 1 | |
| if float(row["risk"]) >= decision_threshold_for_source(str(row["source"])) # type: ignore[arg-type] | |
| else 0 | |
| for row in rows | |
| ], | |
| dtype=np.int32, | |
| ) | |
| split_strategy = "group-shuffle-balance-select: source-aware" | |
| else: | |
| threshold = decision_threshold_for_source("roi_original") | |
| predictions = (probabilities >= threshold).astype(np.int32) | |
| split_strategy = "group-shuffle-balance-select: roi_original" | |
| return { | |
| "accuracy": round(float(accuracy_score(labels, predictions)), 4), | |
| "precision": round(float(precision_score(labels, predictions, zero_division=0)), 4), | |
| "recall": round(float(recall_score(labels, predictions, zero_division=0)), 4), | |
| "f1": round(float(f1_score(labels, predictions, zero_division=0)), 4), | |
| "auc": round(float(roc_auc_score(labels, probabilities)), 4), | |
| "hb_mae": round(float(mean_absolute_error(target_hb, predicted_hb)), 4), | |
| "validation_size": int(len(rows)), | |
| "split_strategy": split_strategy, | |
| } | |
| def _predict_efficientnet_batch( | |
| bundle: dict[str, object] | None, | |
| images: list[object], | |
| ) -> list[dict[str, float] | None]: | |
| if bundle is None: | |
| return [None] * len(images) | |
| transform = bundle["transform"] | |
| model = bundle["model"] | |
| hb_mean = float(bundle.get("hb_mean", 0.0)) | |
| hb_std = max(float(bundle.get("hb_std", 1.0)), 1e-6) | |
| tensors = torch.stack([transform(image) for image in images], dim=0) | |
| with torch.no_grad(): | |
| output = model(tensors) | |
| probabilities = torch.sigmoid(output[:, 0]).cpu().numpy() | |
| hemoglobin = ((output[:, 1].cpu().numpy()) * hb_std) + hb_mean | |
| results: list[dict[str, float]] = [] | |
| for probability, hb_value in zip(probabilities, hemoglobin, strict=True): | |
| margin_uncertainty = 1.0 - min(1.0, abs(float(probability) - 0.5) * 2.0) | |
| results.append( | |
| { | |
| "anemia_risk": float(probability), | |
| "predicted_hemoglobin": float(hb_value), | |
| "uncertainty": float(np.clip((margin_uncertainty * 0.2) + 0.05, 0.05, 0.95)), | |
| } | |
| ) | |
| return results | |
| if __name__ == "__main__": | |
| main() | |