{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.7.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":4104,"databundleVersionId":46661,"sourceType":"competition"},{"sourceId":265751,"sourceType":"datasetVersion","datasetId":110097},{"sourceId":2269470,"sourceType":"datasetVersion","datasetId":1366461}],"dockerImageVersionId":30163,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# =========================\n# IMPORTS & CONFIG\n# =========================\nimport os\nimport random\nimport itertools\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom PIL import Image\nimport cv2\n\nimport tensorflow as tf\nimport tensorflow.keras.backend as K\nfrom tensorflow.keras.applications import InceptionResNetV2\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.layers import (Input, Conv2D, Dropout, Dense, GlobalAveragePooling2D,\n                                     Lambda, multiply, BatchNormalization)\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.callbacks import (ModelCheckpoint, EarlyStopping, ReduceLROnPlateau)\nfrom tensorflow.keras.metrics import top_k_categorical_accuracy\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (confusion_matrix, classification_report, roc_curve, auc,\n                             precision_recall_curve, average_precision_score)\nfrom sklearn.preprocessing import label_binarize\nfrom sklearn.manifold import TSNE\n\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED); tf.random.set_seed(SEED)\n\nprint(\"Imports ready\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T11:17:01.339151Z","iopub.execute_input":"2025-08-20T11:17:01.339523Z","iopub.status.idle":"2025-08-20T11:17:09.582673Z","shell.execute_reply.started":"2025-08-20T11:17:01.339422Z","shell.execute_reply":"2025-08-20T11:17:09.581751Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import shutil\n# folder_to_delete = \"/kaggle/working/\"\n# shutil.rmtree(folder_to_delete)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T11:17:09.584147Z","iopub.execute_input":"2025-08-20T11:17:09.584395Z","iopub.status.idle":"2025-08-20T11:17:09.588166Z","shell.execute_reply.started":"2025-08-20T11:17:09.584365Z","shell.execute_reply":"2025-08-20T11:17:09.587327Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# DATA Loading\n# =========================\n!unzip ../input/diabetic-retinopathy-detection/trainLabels.csv.zip\n!apt install p7zip-full -y\n!7z x ../input/diabetic-retinopathy-detection/train.zip.001 \"-i!train/11*.jpeg\" -y   # removed restriction\n!mkdir data\n!mv train data/train\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T11:17:09.589433Z","iopub.execute_input":"2025-08-20T11:17:09.589694Z","iopub.status.idle":"2025-08-20T11:17:42.032173Z","shell.execute_reply.started":"2025-08-20T11:17:09.589662Z","shell.execute_reply":"2025-08-20T11:17:42.030878Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# Exploratory data anaylsis\n# =========================\nlabels_df = pd.read_csv(\"./trainLabels.csv\")\nbase_image_dir = os.path.join('.', 'data/train')\n\n# Build paths and filter to existing\nlabels_df['path'] = labels_df['image'].map(lambda x: os.path.join(base_image_dir, f\"{x}.jpeg\"))\nlabels_df['exists'] = labels_df['path'].map(os.path.exists)\ndf = labels_df[labels_df['exists']].copy()\ndf['level'] = df['level'].astype(str)\ndf = df.drop(columns=['exists'])\ndf = df.sample(frac=1, random_state=SEED).reset_index(drop=True)\n\nprint(f\"Total available images: {len(df)}\")\ndisplay(df.head())\n\n# Class distribution (original)\nplt.figure(figsize=(8,4))\ndf['level'].value_counts().sort_index().plot(kind=\"bar\")\nplt.title(\"Original Class Distribution\")\nplt.xlabel(\"Level\"); plt.ylabel(\"Count\")\nplt.show()\n\n# Quick sample grid per class for sanity check\nclasses = sorted(df['level'].unique(), key=lambda x: int(x))\nn_show = min(5, max(df['level'].value_counts()))\nplt.figure(figsize=(3*n_show, 3*len(classes)))\nfor r, c in enumerate(classes):\n    samples = df[df['level']==c].sample(min(n_show, len(df[df['level']==c])), random_state=SEED)\n    for i, (_, row) in enumerate(samples.iterrows()):\n        plt.subplot(len(classes), n_show, r*n_show + i + 1)\n        try:\n            img = Image.open(row['path']).resize((128,128))\n            plt.imshow(img); plt.axis('off')\n        except:\n            plt.text(0.5,0.5,\"(missing)\", ha='center', va='center')\n        if i==0:\n            plt.ylabel(f\"Class {c}\")\nplt.suptitle(\"Sample Images per Class\", y=0.92)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T11:17:42.034865Z","iopub.execute_input":"2025-08-20T11:17:42.035162Z","iopub.status.idle":"2025-08-20T11:17:49.352036Z","shell.execute_reply.started":"2025-08-20T11:17:42.035128Z","shell.execute_reply":"2025-08-20T11:17:49.350976Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# SPLIT + BALANCE (with inline visuals)\n# =========================\ntrain_df, val_df = train_test_split(\n    df, test_size=0.2, stratify=df['level'], random_state=SEED\n)\n\ndef balance_data(class_size, in_df):\n    out = in_df.groupby('level', group_keys=False).apply(\n        lambda x: x.sample(class_size, replace=True, random_state=SEED)\n    ).sample(frac=1, random_state=SEED).reset_index(drop=True)\n    return out\n\nmax_class = train_df['level'].value_counts().max()\ntrain_df_bal = balance_data(max_class, train_df)\n\n# Visualize before/after balancing (inline)\nfig, ax = plt.subplots(1, 2, figsize=(14, 4))\ntrain_df['level'].value_counts().sort_index().plot(kind='bar', ax=ax[0], title=\"Train (Before Balance)\")\ntrain_df_bal['level'].value_counts().sort_index().plot(kind='bar', ax=ax[1], title=\"Train (After Balance)\")\nfor a in ax: a.set_xlabel(\"Level\"); a.set_ylabel(\"Count\")\nplt.show()\n\nprint(f\"Train (balanced): {len(train_df_bal)} | Val: {len(val_df)}\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T11:17:49.353227Z","iopub.execute_input":"2025-08-20T11:17:49.353447Z","iopub.status.idle":"2025-08-20T11:17:49.641797Z","shell.execute_reply.started":"2025-08-20T11:17:49.353422Z","shell.execute_reply":"2025-08-20T11:17:49.640686Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n#  DATA GENERATORS\n# =========================\nIMG_SIZE = (256, 256)\nBATCH = 32\n\ntrain_gen = ImageDataGenerator(rescale=1./255, horizontal_flip=True, zoom_range=0.2)\nval_gen   = ImageDataGenerator(rescale=1./255)\n\ntrain_flow = train_gen.flow_from_dataframe(\n    train_df_bal, x_col=\"path\", y_col=\"level\", directory=\".\",\n    target_size=IMG_SIZE, batch_size=BATCH, class_mode='categorical', shuffle=True, seed=SEED\n)\nval_flow = val_gen.flow_from_dataframe(\n    val_df, x_col=\"path\", y_col=\"level\", directory=\".\",\n    target_size=IMG_SIZE, batch_size=BATCH, class_mode='categorical', shuffle=False\n)\n\n# For building model head\nt_x, t_y = next(train_flow)\nn_classes = t_y.shape[-1]\nclass_indices = train_flow.class_indices\nidx_to_class = {v:k for k,v in class_indices.items()}\nprint(\"Classes:\", idx_to_class)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T11:17:49.643738Z","iopub.execute_input":"2025-08-20T11:17:49.644071Z","iopub.status.idle":"2025-08-20T11:17:55.094841Z","shell.execute_reply.started":"2025-08-20T11:17:49.644026Z","shell.execute_reply":"2025-08-20T11:17:55.093772Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# MODEL (InceptionResNetV2 + attention head)\n# =========================\nin_lay = Input(shape=t_x.shape[1:])\nbase = InceptionResNetV2(input_shape=t_x.shape[1:], include_top=False, weights='imagenet')\nbase.trainable = True  \n\npt_features = base(in_lay)\nbn_features = BatchNormalization()(pt_features)\n\n# Attention block\npt_depth = pt_features.shape[-1]\nattn = Dropout(0.5)(bn_features)\nattn = Conv2D(64, 1, padding='same', activation='relu')(attn)\nattn = Conv2D(16, 1, padding='same', activation='relu')(attn)\nattn = Conv2D(8,  1, padding='same', activation='relu')(attn)\nattn = Conv2D(1,  1, padding='valid', activation='sigmoid')(attn)\n\nup_w = np.ones((1,1,1,int(pt_depth)))\nup = Conv2D(int(pt_depth), 1, padding='same', activation='linear', use_bias=False, weights=[up_w])\nup.trainable = False\nattn = up(attn)\n\nmask_features = multiply([attn, bn_features])\ngap_features = GlobalAveragePooling2D()(mask_features)\ngap_mask     = GlobalAveragePooling2D()(attn)\ngap = Lambda(lambda x: x[0]/(x[1] + K.epsilon()), name='RescaleGAP')([gap_features, gap_mask])\n\nx = Dropout(0.25)(gap)\nx = Dense(128, activation='relu')(x)\nx = Dropout(0.25)(x)\nout = Dense(n_classes, activation='softmax')(x)\n\nmodel = Model(in_lay, out)\n\ndef top_2_accuracy(y_true, y_pred):\n    return top_k_categorical_accuracy(y_true, y_pred, k=2)\n\nmodel.compile(\n    optimizer='adam',\n    loss='categorical_crossentropy',\n    metrics=['categorical_accuracy', top_2_accuracy]\n)\nmodel.summary()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T11:17:55.096025Z","iopub.execute_input":"2025-08-20T11:17:55.096237Z","iopub.status.idle":"2025-08-20T11:18:10.633051Z","shell.execute_reply.started":"2025-08-20T11:17:55.096212Z","shell.execute_reply":"2025-08-20T11:18:10.632054Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# CALLBACKS\n# =========================\nckpt = ModelCheckpoint(\n    \"dr-best.h5\", monitor=\"val_categorical_accuracy\",\n    mode=\"max\", save_best_only=True, verbose=1\n)\nearly = EarlyStopping(\n    monitor=\"val_categorical_accuracy\", patience=10,\n    restore_best_weights=True, verbose=1\n)\nplateau = ReduceLROnPlateau(\n    monitor=\"val_loss\", factor=0.2, patience=5,\n    min_lr=1e-5, verbose=1\n)\ncallbacks = [ckpt, early, plateau]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T11:18:10.634217Z","iopub.execute_input":"2025-08-20T11:18:10.634453Z","iopub.status.idle":"2025-08-20T11:18:10.640672Z","shell.execute_reply.started":"2025-08-20T11:18:10.634424Z","shell.execute_reply":"2025-08-20T11:18:10.639518Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# TRAIN (with inline learning curves)\n# =========================\nEPOCHS = 50\nsteps_per_epoch   = max(1, train_flow.samples // (2*BATCH))  # be conservative if dataset is large\nvalidation_steps  = max(1, val_flow.samples   // (2*BATCH))\n\nhistory = model.fit(\n    train_flow,\n    epochs=EPOCHS,\n    steps_per_epoch=steps_per_epoch,\n    validation_data=val_flow,\n    validation_steps=validation_steps,\n    callbacks=callbacks,\n    verbose=1\n)\n\n# Learning curves (inline, not centralized elsewhere)\nplt.figure(figsize=(14,5))\nplt.subplot(1,2,1)\nplt.plot(history.history['categorical_accuracy'], label='Train Acc')\nplt.plot(history.history['val_categorical_accuracy'], label='Val Acc')\nplt.title('Accuracy'); plt.xlabel('Epoch'); plt.ylabel('Acc'); plt.legend()\n\nplt.subplot(1,2,2)\nplt.plot(history.history['loss'], label='Train Loss')\nplt.plot(history.history['val_loss'], label='Val Loss')\nplt.title('Loss'); plt.xlabel('Epoch'); plt.ylabel('Loss'); plt.legend()\nplt.show()\n\n# Save full model & weights\nmodel.save(\"dr-final.h5\")\nmodel.save_weights(\"dr-weights.h5\")\nprint(\"Saved: dr-final.h5 (full), dr-weights.h5 (weights)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-20T11:18:10.641932Z","iopub.execute_input":"2025-08-20T11:18:10.642166Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# EVALUATION (confusion matrix, report, ROC/PR)\n# =========================\n# Predict all validation samples\nval_flow.reset()\ny_prob = model.predict(val_flow, verbose=1)\ny_pred = np.argmax(y_prob, axis=1)\ny_true = val_flow.classes\n\n# Confusion matrix\ncm = confusion_matrix(y_true, y_pred)\nplt.figure(figsize=(6,5))\nsns.heatmap(cm, annot=True, fmt='d',\n            xticklabels=[idx_to_class[i] for i in range(n_classes)],\n            yticklabels=[idx_to_class[i] for i in range(n_classes)])\nplt.title(\"Confusion Matrix\")\nplt.xlabel(\"Predicted\"); plt.ylabel(\"True\")\nplt.tight_layout(); plt.show()\n\n# Classification report\nreport = classification_report(\n    y_true, y_pred, target_names=[idx_to_class[i] for i in range(n_classes)]\n)\nprint(report)\n\n# ROC / PR per class (One-vs-Rest)\ny_true_bin = label_binarize(y_true, classes=list(range(n_classes)))\n\n# ROC\nplt.figure(figsize=(7,6))\nfor i in range(n_classes):\n    fpr, tpr, _ = roc_curve(y_true_bin[:, i], y_prob[:, i])\n    roc_auc = auc(fpr, tpr)\n    plt.plot(fpr, tpr, label=f\"{idx_to_class[i]} (AUC={roc_auc:.2f})\")\nplt.plot([0,1],[0,1],'k--')\nplt.title(\"ROC Curves (OvR)\")\nplt.xlabel(\"False Positive Rate\"); plt.ylabel(\"True Positive Rate\")\nplt.legend(); plt.tight_layout(); plt.show()\n\n# Precision-Recall\nplt.figure(figsize=(7,6))\nfor i in range(n_classes):\n    prec, rec, _ = precision_recall_curve(y_true_bin[:, i], y_prob[:, i])\n    ap = average_precision_score(y_true_bin[:, i], y_prob[:, i])\n    plt.plot(rec, prec, label=f\"{idx_to_class[i]} (AP={ap:.2f})\")\nplt.title(\"Precision-Recall Curves\")\nplt.xlabel(\"Recall\"); plt.ylabel(\"Precision\")\nplt.legend(); plt.tight_layout(); plt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# GRAD-CAM (automatic last conv + multiple samples)\n# =========================\ndef find_last_conv_layer(mdl):\n    # Find last Conv2D layer name\n    for layer in reversed(mdl.layers):\n        if isinstance(layer, tf.keras.layers.Conv2D):\n            return layer.name\n    raise ValueError(\"No Conv2D layer found.\")\n\nlast_conv_layer_name = find_last_conv_layer(model)\nprint(\"Last Conv Layer:\", last_conv_layer_name)\n\ndef get_gradcam_heatmap(mdl, img_array, class_idx, last_layer_name):\n    grad_model = tf.keras.models.Model(\n        [mdl.inputs], [mdl.get_layer(last_layer_name).output, mdl.output]\n    )\n    with tf.GradientTape() as tape:\n        conv_out, preds = grad_model(img_array)\n        loss = preds[:, class_idx]\n    grads = tape.gradient(loss, conv_out)\n    pooled_grads = tf.reduce_mean(grads, axis=(0,1,2))\n    conv_out = conv_out[0]\n    heatmap = conv_out @ pooled_grads[..., tf.newaxis]\n    heatmap = tf.squeeze(heatmap)\n    heatmap = np.maximum(heatmap, 0) / (np.max(heatmap) + 1e-8)\n    return heatmap.numpy()\n\ndef show_gradcam_on_image(path, heatmap, alpha=0.4):\n    img_bgr = cv2.imread(path)\n    if img_bgr is None:\n        return None\n    hmap = cv2.resize(heatmap, (img_bgr.shape[1], img_bgr.shape[0]))\n    hmap = np.uint8(255 * hmap)\n    hmap = cv2.applyColorMap(hmap, cv2.COLORMAP_JET)\n    superimposed = cv2.addWeighted(hmap, alpha, img_bgr, 1-alpha, 0)\n    return cv2.cvtColor(superimposed, cv2.COLOR_BGR2RGB)\n\n# Pick a few validation samples to visualize\nsample_paths = val_df.sample(3, random_state=SEED)['path'].tolist()\nplt.figure(figsize=(15,5))\nfor i, p in enumerate(sample_paths):\n    img = Image.open(p).resize(IMG_SIZE)\n    arr = np.expand_dims(np.array(img)/255.0, axis=0)\n    probs = model.predict(arr, verbose=0)[0]\n    pred_idx = int(np.argmax(probs))\n    heat = get_gradcam_heatmap(model, arr, pred_idx, last_conv_layer_name)\n    overlay = show_gradcam_on_image(p, heat, alpha=0.45)\n    plt.subplot(1, 3, i+1)\n    if overlay is not None:\n        plt.imshow(overlay)\n        plt.title(f\"Pred: {idx_to_class[pred_idx]} ({probs[pred_idx]:.2f})\")\n    else:\n        plt.text(0.5,0.5,\"(image not found)\", ha='center', va='center')\n    plt.axis('off')\nplt.suptitle(\"Grad-CAM on Random Validation Images\")\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# FEATURE SPACE t-SNE (on penultimate layer)\n# =========================\n# Build a feature extractor up to Dense(128) layer\npenultimate_layer = None\nfor layer in model.layers[::-1]:\n    if isinstance(layer, tf.keras.layers.Dense) and layer.output_shape[-1] == 128:\n        penultimate_layer = layer.name\n        break\n\nif penultimate_layer:\n    feat_model = tf.keras.Model(inputs=model.input,\n                                outputs=model.get_layer(penultimate_layer).output)\n\n    # Extract features for a subset (to keep t-SNE quick)\n    N_EMB = min(1500, val_flow.samples)\n    val_flow.reset()\n    feats = []\n    labels_sub = []\n    seen = 0\n    for xb, yb in val_flow:\n        emb = feat_model.predict(xb, verbose=0)\n        feats.append(emb); labels_sub.append(np.argmax(yb, axis=1))\n        seen += xb.shape[0]\n        if seen >= N_EMB:\n            break\n    X = np.concatenate(feats, axis=0)[:N_EMB]\n    y_small = np.concatenate(labels_sub, axis=0)[:N_EMB]\n\n    X_tsne = TSNE(n_components=2, random_state=SEED, init='pca', learning_rate='auto').fit_transform(X)\n    plt.figure(figsize=(7,6))\n    for i in range(n_classes):\n        idx = (y_small == i)\n        plt.scatter(X_tsne[idx,0], X_tsne[idx,1], s=10, label=idx_to_class[i], alpha=0.7)\n    plt.title(\"t-SNE of Penultimate Layer Embeddings (Val subset)\")\n    plt.legend(markerscale=2)\n    plt.tight_layout(); plt.show()\nelse:\n    print(\"t-SNE skipped: penultimate Dense(128) layer not found.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}