AnemiaLens / backend /scripts /analyze_efficientnet_errors.py
asnannp's picture
sync: sync backend code, models, schemas, and API routers to Hugging Face Space cleanly
f559cc0
Raw
History Blame Contribute Delete
7.7 kB
from __future__ import annotations
import json
import sys
from collections import Counter, defaultdict
from pathlib import Path
import numpy as np
import torch
from sklearn.metrics import accuracy_score, confusion_matrix, f1_score, mean_absolute_error, precision_score, recall_score, roc_auc_score
from torch.utils.data import DataLoader
BACKEND_ROOT = Path(__file__).resolve().parents[1]
if str(BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(BACKEND_ROOT))
SCRIPT_ROOT = Path(__file__).resolve().parent
if str(SCRIPT_ROOT) not in sys.path:
sys.path.insert(0, str(SCRIPT_ROOT))
from app.config import DEFAULT_EFFICIENTNET_MODEL_PATH
from app.ml.efficientnet_model import load_efficientnet_checkpoint
from train_efficientnet import (
ARCHIVE_ROOT,
DATA_ROOT,
ConjunctivaDataset,
_balanced_group_split,
_build_records,
build_val_transform,
)
DEFAULT_OUTPUT_PATH = BACKEND_ROOT / "models" / "efficientnet_error_report.json"
def main() -> None:
dataset_root = DATA_ROOT if DATA_ROOT.exists() else ARCHIVE_ROOT
records = _build_records(dataset_root)
if not records:
raise RuntimeError(f"No dataset records found under {dataset_root}.")
if not DEFAULT_EFFICIENTNET_MODEL_PATH.exists():
raise RuntimeError(f"EfficientNet checkpoint not found at {DEFAULT_EFFICIENTNET_MODEL_PATH}.")
train_records, val_records = _balanced_group_split(records, test_size=0.2, n_splits=32)
bundle = load_efficientnet_checkpoint(DEFAULT_EFFICIENTNET_MODEL_PATH)
report = analyze_validation_split(val_records, bundle, dataset_root=dataset_root, train_records=train_records)
DEFAULT_OUTPUT_PATH.write_text(json.dumps(report, indent=2), encoding="utf-8")
print(f"Saved error report to {DEFAULT_OUTPUT_PATH}")
print(json.dumps(report["summary"], indent=2))
def analyze_validation_split(
val_records: list,
bundle: dict[str, object],
*,
dataset_root: Path,
train_records: list,
) -> dict[str, object]:
model = bundle["model"]
device = bundle["device"]
hb_mean = float(bundle.get("hb_mean", 0.0))
hb_std = float(bundle.get("hb_std", 1.0))
threshold = float(bundle.get("decision_threshold", 0.5))
dataset = ConjunctivaDataset(val_records, build_val_transform())
loader = DataLoader(dataset, batch_size=16, shuffle=False, num_workers=0)
probabilities: list[float] = []
predictions: list[int] = []
labels: list[int] = []
hb_predictions: list[float] = []
hb_targets: list[float] = []
model.eval()
with torch.no_grad():
for images, batch_labels, batch_hbs in loader:
output = model(images.to(device))
batch_probabilities = torch.sigmoid(output[:, 0]).cpu().tolist()
batch_hb_predictions = (((output[:, 1].cpu()) * hb_std) + hb_mean).tolist()
probabilities.extend(batch_probabilities)
predictions.extend([1 if value >= threshold else 0 for value in batch_probabilities])
labels.extend(batch_labels.squeeze(1).cpu().int().tolist())
hb_predictions.extend(batch_hb_predictions)
hb_targets.extend(batch_hbs.squeeze(1).cpu().tolist())
summary = {
"dataset_root": str(dataset_root),
"checkpoint_path": str(DEFAULT_EFFICIENTNET_MODEL_PATH),
"record_count": len(val_records),
"subject_count": len({record.subject_id for record in val_records}),
"threshold": round(threshold, 4),
"train_record_count": len(train_records),
"train_subject_count": len({record.subject_id for record in train_records}),
"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(hb_targets, hb_predictions)), 4),
"label_counts": dict(Counter(labels)),
"prediction_counts": dict(Counter(predictions)),
"confusion_matrix": confusion_matrix(labels, predictions).tolist(),
}
source_breakdown = _source_breakdown(val_records, labels, predictions, probabilities, hb_predictions, hb_targets)
false_positives, false_negatives = _mistakes(val_records, labels, predictions, probabilities, hb_predictions, hb_targets)
return {
"summary": summary,
"source_breakdown": source_breakdown,
"false_positives": false_positives,
"false_negatives": false_negatives,
}
def _source_breakdown(
val_records: list,
labels: list[int],
predictions: list[int],
probabilities: list[float],
hb_predictions: list[float],
hb_targets: list[float],
) -> dict[str, object]:
by_source: dict[str, dict[str, object]] = defaultdict(
lambda: {
"count": 0,
"errors": 0,
"false_positives": 0,
"false_negatives": 0,
"probabilities": [],
"hb_abs_error": [],
}
)
for record, label, prediction, probability, hb_prediction, hb_target in zip(
val_records,
labels,
predictions,
probabilities,
hb_predictions,
hb_targets,
):
item = by_source[record.source]
item["count"] += 1
item["errors"] += int(label != prediction)
item["false_positives"] += int(label == 0 and prediction == 1)
item["false_negatives"] += int(label == 1 and prediction == 0)
item["probabilities"].append(float(probability))
item["hb_abs_error"].append(abs(float(hb_prediction) - float(hb_target)))
normalized: dict[str, object] = {}
for source, item in by_source.items():
normalized[source] = {
"count": item["count"],
"errors": item["errors"],
"false_positives": item["false_positives"],
"false_negatives": item["false_negatives"],
"error_rate": round(float(item["errors"] / max(item["count"], 1)), 4),
"mean_probability": round(float(np.mean(item["probabilities"])), 4),
"hb_mae": round(float(np.mean(item["hb_abs_error"])), 4),
}
return normalized
def _mistakes(
val_records: list,
labels: list[int],
predictions: list[int],
probabilities: list[float],
hb_predictions: list[float],
hb_targets: list[float],
) -> tuple[list[dict[str, object]], list[dict[str, object]]]:
false_positives: list[dict[str, object]] = []
false_negatives: list[dict[str, object]] = []
for record, label, prediction, probability, hb_prediction, hb_target in zip(
val_records,
labels,
predictions,
probabilities,
hb_predictions,
hb_targets,
):
if label == prediction:
continue
item = {
"subject_id": record.subject_id,
"source": record.source,
"probability": round(float(probability), 4),
"hb_true": round(float(hb_target), 2),
"hb_predicted": round(float(hb_prediction), 2),
"image_path": str(record.image_path),
}
if label == 0 and prediction == 1:
false_positives.append(item)
else:
false_negatives.append(item)
false_positives.sort(key=lambda item: float(item["probability"]), reverse=True)
false_negatives.sort(key=lambda item: float(item["probability"]))
return false_positives[:12], false_negatives[:12]
if __name__ == "__main__":
main()