{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":20270,"databundleVersionId":1222630,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport matplotlib.pyplot as plt\nimport cv2\nimport os","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T09:39:09.444827Z","iopub.execute_input":"2025-08-06T09:39:09.445105Z","iopub.status.idle":"2025-08-06T09:39:09.933570Z","shell.execute_reply.started":"2025-08-06T09:39:09.445082Z","shell.execute_reply":"2025-08-06T09:39:09.932706Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"! python --version","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:46:50.770379Z","iopub.execute_input":"2025-08-06T10:46:50.770654Z","iopub.status.idle":"2025-08-06T10:46:50.906802Z","shell.execute_reply.started":"2025-08-06T10:46:50.770637Z","shell.execute_reply":"2025-08-06T10:46:50.906021Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load the training data\ntrain_df = pd.read_csv('/kaggle/input/siim-isic-melanoma-classification/train.csv')\n\n# Check for missing values in the dataframe\nprint(\"Missing values in each column:\")\nprint(train_df.isnull().sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T09:39:09.935299Z","iopub.execute_input":"2025-08-06T09:39:09.935657Z","iopub.status.idle":"2025-08-06T09:39:10.022048Z","shell.execute_reply.started":"2025-08-06T09:39:09.935638Z","shell.execute_reply":"2025-08-06T09:39:10.021443Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"rows_with_nulls = train_df[train_df.isnull().any(axis=1)]\nsample_of_nulls = rows_with_nulls.sample(527)\n\n# 3. Print the resulting table\nprint(sample_of_nulls)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T09:39:10.022677Z","iopub.execute_input":"2025-08-06T09:39:10.022884Z","iopub.status.idle":"2025-08-06T09:39:10.054123Z","shell.execute_reply.started":"2025-08-06T09:39:10.022867Z","shell.execute_reply":"2025-08-06T09:39:10.053350Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T09:39:10.054838Z","iopub.execute_input":"2025-08-06T09:39:10.055062Z","iopub.status.idle":"2025-08-06T09:39:10.060318Z","shell.execute_reply.started":"2025-08-06T09:39:10.055045Z","shell.execute_reply":"2025-08-06T09:39:10.059639Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_cleaned = train_df.dropna()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T09:39:10.062123Z","iopub.execute_input":"2025-08-06T09:39:10.062368Z","iopub.status.idle":"2025-08-06T09:39:10.085875Z","shell.execute_reply.started":"2025-08-06T09:39:10.062305Z","shell.execute_reply":"2025-08-06T09:39:10.085106Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_cleaned.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T09:39:10.086604Z","iopub.execute_input":"2025-08-06T09:39:10.086791Z","iopub.status.idle":"2025-08-06T09:39:10.101547Z","shell.execute_reply.started":"2025-08-06T09:39:10.086768Z","shell.execute_reply":"2025-08-06T09:39:10.100811Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# Get the counts of benign vs. malignant cases\nclass_counts = sample_of_nulls['benign_malignant'].value_counts()\n\n# Get the percentage distribution\nclass_percentages = sample_of_nulls['benign_malignant'].value_counts(normalize=True) * 100\n\nprint(\"Class Distribution:\")\nprint(class_counts)\nprint(\"\\nClass Distribution (%):\")\nprint(class_percentages)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T09:39:10.102178Z","iopub.execute_input":"2025-08-06T09:39:10.102463Z","iopub.status.idle":"2025-08-06T09:39:10.125934Z","shell.execute_reply.started":"2025-08-06T09:39:10.102438Z","shell.execute_reply":"2025-08-06T09:39:10.125198Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Path to the training images\nimage_path = '/kaggle/input/siim-isic-melanoma-classification/jpeg/train/'\n\n# Get the list of benign and malignant image names\nbenign_images = train_df[train_df['target'] == 0]['image_name'].values\nmalignant_images = train_df[train_df['target'] == 1]['image_name'].values\n\n# Plot 10 benign images\nprint(\"Benign Images\")\nplt.figure(figsize=(12, 6))\nfor i in range(10):\n    plt.subplot(2, 5, i + 1)\n    img = cv2.imread(os.path.join(image_path, benign_images[i] + '.jpg'))\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    plt.imshow(img)\n    plt.title('Benign')\n    plt.axis('off')\nplt.show()\n\n# Plot 10 malignant images\nprint(\"Malignant Images\")\nplt.figure(figsize=(12, 6))\nfor i in range(10):\n    plt.subplot(2, 5, i + 1)\n    img = cv2.imread(os.path.join(image_path, malignant_images[i] + '.jpg'))\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    plt.imshow(img)\n    plt.title('Malignant')\n    plt.axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T09:39:10.126641Z","iopub.execute_input":"2025-08-06T09:39:10.126827Z","iopub.status.idle":"2025-08-06T09:39:31.149459Z","shell.execute_reply.started":"2025-08-06T09:39:10.126811Z","shell.execute_reply":"2025-08-06T09:39:31.148651Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # use pytorch\n# knowing the dataset class:\n#     the targeted model of multimodal:\n#              ann and cnn","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport torch\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, OneHotEncoder\nfrom sklearn.compose import ColumnTransformer\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:36:52.544555Z","iopub.execute_input":"2025-08-06T10:36:52.544828Z","iopub.status.idle":"2025-08-06T10:36:57.589877Z","shell.execute_reply.started":"2025-08-06T10:36:52.544784Z","shell.execute_reply":"2025-08-06T10:36:57.589245Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Configuration and Setup ---\n# Define paths and parameters\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')\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Using device: {DEVICE}\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:36:57.590580Z","iopub.execute_input":"2025-08-06T10:36:57.591012Z","iopub.status.idle":"2025-08-06T10:36:57.662606Z","shell.execute_reply.started":"2025-08-06T10:36:57.590985Z","shell.execute_reply":"2025-08-06T10:36:57.661783Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Load and Preprocess Tabular Data ---\nprint(\"Loading tabular data...\")\ndf = pd.read_csv(CSV_PATH)\nprint(f\"Original dataset size: {len(df)} samples\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:37:41.647074Z","iopub.execute_input":"2025-08-06T10:37:41.647344Z","iopub.status.idle":"2025-08-06T10:37:41.736866Z","shell.execute_reply.started":"2025-08-06T10:37:41.647318Z","shell.execute_reply":"2025-08-06T10:37:41.736120Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Drop all rows that have at least one missing value\ndf.dropna(inplace=True)\nprint(f\"Dataset size after dropping nulls: {len(df)} samples\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:37:42.005030Z","iopub.execute_input":"2025-08-06T10:37:42.005522Z","iopub.status.idle":"2025-08-06T10:37:42.025745Z","shell.execute_reply.started":"2025-08-06T10:37:42.005498Z","shell.execute_reply":"2025-08-06T10:37:42.024996Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Manually encode the 'sex' column into a binary numerical format\ndf['sex'] = df['sex'].map({'male': 0, 'female': 1})\n\n# **FIX:** Explicitly convert the 'sex' column to a numeric data type\ndf['sex'] = pd.to_numeric(df['sex'])\nprint(\"Manually encoded 'sex' column and converted to numeric type.\")\n\n\n# Define which features are numerical and which are categorical\nnumerical_features = ['age_approx', 'sex']\ncategorical_features = ['anatom_site_general_challenge']\n\n# Create a ColumnTransformer to preprocess the data\npreprocessor = ColumnTransformer(\n    transformers=[\n        ('num', StandardScaler(), numerical_features),\n        ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features)\n    ],\n    remainder='drop'\n)\n\n# Split the data into training and validation sets\ntrain_df, val_df = train_test_split(df, test_size=0.2, random_state=42, stratify=df['target'])\n\n# Fit the preprocessor on the training data\npreprocessor.fit(train_df[numerical_features + categorical_features])\n\nprint(\"\\nPart 1 Complete: Data is loaded, preprocessed, and ready.\")\nprint(f\"Training set size: {len(train_df)} samples\")\nprint(f\"Validation set size: {len(val_df)} samples\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:39:53.200625Z","iopub.execute_input":"2025-08-06T10:39:53.201448Z","iopub.status.idle":"2025-08-06T10:39:53.256128Z","shell.execute_reply.started":"2025-08-06T10:39:53.201424Z","shell.execute_reply":"2025-08-06T10:39:53.255456Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\nfrom torchvision import transforms\n\n# --- Part 2: Custom PyTorch Dataset and DataLoaders ---\n\n# This class will load images and their corresponding tabular data for the model\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        \n        # Pre-process the tabular data and store it.\n        # FIX: Removed .toarray() as the output is already a NumPy array\n        self.tabular_data = self.tabular_preprocessor.transform(\n            self.df[numerical_features + categorical_features]\n        )\n\n    def __len__(self):\n        # This method returns the total number of samples in the dataset.\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        # This method fetches a single sample from the dataset at the given index.\n        \n        # Get the corresponding row from the dataframe\n        row = self.df.iloc[idx]\n        \n        # Load the image from the file path\n        img_name = row['image_name']\n        img_path = os.path.join(self.image_dir, f\"{img_name}.jpg\")\n        image = Image.open(img_path).convert('RGB')\n        \n        # Apply the image transformations (e.g., resize, augment, convert to tensor)\n        if self.transform:\n            image = self.transform(image)\n            \n        # Get the pre-processed tabular data for this index\n        tabular = torch.tensor(self.tabular_data[idx], dtype=torch.float)\n        \n        # Get the label (target) for this sample\n        label = torch.tensor(row['target'], dtype=torch.float)\n        \n        return image, tabular, label\n\n# --- Define Image Transformations ---\nIMG_SIZE = 224 # A standard size for pre-trained models like ResNet/EfficientNet\n\n# Define the transformations for the training set.\ntrain_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(15),\n    transforms.ToTensor(), \n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n# Define the transformations for the validation set.\nval_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n# --- Create Datasets and DataLoaders ---\n# Instantiate the custom datasets\ntrain_dataset = MelanomaDataset(train_df, preprocessor, IMAGE_PATH, transform=train_transform)\nval_dataset = MelanomaDataset(val_df, preprocessor, IMAGE_PATH, transform=val_transform)\n\n# Create the DataLoaders\nBATCH_SIZE = 60\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=4)\n\nprint(\"Part 2 Complete: Custom Datasets and DataLoaders are ready.\")\nprint(f\"One batch of training data will have {BATCH_SIZE} images and {BATCH_SIZE} corresponding tabular data rows.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T11:12:50.192030Z","iopub.execute_input":"2025-08-06T11:12:50.192715Z","iopub.status.idle":"2025-08-06T11:12:50.222747Z","shell.execute_reply.started":"2025-08-06T11:12:50.192674Z","shell.execute_reply":"2025-08-06T11:12:50.222108Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torchvision import models\n\n# --- Part 3: Multimodal Model Architecture ---\n\nclass MultimodalMelanomaNet(nn.Module):\n    def __init__(self, num_tabular_features, pretrained=True):\n        super(MultimodalMelanomaNet, self).__init__()\n        \n        # --- Image Branch (CNN - EfficientNet-B0) ---\n        # Load the pre-trained EfficientNet-B0 model\n        self.image_branch = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT)\n        \n        # We need to replace the final classification layer of the pre-trained model.\n        # Instead of classifying into 1000 ImageNet classes, we want it to output a feature vector.\n        # Let's make it output a vector of size 128.\n        num_image_features = self.image_branch.classifier[1].in_features\n        self.image_branch.classifier = nn.Linear(num_image_features, 128)\n\n        # --- Tabular Branch (ANN/MLP) ---\n        # This is a simple multi-layer perceptron for our metadata.\n        self.tabular_branch = nn.Sequential(\n            nn.Linear(num_tabular_features, 64),\n            nn.ReLU(),\n            nn.Dropout(0.3), # Dropout helps prevent overfitting\n            nn.Linear(64, 32),\n            nn.ReLU(),\n            nn.Dropout(0.3)\n        )\n        \n        # --- Fusion and Final Classifier Head ---\n        # This part combines the outputs from both branches.\n        self.fusion = nn.Linear(128 + 32, 64) # 128 from image branch + 32 from tabular branch\n        \n        self.classifier = nn.Sequential(\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(64, 1) # Final output: a single logit for binary classification\n        )\n\n    def forward(self, image, tabular):\n        # This defines the data flow through the model.\n        \n        # 1. Process inputs through their respective branches\n        image_features = self.image_branch(image)\n        tabular_features = self.tabular_branch(tabular)\n        \n        # 2. Concatenate (fuse) the feature vectors from both branches\n        combined_features = torch.cat([image_features, tabular_features], dim=1)\n        \n        # 3. Pass the fused features through the final classifier layers\n        fused = self.fusion(combined_features)\n        output = self.classifier(fused)\n        \n        return output\n\n# --- Instantiate the Model ---\n# To create the model, we first need to know the exact number of features our tabular preprocessor creates.\nnum_tab_features = preprocessor.transform(train_df.head(1)[numerical_features + categorical_features]).shape[1]\n\n# Now, create an instance of our model and move it to the correct device (CPU or GPU)\nmodel = MultimodalMelanomaNet(num_tab_features).to(DEVICE)\n\n# Optional: Print the model architecture to verify it's correct\nprint(\"--- Model Architecture ---\")\nprint(model)\n\nprint(\"\\nPart 3 Complete: The multimodal model architecture has been defined.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T11:12:50.519550Z","iopub.execute_input":"2025-08-06T11:12:50.519808Z","iopub.status.idle":"2025-08-06T11:12:50.684945Z","shell.execute_reply.started":"2025-08-06T11:12:50.519791Z","shell.execute_reply":"2025-08-06T11:12:50.684236Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom sklearn.metrics import roc_auc_score, accuracy_score, confusion_matrix, classification_report\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm.notebook import tqdm # Import tqdm for progress bars\n\n# --- Part 4: Training, Evaluation, and Visualization ---\n\ndef train_model(model, criterion, optimizer, train_loader, val_loader, epochs):\n    \"\"\"\n    Function to handle the training and validation of the model.\n    \"\"\"\n    # Dictionary to store metrics for each epoch\n    history = {\n        'train_loss': [], 'val_loss': [],\n        'train_acc': [], 'val_acc': [],\n        'train_auc': [], 'val_auc': []\n    }\n\n    for epoch in range(epochs):\n        # --- Training Phase ---\n        model.train()\n        train_loss = 0.0\n        # Wrap train_loader with tqdm for a progress bar\n        train_loop = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{epochs} [Train]\", leave=False)\n        for images, tabular_data, labels in train_loop:\n            images, tabular_data, labels = images.to(DEVICE), tabular_data.to(DEVICE), labels.to(DEVICE).unsqueeze(1)\n            optimizer.zero_grad()\n            outputs = model(images, tabular_data)\n            loss = criterion(outputs, labels)\n            loss.backward()\n            optimizer.step()\n            train_loss += loss.item() * images.size(0)\n            train_loop.set_postfix(loss=loss.item())\n\n        epoch_train_loss = train_loss / len(train_loader.dataset)\n        history['train_loss'].append(epoch_train_loss)\n\n        # --- Evaluation Phase ---\n        model.eval()\n        all_preds = {'train': [], 'val': []}\n        all_labels = {'train': [], 'val': []}\n        val_loss = 0.0\n\n        with torch.no_grad():\n            # Evaluate on training set to get training accuracy and AUC\n            for images, tabular_data, labels in train_loader:\n                images, tabular_data = images.to(DEVICE), tabular_data.to(DEVICE)\n                outputs = model(images, tabular_data)\n                preds_proba = torch.sigmoid(outputs).cpu().numpy()\n                all_preds['train'].extend(preds_proba)\n                all_labels['train'].extend(labels.cpu().numpy())\n\n            # Evaluate on validation set\n            val_loop = tqdm(val_loader, desc=f\"Epoch {epoch+1}/{epochs} [Val]\", leave=False)\n            for images, tabular_data, labels in val_loop:\n                images, tabular_data, labels_dev = images.to(DEVICE), tabular_data.to(DEVICE), labels.to(DEVICE).unsqueeze(1)\n                outputs = model(images, tabular_data)\n                loss = criterion(outputs, labels_dev)\n                val_loss += loss.item() * images.size(0)\n                preds_proba = torch.sigmoid(outputs).cpu().numpy()\n                all_preds['val'].extend(preds_proba)\n                all_labels['val'].extend(labels.cpu().numpy())\n                val_loop.set_postfix(loss=loss.item())\n\n        # Calculate and store metrics for the epoch\n        epoch_val_loss = val_loss / len(val_loader.dataset)\n        history['val_loss'].append(epoch_val_loss)\n\n        train_preds_binary = (np.array(all_preds['train']) > 0.5).astype(int)\n        val_preds_binary = (np.array(all_preds['val']) > 0.5).astype(int)\n\n        history['train_acc'].append(accuracy_score(all_labels['train'], train_preds_binary))\n        history['val_acc'].append(accuracy_score(all_labels['val'], val_preds_binary))\n        history['train_auc'].append(roc_auc_score(all_labels['train'], all_preds['train']))\n        history['val_auc'].append(roc_auc_score(all_labels['val'], all_preds['val']))\n\n        print(f\"Epoch {epoch+1}/{epochs} | Train Loss: {history['train_loss'][-1]:.4f} | Val Loss: {history['val_loss'][-1]:.4f} | Train Acc: {history['train_acc'][-1]:.4f} | Val Acc: {history['val_acc'][-1]:.4f} | Val AUC: {history['val_auc'][-1]:.4f}\")\n    \n    print(\"\\nFinished Training.\")\n    return history, all_labels['val'], val_preds_binary\n\n# --- Define Loss Function and Optimizer ---\npos_weight = len(train_df[train_df['target'] == 0]) / len(train_df[train_df['target'] == 1])\ncriterion = nn.BCEWithLogitsLoss(pos_weight=torch.tensor(pos_weight, device=DEVICE))\noptimizer = optim.AdamW(model.parameters(), lr=1e-4)\n\n# --- Execute Training ---\nEPOCHS = 5\nhistory, final_val_labels, final_val_preds = train_model(model, criterion, optimizer, train_loader, val_loader, epochs=EPOCHS)\n\n# --- Plotting Training Curves ---\ndef plot_training_curves(history):\n    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 6))\n    epochs_range = range(1, len(history['train_loss']) + 1)\n\n    ax1.plot(epochs_range, history['train_loss'], 'o-', label='Train Loss')\n    ax1.plot(epochs_range, history['val_loss'], 'o-', label='Validation Loss')\n    ax1.set_title('Loss vs. Epochs')\n    ax1.set_xlabel('Epoch')\n    ax1.set_ylabel('Loss')\n    ax1.legend()\n    ax1.grid(True)\n\n    ax2.plot(epochs_range, history['train_acc'], 'o-', label='Train Accuracy')\n    ax2.plot(epochs_range, history['val_acc'], 'o-', label='Validation Accuracy')\n    ax2.set_title('Accuracy vs. Epochs')\n    ax2.set_xlabel('Epoch')\n    ax2.set_ylabel('Accuracy')\n    ax2.legend()\n    ax2.grid(True)\n\n    plt.show()\n\nplot_training_curves(history)\n\n# --- Final Evaluation ---\nprint(\"\\n--- Final Model Evaluation on Validation Set ---\")\nprint(\"\\nClassification Report:\")\nprint(classification_report(final_val_labels, final_val_preds, target_names=['Benign', 'Malignant']))\n\nprint(\"Confusion Matrix:\")\ncm = confusion_matrix(final_val_labels, final_val_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')\nplt.ylabel('Actual')\nplt.xlabel('Predicted')\nplt.show()\n\n# --- Exporting the Model ---\nMODEL_EXPORT_PATH = 'multimodal_melanoma_model.pth'\ntorch.save(model.state_dict(), MODEL_EXPORT_PATH)\nprint(f\"\\nModel state dictionary saved to: {MODEL_EXPORT_PATH}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T11:12:50.787874Z","iopub.execute_input":"2025-08-06T11:12:50.788198Z","iopub.status.idle":"2025-08-06T11:15:54.129471Z","shell.execute_reply.started":"2025-08-06T11:12:50.788176Z","shell.execute_reply":"2025-08-06T11:15:54.128099Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}