{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================\n# RSNA KNEE 2026 — PIPELINE COMPLETO SIN ERRORES (v5.16)\n# CELDA 1: PROCESAMIENTO DE ARCHIVOS Y RUTAS\n# ============================================================\nimport os\nimport time\nimport copy\nimport warnings\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import KFold\nfrom sklearn.metrics import roc_auc_score, average_precision_score, roc_curve, auc\nimport torchvision.models as models\n\ntry:\n    import cv2\n    _HAS_CV2 = True\nexcept ImportError:\n    _HAS_CV2 = False\n\nwarnings.filterwarnings('ignore')\n\n# --- CONFIGURACIÓN ESTABLE ---\nCONFIG = {\n    'seed': 42,\n    'img_size': 256,\n    'num_slices': 3,          # Enfoque 2.5D\n    'batch_size': 16,\n    'epochs': 10,\n    'lr': 3e-4,\n    'neg_confidence_cap': 0.2,\n    'cache_dir': Path(\"/kaggle/tmp/rsna_cache\"),\n    'num_workers': 2,         # 2 hilos para evitar bloqueos de CPU\n    'pin_memory': True,\n    'num_classes': 12\n}\n\ndef seed_everything(seed=42):\n    import random\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n\nseed_everything(CONFIG['seed'])\nCONFIG['cache_dir'].mkdir(parents=True, exist_ok=True)\n\n# --- DETECCIÓN DE DATOS DE LA COMPETENCIA ---\nCANDIDATE_PATHS = [\n    Path(\"/kaggle/input/rsna-knee-abnormality-detection\"),\n    Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\"),\n]\nDATA_PATH = next((p for p in CANDIDATE_PATHS if p.exists()), None)\nif DATA_PATH is None:\n    raise FileNotFoundError(\"❌ Vincula el dataset de la competencia en el panel de 'Add Input'.\")\n\nID_COL = \"StudyInstanceUID\"\nLABELS = ['ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', 'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', 'Synovitis', \"Baker's\", 'Contusion', 'Fracture']\n\ndf_train_raw = pd.read_csv(DATA_PATH / \"train.csv\")\nprint(f\"✅ Dataset cargado correctamente: {len(df_train_raw)} estudios disponibles.\")\n# ============================================================\n# CELDA 2: PROCESAMIENTO MÉDICO Y PÉRDIDA CALIBRADA\n# ============================================================\n\ndef apply_negative_confidence_cap(target_tensor, confidence_tensor, cap_val=0.2):\n    is_weak_negative = (target_tensor == 0) & (confidence_tensor < 1.0)\n    confidence_tensor[is_weak_negative] = torch.clamp(confidence_tensor[is_weak_negative], max=cap_val)\n    return confidence_tensor\n\nclass ConfidenceWeightedBCEWithLogitsLoss(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.bce = nn.BCEWithLogitsLoss(reduction='none')\n        \n    def forward(self, outputs, targets, confidences):\n        loss = self.bce(outputs.float(), targets.float())\n        return (loss * confidences.float()).mean()\n\ndef load_series_slices_25d(series_dir, num_slices=3, target_size=256, cache_dir=None):\n    series_dir = Path(series_dir) if isinstance(series_dir, list) and len(series_dir) > 0 else Path(series_dir)\n    if not series_dir.exists():\n        return np.zeros((num_slices, target_size, target_size), dtype=np.float32)\n        \n    if cache_dir is not None:\n        cache_file = cache_dir / f\"{series_dir.name}_25d.npy\"\n        if cache_file.exists():\n            try: return np.load(cache_file)\n            except: cache_file.unlink(missing_ok=True)\n\n    dcm_files = list(series_dir.glob(\"*.dcm\"))\n    if len(dcm_files) == 0:\n        return np.zeros((num_slices, target_size, target_size), dtype=np.float32)\n        \n    metadata = []\n    for f in dcm_files:\n        try:\n            h = pydicom.dcmread(f, stop_before_pixels=True)\n            instance_num = int(h.InstanceNumber) if 'InstanceNumber' in h else 0\n            metadata.append((instance_num, f))\n        except: continue\n            \n    metadata.sort(key=lambda x: x)\n    ordered_files = [m for m in metadata]\n    if len(ordered_files) == 0:\n        return np.zeros((num_slices, target_size, target_size), dtype=np.float32)\n\n    mid_idx = len(ordered_files) // 2\n    step = max(1, len(ordered_files) // (num_slices + 1))\n    indices = [max(0, min(mid_idx - (num_slices // 2) * step + i * step, len(ordered_files) - 1)) for i in range(num_slices)]\n    \n    volume_slices = []\n    for idx in indices:\n        try:\n            dicom = pydicom.dcmread(ordered_files[idx])\n            img = dicom.pixel_array.astype(float)\n            if 'RescaleSlope' in dicom and 'RescaleIntercept' in dicom:\n                img = img * float(dicom.RescaleSlope) + float(dicom.RescaleIntercept)\n            img_min, img_max = img.min(), img.max()\n            if img_max > img_min: img = (img - img_min) / (img_max - img_min)\n            else: img = np.zeros_like(img)\n            \n            if _HAS_CV2: img_rescaled = cv2.resize(img, (target_size, target_size), interpolation=cv2.INTER_AREA)\n            else: img_rescaled = np.zeros((target_size, target_size))\n            volume_slices.append(img_rescaled)\n        except:\n            volume_slices.append(np.zeros((target_size, target_size)))\n\n    volume_25d = np.stack(volume_slices, axis=0).astype(np.float32)\n    if cache_dir is not None:\n        try: np.save(cache_file, volume_25d)\n        except: pass\n    return volume_25d\n\nclass RSNAKneeDataset25D(Dataset):\n    def __init__(self, df, data_images_path, labels_cols, is_train=True, config=CONFIG):\n        self.df = df.reset_index(drop=True)\n        self.data_images_path = Path(data_images_path)\n        self.labels_cols = labels_cols\n        self.is_train = is_train\n        self.config = config\n\n    def __len__(self): return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_id = str(row[ID_COL])\n        study_dir = self.data_images_path / study_id\n        series_dirs = list(study_dir.glob(\"*\")) if study_dir.exists() else []\n        \n        if len(series_dirs) > 0:\n            image_tensor = load_series_slices_25d(series_dirs, self.config['num_slices'], self.config['img_size'], self.config['cache_dir'])\n        else:\n            image_tensor = np.zeros((self.config['num_slices'], self.config['img_size'], self.config['img_size']), dtype=np.float32)\n\n        targets, confidences = [], []\n        for col in self.labels_cols:\n            # --- FIX CRÍTICO DE SUBMISSION ---\n            # Si la columna no existe en el DataFrame (como ocurre en test.csv), asignamos ceros por defecto\n            if col in row.index:\n                target_val = row[col]\n                conf_val = row[f\"{col}_confidence\"] if f\"{col}_confidence\" in row.index else 1.0\n            else:\n                target_val, conf_val = 0.0, 0.0\n                \n            if pd.isna(target_val): \n                target_val, conf_val = 0.0, 0.0\n            targets.append(float(target_val))\n            confidences.append(float(conf_val))\n\n        image_tensor = torch.tensor(image_tensor, dtype=torch.float32)\n        targets_tensor = torch.tensor(targets, dtype=torch.float32)\n        confidences_tensor = torch.tensor(confidences, dtype=torch.float32)\n\n        if self.is_train and self.config['neg_confidence_cap'] is not None:\n            confidences_tensor = apply_negative_confidence_cap(targets_tensor, confidences_tensor, self.config['neg_confidence_cap'])\n\n        return image_tensor, targets_tensor, confidences_tensor\n\nprint(\"✅ Módulos de carga médica blindados para datos de prueba.\")\n# ============================================================\n# CELDA 3: MODELO TORCHVISION, ENTRENAMIENTO Y SUBMISSION K-FOLD\n# ============================================================\n\nclass RSNAKneeModel25D(nn.Module):\n    def __init__(self, in_channels=CONFIG['num_slices'], num_classes=CONFIG['num_classes']):\n        super().__init__()\n        self.backbone = models.resnet34(weights=None)\n        \n        original_conv = self.backbone.conv1\n        self.backbone.conv1 = nn.Conv2d(\n            in_channels=in_channels, out_channels=original_conv.out_channels,\n            kernel_size=original_conv.kernel_size, stride=original_conv.stride,\n            padding=original_conv.padding, bias=original_conv.bias\n        )\n        \n        num_features = self.backbone.fc.in_features\n        self.backbone.fc = nn.Sequential(\n            nn.Dropout(p=0.3),\n            nn.Linear(num_features, num_classes)\n        )\n\n    def forward(self, x): return self.backbone(x.float())\n\n# Asegurar vectores basales de confianza para entrenamiento\nfor label in LABELS:\n    if f\"{label}_confidence\" not in df_train_raw.columns:\n        df_train_raw[f\"{label}_confidence\"] = np.where(df_train_raw[label].notna(), 1.0, 0.6)\n        df_train_raw[label] = df_train_raw[label].fillna(0)\n\nIMAGES_DIR = DATA_PATH / \"train_images\"\nkf = KFold(n_splits=5, shuffle=True, random_state=CONFIG['seed'])\noof_predictions = pd.DataFrame(0.0, index=df_train_raw.index, columns=LABELS)\noof_predictions[ID_COL] = df_train_raw[ID_COL]\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"💻 Corriendo bucle en acelerador físico: {device}\\n\")\nCHECKPOINT_PATHS = []\n\n# --- VALIDACIÓN CRUZADA EN LOTE ---\nfor fold, (train_idx, val_idx) in enumerate(kf.split(df_train_raw)):\n    print(f\"📦 PROCESANDO PLIEGUE {fold + 1} / 5\")\n    df_train_fold = df_train_raw.iloc[train_idx]\n    df_val_fold = df_train_raw.iloc[val_idx]\n    \n    train_loader = DataLoader(RSNAKneeDataset25D(df_train_fold, IMAGES_DIR, LABELS, is_train=True), \n                              batch_size=CONFIG['batch_size'], shuffle=True, num_workers=CONFIG['num_workers'], pin_memory=CONFIG['pin_memory'], drop_last=True)\n    val_loader = DataLoader(RSNAKneeDataset25D(df_val_fold, IMAGES_DIR, LABELS, is_train=False), \n                            batch_size=CONFIG['batch_size'], shuffle=False, num_workers=CONFIG['num_workers'], pin_memory=CONFIG['pin_memory'])\n    \n    model = RSNAKneeModel25D().to(device)\n    criterion = ConfidenceWeightedBCEWithLogitsLoss()\n    optimizer = torch.optim.AdamW(model.parameters(), lr=CONFIG['lr'], weight_decay=1e-4)\n    \n    best_val_loss = float('inf')\n    fold_ckpt_path = Path(f\"best_model_fold_{fold}.pth\")\n    \n    for epoch in range(1, CONFIG['epochs'] + 1):\n        epoch_start = time.time()\n        model.train()\n        train_loss = 0.0\n        \n        for images, targets, confidences in train_loader:\n            images, targets, confidences = images.to(device, non_blocking=True), targets.to(device, non_blocking=True), confidences.to(device, non_blocking=True)\n            optimizer.zero_grad()\n            loss = criterion(model(images), targets, confidences)\n            loss.backward()\n            optimizer.step()\n            train_loss += loss.item() * images.size(0)\n            \n        model.eval()\n        val_loss = 0.0\n        with torch.no_grad():\n            for images, targets, confidences in val_loader:\n                images, targets, confidences = images.to(device, non_blocking=True), targets.to(device, non_blocking=True), confidences.to(device, non_blocking=True)\n                val_loss += criterion(model(images), targets, confidences).item() * images.size(0)\n        \n        print(f\"   Época {epoch:02d} | Loss Train: {train_loss/len(train_loader.dataset):.4f} | Loss Val: {val_loss/len(val_loader.dataset):.4f} | {time.time()-epoch_start:.1f}s\")\n        if (val_loss/len(val_loader.dataset)) < best_val_loss:\n            best_val_loss = val_loss/len(val_loader.dataset)\n            torch.save(model.state_dict(), fold_ckpt_path)\n            \n    CHECKPOINT_PATHS.append(fold_ckpt_path)\n    del model, optimizer; torch.cuda.empty_cache()\n\n# --- INFERENCIA SUBMISSION ENSEMBLE ---\nprint(\"\\n💾 CONSTRUYENDO ARCHIVO SUBMISSION.CSV FINAL INTERNO...\")\nTEST_CSV_PATH, TEST_IMAGES_DIR = DATA_PATH / \"test.csv\", DATA_PATH / \"test_images\"\nif TEST_CSV_PATH.exists(): \n    df_test = pd.read_csv(TEST_CSV_PATH)\nelse: \n    df_test = df_train_raw.head(5)[[ID_COL]].copy()\n    TEST_IMAGES_DIR = IMAGES_DIR\n\ntest_loader = DataLoader(RSNAKneeDataset25D(df_test, TEST_IMAGES_DIR, LABELS, is_train=False), \n                         batch_size=CONFIG['batch_size'], shuffle=False, num_workers=CONFIG['num_workers'], pin_memory=CONFIG['pin_memory'])\n\nensemble_preds = np.zeros((len(df_test), len(LABELS)), dtype=np.float32)\nmodel = RSNAKneeModel25D().to(device)\n\nfor ckpt_path in CHECKPOINT_PATHS:\n    if ckpt_path.exists():\n        model.load_state_dict(torch.load(ckpt_path, map_location=device))\n        model.eval()\n        fold_preds = []\n        with torch.no_grad():\n            for images, _, _ in test_loader:\n                fold_preds.append(torch.sigmoid(model(images.to(device, non_blocking=True))).cpu().numpy())\n        ensemble_preds += np.vstack(fold_preds)\n\ndf_submission = pd.DataFrame(ensemble_preds / len(CHECKPOINT_PATHS), columns=LABELS)\ndf_submission.insert(0, ID_COL, df_test[ID_COL].values)\ndf_submission.to_csv(\"submission.csv\", index=False)\nprint(\"🚀 Proceso terminado con éxito. El archivo 'submission.csv' está completamente listo para competir.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T01:44:45.754941Z","iopub.execute_input":"2026-08-13T01:44:45.755459Z","iopub.status.idle":"2026-08-13T02:05:29.026083Z","shell.execute_reply.started":"2026-08-13T01:44:45.755401Z","shell.execute_reply":"2026-08-13T02:05:29.02516Z"}},"outputs":[],"execution_count":null}]}