{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":14774,"databundleVersionId":875431}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\n\n# Check GPU\n!nvidia-smi\n\n# Check dataset path (Kaggle auto-mounts here)\nprint(\"\\nInput files:\")\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames[:5]:\n        print(os.path.join(dirname, filename))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-03-17T06:45:34.607046Z","iopub.execute_input":"2026-03-17T06:45:34.608038Z","iopub.status.idle":"2026-03-17T06:45:40.629678Z","shell.execute_reply.started":"2026-03-17T06:45:34.607997Z","shell.execute_reply":"2026-03-17T06:45:40.628683Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 2 (UPDATED): Imports ──────────────────────────────────────────────\nimport os, cv2, warnings\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nwarnings.filterwarnings('ignore')\n\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers, mixed_precision\nfrom tensorflow.keras.applications import EfficientNetB4\nfrom tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau, EarlyStopping\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, classification_report, cohen_kappa_score\nfrom sklearn.utils.class_weight import compute_class_weight\n\n# ✅ Enable mixed precision for faster training\nmixed_precision.set_global_policy('mixed_float16')\n\nprint(\"TensorFlow :\", tf.__version__)\nprint(\"GPU        :\", tf.config.list_physical_devices('GPU'))\nprint(\"Policy     :\", mixed_precision.global_policy())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T06:45:46.714381Z","iopub.execute_input":"2026-03-17T06:45:46.714814Z","iopub.status.idle":"2026-03-17T06:46:15.339068Z","shell.execute_reply.started":"2026-03-17T06:45:46.714775Z","shell.execute_reply":"2026-03-17T06:46:15.338292Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\n# ── Hardcode the exact path we already confirmed earlier ──────────────────\n# From your earlier output:\n# /kaggle/input/competitions/aptos2019-blindness-detection/train.csv  ✅\n# /kaggle/input/competitions/aptos2019-blindness-detection/train_images/ ✅\n\nBASE_PATH  = '/kaggle/input/competitions/aptos2019-blindness-detection/'\nTRAIN_CSV  = BASE_PATH + 'train.csv'\nTRAIN_IMGS = BASE_PATH + 'train_images/'\nTEST_CSV   = BASE_PATH + 'test.csv'\nTEST_IMGS  = BASE_PATH + 'test_images/'\nSAVE_DIR   = '/kaggle/working/'\n\n# ── Verify ────────────────────────────────────────────────────────────────\nprint(\"Path verification:\")\nfor name, path in [('train.csv', TRAIN_CSV), ('test.csv', TEST_CSV),\n                   ('train_images/', TRAIN_IMGS), ('test_images/', TEST_IMGS)]:\n    ok = os.path.exists(path)\n    print(f\"  {'✅' if ok else '❌'}  {name:20s} → {path}\")\n\n# ── Config ────────────────────────────────────────────────────────────────\nIMG_SIZE    = 456\nBATCH_SIZE  = 16\nEPOCHS_HEAD = 15\nEPOCHS_FINE = 40\nLR_HEAD     = 1e-3\nLR_FINE     = 3e-4\nNUM_CLASSES = 5\nSEED        = 42\n\nLABEL_MAP = {0:'No DR', 1:'Mild', 2:'Moderate', 3:'Severe', 4:'Proliferative DR'}\nprint(\"\\n✅ Config ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T06:46:41.097507Z","iopub.execute_input":"2026-03-17T06:46:41.098112Z","iopub.status.idle":"2026-03-17T06:46:41.106595Z","shell.execute_reply.started":"2026-03-17T06:46:41.098083Z","shell.execute_reply":"2026-03-17T06:46:41.105870Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 4: Load Data ──────────────────────────────────────────────────────\ntrain_df = pd.read_csv(TRAIN_CSV)\ntest_df  = pd.read_csv(TEST_CSV)\nprint(f\"Train: {len(train_df)}  Test: {len(test_df)}\")\nprint(train_df['diagnosis'].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T06:46:45.902150Z","iopub.execute_input":"2026-03-17T06:46:45.902763Z","iopub.status.idle":"2026-03-17T06:46:45.959038Z","shell.execute_reply.started":"2026-03-17T06:46:45.902737Z","shell.execute_reply":"2026-03-17T06:46:45.958312Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 5: Preprocessing (lighter augmentation) ───────────────────────────\ndef preprocess(path, size=IMG_SIZE):\n    img = cv2.imread(path)\n    if img is None:\n        return np.zeros((size, size, 3), dtype=np.uint8)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n    # Crop black border\n    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n    _, thresh = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY)\n    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL,\n                                    cv2.CHAIN_APPROX_SIMPLE)\n    if contours:\n        x, y, w, h = cv2.boundingRect(max(contours, key=cv2.contourArea))\n        img = img[y:y+h, x:x+w]\n\n    img = cv2.resize(img, (size, size))\n\n    # Ben Graham preprocessing\n    img = cv2.addWeighted(img, 4, cv2.GaussianBlur(img, (0,0), size//40), -4, 128)\n    return img","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T06:46:49.717860Z","iopub.execute_input":"2026-03-17T06:46:49.718158Z","iopub.status.idle":"2026-03-17T06:46:49.724496Z","shell.execute_reply.started":"2026-03-17T06:46:49.718132Z","shell.execute_reply":"2026-03-17T06:46:49.723689Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 6: Save Processed Images ─────────────────────────────────────────\nPROC_TRAIN = '/kaggle/working/proc_train/'\nPROC_TEST  = '/kaggle/working/proc_test/'\nos.makedirs(PROC_TRAIN, exist_ok=True)\nos.makedirs(PROC_TEST,  exist_ok=True)\n\ndef save_processed(df, src_dir, dst_dir, tag='Train'):\n    ok, fail = 0, 0\n    for _, row in df.iterrows():\n        src = src_dir + row['id_code'] + '.png'\n        dst = dst_dir + row['id_code'] + '.png'\n        if os.path.exists(dst):\n            ok += 1; continue\n        img = preprocess(src)\n        if img is not None:\n            cv2.imwrite(dst, cv2.cvtColor(img, cv2.COLOR_RGB2BGR))\n            ok += 1\n        else:\n            fail += 1\n    print(f\"[{tag}] ✅ {ok} saved, ❌ {fail} failed\")\n\nsave_processed(train_df, TRAIN_IMGS, PROC_TRAIN, 'Train')\nsave_processed(test_df,  TEST_IMGS,  PROC_TEST,  'Test')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T06:46:55.167824Z","iopub.execute_input":"2026-03-17T06:46:55.168153Z","iopub.status.idle":"2026-03-17T07:01:40.840032Z","shell.execute_reply.started":"2026-03-17T06:46:55.168128Z","shell.execute_reply":"2026-03-17T07:01:40.839357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 7: Split ──────────────────────────────────────────────────────────\ntrain_df = pd.read_csv(TRAIN_CSV)\ntest_df  = pd.read_csv(TEST_CSV)\n\n# Add .png extension\ntrain_df['id_code'] = train_df['id_code'] + '.png'\ntest_df['id_code']  = test_df['id_code']  + '.png'\n\n# Filter to existing processed files\nproc_files = set(os.listdir(PROC_TRAIN))\ntrain_df   = train_df[train_df['id_code'].isin(proc_files)].reset_index(drop=True)\n\ntrain_df['diagnosis'] = train_df['diagnosis'].astype(str)\n\ntrain_data, val_data = train_test_split(\n    train_df, test_size=0.15,\n    stratify=train_df['diagnosis'], random_state=SEED\n)\nprint(f\"Train: {len(train_data)}  Val: {len(val_data)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T07:01:47.866091Z","iopub.execute_input":"2026-03-17T07:01:47.866806Z","iopub.status.idle":"2026-03-17T07:01:47.898500Z","shell.execute_reply.started":"2026-03-17T07:01:47.866777Z","shell.execute_reply":"2026-03-17T07:01:47.897893Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 8: Generators (lighter augmentation) ─────────────────────────────\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n\n# ✅ Reduced augmentation — 360° rotation was too aggressive for 3662 images\ntrain_datagen = ImageDataGenerator(\n    rescale=1./255,\n    rotation_range=20,           # was 360 — much lighter now\n    width_shift_range=0.05,\n    height_shift_range=0.05,\n    zoom_range=0.1,\n    horizontal_flip=True,\n    vertical_flip=True,\n    brightness_range=[0.9, 1.1],\n    fill_mode='nearest'\n)\nval_datagen = ImageDataGenerator(rescale=1./255)\n\ntrain_gen = train_datagen.flow_from_dataframe(\n    dataframe=train_data, directory=PROC_TRAIN,\n    x_col='id_code', y_col='diagnosis',\n    target_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH_SIZE,\n    class_mode='categorical', shuffle=True, seed=SEED\n)\nval_gen = val_datagen.flow_from_dataframe(\n    dataframe=val_data, directory=PROC_TRAIN,\n    x_col='id_code', y_col='diagnosis',\n    target_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH_SIZE,\n    class_mode='categorical', shuffle=False\n)\n\nimgs, labels = next(iter(train_gen))\nprint(f\"Batch shape: {imgs.shape}  Labels: {labels.shape}\")\nprint(f\"Class indices: {train_gen.class_indices}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T07:01:52.668373Z","iopub.execute_input":"2026-03-17T07:01:52.669039Z","iopub.status.idle":"2026-03-17T07:01:53.565893Z","shell.execute_reply.started":"2026-03-17T07:01:52.668996Z","shell.execute_reply":"2026-03-17T07:01:53.565205Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 9: Build Model ────────────────────────────────────────────────────\ndef build_model(lr=LR_HEAD):\n    base = EfficientNetB4(\n        weights='imagenet',\n        include_top=False,\n        input_shape=(IMG_SIZE, IMG_SIZE, 3)\n    )\n    base.trainable = False\n\n    inp = keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\n    x   = base(inp, training=False)\n    x   = layers.GlobalAveragePooling2D()(x)\n    x   = layers.BatchNormalization()(x)\n    x   = layers.Dropout(0.3)(x)\n    x   = layers.Dense(256, activation='relu')(x)\n    x   = layers.BatchNormalization()(x)\n    x   = layers.Dropout(0.3)(x)\n    # ✅ float32 output (required when using mixed_float16)\n    out = layers.Dense(NUM_CLASSES, activation='softmax', dtype='float32')(x)\n\n    model = keras.Model(inp, out)\n    model.compile(\n        optimizer=keras.optimizers.Adam(lr),\n        # ✅ label_smoothing reduces overconfidence\n        loss=keras.losses.CategoricalCrossentropy(label_smoothing=0.1),\n        metrics=['accuracy',\n                 keras.metrics.AUC(name='auc'),\n                 keras.metrics.Precision(name='precision'),\n                 keras.metrics.Recall(name='recall')]\n    )\n    return model, base\n\nmodel, base_model = build_model(lr=LR_HEAD)\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T07:02:02.610062Z","iopub.execute_input":"2026-03-17T07:02:02.610753Z","iopub.status.idle":"2026-03-17T07:02:07.274357Z","shell.execute_reply.started":"2026-03-17T07:02:02.610719Z","shell.execute_reply":"2026-03-17T07:02:07.273757Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 10: Class Weights ─────────────────────────────────────────────────\ny_ints = train_data['diagnosis'].astype(int).values\ncw_arr = compute_class_weight('balanced', classes=np.unique(y_ints), y=y_ints)\nclass_weights = dict(enumerate(cw_arr))\nprint(\"Class weights:\", {LABEL_MAP[k]: round(v,3) for k,v in class_weights.items()})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T07:02:13.750045Z","iopub.execute_input":"2026-03-17T07:02:13.750744Z","iopub.status.idle":"2026-03-17T07:02:13.760242Z","shell.execute_reply.started":"2026-03-17T07:02:13.750710Z","shell.execute_reply":"2026-03-17T07:02:13.759427Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 11: Callbacks ─────────────────────────────────────────────────────\ndef make_callbacks(monitor='val_accuracy'):\n    return [\n        ModelCheckpoint(\n            SAVE_DIR + 'best_model.keras',\n            monitor=monitor, save_best_only=True, mode='max', verbose=1\n        ),\n        ReduceLROnPlateau(\n            monitor='val_loss', factor=0.5,\n            patience=5, min_lr=1e-7, verbose=1\n        ),\n        EarlyStopping(\n            monitor='val_accuracy', patience=12,\n            restore_best_weights=True, verbose=1\n        )\n    ]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T07:02:22.312966Z","iopub.execute_input":"2026-03-17T07:02:22.313691Z","iopub.status.idle":"2026-03-17T07:02:22.318427Z","shell.execute_reply.started":"2026-03-17T07:02:22.313662Z","shell.execute_reply":"2026-03-17T07:02:22.317759Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 12: Phase 1 — Warmup (frozen base) ───────────────────────────────\nprint(\"=\" * 55)\nprint(\"  PHASE 1 — Warmup: training classification head only\")\nprint(\"=\" * 55)\n\nhistory1 = model.fit(\n    train_gen,\n    epochs=EPOCHS_HEAD,\n    validation_data=val_gen,\n    class_weight=class_weights,\n    callbacks=make_callbacks(),\n    verbose=1\n)\n\n# Print phase 1 best\nbest_val = max(history1.history['val_accuracy'])\nprint(f\"\\n✅ Phase 1 best val_accuracy: {best_val*100:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T07:02:27.212188Z","iopub.execute_input":"2026-03-17T07:02:27.212915Z","iopub.status.idle":"2026-03-17T07:49:38.414843Z","shell.execute_reply.started":"2026-03-17T07:02:27.212888Z","shell.execute_reply":"2026-03-17T07:49:38.414028Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 13: Phase 2 — Fine-tune top layers first ─────────────────────────\nprint(\"=\" * 55)\nprint(\"  PHASE 2 — Fine-tune top 60 layers of EfficientNetB4\")\nprint(\"=\" * 55)\n\n# ✅ Unfreeze only top layers first (not whole model at once)\nbase_model.trainable = True\nfor layer in base_model.layers[:-60]:\n    layer.trainable = False\n\ntrainable_count = sum(1 for l in model.layers if l.trainable)\nprint(f\"Trainable layers: {trainable_count}\")\n\nmodel.compile(\n    optimizer=keras.optimizers.Adam(LR_FINE),\n    loss=keras.losses.CategoricalCrossentropy(label_smoothing=0.1),\n    metrics=['accuracy',\n             keras.metrics.AUC(name='auc'),\n             keras.metrics.Precision(name='precision'),\n             keras.metrics.Recall(name='recall')]\n)\n\nhistory2 = model.fit(\n    train_gen,\n    epochs=EPOCHS_HEAD + 20,\n    initial_epoch=EPOCHS_HEAD,\n    validation_data=val_gen,\n    class_weight=class_weights,\n    callbacks=make_callbacks(),\n    verbose=1\n)\n\nbest_val = max(history2.history['val_accuracy'])\nprint(f\"\\n✅ Phase 2 best val_accuracy: {best_val*100:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T07:49:44.724600Z","iopub.execute_input":"2026-03-17T07:49:44.724980Z","iopub.status.idle":"2026-03-17T08:51:29.791483Z","shell.execute_reply.started":"2026-03-17T07:49:44.724953Z","shell.execute_reply":"2026-03-17T08:51:29.790637Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 14: Phase 3 — Full fine-tune ─────────────────────────────────────\nprint(\"=\" * 55)\nprint(\"  PHASE 3 — Full model fine-tune (all layers)\")\nprint(\"=\" * 55)\n\nbase_model.trainable = True\n\nmodel.compile(\n    optimizer=keras.optimizers.Adam(1e-5),\n    loss=keras.losses.CategoricalCrossentropy(label_smoothing=0.1),\n    metrics=['accuracy',\n             keras.metrics.AUC(name='auc'),\n             keras.metrics.Precision(name='precision'),\n             keras.metrics.Recall(name='recall')]\n)\n\nhistory3 = model.fit(\n    train_gen,\n    epochs=EPOCHS_HEAD + 20 + EPOCHS_FINE,\n    initial_epoch=EPOCHS_HEAD + 20,\n    validation_data=val_gen,\n    class_weight=class_weights,\n    callbacks=make_callbacks(),\n    verbose=1\n)\n\nbest_val = max(history3.history['val_accuracy'])\nprint(f\"\\n✅ Phase 3 best val_accuracy: {best_val*100:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T08:52:42.387821Z","iopub.execute_input":"2026-03-17T08:52:42.388122Z","iopub.status.idle":"2026-03-17T09:33:36.652680Z","shell.execute_reply.started":"2026-03-17T08:52:42.388098Z","shell.execute_reply":"2026-03-17T09:33:36.651767Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 15: Evaluate ──────────────────────────────────────────────────────\nbest_model = keras.models.load_model(SAVE_DIR + 'best_model.keras')\n\nval_gen.reset()\nresults = best_model.evaluate(val_gen, verbose=1)\nprint(f\"\\n{'='*45}\")\nprint(f\"  ✅  Val Accuracy : {results[1]*100:.2f}%\")\nprint(f\"  ✅  Val AUC      : {results[2]*100:.2f}%\")\nprint(f\"{'='*45}\")\n\nval_gen.reset()\npreds       = best_model.predict(val_gen, verbose=1)\npred_labels = np.argmax(preds, axis=1)\ntrue_labels = val_gen.classes\nclass_names = list(LABEL_MAP.values())\n\nprint(\"\\n📊 Classification Report:\\n\")\nprint(classification_report(true_labels, pred_labels, target_names=class_names))\n\nkappa = cohen_kappa_score(true_labels, pred_labels, weights='quadratic')\nprint(f\"🏆 Quadratic Weighted Kappa: {kappa:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T09:51:46.790367Z","iopub.execute_input":"2026-03-17T09:51:46.790715Z","iopub.status.idle":"2026-03-17T09:53:56.540113Z","shell.execute_reply.started":"2026-03-17T09:51:46.790689Z","shell.execute_reply":"2026-03-17T09:53:56.539442Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 16: Confusion Matrix ──────────────────────────────────────────────\ncm = confusion_matrix(true_labels, pred_labels)\nplt.figure(figsize=(9,7))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=class_names, yticklabels=class_names)\nplt.title('Confusion Matrix — Diabetic Retinopathy')\nplt.ylabel('True Label')\nplt.xlabel('Predicted Label')\nplt.tight_layout()\nplt.savefig(SAVE_DIR + 'confusion_matrix.png', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T09:54:02.466977Z","iopub.execute_input":"2026-03-17T09:54:02.467272Z","iopub.status.idle":"2026-03-17T09:54:02.905229Z","shell.execute_reply.started":"2026-03-17T09:54:02.467249Z","shell.execute_reply":"2026-03-17T09:54:02.904385Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Cell 17: Submission ────────────────────────────────────────────────────\ntest_df['diagnosis'] = '0'\ntest_datagen = ImageDataGenerator(rescale=1./255)\ntest_gen = test_datagen.flow_from_dataframe(\n    dataframe=test_df, directory=PROC_TEST,\n    x_col='id_code', y_col='diagnosis',\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=BATCH_SIZE,\n    class_mode='categorical', shuffle=False\n)\n\ntest_preds  = best_model.predict(test_gen, verbose=1)\ntest_labels = np.argmax(test_preds, axis=1)\n\nsubmission = pd.DataFrame({\n    'id_code'  : test_df['id_code'].str.replace('.png','', regex=False),\n    'diagnosis': test_labels\n})\nsubmission.to_csv(SAVE_DIR + 'submission.csv', index=False)\nprint(\"✅ submission.csv saved!\")\nprint(submission['diagnosis'].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T06:45:29.162789Z","iopub.status.idle":"2026-03-17T06:45:29.163292Z","shell.execute_reply.started":"2026-03-17T06:45:29.163152Z","shell.execute_reply":"2026-03-17T06:45:29.163175Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_single(image_path, model=best_model, size=IMG_SIZE):\n    \"\"\"\n    Use this in your Django app.\n    Returns grade, label, confidence and all class probabilities.\n    \"\"\"\n    img   = preprocess(image_path, size=size).astype('float32') / 255.0\n    img   = np.expand_dims(img, axis=0)\n    probs = model.predict(img, verbose=0)[0]\n    grade = int(np.argmax(probs))\n\n    return {\n        'grade'         : grade,\n        'label'         : LABEL_MAP[grade],\n        'confidence'    : f'{probs[grade]*100:.1f}%',\n        'probabilities' : {LABEL_MAP[i]: f'{p*100:.1f}%'\n                           for i, p in enumerate(probs)}\n    }\n\n# Quick test on a training image\nsample_path = PROC_TRAIN + train_df.iloc[0]['id_code'] + '.png'\nresult       = predict_single(sample_path)\nprint(\"Sample prediction:\")\nfor k, v in result.items():\n    print(f\"  {k:15s}: {v}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-17T06:45:29.164740Z","iopub.status.idle":"2026-03-17T06:45:29.165005Z","shell.execute_reply.started":"2026-03-17T06:45:29.164886Z","shell.execute_reply":"2026-03-17T06:45:29.164901Z"}},"outputs":[],"execution_count":null}]}