| |
| """ |
| ================================================================================ |
| INFERENCIA / EVALUACIÓN - DETECTOR APNEA DEL SUEÑO |
| ================================================================================ |
| Carga un modelo entrenado y predice sobre nuevos archivos .mat de SHHS. |
| Genera AHI (apneas por hora) por paciente y conteo de eventos. |
| |
| Uso: |
| python inference_sleep_apnea.py \ |
| --data_dir /path/to/test/mats \ |
| --model_path /app/output/best_model.pt \ |
| --output_dir /app/inference_results \ |
| --batch_size 64 |
| ================================================================================ |
| """ |
|
|
| import os |
| import glob |
| import json |
| import argparse |
| from collections import defaultdict |
| from typing import Dict, List, Optional |
|
|
| import numpy as np |
| import scipy.io as sio |
| import torch |
| import torch.nn.functional as F |
| from torch.utils.data import DataLoader |
|
|
| |
| from train_sleep_apnea import ( |
| Config, load_mat_file, parse_patient_id, zscore_normalize, |
| create_windows, SleepDataset, collate_fn, SleepApneaNetV2, |
| compute_ahi |
| ) |
|
|
|
|
| def load_model(checkpoint_path: str, config: Config): |
| """Carga modelo entrenado desde checkpoint.""" |
| device = config.device |
| model = SleepApneaNetV2(config).to(device) |
| |
| checkpoint = torch.load(checkpoint_path, map_location=device) |
| model.load_state_dict(checkpoint["model_state_dict"]) |
| model.eval() |
| |
| print(f"Modelo cargado desde {checkpoint_path}") |
| print(f" Época: {checkpoint.get('epoch', 'N/A')}") |
| print(f" Val F1: {checkpoint.get('val_f1', 'N/A')}") |
| |
| return model |
|
|
|
|
| def predict_on_files(model, file_paths: List[str], config: Config, threshold: float = 0.5): |
| """ |
| Predice sobre lista de archivos .mat. |
| Retorna dict con predicciones y AHI por paciente. |
| """ |
| results = {} |
| patient_predictions = defaultdict(lambda: {"windows": [], "preds": [], "labels": []}) |
| |
| for fpath in file_paths: |
| data = load_mat_file(fpath) |
| if data is None: |
| continue |
| |
| pid = parse_patient_id(fpath) |
| windows = create_windows(data, config) |
| if not windows: |
| continue |
| |
| |
| ds = SleepDataset(windows, config, augment=False) |
| loader = DataLoader(ds, batch_size=config.batch_size, shuffle=False, |
| num_workers=0, collate_fn=collate_fn, pin_memory=False) |
| |
| all_probs = [] |
| all_preds = [] |
| with torch.no_grad(): |
| for batch in loader: |
| if len(batch) == 3: |
| x, cwt, y = batch |
| x, cwt = x.to(config.device), cwt.to(config.device) |
| logits = model(x, cwt) |
| else: |
| x, y = batch |
| x = x.to(config.device) |
| logits = model(x) |
| |
| probs = F.softmax(logits, dim=-1)[:, 1].cpu().numpy() |
| preds = (probs > threshold).astype(int) |
| all_probs.extend(probs.tolist()) |
| all_preds.extend(preds.tolist()) |
| |
| |
| results[fpath] = { |
| "patient_id": pid, |
| "n_windows": len(windows), |
| "predicted_events": int(sum(all_preds)), |
| "true_events": int(sum([w["label"] for w in windows])), |
| "predictions": all_preds, |
| "probabilities": all_probs, |
| } |
| |
| |
| patient_predictions[pid]["windows"].extend(windows) |
| patient_predictions[pid]["preds"].extend(all_preds) |
| patient_predictions[pid]["labels"].extend([w["label"] for w in windows]) |
| |
| |
| patient_ahi = {} |
| stride_min = config.stride / config.fs / 60.0 |
| |
| for pid, data in patient_predictions.items(): |
| preds = np.array(data["preds"]) |
| labels = np.array(data["labels"]) |
| wins = data["windows"] |
| |
| pred_ahi = compute_ahi(wins, preds, stride_min=stride_min) |
| true_ahi = compute_ahi(wins, labels, stride_min=stride_min) |
| |
| patient_ahi[pid] = { |
| "n_windows": len(wins), |
| "predicted_events": int(preds.sum()), |
| "true_events": int(labels.sum()), |
| "predicted_ahi": round(float(pred_ahi), 2), |
| "true_ahi": round(float(true_ahi), 2), |
| "error_ahi": round(float(pred_ahi - true_ahi), 2), |
| } |
| |
| return results, patient_ahi |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Inferencia Sleep Apnea") |
| parser.add_argument("--data_dir", type=str, required=True) |
| parser.add_argument("--model_path", type=str, required=True) |
| parser.add_argument("--output_dir", type=str, default="/app/inference_results") |
| parser.add_argument("--threshold", type=float, default=0.5) |
| parser.add_argument("--batch_size", type=int, default=64) |
| parser.add_argument("--use_cwt", action="store_true") |
| parser.add_argument("--device", type=str, default="cuda" if torch.cuda.is_available() else "cpu") |
| args = parser.parse_args() |
| |
| os.makedirs(args.output_dir, exist_ok=True) |
| |
| |
| cfg = Config() |
| cfg.data_dir = args.data_dir |
| cfg.batch_size = args.batch_size |
| cfg.device = args.device |
| cfg.use_cwt = args.use_cwt |
| |
| |
| model = load_model(args.model_path, cfg) |
| |
| |
| files = sorted(glob.glob(os.path.join(args.data_dir, "*.mat"))) |
| print(f"\nArchivos encontrados: {len(files)}") |
| |
| if not files: |
| print("ERROR: No se encontraron archivos .mat") |
| return |
| |
| |
| print(f"\nEjecutando inferencia (threshold={args.threshold})...") |
| results, patient_ahi = predict_on_files(model, files, cfg, threshold=args.threshold) |
| |
| |
| with open(os.path.join(args.output_dir, "inference_results.json"), "w") as f: |
| json.dump(results, f, indent=2) |
| |
| with open(os.path.join(args.output_dir, "patient_ahi.json"), "w") as f: |
| json.dump(patient_ahi, f, indent=2) |
| |
| |
| print("\n" + "=" * 70) |
| print("RESULTADOS POR PACIENTE") |
| print("=" * 70) |
| print(f"{'Paciente':<15} {'Ventanas':>10} {'Pred_Evts':>10} {'True_Evts':>10} {'Pred_AHI':>10} {'True_AHI':>10} {'Error':>10}") |
| print("-" * 80) |
| |
| errors = [] |
| for pid in sorted(patient_ahi.keys()): |
| d = patient_ahi[pid] |
| err = d["error_ahi"] |
| errors.append(err) |
| print(f"{pid:<15} {d['n_windows']:>10} {d['predicted_events']:>10} {d['true_events']:>10} " |
| f"{d['predicted_ahi']:>10.2f} {d['true_ahi']:>10.2f} {err:>10.2f}") |
| |
| if errors: |
| errors = np.array(errors) |
| print("\n" + "=" * 70) |
| print("ESTADÍSTICAS GLOBALES AHI") |
| print("=" * 70) |
| print(f" MAE: {np.abs(errors).mean():.2f}") |
| print(f" RMSE: {np.sqrt((errors**2).mean()):.2f}") |
| print(f" Bias: {errors.mean():.2f}") |
| |
| print(f"\nResultados guardados en: {args.output_dir}") |
| print(f" - inference_results.json (detalle por archivo)") |
| print(f" - patient_ahi.json (resumen por paciente)") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|