{"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":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install timm albumentations --quiet\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:07.023166Z","iopub.execute_input":"2026-02-02T07:06:07.023429Z","iopub.status.idle":"2026-02-02T07:06:11.879586Z","shell.execute_reply.started":"2026-02-02T07:06:07.023407Z","shell.execute_reply":"2026-02-02T07:06:11.878602Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import train_test_split\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nimport timm\nfrom torchvision import models\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nfrom PIL import Image\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:11.880866Z","iopub.execute_input":"2026-02-02T07:06:11.881159Z","iopub.status.idle":"2026-02-02T07:06:25.103478Z","shell.execute_reply.started":"2026-02-02T07:06:11.881125Z","shell.execute_reply":"2026-02-02T07:06:25.102875Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_DIR = \"/kaggle/input/aptos2019-blindness-detection\"\nIMG_DIR = os.path.join(DATA_DIR, \"train_images\")\nCSV_PATH = os.path.join(DATA_DIR, \"train.csv\")\n\ndf = pd.read_csv(CSV_PATH)\ndf[\"image_path\"] = df[\"id_code\"].apply(lambda x: os.path.join(IMG_DIR, x + \".png\"))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:25.104921Z","iopub.execute_input":"2026-02-02T07:06:25.105332Z","iopub.status.idle":"2026-02-02T07:06:25.138429Z","shell.execute_reply.started":"2026-02-02T07:06:25.105303Z","shell.execute_reply":"2026-02-02T07:06:25.137614Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df, temp_df = train_test_split(\n    df, test_size=0.30, stratify=df[\"diagnosis\"], random_state=42\n)\n\nval_df, test_df = train_test_split(\n    temp_df, test_size=0.50, stratify=temp_df[\"diagnosis\"], random_state=42\n)\n\nprint(len(train_df), len(val_df), len(test_df))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:25.139413Z","iopub.execute_input":"2026-02-02T07:06:25.139813Z","iopub.status.idle":"2026-02-02T07:06:25.158140Z","shell.execute_reply.started":"2026-02-02T07:06:25.139786Z","shell.execute_reply":"2026-02-02T07:06:25.157359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_tfms = A.Compose([\n    A.RandomResizedCrop(\n        size=(224, 224),\n        scale=(0.8, 1.0),\n        ratio=(0.75, 1.33),\n        p=1.0\n    ),\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.5),\n    A.Rotate(limit=30, p=0.5),\n    A.RandomBrightnessContrast(p=0.4),\n    A.Normalize(\n        mean=(0.485, 0.456, 0.406),\n        std=(0.229, 0.224, 0.225)\n    ),\n    ToTensorV2()\n])\n\nval_tfms = A.Compose([\n    A.Resize(224, 224),\n    A.Normalize(\n        mean=(0.485, 0.456, 0.406),\n        std=(0.229, 0.224, 0.225)\n    ),\n    ToTensorV2()\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:25.159090Z","iopub.execute_input":"2026-02-02T07:06:25.159431Z","iopub.status.idle":"2026-02-02T07:06:25.169760Z","shell.execute_reply.started":"2026-02-02T07:06:25.159403Z","shell.execute_reply":"2026-02-02T07:06:25.169136Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class APTOSDataset(Dataset):\n    def __init__(self, df, transform):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img = Image.open(self.df.loc[idx, \"image_path\"]).convert(\"RGB\")\n        label = self.df.loc[idx, \"diagnosis\"]\n\n        img = self.transform(image=np.array(img))[\"image\"]\n        return img, label\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:25.170619Z","iopub.execute_input":"2026-02-02T07:06:25.170841Z","iopub.status.idle":"2026-02-02T07:06:25.177951Z","shell.execute_reply.started":"2026-02-02T07:06:25.170814Z","shell.execute_reply":"2026-02-02T07:06:25.177361Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ds = APTOSDataset(train_df, train_tfms)\nval_ds   = APTOSDataset(val_df, val_tfms)\ntest_ds  = APTOSDataset(test_df, val_tfms)\n\ntrain_loader = DataLoader(train_ds, batch_size=16, shuffle=True, num_workers=2)\nval_loader   = DataLoader(val_ds, batch_size=16, shuffle=False)\ntest_loader  = DataLoader(test_ds, batch_size=16, shuffle=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:25.178928Z","iopub.execute_input":"2026-02-02T07:06:25.179271Z","iopub.status.idle":"2026-02-02T07:06:25.191796Z","shell.execute_reply.started":"2026-02-02T07:06:25.179249Z","shell.execute_reply":"2026-02-02T07:06:25.191116Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CBAM(nn.Module):\n    def __init__(self, channels, reduction=16):\n        super().__init__()\n        self.avg_pool = nn.AdaptiveAvgPool2d(1)\n        self.max_pool = nn.AdaptiveMaxPool2d(1)\n\n        self.mlp = nn.Sequential(\n            nn.Conv2d(channels, channels // reduction, 1),\n            nn.ReLU(),\n            nn.Conv2d(channels // reduction, channels, 1)\n        )\n\n        self.spatial = nn.Conv2d(2, 1, kernel_size=7, padding=3)\n\n    def forward(self, x):\n        ca = torch.sigmoid(self.mlp(self.avg_pool(x)) + self.mlp(self.max_pool(x)))\n        x = x * ca\n\n        sa = torch.cat([x.mean(1, keepdim=True), x.max(1, keepdim=True)[0]], dim=1)\n        sa = torch.sigmoid(self.spatial(sa))\n        return x * sa\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:25.192632Z","iopub.execute_input":"2026-02-02T07:06:25.192843Z","iopub.status.idle":"2026-02-02T07:06:25.202702Z","shell.execute_reply.started":"2026-02-02T07:06:25.192822Z","shell.execute_reply":"2026-02-02T07:06:25.201900Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CNN_ViT(nn.Module):\n    def __init__(self, num_classes=5):\n        super().__init__()\n\n        self.backbone = models.densenet121(pretrained=True)\n        self.backbone = nn.Sequential(*list(self.backbone.features.children()))\n        self.attn = CBAM(1024)\n\n        self.vit = timm.create_model(\n            \"vit_base_patch16_224\",\n            pretrained=True,\n            num_classes=0\n        )\n\n        self.pool = nn.AdaptiveAvgPool2d((1,1))\n        self.fc = nn.Linear(1024 + 768, num_classes)\n\n    def forward(self, x):\n        cnn_feat = self.backbone(x)\n        cnn_feat = self.attn(cnn_feat)\n        cnn_feat = self.pool(cnn_feat).flatten(1)\n\n        vit_feat = self.vit(x)\n\n        feat = torch.cat([cnn_feat, vit_feat], dim=1)\n        return self.fc(feat)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:25.206807Z","iopub.execute_input":"2026-02-02T07:06:25.207273Z","iopub.status.idle":"2026-02-02T07:06:25.217463Z","shell.execute_reply.started":"2026-02-02T07:06:25.207246Z","shell.execute_reply":"2026-02-02T07:06:25.216758Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class FocalLoss(nn.Module):\n    def __init__(self, gamma=2):\n        super().__init__()\n        self.gamma = gamma\n\n    def forward(self, logits, targets):\n        ce = F.cross_entropy(logits, targets, reduction=\"none\")\n        pt = torch.exp(-ce)\n        return ((1 - pt) ** self.gamma * ce).mean()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:25.218338Z","iopub.execute_input":"2026-02-02T07:06:25.218588Z","iopub.status.idle":"2026-02-02T07:06:25.227946Z","shell.execute_reply.started":"2026-02-02T07:06:25.218564Z","shell.execute_reply":"2026-02-02T07:06:25.227291Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nmodel = CNN_ViT().to(device)\ncriterion = FocalLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:48.083102Z","iopub.execute_input":"2026-02-02T07:06:48.084017Z","iopub.status.idle":"2026-02-02T07:06:49.935989Z","shell.execute_reply.started":"2026-02-02T07:06:48.083987Z","shell.execute_reply":"2026-02-02T07:06:49.935351Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_epoch(loader):\n    model.train()\n    correct, total, loss_sum = 0, 0, 0\n\n    for x, y in loader:\n        x, y = x.to(device), y.to(device)\n        optimizer.zero_grad()\n        out = model(x)\n        loss = criterion(out, y)\n        loss.backward()\n        optimizer.step()\n\n        loss_sum += loss.item()\n        correct += (out.argmax(1) == y).sum().item()\n        total += y.size(0)\n\n    return loss_sum/len(loader), correct/total\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:06:54.617488Z","iopub.execute_input":"2026-02-02T07:06:54.618160Z","iopub.status.idle":"2026-02-02T07:06:54.623021Z","shell.execute_reply.started":"2026-02-02T07:06:54.618128Z","shell.execute_reply":"2026-02-02T07:06:54.622407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def eval_epoch(loader):\n    model.eval()\n    correct, total = 0, 0\n\n    with torch.no_grad():\n        for x, y in loader:\n            x, y = x.to(device), y.to(device)\n            out = model(x)\n            correct += (out.argmax(1) == y).sum().item()\n            total += y.size(0)\n\n    return correct/total\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:07:14.338760Z","iopub.execute_input":"2026-02-02T07:07:14.339052Z","iopub.status.idle":"2026-02-02T07:07:14.343781Z","shell.execute_reply.started":"2026-02-02T07:07:14.339027Z","shell.execute_reply":"2026-02-02T07:07:14.342931Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 10\n\nfor epoch in range(EPOCHS):\n    train_loss, train_acc = train_epoch(train_loader)\n    val_acc = eval_epoch(val_loader)\n\n    print(f\"Epoch {epoch+1}/{EPOCHS} | \"\n          f\"Train Acc: {train_acc:.4f} | Val Acc: {val_acc:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:07:24.687812Z","iopub.execute_input":"2026-02-02T07:07:24.688439Z","iopub.status.idle":"2026-02-02T07:45:13.147031Z","shell.execute_reply.started":"2026-02-02T07:07:24.688410Z","shell.execute_reply":"2026-02-02T07:45:13.146250Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_acc = eval_epoch(test_loader)\nprint(\"Final Test Accuracy:\", test_acc)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:45:58.268275Z","iopub.execute_input":"2026-02-02T07:45:58.268922Z","iopub.status.idle":"2026-02-02T07:47:06.589155Z","shell.execute_reply.started":"2026-02-02T07:45:58.268887Z","shell.execute_reply":"2026-02-02T07:47:06.588509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport torch\nfrom sklearn.metrics import (\n    accuracy_score,\n    precision_score,\n    recall_score,\n    f1_score,\n    confusion_matrix,\n    classification_report,\n    cohen_kappa_score,\n    roc_auc_score\n)\nfrom sklearn.preprocessing import label_binarize\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:55:53.674323Z","iopub.execute_input":"2026-02-02T07:55:53.674618Z","iopub.status.idle":"2026-02-02T07:55:53.678574Z","shell.execute_reply.started":"2026-02-02T07:55:53.674592Z","shell.execute_reply":"2026-02-02T07:55:53.677947Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_predictions(model, loader, device):\n    model.eval()\n    y_true, y_pred, y_prob = [], [], []\n\n    with torch.no_grad():\n        for images, labels in loader:\n            images = images.to(device)\n            labels = labels.to(device)\n\n            outputs = model(images)\n            probs = torch.softmax(outputs, dim=1)\n            preds = torch.argmax(outputs, dim=1)\n\n            y_true.extend(labels.cpu().numpy())\n            y_pred.extend(preds.cpu().numpy())\n            y_prob.extend(probs.cpu().numpy())\n\n    return np.array(y_true), np.array(y_pred), np.array(y_prob)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:56:18.075692Z","iopub.execute_input":"2026-02-02T07:56:18.076288Z","iopub.status.idle":"2026-02-02T07:56:18.081586Z","shell.execute_reply.started":"2026-02-02T07:56:18.076261Z","shell.execute_reply":"2026-02-02T07:56:18.080804Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def evaluate_all_metrics(model, loader, device, name=\"SET\"):\n    y_true, y_pred, y_prob = get_predictions(model, loader, device)\n\n    acc = accuracy_score(y_true, y_pred)\n\n    precision_macro = precision_score(y_true, y_pred, average=\"macro\", zero_division=0)\n    precision_weighted = precision_score(y_true, y_pred, average=\"weighted\", zero_division=0)\n\n    recall_macro = recall_score(y_true, y_pred, average=\"macro\", zero_division=0)\n    recall_weighted = recall_score(y_true, y_pred, average=\"weighted\", zero_division=0)\n\n    f1_macro = f1_score(y_true, y_pred, average=\"macro\", zero_division=0)\n    f1_weighted = f1_score(y_true, y_pred, average=\"weighted\", zero_division=0)\n\n    qwk = cohen_kappa_score(y_true, y_pred, weights=\"quadratic\")\n\n    cm = confusion_matrix(y_true, y_pred)\n\n    print(f\"\\n========== {name} RESULTS ==========\")\n    print(f\"Accuracy              : {acc:.4f}\")\n    print(f\"Precision (Macro)     : {precision_macro:.4f}\")\n    print(f\"Precision (Weighted)  : {precision_weighted:.4f}\")\n    print(f\"Recall/Sensitivity(M) : {recall_macro:.4f}\")\n    print(f\"Recall/Sensitivity(W) : {recall_weighted:.4f}\")\n    print(f\"F1-score (Macro)      : {f1_macro:.4f}\")\n    print(f\"F1-score (Weighted)   : {f1_weighted:.4f}\")\n    print(f\"QWK                   : {qwk:.4f}\")\n\n    print(\"\\nConfusion Matrix:\")\n    print(cm)\n\n    print(\"\\nClassification Report:\")\n    print(classification_report(y_true, y_pred, digits=4))\n\n    return y_true, y_pred, y_prob, cm\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:56:19.961098Z","iopub.execute_input":"2026-02-02T07:56:19.961757Z","iopub.status.idle":"2026-02-02T07:56:19.968514Z","shell.execute_reply.started":"2026-02-02T07:56:19.961731Z","shell.execute_reply":"2026-02-02T07:56:19.967606Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def per_class_accuracy(cm):\n    class_acc = cm.diagonal() / cm.sum(axis=1)\n    for i, acc in enumerate(class_acc):\n        print(f\"Class {i} Accuracy: {acc:.4f}\")\n    return class_acc\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:56:22.948740Z","iopub.execute_input":"2026-02-02T07:56:22.949028Z","iopub.status.idle":"2026-02-02T07:56:22.953638Z","shell.execute_reply.started":"2026-02-02T07:56:22.949003Z","shell.execute_reply":"2026-02-02T07:56:22.952816Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def specificity_per_class(cm):\n    spec = []\n    for i in range(len(cm)):\n        tn = np.sum(cm) - (np.sum(cm[i,:]) + np.sum(cm[:,i]) - cm[i,i])\n        fp = np.sum(cm[:,i]) - cm[i,i]\n        spec_i = tn / (tn + fp)\n        spec.append(spec_i)\n        print(f\"Class {i} Specificity: {spec_i:.4f}\")\n    return spec\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:56:41.134986Z","iopub.execute_input":"2026-02-02T07:56:41.135664Z","iopub.status.idle":"2026-02-02T07:56:41.140501Z","shell.execute_reply.started":"2026-02-02T07:56:41.135636Z","shell.execute_reply":"2026-02-02T07:56:41.139827Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def multiclass_roc_auc(y_true, y_prob, n_classes=5):\n    y_true_bin = label_binarize(y_true, classes=list(range(n_classes)))\n    auc = roc_auc_score(y_true_bin, y_prob, average=\"macro\", multi_class=\"ovr\")\n    print(f\"ROC-AUC (Macro, OVR): {auc:.4f}\")\n    return auc\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:56:51.300742Z","iopub.execute_input":"2026-02-02T07:56:51.301383Z","iopub.status.idle":"2026-02-02T07:56:51.305512Z","shell.execute_reply.started":"2026-02-02T07:56:51.301352Z","shell.execute_reply":"2026-02-02T07:56:51.304902Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# TRAIN\ny_t, y_p, y_prob, cm_train = evaluate_all_metrics(\n    model, train_loader, device, name=\"TRAIN\"\n)\nper_class_accuracy(cm_train)\nspecificity_per_class(cm_train)\nmulticlass_roc_auc(y_t, y_prob)\n\n# VALIDATION\ny_t, y_p, y_prob, cm_val = evaluate_all_metrics(\n    model, val_loader, device, name=\"VALIDATION\"\n)\nper_class_accuracy(cm_val)\nspecificity_per_class(cm_val)\nmulticlass_roc_auc(y_t, y_prob)\n\n# TEST\ny_t, y_p, y_prob, cm_test = evaluate_all_metrics(\n    model, test_loader, device, name=\"TEST\"\n)\nper_class_accuracy(cm_test)\nspecificity_per_class(cm_test)\nmulticlass_roc_auc(y_t, y_prob)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-02T07:57:01.919510Z","iopub.execute_input":"2026-02-02T07:57:01.919808Z","iopub.status.idle":"2026-02-02T08:01:39.947312Z","shell.execute_reply.started":"2026-02-02T07:57:01.919783Z","shell.execute_reply":"2026-02-02T08:01:39.946606Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}