{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":11848,"databundleVersionId":862157,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"51690df7","cell_type":"markdown","source":"\n# Histopathologic Cancer Detection — CNN & Transfer Learning\n\nThis notebook tackles the **Histopathologic Cancer Detection** Kaggle competition.\n\n- **Task:** Binary image classification – predict whether a 96×96 histopathology patch contains metastatic cancer (`label = 1`) or not (`label = 0`).\n- **Data:** ~220k labeled train images (`train/` + `train_labels.csv`) and ~57k unlabeled test images (`test/`). Each image is a 96×96 RGB `.tif` file.\n- **Metric:** ROC AUC on the test set.\n\n### Plan\n\n1. Load and explore the data (EDA).\n2. Build a TensorFlow `tf.data` pipeline for efficient training.\n3. Train **Model A**: a small CNN from scratch.\n4. Train **Model B**: an EfficientNetB0-based transfer learning model.\n5. Compare results (training curves, validation AUC).\n6. Briefly explore a hyperparameter tweak.\n7. Generate a Kaggle submission file.\n8. Summarize conclusions and next steps.\n","metadata":{}},{"id":"13bf8002","cell_type":"code","source":"\nimport os\nimport random\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom PIL import Image\n\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models\n\n# For nicer plots\nplt.style.use(\"seaborn-v0_8\")\n\n# Make runs reproducible-ish\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\nDATA_DIR = \"/kaggle/input/histopathologic-cancer-detection\"\n\nIMG_SIZE = 96\nBATCH_SIZE = 64\nVAL_SPLIT = 0.2\n\n# Optional: use a subset of training data for faster experimentation.\n# Set to None to use the full dataset.\nSUBSET_SIZE = 30000  # e.g., 30000 for dev; set to None for full data\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:20:20.043601Z","iopub.execute_input":"2025-11-25T03:20:20.043879Z","iopub.status.idle":"2025-11-25T03:20:35.783926Z","shell.execute_reply.started":"2025-11-25T03:20:20.043858Z","shell.execute_reply":"2025-11-25T03:20:35.78314Z"}},"outputs":[],"execution_count":null},{"id":"498ab9e7","cell_type":"markdown","source":"## 1. Data loading and basic exploration","metadata":{}},{"id":"cb030b1c","cell_type":"code","source":"\nlabels = pd.read_csv(os.path.join(DATA_DIR, \"train_labels.csv\"))\nprint(\"Head of labels:\")\ndisplay(labels.head())\n\nprint(\"\\nBasic info:\")\nprint(\"Total rows:\", len(labels))\nprint(\"Unique ids:\", labels[\"id\"].nunique())\nprint(\"Missing values:\\n\", labels.isna().sum())\n\nclass_counts = labels[\"label\"].value_counts()\nclass_ratio = labels[\"label\"].value_counts(normalize=True)\nprint(\"\\nClass counts:\\n\", class_counts)\nprint(\"\\nClass ratios:\\n\", class_ratio)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:20:35.785218Z","iopub.execute_input":"2025-11-25T03:20:35.785726Z","iopub.status.idle":"2025-11-25T03:20:36.235564Z","shell.execute_reply.started":"2025-11-25T03:20:35.785694Z","shell.execute_reply":"2025-11-25T03:20:36.234892Z"}},"outputs":[],"execution_count":null},{"id":"cbc0346c","cell_type":"code","source":"\nplt.figure(figsize=(4,4))\nclass_counts.plot(kind=\"bar\")\nplt.xticks([0,1], [\"No tumor (0)\", \"Tumor (1)\"], rotation=0)\nplt.title(\"Class distribution\")\nplt.ylabel(\"Count\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:20:36.236292Z","iopub.execute_input":"2025-11-25T03:20:36.236553Z","iopub.status.idle":"2025-11-25T03:20:36.462219Z","shell.execute_reply.started":"2025-11-25T03:20:36.236536Z","shell.execute_reply":"2025-11-25T03:20:36.461469Z"}},"outputs":[],"execution_count":null},{"id":"a595fc1e","cell_type":"markdown","source":"\nThe dataset is somewhat imbalanced (more negatives than positives) but not severely so.  \nThere are no missing labels, and IDs appear unique.\n","metadata":{}},{"id":"8687d91d","cell_type":"code","source":"\n# Check image shape\nsample_id = labels[\"id\"].iloc[0]\nsample_path = os.path.join(DATA_DIR, \"train\", sample_id + \".tif\")\nsample_img = np.array(Image.open(sample_path))\nprint(\"Sample image shape:\", sample_img.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:20:36.463075Z","iopub.execute_input":"2025-11-25T03:20:36.463418Z","iopub.status.idle":"2025-11-25T03:20:36.624908Z","shell.execute_reply.started":"2025-11-25T03:20:36.463395Z","shell.execute_reply":"2025-11-25T03:20:36.62425Z"}},"outputs":[],"execution_count":null},{"id":"0dde577c","cell_type":"code","source":"\ndef show_examples(df, label, n=16):\n    # Show n example images for a given label.\n    subset = df[df[\"label\"] == label].sample(n, random_state=SEED)\n    cols = 4\n    rows = n // cols\n    plt.figure(figsize=(10, 10))\n    for i, (_, row) in enumerate(subset.iterrows()):\n        img = Image.open(os.path.join(DATA_DIR, \"train\", row[\"id\"] + \".tif\"))\n        plt.subplot(rows, cols, i+1)\n        plt.imshow(img)\n        plt.axis(\"off\")\n    plt.suptitle(f\"Examples, label={label}\")\n    plt.show()\n\nshow_examples(labels, 0)\nshow_examples(labels, 1)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:20:36.626385Z","iopub.execute_input":"2025-11-25T03:20:36.626634Z","iopub.status.idle":"2025-11-25T03:20:38.27972Z","shell.execute_reply.started":"2025-11-25T03:20:36.626618Z","shell.execute_reply":"2025-11-25T03:20:38.278662Z"}},"outputs":[],"execution_count":null},{"id":"9f99654f","cell_type":"markdown","source":"\nVisually, tumor patches (label 1) tend to have darker, irregular regions compared to more uniform non-tumor patches (label 0).\n","metadata":{}},{"id":"fca481f7","cell_type":"markdown","source":"\n## 2. Data pipeline with `tf.data`\n\nWe now build a TensorFlow `tf.data` pipeline:\n\n- Start from image paths + numeric labels.\n- Decode `.tif` images, resize to 96×96.\n- Normalize pixel values to [0, 1].\n- Apply simple augmentations (random flips) for the training split.\n- Shuffle, batch, and prefetch to keep the GPU fed.\n","metadata":{}},{"id":"4cc79ed9","cell_type":"code","source":"# Attach filenames and (optionally) use a subset for faster experiments\nlabels_full = labels.copy()\nlabels_full[\"filename\"] = labels_full[\"id\"] + \".tif\"\nlabels_full[\"label\"] = labels_full[\"label\"].astype(\"float32\")\n\n# Optional subset to speed things up while developing\nSUBSET_SIZE = 30000  # or None for full data\nif SUBSET_SIZE is not None and SUBSET_SIZE < len(labels_full):\n    labels_full = labels_full.sample(SUBSET_SIZE, random_state=SEED).reset_index(drop=True)\n    print(f\"Using subset of size {len(labels_full)} for training/validation.\")\nelse:\n    print(f\"Using full dataset of size {len(labels_full)}.\")\n\npaths = labels_full[\"filename\"].apply(lambda f: os.path.join(DATA_DIR, \"train\", f)).values\nlabels_np = labels_full[\"label\"].values\n\nds = tf.data.Dataset.from_tensor_slices((paths, labels_np))\n\ndef _load_image_pil(path):\n    # path is a tf.EagerTensor; convert to python string\n    path = path.numpy().decode(\"utf-8\")\n    img = Image.open(path).convert(\"RGB\")          # PIL supports .tif\n    img = img.resize((IMG_SIZE, IMG_SIZE))\n    arr = np.array(img).astype(\"float32\") / 255.0\n    return arr\n\ndef load_and_preprocess(path, label):\n    # Wrap the PIL loader in tf.py_function\n    img = tf.py_function(_load_image_pil, [path], Tout=tf.float32)\n    # Tell TF the static shape\n    img.set_shape((IMG_SIZE, IMG_SIZE, 3))\n\n    # Light augmentations\n    img = tf.image.random_flip_left_right(img)\n    img = tf.image.random_flip_up_down(img)\n    return img, label\n\nds = ds.shuffle(10000, reshuffle_each_iteration=True)\nds = ds.map(load_and_preprocess, num_parallel_calls=tf.data.AUTOTUNE)\n\nval_size = int(len(labels_full) * VAL_SPLIT)\nval_ds = ds.take(val_size)\ntrain_ds = ds.skip(val_size)\n\ntrain_ds = train_ds.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)\nval_ds   = val_ds.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)\n\nprint(\"Train batches per epoch:\", len(train_ds))\nprint(\"Val batches per epoch:\", len(val_ds))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:20:38.280912Z","iopub.execute_input":"2025-11-25T03:20:38.281224Z","iopub.status.idle":"2025-11-25T03:20:40.572094Z","shell.execute_reply.started":"2025-11-25T03:20:38.2812Z","shell.execute_reply":"2025-11-25T03:20:40.57145Z"}},"outputs":[],"execution_count":null},{"id":"d17c54de","cell_type":"code","source":"\ndef plot_history(history, title=\"\"):\n    # Plot training & validation loss and AUC over epochs.\n    plt.figure(figsize=(12, 4))\n\n    # Loss\n    plt.subplot(1, 2, 1)\n    plt.plot(history.history.get(\"loss\", []), label=\"train\")\n    plt.plot(history.history.get(\"val_loss\", []), label=\"val\")\n    plt.title(f\"{title} loss\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Loss\")\n    plt.legend()\n\n    # AUC\n    if \"auc\" in history.history and \"val_auc\" in history.history:\n        plt.subplot(1, 2, 2)\n        plt.plot(history.history[\"auc\"], label=\"train\")\n        plt.plot(history.history[\"val_auc\"], label=\"val\")\n        plt.title(f\"{title} AUC\")\n        plt.xlabel(\"Epoch\")\n        plt.ylabel(\"AUC\")\n        plt.legend()\n\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:20:40.572843Z","iopub.execute_input":"2025-11-25T03:20:40.573092Z","iopub.status.idle":"2025-11-25T03:20:40.578754Z","shell.execute_reply.started":"2025-11-25T03:20:40.573069Z","shell.execute_reply":"2025-11-25T03:20:40.578178Z"}},"outputs":[],"execution_count":null},{"id":"0267695c","cell_type":"markdown","source":"\n## 3. Model A — Simple CNN from scratch\n\nAs a baseline, we build a relatively small CNN:\n\n- 3 convolution + max-pooling blocks.\n- Flatten + dense layer.\n- Dropout for regularization.\n- Sigmoid output for binary classification.\n\nWe train it from scratch on the histopathology patches.\n","metadata":{}},{"id":"5d4fe48c","cell_type":"code","source":"\ndef make_simple_cnn(input_shape=(IMG_SIZE, IMG_SIZE, 3)):\n    model = models.Sequential([\n        layers.Input(shape=input_shape),\n        layers.Conv2D(32, (3,3), activation=\"relu\"),\n        layers.MaxPooling2D(),\n        layers.Conv2D(64, (3,3), activation=\"relu\"),\n        layers.MaxPooling2D(),\n        layers.Conv2D(128, (3,3), activation=\"relu\"),\n        layers.MaxPooling2D(),\n        layers.Flatten(),\n        layers.Dropout(0.5),\n        layers.Dense(256, activation=\"relu\"),\n        layers.Dense(1, activation=\"sigmoid\"),\n    ])\n    return model\n\nmodel_a = make_simple_cnn()\nmodel_a.summary()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:20:40.579395Z","iopub.execute_input":"2025-11-25T03:20:40.579742Z","iopub.status.idle":"2025-11-25T03:20:41.36433Z","shell.execute_reply.started":"2025-11-25T03:20:40.57971Z","shell.execute_reply":"2025-11-25T03:20:41.363805Z"}},"outputs":[],"execution_count":null},{"id":"77cda645","cell_type":"code","source":"\nmodel_a.compile(\n    optimizer=tf.keras.optimizers.Adam(1e-3),\n    loss=\"binary_crossentropy\",\n    metrics=[\n        tf.keras.metrics.AUC(name=\"auc\"),\n        tf.keras.metrics.BinaryAccuracy(name=\"accuracy\"),\n    ],\n)\n\nEPOCHS_A = 8  # adjust as needed\n\nhistory_a = model_a.fit(\n    train_ds,\n    epochs=EPOCHS_A,\n    validation_data=val_ds,\n)\n\nplot_history(history_a, \"Model A (Simple CNN)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:20:41.365043Z","iopub.execute_input":"2025-11-25T03:20:41.365255Z","iopub.status.idle":"2025-11-25T03:29:30.983836Z","shell.execute_reply.started":"2025-11-25T03:20:41.365239Z","shell.execute_reply":"2025-11-25T03:29:30.983205Z"}},"outputs":[],"execution_count":null},{"id":"5e761fa8","cell_type":"markdown","source":"\nModel A achieves a baseline validation AUC and loss (see the plots above).  \nWe can use this as a reference to compare against a transfer learning model.\n","metadata":{}},{"id":"025acb2a","cell_type":"markdown","source":"\n## 4. Model B — EfficientNetB0 Transfer Learning\n\nNext, we use a pretrained EfficientNetB0 (trained on ImageNet) as a feature extractor:\n\n- Load EfficientNetB0 without its top classifier.\n- Freeze its weights initially.\n- Add a global average pooling layer, dropout, and a final dense sigmoid layer.\n- Train only the new classification head first.\n","metadata":{}},{"id":"d925275f","cell_type":"code","source":"\nfrom tensorflow.keras.applications import EfficientNetB0\n\nbase_model = EfficientNetB0(\n    include_top=False,\n    weights=\"imagenet\",\n    input_shape=(IMG_SIZE, IMG_SIZE, 3),\n)\nbase_model.trainable = False  # start frozen\n\ninputs = layers.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\nx = base_model(inputs, training=False)\nx = layers.GlobalAveragePooling2D()(x)\nx = layers.Dropout(0.3)(x)\noutputs = layers.Dense(1, activation=\"sigmoid\")(x)\nmodel_b = models.Model(inputs, outputs, name=\"EfficientNetB0_transfer\")\n\nmodel_b.summary()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:29:30.984532Z","iopub.execute_input":"2025-11-25T03:29:30.984784Z","iopub.status.idle":"2025-11-25T03:29:32.375804Z","shell.execute_reply.started":"2025-11-25T03:29:30.984758Z","shell.execute_reply":"2025-11-25T03:29:32.375054Z"}},"outputs":[],"execution_count":null},{"id":"d40fb9e4","cell_type":"code","source":"\nmodel_b.compile(\n    optimizer=tf.keras.optimizers.Adam(1e-4),\n    loss=\"binary_crossentropy\",\n    metrics=[\n        tf.keras.metrics.AUC(name=\"auc\"),\n        tf.keras.metrics.BinaryAccuracy(name=\"accuracy\"),\n    ],\n)\n\nEPOCHS_B = 6  # head-only training\n\nhistory_b = model_b.fit(\n    train_ds,\n    epochs=EPOCHS_B,\n    validation_data=val_ds,\n)\n\nplot_history(history_b, \"Model B (EfficientNetB0 head)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:29:32.376608Z","iopub.execute_input":"2025-11-25T03:29:32.376918Z","iopub.status.idle":"2025-11-25T03:36:26.215012Z","shell.execute_reply.started":"2025-11-25T03:29:32.3769Z","shell.execute_reply":"2025-11-25T03:36:26.214287Z"}},"outputs":[],"execution_count":null},{"id":"32f14054","cell_type":"markdown","source":"\n### 4.1 Fine-tuning EfficientNet\n\nAfter training the new classification head, we can unfreeze the top layers of EfficientNet and fine-tune at a lower learning rate.\n","metadata":{}},{"id":"98fb316a","cell_type":"code","source":"\n# Unfreeze the top portion of the base model for fine-tuning\nbase_model.trainable = True\n\nfine_tune_at = len(base_model.layers) * 3 // 4  # unfreeze top ~25%\nfor layer in base_model.layers[:fine_tune_at]:\n    layer.trainable = False\n\nmodel_b.compile(\n    optimizer=tf.keras.optimizers.Adam(1e-5),\n    loss=\"binary_crossentropy\",\n    metrics=[\n        tf.keras.metrics.AUC(name=\"auc\"),\n        tf.keras.metrics.BinaryAccuracy(name=\"accuracy\"),\n    ],\n)\n\nEPOCHS_FINE = 4  # a few extra epochs\n\nhistory_b_fine = model_b.fit(\n    train_ds,\n    epochs=EPOCHS_FINE,\n    validation_data=val_ds,\n)\n\nplot_history(history_b_fine, \"Model B (fine-tuned)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:36:26.215686Z","iopub.execute_input":"2025-11-25T03:36:26.215881Z","iopub.status.idle":"2025-11-25T03:41:30.296207Z","shell.execute_reply.started":"2025-11-25T03:36:26.215866Z","shell.execute_reply":"2025-11-25T03:41:30.295527Z"}},"outputs":[],"execution_count":null},{"id":"346650b0","cell_type":"markdown","source":"\nWe can compare the final validation AUC and loss of Model A and Model B (including fine-tuning) to see which approach performs better.\n","metadata":{}},{"id":"67778e88","cell_type":"markdown","source":"\n## 5. Simple hyperparameter experiment\n\nAs a small hyperparameter experiment, we train Model A again with a lower learning rate (`1e-4`) for more stable convergence and compare validation AUC.\n","metadata":{}},{"id":"9fbf43ef","cell_type":"code","source":"\nmodel_a_lr_low = make_simple_cnn()\n\nmodel_a_lr_low.compile(\n    optimizer=tf.keras.optimizers.Adam(1e-4),\n    loss=\"binary_crossentropy\",\n    metrics=[\n        tf.keras.metrics.AUC(name=\"auc\"),\n        tf.keras.metrics.BinaryAccuracy(name=\"accuracy\"),\n    ],\n)\n\nEPOCHS_A_LOWLR = 8\n\nhistory_a_lr_low = model_a_lr_low.fit(\n    train_ds,\n    epochs=EPOCHS_A_LOWLR,\n    validation_data=val_ds,\n)\n\nplot_history(history_a_lr_low, \"Model A (lr=1e-4)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:41:30.296964Z","iopub.execute_input":"2025-11-25T03:41:30.297216Z","iopub.status.idle":"2025-11-25T03:51:32.889601Z","shell.execute_reply.started":"2025-11-25T03:41:30.297192Z","shell.execute_reply":"2025-11-25T03:51:32.888997Z"}},"outputs":[],"execution_count":null},{"id":"bc8a4a18","cell_type":"markdown","source":"\nYou can compare:\n\n- Model A (lr=1e-3) vs Model A (lr=1e-4)  \n- Model A vs Model B (head-only) vs Model B (fine-tuned)\n\nFor the assignment, it is enough to discuss which configuration achieved the **best validation AUC** and what you observed about overfitting or underfitting from the curves.\n","metadata":{}},{"id":"6354ba16","cell_type":"markdown","source":"\n## 6. Kaggle submission\n\nFinally, we generate predictions on the test set using the best-performing model (for example, the fine-tuned EfficientNetB0) and create a `submission.csv` file.\n","metadata":{}},{"id":"03ad9e9e","cell_type":"code","source":"# Choose the best model. Here we assume model_b (fine-tuned) performed best.\nbest_model = model_b\n\n# Build a test dataset (no labels, no random augmentation)\ntest_dir = os.path.join(DATA_DIR, \"test\")\ntest_filenames = sorted(os.listdir(test_dir))\ntest_paths = [os.path.join(test_dir, fname) for fname in test_filenames]\n\ntest_ds = tf.data.Dataset.from_tensor_slices(test_paths)\n\ndef _load_image_pil_test(path):\n    # path is a tf.EagerTensor -> convert to Python string\n    path = path.numpy().decode(\"utf-8\")\n    img = Image.open(path).convert(\"RGB\")        # PIL supports .tif\n    img = img.resize((IMG_SIZE, IMG_SIZE))\n    arr = np.array(img).astype(\"float32\") / 255.0\n    return arr\n\ndef load_and_preprocess_test(path):\n    # wrap PIL loader\n    img = tf.py_function(_load_image_pil_test, [path], Tout=tf.float32)\n    img.set_shape((IMG_SIZE, IMG_SIZE, 3))\n    return img\n\ntest_ds = test_ds.map(load_and_preprocess_test,\n                      num_parallel_calls=tf.data.AUTOTUNE)\ntest_ds = test_ds.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)\n\ntest_preds = best_model.predict(test_ds).ravel()\n\nsubmission = pd.DataFrame({\n    \"id\": [os.path.splitext(fname)[0] for fname in test_filenames],\n    \"label\": test_preds,\n})\n\nsubmission.head()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:51:32.891623Z","iopub.execute_input":"2025-11-25T03:51:32.892081Z","iopub.status.idle":"2025-11-25T03:55:56.662828Z","shell.execute_reply.started":"2025-11-25T03:51:32.892061Z","shell.execute_reply":"2025-11-25T03:55:56.662145Z"}},"outputs":[],"execution_count":null},{"id":"b36600f8","cell_type":"code","source":"# Rebuild submission DataFrame (must run before saving)\n\nsubmission = pd.DataFrame({\n    \"id\": [os.path.splitext(fname)[0] for fname in test_filenames],\n    \"label\": test_preds,   # <-- ensure test_preds exists!\n})\n\nsubmission.head()\n\n# Save submission file\nsubmission_path = \"submission.csv\"\nsubmission.to_csv(submission_path, index=False)\nprint(f\"Saved submission to {submission_path}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T03:55:56.663493Z","iopub.execute_input":"2025-11-25T03:55:56.663856Z","iopub.status.idle":"2025-11-25T03:55:56.860977Z","shell.execute_reply.started":"2025-11-25T03:55:56.663837Z","shell.execute_reply":"2025-11-25T03:55:56.860178Z"}},"outputs":[],"execution_count":null},{"id":"9ee9639f","cell_type":"markdown","source":"\n## 7. Conclusion\n\nIn this notebook, we:\n\n- Formulated histopathologic cancer detection as a binary image classification problem.\n- Explored the dataset, checked class balance, and visualized example images.\n- Built an efficient `tf.data` pipeline with on-the-fly decoding, augmentation, batching, and prefetching.\n- Trained a baseline CNN from scratch (Model A).\n- Trained and fine-tuned a transfer learning model based on EfficientNetB0 (Model B).\n- Compared validation loss and AUC for multiple configurations, including a simple learning rate experiment.\n- Generated a Kaggle submission file from the best-performing model.\n\nFor further improvement one could try:\n- Stronger and more diverse data augmentations.\n- Training on the full dataset (if using a subset).\n- Ensembling multiple models.\n- Using focal loss to focus on hard-to-classify examples.\n","metadata":{}}]}