{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"},"colab":{"provenance":[],"gpuType":"T4"},"accelerator":"GPU"},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🧬 Cancer Detection — 90%+ Model\n> Based on the same structure as the reference notebook that exceeds 90%.\n> Key changes: 96×96 image size, 9 Conv2D layers, BatchNorm after each conv, lr=0.0001 + ReduceLR.\n","metadata":{"id":"luDkwmuwM-0a"}},{"cell_type":"markdown","source":"## Cell 1 — GPU Check\n","metadata":{"id":"WeMzHjaiM-0d"}},{"cell_type":"code","source":"import tensorflow as tf\ngpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    print(f'✅ GPU détecté : {gpus[0].name}')\nelse:\n    print('❌ Aucun GPU — va dans : Exécution → Modifier le type d\\'exécution → GPU T4')","metadata":{"id":"qCXDa3MWM-0e","outputId":"6c3c147b-bf1d-4e24-cb6a-f015157f77d3"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 2 — Imports\n","metadata":{"id":"Ydz4LCejM-0f"}},{"cell_type":"code","source":"import os\nimport time\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib.image as mpimg\nimport seaborn as sns\nimport cv2\nfrom tqdm import tqdm\n\nfrom sklearn.metrics import confusion_matrix, classification_report\n\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers, models, regularizers\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n\nimport warnings\nwarnings.filterwarnings('ignore')\ntf.random.set_seed(42)\nnp.random.seed(42)\n\nprint('✅ Imports OK — TensorFlow', tf.__version__)","metadata":{"id":"NU-DmycWM-0g","outputId":"54f20229-f5b7-405a-8389-f3c9264c16c2"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 3 — Google Drive\n","metadata":{"id":"mqVn3GRwM-0g"}},{"cell_type":"code","source":"from google.colab import drive\ndrive.mount('/content/drive')\nprint('✅ Drive monté')","metadata":{"id":"unfvMfq-M-0h","outputId":"bfec09fa-726f-4a65-cbaa-76bda3ca2e54"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 4 — Unzipping the Images\n","metadata":{"id":"QF3Q2rK8M-0h"}},{"cell_type":"code","source":"import zipfile\n\nzip_path     = '/content/drive/MyDrive/PFE_cancer/sorted_data.zip'\nextract_path = '/content/dataset'\n\nif not os.path.exists(extract_path):\n    print('📦 Décompression en cours...')\n    with zipfile.ZipFile(zip_path, 'r') as zip_ref:\n        zip_ref.extractall(extract_path)\n    print('✅ Décompression terminée !')\nelse:\n    print('✅ Dataset déjà extrait, on passe directement.')","metadata":{"id":"oAOTg8VWM-0h","outputId":"c755e4c7-d34c-4074-ed89-842fe2e34af5"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 5 — Configuration (⚠️ key parameters)\n","metadata":{"id":"FIgWlkiYM-0i"}},{"cell_type":"code","source":"# ================================================================\n# ⚙️  PARAMÈTRES — tout est centralisé ici\n# ================================================================\nDATA_PATH   = '/content/dataset/sorted_data'\nSAVE_PATH   = '/content/drive/MyDrive/PFE_cancer/modeles'\nos.makedirs(SAVE_PATH, exist_ok=True)\n\nIMAGE_SIZE  = 96          # ✅ 96×96 au lieu de 32×32 — c'est le changement principal\nBATCH_SIZE  = 32          # 32 est mieux que 64 pour 96×96 (mémoire GPU)\nEPOCHS      = 50          # EarlyStopping s'arrêtera avant si nécessaire\nLEARNING_RATE = 0.0001    # ✅ 10× plus petit que la version 0.86\nVAL_SPLIT   = 0.2\n\nprint('✅ Configuration :')\nprint(f'   IMAGE_SIZE    = {IMAGE_SIZE}×{IMAGE_SIZE}')\nprint(f'   BATCH_SIZE    = {BATCH_SIZE}')\nprint(f'   LEARNING_RATE = {LEARNING_RATE}')\nprint(f'   EPOCHS (max)  = {EPOCHS}')\nprint(f'   DATA_PATH     = {DATA_PATH}')","metadata":{"id":"Cj2TtckLM-0i","outputId":"1818951a-3852-4b56-b255-d55cca1db39e"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 6 — Data Generators\n","metadata":{"id":"XTrTRmqTM-0j"}},{"cell_type":"code","source":"# ================================================================\n# ✅ Data Augmentation sur le train uniquement\n# ✅ Normalisation sur train ET validation\n# ================================================================\ntrain_datagen = ImageDataGenerator(\n    rescale=1./255,\n    validation_split=VAL_SPLIT,\n    horizontal_flip=True,\n    vertical_flip=True,\n    rotation_range=20,\n    zoom_range=0.1,\n    width_shift_range=0.1,\n    height_shift_range=0.1\n)\n\n# Validation : rescale uniquement, pas d'augmentation\nval_datagen = ImageDataGenerator(\n    rescale=1./255,\n    validation_split=VAL_SPLIT\n)\n\ntrain_generator = train_datagen.flow_from_directory(\n    DATA_PATH,\n    target_size=(IMAGE_SIZE, IMAGE_SIZE),\n    batch_size=BATCH_SIZE,\n    class_mode='categorical',\n    subset='training',\n    shuffle=True,\n    seed=42\n)\n\nvalidation_generator = val_datagen.flow_from_directory(\n    DATA_PATH,\n    target_size=(IMAGE_SIZE, IMAGE_SIZE),\n    batch_size=BATCH_SIZE,\n    class_mode='categorical',\n    subset='validation',\n    shuffle=False,\n    seed=42\n)\n\nprint(f'\\n✅ Classes détectées : {train_generator.class_indices}')\nprint(f'   Train      : {train_generator.samples} images')\nprint(f'   Validation : {validation_generator.samples} images')","metadata":{"id":"Ow8weyNfM-0j","outputId":"5f48bce6-4563-488e-8c75-535127757f21"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 7 — Data Visualization\n","metadata":{"id":"i6AdyjAWM-0j"}},{"cell_type":"code","source":"# Afficher quelques exemples d'images\nbatch_images, batch_labels = next(train_generator)\nclass_names = list(train_generator.class_indices.keys())\n\nplt.figure(figsize=(14, 4))\nfor i in range(10):\n    plt.subplot(2, 5, i+1)\n    plt.imshow(batch_images[i])\n    label_idx = np.argmax(batch_labels[i])\n    plt.title(class_names[label_idx], fontsize=9,\n              color='red' if label_idx == 1 else 'green')\n    plt.axis('off')\nplt.suptitle('Exemples d\\'images (96×96)', fontweight='bold')\nplt.tight_layout()\nplt.show()\nprint('✅ Images affichées — noter la qualité vs 32×32')","metadata":{"id":"gtO8mqQ8M-0k","outputId":"cd315efd-3d1a-48e5-880a-54fce9f87ab7"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 8 — Model Architecture (new 90%+ structure)\n","metadata":{"id":"KsdFUUaBM-0k"}},{"cell_type":"code","source":"# ================================================================\n# 🏗️  NOUVELLE ARCHITECTURE — inspirée du notebook de référence\n#\n# Différences vs ton ancien modèle (0.86) :\n#   ✅ Input 96×96 au lieu de 32×32\n#   ✅ 3 Conv2D par bloc au lieu de 2\n#   ✅ Filtres : 32 → 64 → 128 au lieu de 16 → 32 → 128\n#   ✅ BatchNorm APRÈS chaque Conv2D (pas après MaxPool)\n#   ✅ Dense(256) avec L2\n# ================================================================\n\ndef build_model():\n    model = Sequential([\n\n        # ── Bloc 1 : 32 filtres, premier filtre 5×5 puis 3×3 ──\n        layers.Conv2D(32, (5, 5), padding='same', activation='relu',\n                      input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3)),\n        layers.BatchNormalization(),\n\n        layers.Conv2D(32, (3, 3), padding='same', activation='relu'),\n        layers.BatchNormalization(),\n\n        layers.Conv2D(32, (3, 3), padding='same', activation='relu'),\n        layers.BatchNormalization(),\n\n        layers.MaxPooling2D((2, 2)),\n        layers.Dropout(0.2),\n\n        # ── Bloc 2 : 64 filtres ──\n        layers.Conv2D(64, (3, 3), padding='same', activation='relu'),\n        layers.BatchNormalization(),\n\n        layers.Conv2D(64, (3, 3), padding='same', activation='relu'),\n        layers.BatchNormalization(),\n\n        layers.Conv2D(64, (3, 3), padding='same', activation='relu'),\n        layers.BatchNormalization(),\n\n        layers.MaxPooling2D((2, 2)),\n        layers.Dropout(0.2),\n\n        # ── Bloc 3 : 128 filtres ──\n        layers.Conv2D(128, (3, 3), padding='same', activation='relu'),\n        layers.BatchNormalization(),\n\n        layers.Conv2D(128, (3, 3), padding='same', activation='relu'),\n        layers.BatchNormalization(),\n\n        layers.Conv2D(128, (3, 3), padding='same', activation='relu'),\n        layers.BatchNormalization(),\n\n        layers.MaxPooling2D((2, 2)),\n        layers.Dropout(0.25),\n\n        # ── Tête de classification ──\n        layers.Flatten(),\n        layers.Dense(256, activation='relu'),\n        layers.BatchNormalization(),\n        layers.Dropout(0.2),\n\n        layers.Dense(2, activation='softmax')\n    ])\n    return model\n\ncnn = build_model()\ncnn.summary()","metadata":{"id":"7TYNPhFRM-0k","outputId":"c806b98e-a933-4ba2-e406-85df0196a699"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 9 — Model Compilation\n","metadata":{"id":"T7MIDm9yM-0l"}},{"cell_type":"code","source":"# ================================================================\n# ✅ lr=0.0001 (10× plus petit que ton ancienne version)\n# ✅ binary_crossentropy → categorical_crossentropy pour 2 classes\n# ================================================================\ncnn.compile(\n    optimizer=Adam(learning_rate=LEARNING_RATE),\n    loss='categorical_crossentropy',\n    metrics=['accuracy']\n)\n\nprint('✅ Modèle compilé')\nprint(f'   Optimizer     : Adam(lr={LEARNING_RATE})')\nprint(f'   Loss          : categorical_crossentropy')\nprint(f'   Paramètres    : {cnn.count_params():,}')","metadata":{"id":"rM4Rllk_M-0l","outputId":"2ad1e2e7-87cb-4a3b-d4b1-e39d7d13455f"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 10 — Callbacks\n","metadata":{"id":"GiupnNKcM-0m"}},{"cell_type":"code","source":"checkpoint_path = f'{SAVE_PATH}/best_model_96px.h5'\n\ncallbacks_list = [\n    # Sauvegarde automatique du meilleur modèle\n    ModelCheckpoint(\n        filepath=checkpoint_path,\n        monitor='val_accuracy',\n        save_best_only=True,\n        mode='max',\n        verbose=1\n    ),\n    # Arrêt si plus d'amélioration\n    EarlyStopping(\n        monitor='val_accuracy',\n        patience=8,\n        restore_best_weights=True,\n        mode='max',\n        verbose=1\n    ),\n    # Réduction automatique du learning rate\n    ReduceLROnPlateau(\n        monitor='val_accuracy',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-6,\n        mode='max',\n        verbose=1\n    )\n]\n\nprint('✅ Callbacks configurés :')\nprint(f'   ModelCheckpoint  → sauvegarde dans {checkpoint_path}')\nprint(f'   EarlyStopping    → patience=8 epochs')\nprint(f'   ReduceLROnPlateau → factor=0.5, patience=3')","metadata":{"id":"fw5LTo17M-0m","outputId":"7b9c7c93-8448-445c-f7d7-9ec76b953c94"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 11 — Training\n","metadata":{"id":"Yxw_MliLM-0n"}},{"cell_type":"code","source":"print('🚀 Lancement de l\\'entraînement...')\nprint(f'   Images train : {train_generator.samples}')\nprint(f'   Images val   : {validation_generator.samples}')\nprint(f'   Epochs max   : {EPOCHS}\\n')\n\nstart_time = time.time()\n\nhistory = cnn.fit(\n    train_generator,\n    steps_per_epoch=train_generator.samples // BATCH_SIZE,\n    epochs=EPOCHS,\n    validation_data=validation_generator,\n    validation_steps=validation_generator.samples // BATCH_SIZE,\n    callbacks=callbacks_list,\n    verbose=1\n)\n\nelapsed = (time.time() - start_time) / 60\nbest_acc = max(history.history['val_accuracy'])\nprint(f'\\n✅ Entraînement terminé en {elapsed:.1f} minutes')\nprint(f'   Meilleure val_accuracy : {best_acc*100:.2f}%')","metadata":{"id":"Vga8l39CM-0n","outputId":"0ab80476-7d3d-409f-be41-5c112d079010"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 12 — Training Curves\n","metadata":{"id":"jxi20GjRM-0n"}},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n\n# Accuracy\naxes[0].plot(history.history['accuracy'],     label='Train',      linewidth=2, color='#3498db')\naxes[0].plot(history.history['val_accuracy'], label='Validation', linewidth=2, color='#e74c3c', linestyle='--')\naxes[0].axhline(y=0.90, color='green', linestyle=':', linewidth=1.5, label='Cible 90%')\naxes[0].set_title('Accuracy', fontsize=13, fontweight='bold')\naxes[0].set_xlabel('Epoch')\naxes[0].set_ylabel('Accuracy')\naxes[0].legend()\naxes[0].grid(True, alpha=0.3)\n\n# Loss\naxes[1].plot(history.history['loss'],     label='Train',      linewidth=2, color='#3498db')\naxes[1].plot(history.history['val_loss'], label='Validation', linewidth=2, color='#e74c3c', linestyle='--')\naxes[1].set_title('Loss', fontsize=13, fontweight='bold')\naxes[1].set_xlabel('Epoch')\naxes[1].set_ylabel('Loss')\naxes[1].legend()\naxes[1].grid(True, alpha=0.3)\n\nplt.suptitle('Courbes d\\'entraînement — Modèle 96×96', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig(f'{SAVE_PATH}/training_curves_96px.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint('✅ Courbes sauvegardées')","metadata":{"id":"2MtMTIhOM-0o","outputId":"c73f89be-a687-4833-e21b-131e7ca3f712"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 13 — Saving the Final Model\n","metadata":{"id":"4Q4SXFsAM-0o"}},{"cell_type":"code","source":"final_path = f'{SAVE_PATH}/modele_final_96px.h5'\ncnn.save(final_path)\nprint(f'✅ Modèle sauvegardé : {final_path}')\nprint(f'   (Le meilleur checkpoint est aussi dans : {checkpoint_path})')","metadata":{"id":"L7nRkNFtM-0o","outputId":"7c6d5fd8-4489-456a-ddc0-90766c662d48"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 14 — Confusion Matrix (20% Validation)\n","metadata":{"id":"fRLwl8QnM-0p"}},{"cell_type":"code","source":"# Charger le meilleur modèle sauvegardé\nfrom tensorflow.keras.models import load_model\ncnn = load_model(checkpoint_path)\nprint(f'✅ Meilleur modèle chargé depuis : {checkpoint_path}')\n\n# Prédictions sur la validation\nvalidation_generator.reset()\nY_pred = cnn.predict(validation_generator, verbose=1)\ny_pred = np.argmax(Y_pred, axis=1)\ny_true = validation_generator.classes\n\n# Matrice de confusion\ncm = confusion_matrix(y_true, y_pred)\n\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=['Benign', 'Malignant'],\n            yticklabels=['Benign', 'Malignant'],\n            annot_kws={'size': 14})\nplt.title('Matrice de Confusion — Validation Set (20%)', fontweight='bold', fontsize=13)\nplt.ylabel('Vraie étiquette')\nplt.xlabel('Prédiction du modèle')\nplt.tight_layout()\nplt.savefig(f'{SAVE_PATH}/confusion_matrix_val.png', dpi=150, bbox_inches='tight')\nplt.show()\n\nprint('\\n' + classification_report(y_true, y_pred, target_names=['Benign', 'Malignant']))","metadata":{"id":"U7Ffx-DsM-0p","outputId":"f56e5ddd-b249-477a-c4c0-6bc790bac729"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 15 — Confusion Matrix (100% of Dataset)\n","metadata":{"id":"k_iC1RWSM-0p"}},{"cell_type":"code","source":"# Générateur sur la totalité du dataset (sans split)\ntotal_datagen = ImageDataGenerator(rescale=1./255)\n\ntotal_generator = total_datagen.flow_from_directory(\n    DATA_PATH,\n    target_size=(IMAGE_SIZE, IMAGE_SIZE),\n    batch_size=BATCH_SIZE,\n    class_mode='categorical',\n    shuffle=False\n)\n\nprint('📊 Prédiction sur la totalité du dataset...')\nY_pred_total  = cnn.predict(total_generator, verbose=1)\ny_pred_total  = np.argmax(Y_pred_total, axis=1)\ny_true_total  = total_generator.classes\n\ncm_total = confusion_matrix(y_true_total, y_pred_total)\n\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm_total, annot=True, fmt='d', cmap='Blues',\n            xticklabels=['Benign', 'Malignant'],\n            yticklabels=['Benign', 'Malignant'],\n            annot_kws={'size': 14})\nplt.title('Matrice de Confusion — Dataset Complet (100%)', fontweight='bold', fontsize=13)\nplt.ylabel('Réalité')\nplt.xlabel('Prédiction')\nplt.tight_layout()\nplt.savefig(f'{SAVE_PATH}/confusion_matrix_total.png', dpi=150, bbox_inches='tight')\nplt.show()\n\nprint('\\n' + classification_report(y_true_total, y_pred_total, target_names=['Benign', 'Malignant']))","metadata":{"id":"5JJWvPmLM-0q","outputId":"2cee4b2a-7724-4e05-d9b5-35420f558ffc"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for layer in cnn.layers:\n    if 'conv2d' in layer.name:\n        print(layer.name, layer.output.shape)","metadata":{"id":"qpdINhLlHNTB","outputId":"5f1dfd5f-26af-4c6f-dab3-dc112ccfeb27"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 16 — Grad-CAM (Heatmap)\n","metadata":{"id":"iRBKwXhBM-0r"}},{"cell_type":"code","source":"import tensorflow as tf\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport cv2\nimport os\n\n# =========================================================\n# GRAD-CAM FUNCTIONS\n# =========================================================\ndef make_gradcam_heatmap(img_array, model, last_conv_layer_name, pred_index=None):\n\n    # ✅ FIX: Keras 3 blocks model.input/.output on Sequential models\n    # Use @tf.function with explicit layer-by-layer forward pass instead\n\n    last_conv_layer = model.get_layer(last_conv_layer_name)\n\n    # Find the index of the last conv layer\n    layer_names = [l.name for l in model.layers]\n    last_conv_idx = layer_names.index(last_conv_layer_name)\n\n    img_tensor = tf.cast(img_array, tf.float32)\n\n    with tf.GradientTape() as tape:\n        # Forward pass up to and including last conv layer\n        x = img_tensor\n        for layer in model.layers[:last_conv_idx + 1]:\n            x = layer(x, training=False)\n        conv_outputs = x\n        tape.watch(conv_outputs)\n\n        # Forward pass through remaining layers\n        x = conv_outputs\n        for layer in model.layers[last_conv_idx + 1:]:\n            x = layer(x, training=False)\n        predictions = x\n\n        if pred_index is None:\n            pred_index = tf.argmax(predictions[0])\n\n        class_channel = predictions[:, pred_index]\n\n    grads = tape.gradient(class_channel, conv_outputs)\n    pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))\n\n    conv_out = conv_outputs[0]\n    heatmap = conv_out @ pooled_grads[..., tf.newaxis]\n    heatmap = tf.squeeze(heatmap)\n\n    heatmap = tf.maximum(heatmap, 0)\n    max_val = tf.math.reduce_max(heatmap)\n    if max_val == 0:\n        max_val = 1\n    heatmap = heatmap / max_val\n\n    return heatmap.numpy()\n\n\ndef display_gradcam(img_path, heatmap, alpha=0.4):\n    img = cv2.imread(img_path)\n    img = cv2.resize(img, (96, 96))\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n    heatmap_resized = cv2.resize(heatmap, (img.shape[1], img.shape[0]))\n    heatmap_uint8 = np.uint8(255 * heatmap_resized)\n\n    jet = plt.cm.jet(np.arange(256))[:, :3]\n    jet_heatmap = jet[heatmap_uint8]\n\n    jet_heatmap_img = tf.keras.preprocessing.image.array_to_img(jet_heatmap)\n    jet_heatmap = np.array(jet_heatmap_img)\n\n    superimposed_img = jet_heatmap * alpha + img\n    superimposed_img = np.clip(superimposed_img, 0, 255).astype(np.uint8)\n\n    return img, jet_heatmap, superimposed_img\n\n\ndef preprocess_image(img_path):\n    img = tf.keras.preprocessing.image.load_img(img_path, target_size=(96, 96))\n    img_array = tf.keras.preprocessing.image.img_to_array(img)\n    img_array = img_array.astype(\"float32\") / 255.0\n    img_array = np.expand_dims(img_array, axis=0)\n    return img_array\n\n\n# =========================================================\n# MODEL INITIALIZATION\n# =========================================================\ncnn.predict(np.zeros((1, 96, 96, 3), dtype=np.float32), verbose=0)\nprint(\"✅ Model initialized for Grad-CAM\")\n\nlast_conv_layer_name = \"conv2d_8\"  # ✅ confirmed last conv layer\n\n# =========================================================\n# DATASET PATHS\n# =========================================================\nDATA_PATH = \"/content/dataset/sorted_data\"\n\nbenign_img_path = os.path.join(\n    DATA_PATH, \"Benign\",\n    os.listdir(os.path.join(DATA_PATH, \"Benign\"))[0]\n)\nmalignant_img_path = os.path.join(\n    DATA_PATH, \"Malignant\",\n    os.listdir(os.path.join(DATA_PATH, \"Malignant\"))[0]\n)\n\n# =========================================================\n# BENIGN IMAGE\n# =========================================================\nprint(\"Processing Benign Image...\")\nimg_array = preprocess_image(benign_img_path)\npreds = cnn.predict(img_array, verbose=0)\npred_class = np.argmax(preds[0])\n\nheatmap = make_gradcam_heatmap(img_array, cnn, last_conv_layer_name, pred_class)\noriginal, hm, superimposed = display_gradcam(benign_img_path, heatmap)\n\nplt.figure(figsize=(12, 4))\nplt.subplot(1, 3, 1); plt.imshow(original);    plt.title(\"Original Benign\");  plt.axis(\"off\")\nplt.subplot(1, 3, 2); plt.imshow(hm);           plt.title(\"Heatmap\");          plt.axis(\"off\")\nplt.subplot(1, 3, 3); plt.imshow(superimposed); plt.title(\"Grad-CAM Overlay\"); plt.axis(\"off\")\nplt.tight_layout()\nplt.show()\n\n# =========================================================\n# MALIGNANT IMAGE\n# =========================================================\nprint(\"Processing Malignant Image...\")\nimg_array = preprocess_image(malignant_img_path)\npreds = cnn.predict(img_array, verbose=0)\npred_class = np.argmax(preds[0])\n\nheatmap = make_gradcam_heatmap(img_array, cnn, last_conv_layer_name, pred_class)\noriginal, hm, superimposed = display_gradcam(malignant_img_path, heatmap)\n\nplt.figure(figsize=(12, 4))\nplt.subplot(1, 3, 1); plt.imshow(original);    plt.title(\"Original Malignant\"); plt.axis(\"off\")\nplt.subplot(1, 3, 2); plt.imshow(hm);           plt.title(\"Heatmap\");            plt.axis(\"off\")\nplt.subplot(1, 3, 3); plt.imshow(superimposed); plt.title(\"Grad-CAM Overlay\");   plt.axis(\"off\")\nplt.tight_layout()\nplt.show()\n\nprint(\"✅ Grad-CAM finished successfully\")","metadata":{"id":"_Ck_pPlsM-0r","outputId":"348ab5e1-e198-49d8-d325-f062521971dc"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 17 — Displaying the Heatmaps\n","metadata":{"id":"4oTStZwpM-0s"}},{"cell_type":"code","source":"# ── Cell 17: Display Grad-CAM heatmaps ────────────────────────────────\nclass_names_list = list(total_generator.class_indices.keys())\n\ndummy_input = np.zeros((1, IMAGE_SIZE, IMAGE_SIZE, 3), dtype=np.float32)\ncnn.predict(dummy_input, verbose=0)\nprint(\"Model initialized for Grad-CAM\")\n\nsample_images = []\nsample_labels = []\nfor class_name in class_names_list:\n    class_dir = os.path.join(DATA_PATH, class_name)\n    img_files = os.listdir(class_dir)[:3]\n    for f in img_files:\n        sample_images.append(os.path.join(class_dir, f))\n        sample_labels.append(class_name)\n\nfig, axes = plt.subplots(len(sample_images), 3, figsize=(10, 4 * len(sample_images)))\nfig.suptitle('Grad-CAM Heatmaps', fontsize=13, fontweight='bold')\n\nfor i, (img_path, label) in enumerate(zip(sample_images, sample_labels)):\n    img_raw = cv2.imread(img_path)\n    img_raw = cv2.resize(img_raw, (IMAGE_SIZE, IMAGE_SIZE))\n    img_raw = cv2.cvtColor(img_raw, cv2.COLOR_BGR2RGB)\n\n    # ✅ Cast to float32 AND normalize — both required\n    img_array = np.expand_dims(img_raw.astype('float32') / 255.0, axis=0)\n\n    pred = cnn.predict(img_array, verbose=0)[0]\n    pred_class = np.argmax(pred)\n    pred_label = class_names_list[pred_class]\n    confidence = pred[pred_class] * 100\n\n    heatmap = make_gradcam_heatmap(img_array, cnn, last_conv_name)\n    original, hm, superimposed = display_gradcam(img_path, heatmap)\n\n    color = 'red' if label == 'Malignant' else 'green'\n    pred_color = 'red' if pred_label == 'Malignant' else 'green'\n\n    axes[i, 0].imshow(original)\n    axes[i, 0].set_title(f'Réel: {label}', color=color, fontsize=9, fontweight='bold')\n    axes[i, 0].axis('off')\n\n    axes[i, 1].imshow(hm, cmap='jet')\n    axes[i, 1].set_title('Heatmap', fontsize=9)\n    axes[i, 1].axis('off')\n\n    axes[i, 2].imshow(superimposed)\n    axes[i, 2].set_title(f'Prédit: {pred_label} ({confidence:.1f}%)', color=pred_color, fontsize=9)\n    axes[i, 2].axis('off')\n\nplt.tight_layout()\nplt.savefig(f'{SAVE_PATH}/gradcam.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint('✅ Grad-CAM saved')","metadata":{"id":"-OsBpQPTM-0t","outputId":"a7669c5d-5420-412a-8e71-22dffc05e12d"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cell 18 — Final Summary\n","metadata":{"id":"JgRyKRpyM-0t"}},{"cell_type":"code","source":"best_val_acc  = max(history.history['val_accuracy'])\nbest_val_loss = min(history.history['val_loss'])\ntotal_epochs  = len(history.history['accuracy'])\n\nprint('=' * 55)\nprint('        RÉSUMÉ FINAL — Modèle 96×96')\nprint('=' * 55)\nprint(f'  Image size      : {IMAGE_SIZE}×{IMAGE_SIZE} px')\nprint(f'  Architecture    : 9 Conv2D (3 blocs × 3)')\nprint(f'  Filtres         : 32 → 64 → 128')\nprint(f'  Learning rate   : {LEARNING_RATE} + ReduceLROnPlateau')\nprint(f'  Batch size      : {BATCH_SIZE}')\nprint(f'  Epochs réels    : {total_epochs} (sur {EPOCHS} max)')\nprint(f'  Paramètres      : {cnn.count_params():,}')\nprint('-' * 55)\nprint(f'  val_accuracy    : {best_val_acc*100:.2f}%')\nprint(f'  val_loss        : {best_val_loss:.4f}')\nprint('=' * 55)\nprint(f'\\n  Modèle sauvegardé : {checkpoint_path}')","metadata":{"id":"ncuOpRTsM-0u","outputId":"7a0e0790-a397-46e7-f837-00ef8c75853a"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## this is for the web app APIs\n","metadata":{"id":"38kVB6OEpCdt"}},{"cell_type":"code","source":"# ── APP FEED: Training curves ──────────────────────────────────────────\n# This output is parsed by the web app to display the accuracy curves\nepochs_done = len(history.history['accuracy'])\nprint(f\"Training complete. epochs = {epochs_done}\")\nprint(f\"batch_size = 64\")\nprint(f\"learning_rate = 0.0001\")\n\nfor epoch_idx in range(epochs_done):\n    acc     = history.history['accuracy'][epoch_idx]\n    val_acc = history.history['val_accuracy'][epoch_idx]\n    loss    = history.history['loss'][epoch_idx]\n    val_loss= history.history['val_loss'][epoch_idx]\n    # This exact format is what app.py's regex captures for the curves\n    print(f\"Epoch {epoch_idx+1}/{epochs_done} - loss: {loss:.4f} - accuracy: {acc:.4f} - val_loss: {val_loss:.4f} - val_accuracy: {val_acc:.4f}\")","metadata":{"id":"KJIcr6BmpKwQ","outputId":"cb91f5ea-1ebd-4fae-a355-240c007f4e8d"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── APP FEED: Classification Report ───────────────────────────────────\n# The web app parses this output for accuracy, precision, recall, F1\nimport numpy as np\nfrom sklearn.metrics import classification_report, confusion_matrix\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\nvalidation_generator.reset()\nY_pred = cnn.predict(validation_generator, verbose=0)\ny_pred = np.argmax(Y_pred, axis=1)\ny_true = validation_generator.classes\n\n# ⚠️ Print in this exact format — app.py regex depends on it\nprint(classification_report(y_true, y_pred, target_names=['Benign', 'Malignant']))","metadata":{"id":"OKka_TYlqZSl","outputId":"6d0e4f85-0742-4c5f-8187-f5b1e19b3f8c"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── APP FEED: Confusion Matrix ─────────────────────────────────────────\ncm = confusion_matrix(y_true, y_pred)\n\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=['Benign', 'Malignant'],\n            yticklabels=['Benign', 'Malignant'],\n            annot_kws={'size': 14})\nplt.title('Matrice de Confusion (Validation Set)')\nplt.ylabel('Vraie étiquette')\nplt.xlabel('Prédiction du modèle')\nplt.tight_layout()\nplt.show()","metadata":{"id":"LE4XeevGqgYU","outputId":"67c7ce4b-3f83-46c5-e1fc-91f88e8de4be"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── APP FEED: Model config summary ────────────────────────────────────\n# The web app scans the notebook text for these exact patterns\nprint(\"=== Model Configuration ===\")\nprint(f\"optimizer = Adam\")\nprint(f\"loss = 'categorical_crossentropy'\")\nprint(f\"learning_rate = 0.0001\")\nprint(f\"batch_size = 64\")\nprint(f\"epochs = 50\")\nprint(f\"Model compiled with Adam(learning_rate=0.0001)\")","metadata":{"id":"cCLHu57eqjob","outputId":"6c8ce73b-2b0e-4667-93b3-a677f6e44993"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"id":"EsXpk3bm50mv"},"outputs":[],"execution_count":null}]}