{"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":"gpu","dataSources":[{"sourceId":14774,"databundleVersionId":875431,"sourceType":"competition"},{"sourceId":2822109,"sourceType":"datasetVersion","datasetId":1725813},{"sourceId":13394984,"sourceType":"datasetVersion","datasetId":8500103}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, shutil, random\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom collections import Counter\n\nfrom sklearn.metrics import classification_report, confusion_matrix, matthews_corrcoef\nfrom sklearn.model_selection import train_test_split\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import DataLoader, WeightedRandomSampler\nfrom torchvision import datasets, transforms\nfrom torchvision.transforms import RandAugment\nimport timm\nimport seaborn as sns\n\ncsvpath = \"/kaggle/input/aptos-ddr/APTOS_DDR.csv\"\nimgdir1 = \"/kaggle/input/aptos2019-blindness-detection/train_images\"\nimgdir2 = \"/kaggle/input/ddrdataset/DR_grading/DR_grading\"\ndf = pd.read_csv(csvpath)\nprint(\"Total images:\", len(df))\norig_dist = Counter(df['diagnosis'])\nprint(\"Original class distribution:\", orig_dist)\n\n# Remove duplicates\ndf = df.drop_duplicates(subset='id_code')\nprint(\"After removing duplicates:\", len(df))\n\nprint(\"Dataset size:\", len(df))\nprint(\"Class distribution:\", Counter(df['diagnosis']))\n\n# Train/test split\ntrain_df, test_df = train_test_split(\n    df, test_size=0.2, stratify=df['diagnosis'], random_state=42\n)\nprint(\"Train size:\", len(train_df))\nprint(\"Test size:\", len(test_df))\nprint(\"Train distribution:\", Counter(train_df['diagnosis']))\nprint(\"Test distribution:\", Counter(test_df['diagnosis']))\n\n# Folder Creation\ntraindir = \"/kaggle/working/ddr_split/train\"\ntestdir = \"/kaggle/working/ddr_split/test\"\nos.makedirs(traindir, exist_ok=True)\nos.makedirs(testdir, exist_ok=True)\n\nfor c in df['diagnosis'].unique():\n    os.makedirs(os.path.join(traindir, str(c)), exist_ok=True)\n    os.makedirs(os.path.join(testdir, str(c)), exist_ok=True)\n\ndef copy_imgs(data, splitdir):\n    for _, row in data.iterrows():\n        # Try to find image in either directory\n        src = None\n        for d in [imgdir1, imgdir2]:\n            candidate = os.path.join(d, row['id_code'])\n            if os.path.exists(candidate):\n                src = candidate\n                break\n        if src is None:\n            continue\n        dst_dir = os.path.join(splitdir, str(row['diagnosis']))\n        dst = os.path.join(dst_dir, row['id_code'])\n        if not os.path.exists(src):\n            continue\n        try:\n            shutil.copy2(src, dst)\n        except Exception:\n            try:\n                os.makedirs(dst_dir, exist_ok=True)\n                shutil.copy2(src, dst)\n            except Exception:\n                continue\n\ncopy_imgs(train_df, traindir)\ncopy_imgs(test_df, testdir)\n\n# Augmentations\ntrain_tfms = transforms.Compose([\n    transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),\n    transforms.RandomHorizontalFlip(),\n    RandAugment(num_ops=2, magnitude=9),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                         [0.229, 0.224, 0.225])\n])\n\ntest_tfms = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                         [0.229, 0.224, 0.225])\n])\n\ntrain_data = datasets.ImageFolder(traindir, transform=train_tfms)\ntest_data = datasets.ImageFolder(testdir, transform=test_tfms)\n\nprint(\"Train dataset size:\", len(train_data))\nprint(\"Test dataset size:\", len(test_data))\n\n# Weighted Sampler\ntargets = [lbl for _, lbl in train_data.samples]\nclass_counts = Counter(targets)\nclass_weights = [1.0 / class_counts[t] for t in targets]\nsampler = WeightedRandomSampler(class_weights, num_samples=len(targets), replacement=True)\n\ntrain_loader = DataLoader(train_data, batch_size=32, sampler=sampler, num_workers=2, pin_memory=True)\ntest_loader = DataLoader(test_data, batch_size=32, shuffle=False, num_workers=2, pin_memory=True)\n\n# Model\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nnum_classes = len(train_data.classes)\n\nmodel = timm.create_model(\n    \"convnext_tiny\",\n    pretrained=True,\n    num_classes=num_classes,\n    drop_path_rate=0.2,\n    drop_rate=0.3\n).to(device)\n\ncriterion = nn.CrossEntropyLoss(label_smoothing=0.1)\noptimizer = optim.AdamW(model.parameters(), lr=1e-2, weight_decay=1e-5)\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.25, verbose=True)\n\n# Warm-up Freeze\nfor param in model.stem.parameters():\n    param.requires_grad = False\nfor param in model.stages.parameters():\n    param.requires_grad = False\n\n# Training\ndef train_model(epochs=20, unfreeze_epoch=5, patience=5):\n    history = {\"train_loss\": [], \"test_loss\": [], \"train_acc\": [], \"test_acc\": []}\n    best_acc = 0\n    wait = 0\n    best_preds, best_labels = [], []\n\n    for epoch in range(epochs):\n        if epoch == unfreeze_epoch:\n            for param in model.parameters():\n                param.requires_grad = True\n            for g in optimizer.param_groups:\n                g[\"lr\"] = 1e-5\n\n        model.train()\n        tot, cor, runloss = 0, 0, 0.0\n        for imgs, lbls in train_loader:\n            imgs, lbls = imgs.to(device), lbls.to(device)\n            optimizer.zero_grad()\n            out = model(imgs)\n            loss = criterion(out, lbls)\n            loss.backward()\n            optimizer.step()\n            runloss += loss.item() * imgs.size(0)\n            _, preds = torch.max(out, 1)\n            cor += (preds == lbls).sum().item()\n            tot += lbls.size(0)\n        train_acc = cor / tot\n        train_loss = runloss / tot\n\n        model.eval()\n        tot, cor, runloss = 0, 0, 0.0\n        allp, alll = [], []\n        with torch.no_grad():\n            for imgs, lbls in test_loader:\n                imgs, lbls = imgs.to(device), lbls.to(device)\n                out = model(imgs)\n                loss = criterion(out, lbls)\n                runloss += loss.item() * imgs.size(0)\n                _, preds = torch.max(out, 1)\n                allp.extend(preds.cpu().numpy())\n                alll.extend(lbls.cpu().numpy())\n                cor += (preds == lbls).sum().item()\n                tot += lbls.size(0)\n        test_acc = cor / tot\n        test_loss = runloss / tot\n\n        scheduler.step(test_loss)\n\n        history[\"train_loss\"].append(train_loss)\n        history[\"test_loss\"].append(test_loss)\n        history[\"train_acc\"].append(train_acc)\n        history[\"test_acc\"].append(test_acc)\n\n        print(f\"Epoch {epoch+1}/{epochs} | Train Loss {train_loss:.4f} Acc {train_acc:.4f} | \"\n              f\"Test Loss {test_loss:.4f} Acc {test_acc:.4f}\")\n\n        if test_acc > best_acc:\n            best_acc = test_acc\n            wait = 0\n            best_preds, best_labels = allp, alll\n            torch.save(model.state_dict(), \"best_model_v1.pth\")\n        else:\n            wait += 1\n            if wait >= patience:\n                print(\"Early stopping!\")\n                break\n\n    return history, np.array(best_preds), np.array(best_labels)\n\n# Run\nhistory, y_pred, y_true = train_model(epochs=25)\n\n# Plots\nplt.plot(history[\"train_loss\"], label=\"Train Loss\")\nplt.plot(history[\"test_loss\"], label=\"Test Loss\")\nplt.legend(); plt.title(\"Loss Curve\"); plt.show()\n\nplt.plot(history[\"train_acc\"], label=\"Train Acc\")\nplt.plot(history[\"test_acc\"], label=\"Test Acc\")\nplt.legend(); plt.title(\"Accuracy Curve\"); plt.show()\n\ncm = confusion_matrix(y_true, y_pred)\ncm_norm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]\n\nplt.figure(figsize=(8,6))\nsns.heatmap(cm_norm, annot=True, fmt=\".2f\", cmap=\"Blues\",\n            xticklabels=train_data.classes,\n            yticklabels=train_data.classes)\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"True\")\nplt.title(\"Normalized Confusion Matrix\")\nplt.show()\n\nprint(\"\\nClassification Report:\\n\", classification_report(y_true, y_pred, target_names=train_data.classes))\nprint(\"MCC:\", matthews_corrcoef(y_true, y_pred))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-15T14:44:59.043791Z","iopub.execute_input":"2025-10-15T14:44:59.044022Z","iopub.status.idle":"2025-10-15T17:40:01.110820Z","shell.execute_reply.started":"2025-10-15T14:44:59.044005Z","shell.execute_reply":"2025-10-15T17:40:01.110146Z"}},"outputs":[],"execution_count":null}]}