{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":14774,"databundleVersionId":875431,"sourceType":"competition"},{"sourceId":2812287,"sourceType":"datasetVersion","datasetId":1719146}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================================\n# MOBILE-OPTIMIZED DIABETIC RETINOPATHY GRADING WITH ORDINAL REGRESSION\n# Enhanced for Mobile Deployment with APTOS + IDRiD datasets\n# ============================================================================\n\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# ============================================================================\n# SECTION 1: SETUP & CONFIGURATION\n# ============================================================================\n!pip install -q timm==0.9.12 albumentations==1.3.1 scikit-plot scikit-learn opencv-python-headless\n!pip install -q --no-deps coral-pytorch\n\nimport os\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport timm\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import cohen_kappa_score, roc_auc_score\nfrom coral_pytorch.losses import coral_loss\nfrom coral_pytorch.dataset import levels_from_labelbatch\nimport matplotlib.pyplot as plt\n\n# Set seeds for reproducibility\ndef set_seed(seed=42):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\nset_seed()\n\n# Configuration\nIMG_SIZE = 384\nBATCH_SIZE = 32\nEPOCHS = 10\nN_FOLDS = 3\nLR = 3e-4\nLABEL_SMOOTHING = 0.1\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"🚀 Using device: {DEVICE}\")\n\n# ============================================================================\n# SECTION 2: DATA LOADING (APTOS + IDRiD ONLY)\n# ============================================================================\ndef load_datasets():\n    print(\"📂 Loading APTOS and IDRiD datasets...\")\n    \n    # APTOS\n    aptos_df = pd.read_csv(\"/kaggle/input/aptos2019-blindness-detection/train.csv\")\n    aptos_df['image_path'] = aptos_df['id_code'].apply(\n        lambda x: f\"/kaggle/input/aptos2019-blindness-detection/train_images/{x}.png\")\n    aptos_df['dataset'] = 'aptos'\n    \n    # IDRiD - CORRECTED PATHS\n    idrid_df = pd.read_csv(\"/kaggle/input/idrid-dataset/idrid_labels.csv\")\n    \n    # Clean diagnosis column - convert to integer\n    idrid_df['diagnosis'] = pd.to_numeric(idrid_df['diagnosis'], errors='coerce')\n    idrid_df = idrid_df.dropna(subset=['diagnosis'])\n    idrid_df['diagnosis'] = idrid_df['diagnosis'].astype(int)\n    \n    # CORRECTED image paths\n    idrid_df['image_path'] = idrid_df['id_code'].apply(\n        lambda x: f\"/kaggle/input/idrid-dataset/Imagenes/Imagenes/{x}.jpg\")\n    idrid_df['dataset'] = 'idrid'\n    \n    # Combine datasets\n    full_df = pd.concat([aptos_df, idrid_df], ignore_index=True)\n    \n    # Filter out missing images\n    print(\"🔍 Checking image paths exist...\")\n    full_df['exists'] = full_df['image_path'].apply(os.path.exists)\n    missing_count = len(full_df) - full_df['exists'].sum()\n    print(f\"⚠️ Missing images: {missing_count}/{len(full_df)}\")\n    \n    # Show samples of missing files for debugging\n    if missing_count > 0:\n        missing_samples = full_df[~full_df['exists']].sample(min(5, missing_count), random_state=42)\n        print(\"Sample missing paths:\")\n        for path in missing_samples['image_path']:\n            print(f\" - {path}\")\n    \n    return full_df[full_df['exists']].drop(columns=['exists']).reset_index(drop=True)\n\n# ============================================================================\n# SECTION 3: ADVANCED PREPROCESSING (BEN GRAHAM + CIRCULAR CROP)\n# ============================================================================\ndef crop_image_from_gray(img, tol=7):\n    if img.ndim == 2:\n        mask = img > tol\n        return img[np.ix_(mask.any(1), mask.any(0))]\n    elif img.ndim == 3:\n        gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n        mask = gray_img > tol\n        check_shape = img[:,:,0][np.ix_(mask.any(1), mask.any(0))].shape[0]\n        if check_shape == 0: \n            return img\n        img1 = img[:,:,0][np.ix_(mask.any(1), mask.any(0))]\n        img2 = img[:,:,1][np.ix_(mask.any(1), mask.any(0))]\n        img3 = img[:,:,2][np.ix_(mask.any(1), mask.any(0))]\n        return np.stack([img1, img2, img3], axis=-1)\n\ndef circle_crop(img):\n    img = crop_image_from_gray(img)\n    height, width, _ = img.shape\n    x = width//2\n    y = height//2\n    r = np.amin((x,y))\n    circle_img = np.zeros((height, width), np.uint8)\n    cv2.circle(circle_img, (x,y), int(r), 1, thickness=-1)\n    img = cv2.bitwise_and(img, img, mask=circle_img)\n    return crop_image_from_gray(img)\n\ndef ben_graham_preprocess(img, sigmaX=30):\n    img = circle_crop(img)\n    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n    img = cv2.addWeighted(img, 4, cv2.GaussianBlur(img, (0,0), sigmaX), -4, 128)\n    return img\n\n# ============================================================================\n# SECTION 4: DATASET & AUGMENTATIONS\n# ============================================================================\nclass DRDataset(Dataset):\n    def __init__(self, df, transform=None, is_train=True):\n        self.df = df\n        self.transform = transform\n        self.is_train = is_train\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = row['image_path']\n        \n        # Load and preprocess image\n        img = cv2.imread(img_path)\n        if img is None:\n            raise ValueError(f\"Image not found: {img_path}\")\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = ben_graham_preprocess(img)\n        \n        # Apply augmentations\n        if self.transform:\n            img = self.transform(image=img)['image']\n            \n        # Get label and convert to CORAL format\n        label = row['diagnosis']\n        return img, torch.tensor(label, dtype=torch.int64)\n\n# Augmentations\ndef get_train_transforms():\n    return A.Compose([\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=15, p=0.5),\n        A.RandomBrightnessContrast(p=0.5),\n        A.CoarseDropout(max_holes=8, max_height=32, max_width=32, p=0.3),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\ndef get_val_transforms():\n    return A.Compose([\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\n# ============================================================================\n# SECTION 5: MOBILENETV3 WITH ORDINAL REGRESSION (CORAL)\n# ============================================================================\nclass MobileNetV3_CORAL(nn.Module):\n    def __init__(self, num_classes=5):\n        super().__init__()\n        self.num_classes = num_classes\n        self.backbone = timm.create_model('mobilenetv3_large_100', pretrained=True, num_classes=0)\n        in_features = self.backbone.num_features\n        self.fc = nn.Linear(in_features, num_classes-1)  # CORAL requires num_classes-1 outputs\n        \n    def forward(self, x):\n        features = self.backbone(x)\n        logits = self.fc(features)\n        return logits\n\n# ============================================================================\n# SECTION 6: LOSS FUNCTION WITH LABEL SMOOTHING\n# ============================================================================\n\ndef coral_loss_with_smoothing(logits, labels, smoothing=0.0):\n    # Convert labels to CORAL levels and move to same device as logits\n    levels = levels_from_labelbatch(labels, num_classes=5).float().to(logits.device)\n    \n    if smoothing > 0.0:\n        levels = levels * (1 - smoothing) + 0.5 * smoothing\n\n    return coral_loss(logits, levels)\n\n# ============================================================================\n# SECTION 7: TRAINING & VALIDATION FUNCTIONS\n# ============================================================================\ndef train_epoch(model, loader, optimizer, scheduler):\n    model.train()\n    running_loss = 0.0\n    \n    for images, labels in loader:\n        images, labels = images.to(DEVICE), labels.to(DEVICE)\n        \n        optimizer.zero_grad()\n        logits = model(images)\n        loss = coral_loss_with_smoothing(logits, labels, LABEL_SMOOTHING)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item() * images.size(0)\n        \n    if scheduler:\n        scheduler.step()\n        \n    return running_loss / len(loader.dataset)\n\ndef validate(model, loader):\n    model.eval()\n    all_labels = []\n    all_preds = []\n    running_loss = 0.0\n    \n    with torch.no_grad():\n        for images, labels in loader:\n            images, labels = images.to(DEVICE), labels.to(DEVICE)\n            \n            logits = model(images)\n            loss = coral_loss_with_smoothing(logits, labels)\n            running_loss += loss.item() * images.size(0)\n            \n            # Convert logits to predictions\n            probas = torch.sigmoid(logits)\n            preds = torch.sum(probas > 0.5, dim=1)\n            \n            all_labels.append(labels.cpu())\n            all_preds.append(preds.cpu())\n    \n    all_labels = torch.cat(all_labels).numpy()\n    all_preds = torch.cat(all_preds).numpy()\n    loss = running_loss / len(loader.dataset)\n    qwk = cohen_kappa_score(all_labels, all_preds, weights='quadratic')\n    \n    return loss, qwk, all_labels, all_preds\n\n# ============================================================================\n# SECTION 8: TEST TIME AUGMENTATION (TTA)\n# ============================================================================\ndef tta_predict(model, image, n_aug=5):\n    model.eval()\n    aug = A.Compose([\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.05, rotate_limit=15, p=0.5),\n    ])\n    \n    with torch.no_grad():\n        logits = []\n        for _ in range(n_aug):\n            augmented = aug(image=image)['image']\n            augmented = get_val_transforms()(image=augmented)['image']\n            augmented = augmented.unsqueeze(0).to(DEVICE)\n            logits.append(model(augmented))\n        \n        logits = torch.mean(torch.stack(logits), dim=0)\n        probas = torch.sigmoid(logits)\n        pred = torch.sum(probas > 0.5, dim=1).item()\n        return pred\n\n# ============================================================================\n# SECTION 9: MAIN TRAINING LOOP WITH K-FOLD\n# ============================================================================\ndef train_model():\n    full_df = load_datasets()\n    skf = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=42)\n    results = []\n    fold_models = []\n    \n    for fold, (train_idx, val_idx) in enumerate(skf.split(full_df, full_df['diagnosis'])):\n        print(f\"\\n{'='*50}\")\n        print(f\"🚀 FOLD {fold+1}/{N_FOLDS}\")\n        \n        # Split data\n        train_df = full_df.iloc[train_idx]\n        val_df = full_df.iloc[val_idx]\n        \n        # Create datasets and loaders\n        train_ds = DRDataset(train_df, get_train_transforms())\n        val_ds = DRDataset(val_df, get_val_transforms())\n        \n        train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, \n                                 shuffle=True, num_workers=2, pin_memory=True)\n        val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, \n                               shuffle=False, num_workers=2, pin_memory=True)\n        \n        # Create model\n        model = MobileNetV3_CORAL().to(DEVICE)\n        optimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-4)\n        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, EPOCHS)\n        \n        best_qwk = 0.0\n        for epoch in range(1, EPOCHS+1):\n            train_loss = train_epoch(model, train_loader, optimizer, scheduler)\n            val_loss, val_qwk, _, _ = validate(model, val_loader)\n            \n            print(f\"Epoch {epoch}/{EPOCHS} | \"\n                  f\"Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | \"\n                  f\"QWK: {val_qwk:.4f}\")\n            \n            # Save best model for this fold\n            if val_qwk > best_qwk:\n                best_qwk = val_qwk\n                torch.save(model.state_dict(), f\"best_fold{fold}.pth\")\n                print(f\"✅ New best model saved with QWK: {val_qwk:.4f}\")\n        \n        # Load best model for this fold\n        model.load_state_dict(torch.load(f\"best_fold{fold}.pth\"))\n        fold_models.append(model)\n        results.append(best_qwk)\n        \n        # Final validation\n        _, final_qwk, labels, preds = validate(model, val_loader)\n        print(f\"\\n🔥 Final Validation QWK: {final_qwk:.4f}\")\n    \n    # Print overall results\n    print(\"\\n📊 Final Results:\")\n    for i, qwk in enumerate(results):\n        print(f\"Fold {i+1} QWK: {qwk:.4f}\")\n    print(f\"Mean QWK: {np.mean(results):.4f} ± {np.std(results):.4f}\")\n    \n    return fold_models\n\n# ============================================================================\n# SECTION 10: MODEL CALIBRATION\n# ============================================================================\ndef calibrate_model(model, val_loader):\n    \"\"\"Apply temperature scaling to calibrate model\"\"\"\n    logits_list = []\n    labels_list = []\n    \n    model.eval()\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images = images.to(DEVICE)\n            logits = model(images)\n            logits_list.append(logits)\n            labels_list.append(labels)\n    \n    logits = torch.cat(logits_list)\n    labels = torch.cat(labels_list)\n    \n    # Temperature scaling\n    temperature = nn.Parameter(torch.ones(1).to(DEVICE))\n    optimizer = torch.optim.LBFGS([temperature], lr=0.01)\n    \n    def eval():\n        optimizer.zero_grad()\n        loss = coral_loss(logits / temperature, labels)\n        loss.backward()\n        return loss\n    \n    optimizer.step(eval)\n    print(f\"Calibration temperature: {temperature.item():.4f}\")\n    return temperature.item()\n\n# ============================================================================\n# SECTION 11: MODEL EXPORT FOR MOBILE\n# ============================================================================\ndef export_for_mobile(model, temperature=1.0):\n    model.eval()\n    dummy_input = torch.randn(1, 3, IMG_SIZE, IMG_SIZE).to(DEVICE)\n    \n    # Apply temperature scaling\n    class CalibratedModel(nn.Module):\n        def __init__(self, model, temperature):\n            super().__init__()\n            self.model = model\n            self.temperature = temperature\n            \n        def forward(self, x):\n            logits = self.model(x) / self.temperature\n            probas = torch.sigmoid(logits)\n            pred = torch.sum(probas > 0.5, dim=1)\n            return pred\n    \n    calibrated_model = CalibratedModel(model, temperature)\n    \n    # Export to ONNX\n    torch.onnx.export(\n        calibrated_model,\n        dummy_input,\n        \"dr_mobilenetv3.onnx\",\n        input_names=[\"input\"],\n        output_names=[\"output\"],\n        dynamic_axes={\"input\": {0: \"batch\"}, \"output\": {0: \"batch\"}},\n        opset_version=12\n    )\n    \n    # Export to TorchScript\n    scripted_model = torch.jit.script(calibrated_model)\n    scripted_model.save(\"dr_mobilenetv3.pt\")\n    print(\"✅ Model exported for mobile deployment\")\n\n# ============================================================================\n# EXECUTION FLOW\n# ============================================================================\nif __name__ == \"__main__\":\n    # Train model\n    models = train_model()\n    \n    # Create ensemble model\n    class EnsembleModel(nn.Module):\n        def __init__(self, models):\n            super().__init__()\n            self.models = models\n            \n        def forward(self, x):\n            logits = [model(x) for model in self.models]\n            return torch.mean(torch.stack(logits), dim=0)\n    \n    ensemble = EnsembleModel(models).to(DEVICE)\n    \n    # Calibrate ensemble - create validation set\n    from sklearn.model_selection import train_test_split\n    _, val_df = train_test_split(full_df, test_size=0.2, \n                                stratify=full_df['diagnosis'], \n                                random_state=42)\n    val_loader = DataLoader(DRDataset(val_df, get_val_transforms()), \n                           batch_size=BATCH_SIZE, shuffle=False)\n    \n    temperature = calibrate_model(ensemble, val_loader)\n    \n    # Export for mobile\n    export_for_mobile(ensemble, temperature)\n    \n    # Generate downloadable link\n    from IPython.display import FileLink\n    print(\"📥 Download trained model:\")\n    FileLink(\"dr_mobilenetv3.onnx\")\n    FileLink(\"dr_mobilenetv3.pt\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-31T21:35:09.233823Z","iopub.execute_input":"2025-05-31T21:35:09.234459Z","iopub.status.idle":"2025-06-01T05:52:54.235654Z","shell.execute_reply.started":"2025-05-31T21:35:09.234429Z","shell.execute_reply":"2025-06-01T05:52:54.234057Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# FIXED MODEL CALIBRATION FUNCTION\n# ============================================================================\ndef calibrate_model(model, val_loader):\n    \"\"\"Apply temperature scaling to calibrate model\"\"\"\n    logits_list = []\n    labels_list = []\n    \n    model.eval()\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images = images.to(DEVICE)\n            logits = model(images)\n            logits_list.append(logits)\n            labels_list.append(labels)\n    \n    logits = torch.cat(logits_list).to(DEVICE)\n    labels = torch.cat(labels_list).to(DEVICE)\n    \n    # Convert labels to CORAL levels format - THIS WAS THE MISSING PIECE!\n    levels = levels_from_labelbatch(labels, num_classes=5).float().to(DEVICE)\n    \n    # Temperature scaling\n    temperature = nn.Parameter(torch.ones(1).to(DEVICE))\n    optimizer = torch.optim.LBFGS([temperature], lr=0.01, max_iter=50)\n    \n    def eval():\n        optimizer.zero_grad()\n        # Now both logits/temperature and levels have the same shape\n        loss = coral_loss(logits / temperature, levels)\n        loss.backward()\n        return loss\n    \n    optimizer.step(eval)\n    print(f\"Calibration temperature: {temperature.item():.4f}\")\n    return temperature.item()\n\n# ============================================================================\n# FIXED EXECUTION FLOW\n# ============================================================================\nif __name__ == \"__main__\":\n    # 1. LOAD EXISTING MODELS INSTEAD OF RETRAINING\n    full_df = load_datasets()\n    models = []\n    \n    for fold in range(N_FOLDS):\n        model_path = f\"best_fold{fold}.pth\"\n        if os.path.exists(model_path):\n            print(f\"✅ Loading pre-trained model: {model_path}\")\n            model = MobileNetV3_CORAL().to(DEVICE)\n            model.load_state_dict(torch.load(model_path, map_location=DEVICE))\n            model.eval()\n            models.append(model)\n        else:\n            print(f\"❌ Model file not found: {model_path}\")\n    \n    if not models:\n        print(\"❌ No pre-trained models found! Please run training first.\")\n        exit()\n    \n    print(f\"✅ Loaded {len(models)} models for ensemble\")\n    \n    # 2. CREATE ENSEMBLE MODEL\n    class EnsembleModel(nn.Module):\n        def __init__(self, models):\n            super().__init__()\n            self.models = nn.ModuleList(models)  # Use ModuleList for proper registration\n            \n        def forward(self, x):\n            logits = [model(x) for model in self.models]\n            return torch.mean(torch.stack(logits), dim=0)\n    \n    ensemble = EnsembleModel(models).to(DEVICE)\n    \n    # 3. CALIBRATE ENSEMBLE\n    from sklearn.model_selection import train_test_split\n    \n    try:\n        _, val_df = train_test_split(\n            full_df, \n            test_size=0.2, \n            stratify=full_df['diagnosis'], \n            random_state=42\n        )\n        \n        # Use smaller batch size for calibration to avoid memory issues\n        CAL_BATCH_SIZE = 8  # Even smaller to be safe\n        val_loader = DataLoader(\n            DRDataset(val_df, get_val_transforms()), \n            batch_size=CAL_BATCH_SIZE, \n            shuffle=False,\n            num_workers=0  # Avoid multiprocessing issues\n        )\n        \n        print(\"🔧 Starting model calibration...\")\n        temperature = calibrate_model(ensemble, val_loader)\n        print(f\"✅ Calibration complete! Temperature: {temperature:.4f}\")\n        \n    except Exception as e:\n        print(f\"⚠️ Calibration failed: {e}\")\n        print(\"🔧 Using default temperature = 1.0\")\n        temperature = 1.0\n    \n    # 4. EXPORT FOR MOBILE\n    try:\n        print(\"📦 Exporting model for mobile deployment...\")\n        export_for_mobile(ensemble, temperature)\n        print(\"✅ Mobile export complete!\")\n    except Exception as e:\n        print(f\"⚠️ Mobile export failed: {e}\")\n        print(\"💾 Saving ensemble model as PyTorch checkpoint instead...\")\n        torch.save({\n            'model_state_dict': ensemble.state_dict(),\n            'temperature': temperature,\n            'num_classes': 5,\n            'img_size': IMG_SIZE\n        }, 'dr_ensemble_checkpoint.pth')\n        print(\"✅ Checkpoint saved as 'dr_ensemble_checkpoint.pth'\")\n    \n    # 5. GENERATE DOWNLOAD LINKS\n    try:\n        from IPython.display import FileLink, display\n        print(\"\\n📥 Download trained model files:\")\n        \n        # Check which files exist and display links\n        files_to_check = [\n            \"dr_mobilenetv3.onnx\",\n            \"dr_mobilenetv3.pt\", \n            \"dr_ensemble_checkpoint.pth\"\n        ]\n        \n        for filename in files_to_check:\n            if os.path.exists(filename):\n                print(f\"✅ {filename} ready for download\")\n                display(FileLink(filename))\n            else:\n                print(f\"❌ {filename} not found\")\n                \n    except ImportError:\n        print(\"📥 Files saved locally (IPython not available for download links)\")\n    \n    # 6. PRINT FINAL RESULTS\n    print(\"\\n📊 Final Training Results:\")\n    print(\"Fold 1 QWK: 0.8977\")\n    print(\"Fold 2 QWK: 0.9010\") \n    print(\"Fold 3 QWK: 0.9105\")\n    mean_qwk = np.mean([0.8977, 0.9010, 0.9105])\n    std_qwk = np.std([0.8977, 0.9010, 0.9105])\n    print(f\"Mean QWK: {mean_qwk:.4f} ± {std_qwk:.4f}\")\n    print(f\"\\n🎯 This is an excellent result! QWK > 0.89 is considered very strong performance.\")\n    \n    # 7. OPTIONAL: Test inference on a sample\n    try:\n        print(\"\\n🧪 Testing inference on a sample image...\")\n        sample_row = val_df.iloc[0]\n        \n        # Load and preprocess sample image\n        img = cv2.imread(sample_row['image_path'])\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = ben_graham_preprocess(img)\n        \n        # Apply transforms and predict\n        img_tensor = get_val_transforms()(image=img)['image'].unsqueeze(0).to(DEVICE)\n        \n        with torch.no_grad():\n            logits = ensemble(img_tensor) / temperature\n            probas = torch.sigmoid(logits)\n            pred = torch.sum(probas > 0.5, dim=1).item()\n            \n        print(f\"Sample prediction: Grade {pred} (True: {sample_row['diagnosis']})\")\n        print(f\"Confidence scores: {probas.cpu().numpy().flatten()}\")\n        \n    except Exception as e:\n        print(f\"⚠️ Sample inference test failed: {e}\")\n    \n    print(\"\\n🎉 Pipeline completed successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T06:22:04.046182Z","iopub.execute_input":"2025-06-01T06:22:04.046495Z","iopub.status.idle":"2025-06-01T06:26:51.290129Z","shell.execute_reply.started":"2025-06-01T06:22:04.046472Z","shell.execute_reply":"2025-06-01T06:26:51.289382Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# STANDALONE DIABETIC RETINOPATHY ANALYSIS & VISUALIZATION\n# Fixed version that works with existing trained models\n# ============================================================================\n\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport os\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport timm\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (confusion_matrix, classification_report, \n                            cohen_kappa_score, roc_curve, auc, \n                            precision_recall_curve, average_precision_score)\nfrom sklearn.calibration import calibration_curve\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom coral_pytorch.dataset import levels_from_labelbatch\n\n# Configuration\nIMG_SIZE = 384\nBATCH_SIZE = 16  # Reduced for stability\nN_FOLDS = 3\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"🚀 Using device: {DEVICE}\")\n\n# ============================================================================\n# DATA LOADING & PREPROCESSING (COPY FROM ORIGINAL)\n# ============================================================================\ndef load_datasets():\n    print(\"📂 Loading APTOS and IDRiD datasets...\")\n    \n    # APTOS\n    aptos_df = pd.read_csv(\"/kaggle/input/aptos2019-blindness-detection/train.csv\")\n    aptos_df['image_path'] = aptos_df['id_code'].apply(\n        lambda x: f\"/kaggle/input/aptos2019-blindness-detection/train_images/{x}.png\")\n    aptos_df['dataset'] = 'aptos'\n    \n    # IDRiD\n    idrid_df = pd.read_csv(\"/kaggle/input/idrid-dataset/idrid_labels.csv\")\n    idrid_df['diagnosis'] = pd.to_numeric(idrid_df['diagnosis'], errors='coerce')\n    idrid_df = idrid_df.dropna(subset=['diagnosis'])\n    idrid_df['diagnosis'] = idrid_df['diagnosis'].astype(int)\n    idrid_df['image_path'] = idrid_df['id_code'].apply(\n        lambda x: f\"/kaggle/input/idrid-dataset/Imagenes/Imagenes/{x}.jpg\")\n    idrid_df['dataset'] = 'idrid'\n    \n    # Combine datasets\n    full_df = pd.concat([aptos_df, idrid_df], ignore_index=True)\n    \n    # Filter existing images\n    print(\"🔍 Checking image paths exist...\")\n    full_df['exists'] = full_df['image_path'].apply(os.path.exists)\n    missing_count = len(full_df) - full_df['exists'].sum()\n    print(f\"⚠️ Missing images: {missing_count}/{len(full_df)}\")\n    \n    return full_df[full_df['exists']].drop(columns=['exists']).reset_index(drop=True)\n\ndef crop_image_from_gray(img, tol=7):\n    if img.ndim == 2:\n        mask = img > tol\n        return img[np.ix_(mask.any(1), mask.any(0))]\n    elif img.ndim == 3:\n        gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n        mask = gray_img > tol\n        check_shape = img[:,:,0][np.ix_(mask.any(1), mask.any(0))].shape[0]\n        if check_shape == 0: \n            return img\n        img1 = img[:,:,0][np.ix_(mask.any(1), mask.any(0))]\n        img2 = img[:,:,1][np.ix_(mask.any(1), mask.any(0))]\n        img3 = img[:,:,2][np.ix_(mask.any(1), mask.any(0))]\n        return np.stack([img1, img2, img3], axis=-1)\n\ndef circle_crop(img):\n    img = crop_image_from_gray(img)\n    height, width, _ = img.shape\n    x = width//2\n    y = height//2\n    r = np.amin((x,y))\n    circle_img = np.zeros((height, width), np.uint8)\n    cv2.circle(circle_img, (x,y), int(r), 1, thickness=-1)\n    img = cv2.bitwise_and(img, img, mask=circle_img)\n    return crop_image_from_gray(img)\n\ndef ben_graham_preprocess(img, sigmaX=30):\n    img = circle_crop(img)\n    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n    img = cv2.addWeighted(img, 4, cv2.GaussianBlur(img, (0,0), sigmaX), -4, 128)\n    return img\n\nclass DRDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df\n        self.transform = transform\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = row['image_path']\n        \n        img = cv2.imread(img_path)\n        if img is None:\n            raise ValueError(f\"Image not found: {img_path}\")\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = ben_graham_preprocess(img)\n        \n        if self.transform:\n            img = self.transform(image=img)['image']\n            \n        label = row['diagnosis']\n        return img, torch.tensor(label, dtype=torch.int64)\n\ndef get_val_transforms():\n    return A.Compose([\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\n# ============================================================================\n# MODEL DEFINITION (COPY FROM ORIGINAL)\n# ============================================================================\nclass MobileNetV3_CORAL(nn.Module):\n    def __init__(self, num_classes=5):\n        super().__init__()\n        self.num_classes = num_classes\n        self.backbone = timm.create_model('mobilenetv3_large_100', pretrained=True, num_classes=0)\n        in_features = self.backbone.num_features\n        self.fc = nn.Linear(in_features, num_classes-1)\n        \n    def forward(self, x):\n        features = self.backbone(x)\n        logits = self.fc(features)\n        return logits\n\n# ============================================================================\n# FIXED PROBABILITY CONVERSION FOR CORAL\n# ============================================================================\ndef coral_to_class_probabilities(logits):\n    \"\"\"Convert CORAL logits to proper class probabilities\"\"\"\n    # Apply sigmoid to get cumulative probabilities\n    cumulative_probs = torch.sigmoid(logits)\n    \n    # Convert to individual class probabilities\n    batch_size = logits.shape[0]\n    class_probs = torch.zeros(batch_size, 5, device=logits.device)\n    \n    # P(y=0) = 1 - P(y>=1)\n    class_probs[:, 0] = 1 - cumulative_probs[:, 0]\n    \n    # P(y=k) = P(y>=k) - P(y>=k+1) for k = 1,2,3\n    for i in range(1, 4):\n        class_probs[:, i] = cumulative_probs[:, i-1] - cumulative_probs[:, i]\n    \n    # P(y=4) = P(y>=4)\n    class_probs[:, 4] = cumulative_probs[:, 3]\n    \n    # Ensure probabilities are valid\n    class_probs = torch.clamp(class_probs, min=0.0, max=1.0)\n    \n    # Normalize to ensure they sum to 1\n    class_probs = class_probs / class_probs.sum(dim=1, keepdim=True)\n    \n    return class_probs\n\n# ============================================================================\n# FIXED ANALYSIS FUNCTIONS\n# ============================================================================\ndef get_predictions_and_probabilities(model, loader):\n    \"\"\"Get predictions and properly normalized probabilities\"\"\"\n    model.eval()\n    all_labels = []\n    all_preds = []\n    all_probas = []\n    \n    with torch.no_grad():\n        for images, labels in loader:\n            images, labels = images.to(DEVICE), labels.to(DEVICE)\n            \n            # Get logits and convert to probabilities\n            logits = model(images)\n            class_probas = coral_to_class_probabilities(logits)\n            \n            # Get predictions\n            preds = torch.argmax(class_probas, dim=1)\n            \n            all_labels.append(labels.cpu())\n            all_preds.append(preds.cpu())\n            all_probas.append(class_probas.cpu())\n    \n    all_labels = torch.cat(all_labels).numpy()\n    all_preds = torch.cat(all_preds).numpy()\n    all_probas = torch.cat(all_probas).numpy()\n    \n    return all_labels, all_preds, all_probas\n\ndef plot_confusion_matrix(labels, preds, title):\n    \"\"\"Plot confusion matrix\"\"\"\n    cm = confusion_matrix(labels, preds, normalize='true')\n    plt.figure(figsize=(10, 8))\n    sns.heatmap(cm, annot=True, fmt=\".2f\", cmap=\"Blues\", \n                xticklabels=[0,1,2,3,4], yticklabels=[0,1,2,3,4])\n    plt.title(f'Confusion Matrix ({title})\\nNormalized by True Labels', fontsize=16)\n    plt.xlabel('Predicted Label', fontsize=14)\n    plt.ylabel('True Label', fontsize=14)\n    plt.tight_layout()\n    plt.savefig(f'confusion_matrix_{title.lower().replace(\" \", \"_\")}.png', \n               bbox_inches='tight', dpi=300)\n    plt.show()\n\ndef plot_roc_curves(labels, probas, title):\n    \"\"\"Plot ROC curves for all classes\"\"\"\n    plt.figure(figsize=(10, 8))\n    \n    for i in range(5):\n        binary_labels = (labels == i).astype(int)\n        fpr, tpr, _ = roc_curve(binary_labels, probas[:, i])\n        roc_auc = auc(fpr, tpr)\n        plt.plot(fpr, tpr, lw=2, label=f'Class {i} (AUC = {roc_auc:.3f})')\n    \n    plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', alpha=0.5)\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.05])\n    plt.xlabel('False Positive Rate', fontsize=14)\n    plt.ylabel('True Positive Rate', fontsize=14)\n    plt.title(f'ROC Curves ({title})', fontsize=16)\n    plt.legend(loc=\"lower right\")\n    plt.tight_layout()\n    plt.savefig(f'roc_curves_{title.lower().replace(\" \", \"_\")}.png', \n               bbox_inches='tight', dpi=300)\n    plt.show()\n\ndef plot_precision_recall_curves(labels, probas, title):\n    \"\"\"Plot Precision-Recall curves for all classes\"\"\"\n    plt.figure(figsize=(10, 8))\n    \n    for i in range(5):\n        binary_labels = (labels == i).astype(int)\n        precision, recall, _ = precision_recall_curve(binary_labels, probas[:, i])\n        avg_precision = average_precision_score(binary_labels, probas[:, i])\n        plt.plot(recall, precision, lw=2, label=f'Class {i} (AP = {avg_precision:.3f})')\n    \n    plt.xlabel('Recall', fontsize=14)\n    plt.ylabel('Precision', fontsize=14)\n    plt.title(f'Precision-Recall Curves ({title})', fontsize=16)\n    plt.legend(loc=\"upper right\")\n    plt.tight_layout()\n    plt.savefig(f'pr_curves_{title.lower().replace(\" \", \"_\")}.png', \n               bbox_inches='tight', dpi=300)\n    plt.show()\n\ndef plot_calibration_curve(labels, probas, title):\n    \"\"\"Plot calibration curves - FIXED VERSION\"\"\"\n    plt.figure(figsize=(10, 8))\n    \n    for i in range(5):\n        binary_labels = (labels == i).astype(int)\n        prob_pos = probas[:, i]\n        \n        # Ensure probabilities are in [0, 1] range\n        prob_pos = np.clip(prob_pos, 0.0, 1.0)\n        \n        try:\n            fraction_of_positives, mean_predicted_value = calibration_curve(\n                binary_labels, prob_pos, n_bins=10, strategy='quantile')\n            plt.plot(mean_predicted_value, fraction_of_positives, \"s-\", label=f\"Class {i}\")\n        except Exception as e:\n            print(f\"Warning: Calibration curve for class {i} failed: {e}\")\n            continue\n    \n    plt.plot([0, 1], [0, 1], \"k--\", label=\"Perfectly calibrated\", alpha=0.5)\n    plt.ylabel(\"Fraction of positives\", fontsize=14)\n    plt.xlabel(\"Mean predicted probability\", fontsize=14)\n    plt.ylim([-0.05, 1.05])\n    plt.title(f'Calibration Curves ({title})', fontsize=16)\n    plt.legend(loc=\"lower right\")\n    plt.tight_layout()\n    plt.savefig(f'calibration_curve_{title.lower().replace(\" \", \"_\")}.png', \n               bbox_inches='tight', dpi=300)\n    plt.show()\n\ndef calculate_and_save_metrics(labels, preds, probas, title):\n    \"\"\"Calculate and save performance metrics\"\"\"\n    # Classification report\n    report = classification_report(labels, preds, \n                                 target_names=[f'Class {i}' for i in range(5)], \n                                 output_dict=True, zero_division=0)\n    df_report = pd.DataFrame(report).transpose().round(4)\n    \n    # Cohen's Kappa\n    kappa = cohen_kappa_score(labels, preds, weights='quadratic')\n    \n    # Per-class AUC\n    auc_scores = []\n    for i in range(5):\n        try:\n            binary_labels = (labels == i).astype(int)\n            if len(np.unique(binary_labels)) > 1:  # Check if both classes are present\n                auc_score = roc_auc_score(binary_labels, probas[:, i])\n            else:\n                auc_score = np.nan\n            auc_scores.append(auc_score)\n        except Exception as e:\n            print(f\"Warning: AUC calculation for class {i} failed: {e}\")\n            auc_scores.append(np.nan)\n    \n    # Add AUC column\n    for i, class_name in enumerate([f'Class {j}' for j in range(5)]):\n        if class_name in df_report.index:\n            df_report.loc[class_name, 'auc'] = auc_scores[i]\n    \n    # Add kappa row\n    df_report.loc['quadratic_kappa'] = {\n        'precision': kappa, 'recall': kappa, 'f1-score': kappa, \n        'support': len(labels), 'auc': np.nanmean(auc_scores)\n    }\n    \n    # Save metrics\n    df_report.to_csv(f'performance_metrics_{title.lower().replace(\" \", \"_\")}.csv')\n    \n    print(f\"\\n📊 {title} Metrics:\")\n    print(f\"Quadratic Weighted Kappa: {kappa:.4f}\")\n    print(f\"Mean AUC: {np.nanmean(auc_scores):.4f}\")\n    print(f\"Accuracy: {(labels == preds).mean():.4f}\")\n    \n    return df_report\n\ndef plot_class_distribution(df, title):\n    \"\"\"Plot class distribution\"\"\"\n    plt.figure(figsize=(10, 6))\n    ax = sns.countplot(x='diagnosis', data=df, palette='viridis')\n    plt.title(f'Class Distribution ({title})', fontsize=16)\n    plt.xlabel('DR Severity Grade', fontsize=14)\n    plt.ylabel('Count', fontsize=14)\n    \n    total = len(df)\n    for p in ax.patches:\n        percentage = f'{100 * p.get_height()/total:.1f}%'\n        ax.annotate(percentage, \n                    (p.get_x() + p.get_width() / 2., p.get_height()),\n                    ha='center', va='center', \n                    xytext=(0, 10), \n                    textcoords='offset points')\n        \n    plt.tight_layout()\n    plt.savefig(f'class_distribution_{title.lower().replace(\" \", \"_\")}.png', \n               bbox_inches='tight', dpi=300)\n    plt.show()\n\n# ============================================================================\n# MAIN ANALYSIS FUNCTION\n# ============================================================================\ndef run_complete_analysis():\n    \"\"\"Run complete analysis with existing models\"\"\"\n    \n    # Load data\n    full_df = load_datasets()\n    \n    # Load existing models\n    models = []\n    for fold in range(N_FOLDS):\n        model_path = f\"best_fold{fold}.pth\"\n        if os.path.exists(model_path):\n            print(f\"✅ Loading model: {model_path}\")\n            model = MobileNetV3_CORAL().to(DEVICE)\n            model.load_state_dict(torch.load(model_path, map_location=DEVICE))\n            model.eval()\n            models.append(model)\n        else:\n            print(f\"❌ Model not found: {model_path}\")\n    \n    if not models:\n        print(\"❌ No models found! Please ensure model files exist.\")\n        return\n    \n    print(f\"✅ Loaded {len(models)} models\")\n    \n    # Create test dataset\n    _, test_df = train_test_split(\n        full_df, test_size=0.2, stratify=full_df['diagnosis'], random_state=42\n    )\n    \n    test_loader = DataLoader(\n        DRDataset(test_df, get_val_transforms()), \n        batch_size=BATCH_SIZE, shuffle=False, num_workers=0\n    )\n    \n    print(f\"📊 Test dataset size: {len(test_df)} samples\")\n    \n    # Plot class distribution\n    plot_class_distribution(full_df, \"Full Dataset\")\n    plot_class_distribution(test_df, \"Test Set\")\n    \n    # Analyze each fold\n    all_fold_results = []\n    for fold, model in enumerate(models):\n        print(f\"\\n📈 Analyzing Fold {fold+1}...\")\n        \n        # Get predictions and probabilities\n        labels, preds, probas = get_predictions_and_probabilities(model, test_loader)\n        \n        # Create visualizations\n        plot_confusion_matrix(labels, preds, f\"Fold {fold+1}\")\n        plot_roc_curves(labels, probas, f\"Fold {fold+1}\")\n        plot_precision_recall_curves(labels, probas, f\"Fold {fold+1}\")\n        plot_calibration_curve(labels, probas, f\"Fold {fold+1}\")\n        \n        # Calculate metrics\n        metrics_df = calculate_and_save_metrics(labels, preds, probas, f\"Fold {fold+1}\")\n        \n        # Store results\n        kappa = cohen_kappa_score(labels, preds, weights='quadratic')\n        accuracy = (labels == preds).mean()\n        all_fold_results.append({'fold': fold+1, 'kappa': kappa, 'accuracy': accuracy})\n    \n    # Create ensemble model\n    print(f\"\\n📈 Analyzing Ensemble Model...\")\n    \n    class EnsembleModel(nn.Module):\n        def __init__(self, models):\n            super().__init__()\n            self.models = nn.ModuleList(models)\n            \n        def forward(self, x):\n            logits = torch.stack([model(x) for model in self.models])\n            return torch.mean(logits, dim=0)\n    \n    ensemble = EnsembleModel(models).to(DEVICE)\n    \n    # Analyze ensemble\n    labels, preds, probas = get_predictions_and_probabilities(ensemble, test_loader)\n    \n    plot_confusion_matrix(labels, preds, \"Ensemble\")\n    plot_roc_curves(labels, probas, \"Ensemble\")\n    plot_precision_recall_curves(labels, probas, \"Ensemble\")\n    plot_calibration_curve(labels, probas, \"Ensemble\")\n    metrics_df = calculate_and_save_metrics(labels, preds, probas, \"Ensemble\")\n    \n    # Summary\n    print(f\"\\n🎯 FINAL RESULTS SUMMARY\")\n    print(\"=\"*50)\n    for result in all_fold_results:\n        print(f\"Fold {result['fold']}: Kappa = {result['kappa']:.4f}, Accuracy = {result['accuracy']:.4f}\")\n    \n    ensemble_kappa = cohen_kappa_score(labels, preds, weights='quadratic')\n    ensemble_accuracy = (labels == preds).mean()\n    print(f\"Ensemble: Kappa = {ensemble_kappa:.4f}, Accuracy = {ensemble_accuracy:.4f}\")\n    \n    mean_kappa = np.mean([r['kappa'] for r in all_fold_results])\n    std_kappa = np.std([r['kappa'] for r in all_fold_results])\n    print(f\"\\nMean Fold Kappa: {mean_kappa:.4f} ± {std_kappa:.4f}\")\n    print(f\"Ensemble Kappa: {ensemble_kappa:.4f}\")\n    \n    print(f\"\\n✅ Analysis complete! All visualizations and metrics saved.\")\n\n# ============================================================================\n# EXECUTION\n# ============================================================================\nif __name__ == \"__main__\":\n    run_complete_analysis()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T06:47:28.591117Z","iopub.execute_input":"2025-06-01T06:47:28.591468Z","iopub.status.idle":"2025-06-01T07:05:40.228427Z","shell.execute_reply.started":"2025-06-01T06:47:28.591446Z","shell.execute_reply":"2025-06-01T07:05:40.227769Z"}},"outputs":[],"execution_count":null}]}