{"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":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import torch\nimport os\nprint(f\"🔥 GPU: {torch.cuda.is_available()} — {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'NONE'}\")\nif torch.cuda.is_available():\n    print(f\"   VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB\")\n\n# Kaggle has most deps pre-installed, just need timm & albumentations\nos.system('pip install -q timm albumentations')\nprint(\"✅ Dependencies ready!\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-14T10:17:28.654846Z","iopub.execute_input":"2026-05-14T10:17:28.655574Z","iopub.status.idle":"2026-05-14T10:17:31.944747Z","shell.execute_reply.started":"2026-05-14T10:17:28.655544Z","shell.execute_reply":"2026-05-14T10:17:31.942888Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# Auto-detect dataset path (works whether added as competition or dataset)\nOUTPUT_DIR = '/kaggle/working'\nDATA_DIR = None\n\n# Search for train.csv in /kaggle/input/\nprint(\"🔍 Searching for dataset...\")\nfor root, dirs, files in os.walk('/kaggle/input'):\n    if 'train.csv' in files:\n        DATA_DIR = root\n        break\n\nif DATA_DIR is None:\n    # List what's available so user can debug\n    print(\"❌ train.csv not found! Available inputs:\")\n    for item in os.listdir('/kaggle/input'):\n        sub = os.path.join('/kaggle/input', item)\n        print(f\"   📁 {item}/\")\n        if os.path.isdir(sub):\n            for f in os.listdir(sub)[:10]:\n                print(f\"      - {f}\")\n    raise FileNotFoundError(\"Add APTOS 2019 dataset: Notebook → Add Data → Search 'aptos2019'\")\n\n# Auto-detect image directory\nIMG_DIR = os.path.join(DATA_DIR, 'train_images')\nif not os.path.exists(IMG_DIR):\n    # Maybe images are in a subfolder\n    for d in os.listdir(DATA_DIR):\n        candidate = os.path.join(DATA_DIR, d)\n        if os.path.isdir(candidate) and 'train' in d.lower():\n            IMG_DIR = candidate\n            break\n\nprint(f\"✅ Dataset found at: {DATA_DIR}\")\nprint(f\"   Images at: {IMG_DIR}\")\nprint(f\"   Image count: {len(os.listdir(IMG_DIR))}\")\n\ndf = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\nprint(f\"📦 Total samples: {len(df)}\")\nprint(f\"\\n📊 Class Distribution:\")\nprint(df['diagnosis'].value_counts().sort_index())\n\nlabels = ['No DR (0)', 'Mild (1)', 'Moderate (2)', 'Severe (3)', 'PDR (4)']\ncolors = ['#22c55e', '#eab308', '#f97316', '#ef4444', '#dc2626']\nax = df['diagnosis'].value_counts().sort_index().plot(kind='bar', color=colors, figsize=(10, 5))\nax.set_xticklabels(labels, rotation=45)\nax.set_title('APTOS 2019 — DR Stage Distribution')\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'class_distribution.png'), dpi=150)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-14T10:17:31.946337Z","iopub.execute_input":"2026-05-14T10:17:31.946834Z","iopub.status.idle":"2026-05-14T10:17:32.448874Z","shell.execute_reply.started":"2026-05-14T10:17:31.946802Z","shell.execute_reply":"2026-05-14T10:17:32.448234Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import train_test_split\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nIMG_SIZE = 300  # EfficientNet-B3\n\ndef crop_image_from_gray(img, tol=7):\n    \"\"\"Ben Graham's preprocessing — crop black borders.\"\"\"\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 = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n        mask = gray > tol\n        if img[:,:,0][np.ix_(mask.any(1), mask.any(0))].shape[0] == 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    return img\n\nclass APTOSDataset(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        img_path = os.path.join(self.img_dir, f\"{row['id_code']}.png\")\n        image = cv2.imread(img_path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        image = crop_image_from_gray(image)\n        image = cv2.resize(image, (IMG_SIZE, IMG_SIZE))\n        if self.transform:\n            image = self.transform(image=image)['image']\n        return image, row['diagnosis']\n\n# Heavy augmentations for training\ntrain_transform = A.Compose([\n    A.Resize(IMG_SIZE, IMG_SIZE),\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.15, rotate_limit=30, p=0.5),\n    A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),\n    A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=10, p=0.3),\n    A.CoarseDropout(max_holes=8, max_height=IMG_SIZE//12, max_width=IMG_SIZE//12, fill_value=0, p=0.3),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2(),\n])\n\nval_transform = A.Compose([\n    A.Resize(IMG_SIZE, IMG_SIZE),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2(),\n])\n\n# TTA transforms (5 versions per image during eval)\ntta_transforms = [\n    val_transform,\n    A.Compose([A.Resize(IMG_SIZE, IMG_SIZE), A.HorizontalFlip(p=1.0),\n               A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]), ToTensorV2()]),\n    A.Compose([A.Resize(IMG_SIZE, IMG_SIZE), A.VerticalFlip(p=1.0),\n               A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]), ToTensorV2()]),\n    A.Compose([A.Resize(IMG_SIZE, IMG_SIZE), A.RandomRotate90(p=1.0),\n               A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]), ToTensorV2()]),\n    A.Compose([A.Resize(IMG_SIZE, IMG_SIZE), A.HorizontalFlip(p=1.0), A.VerticalFlip(p=1.0),\n               A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]), ToTensorV2()]),\n]\n\n# 80/20 stratified split\ntrain_df, val_df = train_test_split(df, test_size=0.2, stratify=df['diagnosis'], random_state=42)\nprint(f\"📦 Train: {len(train_df)} | Val: {len(val_df)}\")\n\ntrain_dataset = APTOSDataset(train_df, IMG_DIR, train_transform)\nval_dataset = APTOSDataset(val_df, IMG_DIR, val_transform)\n\n# Class weights for imbalanced data\nfrom sklearn.utils.class_weight import compute_class_weight\ncw = compute_class_weight('balanced', classes=np.arange(5), y=train_df['diagnosis'].values)\nclass_weights = torch.FloatTensor(cw).cuda()\nprint(f\"⚖️ Class weights: {class_weights}\")\n\n# AMP allows batch_size=32 on T4\nBATCH_SIZE = 32\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4, pin_memory=True)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=4, pin_memory=True)\nprint(f\"✅ DataLoaders ready! (batch={BATCH_SIZE})\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-14T10:17:32.449710Z","iopub.execute_input":"2026-05-14T10:17:32.449930Z","iopub.status.idle":"2026-05-14T10:17:36.140612Z","shell.execute_reply.started":"2026-05-14T10:17:32.449908Z","shell.execute_reply":"2026-05-14T10:17:36.139717Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import timm\nimport torch.nn as nn\n\nclass DRClassifier(nn.Module):\n    def __init__(self, num_classes=5, model_name='efficientnet_b3'):\n        super().__init__()\n        self.model = timm.create_model(model_name, pretrained=True, num_classes=num_classes)\n        total = sum(p.numel() for p in self.model.parameters())\n        print(f\"🧠 {model_name} — {total:,} params\")\n\n    def forward(self, x):\n        return self.model(x)\n\n    def freeze_backbone(self):\n        \"\"\"Freeze everything except classifier head.\"\"\"\n        for name, param in self.model.named_parameters():\n            if 'classifier' not in name:\n                param.requires_grad = False\n        t = sum(p.numel() for p in self.model.parameters() if p.requires_grad)\n        print(f\"  ❄️ Backbone frozen. Trainable: {t:,}\")\n\n    def unfreeze_last_blocks(self, n_blocks=2):\n        \"\"\"Unfreeze last N blocks + classifier.\"\"\"\n        for param in self.model.parameters():\n            param.requires_grad = False\n        # Unfreeze classifier\n        for param in self.model.classifier.parameters():\n            param.requires_grad = True\n        # Unfreeze last N blocks\n        blocks = list(self.model.blocks.children()) if hasattr(self.model, 'blocks') else []\n        for block in blocks[-n_blocks:]:\n            for param in block.parameters():\n                param.requires_grad = True\n        t = sum(p.numel() for p in self.model.parameters() if p.requires_grad)\n        print(f\"  🔓 Last {n_blocks} blocks unfrozen. Trainable: {t:,}\")\n\n    def unfreeze_all(self):\n        \"\"\"Unfreeze entire network.\"\"\"\n        for param in self.model.parameters():\n            param.requires_grad = True\n        t = sum(p.numel() for p in self.model.parameters() if p.requires_grad)\n        print(f\"  🔓 ALL unfrozen. Trainable: {t:,}\")\n\nmodel = DRClassifier(num_classes=5).cuda()\nmodel.freeze_backbone()  # Phase 1: only classifier head\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-14T10:17:36.141803Z","iopub.execute_input":"2026-05-14T10:17:36.142864Z","iopub.status.idle":"2026-05-14T10:17:44.656575Z","shell.execute_reply.started":"2026-05-14T10:17:36.142808Z","shell.execute_reply":"2026-05-14T10:17:44.655988Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts\n# PyTorch 2.x compatible AMP imports\ntry:\n    from torch.amp import GradScaler, autocast\n    AMP_DEVICE = 'cuda'\nexcept ImportError:\n    from torch.cuda.amp import GradScaler, autocast\n    AMP_DEVICE = None\nfrom sklearn.metrics import cohen_kappa_score\nimport time\n\nEPOCHS = 25\nPATIENCE = 7\n\n# Label smoothing for better generalization\ncriterion = nn.CrossEntropyLoss(weight=class_weights, label_smoothing=0.1)\n\n# AMP scaler for mixed precision\nscaler = GradScaler('cuda') if AMP_DEVICE else GradScaler()\n\n# Optimizer — will be rebuilt on unfreeze\noptimizer = torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3, weight_decay=0.01)\nscheduler = CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)\n\nbest_val_acc = 0\nbest_kappa = 0\npatience_counter = 0\nhistory = {'train_loss': [], 'val_loss': [], 'val_acc': [], 'val_kappa': []}\n\nprint(\"🚀 Training with Progressive Unfreezing + AMP + Label Smoothing\")\nprint(\"=\" * 70)\n\nfor epoch in range(EPOCHS):\n    start = time.time()\n\n    # === PROGRESSIVE UNFREEZING ===\n    if epoch == 5:\n        print(\"\\n🔓 PHASE 2: Unfreezing last 2 blocks...\")\n        model.unfreeze_last_blocks(2)\n        optimizer = torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=5e-4, weight_decay=0.01)\n        scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)\n    elif epoch == 15:\n        print(\"\\n🔓 PHASE 3: Unfreezing ALL layers (fine LR)...\")\n        model.unfreeze_all()\n        optimizer = torch.optim.AdamW(model.parameters(), lr=1e-5, weight_decay=0.01)\n        scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=5, T_mult=2)\n\n    # === TRAIN ===\n    model.train()\n    train_loss = 0\n    for images, labels in train_loader:\n        images, labels = images.cuda(), labels.cuda()\n        optimizer.zero_grad()\n        with autocast('cuda') if AMP_DEVICE else autocast():  # Mixed precision\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        train_loss += loss.item()\n    train_loss /= len(train_loader)\n\n    # === VALIDATE ===\n    model.eval()\n    val_loss = 0\n    all_preds, all_labels = [], []\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.cuda(), labels.cuda()\n            with autocast('cuda') if AMP_DEVICE else autocast():\n                outputs = model(images)\n                loss = criterion(outputs, labels)\n            val_loss += loss.item()\n            all_preds.extend(torch.argmax(outputs, 1).cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n\n    val_loss /= len(val_loader)\n    val_acc = np.mean(np.array(all_preds) == np.array(all_labels)) * 100\n    val_kappa = cohen_kappa_score(all_labels, all_preds, weights='quadratic') * 100\n    scheduler.step()\n\n    history['train_loss'].append(train_loss)\n    history['val_loss'].append(val_loss)\n    history['val_acc'].append(val_acc)\n    history['val_kappa'].append(val_kappa)\n\n    elapsed = time.time() - start\n    phase = \"HEAD\" if epoch < 5 else (\"PARTIAL\" if epoch < 15 else \"FULL\")\n    print(f\"E{epoch+1:02d}/{EPOCHS} [{phase}] {elapsed:.0f}s — \"\n          f\"TrL:{train_loss:.4f} | VL:{val_loss:.4f} | Acc:{val_acc:.1f}% | κ:{val_kappa:.1f}%\")\n\n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        best_kappa = val_kappa\n        patience_counter = 0\n        torch.save(model.model.state_dict(), os.path.join(OUTPUT_DIR, 'optigemma_effnetb3_best.pt'))\n        print(f\"  💾 BEST! Saved (Acc:{val_acc:.1f}%, κ:{val_kappa:.1f}%)\")\n    else:\n        patience_counter += 1\n        if patience_counter >= PATIENCE:\n            print(f\"  ⏹️ Early stop at epoch {epoch+1}\")\n            break\n\nprint(\"=\" * 70)\nprint(f\"🏆 BEST — Accuracy: {best_val_acc:.1f}% | Kappa: {best_kappa:.1f}%\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-14T11:04:03.018237Z","iopub.status.idle":"2026-05-14T11:04:03.018613Z","shell.execute_reply.started":"2026-05-14T11:04:03.018423Z","shell.execute_reply":"2026-05-14T11:04:03.018447Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"🔄 Running TTA (5 augmented passes)...\")\nmodel.model.load_state_dict(torch.load(os.path.join(OUTPUT_DIR, 'optigemma_effnetb3_best.pt'), weights_only=True))\nmodel.eval()\n\ntta_all_probs = []\nall_labels_tta = []\n\nwith torch.no_grad():\n    for idx in range(len(val_df)):\n        row = val_df.iloc[idx]\n        img_path = os.path.join(IMG_DIR, f\"{row['id_code']}.png\")\n        image = cv2.imread(img_path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        image = crop_image_from_gray(image)\n        image = cv2.resize(image, (IMG_SIZE, IMG_SIZE))\n\n        avg_probs = np.zeros(5)\n        for tfm in tta_transforms:\n            aug = tfm(image=image)['image'].unsqueeze(0).cuda()\n            with autocast('cuda') if AMP_DEVICE else autocast():\n                out = model(aug)\n            probs = torch.softmax(out, 1).cpu().numpy()[0]\n            avg_probs += probs\n        avg_probs /= len(tta_transforms)\n        tta_all_probs.append(avg_probs)\n        all_labels_tta.append(row['diagnosis'])\n\n        if (idx + 1) % 100 == 0:\n            print(f\"  TTA: {idx+1}/{len(val_df)}\")\n\ntta_preds = np.argmax(tta_all_probs, axis=1)\ntta_acc = np.mean(tta_preds == np.array(all_labels_tta)) * 100\ntta_kappa = cohen_kappa_score(all_labels_tta, tta_preds, weights='quadratic') * 100\nprint(f\"\\n🏆 TTA Results — Accuracy: {tta_acc:.1f}% | Kappa: {tta_kappa:.1f}%\")\nprint(f\"   (vs non-TTA: Acc {best_val_acc:.1f}% | κ {best_kappa:.1f}%)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-14T11:03:55.765832Z","iopub.execute_input":"2026-05-14T11:03:55.766613Z","iopub.status.idle":"2026-05-14T11:04:03.013977Z","shell.execute_reply.started":"2026-05-14T11:03:55.766577Z","shell.execute_reply":"2026-05-14T11:04:03.012936Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix, classification_report\nimport seaborn as sns\n\nlabels_names = ['No DR', 'Mild', 'Moderate', 'Severe', 'PDR']\nprint(\"📊 Classification Report (with TTA):\")\nprint(classification_report(all_labels_tta, tta_preds, target_names=labels_names))\n\ncm = confusion_matrix(all_labels_tta, tta_preds)\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=labels_names, yticklabels=labels_names)\nplt.title(f'OptiGemma — Confusion Matrix (Acc: {tta_acc:.1f}%, κ: {tta_kappa:.1f}%)')\nplt.xlabel('Predicted'); plt.ylabel('Actual')\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'confusion_matrix.png'), dpi=150)\nplt.show()\n\n# Training curves\nfig, axes = plt.subplots(1, 3, figsize=(18, 5))\naxes[0].plot(history['train_loss'], label='Train'); axes[0].plot(history['val_loss'], label='Val')\naxes[0].set_title('Loss'); axes[0].legend(); axes[0].axvline(x=5, color='r', ls='--', alpha=0.5); axes[0].axvline(x=15, color='r', ls='--', alpha=0.5)\naxes[1].plot(history['val_acc']); axes[1].set_title('Val Accuracy (%)'); axes[1].axvline(x=5, color='r', ls='--', alpha=0.5); axes[1].axvline(x=15, color='r', ls='--', alpha=0.5)\naxes[2].plot(history['val_kappa']); axes[2].set_title('Cohen Kappa (%)'); axes[2].axvline(x=5, color='r', ls='--', alpha=0.5); axes[2].axvline(x=15, color='r', ls='--', alpha=0.5)\nfor ax in axes: ax.set_xlabel('Epoch')\nplt.suptitle('Red lines = unfreeze phases')\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, 'training_curves.png'), dpi=150)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-14T11:04:03.014766Z","iopub.status.idle":"2026-05-14T11:04:03.015125Z","shell.execute_reply.started":"2026-05-14T11:04:03.014970Z","shell.execute_reply":"2026-05-14T11:04:03.014986Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_path = os.path.join(OUTPUT_DIR, 'optigemma_effnetb3_best.pt')\nmodel_size = os.path.getsize(model_path) / 1e6\nprint(f\"\"\"\n{'='*60}\n🏆 TRAINING COMPLETE!\n{'='*60}\n📦 Model: optigemma_effnetb3_best.pt ({model_size:.1f} MB)\n📊 Accuracy: {tta_acc:.1f}% (with TTA)\n📊 Kappa: {tta_kappa:.1f}% (with TTA)\n{'='*60}\n\n📥 HOW TO DOWNLOAD:\n   1. Click \"Save Version\" (top right)\n   2. Select \"Save & Run All\"\n   3. After completion → go to Output tab\n   4. Download: optigemma_effnetb3_best.pt\n\n🔧 HOW TO INTEGRATE:\n   1. Copy file to: models/vessel_model/best_val_loss.pt\n   2. Restart: python app.py\n   3. Done! 🚀\n{'='*60}\n\"\"\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-14T11:04:03.016748Z","iopub.status.idle":"2026-05-14T11:04:03.017062Z","shell.execute_reply.started":"2026-05-14T11:04:03.016932Z","shell.execute_reply":"2026-05-14T11:04:03.016958Z"}},"outputs":[],"execution_count":null}]}