{"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":"none","dataSources":[{"sourceId":20270,"databundleVersionId":1222630,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport pandas as pd\nfrom PIL import Image\nfrom torchvision import transforms\nfrom tqdm.notebook import tqdm\nimport shutil\nimport numpy as np\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\nfrom torchvision import transforms, models\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, OneHotEncoder\nfrom sklearn.compose import ColumnTransformer\nfrom sklearn.metrics import roc_auc_score, accuracy_score, f1_score, confusion_matrix, classification_report\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm.notebook import tqdm\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 1. Configuration and Setup ---\nDATA_PATH = '/kaggle/input/siim-isic-melanoma-classification/'\nIMAGE_PATH = os.path.join(DATA_PATH, 'jpeg/train')\nCSV_PATH = os.path.join(DATA_PATH, 'train.csv')\n\n# New directory for our augmented dataset\nAUG_DATA_PATH = '/kaggle/working/original_augmented/'\nAUG_IMAGE_PATH = os.path.join(AUG_DATA_PATH, 'jpeg')\nAUG_CSV_PATH = os.path.join(AUG_DATA_PATH, 'augmented_train.csv')\n\n# Create the directories, removing any old ones first\nif os.path.exists(AUG_DATA_PATH):\n    shutil.rmtree(AUG_DATA_PATH)\nos.makedirs(AUG_IMAGE_PATH, exist_ok=True)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-05T15:59:35.404402Z","iopub.execute_input":"2025-09-05T15:59:35.404744Z","iopub.status.idle":"2025-09-05T15:59:35.410518Z","shell.execute_reply.started":"2025-09-05T15:59:35.404724Z","shell.execute_reply":"2025-09-05T15:59:35.409503Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 2. Data Loading and Balancing ---\nprint(\"Loading and creating a balanced dataset for augmentation...\")\ndf = pd.read_csv(CSV_PATH)\n#df.dropna(reset_index=True,inplace=True)\ndf.dropna(inplace=True)\ndf.reset_index(drop=True, inplace=True)\ndf","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-05T16:01:00.250066Z","iopub.execute_input":"2025-09-05T16:01:00.250679Z","iopub.status.idle":"2025-09-05T16:01:00.320422Z","shell.execute_reply.started":"2025-09-05T16:01:00.250651Z","shell.execute_reply":"2025-09-05T16:01:00.319424Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"malignant_df = df[df['target'] == 1].copy()\nbenign_df = df[df['target'] == 0].copy()\n\n# Create initial balanced set\nn_malignant = len(malignant_df)\nn_sites = benign_df['anatom_site_general_challenge'].nunique()\nn_per_site = n_malignant // n_sites\nremainder = n_malignant % n_sites\n\nbenign_list = []\nfor site, group in benign_df.groupby('anatom_site_general_challenge'):\n    sample_n = n_per_site + 1 if remainder > 0 else n_per_site\n    benign_list.append(group.sample(n=sample_n, random_state=42))\n    remainder -= 1\nsampled_benign_df = pd.concat(benign_list)\n\nbalanced_df = pd.concat([malignant_df, sampled_benign_df], ignore_index=True)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-02T04:41:29.931323Z","iopub.execute_input":"2025-09-02T04:41:29.931539Z","execution_failed":"2025-09-02T04:41:59.419Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 3. Augmentation Process ---\nprint(f\"Augmenting {len(balanced_df)} images to triple the dataset size...\")\naugmented_data = []\nhflip = transforms.RandomHorizontalFlip(p=1.0)\ncolor_jitter = transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1)\n\nfor idx, row in tqdm(balanced_df.iterrows(), total=len(balanced_df)):\n    img_name = row['image_name']\n    original_img_path = os.path.join(IMAGE_PATH, f\"{img_name}.jpg\")\n    image = Image.open(original_img_path)\n\n    # 1. Original Image, 2. Flipped Image, 3. Color Jittered Image\n    images_to_save = [(image, \"orig\"), (hflip(image), \"flipped\"), (color_jitter(image), \"jittered\")]\n    for img, suffix in images_to_save:\n        new_name = f\"{img_name}_{suffix}.jpg\"\n        img.save(os.path.join(AUG_IMAGE_PATH, new_name))\n        new_row = row.copy()\n        new_row['image_name'] = new_name\n        augmented_data.append(new_row)\n\naugmented_df = pd.DataFrame(augmented_data)\naugmented_df.to_csv(AUG_CSV_PATH, index=False)\nprint(f\"Augmentation complete. New dataset size: {len(augmented_df)} images.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-02T04:41:29.931323Z","iopub.execute_input":"2025-09-02T04:41:29.931539Z","execution_failed":"2025-09-02T04:41:59.419Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 4. Show File Structure and CSV Preview ---\nprint(\"\\n--- Generated File Structure ---\")\nprint(f\"Data saved in: {AUG_DATA_PATH}\")\nprint(\"Directory contents:\")\nfor dirname, _, filenames in os.walk(AUG_DATA_PATH):\n    for filename in filenames[:5]: # Show first 5 files as an example\n        print(os.path.join(dirname, filename))\n    if len(filenames) > 5:\n        print(\"...\")\n\nprint(\"\\n--- Preview of augmented_train.csv ---\")\npreview_df = pd.read_csv(AUG_CSV_PATH)\nprint(preview_df.head())\nprint(\"------------------------------------\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-02T04:41:29.931323Z","iopub.execute_input":"2025-09-02T04:41:29.931539Z","execution_failed":"2025-09-02T04:41:59.419Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# --- 1. Configuration and Setup ---\n# This script assumes the augmented data has already been created.\n# Paths now point to the augmented dataset directory.\nAUG_DATA_PATH = '/kaggle/working/original_augmented/'\nAUG_IMAGE_PATH = os.path.join(AUG_DATA_PATH, 'jpeg')\nAUG_CSV_PATH = os.path.join(AUG_DATA_PATH, 'augmented_train.csv')\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Using device: {DEVICE}\")\n\n# Model & Training parameters\nIMG_SIZE = 224\nBATCH_SIZE = 32\nEPOCHS_HEAD = 5\nEPOCHS_FULL = 50\nLR_HEAD = 1e-3\nLR_FULL = 1e-5\nPATIENCE = 5\nMODEL_SAVE_PATH = 'best_efficientnet_model.pth'\n\n# --- 2. Load Augmented Data and Split (80/10/10) ---\nprint(\"Loading augmented dataset...\")\naugmented_df = pd.read_csv(AUG_CSV_PATH)\n\n# Preprocessing\naugmented_df['sex'] = pd.to_numeric(augmented_df['sex'].map({'male': 0, 'female': 1}))\nnumerical_features = ['age_approx', 'sex']\ncategorical_features = ['anatom_site_general_challenge']\n\npreprocessor = ColumnTransformer(\n    transformers=[\n        ('num', StandardScaler(), numerical_features),\n        ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features)\n    ],\n    remainder='drop'\n)\n\n# First split: 80% train, 20% temp (for validation and test)\ntrain_df, temp_df = train_test_split(augmented_df, test_size=0.2, random_state=42, stratify=augmented_df['target'])\n# Second split: split the 20% temp into 10% validation and 10% test\nval_df, test_df = train_test_split(temp_df, test_size=0.5, random_state=42, stratify=temp_df['target'])\n\npreprocessor.fit(train_df[numerical_features + categorical_features])\n\nprint(\"\\n--- Data Split Summary ---\")\nprint(f\"Training set size:   {len(train_df)} samples\")\nprint(f\"Validation set size: {len(val_df)} samples\")\nprint(f\"Test set size:       {len(test_df)} samples\")\nprint(\"--------------------------\")\n\n# --- 3. Dataset and DataLoader Classes ---\nclass MelanomaDataset(Dataset):\n    def __init__(self, df, tabular_preprocessor, image_dir, transform=None):\n        self.df = df\n        self.tabular_preprocessor = tabular_preprocessor\n        self.image_dir = image_dir\n        self.transform = transform\n        self.tabular_data = self.tabular_preprocessor.transform(self.df[numerical_features + categorical_features])\n    def __len__(self): return len(self.df)\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = os.path.join(self.image_dir, row['image_name'])\n        image = Image.open(img_path).convert('RGB')\n        if self.transform: image = self.transform(image)\n        tabular = torch.tensor(self.tabular_data[idx], dtype=torch.float)\n        label = torch.tensor(row['target'], dtype=torch.float)\n        return image, tabular, label\n\n# --- 4. EfficientNet-B0 Multimodal Model ---\nclass MultimodalEfficientNet(nn.Module):\n    def __init__(self, num_tabular_features):\n        super(MultimodalEfficientNet, self).__init__()\n        self.image_branch = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT)\n        num_image_features = self.image_branch.classifier[1].in_features\n        self.image_branch.classifier = nn.Sequential(\n            nn.Dropout(p=0.2, inplace=True),\n            nn.Linear(num_image_features, 128)\n        )\n        self.tabular_branch = nn.Sequential(nn.Linear(num_tabular_features, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 32), nn.ReLU(), nn.Dropout(0.3))\n        self.fusion = nn.Linear(128 + 32, 64)\n        self.classifier = nn.Sequential(nn.ReLU(), nn.Dropout(0.5), nn.Linear(64, 1))\n    def forward(self, image, tabular):\n        image_features = self.image_branch(image)\n        tabular_features = self.tabular_branch(tabular)\n        combined_features = torch.cat([image_features, tabular_features], dim=1)\n        fused = self.fusion(combined_features)\n        output = self.classifier(fused)\n        return output\n\n# --- 5. Training and Evaluation Functions ---\nclass EarlyStopping:\n    def __init__(self, patience=5, verbose=False, path='best_model.pth'):\n        self.patience, self.verbose, self.path = patience, verbose, path\n        self.counter, self.best_score, self.early_stop, self.val_loss_min = 0, None, False, np.Inf\n    def __call__(self, val_loss, model):\n        score = -val_loss\n        if self.best_score is None:\n            self.best_score = score\n            self.save_checkpoint(val_loss, model)\n        elif score < self.best_score:\n            self.counter += 1\n            if self.verbose: print(f'EarlyStopping counter: {self.counter}/{self.patience}')\n            if self.counter >= self.patience: self.early_stop = True\n        else:\n            self.best_score = score\n            self.save_checkpoint(val_loss, model)\n            self.counter = 0\n    def save_checkpoint(self, val_loss, model):\n        if self.verbose: print(f'Validation loss decreased ({self.val_loss_min:.4f} --> {val_loss:.4f}). Saving model...')\n        torch.save(model.state_dict(), self.path)\n        self.val_loss_min = val_loss\n\ndef train_model(model, criterion, optimizer, scheduler, train_loader, val_loader, max_epochs, patience):\n    history = {'train_loss': [], 'val_loss': [], 'train_acc': [], 'val_acc': []}\n    early_stopper = EarlyStopping(patience=patience, verbose=True, path=MODEL_SAVE_PATH)\n    for epoch in range(max_epochs):\n        model.train()\n        train_loss, train_corrects = 0.0, 0\n        train_loop = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{max_epochs} [Train]\", leave=False)\n        for images, tabular, labels in train_loop:\n            images, tabular, labels = images.to(DEVICE), tabular.to(DEVICE), labels.to(DEVICE).unsqueeze(1)\n            optimizer.zero_grad()\n            outputs = model(images, tabular)\n            loss = criterion(outputs, labels)\n            loss.backward()\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n            optimizer.step()\n            train_loss += loss.item() * images.size(0)\n            preds = (torch.sigmoid(outputs) > 0.5).float()\n            train_corrects += torch.sum(preds == labels.data)\n            train_loop.set_postfix(loss=loss.item())\n        model.eval()\n        val_loss, val_corrects = 0.0, 0\n        with torch.no_grad():\n            for images, tabular, labels in val_loader:\n                images, tabular, labels_dev = images.to(DEVICE), tabular.to(DEVICE), labels.to(DEVICE).unsqueeze(1)\n                outputs = model(images, tabular)\n                loss = criterion(outputs, labels_dev)\n                val_loss += loss.item() * images.size(0)\n                preds = (torch.sigmoid(outputs) > 0.5).float()\n                val_corrects += torch.sum(preds == labels_dev.data)\n        epoch_train_loss = train_loss / len(train_loader.dataset)\n        epoch_val_loss = val_loss / len(val_loader.dataset)\n        epoch_train_acc = train_corrects.double() / len(train_loader.dataset)\n        epoch_val_acc = val_corrects.double() / len(val_loader.dataset)\n        history['train_loss'].append(epoch_train_loss); history['val_loss'].append(epoch_val_loss)\n        history['train_acc'].append(epoch_train_acc.item()); history['val_acc'].append(epoch_val_acc.item())\n        current_lr = optimizer.param_groups[0]['lr']\n        print(f\"Epoch {epoch+1}/{max_epochs} | Train Loss: {epoch_train_loss:.4f} | Val Loss: {epoch_val_loss:.4f} | Train Acc: {epoch_train_acc:.4f} | Val Acc: {epoch_val_acc:.4f} | LR: {current_lr:.6f}\")\n        scheduler.step(epoch_val_loss)\n        early_stopper(epoch_val_loss, model)\n        if early_stopper.early_stop:\n            print(\"Early stopping triggered\")\n            break\n    print(\"\\nFinished Training.\")\n    return history\n\n# --- 6. Main Execution ---\n# DataLoaders with more advanced augmentation\ntrain_transform = transforms.Compose([\n    transforms.RandomResizedCrop(IMG_SIZE, scale=(0.8, 1.0)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.1),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    transforms.RandomErasing(p=0.5, scale=(0.02, 0.2))\n])\nval_transform = transforms.Compose([transforms.Resize((IMG_SIZE, IMG_SIZE)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])\ntrain_dataset = MelanomaDataset(train_df, preprocessor, AUG_IMAGE_PATH, transform=train_transform)\nval_dataset = MelanomaDataset(val_df, preprocessor, AUG_IMAGE_PATH, transform=val_transform)\ntest_dataset = MelanomaDataset(test_df, preprocessor, AUG_IMAGE_PATH, transform=val_transform)\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\n# Instantiate Model\nnum_tab_features = preprocessor.transform(train_df.head(1)[numerical_features + categorical_features]).shape[1]\nmodel = MultimodalEfficientNet(num_tab_features).to(DEVICE)\n\n# --- Two-Phase Training ---\n# Phase 1: Train the head\nfor param in model.image_branch.parameters(): param.requires_grad = False\nfor param in model.image_branch.classifier.parameters(): param.requires_grad = True\noptimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=LR_HEAD)\ncriterion = nn.BCEWithLogitsLoss()\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS_HEAD)\nprint(\"--- Phase 1: Training Classifier Head ---\")\nhistory_head = train_model(model, criterion, optimizer, scheduler, train_loader, val_loader, max_epochs=EPOCHS_HEAD, patience=PATIENCE)\n\n# Phase 2: Fine-tune the full model\nfor param in model.parameters(): param.requires_grad = True\noptimizer = optim.Adam(model.parameters(), lr=LR_FULL)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS_FULL)\nprint(\"\\n--- Phase 2: Fine-Tuning Full Model ---\")\nhistory_full = train_model(model, criterion, optimizer, scheduler, train_loader, val_loader, max_epochs=EPOCHS_FULL, patience=PATIENCE)\n\n# --- 7. Final Evaluation on the HOLD-OUT TEST SET ---\nprint(\"\\n--- Loading best model for final TEST evaluation ---\")\nmodel.load_state_dict(torch.load(MODEL_SAVE_PATH))\n\nmodel.eval()\ntest_preds, test_labels, test_probs = [], [], []\nwith torch.no_grad():\n    for images, tabular, labels in tqdm(test_loader, desc=\"Testing\"):\n        images, tabular = images.to(DEVICE), tabular.to(DEVICE)\n        outputs = model(images, tabular)\n        probs = torch.sigmoid(outputs).cpu().numpy()\n        preds = (probs > 0.5).astype(int)\n        test_preds.extend(preds)\n        test_labels.extend(labels.cpu().numpy())\n        test_probs.extend(probs)\n\n# --- Final Reports for the TEST SET ---\ntest_accuracy = accuracy_score(test_labels, test_preds)\nauc_score = roc_auc_score(test_labels, test_probs)\nf1 = f1_score(test_labels, test_preds)\nprint(f\"\\n--- TEST SET RESULTS ---\")\nprint(f\"Test Accuracy: {test_accuracy:.4f}\")\nprint(f\"Test AUC:      {auc_score:.4f}\")\nprint(f\"Test F1-Score:   {f1:.4f}\")\n\nprint(\"\\nClassification Report (Test Set):\")\nprint(classification_report(test_labels, test_preds, target_names=['Benign', 'Malignant']))\n\nprint(\"\\nConfusion Matrix (Test Set):\")\ncm = confusion_matrix(test_labels, test_preds)\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['Benign', 'Malignant'], yticklabels=['Benign', 'Malignant'])\nplt.title('Confusion Matrix (Test Set)'); plt.ylabel('Actual'); plt.xlabel('Predicted')\nplt.show()","metadata":{"trusted":true,"execution":{"execution_failed":"2025-09-02T04:41:59.419Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}