{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","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":31012,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Melanoma Classification: Streamlined Model Training (GPU)\n#\nThis notebook focuses on training a MobileNetV2 model for melanoma classification using TFRecords on a Kaggle GPU environment. It's a simplified version of the original Notebook III.","metadata":{"_uuid":"ec4ff3ac-1a17-4bbf-8ff5-7c4065c2739e","_cell_guid":"f54d1aaf-d8b5-455f-89d7-6a49c7886968","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"markdown","source":"## 1. Setup and Imports","metadata":{"_uuid":"7b232f84-6af0-4ae6-b9df-3b15d5b87bfd","_cell_guid":"b1a25567-d5f4-4b5d-bcb5-496b2175ef69","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"import os\nimport re\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nfrom kaggle_datasets import KaggleDatasets\nfrom functools import partial\nfrom sklearn.model_selection import train_test_split\nfrom tqdm.notebook import tqdm # Use tqdm.notebook for Kaggle\nimport gc","metadata":{"_uuid":"b05bca93-5683-4cda-a5a4-abc6aea7ad5f","_cell_guid":"21df9d26-6b23-4d81-9b92-d216eb70873b","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-05-12T12:45:06.202523Z","iopub.execute_input":"2025-05-12T12:45:06.203532Z","iopub.status.idle":"2025-05-12T12:45:20.058253Z","shell.execute_reply.started":"2025-05-12T12:45:06.203497Z","shell.execute_reply":"2025-05-12T12:45:20.057590Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Configuration","metadata":{"_uuid":"a3331fc5-4bd4-43e1-84fb-d966ad6fd301","_cell_guid":"423f4a2b-0ec7-41f8-a10a-19c67e45fe96","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# Environment Setup\nprint(\"TensorFlow Version:\", tf.__version__)\ngpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    try:\n        # Currently, memory growth needs to be the same across GPUs\n        for gpu in gpus:\n            tf.config.experimental.set_memory_growth(gpu, True)\n        logical_gpus = tf.config.list_logical_devices('GPU')\n        print(len(gpus), \"Physical GPUs,\", len(logical_gpus), \"Logical GPUs\")\n    except RuntimeError as e:\n        # Memory growth must be set before GPUs have been initialized\n        print(e)\nelse:\n    print(\"No GPU detected. Running on CPU.\")\n\n# TPU setup removed, using default strategy for GPU/CPU\nstrategy = tf.distribute.get_strategy()\nprint('Number of replicas:', strategy.num_replicas_in_sync)\n\n# Constants\nGCS_PATH = KaggleDatasets().get_gcs_path(\"siim-isic-melanoma-classification\")\nKAGGLE_PATH = '/kaggle/input/siim-isic-melanoma-classification'\n# Adjust BATCH_SIZE for GPU memory (original was 16 * replicas for TPU)\nBATCH_SIZE = 32 * strategy.num_replicas_in_sync # Start with 32 or 64 per replica (GPU)\nIMAGE_SIZE = [256, 256] # Target size for the model\nAUTOTUNE = tf.data.experimental.AUTOTUNE\n\nprint(\"Batch Size:\", BATCH_SIZE)\nprint(\"Image Size:\", IMAGE_SIZE)\nprint(\"GCS Path:\", GCS_PATH)","metadata":{"_uuid":"92f28ec8-ee22-489e-a517-fd7269e045c0","_cell_guid":"713adc9b-6494-42b6-baea-50e7b97b5920","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-05-12T12:45:20.059728Z","iopub.execute_input":"2025-05-12T12:45:20.060157Z","iopub.status.idle":"2025-05-12T12:45:21.483365Z","shell.execute_reply.started":"2025-05-12T12:45:20.060137Z","shell.execute_reply":"2025-05-12T12:45:21.482693Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Load Data Paths and Split","metadata":{"_uuid":"adcc1f51-89f3-4c80-b920-428f035fdd11","_cell_guid":"b0e92737-57eb-4c11-b830-1ce6387612ca","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# Get TFRecord file paths\nALL_TRAINING_FILENAMES = tf.io.gfile.glob(KAGGLE_PATH + \"/tfrecords/train*.tfrec\")\nTEST_FILENAMES = tf.io.gfile.glob(KAGGLE_PATH + \"/tfrecords/test*.tfrec\")\n\n# Split training files for validation\nTRAIN_FILENAMES, VAL_FILENAMES = train_test_split(\n    ALL_TRAINING_FILENAMES,\n    test_size=0.1, # 10% for validation\n    random_state=42 # Use a fixed random state for reproducibility\n)\n\nprint(\"Number of training TFRecord files:\", len(TRAIN_FILENAMES))\nprint(\"Number of validation TFRecord files:\", len(VAL_FILENAMES))\nprint(\"Number of test TFRecord files:\", len(TEST_FILENAMES))","metadata":{"_uuid":"eb6d2915-cbf8-4479-96fc-b3448a405463","_cell_guid":"be0b834a-8467-431a-aaab-94b4f7badf1d","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-05-12T12:45:21.484118Z","iopub.execute_input":"2025-05-12T12:45:21.484434Z","iopub.status.idle":"2025-05-12T12:45:21.591191Z","shell.execute_reply.started":"2025-05-12T12:45:21.484415Z","shell.execute_reply":"2025-05-12T12:45:21.590592Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Data Pipeline Functions","metadata":{"_uuid":"83bfef0e-bfff-4894-82c9-c338c95eb2e4","_cell_guid":"1edd1496-4911-45a2-a137-0c982743c8cd","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"def decode_image(image_data):\n    \"\"\"Decodes JPEG image, casts to float32, and normalizes.\"\"\"\n    image = tf.image.decode_jpeg(image_data, channels=3)\n    image = tf.cast(image, tf.float32) / 255.0  # normalize to [0,1]\n    # No initial resize here, will be done in augmentation/preprocessing step\n    return image\n\ndef read_tfrecord(example, labeled):\n    \"\"\"Parses a single TFRecord example.\"\"\"\n    if labeled:\n        tfrecord_format = {\n            \"image\": tf.io.FixedLenFeature([], tf.string),\n            \"target\": tf.io.FixedLenFeature([], tf.int64)\n        }\n    else:\n        tfrecord_format = {\n            \"image\": tf.io.FixedLenFeature([], tf.string),\n            \"image_name\": tf.io.FixedLenFeature([], tf.string)\n        }\n    example = tf.io.parse_single_example(example, tfrecord_format)\n    image = decode_image(example['image'])\n    if labeled:\n        label = tf.cast(example['target'], tf.int32)\n        return image, label\n    else:\n        image_name = example['image_name']\n        return image, image_name\n\ndef preprocess_image(image, label=None, is_training=False):\n    \"\"\"Resizes and optionally augments the image.\"\"\"\n    image = tf.image.resize(image, IMAGE_SIZE)\n    if is_training:\n        # Basic augmentation\n        image = tf.image.random_flip_left_right(image)\n        # image = tf.image.random_flip_up_down(image) # Optional\n        # image = tf.image.random_saturation(image, 0.8, 1.2) # Optional\n        # image = tf.image.random_brightness(image, 0.1) # Optional\n        # image = tf.image.random_contrast(image, 0.8, 1.2) # Optional\n    if label is None:\n        return image\n    else:\n        return image, label\n\n\ndef load_dataset(filenames, labeled=True, ordered=False, is_training=False):\n    \"\"\"Loads TFRecords, preprocesses, and batches the dataset.\"\"\"\n    ignore_order = tf.data.Options()\n    if not ordered:\n        ignore_order.experimental_deterministic = False # disable order, increase speed\n\n    dataset = tf.data.TFRecordDataset(filenames, num_parallel_reads=AUTOTUNE)\n    dataset = dataset.with_options(ignore_order)\n    dataset = dataset.map(partial(read_tfrecord, labeled=labeled), num_parallel_calls=AUTOTUNE)\n    # Apply preprocessing and augmentation\n    dataset = dataset.map(partial(preprocess_image, is_training=is_training), num_parallel_calls=AUTOTUNE)\n\n    if is_training:\n        dataset = dataset.shuffle(2048) # Shuffle buffer size\n        dataset = dataset.repeat()\n\n    dataset = dataset.batch(BATCH_SIZE)\n    dataset = dataset.prefetch(AUTOTUNE) # prefetch next batch while training\n    return dataset","metadata":{"_uuid":"c6478d28-24d3-4544-a037-41c05b021c74","_cell_guid":"f809b00d-f4d4-4952-8c21-77b694d39557","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-05-12T12:45:21.592022Z","iopub.execute_input":"2025-05-12T12:45:21.592260Z","iopub.status.idle":"2025-05-12T12:45:21.600850Z","shell.execute_reply.started":"2025-05-12T12:45:21.592242Z","shell.execute_reply":"2025-05-12T12:45:21.600075Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Create Datasets and Calculate Steps","metadata":{"_uuid":"2f4ac5f1-a385-46b5-b1b3-0b3ca4c57b9f","_cell_guid":"781ba02d-d227-464f-9e03-8627855a9fe1","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"train_dataset = load_dataset(TRAIN_FILENAMES, labeled=True, ordered=False, is_training=True)\nval_dataset = load_dataset(VAL_FILENAMES, labeled=True, ordered=False, is_training=False) # No augmentation/repeat/shuffle for val\ntest_dataset = load_dataset(TEST_FILENAMES, labeled=False, ordered=True, is_training=False) # Ordered for submission\n\n# Calculate number of images and steps\ndef count_data_items(filenames):\n    n = [int(re.compile(r\"-([0-9]*)\\.\").search(filename).group(1)) for filename in filenames]\n    return np.sum(n)\n\nnum_training_images = count_data_items(TRAIN_FILENAMES)\nnum_validation_images = count_data_items(VAL_FILENAMES)\nnum_test_images = count_data_items(TEST_FILENAMES)\n\nSTEPS_PER_EPOCH_TRAIN = num_training_images // BATCH_SIZE\n\nprint(f\"Training images: {num_training_images}, Steps/epoch: {STEPS_PER_EPOCH_TRAIN}\")\nprint(f\"Test images: {num_test_images}\")","metadata":{"_uuid":"5ffaf5e8-5667-4e69-a56d-1f66dc5d23a2","_cell_guid":"1f353a70-f486-4668-bec1-a7d1377bf35d","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-05-12T12:45:21.602640Z","iopub.execute_input":"2025-05-12T12:45:21.603038Z","iopub.status.idle":"2025-05-12T12:45:21.954927Z","shell.execute_reply.started":"2025-05-12T12:45:21.603021Z","shell.execute_reply":"2025-05-12T12:45:21.954286Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. (Optional) Visualize a Batch","metadata":{"_uuid":"c2855cb9-23fa-473b-89e4-625428d78f3e","_cell_guid":"c0097160-27f6-4666-8087-4b1009cbad01","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# def show_batch(image_batch, label_batch):\n#     plt.figure(figsize=(15, 15))\n#     for n in range(min(8, BATCH_SIZE)): # Show up to 8 images\n#         ax = plt.subplot(2, 4, n + 1)\n#         plt.imshow(image_batch[n])\n#         if label_batch[n] == 0:\n#             plt.title(\"BENIGN\")\n#         else:\n#             plt.title(\"MALIGNANT\")\n#         plt.axis(\"off\")\n#     plt.tight_layout()\n#     plt.show()\n\n# # Fetch a batch from the training dataset to visualize\n# image_batch, label_batch = next(iter(train_dataset))\n# show_batch(image_batch.numpy(), label_batch.numpy())\n\n# # Clean up memory\n# del image_batch, label_batch\n# gc.collect()","metadata":{"_uuid":"95335123-2909-49e8-af2c-50643041ce87","_cell_guid":"505e7bd6-a75f-41b9-b52e-bbf6a2fe685d","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-05-12T12:45:21.955558Z","iopub.execute_input":"2025-05-12T12:45:21.955786Z","iopub.status.idle":"2025-05-12T12:45:21.959350Z","shell.execute_reply.started":"2025-05-12T12:45:21.955768Z","shell.execute_reply":"2025-05-12T12:45:21.958583Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Model Building (MobileNetV2)","metadata":{"_uuid":"57b0bbc7-948c-4d9d-a67b-348f6714a5ef","_cell_guid":"26e4729f-e1c6-4036-abf2-f9cdf1cdd803","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# --- Class Weights and Bias Initialization (Requires train.csv) ---\n# Load train CSV briefly to calculate weights\ntrain_df = pd.read_csv(\"/kaggle/input/siim-isic-melanoma-classification/train.csv\")\nmalignant_count = train_df['target'].sum()\ntotal_count = len(train_df)\nbenign_count = total_count - malignant_count\n\nweight_malignant = (total_count / malignant_count) / 2.0\nweight_benign = (total_count / benign_count) / 2.0\nclass_weight = {0: weight_benign, 1: weight_malignant}\ninitial_bias = np.log([malignant_count / benign_count]) # Calculate initial bias\n\nprint(f\"Benign cases: {benign_count}, Malignant cases: {malignant_count}\")\nprint(f\"Weight for class 0 (Benign): {class_weight[0]:.2f}\")\nprint(f\"Weight for class 1 (Malignant): {class_weight[1]:.2f}\")\nprint(f\"Initial bias: {initial_bias[0]:.2f}\")\n\ndel train_df # Free memory\ngc.collect()\n# --- End Class Weights ---\n\n\n# --- Model Definition ---\n# No strategy.scope() needed for default strategy\nbase_model = tf.keras.applications.MobileNetV2(\n    input_shape=(*IMAGE_SIZE, 3),\n    include_top=False, # Exclude the final classification layer\n    weights='imagenet' # Use pre-trained ImageNet weights\n)\nbase_model.trainable = False # Freeze the base model layers initially\n\nmodel = tf.keras.Sequential([\n    base_model,\n    tf.keras.layers.GlobalAveragePooling2D(),\n    tf.keras.layers.Dense(20, activation=\"relu\"), # Smaller dense layers from original\n    tf.keras.layers.Dropout(0.4),                 # Dropout for regularization\n    tf.keras.layers.Dense(10, activation=\"relu\"),\n    tf.keras.layers.Dropout(0.3),\n    tf.keras.layers.Dense(1, activation='sigmoid', # Output layer for binary classification\n                          bias_initializer=tf.keras.initializers.Constant(initial_bias)) # Set initial bias\n])\n\n# Compile the model\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), # Standard Adam optimizer\n    loss='binary_crossentropy', # Suitable for binary classification\n    metrics=[tf.keras.metrics.AUC(name='auc')] # Competition metric\n)\n\nmodel.summary()\n# --- End Model Definition ---","metadata":{"_uuid":"052d5b1f-e1f3-4fdb-88fa-0780380e67ae","_cell_guid":"36ecb155-a08d-4936-bbab-8de91d92b8a5","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-05-12T12:45:21.960037Z","iopub.execute_input":"2025-05-12T12:45:21.960257Z","iopub.status.idle":"2025-05-12T12:45:24.650029Z","shell.execute_reply.started":"2025-05-12T12:45:21.960242Z","shell.execute_reply":"2025-05-12T12:45:24.649459Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. Define Callbacks","metadata":{"_uuid":"909b25e5-ee5e-4bfe-a630-4d26837d0b64","_cell_guid":"b58f68cb-b242-4359-9ed9-0426d312160c","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# Callbacks for training\ncallback_early_stopping = tf.keras.callbacks.EarlyStopping(\n    monitor='val_auc', # Monitor validation AUC\n    patience=15,         # Stop after 15 epochs with no improvement\n    mode='max',         # Maximize AUC\n    verbose=1,\n    restore_best_weights=True # Restore weights from the epoch with the best val_auc\n)\n\ncallback_lr_reduce = tf.keras.callbacks.ReduceLROnPlateau(\n    monitor='val_auc', # Monitor validation AUC\n    factor=0.1,        # Reduce LR by factor of 10\n    patience=5,        # Reduce after 5 epochs with no improvement\n    mode='max',        # Maximize AUC\n    verbose=1,\n    min_lr=1e-6        # Minimum learning rate\n)\n\n# Checkpoint saving the best weights based on validation AUC\ncallback_checkpoint = tf.keras.callbacks.ModelCheckpoint(\n    \"melanoma_best.weights.h5\", # File path\n    monitor='val_auc',          # Monitor validation AUC\n    mode='max',                 # Maximize AUC\n    save_best_only=True,        # Only save the best model\n    save_weights_only=True,     # Save only the weights\n    verbose=0                   # Less verbose output\n)","metadata":{"_uuid":"f135affe-30f8-49ed-9cce-616a1752c224","_cell_guid":"61365060-68ce-418d-b76f-e6f789449eed","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-05-12T12:45:24.650588Z","iopub.execute_input":"2025-05-12T12:45:24.650821Z","iopub.status.idle":"2025-05-12T12:45:24.655819Z","shell.execute_reply.started":"2025-05-12T12:45:24.650804Z","shell.execute_reply":"2025-05-12T12:45:24.655057Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9. Train the Model","metadata":{"_uuid":"dc49fd0d-ad37-4b2c-9df0-a528796f8547","_cell_guid":"3608ed36-e676-4609-9450-846b8ba78d1f","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"EPOCHS = 40 # Set a reasonable number of epochs, EarlyStopping will likely stop it sooner\n\nhistory = model.fit(\n    train_dataset,\n    epochs=EPOCHS,\n    steps_per_epoch=STEPS_PER_EPOCH_TRAIN,\n    validation_data=val_dataset,\n    validation_steps=None,\n    callbacks=[callback_early_stopping, callback_lr_reduce, callback_checkpoint],\n    class_weight=class_weight, # Use calculated class weights\n    verbose=1 # Show progress bar\n)","metadata":{"_uuid":"cf96132a-c92f-436b-8b93-e68b459d9ee5","_cell_guid":"01fdbe83-7e9a-4e1d-b508-56ac31076395","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-05-12T12:45:24.656775Z","iopub.execute_input":"2025-05-12T12:45:24.656984Z","iopub.status.idle":"2025-05-12T12:48:14.845148Z","shell.execute_reply.started":"2025-05-12T12:45:24.656961Z","shell.execute_reply":"2025-05-12T12:48:14.844482Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10. Plot Training History","metadata":{"_uuid":"c15eeac9-506b-4278-b27f-1a71e601c984","_cell_guid":"7db07331-107d-4351-ae46-49c0e17f308f","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"def plot_history(history):\n    hist = history.history\n    epochs = range(1, len(hist['loss']) + 1)\n\n    plt.figure(figsize=(12, 5))\n\n    # Plot Loss\n    plt.subplot(1, 2, 1)\n    plt.plot(epochs, hist['loss'], 'bo-', label='Training loss')\n    plt.plot(epochs, hist['val_loss'], 'ro-', label='Validation loss')\n    plt.title('Training and Validation Loss')\n    plt.xlabel('Epochs')\n    plt.ylabel('Loss')\n    plt.legend()\n    plt.grid(True)\n\n    # Plot AUC\n    plt.subplot(1, 2, 2)\n    plt.plot(epochs, hist['auc'], 'bo-', label='Training AUC')\n    plt.plot(epochs, hist['val_auc'], 'ro-', label='Validation AUC')\n    plt.title('Training and Validation AUC')\n    plt.xlabel('Epochs')\n    plt.ylabel('AUC')\n    plt.legend()\n    plt.grid(True)\n\n    plt.tight_layout()\n    plt.show()\n\n# Plot the results (EarlyStopping restores best weights)\nplot_history(history)","metadata":{"_uuid":"ce0aae66-2e2a-4e53-82d7-aa2a2764b3cd","_cell_guid":"db409a43-103f-4d34-9346-e335f1a1503c","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-05-12T12:48:14.846183Z","iopub.execute_input":"2025-05-12T12:48:14.846457Z","iopub.status.idle":"2025-05-12T12:48:15.252635Z","shell.execute_reply.started":"2025-05-12T12:48:14.846440Z","shell.execute_reply":"2025-05-12T12:48:15.251885Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 11. Pre-Fine-Tuning Evaluation on Full Validation Set\n\nThe following evaluation uses the model weights obtained from the initial training phase (the best weights should have been restored by `EarlyStopping`). This serves as our baseline before fine-tuning.","metadata":{"_uuid":"95702905-623c-4aa4-b7e6-d2da3bb53d3c","_cell_guid":"3d3ce7e7-6521-4df2-a60b-f50ff67f7c19","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"from sklearn.metrics import roc_auc_score, accuracy_score, precision_score, recall_score, f1_score, confusion_matrix\nimport seaborn as sns # For a prettier confusion matrix\n\n# --- Important: Ensure val_dataset for evaluation covers all data and is ordered ---\n# If your original val_dataset was created with ordered=False, it's best to create\n# a new one for evaluation to ensure labels and predictions align perfectly.\n# Also, ensure we use all validation images.\n\nprint(\"Re-creating validation dataset for full evaluation (ordered)...\")\n# Use the same BATCH_SIZE as training, or adjust if needed for prediction memory/speed\n# For evaluation, is_training should be False, and ordered should be True.\nEVAL_BATCH_SIZE = BATCH_SIZE # Can be different from training BATCH_SIZE if desired\nval_eval_dataset = load_dataset(VAL_FILENAMES, labeled=True, ordered=True, is_training=False)\n\n# Calculate the correct number of steps to cover the entire validation set\nnum_validation_images = count_data_items(VAL_FILENAMES)\nVAL_EVAL_STEPS = (num_validation_images + EVAL_BATCH_SIZE - 1) // EVAL_BATCH_SIZE\nprint(f\"Total validation images: {num_validation_images}\")\nprint(f\"Evaluation batch size: {EVAL_BATCH_SIZE}\")\nprint(f\"Validation evaluation steps: {VAL_EVAL_STEPS}\")\n\n# --- Get True Labels ---\nprint(\"Extracting true labels from the validation dataset...\")\ny_true_val = []\n# .take(VAL_EVAL_STEPS) ensures we iterate through the entire dataset once\nfor images, labels in tqdm(val_eval_dataset.take(VAL_EVAL_STEPS), total=VAL_EVAL_STEPS):\n    y_true_val.extend(labels.numpy())\ny_true_val = np.array(y_true_val)\nprint(f\"Extracted {len(y_true_val)} true labels.\")\n\n# --- Get Model Predictions (Probabilities) ---\n# The model should already have the best weights loaded if EarlyStopping's restore_best_weights=True\n# Or, if you saved weights: model.load_weights(\"melanoma_best.weights.h5\")\nprint(\"Generating predictions on the full validation dataset...\")\n# model.predict will iterate through the dataset.\n# Providing 'steps' ensures it processes the correct amount of data if the dataset could be infinite (though ours is not here).\ny_pred_probs_val = model.predict(val_eval_dataset, steps=VAL_EVAL_STEPS, verbose=1)\n# Ensure predictions match the number of true labels\ny_pred_probs_val = y_pred_probs_val[:len(y_true_val)] # Trim if predict gives more due to batching\nprint(f\"Generated {len(y_pred_probs_val)} predictions.\")\n\n\n# --- Calculate Metrics ---\n# For AUC, we use the probabilities\nauc_val = roc_auc_score(y_true_val, y_pred_probs_val)\n\n# For other metrics, we need binary predictions (threshold at 0.5)\nTHRESHOLD = 0.5\ny_pred_binary_val = (y_pred_probs_val > THRESHOLD).astype(int).flatten() # flatten in case of (N,1) shape\n\naccuracy_val = accuracy_score(y_true_val, y_pred_binary_val)\nprecision_val = precision_score(y_true_val, y_pred_binary_val)\nrecall_val = recall_score(y_true_val, y_pred_binary_val)\nf1_val = f1_score(y_true_val, y_pred_binary_val)\ncm_val = confusion_matrix(y_true_val, y_pred_binary_val)\n\nprint(\"\\n--- Validation Set Evaluation Results ---\")\nprint(f\"AUC: {auc_val:.4f}\")\nprint(f\"Accuracy: {accuracy_val:.4f}\")\nprint(f\"Precision: {precision_val:.4f}\")\nprint(f\"Recall: {recall_val:.4f}\")\nprint(f\"F1-Score: {f1_val:.4f}\")\n\n# Plot Confusion Matrix\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm_val, annot=True, fmt='d', cmap='Blues',\n            xticklabels=['Benign (0)', 'Malignant (1)'],\n            yticklabels=['Benign (0)', 'Malignant (1)'])\nplt.title('Confusion Matrix - Validation Set')\nplt.xlabel('Predicted Label')\nplt.ylabel('True Label')\nplt.show()\n\n# ## End of Notebook","metadata":{"_uuid":"2a1b5211-d07c-4cf9-ad11-ecb6279a5006","_cell_guid":"49645405-2074-4f26-9699-320f9f94f16e","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-05-12T12:48:15.253462Z","iopub.execute_input":"2025-05-12T12:48:15.253812Z","iopub.status.idle":"2025-05-12T12:48:55.205960Z","shell.execute_reply.started":"2025-05-12T12:48:15.253782Z","shell.execute_reply":"2025-05-12T12:48:55.205172Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 12. Fine-Tuning the Model\n\nNow we will unfreeze some layers of the pre-trained base model (MobileNetV2) and continue training with a very low learning rate. This process is known as fine-tuning.","metadata":{}},{"cell_type":"code","source":"# --- Unfreeze Base Model Layers ---\n# First, let's ensure the best weights from the initial training are loaded\n# (EarlyStopping with restore_best_weights=True should have done this,\n# but explicitly loading can be safer if you ran cells out of order or changed callbacks)\nprint(\"Loading best weights from initial training: melanoma_best.weights.h5\")\nmodel.load_weights(\"melanoma_best.weights.h5\")\n\nbase_model = model.layers[0] # Get the base MobileNetV2 model\nbase_model.trainable = True\n\n# Let's see how many layers are in the base model\nprint(f\"Number of layers in the base model: {len(base_model.layers)}\")\n\n# Fine-tune from this layer onwards.\n# For MobileNetV2, a common strategy is to unfreeze the top blocks.\n# Let's try to unfreeze layers from 'block_13_expand' onwards.\n# You can inspect `base_model.summary()` to choose a different layer.\nfine_tune_at_layer_name = 'block_13_expand'\nfine_tune_at_index = -1\n\nfor i, layer in enumerate(base_model.layers):\n    if layer.name == fine_tune_at_layer_name:\n        fine_tune_at_index = i\n        break\n\nif fine_tune_at_index != -1:\n    print(f\"Found layer '{fine_tune_at_layer_name}' at index {fine_tune_at_index}.\")\n    # Freeze all layers before the `fine_tune_at_layer_name`\n    for layer in base_model.layers[:fine_tune_at_index]:\n        layer.trainable = False\n    print(f\"Froze layers before index {fine_tune_at_index}. Layers from '{fine_tune_at_layer_name}' onwards are trainable.\")\nelse:\n    print(f\"Layer '{fine_tune_at_layer_name}' not found. Unfreezing last 20 layers as a fallback.\")\n    # Fallback: unfreeze the last, e.g., 20 layers. Adjust as needed.\n    num_layers_to_unfreeze = 20\n    for layer in base_model.layers[:-num_layers_to_unfreeze]:\n        layer.trainable = False\n\n# --- Re-compile the Model for Fine-Tuning ---\n# Use a much lower learning rate for fine-tuning\nFT_LEARNING_RATE = 1e-5 # Original Adam LR was 1e-3\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=FT_LEARNING_RATE),\n    loss='binary_crossentropy',\n    metrics=[tf.keras.metrics.AUC(name='auc')]\n)\n\nmodel.summary() # Verify trainable params have changed\n\n# --- Define Callbacks for Fine-Tuning ---\n# Use new callback instances for fine-tuning\ncallback_early_stopping_ft = tf.keras.callbacks.EarlyStopping(\n    monitor='val_auc',\n    patience=10, # Shorter patience for fine-tuning\n    mode='max',\n    verbose=1,\n    restore_best_weights=True\n)\n\ncallback_lr_reduce_ft = tf.keras.callbacks.ReduceLROnPlateau(\n    monitor='val_auc',\n    factor=0.2, # Can be a bit more aggressive or similar\n    patience=3,  # Shorter patience\n    mode='max',\n    verbose=1,\n    min_lr=1e-7 # Even lower min LR\n)\n\ncallback_checkpoint_ft = tf.keras.callbacks.ModelCheckpoint(\n    \"melanoma_best_finetuned.weights.h5\", # New checkpoint file\n    monitor='val_auc',\n    mode='max',\n    save_best_only=True,\n    save_weights_only=True,\n    verbose=0\n)\n\n# --- Train for Fine-Tuning ---\nEPOCHS_FINE_TUNE = 20 # Number of epochs for fine-tuning (EarlyStopping will likely stop it sooner)\n# Initial epoch should be set to the number of epochs already trained if you want continuous history plots,\n# but since we are plotting fine-tuning separately, we can start from 0.\n# For fine-tuning, we continue from where the previous training left off in terms of epochs.\n# However, history objects are separate, so `initial_epoch` is mostly for logging.\n# Let's get the number of epochs from the previous training.\ninitial_epochs_trained = len(history.epoch)\n\nprint(f\"\\nStarting fine-tuning for {EPOCHS_FINE_TUNE} epochs...\")\nhistory_fine_tune = model.fit(\n    train_dataset,\n    epochs=initial_epochs_trained + EPOCHS_FINE_TUNE, # Total epochs\n    initial_epoch=initial_epochs_trained,        # Start from where previous training ended\n    steps_per_epoch=STEPS_PER_EPOCH_TRAIN,\n    validation_data=val_dataset,\n    validation_steps=None, # Evaluate on the full validation dataset each time\n    callbacks=[callback_early_stopping_ft, callback_lr_reduce_ft, callback_checkpoint_ft],\n    class_weight=class_weight, # Continue using class weights\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T12:48:55.206905Z","iopub.execute_input":"2025-05-12T12:48:55.207534Z","iopub.status.idle":"2025-05-12T12:51:48.463960Z","shell.execute_reply.started":"2025-05-12T12:48:55.207508Z","shell.execute_reply":"2025-05-12T12:51:48.463147Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 13. Plot Fine-Tuning History\n\nLet's visualize the training progress during the fine-tuning phase.","metadata":{}},{"cell_type":"code","source":"# The plot_history function is already defined. We just need to call it.\n# Note: The 'epochs' in this plot will be relative to the start of fine-tuning if initial_epoch was used correctly.\n# If not, and you want the x-axis to represent total epochs, you might need to adjust the plot_history function\n# or the epoch numbers passed to it. The current plot_history uses range(1, len(hist['loss']) + 1).\n\nprint(\"Plotting fine-tuning training history...\")\n\n# To make the plot x-axis represent the actual epochs (continued from initial training)\n# We can adjust the history object slightly for plotting if needed, or adjust the plot function.\n# For simplicity, let's plot it as is; the title will clarify it's fine-tuning.\n\ndef plot_fine_tuning_history(history, initial_epochs_trained):\n    hist = history.history\n    # Create an epoch range that starts after the initial training\n    epochs = range(initial_epochs_trained + 1, initial_epochs_trained + len(hist['loss']) + 1)\n\n    plt.figure(figsize=(12, 5))\n\n    # Plot Loss\n    plt.subplot(1, 2, 1)\n    plt.plot(epochs, hist['loss'], 'bo-', label='Training loss (FT)')\n    plt.plot(epochs, hist['val_loss'], 'ro-', label='Validation loss (FT)')\n    plt.title('Fine-Tuning: Training and Validation Loss')\n    plt.xlabel('Epochs (Total)')\n    plt.ylabel('Loss')\n    plt.legend()\n    plt.grid(True)\n\n    # Plot AUC\n    plt.subplot(1, 2, 2)\n    plt.plot(epochs, hist['auc'], 'bo-', label='Training AUC (FT)')\n    plt.plot(epochs, hist['val_auc'], 'ro-', label='Validation AUC (FT)')\n    plt.title('Fine-Tuning: Training and Validation AUC')\n    plt.xlabel('Epochs (Total)')\n    plt.ylabel('AUC')\n    plt.legend()\n    plt.grid(True)\n\n    plt.tight_layout()\n    plt.show()\n\nplot_fine_tuning_history(history_fine_tune, initial_epochs_trained)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T12:51:48.465085Z","iopub.execute_input":"2025-05-12T12:51:48.465330Z","iopub.status.idle":"2025-05-12T12:51:48.838092Z","shell.execute_reply.started":"2025-05-12T12:51:48.465306Z","shell.execute_reply":"2025-05-12T12:51:48.837290Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 14. Evaluate Model After Fine-Tuning\n\nNow, we evaluate the fine-tuned model on the full validation set to see if performance has improved.\nEarlyStopping should have restored the best weights from the fine-tuning phase.\nAlternatively, we can load `melanoma_best_finetuned.weights.h5`.","metadata":{}},{"cell_type":"code","source":"# Ensure the model has the best fine-tuned weights\n# (restore_best_weights=True in callback_early_stopping_ft should handle this)\n# For certainty, or if you want to run this cell independently:\nprint(\"Loading best weights from fine-tuning: melanoma_best_finetuned.weights.h5\")\nmodel.load_weights(\"melanoma_best_finetuned.weights.h5\")\n\n\n# The val_eval_dataset and y_true_val are already prepared from Section 11.\n# We just need to get new predictions.\n# VAL_EVAL_STEPS is also already calculated.\n\nprint(\"Generating predictions on the full validation dataset with the fine-tuned model...\")\ny_pred_probs_val_ft = model.predict(val_eval_dataset, steps=VAL_EVAL_STEPS, verbose=1)\n# Ensure predictions match the number of true labels\ny_pred_probs_val_ft = y_pred_probs_val_ft[:len(y_true_val)]\nprint(f\"Generated {len(y_pred_probs_val_ft)} predictions after fine-tuning.\")\n\n# --- Calculate Metrics for Fine-Tuned Model ---\nauc_val_ft = roc_auc_score(y_true_val, y_pred_probs_val_ft)\n\n# For other metrics, use the same THRESHOLD = 0.5\ny_pred_binary_val_ft = (y_pred_probs_val_ft > THRESHOLD).astype(int).flatten()\n\naccuracy_val_ft = accuracy_score(y_true_val, y_pred_binary_val_ft)\nprecision_val_ft = precision_score(y_true_val, y_pred_binary_val_ft)\nrecall_val_ft = recall_score(y_true_val, y_pred_binary_val_ft)\nf1_val_ft = f1_score(y_true_val, y_pred_binary_val_ft)\ncm_val_ft = confusion_matrix(y_true_val, y_pred_binary_val_ft)\n\nprint(\"\\n--- Validation Set Evaluation Results (After Fine-Tuning) ---\")\nprint(f\"AUC: {auc_val_ft:.4f}\")\nprint(f\"Accuracy: {accuracy_val_ft:.4f}\")\nprint(f\"Precision: {precision_val_ft:.4f}\")\nprint(f\"Recall: {recall_val_ft:.4f}\")\nprint(f\"F1-Score: {f1_val_ft:.4f}\")\n\n# Plot Confusion Matrix for Fine-Tuned Model\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm_val_ft, annot=True, fmt='d', cmap='Greens', # Changed cmap for distinction\n            xticklabels=['Benign (0)', 'Malignant (1)'],\n            yticklabels=['Benign (0)', 'Malignant (1)'])\nplt.title('Confusion Matrix - Validation Set (After Fine-Tuning)')\nplt.xlabel('Predicted Label')\nplt.ylabel('True Label')\nplt.show()\n\n# --- Optional: Compare Pre-FT and Post-FT metrics ---\nprint(\"\\n--- Comparison of Validation Metrics ---\")\nprint(f\"Metric         | Pre-FT  | Post-FT\")\nprint(f\"----------------|---------|---------\")\nprint(f\"AUC             | {auc_val:.4f}   | {auc_val_ft:.4f}\")\nprint(f\"Accuracy        | {accuracy_val:.4f}   | {accuracy_val_ft:.4f}\")\nprint(f\"Precision       | {precision_val:.4f}   | {precision_val_ft:.4f}\")\nprint(f\"Recall          | {recall_val:.4f}   | {recall_val_ft:.4f}\")\nprint(f\"F1-Score        | {f1_val:.4f}   | {f1_val_ft:.4f}\")\n\n# Clean up memory (optional, but good practice if notebook is long)\ndel history_fine_tune\ndel y_pred_binary_val_ft","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T12:51:48.840456Z","iopub.execute_input":"2025-05-12T12:51:48.841073Z","iopub.status.idle":"2025-05-12T12:52:11.860756Z","shell.execute_reply.started":"2025-05-12T12:51:48.841045Z","shell.execute_reply":"2025-05-12T12:52:11.859989Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- SAVE DATA FOR THRESHOLD OPTIMIZATION ---\n# Ensure y_true_val and y_pred_probs_val_ft contain the final validation labels and predictions\n# from the best model (likely the fine-tuned one based on your metrics comparison)\n\nprint(\"Saving validation true labels and predicted probabilities...\")\n\n# Convert to numpy arrays if they aren't already\ny_true_val_np = np.array(y_true_val)\ny_pred_probs_val_ft_np = np.array(y_pred_probs_val_ft).flatten() # Ensure it's 1D\n\n# Save the arrays\nnp.save('y_true_val.npy', y_true_val_np)\nnp.save('y_pred_probs_val_ft.npy', y_pred_probs_val_ft_np)\n\nprint(\"Data saved successfully: y_true_val.npy, y_pred_probs_val_ft.npy\")\n# Now commit this notebook, and its output files will be available to add as input to a new notebook.","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T12:52:11.861488Z","iopub.execute_input":"2025-05-12T12:52:11.861720Z","iopub.status.idle":"2025-05-12T12:52:11.867214Z","shell.execute_reply.started":"2025-05-12T12:52:11.861704Z","shell.execute_reply":"2025-05-12T12:52:11.866656Z"}},"outputs":[],"execution_count":null}]}