{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","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"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================ CELL 1: IMPORTS + CONFIG ============================\nimport os, random, warnings\nimport numpy as np, pandas as pd\nimport cv2, torch, timm\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import cohen_kappa_score, classification_report, confusion_matrix\nfrom tqdm import tqdm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport matplotlib.pyplot as plt, seaborn as sns\n\nwarnings.filterwarnings(\"ignore\")\n\n# ---- SEED + DEVICE ----\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED); torch.cuda.manual_seed_all(SEED)\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"✅ Device:\", DEVICE)\n\n# ---- GLOBAL PARAMS ----\nIMG_SIZE = 512\nBATCH_SIZE = 16\nEPOCHS = 12\nNUM_CLASSES = 5\nDATA_DIR = \"/kaggle/input/aptos2019-blindness-detection\"\n\n# ---- Focal Loss ----\nclass FocalLoss(nn.Module):\n    def __init__(self, alpha=1, gamma=2, reduction=\"mean\"):\n        super().__init__()\n        self.alpha, self.gamma, self.reduction = alpha, gamma, reduction\n        self.ce = nn.CrossEntropyLoss(reduction=\"none\")\n    def forward(self, inputs, targets):\n        ce = self.ce(inputs, targets)\n        pt = torch.exp(-ce)\n        loss = self.alpha * (1-pt)**self.gamma * ce\n        return loss.mean() if self.reduction==\"mean\" else loss.sum()\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-18T16:22:59.282091Z","iopub.execute_input":"2025-10-18T16:22:59.282342Z","iopub.status.idle":"2025-10-18T16:23:13.396001Z","shell.execute_reply.started":"2025-10-18T16:22:59.282318Z","shell.execute_reply":"2025-10-18T16:23:13.395299Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 2: LOAD CSV + SPLIT ============================\ndf = pd.read_csv(f\"{DATA_DIR}/train.csv\")\ndf[\"id_code\"] = df[\"id_code\"].astype(str)\ndf[\"image_path\"] = df[\"id_code\"].apply(lambda x: f\"{DATA_DIR}/train_images/{x}.png\")\n\ntrain_df, val_df = train_test_split(df, test_size=0.15, stratify=df[\"diagnosis\"], random_state=SEED)\nval_df, test_df = train_test_split(val_df, test_size=0.5, stratify=val_df[\"diagnosis\"], random_state=SEED)\nprint(f\"Train: {len(train_df)} | Val: {len(val_df)} | Test: {len(test_df)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T16:23:22.265782Z","iopub.execute_input":"2025-10-18T16:23:22.266369Z","iopub.status.idle":"2025-10-18T16:23:22.309134Z","shell.execute_reply.started":"2025-10-18T16:23:22.266343Z","shell.execute_reply":"2025-10-18T16:23:22.308523Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 3: IMAGE PREPROCESSING ============================\ndef circular_crop(img):\n    h, w, _ = img.shape\n    x = w//2; y = h//2; r = min(x, y)\n    mask = np.zeros((h, w), np.uint8)\n    cv2.circle(mask, (x, y), r, 1, -1)\n    img = cv2.bitwise_and(img, img, mask=mask)\n    x1, y1, x2, y2 = x-r, y-r, x+r, y+r\n    img = img[y1:y2, x1:x2]\n    return img\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T16:23:39.114351Z","iopub.execute_input":"2025-10-18T16:23:39.114895Z","iopub.status.idle":"2025-10-18T16:23:39.119372Z","shell.execute_reply.started":"2025-10-18T16:23:39.114873Z","shell.execute_reply":"2025-10-18T16:23:39.118786Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 4: TRANSFORMS (FIXED) ============================\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# use tuple for size because some albumentations versions expect `size` arg\ntrain_transforms = A.Compose([\n    A.RandomResizedCrop(size=(IMG_SIZE, IMG_SIZE), scale=(0.8, 1.0), ratio=(0.9, 1.1), p=0.6),\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.2),\n    A.RandomBrightnessContrast(p=0.5),\n    A.ShiftScaleRotate(shift_limit=0.06, scale_limit=0.06, rotate_limit=15, p=0.4),\n    A.OneOf([A.GaussNoise(), A.ISONoise()], p=0.2),\n    A.Normalize(mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225)),\n    ToTensorV2(),\n])\n\nvalid_transforms = A.Compose([\n    A.Resize(height=IMG_SIZE, width=IMG_SIZE),\n    A.Normalize(mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225)),\n    ToTensorV2(),\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T16:25:31.156978Z","iopub.execute_input":"2025-10-18T16:25:31.157285Z","iopub.status.idle":"2025-10-18T16:25:31.171853Z","shell.execute_reply.started":"2025-10-18T16:25:31.157264Z","shell.execute_reply":"2025-10-18T16:25:31.171244Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 5: DATASET CLASS ============================\nclass APTOSDataset(Dataset):\n    def __init__(self, df, transforms=None, img_size=IMG_SIZE):\n        self.df = df.reset_index(drop=True)\n        self.transforms = transforms\n        self.img_size = img_size\n\n    def __len__(self): return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img = cv2.imread(row.image_path)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = circular_crop(img)\n        img = cv2.resize(img, (self.img_size, self.img_size))\n        if self.transforms: img = self.transforms(image=img)[\"image\"]\n        label = int(row.diagnosis)\n        return img, label\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T16:25:48.074096Z","iopub.execute_input":"2025-10-18T16:25:48.074379Z","iopub.status.idle":"2025-10-18T16:25:48.079862Z","shell.execute_reply.started":"2025-10-18T16:25:48.074360Z","shell.execute_reply":"2025-10-18T16:25:48.079101Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 6: DATALOADERS ============================\ntrain_ds = APTOSDataset(train_df, transforms=train_transforms)\nval_ds   = APTOSDataset(val_df,   transforms=valid_transforms)\ntest_ds  = APTOSDataset(test_df,  transforms=valid_transforms)\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_ds,   batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\ntest_loader  = DataLoader(test_ds,  batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T16:25:58.130079Z","iopub.execute_input":"2025-10-18T16:25:58.130806Z","iopub.status.idle":"2025-10-18T16:25:58.138394Z","shell.execute_reply.started":"2025-10-18T16:25:58.130779Z","shell.execute_reply":"2025-10-18T16:25:58.137486Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 7: MODEL (DenseNet121) ============================\nmodel = timm.create_model(\"densenet121\", pretrained=True)\nin_features = model.classifier.in_features\nmodel.classifier = nn.Linear(in_features, NUM_CLASSES)\nmodel = model.to(DEVICE)\nprint(\"✅ DenseNet121 loaded.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T16:26:12.874995Z","iopub.execute_input":"2025-10-18T16:26:12.875328Z","iopub.status.idle":"2025-10-18T16:26:14.579157Z","shell.execute_reply.started":"2025-10-18T16:26:12.875303Z","shell.execute_reply":"2025-10-18T16:26:14.578350Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 8: OPTIMIZER + LOSS ============================\ncriterion = FocalLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-5)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\nscaler = torch.cuda.amp.GradScaler()   # works fine on PyTorch ≥ 1.11\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T16:26:21.266742Z","iopub.execute_input":"2025-10-18T16:26:21.267432Z","iopub.status.idle":"2025-10-18T16:26:21.272979Z","shell.execute_reply.started":"2025-10-18T16:26:21.267406Z","shell.execute_reply":"2025-10-18T16:26:21.272325Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 9: EVALUATION FUNCTION ============================\n@torch.no_grad()\ndef evaluate(model, loader, criterion):\n    model.eval()\n    total_loss, correct, total = 0, 0, 0\n    all_true, all_pred = [], []\n    device_type = \"cuda\" if DEVICE.type == \"cuda\" else \"cpu\"\n    with torch.amp.autocast(device_type=device_type):\n        for imgs, targets in loader:\n            imgs, targets = imgs.to(DEVICE), targets.to(DEVICE)\n            logits = model(imgs)\n            loss = criterion(logits, targets)\n            total_loss += loss.item() * imgs.size(0)\n            preds = logits.softmax(1).argmax(1)\n            correct += (preds == targets).sum().item()\n            total += imgs.size(0)\n            all_true.extend(targets.cpu().numpy())\n            all_pred.extend(preds.cpu().numpy())\n    acc = correct / total\n    qwk = cohen_kappa_score(all_true, all_pred, weights='quadratic')\n    return total_loss/len(loader.dataset), acc, qwk, all_true, all_pred\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T16:26:28.470811Z","iopub.execute_input":"2025-10-18T16:26:28.471373Z","iopub.status.idle":"2025-10-18T16:26:28.477297Z","shell.execute_reply.started":"2025-10-18T16:26:28.471350Z","shell.execute_reply":"2025-10-18T16:26:28.476660Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 9: EVALUATION FUNCTION ============================\n@torch.no_grad()\ndef evaluate(model, loader, criterion):\n    model.eval()\n    total_loss, correct, total = 0, 0, 0\n    all_true, all_pred = [], []\n    device_type = \"cuda\" if DEVICE.type == \"cuda\" else \"cpu\"\n    with torch.amp.autocast(device_type=device_type):\n        for imgs, targets in loader:\n            imgs, targets = imgs.to(DEVICE), targets.to(DEVICE)\n            logits = model(imgs)\n            loss = criterion(logits, targets)\n            total_loss += loss.item() * imgs.size(0)\n            preds = logits.softmax(1).argmax(1)\n            correct += (preds == targets).sum().item()\n            total += imgs.size(0)\n            all_true.extend(targets.cpu().numpy())\n            all_pred.extend(preds.cpu().numpy())\n    acc = correct / total\n    qwk = cohen_kappa_score(all_true, all_pred, weights='quadratic')\n    return total_loss/len(loader.dataset), acc, qwk, all_true, all_pred\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T16:26:44.924533Z","iopub.execute_input":"2025-10-18T16:26:44.925138Z","iopub.status.idle":"2025-10-18T16:26:44.930986Z","shell.execute_reply.started":"2025-10-18T16:26:44.925115Z","shell.execute_reply":"2025-10-18T16:26:44.930188Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 10: TRAIN LOOP ============================\nhistory = {'train_loss':[], 'train_acc':[], 'val_loss':[], 'val_acc':[], 'val_qwk':[]}\nbest_qwk = -999.0\n\nfor epoch in range(EPOCHS):\n    model.train()\n    running_loss, correct, total = 0.0, 0, 0\n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{EPOCHS}\", leave=False)\n    device_type = \"cuda\" if DEVICE.type == \"cuda\" else \"cpu\"\n\n    for imgs, targets in pbar:\n        imgs, targets = imgs.to(DEVICE), targets.to(DEVICE)\n        optimizer.zero_grad()\n        with torch.amp.autocast(device_type=device_type):\n            logits = model(imgs)\n            loss = criterion(logits, targets)\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        running_loss += loss.item() * imgs.size(0)\n        preds = logits.softmax(1).argmax(1)\n        correct += (preds == targets).sum().item()\n        total += imgs.size(0)\n        pbar.set_postfix({'loss': f'{running_loss/total:.4f}', 'acc': f'{correct/total:.4f}'})\n\n    train_loss = running_loss / len(train_loader.dataset)\n    train_acc = correct / total\n    val_loss, val_acc, val_qwk, _, _ = evaluate(model, val_loader, criterion)\n    history['train_loss'].append(train_loss); history['train_acc'].append(train_acc)\n    history['val_loss'].append(val_loss); history['val_acc'].append(val_acc); history['val_qwk'].append(val_qwk)\n    print(f\"Epoch {epoch+1}: train_loss={train_loss:.4f} train_acc={train_acc:.4f} val_loss={val_loss:.4f} val_acc={val_acc:.4f} val_qwk={val_qwk:.4f}\")\n\n    if val_qwk > best_qwk:\n        best_qwk = val_qwk\n        torch.save(model.state_dict(), \"densenet121_best.pth\")\n        print(f\"✅ Saved best model (val_qwk={best_qwk:.4f})\")\n    scheduler.step()\n\ntorch.save(model.state_dict(), \"densenet121_last.pth\")\nprint(\"Training finished.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T16:26:58.251512Z","iopub.execute_input":"2025-10-18T16:26:58.251782Z","iopub.status.idle":"2025-10-18T17:20:54.878398Z","shell.execute_reply.started":"2025-10-18T16:26:58.251763Z","shell.execute_reply":"2025-10-18T17:20:54.877637Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 11: PLOT METRICS ============================\nplt.figure(figsize=(14,5))\nplt.subplot(1,2,1)\nplt.plot(history['train_loss'], label='train_loss'); plt.plot(history['val_loss'], label='val_loss')\nplt.legend(); plt.title('Loss')\n\nplt.subplot(1,2,2)\nplt.plot(history['train_acc'], label='train_acc'); plt.plot(history['val_acc'], label='val_acc'); plt.plot(history['val_qwk'], label='val_qwk')\nplt.legend(); plt.title('Accuracy / QWK')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T17:21:15.315168Z","iopub.execute_input":"2025-10-18T17:21:15.315470Z","iopub.status.idle":"2025-10-18T17:21:15.754981Z","shell.execute_reply.started":"2025-10-18T17:21:15.315448Z","shell.execute_reply":"2025-10-18T17:21:15.754225Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================ CELL 12: TEST EVALUATION ============================\nimport seaborn as sns\nfrom sklearn.metrics import classification_report, confusion_matrix\n\nckpt_path = \"densenet121_best.pth\"  # file will be in /kaggle/working after training\nassert os.path.exists(ckpt_path), f\"Checkpoint not found: {ckpt_path}\"\n\nmodel.load_state_dict(torch.load(ckpt_path, map_location=DEVICE))\nmodel.to(DEVICE); model.eval()\nprint(f\"✅ Loaded: {ckpt_path}\")\n\ntest_loss, test_acc, test_qwk, y_true, y_pred = evaluate(model, test_loader, criterion)\nprint(f\"\\n📊 Test Loss: {test_loss:.4f}\\nTest Acc: {test_acc:.4f}\\nTest QWK: {test_qwk:.4f}\")\n\nprint(\"\\nClassification Report:\\n\", classification_report(y_true, y_pred, digits=4))\ncm = confusion_matrix(y_true, y_pred)\nplt.figure(figsize=(6,5))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=False)\nplt.title(\"DenseNet121 – Confusion Matrix\")\nplt.xlabel(\"Predicted\"); plt.ylabel(\"True\"); plt.tight_layout(); plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-18T17:21:27.878554Z","iopub.execute_input":"2025-10-18T17:21:27.879295Z","iopub.status.idle":"2025-10-18T17:21:49.110708Z","shell.execute_reply.started":"2025-10-18T17:21:27.879270Z","shell.execute_reply":"2025-10-18T17:21:49.109970Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}