{"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":"gpu","dataSources":[{"sourceId":14774,"databundleVersionId":875431,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip -q install timm\n\nimport os, glob, random\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nimport timm\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, f1_score, accuracy_score\n\nimport matplotlib.pyplot as plt\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Device:\", device)\n\nseed = 42\nrandom.seed(seed); np.random.seed(seed); torch.manual_seed(seed)\nif device == \"cuda\":\n    torch.cuda.manual_seed_all(seed)\n\nDATA_ROOT = \"/kaggle/input\"\ncsv_candidates = glob.glob(DATA_ROOT + \"/**/train.csv\", recursive=True)\nprint(\"Found train.csv files:\", csv_candidates[:10])\n\ndef pick_aptos_train_csv(paths):\n    for p in paths:\n        try:\n            df = pd.read_csv(p)\n        except:\n            continue\n        if {\"id_code\",\"diagnosis\"}.issubset(set(df.columns)):\n            return p\n    return None\n\nTRAIN_CSV = pick_aptos_train_csv(csv_candidates)\nprint(\"Chosen TRAIN_CSV:\", TRAIN_CSV)\n\nif TRAIN_CSV is None:\n    raise RuntimeError(\"Couldn't find a train.csv with columns id_code and diagnosis. Check that the competition data is added.\")\n\ndf = pd.read_csv(TRAIN_CSV)\nprint(df.head())\nprint(\"Label counts:\\n\", df[\"diagnosis\"].value_counts().sort_index())\n\nimg_dirs = []\nfor d in glob.glob(DATA_ROOT + \"/**/train_images\", recursive=True):\n    img_dirs.append(d)\n\nif not img_dirs:\n    possible_dirs = set(os.path.dirname(p) for p in glob.glob(DATA_ROOT + \"/**/*.png\", recursive=True))\n    for d in possible_dirs:\n        if len(glob.glob(d + \"/*.png\")) > 1000:\n            img_dirs.append(d)\n\nprint(\"Image directories found:\", img_dirs)\n\ndef get_img_path(id_code):\n    filename = f\"{id_code}.png\"\n    for d in img_dirs:\n        candidate = os.path.join(d, filename)\n        if os.path.exists(candidate):\n            return candidate\n    return None\n\ntest_path = get_img_path(df.iloc[0][\"id_code\"])\nprint(\"Example image path:\", test_path)\nassert test_path is not None, \"Couldn't locate image files. Open /kaggle/input on the left and tell me what folders you see.\"\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:45:53.359618Z","iopub.execute_input":"2026-01-28T01:45:53.360335Z","iopub.status.idle":"2026-01-28T01:46:13.083143Z","shell.execute_reply.started":"2026-01-28T01:45:53.360305Z","shell.execute_reply":"2026-01-28T01:46:13.082326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Split into train + validation (so we can test honestly)\ntrain_df, val_df = train_test_split(\n    df,\n    test_size=0.2,\n    random_state=42,\n    stratify=df[\"diagnosis\"]   # keeps class proportions similar\n)\n\nprint(\"Train size:\", len(train_df), \"Val size:\", len(val_df))\n\n# Image transforms = how we prep images for the model\ntrain_tf = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ColorJitter(brightness=0.15, contrast=0.15),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]),\n])\n\nval_tf = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]),\n])\n\nclass APTOSDataset(Dataset):\n    def __init__(self, dataframe, tfm):\n        self.df = dataframe.reset_index(drop=True)\n        self.tfm = tfm\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 = get_img_path(row[\"id_code\"])\n        img = Image.open(img_path).convert(\"RGB\")\n        x = self.tfm(img)\n        y = int(row[\"diagnosis\"])  # 0–4\n        return x, y\n\ntrain_ds = APTOSDataset(train_df, train_tf)\nval_ds   = APTOSDataset(val_df, val_tf)\n\ntrain_ld = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=2, pin_memory=True)\nval_ld   = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=2, pin_memory=True)\n\nxb, yb = next(iter(train_ld))\nprint(\"Batch shapes:\", xb.shape, yb.shape)\nprint(\"Example labels in batch:\", yb[:20].tolist())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:46:20.055025Z","iopub.execute_input":"2026-01-28T01:46:20.055766Z","iopub.status.idle":"2026-01-28T01:46:30.818851Z","shell.execute_reply.started":"2026-01-28T01:46:20.055731Z","shell.execute_reply":"2026-01-28T01:46:30.817746Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# BASELINE TRAINING (normal loss) + early stopping\n# =========================\n\n# Model: EfficientNet-B0 pretrained, output 5 classes (0–4)\nmodel = timm.create_model(\"efficientnet_b0\", pretrained=True, num_classes=5).to(device)\n\n# Loss = how wrong the model is (baseline: normal cross-entropy, no weights)\nloss_fn = nn.CrossEntropyLoss()\n\n# Optimizer = how the model updates itself to improve\nopt = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-4)\n\n# Mixed precision = faster training on GPU\nscaler = torch.cuda.amp.GradScaler(enabled=(device==\"cuda\"))\n\ndef run_epoch(loader, train=True):\n    model.train(train)\n    total_loss = 0.0\n    all_preds, all_true = [], []\n\n    for x, y in loader:\n        x, y = x.to(device), y.to(device)\n\n        with torch.cuda.amp.autocast(enabled=(device==\"cuda\")):\n            logits = model(x)\n            loss = loss_fn(logits, y)\n\n        if train:\n            opt.zero_grad()\n            scaler.scale(loss).backward()\n            scaler.step(opt)\n            scaler.update()\n\n        total_loss += loss.item() * x.size(0)\n        preds = torch.argmax(logits, dim=1)\n\n        all_preds.append(preds.detach().cpu().numpy())\n        all_true.append(y.detach().cpu().numpy())\n\n    all_preds = np.concatenate(all_preds)\n    all_true  = np.concatenate(all_true)\n\n    acc = accuracy_score(all_true, all_preds)\n    macro_f1 = f1_score(all_true, all_preds, average=\"macro\")\n    return total_loss / len(loader.dataset), acc, macro_f1, all_true, all_preds\n\n\n# Train longer, but stop automatically if it stops improving\nEPOCHS = 10\nPATIENCE = 2  # stop after 2 epochs with no improvement\nbest_f1 = -1\nno_improve = 0\n\nfor epoch in range(1, EPOCHS+1):\n    tr_loss, tr_acc, tr_f1, _, _ = run_epoch(train_ld, train=True)\n    va_loss, va_acc, va_f1, y_true, y_pred = run_epoch(val_ld, train=False)\n\n    print(f\"\\nEpoch {epoch}\")\n    print(\" Train | loss:\", round(tr_loss,4), \"acc:\", round(tr_acc,4), \"macroF1:\", round(tr_f1,4))\n    print(\" Val   | loss:\", round(va_loss,4), \"acc:\", round(va_acc,4), \"macroF1:\", round(va_f1,4))\n\n    # Save best model (highest val macroF1)\n    if va_f1 > best_f1 + 1e-4:\n        best_f1 = va_f1\n        no_improve = 0\n        torch.save(model.state_dict(), \"baseline_best.pt\")\n        print(\"Saved baseline_best.pt (new best)\")\n    else:\n        no_improve += 1\n        print(f\"No improvement count: {no_improve}/{PATIENCE}\")\n\n    if no_improve >= PATIENCE:\n        print(\"Early stopping triggered ✅\")\n        break\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:52:26.942992Z","iopub.execute_input":"2026-01-28T01:52:26.943885Z","iopub.status.idle":"2026-01-28T02:27:33.135267Z","shell.execute_reply.started":"2026-01-28T01:52:26.943841Z","shell.execute_reply":"2026-01-28T02:27:33.134235Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# BASELINE RESULTS (poster-ready)\n# =========================\n\nmodel.load_state_dict(torch.load(\"baseline_best.pt\", map_location=device))\nmodel.eval()\n\n_, _, _, y_true, y_pred = run_epoch(val_ld, train=False)\n\ncm = confusion_matrix(y_true, y_pred, labels=[0,1,2,3,4])\nprint(\"Confusion matrix:\\n\", cm)\n\nrecall = []\nfor k in range(5):\n    total_true_k = (y_true == k).sum()\n    correct_k = ((y_true == k) & (y_pred == k)).sum()\n    r = (correct_k / total_true_k) if total_true_k > 0 else 0\n    recall.append(r)\n\nprint(\"\\nPer-class recall (0-4):\", [round(r,3) for r in recall])\nprint(\"GRADE 1 (mild/early) recall:\", round(recall[1],3))\n\nimport matplotlib.pyplot as plt\nplt.figure(figsize=(6,5))\nplt.imshow(cm)\nplt.title(\"Baseline Confusion Matrix (Grades 0-4)\")\nplt.xlabel(\"Predicted Grade\")\nplt.ylabel(\"True Grade\")\nplt.xticks(range(5)); plt.yticks(range(5))\nfor i in range(5):\n    for j in range(5):\n        plt.text(j, i, cm[i, j], ha=\"center\", va=\"center\")\nplt.tight_layout()\nplt.savefig(\"baseline_confusion_matrix.png\", dpi=200)\nplt.show()\n\nplt.figure(figsize=(6,4))\nplt.bar([0,1,2,3,4], recall)\nplt.title(\"Baseline Per-Class Recall\")\nplt.xlabel(\"DR Grade\")\nplt.ylabel(\"Recall\")\nplt.ylim(0,1)\nplt.tight_layout()\nplt.savefig(\"baseline_recall.png\", dpi=200)\nplt.show()\n\nprint(\"\\nSaved in /kaggle/working:\")\nprint(\" - baseline_best.pt\")\nprint(\" - baseline_confusion_matrix.png\")\nprint(\" - baseline_recall.png\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T02:29:04.986129Z","iopub.execute_input":"2026-01-28T02:29:04.986503Z","iopub.status.idle":"2026-01-28T02:29:53.145848Z","shell.execute_reply.started":"2026-01-28T02:29:04.986468Z","shell.execute_reply":"2026-01-28T02:29:53.144998Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# MODEL 2: COST-SENSITIVE TRAINING (prioritize Grade 1)\n# =========================\n\n# Make class weights from training distribution\ncounts = train_df[\"diagnosis\"].value_counts().sort_index()\nbase_weights = 1.0 / counts.values\nbase_weights = base_weights / base_weights.mean()  # normalize average to ~1\n\nGRADE1_MULT = 2.0  # main knob: higher = more focus on Grade 1\nweights = base_weights.copy()\nweights[1] *= GRADE1_MULT\n\nprint(\"Train label counts:\\n\", counts)\nprint(\"Base weights:\", np.round(base_weights, 3))\nprint(\"Final weights:\", np.round(weights, 3))\n\nweights_tensor = torch.tensor(weights, dtype=torch.float32).to(device)\n\n# New model (same architecture as baseline)\nmodel_cs = timm.create_model(\"efficientnet_b0\", pretrained=True, num_classes=5).to(device)\n\n# Weighted loss (punish missing grade 1 more)\nloss_fn_cs = nn.CrossEntropyLoss(weight=weights_tensor)\n\nopt_cs = torch.optim.AdamW(model_cs.parameters(), lr=2e-4, weight_decay=1e-4)\nscaler_cs = torch.cuda.amp.GradScaler(enabled=(device==\"cuda\"))\n\ndef run_epoch_cs(loader, train=True):\n    model_cs.train(train)\n    total_loss = 0.0\n    all_preds, all_true = [], []\n\n    for x, y in loader:\n        x, y = x.to(device), y.to(device)\n\n        with torch.cuda.amp.autocast(enabled=(device==\"cuda\")):\n            logits = model_cs(x)\n            loss = loss_fn_cs(logits, y)\n\n        if train:\n            opt_cs.zero_grad()\n            scaler_cs.scale(loss).backward()\n            scaler_cs.step(opt_cs)\n            scaler_cs.update()\n\n        total_loss += loss.item() * x.size(0)\n        preds = torch.argmax(logits, dim=1)\n        all_preds.append(preds.detach().cpu().numpy())\n        all_true.append(y.detach().cpu().numpy())\n\n    all_preds = np.concatenate(all_preds)\n    all_true  = np.concatenate(all_true)\n\n    acc = accuracy_score(all_true, all_preds)\n    macro_f1 = f1_score(all_true, all_preds, average=\"macro\")\n    return total_loss / len(loader.dataset), acc, macro_f1, all_true, all_preds\n\n\n# Early stopping (same setup as baseline)\nEPOCHS = 10\nPATIENCE = 2\nbest_f1_cs = -1\nno_improve = 0\n\nfor epoch in range(1, EPOCHS+1):\n    tr_loss, tr_acc, tr_f1, _, _ = run_epoch_cs(train_ld, train=True)\n    va_loss, va_acc, va_f1, y_true_cs, y_pred_cs = run_epoch_cs(val_ld, train=False)\n\n    print(f\"\\n[Cost-Sensitive] Epoch {epoch}\")\n    print(\" Train | loss:\", round(tr_loss,4), \"acc:\", round(tr_acc,4), \"macroF1:\", round(tr_f1,4))\n    print(\" Val   | loss:\", round(va_loss,4), \"acc:\", round(va_acc,4), \"macroF1:\", round(va_f1,4))\n\n    if va_f1 > best_f1_cs + 1e-4:\n        best_f1_cs = va_f1\n        no_improve = 0\n        torch.save(model_cs.state_dict(), \"cost_sensitive_best.pt\")\n        print(\"Saved cost_sensitive_best.pt (new best)\")\n    else:\n        no_improve += 1\n        print(f\"No improvement count: {no_improve}/{PATIENCE}\")\n\n    if no_improve >= PATIENCE:\n        print(\"Early stopping triggered ✅\")\n        break\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T03:10:28.738422Z","iopub.execute_input":"2026-01-28T03:10:28.738814Z","iopub.status.idle":"2026-01-28T03:51:10.017713Z","shell.execute_reply.started":"2026-01-28T03:10:28.738777Z","shell.execute_reply":"2026-01-28T03:51:10.016735Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# MODEL 2 RESULTS (poster-ready)\n# =========================\n\nmodel_cs.load_state_dict(torch.load(\"cost_sensitive_best.pt\", map_location=device))\nmodel_cs.eval()\n\n_, _, _, y_true_cs, y_pred_cs = run_epoch_cs(val_ld, train=False)\n\ncm_cs = confusion_matrix(y_true_cs, y_pred_cs, labels=[0,1,2,3,4])\nprint(\"Cost-sensitive confusion matrix:\\n\", cm_cs)\n\nrecall_cs = []\nfor k in range(5):\n    total_true_k = (y_true_cs == k).sum()\n    correct_k = ((y_true_cs == k) & (y_pred_cs == k)).sum()\n    r = (correct_k / total_true_k) if total_true_k > 0 else 0\n    recall_cs.append(r)\n\nprint(\"\\nCost-sensitive per-class recall (0-4):\", [round(r,3) for r in recall_cs])\nprint(\"COST-SENSITIVE Grade 1 recall:\", round(recall_cs[1],3))\n\nimport matplotlib.pyplot as plt\nplt.figure(figsize=(6,5))\nplt.imshow(cm_cs)\nplt.title(\"Cost-Sensitive Confusion Matrix (Grades 0-4)\")\nplt.xlabel(\"Predicted Grade\")\nplt.ylabel(\"True Grade\")\nplt.xticks(range(5)); plt.yticks(range(5))\nfor i in range(5):\n    for j in range(5):\n        plt.text(j, i, cm_cs[i, j], ha=\"center\", va=\"center\")\nplt.tight_layout()\nplt.savefig(\"cost_sensitive_confusion_matrix.png\", dpi=200)\nplt.show()\n\nplt.figure(figsize=(6,4))\nplt.bar([0,1,2,3,4], recall_cs)\nplt.title(\"Cost-Sensitive Per-Class Recall\")\nplt.xlabel(\"DR Grade\")\nplt.ylabel(\"Recall\")\nplt.ylim(0,1)\nplt.tight_layout()\nplt.savefig(\"cost_sensitive_recall.png\", dpi=200)\nplt.show()\n\nprint(\"\\nSaved in /kaggle/working:\")\nprint(\" - cost_sensitive_best.pt\")\nprint(\" - cost_sensitive_confusion_matrix.png\")\nprint(\" - cost_sensitive_recall.png\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T04:00:40.208269Z","iopub.execute_input":"2026-01-28T04:00:40.208982Z","iopub.status.idle":"2026-01-28T04:01:27.488950Z","shell.execute_reply.started":"2026-01-28T04:00:40.208943Z","shell.execute_reply":"2026-01-28T04:01:27.488199Z"}},"outputs":[],"execution_count":null}]}