{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\nimport tensorflow as tf","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:19:20.343386Z","iopub.execute_input":"2026-09-22T18:19:20.343837Z","iopub.status.idle":"2026-09-22T18:19:36.777089Z","shell.execute_reply.started":"2026-09-22T18:19:20.343796Z","shell.execute_reply":"2026-09-22T18:19:36.776485Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = pd.read_csv(\"/kaggle/input/competitions/aptos2019-blindness-detection/train.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:19:47.014574Z","iopub.execute_input":"2026-09-22T18:19:47.015183Z","iopub.status.idle":"2026-09-22T18:19:47.022333Z","shell.execute_reply.started":"2026-09-22T18:19:47.015153Z","shell.execute_reply":"2026-09-22T18:19:47.021174Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(train.head())\nprint(train.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:19:51.102839Z","iopub.execute_input":"2026-09-22T18:19:51.103567Z","iopub.status.idle":"2026-09-22T18:19:51.122029Z","shell.execute_reply.started":"2026-09-22T18:19:51.103537Z","shell.execute_reply":"2026-09-22T18:19:51.121425Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(train[\"diagnosis\"].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:19:55.111611Z","iopub.execute_input":"2026-09-22T18:19:55.112038Z","iopub.status.idle":"2026-09-22T18:19:55.131242Z","shell.execute_reply.started":"2026-09-22T18:19:55.112008Z","shell.execute_reply":"2026-09-22T18:19:55.130514Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train[\"filename\"] = train[\"id_code\"] + \".png\"\nprint(train.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:19:58.524351Z","iopub.execute_input":"2026-09-22T18:19:58.524736Z","iopub.status.idle":"2026-09-22T18:19:58.532014Z","shell.execute_reply.started":"2026-09-22T18:19:58.524708Z","shell.execute_reply":"2026-09-22T18:19:58.531138Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tr_df, val_df = train_test_split(\n    train, test_size=0.2, stratify=train[\"diagnosis\"], random_state=42\n)\n\nprint(len(tr_df), len(val_df))\nprint(tr_df[\"diagnosis\"].value_counts().sort_index())\nprint(val_df[\"diagnosis\"].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:20:01.697349Z","iopub.execute_input":"2026-09-22T18:20:01.698124Z","iopub.status.idle":"2026-09-22T18:20:01.713724Z","shell.execute_reply.started":"2026-09-22T18:20:01.698091Z","shell.execute_reply":"2026-09-22T18:20:01.713015Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.preprocessing.image import ImageDataGenerator\n\ndatagen = ImageDataGenerator(rescale=1./255)\n\ntrain_gen = datagen.flow_from_dataframe(\n    tr_df,\n    directory=\"/kaggle/input/competitions/aptos2019-blindness-detection/train_images\",\n    x_col=\"filename\",\n    y_col=\"diagnosis\",\n    target_size=(224,224),\n    class_mode=\"raw\",\n    batch_size=32,\n)\n\nimages, labels = next(train_gen)\nprint(images.shape, labels.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:20:05.316213Z","iopub.execute_input":"2026-09-22T18:20:05.316619Z","iopub.status.idle":"2026-09-22T18:20:17.263899Z","shell.execute_reply.started":"2026-09-22T18:20:05.316592Z","shell.execute_reply":"2026-09-22T18:20:17.262938Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_gen = datagen.flow_from_dataframe(\n    val_df,\n    directory=\"/kaggle/input/competitions/aptos2019-blindness-detection/train_images\",\n    x_col=\"filename\",\n    y_col=\"diagnosis\",\n    target_size=(224, 224),\n    class_mode=\"raw\",\n    batch_size=32,\n    shuffle=False,\n)\n\nimages, labels = next(val_gen)\nprint(images.shape, labels.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:20:21.703Z","iopub.execute_input":"2026-09-22T18:20:21.703774Z","iopub.status.idle":"2026-09-22T18:20:27.628089Z","shell.execute_reply.started":"2026-09-22T18:20:21.703745Z","shell.execute_reply":"2026-09-22T18:20:27.627287Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.applications import MobileNetV2\nfrom tensorflow.keras import layers, models\n\nbase = MobileNetV2(input_shape=(224, 224, 3), include_top=False, weights=\"imagenet\")\nbase.trainable = False   # freeze it: don't change what it already learned\n\nmodel = models.Sequential([\n    base,\n    layers.GlobalAveragePooling2D(),\n    layers.Dense(64, activation=\"relu\"),\n    layers.Dense(5, activation=\"softmax\"),   # 5 grades\n])\n\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:20:32.11Z","iopub.execute_input":"2026-09-22T18:20:32.110671Z","iopub.status.idle":"2026-09-22T18:20:36.501973Z","shell.execute_reply.started":"2026-09-22T18:20:32.11064Z","shell.execute_reply":"2026-09-22T18:20:36.501345Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.compile(\n    optimizer=\"adam\",\n    loss=\"sparse_categorical_crossentropy\",\n    metrics=[\"accuracy\"],\n)\nmodel.fit(train_gen, validation_data=val_gen, epochs=5)\nmodel.save(\"/kaggle/working/dr_model.h5\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:21:51.273008Z","iopub.execute_input":"2026-09-22T18:21:51.273584Z","iopub.status.idle":"2026-09-22T18:53:06.164586Z","shell.execute_reply.started":"2026-09-22T18:21:51.273555Z","shell.execute_reply":"2026-09-22T18:53:06.163699Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(os.path.exists(\"/kaggle/working/dr_model.h5\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:54:47.28055Z","iopub.execute_input":"2026-09-22T18:54:47.281224Z","iopub.status.idle":"2026-09-22T18:54:47.285442Z","shell.execute_reply.started":"2026-09-22T18:54:47.281189Z","shell.execute_reply":"2026-09-22T18:54:47.284759Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for layer in model.get_layer(\"mobilenetv2_1.00_224\").layers[-10:]:\n    print(layer.name, layer.output.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T18:55:43.039617Z","iopub.execute_input":"2026-09-22T18:55:43.040018Z","iopub.status.idle":"2026-09-22T18:55:43.044969Z","shell.execute_reply.started":"2026-09-22T18:55:43.03999Z","shell.execute_reply":"2026-09-22T18:55:43.044136Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Toy stand-in \"model\" with two named parts\ninputs = tf.keras.Input(shape=(4,))\nhidden = tf.keras.layers.Dense(2, name=\"hidden_layer\")(inputs)\noutputs = tf.keras.layers.Dense(3, activation=\"softmax\")(hidden)\ntoy_model = tf.keras.Model(inputs, outputs)\n\n# A second model that reveals the hidden layer's output too\ngrad_model = tf.keras.Model(\n    toy_model.input,\n    [toy_model.get_layer(\"hidden_layer\").output, toy_model.output]\n)\n\nsample = tf.random.normal((1, 4))\nhidden_vals, preds = grad_model(sample)\nprint(hidden_vals.shape, preds.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:02:36.797367Z","iopub.execute_input":"2026-09-22T19:02:36.798155Z","iopub.status.idle":"2026-09-22T19:02:36.828512Z","shell.execute_reply.started":"2026-09-22T19:02:36.798121Z","shell.execute_reply":"2026-09-22T19:02:36.82798Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"images, labels = next(val_gen)\nsample_image = images[0:1]\ntrue_label = labels[0]\nprint(sample_image.shape, true_label)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:09:08.058444Z","iopub.execute_input":"2026-09-22T19:09:08.05884Z","iopub.status.idle":"2026-09-22T19:09:11.587075Z","shell.execute_reply.started":"2026-09-22T19:09:08.058811Z","shell.execute_reply":"2026-09-22T19:09:11.586422Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inputs = tf.keras.Input(shape=(224, 224, 3))\nx = inputs\nfor layer in model.layers:\n    x = layer(x)\n\nfixed_model = tf.keras.Model(inputs, x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:06:03.963604Z","iopub.execute_input":"2026-09-22T19:06:03.964373Z","iopub.status.idle":"2026-09-22T19:06:03.974676Z","shell.execute_reply.started":"2026-09-22T19:06:03.964332Z","shell.execute_reply":"2026-09-22T19:06:03.973592Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"grad_model = tf.keras.Model(\n    fixed_model.input,\n    [fixed_model.get_layer(\"mobilenetv2_1.00_224\").get_layer(\"out_relu\").output, fixed_model.output]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:06:30.461551Z","iopub.execute_input":"2026-09-22T19:06:30.461993Z","iopub.status.idle":"2026-09-22T19:06:30.474798Z","shell.execute_reply.started":"2026-09-22T19:06:30.461966Z","shell.execute_reply":"2026-09-22T19:06:30.47399Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_pred_original = model.predict(sample_image)\ntest_pred_fixed = fixed_model.predict(sample_image)\nprint(np.allclose(test_pred_original, test_pred_fixed))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:09:18.360716Z","iopub.execute_input":"2026-09-22T19:09:18.36143Z","iopub.status.idle":"2026-09-22T19:09:33.558043Z","shell.execute_reply.started":"2026-09-22T19:09:18.361398Z","shell.execute_reply":"2026-09-22T19:09:33.557259Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inputs = tf.keras.Input(shape=(224, 224, 3))\nconv_output = base(inputs, training=False)\nx = layers.GlobalAveragePooling2D()(conv_output)\nx = model.get_layer(\"dense\")(x)\noutputs = model.get_layer(\"dense_1\")(x)\n\ngrad_model = tf.keras.Model(inputs, [conv_output, outputs])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:12:41.252682Z","iopub.execute_input":"2026-09-22T19:12:41.253067Z","iopub.status.idle":"2026-09-22T19:12:41.264713Z","shell.execute_reply.started":"2026-09-22T19:12:41.253039Z","shell.execute_reply":"2026-09-22T19:12:41.263908Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"check_pred = grad_model.predict(sample_image)[1]\nprint(np.allclose(check_pred, test_pred_original))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:12:57.291798Z","iopub.execute_input":"2026-09-22T19:12:57.29269Z","iopub.status.idle":"2026-09-22T19:13:01.657479Z","shell.execute_reply.started":"2026-09-22T19:12:57.29266Z","shell.execute_reply":"2026-09-22T19:13:01.656839Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_index = int(np.argmax(test_pred_original[0]))\n\nwith tf.GradientTape() as tape:\n    conv_output_val, predictions = grad_model(sample_image)\n    class_score = predictions[:, class_index]\n\ngrads = tape.gradient(class_score, conv_output_val)\nprint(conv_output_val.shape, grads.shape, class_index)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:13:17.508219Z","iopub.execute_input":"2026-09-22T19:13:17.508781Z","iopub.status.idle":"2026-09-22T19:13:18.300364Z","shell.execute_reply.started":"2026-09-22T19:13:17.508754Z","shell.execute_reply":"2026-09-22T19:13:18.29957Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"weights = tf.reduce_mean(grads, axis=(0, 1, 2))          # shape: (1280,)\nheatmap = tf.reduce_sum(conv_output_val[0] * weights, axis=-1)  # shape: (7, 7)\n\nheatmap = tf.maximum(heatmap, 0)          # keep only positive influence\nheatmap = heatmap / tf.reduce_max(heatmap)  # scale to 0-1\nheatmap = heatmap.numpy()\n\nprint(heatmap.shape)\nplt.imshow(heatmap)\nplt.colorbar()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:14:43.147364Z","iopub.execute_input":"2026-09-22T19:14:43.147729Z","iopub.status.idle":"2026-09-22T19:14:43.729535Z","shell.execute_reply.started":"2026-09-22T19:14:43.1477Z","shell.execute_reply":"2026-09-22T19:14:43.728884Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\n\nheatmap_resized = cv2.resize(heatmap, (224, 224))\nheatmap_uint8 = np.uint8(255 * heatmap_resized)\nheatmap_colored = cv2.applyColorMap(heatmap_uint8, cv2.COLORMAP_JET)\nheatmap_colored = cv2.cvtColor(heatmap_colored, cv2.COLOR_BGR2RGB)\n\noriginal = np.uint8(sample_image[0] * 255)   # undo the earlier 1/255 rescale\noverlay = np.uint8(0.6 * original + 0.4 * heatmap_colored)\n\nfig, axes = plt.subplots(1, 3, figsize=(15, 5))\naxes[0].imshow(original); axes[0].set_title(\"Original\"); axes[0].axis(\"off\")\naxes[1].imshow(heatmap_colored); axes[1].set_title(\"Heatmap\"); axes[1].axis(\"off\")\naxes[2].imshow(overlay); axes[2].set_title(\"Overlay\"); axes[2].axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:16:25.99624Z","iopub.execute_input":"2026-09-22T19:16:25.996836Z","iopub.status.idle":"2026-09-22T19:16:26.408648Z","shell.execute_reply.started":"2026-09-22T19:16:25.996808Z","shell.execute_reply":"2026-09-22T19:16:26.40788Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_and_explain(image_array):\n    # image_array: shape (1, 224, 224, 3), already resized and rescaled\n\n    preds = grad_model.predict(image_array)[1]\n    class_index = int(np.argmax(preds[0]))\n\n    with tf.GradientTape() as tape:\n        conv_output_val, predictions = grad_model(image_array)\n        class_score = predictions[:, class_index]\n\n    grads = tape.gradient(class_score, conv_output_val)\n\n    weights = tf.reduce_mean(grads, axis=(0, 1, 2))\n    heatmap = tf.reduce_sum(conv_output_val[0] * weights, axis=-1)\n    heatmap = tf.maximum(heatmap, 0)\n    heatmap = heatmap / tf.reduce_max(heatmap)\n    heatmap = heatmap.numpy()\n\n    heatmap_resized = cv2.resize(heatmap, (224, 224))\n    heatmap_uint8 = np.uint8(255 * heatmap_resized)\n    heatmap_colored = cv2.applyColorMap(heatmap_uint8, cv2.COLORMAP_JET)\n    heatmap_colored = cv2.cvtColor(heatmap_colored, cv2.COLOR_BGR2RGB)\n\n    original = np.uint8(image_array[0] * 255)\n    overlay = np.uint8(0.6 * original + 0.4 * heatmap_colored)\n\n    return class_index, overlay","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:27:18.716529Z","iopub.execute_input":"2026-09-22T19:27:18.717409Z","iopub.status.idle":"2026-09-22T19:27:18.7242Z","shell.execute_reply.started":"2026-09-22T19:27:18.717373Z","shell.execute_reply":"2026-09-22T19:27:18.723268Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"images, labels = next(val_gen)\ntest_img = images[5:6]   # try a different image than before\n\ngrade, overlay_img = predict_and_explain(test_img)\nprint(\"Predicted grade:\", grade)\nplt.imshow(overlay_img)\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:29:27.91713Z","iopub.execute_input":"2026-09-22T19:29:27.917541Z","iopub.status.idle":"2026-09-22T19:29:31.075091Z","shell.execute_reply.started":"2026-09-22T19:29:27.917513Z","shell.execute_reply":"2026-09-22T19:29:31.074493Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"converter = tf.lite.TFLiteConverter.from_keras_model(fixed_model)\ntflite_model = converter.convert()\n\nwith open(\"/kaggle/working/dr_model.tflite\", \"wb\") as f:\n    f.write(tflite_model)\n\nimport os\nprint(os.path.getsize(\"/kaggle/working/dr_model.tflite\") / 1e6, \"MB\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:31:13.11946Z","iopub.execute_input":"2026-09-22T19:31:13.119923Z","iopub.status.idle":"2026-09-22T19:31:22.529234Z","shell.execute_reply.started":"2026-09-22T19:31:13.119882Z","shell.execute_reply":"2026-09-22T19:31:22.528418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"interpreter = tf.lite.Interpreter(model_path=\"/kaggle/working/dr_model.tflite\")\ninterpreter.allocate_tensors()\n\ninput_details = interpreter.get_input_details()\noutput_details = interpreter.get_output_details()\n\ninterpreter.set_tensor(input_details[0]['index'], sample_image.astype(np.float32))\ninterpreter.invoke()\ntflite_pred = interpreter.get_tensor(output_details[0]['index'])\n\nprint(\"Original model:\", np.argmax(test_pred_original))\nprint(\"TFLite model:  \", np.argmax(tflite_pred))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T19:34:09.000337Z","iopub.execute_input":"2026-09-22T19:34:09.000926Z","iopub.status.idle":"2026-09-22T19:34:09.064252Z","shell.execute_reply.started":"2026-09-22T19:34:09.000862Z","shell.execute_reply":"2026-09-22T19:34:09.063557Z"}},"outputs":[],"execution_count":null}]}