{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -q tensorflow keras-cv grad-cam","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-08-01T13:05:33.287176Z","iopub.execute_input":"2026-08-01T13:05:33.288033Z","iopub.status.idle":"2026-08-01T13:05:37.006640Z","shell.execute_reply.started":"2026-08-01T13:05:33.287996Z","shell.execute_reply":"2026-08-01T13:05:37.005776Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\n\nprint(f\"TensorFlow Version: {tf.__version__}\")\nprint(f\"GPUs Available: {tf.config.list_physical_devices('GPU')}\")\n\n# Dataset Paths\nDATA_DIR = '/kaggle/input/competitions/siim-isic-melanoma-classification'\ntrain_df = pd.read_csv(f'{DATA_DIR}/train.csv')\n\n# Build complete file paths for Keras\ntrain_df['filepath'] = train_df['image_name'].apply(lambda x: f\"{DATA_DIR}/jpeg/train/{x}.jpg\")\n\nprint(f\"Total Images: {len(train_df)}\")\nprint(f\"Class Distribution:\\n{train_df['target'].value_counts(normalize=True)}\")\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T13:11:33.680397Z","iopub.execute_input":"2026-08-01T13:11:33.680844Z","iopub.status.idle":"2026-08-01T13:11:33.751607Z","shell.execute_reply.started":"2026-08-01T13:11:33.680813Z","shell.execute_reply":"2026-08-01T13:11:33.750730Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"benign_samples = train_df[train_df['target'] == 0].sample(3, random_state=42)\nmalignant_samples = train_df[train_df['target'] == 1].sample(3, random_state=42)\n\nfig, axes = plt.subplots(2, 3, figsize=(12, 8))\nfig.suptitle('Figure 1: Comparison of Dermoscopic Imagery (SIIM-ISIC)', fontsize=14, fontweight='bold')\n\nfor i, (_, row) in enumerate(benign_samples.iterrows()):\n    img = keras.utils.load_img(row['filepath'], target_size=(224, 224))\n    axes[0, i].imshow(img)\n    axes[0, i].set_title(f\"Benign (Target: 0)\\n{row['image_name']}\")\n    axes[0, i].axis('off')\n\nfor i, (_, row) in enumerate(malignant_samples.iterrows()):\n    img = keras.utils.load_img(row['filepath'], target_size=(224, 224))\n    axes[1, i].imshow(img)\n    axes[1, i].set_title(f\"Malignant (Target: 1)\\n{row['image_name']}\", color='darkred', fontweight='bold')\n    axes[1, i].axis('off')\n\nplt.tight_layout()\nplt.savefig('figure1_sample_imagery.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T13:11:56.433319Z","iopub.execute_input":"2026-08-01T13:11:56.434160Z","iopub.status.idle":"2026-08-01T13:11:58.475568Z","shell.execute_reply.started":"2026-08-01T13:11:56.434128Z","shell.execute_reply":"2026-08-01T13:11:58.474490Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.style.use('seaborn-v0_8-whitegrid' if 'seaborn-v0_8-whitegrid' in plt.style.available else 'default')\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\nfig.suptitle('Figure 2: Dataset Class Imbalance and Demographic (Age) Analysis', fontsize=14, fontweight='bold')\n\n# Subplot A: Class Distribution\nsns.countplot(x='target', data=train_df, ax=axes[0], palette=['#2b5c8f', '#d9534f'])\naxes[0].set_title('Target Distribution (0 = Benign, 1 = Malignant)')\naxes[0].set_xticklabels(['Benign (>98%)', 'Malignant (<2%)'])\naxes[0].set_ylabel('Total Count')\n\nfor p in axes[0].patches:\n    axes[0].annotate(f'{int(p.get_height())}', (p.get_x() + p.get_width() / 2., p.get_height()),\n                     ha='center', va='center', xytext=(0, 5), textcoords='offset points')\n\n# Subplot B: Age Distribution by Class\nsns.histplot(data=train_df, x='age_approx', hue='target', kde=True, element='step', \n             palette=['#2b5c8f', '#d9534f'], ax=axes[1], bins=15)\naxes[1].set_title('Age Distribution Across Classes')\naxes[1].set_xlabel('Approximate Patient Age')\naxes[1].set_ylabel('Density / Count')\n\nplt.tight_layout()\nplt.savefig('figure2_dataset_distributions.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T13:12:30.744939Z","iopub.execute_input":"2026-08-01T13:12:30.745828Z","iopub.status.idle":"2026-08-01T13:12:31.907434Z","shell.execute_reply.started":"2026-08-01T13:12:30.745798Z","shell.execute_reply":"2026-08-01T13:12:31.906721Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE = 224\nBATCH_SIZE = 32\n\ndef decode_image(filepath, label):\n    img = tf.io.read_file(filepath)\n    img = tf.image.decode_jpeg(img, channels=3)\n    img = tf.image.resize(img, [IMG_SIZE, IMG_SIZE])\n    img = tf.cast(img, tf.float32) / 255.0  # Rescale [0, 1]\n    return img, label\n\n# Create TensorFlow Datasets\nfilepaths = train_df['filepath'].values\nlabels = train_df['target'].values\n\ndataset = tf.data.Dataset.from_tensor_slices((filepaths, labels))\ndataset = dataset.map(decode_image, num_parallel_calls=tf.data.AUTOTUNE)\ndataset = dataset.shuffle(buffer_size=1000).batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)\n\n# Data Augmentation Layer in Keras\ndata_augmentation = keras.Sequential([\n    layers.RandomFlip(\"horizontal_and_vertical\"),\n    layers.RandomRotation(0.2),\n    layers.RandomContrast(0.1),\n], name=\"data_augmentation\")\n\nprint(\"tf.data pipeline successfully configured!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T13:12:52.425685Z","iopub.execute_input":"2026-08-01T13:12:52.426339Z","iopub.status.idle":"2026-08-01T13:12:52.706907Z","shell.execute_reply.started":"2026-08-01T13:12:52.426308Z","shell.execute_reply":"2026-08-01T13:12:52.706071Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_resnet_baseline(input_shape=(224, 224, 3)):\n    inputs = layers.Input(shape=input_shape)\n    x = data_augmentation(inputs)\n    \n    # Pre-trained ResNet50 Backbone\n    base_model = keras.applications.ResNet50(\n        include_top=False, weights=\"imagenet\", input_tensor=x\n    )\n    base_model.trainable = False  # Transfer Learning: Freeze initial weights\n    \n    x = layers.GlobalAveragePooling2D()(base_model.output)\n    x = layers.Dropout(0.3)(x)\n    outputs = layers.Dense(1, activation=\"sigmoid\", name=\"classification_head\")(x)\n    \n    model = keras.Model(inputs, outputs, name=\"ResNet50_Baseline\")\n    return model\n\nresnet_model = build_resnet_baseline()\nresnet_model.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T13:13:17.255015Z","iopub.execute_input":"2026-08-01T13:13:17.255416Z","iopub.status.idle":"2026-08-01T13:13:20.329674Z","shell.execute_reply.started":"2026-08-01T13:13:17.255388Z","shell.execute_reply":"2026-08-01T13:13:20.329102Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BinaryFocalLoss(keras.losses.Loss):\n    def __init__(self, gamma=2.0, alpha=0.25, name=\"binary_focal_loss\"):\n        super().__init__(name=name)\n        self.gamma = gamma\n        self.alpha = alpha\n\n    def call(self, y_true, y_pred):\n        y_true = tf.cast(y_true, tf.float32)\n        # Clip values to prevent log(0) numerical instability\n        y_pred = tf.clip_by_value(y_pred, 1e-7, 1.0 - 1e-7)\n        \n        # Binary Focal Loss formula\n        loss_zero = - (1 - self.alpha) * (y_pred ** self.gamma) * tf.math.log(1 - y_pred) * (1 - y_true)\n        loss_one = - self.alpha * ((1 - y_pred) ** self.gamma) * tf.math.log(y_pred) * y_true\n        \n        return tf.reduce_mean(loss_zero + loss_one)\n\n# Compile Model\nresnet_model.compile(\n    optimizer=keras.optimizers.AdamW(learning_rate=1e-4),\n    loss=BinaryFocalLoss(gamma=2.0, alpha=0.25),\n    metrics=[keras.metrics.AUC(name=\"roc_auc\"), keras.metrics.Precision(), keras.metrics.Recall()]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T13:14:35.020745Z","iopub.execute_input":"2026-08-01T13:14:35.021480Z","iopub.status.idle":"2026-08-01T13:14:35.043176Z","shell.execute_reply.started":"2026-08-01T13:14:35.021450Z","shell.execute_reply":"2026-08-01T13:14:35.042359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def make_gradcam_heatmap(img_array, model, last_conv_layer_name, pred_index=None):\n    grad_model = keras.models.Model(\n        inputs=[model.inputs],\n        outputs=[model.get_layer(last_conv_layer_name).output, model.output]\n    )\n\n    with tf.GradientTape() as tape:\n        last_conv_layer_output, preds = grad_model(img_array)\n        if pred_index is None:\n            pred_index = tf.argmax(preds[0])\n        class_channel = preds[:, pred_index]\n\n    grads = tape.gradient(class_channel, last_conv_layer_output)\n    pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))\n\n    last_conv_layer_output = last_conv_layer_output[0]\n    heatmap = last_conv_layer_output @ pooled_grads[..., tf.newaxis]\n    heatmap = tf.squeeze(heatmap)\n\n    heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)\n    return heatmap.numpy()\n\n# Grab sample malignant image\nsample_row = train_df[train_df['target'] == 1].iloc[0]\nraw_img = keras.utils.load_img(sample_row['filepath'], target_size=(224, 224))\nimg_array = keras.utils.img_to_array(raw_img) / 255.0\nimg_tensor = tf.expand_dims(img_array, axis=0)\n\n# Generate Heatmap from ResNet layer\nheatmap = make_gradcam_heatmap(img_tensor, resnet_model, last_conv_layer_name=\"conv5_block3_out\")\n\n# Plot Figure 5\nfig, axes = plt.subplots(1, 3, figsize=(12, 4))\nfig.suptitle('Figure 5: Explainable AI Heatmap Verification (Keras Grad-CAM)', fontsize=14, fontweight='bold')\n\naxes[0].imshow(raw_img)\naxes[0].set_title('Original Dermoscopic Image')\naxes[0].axis('off')\n\naxes[1].imshow(heatmap, cmap='jet')\naxes[1].set_title('Raw Attention Map')\naxes[1].axis('off')\n\naxes[2].imshow(raw_img)\naxes[2].imshow(heatmap, cmap='jet', alpha=0.4) # Overlay\naxes[2].set_title('Grad-CAM Heatmap Overlay')\naxes[2].axis('off')\n\nplt.tight_layout()\nplt.savefig('figure5_gradcam_xai.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T13:14:46.214451Z","iopub.execute_input":"2026-08-01T13:14:46.214750Z","iopub.status.idle":"2026-08-01T13:14:51.366443Z","shell.execute_reply.started":"2026-08-01T13:14:46.214726Z","shell.execute_reply":"2026-08-01T13:14:51.365537Z"}},"outputs":[],"execution_count":null}]}