{"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":20270,"databundleVersionId":1222630},{"sourceType":"datasetVersion","sourceId":1353811,"datasetId":762203,"databundleVersionId":1386220}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"### Confirming paths","metadata":{}},{"cell_type":"code","source":"import os\n\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    print(dirname)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-06T11:13:31.930513Z","iopub.execute_input":"2026-05-06T11:13:31.930806Z","iopub.status.idle":"2026-05-06T11:16:50.383772Z","shell.execute_reply.started":"2026-05-06T11:13:31.930783Z","shell.execute_reply":"2026-05-06T11:16:50.383157Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport timm\n\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm import tqdm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:20.171379Z","iopub.execute_input":"2026-05-06T15:10:20.171650Z","iopub.status.idle":"2026-05-06T15:10:34.565852Z","shell.execute_reply.started":"2026-05-06T15:10:20.171627Z","shell.execute_reply":"2026-05-06T15:10:34.564983Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Load 2020 dataset","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\ndf_2020 = pd.read_csv('/kaggle/input/competitions/siim-isic-melanoma-classification/train.csv')\ndf_2020.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:34.567148Z","iopub.execute_input":"2026-05-06T15:10:34.567669Z","iopub.status.idle":"2026-05-06T15:10:34.670786Z","shell.execute_reply.started":"2026-05-06T15:10:34.567644Z","shell.execute_reply":"2026-05-06T15:10:34.669969Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Attach image paths","metadata":{}},{"cell_type":"code","source":"import os\n\nimg_dir_2020 = \"/kaggle/input/competitions/siim-isic-melanoma-classification/jpeg/train\"\n\ndf_2020['image_path'] = df_2020['image_name'].apply(\n    lambda x: os.path.join(img_dir_2020, x + \".jpg\")\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:34.671882Z","iopub.execute_input":"2026-05-06T15:10:34.672208Z","iopub.status.idle":"2026-05-06T15:10:34.711853Z","shell.execute_reply.started":"2026-05-06T15:10:34.672172Z","shell.execute_reply":"2026-05-06T15:10:34.711272Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Creating clean working subset","metadata":{}},{"cell_type":"code","source":"df = df_2020.copy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:34.713388Z","iopub.execute_input":"2026-05-06T15:10:34.713739Z","iopub.status.idle":"2026-05-06T15:10:34.723610Z","shell.execute_reply.started":"2026-05-06T15:10:34.713714Z","shell.execute_reply":"2026-05-06T15:10:34.722882Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df.head(5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:34.724569Z","iopub.execute_input":"2026-05-06T15:10:34.724839Z","iopub.status.idle":"2026-05-06T15:10:34.735970Z","shell.execute_reply.started":"2026-05-06T15:10:34.724806Z","shell.execute_reply":"2026-05-06T15:10:34.735048Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Checking for missing values","metadata":{}},{"cell_type":"code","source":"print(\"Missing value counts for Age: \")\ndf['age_approx'].isnull().value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:34.737546Z","iopub.execute_input":"2026-05-06T15:10:34.738336Z","iopub.status.idle":"2026-05-06T15:10:34.756562Z","shell.execute_reply.started":"2026-05-06T15:10:34.738292Z","shell.execute_reply":"2026-05-06T15:10:34.755973Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Missing value counts for sex: \")\ndf['sex'].isnull().value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:34.757813Z","iopub.execute_input":"2026-05-06T15:10:34.758002Z","iopub.status.idle":"2026-05-06T15:10:34.765406Z","shell.execute_reply.started":"2026-05-06T15:10:34.757983Z","shell.execute_reply":"2026-05-06T15:10:34.764700Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Missing value counts for Anatom site: \")\ndf['anatom_site_general_challenge'].isnull().value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:38.516851Z","iopub.execute_input":"2026-05-06T15:10:38.517617Z","iopub.status.idle":"2026-05-06T15:10:38.526357Z","shell.execute_reply.started":"2026-05-06T15:10:38.517587Z","shell.execute_reply":"2026-05-06T15:10:38.525478Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Value Imputation and checking if there is missing or not","metadata":{}},{"cell_type":"code","source":"df['age_approx'] = df['age_approx'].fillna(df['age_approx'].median())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:40.589869Z","iopub.execute_input":"2026-05-06T15:10:40.590680Z","iopub.status.idle":"2026-05-06T15:10:40.595125Z","shell.execute_reply.started":"2026-05-06T15:10:40.590647Z","shell.execute_reply":"2026-05-06T15:10:40.594547Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df['age_approx'].isnull().value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:42.529922Z","iopub.execute_input":"2026-05-06T15:10:42.530560Z","iopub.status.idle":"2026-05-06T15:10:42.536690Z","shell.execute_reply.started":"2026-05-06T15:10:42.530529Z","shell.execute_reply":"2026-05-06T15:10:42.536079Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df['sex'] = df['sex'].map({'male': 1, 'female': 0})\ndf['sex'] = df['sex'].fillna(-1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:43.327140Z","iopub.execute_input":"2026-05-06T15:10:43.327939Z","iopub.status.idle":"2026-05-06T15:10:43.337579Z","shell.execute_reply.started":"2026-05-06T15:10:43.327906Z","shell.execute_reply":"2026-05-06T15:10:43.336859Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df['sex'].isnull().value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:44.234730Z","iopub.execute_input":"2026-05-06T15:10:44.235176Z","iopub.status.idle":"2026-05-06T15:10:44.242171Z","shell.execute_reply.started":"2026-05-06T15:10:44.235146Z","shell.execute_reply":"2026-05-06T15:10:44.241407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df['anatom_site_general_challenge'] = df['anatom_site_general_challenge'].fillna('unknown')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:46.176483Z","iopub.execute_input":"2026-05-06T15:10:46.177339Z","iopub.status.idle":"2026-05-06T15:10:46.184652Z","shell.execute_reply.started":"2026-05-06T15:10:46.177308Z","shell.execute_reply":"2026-05-06T15:10:46.183813Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df['anatom_site_general_challenge'].isnull().value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:47.265418Z","iopub.execute_input":"2026-05-06T15:10:47.266327Z","iopub.status.idle":"2026-05-06T15:10:47.275548Z","shell.execute_reply.started":"2026-05-06T15:10:47.266281Z","shell.execute_reply":"2026-05-06T15:10:47.274750Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Encoding Anatomical site**","metadata":{}},{"cell_type":"code","source":"from sklearn.preprocessing import LabelEncoder\n\nle = LabelEncoder()\ndf['site_encoded'] = le.fit_transform(df['anatom_site_general_challenge'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:49.907820Z","iopub.execute_input":"2026-05-06T15:10:49.908256Z","iopub.status.idle":"2026-05-06T15:10:49.917591Z","shell.execute_reply.started":"2026-05-06T15:10:49.908210Z","shell.execute_reply":"2026-05-06T15:10:49.916863Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(df[['image_path', 'age_approx', 'sex','site_encoded', 'target']].head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:50.689745Z","iopub.execute_input":"2026-05-06T15:10:50.690530Z","iopub.status.idle":"2026-05-06T15:10:50.702172Z","shell.execute_reply.started":"2026-05-06T15:10:50.690502Z","shell.execute_reply":"2026-05-06T15:10:50.701306Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Stratified Split","metadata":{}},{"cell_type":"code","source":"df['fold'] = -1\n\nskf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n\nfor fold, (train_idx, val_idx) in enumerate(skf.split(df, df['target'])):\n    df.loc[val_idx, 'fold'] = fold\n\ntrain_df = df[df.fold != 0].reset_index(drop=True)\nval_df   = df[df.fold == 0].reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:10:54.518863Z","iopub.execute_input":"2026-05-06T15:10:54.519819Z","iopub.status.idle":"2026-05-06T15:10:54.556262Z","shell.execute_reply.started":"2026-05-06T15:10:54.519778Z","shell.execute_reply":"2026-05-06T15:10:54.555388Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Normalize Metadata","metadata":{}},{"cell_type":"code","source":"mean = train_df['age_approx'].mean()\nstd  = train_df['age_approx'].std()\n\ntrain_df['age_approx'] = (train_df['age_approx'] - mean) / std\nval_df['age_approx']   = (val_df['age_approx'] - mean) / std","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:12:23.392965Z","iopub.execute_input":"2026-05-06T15:12:23.393316Z","iopub.status.idle":"2026-05-06T15:12:23.402036Z","shell.execute_reply.started":"2026-05-06T15:12:23.393288Z","shell.execute_reply":"2026-05-06T15:12:23.400860Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Transforms\n**Albumentations** (standard in ISIC work):","metadata":{}},{"cell_type":"code","source":"def get_transforms():\n    return A.Compose([\n        A.Resize(512, 512),\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.Normalize(),\n        ToTensorV2(),\n    ])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:12:25.406780Z","iopub.execute_input":"2026-05-06T15:12:25.407437Z","iopub.status.idle":"2026-05-06T15:12:25.411608Z","shell.execute_reply.started":"2026-05-06T15:12:25.407405Z","shell.execute_reply":"2026-05-06T15:12:25.410752Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Dataset","metadata":{}},{"cell_type":"code","source":"class ISICDataset(Dataset):\n    def __init__(self, df, transforms=None):\n        self.df = df.reset_index(drop=True)\n        self.transforms = transforms\n        self.meta_features = ['age_approx', 'sex', 'site_encoded']\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.loc[idx]\n\n        image = cv2.imread(row.image_path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n\n        if self.transforms:\n            image = self.transforms(image=image)['image']\n\n        meta = torch.tensor(row[self.meta_features].values.astype(np.float32))\n        target = torch.tensor(row.target).float()\n\n        return image, meta, target","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:12:27.812419Z","iopub.execute_input":"2026-05-06T15:12:27.813210Z","iopub.status.idle":"2026-05-06T15:12:27.818790Z","shell.execute_reply.started":"2026-05-06T15:12:27.813180Z","shell.execute_reply":"2026-05-06T15:12:27.818034Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Base Model","metadata":{}},{"cell_type":"code","source":"class FusionModel(nn.Module):\n    def __init__(self, meta_features):\n        super().__init__()\n\n        self.backbone = timm.create_model('efficientnet_b0', pretrained=True, num_classes=0)\n        self.meta = nn.Sequential(\n            nn.Linear(meta_features, 128),\n            nn.ReLU(),\n            nn.BatchNorm1d(128),\n            nn.Dropout(0.3),\n        )\n\n        self.classifier = nn.Sequential(\n            nn.Linear(self.backbone.num_features + 128, 256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, 1)\n        )\n\n    def forward(self, image, meta):\n        img_feat = self.backbone(image)\n        meta_feat = self.meta(meta)\n\n        x = torch.cat([img_feat, meta_feat], dim=1)\n        return self.classifier(x).squeeze(1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:12:31.053529Z","iopub.execute_input":"2026-05-06T15:12:31.054217Z","iopub.status.idle":"2026-05-06T15:12:31.059937Z","shell.execute_reply.started":"2026-05-06T15:12:31.054183Z","shell.execute_reply":"2026-05-06T15:12:31.059064Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Loader","metadata":{}},{"cell_type":"code","source":"train_dataset = ISICDataset(train_df, transforms=get_transforms())\nval_dataset   = ISICDataset(val_df, transforms=get_transforms())\n\ntrain_loader = DataLoader(train_dataset, batch_size=16, shuffle=True, num_workers=2)\nval_loader   = DataLoader(val_dataset, batch_size=16, shuffle=False, num_workers=2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:12:33.113230Z","iopub.execute_input":"2026-05-06T15:12:33.114130Z","iopub.status.idle":"2026-05-06T15:12:33.126656Z","shell.execute_reply.started":"2026-05-06T15:12:33.114099Z","shell.execute_reply":"2026-05-06T15:12:33.125991Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nmodel = FusionModel(meta_features=3).to(device)\n\npos = train_df['target'].sum()\nneg = len(train_df) - pos\n\npos_weight = torch.tensor([neg / pos]).to(device)\n\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:12:34.136608Z","iopub.execute_input":"2026-05-06T15:12:34.136893Z","iopub.status.idle":"2026-05-06T15:12:36.338712Z","shell.execute_reply.started":"2026-05-06T15:12:34.136868Z","shell.execute_reply":"2026-05-06T15:12:36.338065Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training and Validation","metadata":{}},{"cell_type":"code","source":"def train_one_epoch(model, loader):\n    model.train()\n    total_loss = 0\n\n    for images, meta, targets in tqdm(loader):\n        images, meta, targets = images.to(device), meta.to(device), targets.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(images, meta)\n        loss = criterion(outputs, targets)\n\n        loss.backward()\n        optimizer.step()\n\n        total_loss += loss.item()\n\n    return total_loss / len(loader)\n\n\nfrom sklearn.metrics import roc_auc_score, roc_curve\nimport numpy as np\n\ndef validate(model, loader):\n    model.eval()\n    preds, targets_list = [], []\n\n    with torch.no_grad():\n        for images, meta, targets in loader:\n            images, meta = images.to(device), meta.to(device)\n\n            outputs = model(images, meta)\n\n            preds.extend(torch.sigmoid(outputs).cpu().numpy().ravel())\n            targets_list.extend(targets.cpu().numpy().ravel())\n\n    preds = np.array(preds)\n    targets_list = np.array(targets_list)\n\n    auc = roc_auc_score(targets_list, preds)\n    fpr, tpr, _ = roc_curve(targets_list, preds)\n\n    return auc, fpr, tpr","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:12:37.253617Z","iopub.execute_input":"2026-05-06T15:12:37.253892Z","iopub.status.idle":"2026-05-06T15:12:37.260971Z","shell.execute_reply.started":"2026-05-06T15:12:37.253870Z","shell.execute_reply":"2026-05-06T15:12:37.260170Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nroc_data = []\n\nfor epoch in range(5):\n    train_loss = train_one_epoch(model, train_loader)\n    val_auc, fpr, tpr = validate(model, val_loader)\n\n    roc_data.append((fpr, tpr, val_auc))\n\n    print(f\"Epoch {epoch}: Loss={train_loss:.4f}, AUC={val_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T15:12:40.547882Z","iopub.execute_input":"2026-05-06T15:12:40.548590Z","iopub.status.idle":"2026-05-06T17:59:11.824861Z","shell.execute_reply.started":"2026-05-06T15:12:40.548559Z","shell.execute_reply":"2026-05-06T17:59:11.824019Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(7, 7))\n\nfor i, (fpr, tpr, auc) in enumerate(roc_data):\n    plt.plot(fpr, tpr, label=f\"Epoch {i} (AUC = {auc:.4f})\")\n\n# random baseline\nplt.plot([0, 1], [0, 1], linestyle=\"--\", color=\"gray\")\n\nplt.xlabel(\"False Positive Rate\")\nplt.ylabel(\"True Positive Rate\")\nplt.title(\"ROC Curve Across Epochs\")\nplt.legend()\nplt.grid()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-06T17:59:11.826665Z","iopub.execute_input":"2026-05-06T17:59:11.826993Z","iopub.status.idle":"2026-05-06T17:59:12.083348Z","shell.execute_reply.started":"2026-05-06T17:59:11.826962Z","shell.execute_reply":"2026-05-06T17:59:12.082659Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}