{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":14774,"databundleVersionId":875431}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ── CELL 1: Environment Setup (Run this FIRST before everything) ──\nimport os\nimport gc\nimport torch\n\n# Fix memory fragmentation\nos.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'expandable_segments:True'\n\n# Clear any leftover memory\ngc.collect()\ntorch.cuda.empty_cache()\n\n# Check GPU status\ntotal = torch.cuda.get_device_properties(0).total_memory / 1e9\nused  = torch.cuda.memory_allocated() / 1e9\nfree  = total - used\n\nprint(f\"✅ GPU: {torch.cuda.get_device_name(0)}\")\nprint(f\"   Total: {total:.2f} GB\")\nprint(f\"   Used:  {used:.2f} GB\")\nprint(f\"   Free:  {free:.2f} GB\")\n\nif free > 10:\n    print(\"\\n🟢 Memory is clean. Safe to proceed!\")\nelse:\n    print(\"\\n🔴 Memory still dirty. Do Restart & Clear Outputs again.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-02-26T12:11:02.660493Z","iopub.execute_input":"2026-02-26T12:11:02.661233Z","iopub.status.idle":"2026-02-26T12:11:02.926909Z","shell.execute_reply.started":"2026-02-26T12:11:02.661199Z","shell.execute_reply":"2026-02-26T12:11:02.925994Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 1: Install extra libraries ──\n!pip install grad-cam timm -q\n\n# ── CELL 2: All Imports ──\nimport os\nimport gc\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport cv2\nfrom PIL import Image\nfrom collections import Counter\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import cohen_kappa_score\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torch.cuda.amp import autocast, GradScaler\nimport timm\nimport warnings\nwarnings.filterwarnings('ignore')\n\nprint(\"✅ All imports successful\")\nprint(f\"   PyTorch version: {torch.__version__}\")\nprint(f\"   CUDA available:  {torch.cuda.is_available()}\")\nprint(f\"   timm version:    {timm.__version__}\")\n\n# ── CELL 3: Load Dataset ──\ndf = pd.read_csv('/kaggle/input/aptos2019-blindness-detection/train.csv')\n\ngrade_names = {\n    0: 'No DR',\n    1: 'Mild',\n    2: 'Moderate',\n    3: 'Severe',\n    4: 'Proliferative'\n}\n\nprint(f\"✅ Dataset loaded: {len(df)} images\")\nprint(\"\\nGrade Distribution:\")\nprint(\"-\" * 35)\nfor grade, count in df['diagnosis'].value_counts().sort_index().items():\n    pct = count / len(df) * 100\n    bar = '█' * int(pct / 2)\n    print(f\"  Grade {grade} ({grade_names[grade]:<14}): \"\n          f\"{count:>4} ({pct:4.1f}%) {bar}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T12:11:02.928403Z","iopub.execute_input":"2026-02-26T12:11:02.928697Z","iopub.status.idle":"2026-02-26T12:11:06.72555Z","shell.execute_reply.started":"2026-02-26T12:11:02.928672Z","shell.execute_reply":"2026-02-26T12:11:06.724686Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 4: Class Distribution Chart ──\ncolors = ['#2196F3','#4CAF50','#FF9800','#F44336','#9C27B0']\ncounts = df['diagnosis'].value_counts().sort_index()\n\nplt.figure(figsize=(10, 5))\nbars = plt.bar(\n    [f\"Grade {i}\\n{grade_names[i]}\" for i in counts.index],\n    counts.values, color=colors, edgecolor='white', linewidth=1.5\n)\nfor bar, val in zip(bars, counts.values):\n    plt.text(bar.get_x() + bar.get_width()/2,\n             bar.get_height() + 30,\n             str(val), ha='center', fontweight='bold', fontsize=11)\n\nplt.title('APTOS 2019 — Class Distribution\\n'\n          '(Imbalance toward Grade 0 is why we need Ordinal Loss)',\n          fontsize=13, fontweight='bold')\nplt.ylabel('Number of Images')\nplt.tight_layout()\nplt.savefig('class_distribution.png', dpi=150)\nplt.show()\nprint(\"✅ Chart saved\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T12:11:06.727112Z","iopub.execute_input":"2026-02-26T12:11:06.727543Z","iopub.status.idle":"2026-02-26T12:11:07.124885Z","shell.execute_reply.started":"2026-02-26T12:11:06.727511Z","shell.execute_reply":"2026-02-26T12:11:07.124106Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 5: Sample Images Per Grade ──\nimg_dir = '/kaggle/input/aptos2019-blindness-detection/train_images/'\n\nfig, axes = plt.subplots(1, 5, figsize=(18, 4))\nfor grade in range(5):\n    sample_id = df[df['diagnosis'] == grade].iloc[0]['id_code']\n    img = cv2.imread(os.path.join(img_dir, sample_id + '.png'))\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = cv2.resize(img, (256, 256))\n    axes[grade].imshow(img)\n    axes[grade].set_title(f'Grade {grade}\\n{grade_names[grade]}',\n                          fontweight='bold', fontsize=11)\n    axes[grade].axis('off')\n\nplt.suptitle('Sample Fundus Images — One Per DR Grade',\n             fontsize=13, fontweight='bold')\nplt.tight_layout()\nplt.savefig('sample_images.png', dpi=150)\nplt.show()\nprint(\"✅ Sample images saved\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T12:11:07.126622Z","iopub.execute_input":"2026-02-26T12:11:07.126886Z","iopub.status.idle":"2026-02-26T12:11:09.323855Z","shell.execute_reply.started":"2026-02-26T12:11:07.126864Z","shell.execute_reply":"2026-02-26T12:11:09.322939Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 6: Image Quality Gate ──\ndef check_image_quality(image_path,\n                         blur_threshold=80,\n                         dark_threshold=20,\n                         bright_threshold=235):\n    img = cv2.imread(image_path)\n    if img is None:\n        return False, 0, \"Cannot read image\"\n\n    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n    blur_score      = cv2.Laplacian(gray, cv2.CV_64F).var()\n    mean_brightness = gray.mean()\n\n    if blur_score < blur_threshold:\n        return False, blur_score, f\"Too blurry (score: {blur_score:.1f})\"\n    if mean_brightness < dark_threshold:\n        return False, blur_score, f\"Too dark (brightness: {mean_brightness:.1f})\"\n    if mean_brightness > bright_threshold:\n        return False, blur_score, f\"Overexposed (brightness: {mean_brightness:.1f})\"\n\n    return True, blur_score, f\"Quality OK (sharpness: {blur_score:.1f})\"\n\n\n# Quick test\nprint(\"Quality Gate Test:\")\nprint(\"-\" * 60)\nfor sid in df.sample(5, random_state=42)['id_code']:\n    path = os.path.join(img_dir, sid + '.png')\n    ok, score, reason = check_image_quality(path)\n    status = \"✅ PASS\" if ok else \"❌ FAIL\"\n    print(f\"  {status} | Score: {score:7.1f} | {reason}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T12:11:09.325019Z","iopub.execute_input":"2026-02-26T12:11:09.325374Z","iopub.status.idle":"2026-02-26T12:11:10.276958Z","shell.execute_reply.started":"2026-02-26T12:11:09.325341Z","shell.execute_reply":"2026-02-26T12:11:10.27629Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 7: CLAHE Preprocessing ──\nTARGET_SIZE = 380\n\ndef apply_clahe(image_path, target_size=TARGET_SIZE):\n    img = cv2.imread(image_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = cv2.resize(img, (target_size, target_size))\n    lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)\n    l, a, b = cv2.split(lab)\n    clahe      = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n    l_enhanced = clahe.apply(l)\n    enhanced   = cv2.cvtColor(cv2.merge([l_enhanced, a, b]), cv2.COLOR_LAB2RGB)\n    return enhanced\n\n\n# Visualize\nfig, axes = plt.subplots(2, 4, figsize=(16, 8))\nsamples = df.sample(4, random_state=1)['id_code'].tolist()\n\nfor i, sid in enumerate(samples):\n    path     = os.path.join(img_dir, sid + '.png')\n    original = cv2.resize(\n        cv2.cvtColor(cv2.imread(path), cv2.COLOR_BGR2RGB),\n        (TARGET_SIZE, TARGET_SIZE)\n    )\n    enhanced = apply_clahe(path)\n\n    axes[0][i].imshow(original)\n    axes[0][i].set_title('Original', fontweight='bold')\n    axes[0][i].axis('off')\n\n    axes[1][i].imshow(enhanced)\n    axes[1][i].set_title('CLAHE Enhanced', fontweight='bold', color='#1565C0')\n    axes[1][i].axis('off')\n\nplt.suptitle('CLAHE Enhancement — Microaneurysms become more visible',\n             fontsize=13, fontweight='bold')\nplt.tight_layout()\nplt.savefig('clahe_comparison.png', dpi=150)\nplt.show()\nprint(f\"✅ CLAHE ready | Image size: {TARGET_SIZE}×{TARGET_SIZE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T12:11:10.277927Z","iopub.execute_input":"2026-02-26T12:11:10.27831Z","iopub.status.idle":"2026-02-26T12:11:13.842737Z","shell.execute_reply.started":"2026-02-26T12:11:10.278286Z","shell.execute_reply":"2026-02-26T12:11:13.841688Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 8: Dataset & DataLoaders ──\nclass DRDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None):\n        self.df       = df.reset_index(drop=True)\n        self.img_dir  = img_dir\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        path     = os.path.join(self.img_dir, row['id_code'] + '.png')\n        img      = apply_clahe(path)\n        img      = Image.fromarray(img)\n        if self.transform:\n            img = self.transform(img)\n        return img, int(row['diagnosis'])\n\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((TARGET_SIZE, TARGET_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(20),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                         [0.229, 0.224, 0.225])\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((TARGET_SIZE, TARGET_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                         [0.229, 0.224, 0.225])\n])\n\ntrain_df, val_df = train_test_split(\n    df, test_size=0.2, random_state=42, stratify=df['diagnosis']\n)\n\ntrain_dataset = DRDataset(train_df, img_dir, transform=train_transform)\nval_dataset   = DRDataset(val_df,   img_dir, transform=val_transform)\n\ntrain_loader = DataLoader(train_dataset, batch_size=4,\n                          shuffle=True,  num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_dataset,   batch_size=4,\n                          shuffle=False, num_workers=2, pin_memory=True)\n\nprint(f\"✅ Datasets ready\")\nprint(f\"   Train: {len(train_dataset)} images | \"\n      f\"Val: {len(val_dataset)} images\")\nprint(f\"   Batch size: 4 | Image size: {TARGET_SIZE}×{TARGET_SIZE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T12:11:13.843844Z","iopub.execute_input":"2026-02-26T12:11:13.844106Z","iopub.status.idle":"2026-02-26T12:11:13.861881Z","shell.execute_reply.started":"2026-02-26T12:11:13.844085Z","shell.execute_reply":"2026-02-26T12:11:13.860962Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 9: Clinically-Weighted Ordinal Loss (FIXED) ──\nclass OrdinalLoss(nn.Module):\n    def __init__(self, num_classes=5, alpha=0.5):\n        super(OrdinalLoss, self).__init__()\n        self.alpha   = alpha\n        self.ce_loss = nn.CrossEntropyLoss()\n\n        penalty = torch.zeros(num_classes, num_classes)\n        for i in range(num_classes):\n            for j in range(num_classes):\n                penalty[i][j] = abs(i - j)\n        self.register_buffer('penalty_matrix', penalty)\n\n    def forward(self, predictions, targets):\n        ce = self.ce_loss(predictions, targets)\n        probs = torch.softmax(predictions, dim=1)\n\n        # ── THE FIX: move penalty_matrix to same device as targets ──\n        penalties = self.penalty_matrix.to(targets.device)[targets]\n\n        ordinal_penalty = (probs * penalties).sum(dim=1).mean()\n        return ce + self.alpha * ordinal_penalty\n\n\n# Verify\nloss_fn    = OrdinalLoss(num_classes=5, alpha=0.5).to(device)\ntest_pred  = torch.randn(4, 5).to(device)\ntest_label = torch.tensor([0, 2, 3, 4]).to(device)\ntest_loss  = loss_fn(test_pred, test_label)\nprint(f\"OrdinalLoss working | Test loss: {test_loss.item():.4f}\")\nprint(f\"penalty_matrix device: {loss_fn.penalty_matrix.device}\")\nprint(f\"targets device:        {test_label.device}\")\nprint(\"Devices match - safe to train!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T12:15:47.172217Z","iopub.execute_input":"2026-02-26T12:15:47.173046Z","iopub.status.idle":"2026-02-26T12:15:47.183488Z","shell.execute_reply.started":"2026-02-26T12:15:47.173015Z","shell.execute_reply":"2026-02-26T12:15:47.18267Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 10: Model Definition ──\nclass DRGradingModel(nn.Module):\n    def __init__(self, num_classes=5, pretrained=True):\n        super(DRGradingModel, self).__init__()\n        self.backbone = timm.create_model(\n            'efficientnet_b2',\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool='avg'\n        )\n        feature_dim = self.backbone.num_features   # 1408\n\n        self.classifier = nn.Sequential(\n            nn.Dropout(p=0.3),\n            nn.Linear(feature_dim, 256),\n            nn.ReLU(),\n            nn.Dropout(p=0.2),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x):\n        return self.classifier(self.backbone(x))\n\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\ngc.collect()\ntorch.cuda.empty_cache()\n\nmodel       = DRGradingModel(num_classes=5, pretrained=True).to(device)\nfree_after  = (torch.cuda.get_device_properties(0).total_memory\n               - torch.cuda.memory_allocated()) / 1e9\ntotal_params = sum(p.numel() for p in model.parameters())\n\nprint(f\"✅ Model loaded: EfficientNet-B2\")\nprint(f\"   Parameters:  {total_params:,}\")\nprint(f\"   GPU free:    {free_after:.2f} GB  ← should be > 10GB\")\n\nif free_after > 10:\n    print(\"\\n🟢 Perfect! Proceed to training (Cell 11)\")\nelse:\n    print(\"\\n🔴 Low memory — restart and rerun from Cell 1\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T12:15:51.474035Z","iopub.execute_input":"2026-02-26T12:15:51.474667Z","iopub.status.idle":"2026-02-26T12:15:52.012842Z","shell.execute_reply.started":"2026-02-26T12:15:51.474636Z","shell.execute_reply":"2026-02-26T12:15:52.01221Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 11: Training Loop ──\ndef train_one_epoch(model, loader, optimizer, loss_fn, device, scaler):\n    model.train()\n    total_loss = 0\n    all_preds, all_labels = [], []\n\n    for batch_idx, (images, labels) in enumerate(loader):\n        images = images.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True)\n\n        optimizer.zero_grad(set_to_none=True)\n\n        with autocast():\n            outputs = model(images)\n            loss    = loss_fn(outputs, labels)\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        total_loss += loss.item()\n        all_preds.extend(torch.argmax(outputs, dim=1).cpu().numpy())\n        all_labels.extend(labels.cpu().numpy())\n\n        if batch_idx % 10 == 0:\n            torch.cuda.empty_cache()\n\n        if batch_idx % 50 == 0:\n            mem = torch.cuda.memory_allocated() / 1e9\n            print(f\"  [{batch_idx:>3}/{len(loader)}] \"\n                  f\"Loss: {loss.item():.4f} | GPU: {mem:.2f}GB\")\n\n    return (total_loss / len(loader),\n            cohen_kappa_score(all_labels, all_preds, weights='quadratic'))\n\n\ndef validate(model, loader, loss_fn, device):\n    model.eval()\n    total_loss = 0\n    all_preds, all_labels = [], []\n\n    with torch.no_grad():\n        for images, labels in loader:\n            images = images.to(device, non_blocking=True)\n            labels = labels.to(device, non_blocking=True)\n            with autocast():\n                outputs = model(images)\n                loss    = loss_fn(outputs, labels)\n            total_loss += loss.item()\n            all_preds.extend(torch.argmax(outputs, dim=1).cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n\n    kappa    = cohen_kappa_score(all_labels, all_preds, weights='quadratic')\n    accuracy = sum(p==l for p,l in zip(all_preds,all_labels)) / len(all_labels)\n    return total_loss / len(loader), kappa, accuracy\n\n\n# ── Setup ──\nloss_fn   = OrdinalLoss(num_classes=5, alpha=0.5).to(device)\noptimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=15)\nscaler    = GradScaler()\n\ntorch.cuda.empty_cache()\n\nNUM_EPOCHS = 15\nbest_kappa = 0\nhistory    = {'train_loss':[], 'val_loss':[],\n              'train_kappa':[], 'val_kappa':[]}\n\nprint(\"=\" * 60)\nprint(\"  SmartDR-XAI Training - EfficientNet-B2 + Ordinal Loss\")\nprint(\"=\" * 60)\n\nfor epoch in range(NUM_EPOCHS):\n    print(f\"\\nEpoch {epoch+1}/{NUM_EPOCHS}\")\n    print(\"-\" * 40)\n\n    train_loss, train_kappa = train_one_epoch(\n        model, train_loader, optimizer, loss_fn, device, scaler\n    )\n    val_loss, val_kappa, val_acc = validate(\n        model, val_loader, loss_fn, device\n    )\n    scheduler.step()\n\n    history['train_loss'].append(train_loss)\n    history['val_loss'].append(val_loss)\n    history['train_kappa'].append(train_kappa)\n    history['val_kappa'].append(val_kappa)\n\n    print(f\"\\n  Train --> Loss: {train_loss:.4f} | Kappa: {train_kappa:.4f}\")\n    print(f\"  Val   --> Loss: {val_loss:.4f}  | Kappa: {val_kappa:.4f} | Acc: {val_acc:.2%}\")\n\n    if val_kappa > best_kappa:\n        best_kappa = val_kappa\n        torch.save(model.state_dict(), 'best_model.pth')\n        print(f\"  >> Best model saved! Kappa: {best_kappa:.4f}\")\n\nprint(f\"\\nTraining complete! Best Kappa: {best_kappa:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T12:15:56.702906Z","iopub.execute_input":"2026-02-26T12:15:56.703728Z","iopub.status.idle":"2026-02-26T13:11:20.976129Z","shell.execute_reply.started":"2026-02-26T12:15:56.703688Z","shell.execute_reply":"2026-02-26T13:11:20.975379Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 12: Training Curves ──\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n\nax1.plot(history['train_loss'], label='Train Loss', color='#1565C0', linewidth=2, marker='o', markersize=4)\nax1.plot(history['val_loss'],   label='Val Loss',   color='#E65100', linewidth=2, marker='o', markersize=4)\nax1.set_title('Loss Curve', fontweight='bold', fontsize=12)\nax1.set_xlabel('Epoch')\nax1.set_ylabel('Loss')\nax1.legend()\nax1.grid(alpha=0.3)\n\nax2.plot(history['train_kappa'], label='Train Kappa', color='#1565C0', linewidth=2, marker='o', markersize=4)\nax2.plot(history['val_kappa'],   label='Val Kappa',   color='#2E7D32', linewidth=2, marker='o', markersize=4)\nax2.axhline(y=0.85, color='red', linestyle='--', linewidth=1.5, label='Target (0.85)')\nax2.set_title('Quadratic Weighted Kappa (QWK)', fontweight='bold', fontsize=12)\nax2.set_xlabel('Epoch')\nax2.set_ylabel('QWK Score')\nax2.set_ylim(0, 1.0)\nax2.legend()\nax2.grid(alpha=0.3)\n\nplt.suptitle('SmartDR-XAI Training Results', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('training_curves.png', dpi=150)\nplt.show()\nprint(f\"Best Validation Kappa achieved: {best_kappa:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T13:13:00.125287Z","iopub.execute_input":"2026-02-26T13:13:00.126048Z","iopub.status.idle":"2026-02-26T13:13:00.763144Z","shell.execute_reply.started":"2026-02-26T13:13:00.126015Z","shell.execute_reply":"2026-02-26T13:13:00.762387Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 13: Load Best Model ──\nmodel.load_state_dict(torch.load('best_model.pth', map_location=device))\nmodel.eval()\n\nfree = (torch.cuda.get_device_properties(0).total_memory\n        - torch.cuda.memory_allocated()) / 1e9\nprint(\"Best model loaded successfully\")\nprint(f\"Best Kappa: {best_kappa:.4f}\")\nprint(f\"GPU free:   {free:.2f} GB\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T13:16:00.610521Z","iopub.execute_input":"2026-02-26T13:16:00.610888Z","iopub.status.idle":"2026-02-26T13:16:00.753295Z","shell.execute_reply.started":"2026-02-26T13:16:00.610861Z","shell.execute_reply":"2026-02-26T13:16:00.752563Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 14: Grad-CAM++ Heatmaps ──\n!pip install grad-cam -q\n\nfrom pytorch_grad_cam import GradCAMPlusPlus\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\n\n# Target last conv layer of EfficientNet-B2\ntarget_layers = [model.backbone.conv_head]\n\ndef generate_gradcam(image_tensor, original_img_array, predicted_class):\n    cam = GradCAMPlusPlus(model=model, target_layers=target_layers)\n    targets = [ClassifierOutputTarget(predicted_class)]\n    grayscale_cam = cam(\n        input_tensor=image_tensor.unsqueeze(0).to(device),\n        targets=targets\n    )[0]\n    img_normalized = original_img_array.astype(np.float32) / 255.0\n    overlay = show_cam_on_image(img_normalized, grayscale_cam, use_rgb=True)\n    return overlay, grayscale_cam\n\n\nreferral = {\n    0: 'No Action Needed',\n    1: 'Monitor in 12 months',\n    2: 'Refer within 6 months',\n    3: 'Refer within 1 month',\n    4: 'URGENT Referral Today'\n}\nref_colors = {\n    0: '#2E7D32',\n    1: '#1565C0',\n    2: '#FF9800',\n    3: '#E65100',\n    4: '#B71C1C'\n}\n\nfig, axes = plt.subplots(3, 5, figsize=(20, 12))\n\nfor grade in range(5):\n    # Get one sample per grade\n    sample_id = df[df['diagnosis'] == grade].iloc[2]['id_code']\n    path      = os.path.join(img_dir, sample_id + '.png')\n\n    # Preprocess\n    enhanced   = apply_clahe(path)\n    img_tensor = val_transform(Image.fromarray(enhanced)).to(device)\n\n    # Predict\n    with torch.no_grad():\n        output     = model(img_tensor.unsqueeze(0))\n        probs      = torch.softmax(output, dim=1)[0]\n        pred_class = torch.argmax(probs).item()\n        confidence = probs[pred_class].item()\n\n    # Generate heatmap\n    overlay, _ = generate_gradcam(img_tensor, enhanced, pred_class)\n\n    # Row 0: Original image\n    axes[0][grade].imshow(enhanced)\n    axes[0][grade].set_title(\n        f'Grade {grade}: {grade_names[grade]}',\n        fontweight='bold', fontsize=10\n    )\n    axes[0][grade].axis('off')\n\n    # Row 1: Heatmap overlay\n    axes[1][grade].imshow(overlay)\n    axes[1][grade].set_title(\n        f'Grad-CAM++\\nConf: {confidence:.1%}',\n        fontsize=9\n    )\n    axes[1][grade].axis('off')\n\n    # Row 2: Referral decision\n    axes[2][grade].set_facecolor(ref_colors[pred_class])\n    axes[2][grade].text(\n        0.5, 0.5,\n        f'Grade {pred_class}\\n{referral[pred_class]}',\n        ha='center', va='center',\n        fontsize=8, fontweight='bold',\n        color='white',\n        transform=axes[2][grade].transAxes,\n        multialignment='center'\n    )\n    axes[2][grade].axis('off')\n\nplt.suptitle(\n    'SmartDR-XAI: Grade Classification + Grad-CAM++ Explainability + Referral',\n    fontsize=13, fontweight='bold', y=1.01\n)\nplt.tight_layout()\nplt.savefig('gradcam_results.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"Grad-CAM++ visualization saved\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T13:16:21.634436Z","iopub.execute_input":"2026-02-26T13:16:21.634768Z","iopub.status.idle":"2026-02-26T13:16:30.180591Z","shell.execute_reply.started":"2026-02-26T13:16:21.634741Z","shell.execute_reply":"2026-02-26T13:16:30.179325Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 15: Final Evaluation Report ──\nfrom sklearn.metrics import (classification_report,\n                              confusion_matrix,\n                              ConfusionMatrixDisplay)\n\n# Get all predictions on validation set\nmodel.eval()\nall_preds, all_labels = [], []\n\nwith torch.no_grad():\n    for images, labels in val_loader:\n        images = images.to(device)\n        with autocast():\n            outputs = model(images)\n        preds = torch.argmax(outputs, dim=1).cpu().numpy()\n        all_preds.extend(preds)\n        all_labels.extend(labels.numpy())\n\n# Metrics\nfinal_kappa = cohen_kappa_score(all_labels, all_preds, weights='quadratic')\naccuracy    = sum(p==l for p,l in zip(all_preds, all_labels)) / len(all_labels)\n\nprint(\"=\" * 55)\nprint(\"  SmartDR-XAI Final Evaluation Results\")\nprint(\"=\" * 55)\nprint(f\"  Quadratic Weighted Kappa : {final_kappa:.4f}\")\nprint(f\"  Overall Accuracy         : {accuracy:.2%}\")\nprint()\nprint(\"  Per-Class Report:\")\nprint(\"-\" * 55)\nprint(classification_report(\n    all_labels, all_preds,\n    target_names=[f'Grade {i}: {grade_names[i]}' for i in range(5)]\n))\n\n# Confusion Matrix\nfig, ax = plt.subplots(figsize=(8, 7))\ncm = confusion_matrix(all_labels, all_preds)\ndisp = ConfusionMatrixDisplay(\n    confusion_matrix=cm,\n    display_labels=[f'G{i}:{grade_names[i][:3]}' for i in range(5)]\n)\ndisp.plot(ax=ax, colorbar=True, cmap='Blues')\nax.set_title(\n    f'Confusion Matrix — SmartDR-XAI\\nQWK: {final_kappa:.4f}',\n    fontweight='bold', fontsize=12\n)\nplt.tight_layout()\nplt.savefig('confusion_matrix.png', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T13:17:34.68562Z","iopub.execute_input":"2026-02-26T13:17:34.68624Z","iopub.status.idle":"2026-02-26T13:18:15.281452Z","shell.execute_reply.started":"2026-02-26T13:17:34.686185Z","shell.execute_reply":"2026-02-26T13:18:15.280571Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── CELL 16: Save All Files for Download ──\nimport os\n\nfiles_to_save = [\n    'best_model.pth',\n    'training_curves.png',\n    'gradcam_results.png',\n    'confusion_matrix.png',\n    'class_distribution.png',\n    'clahe_comparison.png',\n    'sample_images.png',\n]\n\nprint(\"Files ready to download:\")\nprint(\"-\" * 40)\nfor f in files_to_save:\n    if os.path.exists(f):\n        size = os.path.getsize(f) / 1024\n        print(f\"  [OK]  {f:<30} {size:>7.1f} KB\")\n    else:\n        print(f\"  [--]  {f:<30} not found yet\")\n\nprint()\nprint(\"To download:\")\nprint(\"  1. Click the folder icon in left sidebar\")\nprint(\"  2. Right-click each file\")\nprint(\"  3. Click Download\")\nprint()\nprint(\"Most important file to download: best_model.pth\")\nprint(\"You need this for the Streamlit app in VS Code\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-26T13:19:34.577749Z","iopub.execute_input":"2026-02-26T13:19:34.578047Z","iopub.status.idle":"2026-02-26T13:19:34.584959Z","shell.execute_reply.started":"2026-02-26T13:19:34.578022Z","shell.execute_reply":"2026-02-26T13:19:34.584284Z"}},"outputs":[],"execution_count":null}]}