{"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":24800,"datasetId":1042002,"databundleVersionId":1831594}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -q timm ensemble-boxes pycocotools grad-cam","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:50:43.75908Z","iopub.execute_input":"2026-05-20T07:50:43.75932Z","iopub.status.idle":"2026-05-20T07:50:56.081998Z","shell.execute_reply.started":"2026-05-20T07:50:43.759297Z","shell.execute_reply":"2026-05-20T07:50:56.081009Z"}},"outputs":[{"name":"stdout","text":"\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m7.8/7.8 MB\u001b[0m \u001b[31m22.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n\u001b[?25h  Installing build dependencies ... \u001b[?25l\u001b[?25hdone\n  Getting requirements to build wheel ... \u001b[?25l\u001b[?25hdone\n  Preparing metadata (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n  Building wheel for grad-cam (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n","output_type":"stream"}],"execution_count":1},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport json\nimport time\nimport math\nimport random\nimport warnings\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom collections import Counter\n\nimport matplotlib.pyplot as plt\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nimport torchvision\nfrom torchvision.models.detection import fasterrcnn_resnet50_fpn\nfrom torchvision.models.detection.faster_rcnn import FastRCNNPredictor\n\nimport timm\n\nimport albumentations as A\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report\n\nfrom torchmetrics.detection.mean_ap import MeanAveragePrecision\n\nfrom pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\n\nwarnings.filterwarnings(\"ignore\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:50:56.083708Z","iopub.execute_input":"2026-05-20T07:50:56.084055Z","iopub.status.idle":"2026-05-20T07:51:17.526112Z","shell.execute_reply.started":"2026-05-20T07:50:56.084026Z","shell.execute_reply":"2026-05-20T07:51:17.525503Z"}},"outputs":[],"execution_count":2},{"cell_type":"markdown","source":"# 3. CONFIG\n=========================================================","metadata":{}},{"cell_type":"code","source":"\nCONFIG = {\n    \"seed\": 42,\n    \"img_size\": 768,\n    \"epochs\": 30,\n    \"train_bs\": 4,\n    \"val_bs\": 4,\n    \"num_workers\": 2,\n    \"lr\": 1e-4,\n    \"weight_decay\": 1e-4,\n    \"device\": \"cuda\" if torch.cuda.is_available() else \"cpu\",\n    \"mixed_precision\": True,\n    \"grad_clip\": 5.0,\n    \"save_dir\": \"/kaggle/working/foundation_outputs\",\n}\nprint(CONFIG)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:17.526921Z","iopub.execute_input":"2026-05-20T07:51:17.527336Z","iopub.status.idle":"2026-05-20T07:51:17.779978Z","shell.execute_reply.started":"2026-05-20T07:51:17.527314Z","shell.execute_reply":"2026-05-20T07:51:17.778785Z"}},"outputs":[{"name":"stdout","text":"{'seed': 42, 'img_size': 768, 'epochs': 30, 'train_bs': 4, 'val_bs': 4, 'num_workers': 2, 'lr': 0.0001, 'weight_decay': 0.0001, 'device': 'cuda', 'mixed_precision': True, 'grad_clip': 5.0, 'save_dir': '/kaggle/working/foundation_outputs'}\n","output_type":"stream"}],"execution_count":3},{"cell_type":"markdown","source":"# 4. REPRODUCIBILITY\n=========================================================","metadata":{}},{"cell_type":"code","source":"\n\ndef seed_everything(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n\nseed_everything(CONFIG[\"seed\"])\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:17.781149Z","iopub.execute_input":"2026-05-20T07:51:17.781485Z","iopub.status.idle":"2026-05-20T07:51:17.824487Z","shell.execute_reply.started":"2026-05-20T07:51:17.781453Z","shell.execute_reply":"2026-05-20T07:51:17.82366Z"}},"outputs":[],"execution_count":4},{"cell_type":"markdown","source":"# 5. OUTPUT DIRECTORIES\n=========================================================","metadata":{}},{"cell_type":"code","source":"\nRUN_NAME = time.strftime(\"run_%Y%m%d_%H%M%S\")\nOUT_DIR = Path(CONFIG[\"save_dir\"]) / RUN_NAME\n\nCKPT_DIR = OUT_DIR / \"checkpoints\"\nVIS_DIR = OUT_DIR / \"visualizations\"\nLOG_DIR = OUT_DIR / \"logs\"\nXAI_DIR = OUT_DIR / \"xai\"\n\nfor d in [CKPT_DIR, VIS_DIR, LOG_DIR, XAI_DIR]:\n    d.mkdir(parents=True, exist_ok=True)\n\nprint(\"Output directory:\", OUT_DIR)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:17.826519Z","iopub.execute_input":"2026-05-20T07:51:17.826812Z","iopub.status.idle":"2026-05-20T07:51:17.833095Z","shell.execute_reply.started":"2026-05-20T07:51:17.826789Z","shell.execute_reply":"2026-05-20T07:51:17.832417Z"}},"outputs":[{"name":"stdout","text":"Output directory: /kaggle/working/foundation_outputs/run_20260520_075117\n","output_type":"stream"}],"execution_count":5},{"cell_type":"markdown","source":"# 6. LOAD DATA\n=========================================================","metadata":{}},{"cell_type":"code","source":"\ntrain_csv = \"/kaggle/input/competitions/vinbigdata-chest-xray-abnormalities-detection/train.csv\"\ntrain_img_dir = \"/kaggle/input/competitions/vinbigdata-chest-xray-abnormalities-detection/train\"\n\ntrain_df = pd.read_csv(train_csv)\n\nprint(train_df.shape)\ntrain_df.head()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:17.833938Z","iopub.execute_input":"2026-05-20T07:51:17.834171Z","iopub.status.idle":"2026-05-20T07:51:17.974941Z","shell.execute_reply.started":"2026-05-20T07:51:17.834151Z","shell.execute_reply":"2026-05-20T07:51:17.974305Z"}},"outputs":[{"name":"stdout","text":"(67914, 8)\n","output_type":"stream"},{"execution_count":6,"output_type":"execute_result","data":{"text/plain":"                           image_id          class_name  class_id rad_id  \\\n0  50a418190bc3fb1ef1633bf9678929b3          No finding        14    R11   \n1  21a10246a5ec7af151081d0cd6d65dc9          No finding        14     R7   \n2  9a5094b2563a1ef3ff50dc5c7ff71345        Cardiomegaly         3    R10   \n3  051132a778e61a86eb147c7c6f564dfe  Aortic enlargement         0    R10   \n4  063319de25ce7edb9b1c6b8881290140          No finding        14    R10   \n\n    x_min   y_min   x_max   y_max  \n0     NaN     NaN     NaN     NaN  \n1     NaN     NaN     NaN     NaN  \n2   691.0  1375.0  1653.0  1831.0  \n3  1264.0   743.0  1611.0  1019.0  \n4     NaN     NaN     NaN     NaN  ","text/html":"<div>\n<style scoped>\n    .dataframe tbody tr th:only-of-type {\n        vertical-align: middle;\n    }\n\n    .dataframe tbody tr th {\n        vertical-align: top;\n    }\n\n    .dataframe thead th {\n        text-align: right;\n    }\n</style>\n<table border=\"1\" class=\"dataframe\">\n  <thead>\n    <tr style=\"text-align: right;\">\n      <th></th>\n      <th>image_id</th>\n      <th>class_name</th>\n      <th>class_id</th>\n      <th>rad_id</th>\n      <th>x_min</th>\n      <th>y_min</th>\n      <th>x_max</th>\n      <th>y_max</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <th>0</th>\n      <td>50a418190bc3fb1ef1633bf9678929b3</td>\n      <td>No finding</td>\n      <td>14</td>\n      <td>R11</td>\n      <td>NaN</td>\n      <td>NaN</td>\n      <td>NaN</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>1</th>\n      <td>21a10246a5ec7af151081d0cd6d65dc9</td>\n      <td>No finding</td>\n      <td>14</td>\n      <td>R7</td>\n      <td>NaN</td>\n      <td>NaN</td>\n      <td>NaN</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>2</th>\n      <td>9a5094b2563a1ef3ff50dc5c7ff71345</td>\n      <td>Cardiomegaly</td>\n      <td>3</td>\n      <td>R10</td>\n      <td>691.0</td>\n      <td>1375.0</td>\n      <td>1653.0</td>\n      <td>1831.0</td>\n    </tr>\n    <tr>\n      <th>3</th>\n      <td>051132a778e61a86eb147c7c6f564dfe</td>\n      <td>Aortic enlargement</td>\n      <td>0</td>\n      <td>R10</td>\n      <td>1264.0</td>\n      <td>743.0</td>\n      <td>1611.0</td>\n      <td>1019.0</td>\n    </tr>\n    <tr>\n      <th>4</th>\n      <td>063319de25ce7edb9b1c6b8881290140</td>\n      <td>No finding</td>\n      <td>14</td>\n      <td>R10</td>\n      <td>NaN</td>\n      <td>NaN</td>\n      <td>NaN</td>\n      <td>NaN</td>\n    </tr>\n  </tbody>\n</table>\n</div>"},"metadata":{}}],"execution_count":6},{"cell_type":"markdown","source":"# 7. EDA\n=========================================================","metadata":{}},{"cell_type":"code","source":"\n#7.1 Number of labels\nprint(train_df[\"class_name\"].value_counts())\n#7.2 Distribution plot\nplt.figure(figsize=(12,6))\ntrain_df[\"class_name\"].value_counts().plot(kind=\"bar\")\nplt.title(\"Class Distribution\")\nplt.tight_layout()\nplt.savefig(VIS_DIR / \"class_distribution.png\")\nplt.show()\n#7.3 Multi-label statistics\nmulti_counts = train_df.groupby(\"image_id\")[\"class_id\"].nunique()\n\nprint(multi_counts.describe())\n\nplt.figure(figsize=(8,5))\nplt.hist(multi_counts, bins=20)\nplt.title(\"Number of Labels per Image\")\nplt.savefig(VIS_DIR / \"labels_per_image.png\")\nplt.show()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:17.975882Z","iopub.execute_input":"2026-05-20T07:51:17.976208Z","iopub.status.idle":"2026-05-20T07:51:18.602597Z","shell.execute_reply.started":"2026-05-20T07:51:17.976183Z","shell.execute_reply":"2026-05-20T07:51:18.601851Z"}},"outputs":[{"name":"stdout","text":"class_name\nNo finding            31818\nAortic enlargement     7162\nCardiomegaly           5427\nPleural thickening     4842\nPulmonary fibrosis     4655\nNodule/Mass            2580\nLung Opacity           2483\nPleural effusion       2476\nOther lesion           2203\nInfiltration           1247\nILD                    1000\nCalcification           960\nConsolidation           556\nAtelectasis             279\nPneumothorax            226\nName: count, dtype: int64\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"<Figure size 1200x600 with 1 Axes>","image/png":"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gwEAx48fh6enJ+Lj4zFmzBiBE5JYLF26FIsWLYKXlxeMjIwQHR2N0tJSNuuuxaxZs9ReK5VKFBcX49///jdPxCQiIiIiotfG7XskCo6Ojpg8eTJmzpypNh4VFYXvvvsO2dnZAiUjsZFKpQgJCcGUKVMAAAcPHsTQoUNRXl5ea0P9xszDw0PttZ6eHszNzdG/f39MmDABTZrw+QYREREREb06FqVIFJo1a4aLFy/Czs5ObTwvLw/Ozs6oqKgQKBmJTbNmzZCXlwcrKyvVmKGhIfLy8mBpaSlgMiIiIiIiosaFywJIFKysrHDo0CGN8YMHD6oVF4h+T2VlJQwNDdXGmjZtimfPngmUSHex/xYREREREdUn7rkgUZg9ezaCg4ORkZGB3r17A3jeUyo+Ph7R0dECpyMxUSqVGD9+PJo1a6Yaq6iowNSpU2FsbKwaS05OFiKeTmH/LSIiIiIiqk/cvkeisWPHDnz99deq/lGOjo6YM2cOhg8fLnAyEpPAwMA6Xbdx48Z6TqL72H+LiIiIiIjqE4tSJHpZWVlwdnYWOgZRg8P+W0REREREVJ/4qJtE6dGjR1i/fj3c3d3RtWtXoeMQNUjsv0VERERERPWJPaVIVNLS0rBhwwYkJyejXbt28PPzQ0xMjNCxiBok9t8iIiIiIqL6xKIU6bzbt28jPj4esbGxePjwIf7617/iyZMn+Ne//gUnJyeh4xE1WAEBARpjH3/8sQBJiIiIiIioIWJPKdJpPj4+SEtLw9ChQzF27FgMGjQI+vr6aNq0KWQyGYtSRERERERERCLFlVKk0/bu3Yvg4GBMmzYNUqlU6DhERERERERE9Iaw0TnptGPHjuHRo0fo3r073nvvPaxevRp3794VOhYRERERERER/UHcvkei8PjxYyQlJSEuLg6nT5+GXC5HVFQUJkyYAFNTU6HjEREREREREdErYlGKRCc3NxexsbHYtGkTHjx4gAEDBmDXrl1CxyIiIiIiIiKiV8CiFImWXC7H7t27ERcXx6IUERERERERkciwKEVERERERERERFrHRudERERERERERKR1LEoREREREREREZHWsShFRERERERERERax6IUERERERERERFpHYtSRERERHVQWFgIiUSCjIwMoaMQERERNQgsShERERERERERkdaxKEVERERERERERFrHohQRERFRFQqFAsuXL4ednR2aNWsGa2trLF26VOM6uVyOiRMnwsbGBkZGRnj33XcRHR2tds2RI0fg7u4OY2NjtGrVCn369MG1a9cAADKZDB4eHjA1NUWLFi3QvXt3nD179nfzxcfHo1WrVti/fz8cHR1hYmKCQYMGobi4WHXNmTNnMGDAALz99tto2bIl+vbti3Pnzql9HYlEgnXr1mHYsGFo3rw5HB0dcfLkSeTl5aFfv34wNjZG7969kZ+fr/bndu7cCTc3NxgaGsLW1hZhYWGorKys8/9fIiIiohdYlCIiIiKqYv78+YiMjERoaCguXbqEzZs345133tG4TqFQwNLSEj/88AMuXbqEBQsW4PPPP8e2bdsAAJWVlfjwww/Rt29fXLhwASdPnsTkyZMhkUgAAGPHjoWlpSXOnDmD9PR0zJs3D02bNq1Txl9//RVfffUVNm3ahLS0NFy/fh0hISGq+UePHiEgIADHjh3DqVOnIJVKMWTIEDx69Ejt6yxevBj+/v7IyMiAg4MDxowZgylTpmD+/Pk4e/YslEolgoKCVNcfPXoU/v7+mDFjBi5duoR169YhPj6+2qIdERER0e+RKJVKpdAhiIiIiHTBo0ePYG5ujtWrV+OTTz5RmyssLISNjQ3Onz+Pbt26Vfvng4KCcPv2bWzfvh337t3DW2+9hSNHjqBv374a17Zo0QLffPMNAgICXiljfHw8AgMDkZeXh06dOgEA1qxZg/DwcNy+fbvaP6NQKNCqVSts3rwZw4YNA/B8pdSXX36JxYsXAwBOnTqF999/H7GxsZgwYQIAYOvWrQgMDER5eTkAwMvLC56enpg/f77qa3///feYO3cubt269UrfBxERERFXShERERH9v+zsbDx58gSenp51uj4mJgbdu3eHubk5TExMsH79ely/fh0A0Lp1a4wfPx7e3t7w8fFBdHS02ha7WbNm4ZNPPoGXlxciIyM1tsnVpnnz5qqCFAC0bdsWpaWlqtclJSWYNGkSpFIpWrZsiRYtWqCsrEyV7YUuXbqoPn+xGszFxUVtrKKiAg8fPgTwfMtheHg4TExMVB+TJk1CcXExfv311zrnJyIiIgJYlCIiIiJSMTIyqvO1W7duRUhICCZOnIgDBw4gIyMDgYGBePr0qeqajRs34uTJk+jduzeSkpJgb2+PU6dOAQAWLVqEixcvYujQoTh8+DCcnJywY8eOOv23X97mJ5FIUHXxe0BAADIyMhAdHY0TJ04gIyMDb731llq2l7/Oi22F1Y0pFAoAQFlZGcLCwpCRkaH6yMzMxJUrV2BoaFin7EREREQvNBE6ABEREZGukEqlMDIywqFDhzS2773s+PHj6N27Nz799FPVWHWrnVxdXeHq6or58+fj/fffx+bNm9GrVy8AgL29Pezt7TFz5kyMHj0aGzduxIgRI/7w93H8+HGsWbMGQ4YMAQDcuHEDd+/e/cNf183NDbm5ubCzs/vDX4uIiIiIRSkiIiKi/2doaIjPPvsMc+fOhYGBAfr06YM7d+7g4sWLGlv6pFIpEhMTsX//ftjY2GDTpk04c+YMbGxsAAAFBQVYv349fH190a5dO+Tm5uLKlSvw9/dHeXk55syZg48++gg2Nja4efMmzpw5gz//+c9v5PuQSqXYtGkTevTogYcPH2LOnDmvtAqsJgsWLMCwYcNgbW2Njz76CHp6epDJZMjKysKSJUveQHIiIiJqTFiUIiIiIqoiNDQUTZo0wYIFC3Dr1i20bdsWU6dO1bhuypQpOH/+PEaOHAmJRILRo0fj008/xd69ewE87/uUk5ODhIQE/Pzzz2jbti2mT5+OKVOmoLKyEj///DP8/f1RUlKCt99+G35+fggLC3sj30NsbCwmT54MNzc3WFlZISIiQu10vtfl7e2Nn376CeHh4Vi2bBmaNm0KBweH311VRkRERFQdnr5HRERERERERERax0bnRERERERERESkdSxKEREREemQwYMHw8TEpNqPiIgIoeMRERERvTHcvkdERESkQ4qKilBeXl7tXOvWrdG6dWstJyIiIiKqHyxKERERERERERGR1nH7HhERERERERERaR2LUkREREREREREpHUsShERERERERERkdaxKEVERERERERERFrHohQREREREREREWkdi1JERERERERERKR1LEoREREREREREZHWsShFRERERERERERa939LEZqxZM0RdQAAAABJRU5ErkJggg==\n"},"metadata":{}},{"name":"stdout","text":"count    15000.000000\nmean         1.731400\nstd          1.466355\nmin          1.000000\n25%          1.000000\n50%          1.000000\n75%          2.000000\nmax         10.000000\nName: class_id, dtype: float64\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"<Figure size 800x500 with 1 Axes>","image/png":"iVBORw0KGgoAAAANSUhEUgAAArUAAAHDCAYAAAA6M+IAAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAANLBJREFUeJzt3X1cVHX+///nIHERMINYgJOKrFpquZmwKWqayYqKlZ/swiI1M60W1tDMsBLNvKQsNUuy2uhjumXtaqWbxldNdo28oLU1r7LSNF2gUpjEvILz+6Mf5+OEltnA+NbH/XY7t1vzfr/OOa8zM9Xzdji8cViWZQkAAAAwWIC/GwAAAAB+K0ItAAAAjEeoBQAAgPEItQAAADAeoRYAAADGI9QCAADAeIRaAAAAGI9QCwAAAOMRagEAAGA8Qi2As9IHH3wgh8Oht956y9+tnJaSkhLdfPPNatCggRwOh2bMmFEn57322mt1xRVX+PSYTZs21V133eXTYwJAbSPUAuexvLw8ORwOhYSEaO/evTXmayMwnatGjBih5cuXa8yYMZo3b5569ux5ylqHw6GMjIw67O78Vf0d37Bhg79bAVDLAv3dAAD/O3LkiKZOnapnn33W360Ya+XKlbrxxhs1atQof7cCAOcl7tQCUNu2bfXiiy9q3759/m6lzlVUVPjkOKWlpYqMjPTJsfDr+OozBGA2Qi0APfLII6qsrNTUqVN/tm7Xrl1yOBzKy8urMedwODR+/Hj79fjx4+VwOPTZZ5/pzjvvlMvl0sUXX6yxY8fKsizt2bNHN954o5xOp2JjYzV9+vSTnrOyslKPPPKIYmNjFRYWphtuuEF79uypUbd27Vr17NlTLpdLF154obp27ao1a9Z41VT3tGXLFt1xxx2qX7++Onfu/LPX/OWXX+qWW25RVFSULrzwQnXo0EFLly6156t/vG1Zlp577jk5HA45HI6fPebpePvtt5Wamiq3263g4GA1a9ZMTzzxhCorK09aX1RUpI4dOyo0NFTx8fHKzc2tUXPkyBGNGzdOzZs3V3BwsBo3bqzRo0fryJEjP9vLsWPH9Pjjj6tFixYKCQlRgwYN1LlzZ+Xn5//sftXvTUFBge699141aNBATqdTAwcO1IEDB2rUv/fee7rmmmsUFhamiIgIpaamavPmzV41d911l8LDw/XFF1+od+/eioiIUFpa2s/28VPVx9i9e7f69Omj8PBwXXLJJXruueckSZs2bdJ1112nsLAwxcXFacGCBV7779+/X6NGjVKbNm0UHh4up9OpXr166ZNPPqlxrq+++ko33HCDwsLCFB0dbT+m4nA49MEHH3jVns53GMCpEWoBKD4+XgMHDqyVu7W33XabqqqqNHXqVLVv314TJ07UjBkz9Mc//lGXXHKJpk2bpubNm2vUqFEqKCiosf+kSZO0dOlSPfzwwxo+fLjy8/OVnJysH374wa5ZuXKlunTpIo/Ho3Hjxmny5MkqKyvTddddp3Xr1tU45i233KJDhw5p8uTJGjp06Cl7LykpUceOHbV8+XL96U9/0qRJk3T48GHdcMMNWrRokSSpS5cumjdvniTpj3/8o+bNm2e//i3y8vIUHh6ukSNHaubMmUpISFB2draysrJq1B44cEC9e/dWQkKCcnJy1KhRI91///36y1/+YtdUVVXphhtu0FNPPaXrr79ezz77rPr27atnnnlGt91228/2Mn78eD3++OPq1q2bZs+erUcffVRNmjTRxx9/fFrXkpGRoa1bt2r8+PEaOHCg5s+fr759+8qyLLtm3rx5Sk1NVXh4uKZNm6axY8dqy5Yt6ty5s3bt2uV1vOPHjyslJUXR0dF66qmn1K9fv9Pq40SVlZXq1auXGjdurJycHDVt2lQZGRnKy8tTz549lZiYqGnTpikiIkIDBw7Uzp077X2//PJLLV68WH369NHTTz+thx56SJs2bVLXrl29/v2pqKjQddddp//3//6fhg8frkcffVQffvihHn744Rr9/NrvMICTsACct1555RVLkrV+/Xrriy++sAIDA63hw4fb8127drUuv/xy+/XOnTstSdYrr7xS41iSrHHjxtmvx40bZ0myhg0bZo8dP37catSokeVwOKypU6fa4wcOHLBCQ0OtQYMG2WOrVq2yJFmXXHKJ5fF47PGFCxdakqyZM2dalmVZVVVVVosWLayUlBSrqqrKrjt06JAVHx9v/fGPf6zR0+23335a709mZqYlyfrnP/9pj33//fdWfHy81bRpU6uystLr+tPT00/ruKdTe+jQoRpj9957r3XhhRdahw8ftse6du1qSbKmT59ujx05csRq27atFR0dbR09etSyLMuaN2+eFRAQ4HUtlmVZubm5liRrzZo19lhcXJzXZ3HllVdaqampp3VtJ6r+fiUkJNh9WJZl5eTkWJKst99+27KsH9/TyMhIa+jQoV77FxcXWy6Xy2t80KBBliQrKyvrV/Wwfv36GseYPHmyPVb9HXQ4HNbrr79uj2/btq3Gd/vw4cNen71l/fjvRnBwsDVhwgR7bPr06ZYka/HixfbYDz/8YLVs2dKSZK1atcqyrF/3HQZwatypBSBJ+t3vfqcBAwZo7ty5+u9//+uz495zzz32P9erV0+JiYmyLEtDhgyxxyMjI3XZZZfpyy+/rLH/wIEDFRERYb+++eab1bBhQ/3jH/+QJG3cuFE7duzQHXfcoe+++07ffvutvv32W1VUVKh79+4qKChQVVWV1zHvu+++0+r9H//4h66++mqvRxTCw8M1bNgw7dq1S1u2bDm9N+EMhIaG2v/8/fff69tvv9U111yjQ4cOadu2bV61gYGBuvfee+3XQUFBuvfee1VaWqqioiJJ0ptvvqlWrVqpZcuW9nv07bff6rrrrpMkrVq16pS9REZGavPmzdqxY8cZXcuwYcN0wQUX2K/vv/9+BQYG2p9hfn6+ysrKdPvtt3v1Vq9ePbVv3/6kvd1///1n1MuJTvxuVn8Hw8LCdOutt9rjl112mSIjI72+m8HBwQoI+PF/n5WVlfruu+8UHh6uyy67zOvu9bJly3TJJZfohhtusMdCQkJq/HTgTL7DAGpi9QMAtscee0zz5s3T1KlTNXPmTJ8cs0mTJl6vXS6XQkJCdNFFF9UY/+6772rs36JFC6/XDodDzZs3t38kXR20Bg0adMoeysvLVb9+fft1fHz8afX+1VdfqX379jXGW7VqZc/X1pJnmzdv1mOPPaaVK1fK4/F4zZWXl3u9drvdCgsL8xq79NJLJf34HHSHDh20Y8cObd26VRdffPFJz1daWnrKXiZMmKAbb7xRl156qa644gr17NlTAwYM0O9///vTupaffobh4eFq2LBhjc+wOmD/lNPp9HodGBioRo0anda5TyUkJKTGe+FyudSoUaMaz0S7XC6vZ4Crqqo0c+ZMPf/889q5c6fXc84NGjSw//mrr75Ss2bNahyvefPmXq/P5DsMoCZCLQDb7373O915552aO3fuSZ/dPNUvQJ3ql5ekH+/Ons6YJK9nLE9X9R2sJ598Um3btj1pTXh4uNfrE++Cno3KysrUtWtXOZ1OTZgwQc2aNVNISIg+/vhjPfzww2d0166qqkpt2rTR008/fdL5xo0bn3LfLl266IsvvtDbb7+t999/Xy+99JKeeeYZ5ebmet3tPFPV1zNv3jzFxsbWmA8M9P5f1Yl3Ss/Uqb6Dp/PdnDx5ssaOHau7775bTzzxhKKiohQQEKDMzMwz/mykX/cdBlAToRaAl8cee0yvvfaapk2bVmOu+k5RWVmZ1/hXX31Va/389EfelmXp888/t+8SNmvWTNKPd/OSk5N9eu64uDht3769xnj1j//j4uJ8er5qH3zwgb777jv9/e9/V5cuXezxE39Z6UT79u1TRUWF193azz77TNKPfx1M+vF9+uSTT9S9e/czWp0hKipKgwcP1uDBg3Xw4EF16dJF48ePP61Qu2PHDnXr1s1+ffDgQf33v/9V79697d4kKTo62uefYW1466231K1bN7388ste42VlZV4/gYiLi9OWLVtkWZbXe/7555977Veb32HgfMIztQC8NGvWTHfeeadeeOEFFRcXe805nU5ddNFFNVYpeP7552utn//93//V999/b79+66239N///le9evWSJCUkJKhZs2Z66qmndPDgwRr7f/PNN2d87t69e2vdunUqLCy0xyoqKjR37lw1bdpUrVu3PuNj/5zqu4Un3h08evToKd/n48eP64UXXvCqfeGFF3TxxRcrISFBknTrrbdq7969evHFF2vs/8MPP/zsWq8/fSwkPDxczZs3/8WlwKrNnTtXx44ds1/PmTNHx48ftz/DlJQUOZ1OTZ482auu2m/5DGtDvXr1avxU4c0336zxV/lSUlK0d+9evfPOO/bY4cOHa3wGtfkdBs4n3KkFUMOjjz6qefPmafv27br88su95u655x5NnTpV99xzjxITE1VQUGDfFawNUVFR6ty5swYPHqySkhLNmDFDzZs3t3/ZJiAgQC+99JJ69eqlyy+/XIMHD9Yll1yivXv3atWqVXI6nXr33XfP6NxZWVn661//ql69emn48OGKiorSq6++qp07d+pvf/vbb/oR+IYNGzRx4sQa49dee606duyo+vXra9CgQRo+fLgcDofmzZt3yscz3G63pk2bpl27dunSSy/VG2+8oY0bN2ru3Ln2L2gNGDBACxcu1H333adVq1apU6dOqqys1LZt27Rw4UItX75ciYmJJz1+69atde211yohIUFRUVHasGGD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2rVrFRYWppSUFB0+fNiuSUtL0+bNm5Wfn68lS5aooKBAw4YNs+c9Ho969OihuLg4FRUV6cknn9T48eM1d+5cX18SAAAAznIO68RbqD7Qp08fxcTE6OWXX7bH+vXrp9DQUL322muyLEtut1sPPvigRo0aJUkqLy9XTEyM8vLy1L9/f23dulWtW7fW+vXrlZiYKElatmyZevfura+//lput1tz5szRo48+quLiYgUFBUmSsrKytHjxYm3btu20evV4PHK5XCovL5fT6fTl23BKTbOW1sl5qu2amlqn5wMAAPCl081rPr9T27FjR61YsUKfffaZJOmTTz7Rv/71L/Xq1UuStHPnThUXFys5Odnex+VyqX379iosLJQkFRYWKjIy0g60kpScnKyAgACtXbvWrunSpYsdaCUpJSVF27dv14EDB07a25EjR+TxeLw2AAAAmC/Q1wfMysqSx+NRy5YtVa9ePVVWVmrSpElKS0uTJBUXF0uSYmJivPaLiYmx54qLixUdHe3daGCgoqKivGri4+NrHKN6rn79+jV6mzJlih5//HEfXCUAAADOJj6/U7tw4ULNnz9fCxYs0Mcff6xXX31VTz31lF599VVfn+pXGzNmjMrLy+1tz549/m4JAAAAPuDzO7UPPfSQsrKy1L9/f0lSmzZt9NVXX2nKlCkaNGiQYmNjJUklJSVq2LChvV9JSYnatm0rSYqNjVVpaanXcY8fP679+/fb+8fGxqqkpMSrpvp1dc1PBQcHKzg4+LdfJAAAAM4qPr9Te+jQIQUEeB+2Xr16qqqqkiTFx8crNjZWK1assOc9Ho/Wrl2rpKQkSVJSUpLKyspUVFRk16xcuVJVVVVq3769XVNQUKBjx47ZNfn5+brssstO+ugBAAAAzl0+D7XXX3+9Jk2apKVLl2rXrl1atGiRnn76af3P//yPJMnhcCgzM1MTJ07UO++8o02bNmngwIFyu93q27evJKlVq1bq2bOnhg4dqnXr1mnNmjXKyMhQ//795Xa7JUl33HGHgoKCNGTIEG3evFlvvPGGZs6cqZEjR/r6kgAAAHCW8/njB88++6zGjh2rP/3pTyotLZXb7da9996r7Oxsu2b06NGqqKjQsGHDVFZWps6dO2vZsmUKCQmxa+bPn6+MjAx1795dAQEB6tevn2bNmmXPu1wuvf/++0pPT1dCQoIuuugi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Ztpjhw8fVnp6uho0aKDw8HD169dPJSUlXvvt3r1bqampuvDCCxUdHa2HHnpIx48f96r54IMP1K5dOwUHB6t58+bKy8ur7csBAADAWahWQ+369ev1wgsv6Pe//73X+IgRI/Tuu+/qzTff1OrVq7Vv3z7ddNNN9nxlZaVSU1N19OhRffjhh3r11VeVl5en7Oxsu2bnzp1KTU1Vt27dtHHjRmVmZuqee+7R8uXLa/OSAAAAcBaqtVB78OBBpaWl6cUXX1T9+vXt8fLycr388st6+umndd111ykhIUGvvPKKPvzwQ3300UeSpPfff19btmzRa6+9prZt26pXr1564okn9Nxzz+no0aOSpNzcXMXHx2v69Olq1aqVMjIydPPNN+uZZ56prUsCAADAWarWQm16erpSU1OVnJzsNV5UVKRjx455jbds2VJNmjRRYWGhJKmwsFBt2rRRTEyMXZOSkiKPx6PNmzfbNT89dkpKin2Mkzly5Ig8Ho/XBgAAAPMF1sZBX3/9dX388cdav359jbni4mIFBQUpMjLSazwmJkbFxcV2zYmBtnq+eu7najwej3744QeFhobWOPeUKVP0+OOPn/F1AQAA4Ozk8zu1e/bs0QMPPKD58+crJCTE14f/TcaMGaPy8nJ727Nnj79bAgAAgA/4PNQWFRWptLRU7dq1U2BgoAIDA7V69WrNmjVLgYGBiomJ0dGjR1VWVua1X0lJiWJjYyVJsbGxNVZDqH79SzVOp/Okd2klKTg4WE6n02sDAACA+Xweart3765NmzZp48aN9paYmKi0tDT7ny+44AKtWLHC3mf79u3avXu3kpKSJElJSUnatGmTSktL7Zr8/Hw5nU61bt3arjnxGNU11ccAAADA+cPnz9RGREToiiuu8BoLCwtTgwYN7PEhQ4Zo5MiRioqKktPp1J///GclJSWpQ4cOkqQePXqodevWGjBggHJyclRcXKzHHntM6enpCg4OliTdd999mj17tkaPHq27775bK1eu1MKFC7V06VJfXxIAAADOcrXyi2K/5JlnnlFAQID69eunI0eOKCUlRc8//7w9X69ePS1ZskT333+/kpKSFBYWpkGDBmnChAl2TXx8vJYuXaoRI0Zo5syZatSokV566SWlpKT445IAAADgRw7Lsix/N+EvHo9HLpdL5eXldfZ8bdOsur2TvGtqap2eDwAA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x/r166fQ0FC99tprfuwMtcHhcGjRokXq27evpB/v0rrdbj344IMaNWqUJKm8vFwxMTHKy8tT//79/dInd2pRZ8rLyyVJUVFRfu4EtS09PV2pqalKTk72dyuoZe+8844SExN1yy23KDo6WldddZVefPFFf7eFWtaxY0etWLFCn332mSTpk08+0b/+9S/16tXLz52hLuzcuVPFxcVe/413uVxq3769CgsL/daX0X9RDOaoqqpSZmamOnXqpCuuuMLf7aAWvf766/r444+1fv16f7eCOvDll19qzpw5GjlypB555BGtX79ew4cPV1BQkAYNGuTv9lBLsrKy5PF41LJlS9WrV0+VlZWaNGmS0tLS/N0a6kBxcbEk1fgLrjExMfacPxBqUSfS09P16aef6l//+pe/W0Et2rNnjx544AHl5+crJCTE3+2gDlRVVSkxMVGTJ0+WJF111VX69NNPlZubS6g9hy1cuFDz58/XggULdPnll2vjxo3KzMyU2+3mc4ff8PgBal1GRoaWLFmiVatWqVGjRv5uB7WoqKhIpaWlateunQIDAxUYGKjVq1dr1qxZCgwMVGVlpb9bhI81bNhQrVu39hpr1aqVdu/e7aeOUBceeughZWVlqX///mrTpo0GDBigESNGaMqUKf5uDXUgNjZWklRSUuI1XlJSYs/5A6EWtcayLGVkZGjRokVauXKl4uPj/d0Saln37t21adMmbdy40d4SExOVlpamjRs3ql69ev5uET7WqVOnGkv1ffbZZ4qLi/NTR6gLhw4dUkCAd4SoV6+eqqqq/NQR6lJ8fLxiY2O1YsUKe8zj8Wjt2rVKSkryW188foBak56ergULFujtt99WRESE/ZyNy+VSaGion7tDbYiIiKjxzHRYWJgaNGjAs9TnqBEjRqhjx46aPHmybr31Vq1bt05z587V3Llz/d0aatH111+vSZMmqUmTJrr88sv173//W08//bTuvvtuf7cGHzl48KA+//xz+/XOnTu1ceNGRUVFqUmTJsrMzNTEiRPVokULxcfHa+zYsXK73fYKCf7Akl6oNQ6H46Tjr7zyiu666666bQZ+c+2117Kk1zluyZIlGjNmjHbs2KH4+HiNHDlSQ4cO9XdbqEXff/+9xo4dq0WLFqm0tFRut1u33367srOzFRQU5O/24AMffPCBunXrVmN80KBBysvLk2VZGjdunObOnauysjJ17txZzz//vC699FI/dPsjQi0AAACMxzO1AAAAMB6hFgAAAMYj1AIAAMB4hFoAAAAYj1ALAAAA4xFqAQAAYDxCLQAAAIxHqAUAAIDxCLUAAAAwHqEWAAAAxiPUAgAAwHiEWgAAABjv/wOnNd/w+Yzk4gAAAABJRU5ErkJggg==\n"},"metadata":{}}],"execution_count":7},{"cell_type":"markdown","source":"# 8. BUILD IMAGE TARGETS\n=========================================================","metadata":{}},{"cell_type":"code","source":"\nNO_FINDING_ID = 14\n\nbbox_df = train_df.dropna(subset=[\"x_min\", \"y_min\", \"x_max\", \"y_max\"])\n\nbbox_df = bbox_df[bbox_df[\"class_id\"] != NO_FINDING_ID]\n\n\ndef build_targets(df):\n\n    rows = []\n\n    for image_id, g in df.groupby(\"image_id\"):\n\n        boxes = g[[\"x_min\", \"y_min\", \"x_max\", \"y_max\"]].values.astype(np.float32)\n        labels = g[\"class_id\"].values.astype(np.int64)\n\n        rows.append({\n            \"image_id\": image_id,\n            \"boxes\": boxes,\n            \"labels\": labels\n        })\n\n    return pd.DataFrame(rows)\n\n\ntargets_df = build_targets(bbox_df)\n\nprint(targets_df.shape)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:18.603768Z","iopub.execute_input":"2026-05-20T07:51:18.604077Z","iopub.status.idle":"2026-05-20T07:51:20.22288Z","shell.execute_reply.started":"2026-05-20T07:51:18.604051Z","shell.execute_reply":"2026-05-20T07:51:20.222177Z"}},"outputs":[{"name":"stdout","text":"(4394, 3)\n","output_type":"stream"}],"execution_count":8},{"cell_type":"markdown","source":"# 9. TRAIN VALID SPLIT\n=========================================================","metadata":{}},{"cell_type":"code","source":"\ntr_targets, va_targets = train_test_split(\n    targets_df,\n    test_size=0.2,\n    random_state=CONFIG[\"seed\"]\n)\n\ntr_targets = tr_targets.reset_index(drop=True)\nva_targets = va_targets.reset_index(drop=True)\n\nprint(len(tr_targets), len(va_targets))\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:20.223778Z","iopub.execute_input":"2026-05-20T07:51:20.224029Z","iopub.status.idle":"2026-05-20T07:51:20.23165Z","shell.execute_reply.started":"2026-05-20T07:51:20.224006Z","shell.execute_reply":"2026-05-20T07:51:20.230872Z"}},"outputs":[{"name":"stdout","text":"3515 879\n","output_type":"stream"}],"execution_count":9},{"cell_type":"markdown","source":"# 10. DICOM + CLAHE\n=========================================================","metadata":{}},{"cell_type":"code","source":"\nimport pydicom\n\n\ndef read_dicom(path):\n\n    dcm = pydicom.dcmread(path)\n\n    img = dcm.pixel_array.astype(np.float32)\n\n    img = (img - img.min()) / (img.max() - img.min() + 1e-6)\n\n    return img\n\n\ndef apply_clahe01(img01):\n\n    img_u8 = (img01 * 255).astype(np.uint8)\n\n    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))\n\n    out = clahe.apply(img_u8)\n\n    out = out.astype(np.float32) / 255.0\n\n    return out\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:20.232726Z","iopub.execute_input":"2026-05-20T07:51:20.233161Z","iopub.status.idle":"2026-05-20T07:51:20.646693Z","shell.execute_reply.started":"2026-05-20T07:51:20.233119Z","shell.execute_reply":"2026-05-20T07:51:20.646096Z"}},"outputs":[],"execution_count":10},{"cell_type":"markdown","source":"# 11. ALBUMENTATIONS\n=========================================================","metadata":{}},{"cell_type":"code","source":"\nIMG_SIZE = CONFIG[\"img_size\"]\n\ntrain_tfms = A.Compose([\n\n    A.LongestMaxSize(max_size=IMG_SIZE),\n\n    A.PadIfNeeded(\n        min_height=IMG_SIZE,\n        min_width=IMG_SIZE,\n        border_mode=cv2.BORDER_CONSTANT,\n        fill=0\n    ),\n\n    A.RandomBrightnessContrast(p=0.5),\n\n    A.GaussNoise(p=0.2),\n\n    A.Affine(\n        translate_percent=(-0.02, 0.02),\n        scale=(0.95, 1.05),\n        rotate=(-5, 5),\n        border_mode=cv2.BORDER_CONSTANT,\n        fill=0,\n        p=0.5\n    )\n\n], bbox_params=A.BboxParams(\n    format=\"pascal_voc\",\n    label_fields=[\"labels\"]\n))\n\nval_tfms = A.Compose([\n\n    A.LongestMaxSize(max_size=IMG_SIZE),\n\n    A.PadIfNeeded(\n        min_height=IMG_SIZE,\n        min_width=IMG_SIZE,\n        border_mode=cv2.BORDER_CONSTANT,\n        fill=0\n    )\n\n], bbox_params=A.BboxParams(\n    format=\"pascal_voc\",\n    label_fields=[\"labels\"]\n))\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:20.64754Z","iopub.execute_input":"2026-05-20T07:51:20.647782Z","iopub.status.idle":"2026-05-20T07:51:20.657616Z","shell.execute_reply.started":"2026-05-20T07:51:20.647762Z","shell.execute_reply":"2026-05-20T07:51:20.656887Z"}},"outputs":[],"execution_count":11},{"cell_type":"markdown","source":"# 12. DATASET\n=========================================================","metadata":{}},{"cell_type":"code","source":"\nbbox_class_ids = sorted(bbox_df[\"class_id\"].unique().tolist())\nid2cont = {cid: i+1 for i, cid in enumerate(bbox_class_ids)}\ncont2id = {v:k for k,v in id2cont.items()}\nNUM_CLASSES = len(bbox_class_ids) + 1\nclass VinCXRDataset(Dataset):\n\n    def __init__(self, targets_df, img_dir, tfms):\n\n        self.targets = targets_df.reset_index(drop=True)\n        self.img_dir = Path(img_dir)\n        self.tfms = tfms\n\n    def __len__(self):\n        return len(self.targets)\n\n    def __getitem__(self, idx):\n\n        row = self.targets.iloc[idx]\n\n        image_id = row.image_id\n\n        img = read_dicom(self.img_dir / f\"{image_id}.dicom\")\n\n        img = apply_clahe01(img)\n\n        boxes = np.array(row.boxes, dtype=np.float32)\n        labels = np.array(row.labels, dtype=np.int64)\n\n        labels = np.array([id2cont[x] for x in labels])\n\n        out = self.tfms(\n            image=img,\n            bboxes=boxes,\n            labels=labels\n        )\n\n        img_t = out[\"image\"]\n\n        boxes = np.array(out[\"bboxes\"], dtype=np.float32)\n        labels = np.array(out[\"labels\"], dtype=np.int64)\n\n        img_t = torch.tensor(img_t).float()\n\n        img_t = img_t.unsqueeze(0).repeat(3,1,1)\n\n        mean = torch.tensor([0.485, 0.456, 0.406]).view(3,1,1)\n        std = torch.tensor([0.229, 0.224, 0.225]).view(3,1,1)\n\n        img_t = (img_t - mean) / std\n\n        target = {\n            \"boxes\": torch.tensor(boxes).float(),\n            \"labels\": torch.tensor(labels).long(),\n            \"image_id\": torch.tensor([idx])\n        }\n\n        return img_t, target, image_id\n\ndef collate_fn(batch):\n    return tuple(zip(*batch))\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:20.658879Z","iopub.execute_input":"2026-05-20T07:51:20.659143Z","iopub.status.idle":"2026-05-20T07:51:20.671645Z","shell.execute_reply.started":"2026-05-20T07:51:20.659115Z","shell.execute_reply":"2026-05-20T07:51:20.67107Z"}},"outputs":[],"execution_count":12},{"cell_type":"markdown","source":"# 13. DATALOADER\n=========================================================","metadata":{}},{"cell_type":"code","source":"\ntrain_ds = VinCXRDataset(tr_targets, train_img_dir, train_tfms)\nval_ds = VinCXRDataset(va_targets, train_img_dir, val_tfms)\ntrain_loader = DataLoader(\n    train_ds,\n    batch_size=CONFIG[\"train_bs\"],\n    shuffle=True,\n    num_workers=CONFIG[\"num_workers\"],\n    pin_memory=torch.cuda.is_available(),\n    collate_fn=collate_fn\n)\nval_loader = DataLoader(\n    val_ds,\n    batch_size=CONFIG[\"val_bs\"],\n    shuffle=False,\n    num_workers=CONFIG[\"num_workers\"],\n    pin_memory=torch.cuda.is_available(),\n    collate_fn=collate_fn\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:20.672541Z","iopub.execute_input":"2026-05-20T07:51:20.672943Z","iopub.status.idle":"2026-05-20T07:51:20.684335Z","shell.execute_reply.started":"2026-05-20T07:51:20.67291Z","shell.execute_reply":"2026-05-20T07:51:20.683569Z"}},"outputs":[],"execution_count":13},{"cell_type":"markdown","source":"# 14. FOUNDATION DETECTOR\n=========================================================\nOption A: FasterRCNN baseline\nmodel = fasterrcnn_resnet50_fpn(weights=\"DEFAULT\")\n","metadata":{}},{"cell_type":"code","source":"model = fasterrcnn_resnet50_fpn(weights=\"DEFAULT\")\nin_features = model.roi_heads.box_predictor.cls_score.in_features\n\nmodel.roi_heads.box_predictor = FastRCNNPredictor(\n    in_features,\n    NUM_CLASSES\n)\n\nmodel.to(CONFIG[\"device\"])\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:20.686748Z","iopub.execute_input":"2026-05-20T07:51:20.687054Z","iopub.status.idle":"2026-05-20T07:51:22.559198Z","shell.execute_reply.started":"2026-05-20T07:51:20.687026Z","shell.execute_reply":"2026-05-20T07:51:22.55849Z"}},"outputs":[{"name":"stdout","text":"Downloading: \"https://download.pytorch.org/models/fasterrcnn_resnet50_fpn_coco-258fb6c6.pth\" to /root/.cache/torch/hub/checkpoints/fasterrcnn_resnet50_fpn_coco-258fb6c6.pth\n","output_type":"stream"},{"name":"stderr","text":"100%|██████████| 160M/160M [00:00<00:00, 206MB/s] \n","output_type":"stream"},{"execution_count":14,"output_type":"execute_result","data":{"text/plain":"FasterRCNN(\n  (transform): GeneralizedRCNNTransform(\n      Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n      Resize(min_size=(800,), max_size=1333, mode='bilinear')\n  )\n  (backbone): BackboneWithFPN(\n    (body): IntermediateLayerGetter(\n      (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)\n      (bn1): FrozenBatchNorm2d(64, eps=0.0)\n      (relu): ReLU(inplace=True)\n      (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n      (layer1): Sequential(\n        (0): Bottleneck(\n          (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(64, eps=0.0)\n          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(64, eps=0.0)\n          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(256, eps=0.0)\n          (relu): ReLU(inplace=True)\n          (downsample): Sequential(\n            (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n            (1): FrozenBatchNorm2d(256, eps=0.0)\n          )\n        )\n        (1): Bottleneck(\n          (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(64, eps=0.0)\n          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(64, eps=0.0)\n          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(256, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n        (2): Bottleneck(\n          (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(64, eps=0.0)\n          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(64, eps=0.0)\n          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(256, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n      )\n      (layer2): Sequential(\n        (0): Bottleneck(\n          (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(128, eps=0.0)\n          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(128, eps=0.0)\n          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(512, eps=0.0)\n          (relu): ReLU(inplace=True)\n          (downsample): Sequential(\n            (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)\n            (1): FrozenBatchNorm2d(512, eps=0.0)\n          )\n        )\n        (1): Bottleneck(\n          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(128, eps=0.0)\n          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(128, eps=0.0)\n          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(512, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n        (2): Bottleneck(\n          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(128, eps=0.0)\n          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(128, eps=0.0)\n          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(512, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n        (3): Bottleneck(\n          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(128, eps=0.0)\n          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(128, eps=0.0)\n          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(512, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n      )\n      (layer3): Sequential(\n        (0): Bottleneck(\n          (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(256, eps=0.0)\n          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(256, eps=0.0)\n          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(1024, eps=0.0)\n          (relu): ReLU(inplace=True)\n          (downsample): Sequential(\n            (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)\n            (1): FrozenBatchNorm2d(1024, eps=0.0)\n          )\n        )\n        (1): Bottleneck(\n          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(256, eps=0.0)\n          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(256, eps=0.0)\n          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(1024, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n        (2): Bottleneck(\n          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(256, eps=0.0)\n          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(256, eps=0.0)\n          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(1024, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n        (3): Bottleneck(\n          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(256, eps=0.0)\n          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(256, eps=0.0)\n          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(1024, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n        (4): Bottleneck(\n          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(256, eps=0.0)\n          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(256, eps=0.0)\n          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(1024, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n        (5): Bottleneck(\n          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(256, eps=0.0)\n          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(256, eps=0.0)\n          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(1024, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n      )\n      (layer4): Sequential(\n        (0): Bottleneck(\n          (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(512, eps=0.0)\n          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(512, eps=0.0)\n          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(2048, eps=0.0)\n          (relu): ReLU(inplace=True)\n          (downsample): Sequential(\n            (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)\n            (1): FrozenBatchNorm2d(2048, eps=0.0)\n          )\n        )\n        (1): Bottleneck(\n          (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(512, eps=0.0)\n          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(512, eps=0.0)\n          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(2048, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n        (2): Bottleneck(\n          (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn1): FrozenBatchNorm2d(512, eps=0.0)\n          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n          (bn2): FrozenBatchNorm2d(512, eps=0.0)\n          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n          (bn3): FrozenBatchNorm2d(2048, eps=0.0)\n          (relu): ReLU(inplace=True)\n        )\n      )\n    )\n    (fpn): FeaturePyramidNetwork(\n      (inner_blocks): ModuleList(\n        (0): Conv2dNormActivation(\n          (0): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1))\n        )\n        (1): Conv2dNormActivation(\n          (0): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))\n        )\n        (2): Conv2dNormActivation(\n          (0): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1))\n        )\n        (3): Conv2dNormActivation(\n          (0): Conv2d(2048, 256, kernel_size=(1, 1), stride=(1, 1))\n        )\n      )\n      (layer_blocks): ModuleList(\n        (0-3): 4 x Conv2dNormActivation(\n          (0): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))\n        )\n      )\n      (extra_blocks): LastLevelMaxPool()\n    )\n  )\n  (rpn): RegionProposalNetwork(\n    (anchor_generator): AnchorGenerator()\n    (head): RPNHead(\n      (conv): Sequential(\n        (0): Conv2dNormActivation(\n          (0): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))\n          (1): ReLU(inplace=True)\n        )\n      )\n      (cls_logits): Conv2d(256, 3, kernel_size=(1, 1), stride=(1, 1))\n      (bbox_pred): Conv2d(256, 12, kernel_size=(1, 1), stride=(1, 1))\n    )\n  )\n  (roi_heads): RoIHeads(\n    (box_roi_pool): MultiScaleRoIAlign(featmap_names=['0', '1', '2', '3'], output_size=(7, 7), sampling_ratio=2)\n    (box_head): TwoMLPHead(\n      (fc6): Linear(in_features=12544, out_features=1024, bias=True)\n      (fc7): Linear(in_features=1024, out_features=1024, bias=True)\n    )\n    (box_predictor): FastRCNNPredictor(\n      (cls_score): Linear(in_features=1024, out_features=15, bias=True)\n      (bbox_pred): Linear(in_features=1024, out_features=60, bias=True)\n    )\n  )\n)"},"metadata":{}}],"execution_count":14},{"cell_type":"markdown","source":"# 15. PNEUMOFUSION BACKBONE IDEAS\n\nKey deployment modifications\n•\tgrayscale adaptation\n•\tDSC blocks\n•\tchannel attention\n•\tspatial attention\n•\tSwin transformer compatible features\nSuggested production architecture\n1.\tEfficientNet backbone\n2.\tBiFPN fusion\n3.\tGCSA attention\n4.\tDetection head\n5.\tClassification head\n6.\tXAI branch\n__________________","metadata":{}},{"cell_type":"code","source":"# =========================================================\n# 15) GCSA + SWIN FOUNDATION BACKBONE\n# =========================================================\n#\n# Objective:\n# Hospital-grade chest X-ray foundation backbone\n#\n# Core ideas:\n# 1. Grayscale medical adaptation\n# 2. Depthwise separable convolution\n# 3. Global Channel-Spatial Attention (GCSA)\n# 4. Swin Transformer fusion\n# 5. Multi-scale feature learning\n# 6. Explainability-friendly architecture\n#\n# =========================================================\n\nimport timm\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n# =========================================================\n# 15.1 Depthwise Separable Convolution\n# =========================================================\n\nclass DepthwiseSeparableConv(nn.Module):\n\n    def __init__(self, in_ch, out_ch):\n\n        super().__init__()\n\n        self.depthwise = nn.Conv2d(\n            in_ch,\n            in_ch,\n            kernel_size=3,\n            padding=1,\n            groups=in_ch,\n            bias=False\n        )\n\n        self.pointwise = nn.Conv2d(\n            in_ch,\n            out_ch,\n            kernel_size=1,\n            bias=False\n        )\n\n        self.bn = nn.BatchNorm2d(out_ch)\n\n        self.act = nn.SiLU(inplace=True)\n\n    def forward(self, x):\n\n        x = self.depthwise(x)\n\n        x = self.pointwise(x)\n\n        x = self.bn(x)\n\n        x = self.act(x)\n\n        return x\n\n\n# =========================================================\n# 15.2 Global Channel-Spatial Attention\n# =========================================================\n\nclass GCSA(nn.Module):\n\n    def __init__(self, channels, reduction=16):\n\n        super().__init__()\n\n        # Channel Attention\n        self.avg_pool = nn.AdaptiveAvgPool2d(1)\n\n        self.max_pool = nn.AdaptiveMaxPool2d(1)\n\n        self.mlp = nn.Sequential(\n            nn.Linear(channels, channels // reduction),\n            nn.ReLU(inplace=True),\n            nn.Linear(channels // reduction, channels)\n        )\n\n        # Spatial Attention\n        self.spatial_conv = nn.Conv2d(\n            2,\n            1,\n            kernel_size=7,\n            padding=3,\n            bias=False\n        )\n\n        self.sigmoid = nn.Sigmoid()\n\n    # -----------------------------------------------------\n    # Channel Shuffle\n    # -----------------------------------------------------\n\n    def channel_shuffle(self, x, groups=4):\n\n        B, C, H, W = x.size()\n\n        x = x.view(B, groups, C // groups, H, W)\n\n        x = x.transpose(1, 2).contiguous()\n\n        x = x.view(B, C, H, W)\n\n        return x\n\n    # -----------------------------------------------------\n    # Forward\n    # -----------------------------------------------------\n\n    def forward(self, x):\n\n        B, C, H, W = x.size()\n\n        # =================================================\n        # Channel Attention\n        # =================================================\n\n        avg_feat = self.avg_pool(x).view(B, C)\n\n        max_feat = self.max_pool(x).view(B, C)\n\n        ch_att = self.mlp(avg_feat) + self.mlp(max_feat)\n\n        ch_att = self.sigmoid(ch_att).view(B, C, 1, 1)\n\n        x = x * ch_att\n\n        # =================================================\n        # Channel Shuffle\n        # =================================================\n\n        x = self.channel_shuffle(x)\n\n        # =================================================\n        # Spatial Attention\n        # =================================================\n\n        avg_spatial = torch.mean(x, dim=1, keepdim=True)\n\n        max_spatial, _ = torch.max(x, dim=1, keepdim=True)\n\n        spatial = torch.cat(\n            [avg_spatial, max_spatial],\n            dim=1\n        )\n\n        spatial_att = self.sigmoid(\n            self.spatial_conv(spatial)\n        )\n\n        out = x * spatial_att\n\n        return out\n\n\n# =========================================================\n# 15.3 Swin Transformer Fusion Block\n# =========================================================\n\nclass SwinFusionBlock(nn.Module):\n\n    def __init__(\n        self,\n        img_dim=1024,\n        embed_dim=96,\n        pretrained=True\n    ):\n\n        super().__init__()\n\n        # =================================================\n        # Swin Transformer Backbone\n        # =================================================\n\n        self.backbone = timm.create_model(\n            \"swin_tiny_patch4_window7_224\",\n            pretrained=pretrained,\n            features_only=True,\n            in_chans=3\n        )\n\n        backbone_channels = self.backbone.feature_info.channels()\n\n        # =================================================\n        # DSC + GCSA stages\n        # =================================================\n\n        self.refine_blocks = nn.ModuleList()\n\n        for ch in backbone_channels:\n\n            block = nn.Sequential(\n\n                DepthwiseSeparableConv(\n                    ch,\n                    ch\n                ),\n\n                GCSA(ch)\n\n            )\n\n            self.refine_blocks.append(block)\n\n        # =================================================\n        # Multi-scale Fusion\n        # =================================================\n\n        total_channels = sum(backbone_channels)\n\n        self.fusion_conv = nn.Sequential(\n\n            nn.Conv2d(\n                total_channels,\n                embed_dim,\n                kernel_size=1,\n                bias=False\n            ),\n\n            nn.BatchNorm2d(embed_dim),\n\n            nn.SiLU(inplace=True)\n\n        )\n\n    # -----------------------------------------------------\n    # Forward\n    # -----------------------------------------------------\n\n    def forward(self, x):\n\n        # =================================================\n        # Swin Features\n        # =================================================\n\n        feats = self.backbone(x)\n\n        refined = []\n\n        target_size = feats[0].shape[-2:]\n\n        # =================================================\n        # DSC + GCSA refinement\n        # =================================================\n\n        for feat, block in zip(feats, self.refine_blocks):\n\n            feat = block(feat)\n\n            if feat.shape[-2:] != target_size:\n\n                feat = F.interpolate(\n                    feat,\n                    size=target_size,\n                    mode=\"bilinear\",\n                    align_corners=False\n                )\n\n            refined.append(feat)\n\n        # =================================================\n        # Multi-scale fusion\n        # =================================================\n\n        fused = torch.cat(refined, dim=1)\n\n        fused = self.fusion_conv(fused)\n\n        return fused\n\n\n# =========================================================\n# 15.4 Foundation Detector Backbone\n# =========================================================\n\nclass PneumoFoundationBackbone(nn.Module):\n\n    def __init__(self):\n\n        super().__init__()\n\n        self.encoder = SwinFusionBlock(\n            embed_dim=256\n        )\n\n        self.global_pool = nn.AdaptiveAvgPool2d(1)\n\n        # =================================================\n        # Classification Head\n        # =================================================\n\n        self.cls_head = nn.Sequential(\n\n            nn.Linear(256, 128),\n\n            nn.ReLU(inplace=True),\n\n            nn.Dropout(0.2),\n\n            nn.Linear(128, NUM_CLASSES)\n\n        )\n\n    # -----------------------------------------------------\n    # Forward\n    # -----------------------------------------------------\n\n    def forward(self, x):\n\n        # =================================================\n        # Feature Extraction\n        # =================================================\n\n        feat = self.encoder(x)\n\n        pooled = self.global_pool(feat)\n\n        pooled = pooled.flatten(1)\n\n        logits = self.cls_head(pooled)\n\n        return {\n            \"features\": feat,\n            \"logits\": logits\n        }\n\n\n# =========================================================\n# 15.5 Initialize Foundation Model\n# =========================================================\n\nfoundation_model = PneumoFoundationBackbone()\n\nfoundation_model = foundation_model.to(CONFIG[\"device\"])\n\nprint(\"Foundation backbone initialized\")\n\n# =========================================================\n# Expected outputs\n# =========================================================\n#\n# output[\"features\"]\n# -> multi-scale medical feature map\n#\n# output[\"logits\"]\n# -> disease classification logits\n#\n# =========================================================","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:22.560186Z","iopub.execute_input":"2026-05-20T07:51:22.560463Z","iopub.status.idle":"2026-05-20T07:51:37.659762Z","shell.execute_reply.started":"2026-05-20T07:51:22.560439Z","shell.execute_reply":"2026-05-20T07:51:37.659109Z"}},"outputs":[{"name":"stderr","text":"Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.\n","output_type":"stream"},{"output_type":"display_data","data":{"text/plain":"model.safetensors:   0%|          | 0.00/114M [00:00<?, ?B/s]","application/vnd.jupyter.widget-view+json":{"version_major":2,"version_minor":0,"model_id":"0a2c83ed292640888b8690f2de0b9d24"}},"metadata":{}},{"name":"stdout","text":"Foundation backbone initialized\n","output_type":"stream"}],"execution_count":15},{"cell_type":"markdown","source":"# 16. OPTIMIZER + SCHEDULER\n=========================================================","metadata":{}},{"cell_type":"code","source":"\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=CONFIG[\"lr\"],\n    weight_decay=CONFIG[\"weight_decay\"]\n)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n    optimizer,\n    T_max=CONFIG[\"epochs\"]\n)\nscaler = torch.cuda.amp.GradScaler(enabled=CONFIG[\"mixed_precision\"])\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:37.66082Z","iopub.execute_input":"2026-05-20T07:51:37.661148Z","iopub.status.idle":"2026-05-20T07:51:37.666188Z","shell.execute_reply.started":"2026-05-20T07:51:37.661092Z","shell.execute_reply":"2026-05-20T07:51:37.665446Z"}},"outputs":[],"execution_count":16},{"cell_type":"markdown","source":"17. CHECKPOINTING\n=========================================================","metadata":{}},{"cell_type":"code","source":"\nbest_map = 0\n\ndef save_ckpt(model, optimizer, scheduler, epoch, score, path):\n\n    torch.save({\n        \"model\": model.state_dict(),\n        \"optimizer\": optimizer.state_dict(),\n        \"scheduler\": scheduler.state_dict(),\n        \"epoch\": epoch,\n        \"score\": score\n    }, path)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:37.667132Z","iopub.execute_input":"2026-05-20T07:51:37.667865Z","iopub.status.idle":"2026-05-20T07:51:37.686841Z","shell.execute_reply.started":"2026-05-20T07:51:37.667843Z","shell.execute_reply":"2026-05-20T07:51:37.686087Z"}},"outputs":[],"execution_count":17},{"cell_type":"markdown","source":"# 18. TRAIN LOOP\n=========================================================","metadata":{}},{"cell_type":"code","source":"\n\ndef train_one_epoch(model, loader):\n\n    model.train()\n\n    running_loss = 0\n\n    for imgs, targets, ids in loader:\n\n        imgs = [x.to(CONFIG[\"device\"]) for x in imgs]\n\n        targets = [\n            {k:v.to(CONFIG[\"device\"]) for k,v in t.items()}\n            for t in targets\n        ]\n\n        optimizer.zero_grad()\n\n        with torch.cuda.amp.autocast(enabled=CONFIG[\"mixed_precision\"]):\n\n            loss_dict = model(imgs, targets)\n\n            loss = sum(loss_dict.values())\n\n        scaler.scale(loss).backward()\n\n        torch.nn.utils.clip_grad_norm_(\n            model.parameters(),\n            CONFIG[\"grad_clip\"]\n        )\n\n        scaler.step(optimizer)\n        scaler.update()\n\n        running_loss += loss.item()\n\n    return running_loss / len(loader)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:37.687703Z","iopub.execute_input":"2026-05-20T07:51:37.687998Z","iopub.status.idle":"2026-05-20T07:51:37.699783Z","shell.execute_reply.started":"2026-05-20T07:51:37.687977Z","shell.execute_reply":"2026-05-20T07:51:37.698953Z"}},"outputs":[],"execution_count":18},{"cell_type":"markdown","source":"# 19. VALIDATION mAP\n=========================================================","metadata":{}},{"cell_type":"code","source":"\nmetric = MeanAveragePrecision()\n@torch.no_grad()\ndef validate(model, loader):\n\n    model.eval()\n\n    metric.reset()\n\n    for imgs, targets, ids in loader:\n\n        imgs = [x.to(CONFIG[\"device\"]) for x in imgs]\n\n        preds = model(imgs)\n\n        preds_cpu = [\n            {\n                \"boxes\": p[\"boxes\"].cpu(),\n                \"scores\": p[\"scores\"].cpu(),\n                \"labels\": p[\"labels\"].cpu()\n            }\n            for p in preds\n        ]\n\n        targets_cpu = [\n            {\n                \"boxes\": t[\"boxes\"],\n                \"labels\": t[\"labels\"]\n            }\n            for t in targets\n        ]\n\n        metric.update(preds_cpu, targets_cpu)\n\n    res = metric.compute()\n\n    return res\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:37.70087Z","iopub.execute_input":"2026-05-20T07:51:37.701329Z","iopub.status.idle":"2026-05-20T07:51:37.722404Z","shell.execute_reply.started":"2026-05-20T07:51:37.701301Z","shell.execute_reply":"2026-05-20T07:51:37.721801Z"}},"outputs":[],"execution_count":19},{"cell_type":"markdown","source":"# 20. MAIN TRAINING LOOP\n=========================================================","metadata":{}},{"cell_type":"code","source":"\nhistory = []\nfor epoch in range(CONFIG[\"epochs\"]):\n\n    print(f\"\\nEpoch {epoch+1}/{CONFIG['epochs']}\")\n\n    train_loss = train_one_epoch(model, train_loader)\n\n    val_metrics = validate(model, val_loader)\n\n    map50 = val_metrics[\"map_50\"].item()\n\n    scheduler.step()\n\n    print(\"Train Loss:\", train_loss)\n    print(\"mAP50:\", map50)\n\n    history.append({\n        \"epoch\": epoch,\n        \"train_loss\": train_loss,\n        \"map50\": map50\n    })\n\n    pd.DataFrame(history).to_csv(\n        LOG_DIR / \"training_history.csv\",\n        index=False\n    )\n\n    if map50 > best_map:\n\n        best_map = map50\n\n        save_ckpt(\n            model,\n            optimizer,\n            scheduler,\n            epoch,\n            map50,\n            CKPT_DIR / \"best_model.pth\"\n        )\n\n        print(\"Saved best checkpoint\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-20T07:51:37.723136Z","iopub.execute_input":"2026-05-20T07:51:37.723492Z","execution_failed":"2026-05-20T15:25:01.989Z"}},"outputs":[{"name":"stdout","text":"\nEpoch 1/30\nTrain Loss: 0.8128780960799349\nmAP50: 0.11827796697616577\nSaved best checkpoint\n\nEpoch 2/30\nTrain Loss: 0.7435098238230022\nmAP50: 0.1598488986492157\nSaved best checkpoint\n\nEpoch 3/30\nTrain Loss: 0.7163124901917471\nmAP50: 0.17500649392604828\nSaved best checkpoint\n\nEpoch 4/30\nTrain Loss: 0.6981388967700379\nmAP50: 0.1893351525068283\nSaved best checkpoint\n\nEpoch 5/30\nTrain Loss: 0.6794557800268557\nmAP50: 0.21148784458637238\nSaved best checkpoint\n\nEpoch 6/30\nTrain Loss: 0.6575068936718201\nmAP50: 0.2106371372938156\n\nEpoch 7/30\nTrain Loss: 0.6498780483305794\nmAP50: 0.21357432007789612\nSaved best checkpoint\n\nEpoch 8/30\nTrain Loss: 0.6278836965154055\nmAP50: 0.20947350561618805\n\nEpoch 9/30\nTrain Loss: 0.6264285731369861\nmAP50: 0.20867833495140076\n\nEpoch 10/30\nTrain Loss: 0.6340909950833543\nmAP50: 0.21691635251045227\nSaved best checkpoint\n\nEpoch 11/30\n","output_type":"stream"}],"execution_count":null},{"cell_type":"markdown","source":"# 21. VISUALIZATION\n=========================================================","metadata":{}},{"cell_type":"code","source":"\n\ndef draw_boxes(img, boxes):\n\n    canvas = (img * 255).astype(np.uint8)\n\n    canvas = cv2.cvtColor(canvas, cv2.COLOR_GRAY2BGR)\n\n    for b in boxes:\n\n        x1,y1,x2,y2 = map(int, b)\n\n        cv2.rectangle(canvas, (x1,y1), (x2,y2), (0,255,0), 2)\n\n    return canvas\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-05-20T15:25:01.989Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 22. INFERENCE BLOCK\n=========================================================","metadata":{}},{"cell_type":"code","source":"\n@torch.no_grad()\ndef predict_single(model, image_path, score_thr=0.3):\n\n    model.eval()\n\n    img = read_dicom(image_path)\n    img = apply_clahe01(img)\n\n    transformed = val_tfms(\n        image=img,\n        bboxes=[],\n        labels=[]\n    )\n\n    img_t = torch.tensor(transformed[\"image\"]).float()\n\n    img_t = img_t.unsqueeze(0).repeat(3,1,1)\n\n    mean = torch.tensor([0.485,0.456,0.406]).view(3,1,1)\n    std = torch.tensor([0.229,0.224,0.225]).view(3,1,1)\n\n    img_t = (img_t - mean) / std\n\n    pred = model([img_t.to(CONFIG[\"device\"])])[0]\n\n    keep = pred[\"scores\"].cpu().numpy() > score_thr\n\n    boxes = pred[\"boxes\"].cpu().numpy()[keep]\n\n    return img, boxes\n\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-05-20T15:25:01.99Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Visualize prediction ","metadata":{}},{"cell_type":"code","source":"# =========================================================\n# Visualize FasterRCNN predictions\n# =========================================================\n\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\n\nmodel.eval()\n\n# ---------------------------------------------------------\n# class mapping\n# ---------------------------------------------------------\n\nidx2name = {\n    v: str(k)\n    for k, v in id2cont.items()\n}\n\n# ---------------------------------------------------------\n# prediction function\n# ---------------------------------------------------------\n\ndef predict_one(model, img_tensor, score_thr=0.3):\n\n    with torch.no_grad():\n\n        outputs = model([\n            img_tensor.to(CONFIG[\"device\"])\n        ])\n\n    pred = outputs[0]\n\n    boxes = pred[\"boxes\"].detach().cpu().numpy()\n    scores = pred[\"scores\"].detach().cpu().numpy()\n    labels = pred[\"labels\"].detach().cpu().numpy()\n\n    keep = scores >= score_thr\n\n    return (\n        boxes[keep],\n        scores[keep],\n        labels[keep]\n    )\n\n# ---------------------------------------------------------\n# visualization\n# ---------------------------------------------------------\n\ndef show_prediction(img_tensor, boxes, scores, labels):\n\n    img = img_tensor[0].cpu().numpy()\n\n    fig, ax = plt.subplots(1, figsize=(10,10))\n\n    ax.imshow(img, cmap=\"gray\")\n\n    for box, score, label in zip(boxes, scores, labels):\n\n        x1, y1, x2, y2 = box\n\n        rect = patches.Rectangle(\n            (x1, y1),\n            x2 - x1,\n            y2 - y1,\n            linewidth=2,\n            edgecolor='r',\n            facecolor='none'\n        )\n\n        ax.add_patch(rect)\n\n        cls_name = idx2name.get(label, str(label))\n\n        ax.text(\n            x1,\n            y1 - 5,\n            f\"{cls_name}: {score:.2f}\",\n            color='yellow',\n            fontsize=10,\n            bbox=dict(facecolor='black', alpha=0.5)\n        )\n\n    plt.axis(\"off\")\n    plt.show()\n\n# =========================================================\n# run on some validation images\n# =========================================================\n\nnum_samples = 5\n\nfor i in range(num_samples):\n\n    img_tensor, target, image_id = val_ds[i]\n\n    boxes, scores, labels = predict_one(\n        model,\n        img_tensor,\n        score_thr=0.3\n    )\n\n    print(\"=\"*80)\n    print(\"IMAGE:\", image_id)\n    print(\"NUM PREDICTIONS:\", len(boxes))\n\n    show_prediction(\n        img_tensor,\n        boxes,\n        scores,\n        labels\n    )","metadata":{"trusted":true,"execution":{"execution_failed":"2026-05-20T15:25:01.99Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# Compare predictions vs ground truth\n# =========================================================\n\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\n\nmodel.eval()\n\n# ---------------------------------------------------------\n# label mapping\n# ---------------------------------------------------------\n\nidx2name = {\n    v: str(k)\n    for k, v in id2cont.items()\n}\n\n# ---------------------------------------------------------\n# prediction\n# ---------------------------------------------------------\n\ndef predict_one(model, img_tensor, score_thr=0.3):\n\n    with torch.no_grad():\n\n        outputs = model([\n            img_tensor.to(CONFIG[\"device\"])\n        ])\n\n    pred = outputs[0]\n\n    boxes = pred[\"boxes\"].detach().cpu().numpy()\n    scores = pred[\"scores\"].detach().cpu().numpy()\n    labels = pred[\"labels\"].detach().cpu().numpy()\n\n    keep = scores >= score_thr\n\n    return (\n        boxes[keep],\n        scores[keep],\n        labels[keep]\n    )\n\n# ---------------------------------------------------------\n# visualization\n# GT  : green\n# Pred: red\n# ---------------------------------------------------------\n\ndef show_gt_vs_pred(\n    img_tensor,\n    gt_boxes,\n    gt_labels,\n    pred_boxes,\n    pred_scores,\n    pred_labels\n):\n\n    img = img_tensor[0].cpu().numpy()\n\n    fig, ax = plt.subplots(1, figsize=(12,12))\n\n    ax.imshow(img, cmap=\"gray\")\n\n    # -----------------------------------------------------\n    # Ground truth\n    # -----------------------------------------------------\n\n    for box, label in zip(gt_boxes, gt_labels):\n\n        x1, y1, x2, y2 = box\n\n        rect = patches.Rectangle(\n            (x1, y1),\n            x2 - x1,\n            y2 - y1,\n            linewidth=2,\n            edgecolor='green',\n            facecolor='none'\n        )\n\n        ax.add_patch(rect)\n\n        cls_name = idx2name.get(int(label), str(label))\n\n        ax.text(\n            x1,\n            y1 - 5,\n            f\"GT: {cls_name}\",\n            color='lime',\n            fontsize=10,\n            bbox=dict(facecolor='black', alpha=0.5)\n        )\n\n    # -----------------------------------------------------\n    # Predictions\n    # -----------------------------------------------------\n\n    for box, score, label in zip(\n        pred_boxes,\n        pred_scores,\n        pred_labels\n    ):\n\n        x1, y1, x2, y2 = box\n\n        rect = patches.Rectangle(\n            (x1, y1),\n            x2 - x1,\n            y2 - y1,\n            linewidth=2,\n            edgecolor='red',\n            facecolor='none'\n        )\n\n        ax.add_patch(rect)\n\n        cls_name = idx2name.get(int(label), str(label))\n\n        ax.text(\n            x1,\n            y2 + 10,\n            f\"PRED: {cls_name} ({score:.2f})\",\n            color='yellow',\n            fontsize=10,\n            bbox=dict(facecolor='black', alpha=0.5)\n        )\n\n    plt.axis(\"off\")\n    plt.show()\n\n# =========================================================\n# visualize samples\n# =========================================================\n\nnum_samples = 5\n\nfor i in range(num_samples):\n\n    img_tensor, target, image_id = val_ds[i]\n\n    pred_boxes, pred_scores, pred_labels = predict_one(\n        model,\n        img_tensor,\n        score_thr=0.3\n    )\n\n    gt_boxes = target[\"boxes\"].cpu().numpy()\n    gt_labels = target[\"labels\"].cpu().numpy()\n\n    print(\"=\"*80)\n    print(\"IMAGE:\", image_id)\n    print(\"GT BOXES:\", len(gt_boxes))\n    print(\"PRED BOXES:\", len(pred_boxes))\n\n    show_gt_vs_pred(\n        img_tensor,\n        gt_boxes,\n        gt_labels,\n        pred_boxes,\n        pred_scores,\n        pred_labels\n    )","metadata":{"trusted":true,"execution":{"execution_failed":"2026-05-20T15:25:01.99Z"}},"outputs":[],"execution_count":null}]}