{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":2822650,"sourceType":"datasetVersion","datasetId":1715304}],"dockerImageVersionId":31240,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-04T15:25:15.541354Z","iopub.execute_input":"2026-01-04T15:25:15.541649Z","iopub.status.idle":"2026-01-04T15:25:20.704676Z","shell.execute_reply.started":"2026-01-04T15:25:15.541627Z","shell.execute_reply":"2026-01-04T15:25:20.703704Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport warnings\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import confusion_matrix, cohen_kappa_score\nfrom keras.models import Model\nfrom keras import optimizers, applications\nfrom keras.callbacks import EarlyStopping, ReduceLROnPlateau\nfrom keras.layers import Dense, Dropout, GlobalAveragePooling2D, Input\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization, GlobalAveragePooling2D\nfrom tensorflow.keras.optimizers import Adam\nimport cv2\nimport matplotlib.image as mpimg\n\nwarnings.filterwarnings(\"ignore\")\n# Load the CSV files\ntrain_data = pd.read_csv(\"/kaggle/input/aptos2019/train_1.csv\")\nvalid_data = pd.read_csv(\"/kaggle/input/aptos2019/valid.csv\")\ntest_data = pd.read_csv(\"/kaggle/input/aptos2019/test.csv\")\n\n# Print the number of samples in each dataset\nprint('Number of train samples:', train_data.shape[0])\nprint('Number of validation samples:', valid_data.shape[0])\nprint('Number of test samples:', test_data.shape[0])\n\n\nimport os\n\n# Define the image directories\ntrain_image_dir = \"/kaggle/input/aptos2019/train_images/train_images\"\nval_image_dir = \"/kaggle/input/aptos2019/val_images/val_images\"\ntest_image_dir = \"/kaggle/input/aptos2019/test_images/test_images\"\n\n# Function to count images in a directory\ndef count_images(directory):\n    return len([file for file in os.listdir(directory) if file.endswith(('.png', '.jpg', '.jpeg'))])\n\n# Count images in each directory\ntrain_image_count = count_images(train_image_dir)\nval_image_count = count_images(val_image_dir)\ntest_image_count = count_images(test_image_dir)\n\n# Print the counts\nprint(f'There are {train_image_count} training images.')\nprint(f'There are {val_image_count} validation images.')\nprint(f'There are {test_image_count} test images.')\n\n\nimport matplotlib.pyplot as plt\nimport random\nimport matplotlib.image as mpimg\n\n# Class labels dictionary\nclass_labels = {\n    0: \"No DR\",\n    1: \"Mild\",\n    2: \"Moderate\",\n    3: \"Severe\",\n    4: \"Proliferative DR\"\n}\n\n# Check some random images from the training set to ensure they are correctly labeled\nrandom_samples = random.sample(list(train_data['id_code']), 10)\n\nplt.figure(figsize=(20, 10))\nfor i, image_id in enumerate(random_samples):\n    # Image file path\n    img_path = os.path.join(train_image_dir, f\"{image_id}.png\")  # Adjust extension if necessary\n    img = mpimg.imread(img_path)\n    \n    # Get the class label for the image\n    image_class = train_data.loc[train_data['id_code'] == image_id, 'diagnosis'].values[0]\n    class_name = class_labels.get(image_class, \"Unknown\")\n    \n    # Plot the image\n    plt.subplot(1, 10, i + 1)\n    plt.imshow(img)\n    plt.axis('off')\n    plt.title(f\"Class: {class_name}\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T15:25:29.600997Z","iopub.execute_input":"2026-01-04T15:25:29.601735Z","iopub.status.idle":"2026-01-04T15:25:40.379648Z","shell.execute_reply.started":"2026-01-04T15:25:29.601710Z","shell.execute_reply":"2026-01-04T15:25:40.378818Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport seaborn as sns\nimport pandas as pd\n\n# Load the CSV files (if not already loaded)\ntrain_data = pd.read_csv(\"/kaggle/input/aptos2019/train_1.csv\")\nvalid_data = pd.read_csv(\"/kaggle/input/aptos2019/valid.csv\")\ntest_data = pd.read_csv(\"/kaggle/input/aptos2019/test.csv\")\n\n# Count the occurrences of each class in the training, validation, and test datasets\ntrain_class_counts = train_data['diagnosis'].value_counts().sort_index()\nval_class_counts = valid_data['diagnosis'].value_counts().sort_index()\ntest_class_counts = test_data['diagnosis'].value_counts().sort_index()\n\n# Set up the bar charts for each dataset\nplt.figure(figsize=(18, 6))\n\n# Training Data\nplt.subplot(1, 3, 1)\nsns.barplot(x=train_class_counts.index, y=train_class_counts.values, palette='viridis')\nplt.title('Training Data Class Distribution', fontsize=16)\nplt.xlabel('Class (Diagnosis)', fontsize=14)\nplt.ylabel('Number of Samples', fontsize=14)\nplt.xticks(ticks=train_class_counts.index, labels=train_class_counts.index)\nplt.grid(axis='y', linestyle='--')\n\n# Validation Data\nplt.subplot(1, 3, 2)\nsns.barplot(x=val_class_counts.index, y=val_class_counts.values, palette='viridis')\nplt.title('Validation Data Class Distribution', fontsize=16)\nplt.xlabel('Class (Diagnosis)', fontsize=14)\nplt.ylabel('Number of Samples', fontsize=14)\nplt.xticks(ticks=val_class_counts.index, labels=val_class_counts.index)\nplt.grid(axis='y', linestyle='--')\n\n# Test Data\nplt.subplot(1, 3, 3)\nsns.barplot(x=test_class_counts.index, y=test_class_counts.values, palette='viridis')\nplt.title('Test Data Class Distribution', fontsize=16)\nplt.xlabel('Class (Diagnosis)', fontsize=14)\nplt.ylabel('Number of Samples', fontsize=14)\nplt.xticks(ticks=test_class_counts.index, labels=test_class_counts.index)\nplt.grid(axis='y', linestyle='--')\n\n# Adjust layout and show the plot\nplt.tight_layout()\nplt.show()\n\n# Print the number of samples for each class in the training data\nprint(\"Training Data Class Distribution:\")\nprint(train_class_counts)\nprint(\"\\nValidation Data Class Distribution:\")\nprint(val_class_counts)\nprint(\"\\nTest Data Class Distribution:\")\nprint(test_class_counts)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T15:25:57.631847Z","iopub.execute_input":"2026-01-04T15:25:57.632457Z","iopub.status.idle":"2026-01-04T15:25:58.283041Z","shell.execute_reply.started":"2026-01-04T15:25:57.632431Z","shell.execute_reply":"2026-01-04T15:25:58.282451Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sklearn\nimport imblearn\nfrom imblearn.over_sampling import SMOTE\n\nprint(\"Success!\")\nprint(f\"sklearn: {sklearn.__version__}\")\nprint(f\"imblearn: {imblearn.__version__}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T15:26:06.336956Z","iopub.execute_input":"2026-01-04T15:26:06.337730Z","iopub.status.idle":"2026-01-04T15:26:06.448915Z","shell.execute_reply.started":"2026-01-04T15:26:06.337703Z","shell.execute_reply":"2026-01-04T15:26:06.448156Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport tensorflow as tf\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import (Conv2D, MaxPooling2D, BatchNormalization, \n                                      Flatten, Dense, Dropout, GlobalAveragePooling2D,\n                                      Multiply, Reshape)\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint, LearningRateScheduler\nfrom tensorflow.keras.utils import to_categorical\nfrom sklearn.model_selection import train_test_split\nfrom imblearn.over_sampling import SMOTE\nfrom sklearn.metrics import classification_report, confusion_matrix, cohen_kappa_score\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\n# ============================================================================\n# CONFIGURATION - OPTIMIZED FOR SMOTE + CLAHE\n# ============================================================================\nIMG_SIZE = 128\nBATCH_SIZE = 32\nEPOCHS = 70  # Extended from 50 to 70 for better convergence\nINITIAL_LR = 1e-3\n\n# ============================================================================\n# DATA LOADING & PREPROCESSING\n# ============================================================================\ntrain_data = pd.read_csv(\"/kaggle/input/aptos2019/train_1.csv\")\ntrain_image_dir = \"/kaggle/input/aptos2019/train_images/train_images\"\n\ndef apply_clahe(img):\n    \"\"\"Apply CLAHE for contrast enhancement\"\"\"\n    img = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)\n    l, a, b = cv2.split(img)\n    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n    l = clahe.apply(l.astype(np.uint8))\n    img = cv2.merge((l, a, b))\n    img = cv2.cvtColor(img, cv2.COLOR_LAB2RGB)\n    return img\n\ndef load_preprocess_image(image_id):\n    \"\"\"Load and preprocess with CLAHE\"\"\"\n    img_path = os.path.join(train_image_dir, f\"{image_id}.png\")\n    img = cv2.imread(img_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n    img = apply_clahe(img)\n    img = img / 255.0\n    return img\n\nprint(\"Loading and preprocessing images with CLAHE...\")\nX = np.array([load_preprocess_image(image_id) for image_id in train_data['id_code']])\ny = train_data['diagnosis'].values\n\nprint(f\"Original X shape: {X.shape}\")\nprint(f\"Original y shape: {y.shape}\")\nprint(f\"\\nOriginal class distribution:\")\nunique, counts = np.unique(y, return_counts=True)\nfor cls, count in zip(unique, counts):\n    print(f\"  Class {cls}: {count} samples\")\n\n# ============================================================================\n# APPLY SMOTE FOR BALANCED DATASET\n# ============================================================================\nprint(\"\\nApplying SMOTE to balance dataset...\")\nsmote = SMOTE(random_state=42, k_neighbors=5)\n\nX_flat = X.reshape(X.shape[0], -1)\nX_resampled, y_resampled = smote.fit_resample(X_flat, y)\nX_resampled = X_resampled.reshape(-1, IMG_SIZE, IMG_SIZE, 3)\n\nprint(f\"\\nBalanced X shape: {X_resampled.shape}\")\nprint(f\"\\nBalanced class distribution:\")\nunique, counts = np.unique(y_resampled, return_counts=True)\nfor cls, count in zip(unique, counts):\n    print(f\"  Class {cls}: {count} samples\")\n\ny_categorical = to_categorical(y_resampled, num_classes=5)\n\n# ============================================================================\n# TRAIN-VALIDATION SPLIT\n# ============================================================================\nX_train, X_val, y_train, y_val = train_test_split(\n    X_resampled, y_categorical,\n    test_size=0.2,\n    stratify=y_resampled,\n    random_state=42\n)\n\nprint(f\"\\nTrain: {X_train.shape}, {y_train.shape}\")\nprint(f\"Val: {X_val.shape}, {y_val.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T15:26:08.539051Z","iopub.execute_input":"2026-01-04T15:26:08.539643Z","iopub.status.idle":"2026-01-04T15:32:32.530070Z","shell.execute_reply.started":"2026-01-04T15:26:08.539616Z","shell.execute_reply":"2026-01-04T15:32:32.529395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# IMPROVED CNN MODEL\n# ============================================================================\ndef create_improved_cnn(input_shape=(IMG_SIZE, IMG_SIZE, 3)):\n    \"\"\"\n    Custom CNN optimized for:\n    - SMOTE-augmented data\n    - CLAHE preprocessing\n    - Balanced classes\n    - Diabetic retinopathy features\n    \"\"\"\n    model = Sequential(name='DR_Detection_CNN')\n    \n    # Block 1: Initial feature extraction\n    model.add(Conv2D(32, (3, 3), activation='relu', padding='same', \n                     input_shape=input_shape, name='conv1_1'))\n    model.add(BatchNormalization(name='bn1_1'))\n    model.add(Conv2D(32, (3, 3), activation='relu', padding='same', name='conv1_2'))\n    model.add(BatchNormalization(name='bn1_2'))\n    model.add(MaxPooling2D((2, 2), name='pool1'))\n    model.add(Dropout(0.25, name='dropout1'))\n    \n    # Block 2: Deeper features\n    model.add(Conv2D(64, (3, 3), activation='relu', padding='same', name='conv2_1'))\n    model.add(BatchNormalization(name='bn2_1'))\n    model.add(Conv2D(64, (3, 3), activation='relu', padding='same', name='conv2_2'))\n    model.add(BatchNormalization(name='bn2_2'))\n    model.add(MaxPooling2D((2, 2), name='pool2'))\n    model.add(Dropout(0.25, name='dropout2'))\n    \n    # Block 3: High-level features\n    model.add(Conv2D(128, (3, 3), activation='relu', padding='same', name='conv3_1'))\n    model.add(BatchNormalization(name='bn3_1'))\n    model.add(Conv2D(128, (3, 3), activation='relu', padding='same', name='conv3_2'))\n    model.add(BatchNormalization(name='bn3_2'))\n    model.add(MaxPooling2D((2, 2), name='pool3'))\n    model.add(Dropout(0.3, name='dropout3'))\n    \n    # Block 4: Refined features\n    model.add(Conv2D(256, (3, 3), activation='relu', padding='same', name='conv4_1'))\n    model.add(BatchNormalization(name='bn4_1'))\n    model.add(Conv2D(256, (3, 3), activation='relu', padding='same', name='conv4_2'))\n    model.add(BatchNormalization(name='bn4_2'))\n    model.add(GlobalAveragePooling2D(name='gap'))\n    model.add(Dropout(0.4, name='dropout4'))\n    \n    # Classification head\n    model.add(Dense(256, activation='relu', name='fc1'))\n    model.add(BatchNormalization(name='bn_fc1'))\n    model.add(Dropout(0.5, name='dropout_fc1'))\n    model.add(Dense(128, activation='relu', name='fc2'))\n    model.add(Dropout(0.4, name='dropout_fc2'))\n    model.add(Dense(5, activation='softmax', name='output'))\n    \n    return model\n\n# ============================================================================\n# MODEL COMPILATION\n# ============================================================================\nprint(\"\\nBuilding improved CNN model...\")\nmodel = create_improved_cnn()\n\nmodel.compile(\n    optimizer=Adam(learning_rate=INITIAL_LR),\n    loss='categorical_crossentropy',\n    metrics=['accuracy', \n             tf.keras.metrics.AUC(name='auc'),\n             tf.keras.metrics.Precision(name='precision'),\n             tf.keras.metrics.Recall(name='recall')]\n)\n\nprint(\"\\nModel Summary:\")\nmodel.summary()\nprint(f\"\\nTotal parameters: {model.count_params():,}\")\n\n# ============================================================================\n# CUSTOM LEARNING RATE SCHEDULE\n# ============================================================================\nclass LRLogger(tf.keras.callbacks.Callback):\n    \"\"\"Custom callback to log learning rate at each epoch\"\"\"\n    def __init__(self):\n        super().__init__()\n        self.lrs = []\n    \n    def on_epoch_end(self, epoch, logs=None):\n        # Handle both old (lr) and new (learning_rate) attribute names\n        optimizer = self.model.optimizer\n        if hasattr(optimizer, 'learning_rate'):\n            lr = float(tf.keras.backend.get_value(optimizer.learning_rate))\n        elif hasattr(optimizer, 'lr'):\n            lr = float(tf.keras.backend.get_value(optimizer.lr))\n        else:\n            lr = float(optimizer._learning_rate)\n        \n        self.lrs.append(lr)\n        if logs is not None:\n            logs['lr'] = lr\n\nlr_logger = LRLogger()\n\n# ============================================================================\n# CALLBACKS\n# ============================================================================\ncallbacks = [\n    EarlyStopping(\n        monitor='val_accuracy',\n        patience=15,\n        restore_best_weights=True,\n        verbose=1\n    ),\n    ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=5,\n        min_lr=1e-7,\n        verbose=1\n    ),\n    ModelCheckpoint(\n        'best_dr_model.keras',\n        monitor='val_accuracy',\n        save_best_only=True,\n        mode='max',\n        verbose=1\n    ),\n    lr_logger  # Add LR logger\n]\n\n# ============================================================================\n# TRAINING - INITIAL PHASE\n# ============================================================================\nprint(\"\\n\" + \"=\"*80)\nprint(\"STARTING TRAINING - INITIAL PHASE\")\nprint(\"=\"*80)\n\nhistory = model.fit(\n    X_train, y_train,\n    validation_data=(X_val, y_val),\n    epochs=EPOCHS,\n    batch_size=BATCH_SIZE,\n    callbacks=callbacks,\n    verbose=1\n)\n\n# ============================================================================\n# FINE-TUNING PHASE (if initial training completes)\n# ============================================================================\nprint(\"\\n\" + \"=\"*80)\nprint(\"FINE-TUNING PHASE - LOWER LEARNING RATE\")\nprint(\"=\"*80)\n\n# Load best model from initial phase\nmodel = tf.keras.models.load_model('best_dr_model.keras')\n\n# Compile with lower learning rate for fine-tuning\nmodel.compile(\n    optimizer=Adam(learning_rate=1e-4),  # 10x lower than initial\n    loss='categorical_crossentropy',\n    metrics=['accuracy', \n             tf.keras.metrics.AUC(name='auc'),\n             tf.keras.metrics.Precision(name='precision'),\n             tf.keras.metrics.Recall(name='recall')]\n)\n\n# Reset LR logger\nlr_logger_finetune = LRLogger()\n\n# Fine-tuning callbacks\nfinetune_callbacks = [\n    EarlyStopping(\n        monitor='val_accuracy',\n        patience=10,\n        restore_best_weights=True,\n        verbose=1\n    ),\n    ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=4,\n        min_lr=1e-8,\n        verbose=1\n    ),\n    ModelCheckpoint(\n        'best_dr_model_final.keras',\n        monitor='val_accuracy',\n        save_best_only=True,\n        mode='max',\n        verbose=1\n    ),\n    lr_logger_finetune\n]\n\n# Continue training for 20 more epochs\nhistory_finetune = model.fit(\n    X_train, y_train,\n    validation_data=(X_val, y_val),\n    epochs=20,\n    batch_size=BATCH_SIZE,\n    callbacks=finetune_callbacks,\n    verbose=1\n)\n\n# Combine histories\nfor key in history.history.keys():\n    if key in history_finetune.history:\n        history.history[key].extend(history_finetune.history[key])\n\n# Combine learning rates\nlr_logger.lrs.extend(lr_logger_finetune.lrs)\n\n# ============================================================================\n# EVALUATION ON FINAL MODEL\n# ============================================================================\nprint(\"\\n\" + \"=\"*80)\nprint(\"FINAL EVALUATION RESULTS\")\nprint(\"=\"*80)\n\n# Load best final model\nmodel = tf.keras.models.load_model('best_dr_model_final.keras')\n\n# Predictions\ny_pred_probs = model.predict(X_val, verbose=0)\ny_pred = np.argmax(y_pred_probs, axis=1)\ny_true = np.argmax(y_val, axis=1)\n\n# Overall metrics\nval_loss, val_acc, val_auc, val_prec, val_rec = model.evaluate(X_val, y_val, verbose=0)\nprint(f\"\\nOverall Validation Metrics:\")\nprint(f\"  Accuracy:  {val_acc:.4f} ({val_acc*100:.2f}%)\")\nprint(f\"  AUC:       {val_auc:.4f}\")\nprint(f\"  Precision: {val_prec:.4f}\")\nprint(f\"  Recall:    {val_rec:.4f}\")\n\n# Classification Report\nprint(\"\\nDetailed Classification Report:\")\nprint(classification_report(y_true, y_pred, \n                          target_names=['No DR', 'Mild', 'Moderate', 'Severe', 'Proliferative'],\n                          digits=4))\n\n# Quadratic Weighted Kappa\nkappa = cohen_kappa_score(y_true, y_pred, weights='quadratic')\nprint(f\"\\nQuadratic Weighted Kappa: {kappa:.4f}\")\ninterpretation = (\n    \"Almost Perfect Agreement\" if kappa > 0.8 else\n    \"Substantial Agreement\" if kappa > 0.6 else\n    \"Moderate Agreement\" if kappa > 0.4 else\n    \"Fair Agreement\"\n)\nprint(f\"Clinical Interpretation: {interpretation}\")\n\n# Per-class accuracy\nprint(\"\\nPer-Class Performance:\")\ncm = confusion_matrix(y_true, y_pred)\nclass_names = ['No DR', 'Mild', 'Moderate', 'Severe', 'Proliferative']\nfor i, class_name in enumerate(class_names):\n    class_acc = cm[i, i] / cm[i].sum() if cm[i].sum() > 0 else 0\n    class_total = cm[i].sum()\n    class_correct = cm[i, i]\n    print(f\"  {class_name:15s}: {class_acc:6.2%} ({class_correct:3d}/{class_total:3d})\")\n\n# ============================================================================\n# VISUALIZATIONS\n# ============================================================================\n\n# 1. Confusion Matrix with Enhanced Styling\nplt.figure(figsize=(14, 11))\ncm_percent = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis] * 100\n\n# Create heatmap\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=True,\n            xticklabels=class_names, yticklabels=class_names,\n            square=True, linewidths=1.5, linecolor='gray',\n            cbar_kws={'label': 'Count'})\n\n# Add percentages as text\nfor i in range(len(class_names)):\n    for j in range(len(class_names)):\n        text_color = 'white' if cm_percent[i, j] > 50 else 'black'\n        plt.text(j + 0.5, i + 0.72, f'({cm_percent[i, j]:.1f}%)', \n                ha='center', va='center', fontsize=10, color=text_color,\n                weight='bold' if i == j else 'normal')\n\nplt.title(f'Confusion Matrix - Diabetic Retinopathy Detection\\n' + \n         f'Accuracy: {val_acc:.2%} | AUC: {val_auc:.4f} | Kappa: {kappa:.4f} ({interpretation})', \n         fontsize=16, pad=20, fontweight='bold')\nplt.ylabel('True Diagnosis', fontsize=14, fontweight='bold')\nplt.xlabel('Predicted Diagnosis', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('confusion_matrix_final.png', dpi=300, bbox_inches='tight')\nplt.show()\n\n# 2. Training History - Comprehensive View\nfig, axes = plt.subplots(2, 3, figsize=(20, 11))\nfig.suptitle('Complete Training History - DR Detection Model', fontsize=18, fontweight='bold', y=0.995)\n\n# Accuracy\naxes[0, 0].plot(history.history['accuracy'], label='Train', linewidth=2.5, marker='o', \n               markersize=3, alpha=0.8, color='#2E86AB')\naxes[0, 0].plot(history.history['val_accuracy'], label='Validation', linewidth=2.5, \n               marker='s', markersize=3, alpha=0.8, color='#A23B72')\nbest_epoch = np.argmax(history.history['val_accuracy'])\naxes[0, 0].axvline(x=best_epoch, color='red', linestyle='--', alpha=0.5, linewidth=2)\naxes[0, 0].scatter(best_epoch, history.history['val_accuracy'][best_epoch], \n                  color='red', s=100, zorder=5, marker='*')\naxes[0, 0].set_title('Model Accuracy', fontsize=14, fontweight='bold')\naxes[0, 0].set_xlabel('Epoch', fontsize=12)\naxes[0, 0].set_ylabel('Accuracy', fontsize=12)\naxes[0, 0].legend(fontsize=11)\naxes[0, 0].grid(True, alpha=0.3)\naxes[0, 0].text(best_epoch, history.history['val_accuracy'][best_epoch], \n               f' Best: {max(history.history[\"val_accuracy\"]):.4f}', \n               fontsize=10, va='bottom')\n\n# Loss\naxes[0, 1].plot(history.history['loss'], label='Train', linewidth=2.5, marker='o', \n               markersize=3, alpha=0.8, color='#2E86AB')\naxes[0, 1].plot(history.history['val_loss'], label='Validation', linewidth=2.5, \n               marker='s', markersize=3, alpha=0.8, color='#A23B72')\naxes[0, 1].set_title('Model Loss', fontsize=14, fontweight='bold')\naxes[0, 1].set_xlabel('Epoch', fontsize=12)\naxes[0, 1].set_ylabel('Loss', fontsize=12)\naxes[0, 1].legend(fontsize=11)\naxes[0, 1].grid(True, alpha=0.3)\n\n# AUC\naxes[0, 2].plot(history.history['auc'], label='Train', linewidth=2.5, marker='o', \n               markersize=3, alpha=0.8, color='#2E86AB')\naxes[0, 2].plot(history.history['val_auc'], label='Validation', linewidth=2.5, \n               marker='s', markersize=3, alpha=0.8, color='#A23B72')\naxes[0, 2].set_title('Model AUC (Area Under Curve)', fontsize=14, fontweight='bold')\naxes[0, 2].set_xlabel('Epoch', fontsize=12)\naxes[0, 2].set_ylabel('AUC', fontsize=12)\naxes[0, 2].legend(fontsize=11)\naxes[0, 2].grid(True, alpha=0.3)\n\n# Precision\naxes[1, 0].plot(history.history['precision'], label='Train', linewidth=2.5, marker='o', \n               markersize=3, alpha=0.8, color='#2E86AB')\naxes[1, 0].plot(history.history['val_precision'], label='Validation', linewidth=2.5, \n               marker='s', markersize=3, alpha=0.8, color='#A23B72')\naxes[1, 0].set_title('Model Precision', fontsize=14, fontweight='bold')\naxes[1, 0].set_xlabel('Epoch', fontsize=12)\naxes[1, 0].set_ylabel('Precision', fontsize=12)\naxes[1, 0].legend(fontsize=11)\naxes[1, 0].grid(True, alpha=0.3)\n\n# Recall\naxes[1, 1].plot(history.history['recall'], label='Train', linewidth=2.5, marker='o', \n               markersize=3, alpha=0.8, color='#2E86AB')\naxes[1, 1].plot(history.history['val_recall'], label='Validation', linewidth=2.5, \n               marker='s', markersize=3, alpha=0.8, color='#A23B72')\naxes[1, 1].set_title('Model Recall', fontsize=14, fontweight='bold')\naxes[1, 1].set_xlabel('Epoch', fontsize=12)\naxes[1, 1].set_ylabel('Recall', fontsize=12)\naxes[1, 1].legend(fontsize=11)\naxes[1, 1].grid(True, alpha=0.3)\n\n# Learning Rate\naxes[1, 2].plot(lr_logger.lrs, label='Learning Rate', \n               color='#F18F01', linewidth=2.5, marker='o', markersize=3, alpha=0.8)\naxes[1, 2].set_title('Learning Rate Schedule', fontsize=14, fontweight='bold')\naxes[1, 2].set_xlabel('Epoch', fontsize=12)\naxes[1, 2].set_ylabel('Learning Rate', fontsize=12)\naxes[1, 2].set_yscale('log')\naxes[1, 2].legend(fontsize=11)\naxes[1, 2].grid(True, alpha=0.3, which='both')\n\nplt.tight_layout()\nplt.savefig('training_history_complete.png', dpi=300, bbox_inches='tight')\nplt.show()\n\n# 3. Prediction Analysis\nfig, axes = plt.subplots(1, 2, figsize=(16, 6))\n\n# True vs Predicted Distribution\nx_pos = np.arange(5)\nwidth = 0.35\naxes[0].bar(x_pos - width/2, np.bincount(y_true, minlength=5), width, \n           alpha=0.8, label='True', color='#2E86AB', edgecolor='black', linewidth=1.5)\naxes[0].bar(x_pos + width/2, np.bincount(y_pred, minlength=5), width, \n           alpha=0.8, label='Predicted', color='#A23B72', edgecolor='black', linewidth=1.5)\naxes[0].set_xlabel('Diagnosis Class', fontsize=13, fontweight='bold')\naxes[0].set_ylabel('Sample Count', fontsize=13, fontweight='bold')\naxes[0].set_title('True vs Predicted Class Distribution', fontsize=14, fontweight='bold')\naxes[0].set_xticks(x_pos)\naxes[0].set_xticklabels(class_names, rotation=30, ha='right', fontsize=11)\naxes[0].legend(fontsize=12)\naxes[0].grid(True, alpha=0.3, axis='y')\n\n# Prediction Confidence\nconfidence = np.max(y_pred_probs, axis=1)\naxes[1].hist(confidence, bins=40, edgecolor='black', alpha=0.8, color='#06A77D', linewidth=1.2)\naxes[1].set_xlabel('Prediction Confidence', fontsize=13, fontweight='bold')\naxes[1].set_ylabel('Frequency', fontsize=13, fontweight='bold')\naxes[1].set_title(f'Prediction Confidence Distribution\\nMean Confidence: {confidence.mean():.3f} | Std: {confidence.std():.3f}', \n                 fontsize=14, fontweight='bold')\naxes[1].axvline(confidence.mean(), color='red', linestyle='--', linewidth=2.5, \n               label=f'Mean: {confidence.mean():.3f}')\naxes[1].axvline(np.median(confidence), color='orange', linestyle='--', linewidth=2.5, \n               label=f'Median: {np.median(confidence):.3f}')\naxes[1].legend(fontsize=12)\naxes[1].grid(True, alpha=0.3, axis='y')\n\nplt.tight_layout()\nplt.savefig('prediction_analysis_final.png', dpi=300, bbox_inches='tight')\nplt.show()\n\n# ============================================================================\n# FINAL SUMMARY FOR PPT\n# ============================================================================\nprint(\"\\n\" + \"=\"*80)\nprint(\"TRAINING COMPLETE - FINAL SUMMARY\")\nprint(\"=\"*80)\nprint(f\"\\n{'='*80}\")\nprint(f\"{'FINAL MODEL PERFORMANCE':^80}\")\nprint(f\"{'='*80}\")\nprint(f\"\\nValidation Metrics:\")\nprint(f\"  • Accuracy:              {val_acc*100:.2f}%\")\nprint(f\"  • AUC:                   {val_auc:.4f}\")\nprint(f\"  • Precision:             {val_prec:.4f}\")\nprint(f\"  • Recall:                {val_rec:.4f}\")\nprint(f\"  • Quadratic Kappa:       {kappa:.4f} ({interpretation})\")\nprint(f\"\\nTraining Details:\")\nprint(f\"  • Total Epochs:          {len(history.history['accuracy'])}\")\nprint(f\"  • Best Epoch:            {np.argmax(history.history['val_accuracy']) + 1}\")\nprint(f\"  • Final Learning Rate:   {lr_logger.lrs[-1]:.2e}\")\nprint(f\"  • Model Parameters:      {model.count_params():,}\")\nprint(f\"\\nDataset Information:\")\nprint(f\"  • Training Samples:      {X_train.shape[0]:,}\")\nprint(f\"  • Validation Samples:    {X_val.shape[0]:,}\")\nprint(f\"  • Balanced via SMOTE:    ✓\")\nprint(f\"  • Input Resolution:      {IMG_SIZE}×{IMG_SIZE}\")\nprint(f\"\\nModel Files Saved:\")\nprint(f\"  • Final Model:           'best_dr_model_final.keras'\")\nprint(f\"  • Initial Model:         'best_dr_model.keras'\")\nprint(f\"  • Confusion Matrix:      'confusion_matrix_final.png'\")\nprint(f\"  • Training History:      'training_history_complete.png'\")\nprint(f\"  • Prediction Analysis:   'prediction_analysis_final.png'\")\nprint(f\"\\n{'='*80}\")\nprint(f\"Key Implementation Features:\")\nprint(f\"{'='*80}\")\nprint(\"  ✓ SMOTE-balanced dataset (7,170 samples from 2,930)\")\nprint(\"  ✓ CLAHE preprocessing for retinal contrast enhancement\")\nprint(\"  ✓ Custom 4-block CNN architecture with BatchNormalization\")\nprint(\"  ✓ Two-phase training: Initial (70 epochs) + Fine-tuning (20 epochs)\")\nprint(\"  ✓ Adaptive learning rate with ReduceLROnPlateau\")\nprint(\"  ✓ Early stopping with patience for optimal convergence\")\nprint(\"  ✓ 128×128 input resolution (optimal for SMOTE-augmented data)\")\nprint(\"  ✓ Comprehensive evaluation with clinical metrics (Kappa)\")\nprint(f\"{'='*80}\\n\")\n\n# PPT-ready results summary\nprint(\"\\n\" + \"=\"*80)\nprint(\"RESULTS FOR PPT PRESENTATION\")\nprint(\"=\"*80)\nprint(\"\\n📊 Model Architecture:\")\nprint(f\"   Architecture: Custom CNN with 4 Convolutional Blocks\")\nprint(f\"   Input Size: 128×128×3 (RGB with CLAHE)\")\nprint(f\"   Total Parameters: {model.count_params():,}\")\nprint(f\"   Trainable Parameters: {model.count_params():,}\")\nprint(f\"   Model Depth: 4 blocks + 2 FC layers\")\n\nprint(\"\\n📈 Performance Metrics:\")\nprint(f\"   Validation Accuracy: {val_acc*100:.2f}%\")\nprint(f\"   Validation AUC-ROC: {val_auc:.4f}\")\nprint(f\"   Quadratic Weighted Kappa: {kappa:.4f}\")\nprint(f\"   Clinical Agreement: {interpretation}\")\n\nprint(\"\\n🎯 Per-Class Recall (Sensitivity):\")\nfor i, class_name in enumerate(class_names):\n    class_recall = cm[i, i] / cm[i].sum() if cm[i].sum() > 0 else 0\n    print(f\"   {class_name:15s}: {class_recall*100:.1f}%\")\n\nprint(\"\\n⚙️ Training Configuration:\")\nprint(f\"   Initial Learning Rate: {INITIAL_LR}\")\nprint(f\"   Fine-tuning LR: 1e-4\")\nprint(f\"   Batch Size: {BATCH_SIZE}\")\nprint(f\"   Total Training Epochs: {len(history.history['accuracy'])}\")\nprint(f\"   Data Augmentation: SMOTE + CLAHE\")\nprint(f\"   Class Balance: Perfect (1,434 samples per class)\")\n\nprint(\"\\n\" + \"=\"*80)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T15:34:17.018531Z","iopub.execute_input":"2026-01-04T15:34:17.018880Z","iopub.status.idle":"2026-01-04T15:43:27.570421Z","shell.execute_reply.started":"2026-01-04T15:34:17.018855Z","shell.execute_reply":"2026-01-04T15:43:27.569460Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.save('final_dr_model.keras')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T15:44:13.930402Z","iopub.execute_input":"2026-01-04T15:44:13.930725Z","iopub.status.idle":"2026-01-04T15:44:14.113278Z","shell.execute_reply.started":"2026-01-04T15:44:13.930700Z","shell.execute_reply":"2026-01-04T15:44:14.112451Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}