{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.12"},"kaggle":{"accelerator":"gpu","dataSources":[{"datasetId":2858199,"sourceId":5043534,"sourceType":"datasetVersion"},{"datasetId":9107696,"sourceId":14405729,"sourceType":"datasetVersion"},{"sourceId":290550400,"sourceType":"kernelVersion"}],"dockerImageVersionId":31234,"isGpuEnabled":true,"isInternetEnabled":true,"language":"python","sourceType":"notebook"},"papermill":{"default_parameters":{},"duration":5443.564748,"end_time":"2026-01-07T23:51:13.968427","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-07T22:20:30.403679","version":"2.6.0"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"0289320499f94cdfa2f92753488d0092":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"0d6f2eb3e62b4dc997b710424009c3f2":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_1d2cf888e8b04a1581f9a7d40a7df561","max":36,"min":0,"orientation":"horizontal","style":"IPY_MODEL_cc0c0a809e084a6cba39a38c35cf46ca","tabbable":null,"tooltip":null,"value":36}},"193e1919564149aea549b831d2c404e3":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"1d2cf888e8b04a1581f9a7d40a7df561":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"37e4049ddf3e4d9fb1f667cfb4a55024":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"4a92954ce56d4cc3ac944715183ed307":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_70e216e1fa804628930cc4f4dc4e24d4","IPY_MODEL_0d6f2eb3e62b4dc997b710424009c3f2","IPY_MODEL_d11bd6d27910433bb8f19e49a7c3e38d"],"layout":"IPY_MODEL_ae18c75d68244093b63cb0391704bcf5","tabbable":null,"tooltip":null}},"70e216e1fa804628930cc4f4dc4e24d4":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_c777041b759c443185784a5788dc9ba1","placeholder":"​","style":"IPY_MODEL_193e1919564149aea549b831d2c404e3","tabbable":null,"tooltip":null,"value":"100%"}},"ae18c75d68244093b63cb0391704bcf5":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"c777041b759c443185784a5788dc9ba1":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"cc0c0a809e084a6cba39a38c35cf46ca":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"d11bd6d27910433bb8f19e49a7c3e38d":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_37e4049ddf3e4d9fb1f667cfb4a55024","placeholder":"​","style":"IPY_MODEL_0289320499f94cdfa2f92753488d0092","tabbable":null,"tooltip":null,"value":" 36/36 [00:00&lt;00:00, 4468.10it/s]"}}},"version_major":2,"version_minor":0}}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"4b4d285e","cell_type":"markdown","source":"# Imports","metadata":{"papermill":{"duration":0.005932,"end_time":"2026-01-07T22:20:32.863150","exception":false,"start_time":"2026-01-07T22:20:32.857218","status":"completed"},"tags":[]}},{"id":"71c91b01","cell_type":"code","source":"import os\nimport cv2\nfrom tqdm.auto import tqdm\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nfrom tensorflow.keras.layers import (Input, Conv2D, BatchNormalization, LeakyReLU, Add, MaxPooling2D,\n                                     GlobalAveragePooling2D, Dense, Dropout, SpatialDropout2D)\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\nfrom tensorflow.keras.regularizers import l2\nfrom sklearn.model_selection import train_test_split","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.execute_input":"2026-01-07T22:20:32.874345Z","iopub.status.busy":"2026-01-07T22:20:32.873811Z","iopub.status.idle":"2026-01-07T22:20:47.877361Z","shell.execute_reply":"2026-01-07T22:20:47.876551Z"},"papermill":{"duration":15.011098,"end_time":"2026-01-07T22:20:47.879164","exception":false,"start_time":"2026-01-07T22:20:32.868066","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"da88af8e","cell_type":"markdown","source":"# Loading Data","metadata":{"papermill":{"duration":0.004802,"end_time":"2026-01-07T22:20:47.889222","exception":false,"start_time":"2026-01-07T22:20:47.884420","status":"completed"},"tags":[]}},{"id":"28c5b447","cell_type":"code","source":"# (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()\narabic_digits = np.load(\"/kaggle/input/hdr-datasets/Arabic-optimized.npz\")\nx_train_arabic, y_train_arabic = arabic_digits['x_train'], arabic_digits['y_train']\nx_test_arabic, y_test_arabic = arabic_digits['x_test'], arabic_digits['y_test']\nprint(f\"Ar - x_train, x_test, y_train, y_test: {x_train_arabic.shape} {x_test_arabic.shape} {y_train_arabic.shape} {y_test_arabic.shape}\")\n\nenglish_digits = np.load(\"/kaggle/input/hdr-datasets/Eng-optimized.npz\")\nx_train_english, y_train_english = english_digits['x_train'], english_digits['y_train']\nx_test_english, y_test_english = english_digits['x_test'], english_digits['y_test']\nprint(f\"En - x_train, x_test, y_train, y_test: {x_train_english.shape} {x_test_english.shape} {y_train_english.shape} {y_test_english.shape}\")\n","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:20:47.901324Z","iopub.status.busy":"2026-01-07T22:20:47.900452Z","iopub.status.idle":"2026-01-07T22:20:54.128569Z","shell.execute_reply":"2026-01-07T22:20:54.127683Z"},"papermill":{"duration":6.235259,"end_time":"2026-01-07T22:20:54.130090","exception":false,"start_time":"2026-01-07T22:20:47.894831","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"16b19487","cell_type":"code","source":"x_train = np.concatenate((x_train_arabic, x_train_english), axis=0)\ny_train = np.concatenate((y_train_arabic, y_train_english), axis=0)\n\nperm = np.random.permutation(len(x_train))\nx_train = x_train[perm]\ny_train = y_train[perm]","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:20:54.141550Z","iopub.status.busy":"2026-01-07T22:20:54.141081Z","iopub.status.idle":"2026-01-07T22:20:54.633841Z","shell.execute_reply":"2026-01-07T22:20:54.633062Z"},"papermill":{"duration":0.500248,"end_time":"2026-01-07T22:20:54.635581","exception":false,"start_time":"2026-01-07T22:20:54.135333","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"cdc3163b","cell_type":"code","source":"x_test = np.concatenate((x_test_arabic, x_test_english), axis=0)\ny_test = np.concatenate((y_test_arabic, y_test_english), axis=0)\n\nperm = np.random.permutation(len(x_test))\nx_test = x_test[perm]\ny_test = y_test[perm]","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:20:54.647043Z","iopub.status.busy":"2026-01-07T22:20:54.646482Z","iopub.status.idle":"2026-01-07T22:20:54.776924Z","shell.execute_reply":"2026-01-07T22:20:54.776054Z"},"papermill":{"duration":0.137916,"end_time":"2026-01-07T22:20:54.778630","exception":false,"start_time":"2026-01-07T22:20:54.640714","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"955ed320","cell_type":"code","source":"print(x_train.shape, y_train.shape)\nprint(x_test.shape, y_test.shape)","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:20:54.790028Z","iopub.status.busy":"2026-01-07T22:20:54.789428Z","iopub.status.idle":"2026-01-07T22:20:54.793657Z","shell.execute_reply":"2026-01-07T22:20:54.792894Z"},"papermill":{"duration":0.011371,"end_time":"2026-01-07T22:20:54.795068","exception":false,"start_time":"2026-01-07T22:20:54.783697","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"4f9a6d91","cell_type":"markdown","source":"#  Check For Data Imbalance","metadata":{"papermill":{"duration":0.004894,"end_time":"2026-01-07T22:20:54.804926","exception":false,"start_time":"2026-01-07T22:20:54.800032","status":"completed"},"tags":[]}},{"id":"67274fa1","cell_type":"code","source":"classes, counts = np.unique(y_train, return_counts=True)\n\nfor c, n in zip(classes, counts):\n    print(f\"Class {c}: {n}\")\n\n# Optional visualization\nplt.bar(classes, counts)\nplt.xlabel(\"Digit\")\nplt.ylabel(\"Count\")\nplt.title(\"Train Class Distribution\")\nplt.show()","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:20:54.815812Z","iopub.status.busy":"2026-01-07T22:20:54.815563Z","iopub.status.idle":"2026-01-07T22:20:54.989462Z","shell.execute_reply":"2026-01-07T22:20:54.988736Z"},"papermill":{"duration":0.181175,"end_time":"2026-01-07T22:20:54.990921","exception":false,"start_time":"2026-01-07T22:20:54.809746","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"03df6a39","cell_type":"code","source":"classes, counts = np.unique(y_test, return_counts=True)\n\nfor c, n in zip(classes, counts):\n    print(f\"Class {c}: {n}\")\n\n# Optional visualization\nplt.bar(classes, counts)\nplt.xlabel(\"Digit\")\nplt.ylabel(\"Count\")\nplt.title(\"Test Class Distribution\")\nplt.show()","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:20:55.003484Z","iopub.status.busy":"2026-01-07T22:20:55.002788Z","iopub.status.idle":"2026-01-07T22:20:55.117168Z","shell.execute_reply":"2026-01-07T22:20:55.116452Z"},"papermill":{"duration":0.122217,"end_time":"2026-01-07T22:20:55.118648","exception":false,"start_time":"2026-01-07T22:20:54.996431","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"0e0c2a70","cell_type":"markdown","source":"# Check For Data Imbalance","metadata":{"papermill":{"duration":0.005613,"end_time":"2026-01-07T22:20:55.130185","exception":false,"start_time":"2026-01-07T22:20:55.124572","status":"completed"},"tags":[]}},{"id":"b4d9927a","cell_type":"code","source":"classes, counts = np.unique(y_test, return_counts=True)\n\nfor c, n in zip(classes, counts):\n    print(f\"Class {c}: {n}\")\n\n# Optional visualization\nplt.bar(classes, counts)\nplt.xlabel(\"Digit\")\nplt.ylabel(\"Count\")\nplt.title(\"Train Class Distribution\")\nplt.show()","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:20:55.142758Z","iopub.status.busy":"2026-01-07T22:20:55.142093Z","iopub.status.idle":"2026-01-07T22:20:55.253763Z","shell.execute_reply":"2026-01-07T22:20:55.253037Z"},"papermill":{"duration":0.119568,"end_time":"2026-01-07T22:20:55.255216","exception":false,"start_time":"2026-01-07T22:20:55.135648","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"b9c08dc8","cell_type":"markdown","source":"# Preprocess Data","metadata":{"papermill":{"duration":0.005747,"end_time":"2026-01-07T22:20:55.266958","exception":false,"start_time":"2026-01-07T22:20:55.261211","status":"completed"},"tags":[]}},{"id":"a228a54f","cell_type":"code","source":"def preprocess_image(img):\n    \"\"\"\n    img: (28, 28, 3) any dtype\n    returns: (28, 28, 1) float32\n    digit = 1, background = 0\n    \"\"\"\n    # 0. Make sure image is uint8\n    img = img.astype(np.uint8)\n\n    # 1. Convert to grayscale\n    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n    \n    # 2. Optional: blur to reduce noise\n    gray = cv2.GaussianBlur(gray, (3, 3), 0)\n    \n    # # 3. Threshold to binary: digits = 1, background = 0\n    _, gray = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n    \n    # 4. Convert 255 -> 1, 0 -> 0\n    gray = gray / 255.0\n    \n    # 5. Add channel dim\n    gray = gray[..., np.newaxis].astype(np.float32)\n    \n    return gray\n\ndef show_image_heatmap(img):\n    plt.imshow(img.squeeze(), cmap=\"gray\")\n    plt.colorbar()\n    plt.show()","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:20:55.279571Z","iopub.status.busy":"2026-01-07T22:20:55.279139Z","iopub.status.idle":"2026-01-07T22:20:55.284240Z","shell.execute_reply":"2026-01-07T22:20:55.283559Z"},"papermill":{"duration":0.012957,"end_time":"2026-01-07T22:20:55.285540","exception":false,"start_time":"2026-01-07T22:20:55.272583","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"a492dae7","cell_type":"code","source":"x_train = np.array([preprocess_image(x) for x in x_train])\nx_test = np.array([preprocess_image(x) for x in x_test])","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:20:55.298541Z","iopub.status.busy":"2026-01-07T22:20:55.298099Z","iopub.status.idle":"2026-01-07T22:21:06.257917Z","shell.execute_reply":"2026-01-07T22:21:06.257070Z"},"papermill":{"duration":10.96835,"end_time":"2026-01-07T22:21:06.259765","exception":false,"start_time":"2026-01-07T22:20:55.291415","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"df41c0f4","cell_type":"code","source":"print(x_train.shape, y_train.shape)\nprint(x_test.shape, y_test.shape)","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:21:06.273456Z","iopub.status.busy":"2026-01-07T22:21:06.273203Z","iopub.status.idle":"2026-01-07T22:21:06.277379Z","shell.execute_reply":"2026-01-07T22:21:06.276542Z"},"papermill":{"duration":0.012526,"end_time":"2026-01-07T22:21:06.278779","exception":false,"start_time":"2026-01-07T22:21:06.266253","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"2277cd4c","cell_type":"code","source":"idx = np.random.randint(0, len(x_train))\nshow_image_heatmap(x_train[idx])\nprint(f\"====={y_train[idx]}========\")","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:21:06.292098Z","iopub.status.busy":"2026-01-07T22:21:06.291302Z","iopub.status.idle":"2026-01-07T22:21:06.418616Z","shell.execute_reply":"2026-01-07T22:21:06.417982Z"},"papermill":{"duration":0.13601,"end_time":"2026-01-07T22:21:06.420586","exception":false,"start_time":"2026-01-07T22:21:06.284576","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"20fc90cb","cell_type":"markdown","source":"# Building Model Arch","metadata":{"papermill":{"duration":0.006403,"end_time":"2026-01-07T22:21:06.433688","exception":false,"start_time":"2026-01-07T22:21:06.427285","status":"completed"},"tags":[]}},{"id":"0d019ca5","cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras.layers import (\n    Input, Conv2D, BatchNormalization, ReLU, LeakyReLU, Add,\n    GlobalAveragePooling2D, Dense, Dropout\n)\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.regularizers import l2\n\n# -------------------------\n# Residual Block (SOTA)\n# -------------------------\n# NOTE: L2 lowered from 1e-4 -> 5e-5. Combined with the augmentation pipeline,\n# 1e-4 on every conv layer was over-regularizing and contributing to the\n# training-accuracy plateau (in addition to the augmentation bug fixed below).\ndef residual_block(x, filters, stride=1, l2_reg=5e-5):\n    shortcut = x\n\n    x = Conv2D(filters, 3, strides=stride, padding='same',\n               use_bias=False, kernel_regularizer=l2(l2_reg))(x)\n    x = BatchNormalization()(x)\n    x = LeakyReLU()(x)\n\n    x = Conv2D(filters, 3, padding='same',\n               use_bias=False, kernel_regularizer=l2(l2_reg))(x)\n    x = BatchNormalization()(x)\n\n    if shortcut.shape[-1] != filters or stride != 1:\n        shortcut = Conv2D(filters, 1, strides=stride,\n                          use_bias=False, kernel_regularizer=l2(l2_reg))(shortcut)\n        shortcut = BatchNormalization()(shortcut)\n\n    x = Add()([x, shortcut])\n    x = LeakyReLU()(x)\n    return x\n\n# -------------------------\n# Build SOTA Model\n# -------------------------\ninputs = Input(shape=(28, 28, 1))\n\nx = Conv2D(64, 3, padding='same', use_bias=False,\n           kernel_regularizer=l2(5e-5))(inputs)\nx = BatchNormalization()(x)\nx = LeakyReLU()(x)\n\n# Residual stages\nx = residual_block(x, 64)\nx = residual_block(x, 64)\n\nx = residual_block(x, 128, stride=2)\nx = residual_block(x, 128)\n\nx = residual_block(x, 256, stride=2)\nx = residual_block(x, 256)\n\n# Head\nx = GlobalAveragePooling2D()(x)\nx = Dropout(0.2)(x)  # lowered from 0.3 - was compounding with augmentation + L2\noutputs = Dense(10, activation='softmax')(x)\n\nmodel = Model(inputs, outputs)\n","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:21:06.447026Z","iopub.status.busy":"2026-01-07T22:21:06.446555Z","iopub.status.idle":"2026-01-07T22:21:08.547097Z","shell.execute_reply":"2026-01-07T22:21:08.546454Z"},"papermill":{"duration":2.109203,"end_time":"2026-01-07T22:21:08.548812","exception":false,"start_time":"2026-01-07T22:21:06.439609","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"326c2626","cell_type":"code","source":"model.compile(\n    optimizer='adam',\n    loss='sparse_categorical_crossentropy',\n    metrics=['accuracy']\n)","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:21:08.562672Z","iopub.status.busy":"2026-01-07T22:21:08.562419Z","iopub.status.idle":"2026-01-07T22:21:08.573532Z","shell.execute_reply":"2026-01-07T22:21:08.573025Z"},"papermill":{"duration":0.019757,"end_time":"2026-01-07T22:21:08.575045","exception":false,"start_time":"2026-01-07T22:21:08.555288","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"954737c8","cell_type":"code","source":"model.summary()","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:21:08.588640Z","iopub.status.busy":"2026-01-07T22:21:08.588387Z","iopub.status.idle":"2026-01-07T22:21:08.645129Z","shell.execute_reply":"2026-01-07T22:21:08.644537Z"},"papermill":{"duration":0.066834,"end_time":"2026-01-07T22:21:08.648072","exception":false,"start_time":"2026-01-07T22:21:08.581238","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"16402313","cell_type":"markdown","source":"# Training Model","metadata":{"papermill":{"duration":0.007191,"end_time":"2026-01-07T22:21:08.663283","exception":false,"start_time":"2026-01-07T22:21:08.656092","status":"completed"},"tags":[]}},{"id":"460f677d","cell_type":"code","source":"def add_random_border(img, max_border=3, p=0.3):\n    \"\"\"\n    img: (H, W, C) float32, values in [0, 1]\n    returns: (H, W, C)\n\n    FIX: previously `p` defaulted to 4 and the skip condition was\n    `np.random.rand() > p`. Since np.random.rand() is always < 1, that\n    condition was NEVER true with p=4, so the border distortion (with a\n    lossy INTER_NEAREST resize) was being applied to literally every\n    training image on every epoch. The model was never seeing clean\n    training data, which caps how well it can fit even the training set\n    (the \"underfitting\" plateau you were seeing around ~97.5% train acc).\n    `p` is now a proper probability (apply the border ~30% of the time).\n    \"\"\"\n    if np.random.rand() > p:\n        return img\n\n    h, w, c = img.shape\n\n    top = np.random.randint(0, max_border + 1)\n    bottom = np.random.randint(0, max_border + 1)\n    left = np.random.randint(0, max_border + 1)\n    right = np.random.randint(0, max_border + 1)\n\n    # Pad with 1s (white/background border)\n    img = np.pad(\n        img,\n        ((top, bottom), (left, right), (0, 0)),\n        mode=\"constant\",\n        constant_values=1.0\n    )\n\n    # Resize back to original size\n    img = cv2.resize(img, (w, h), interpolation=cv2.INTER_NEAREST)\n\n    if img.ndim == 2:\n        img = img[..., np.newaxis]\n\n    return img\n\n\n# Data Augmentation\ndatagen = ImageDataGenerator(\n    rotation_range=10,\n    zoom_range=0.1,\n    width_shift_range=0.1,\n    height_shift_range=0.1,\n    shear_range=0.1,\n    preprocessing_function=add_random_border\n)\n","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:21:08.679653Z","iopub.status.busy":"2026-01-07T22:21:08.678999Z","iopub.status.idle":"2026-01-07T22:21:08.685173Z","shell.execute_reply":"2026-01-07T22:21:08.684438Z"},"papermill":{"duration":0.016037,"end_time":"2026-01-07T22:21:08.686528","exception":false,"start_time":"2026-01-07T22:21:08.670491","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"14ba4ced","cell_type":"code","source":"callbacks = [\n    tf.keras.callbacks.ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.3,\n        patience=4,\n        min_lr=1e-6,\n        verbose=1\n    ),\n    tf.keras.callbacks.EarlyStopping(\n        monitor='val_accuracy',\n        patience=10,\n        restore_best_weights=True\n    )\n]","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:21:08.702417Z","iopub.status.busy":"2026-01-07T22:21:08.702154Z","iopub.status.idle":"2026-01-07T22:21:08.706021Z","shell.execute_reply":"2026-01-07T22:21:08.705446Z"},"papermill":{"duration":0.013646,"end_time":"2026-01-07T22:21:08.707446","exception":false,"start_time":"2026-01-07T22:21:08.693800","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"891317f2","cell_type":"code","source":"x_tr, x_val, y_tr, y_val = train_test_split(\n    x_train, y_train, test_size=0.2, random_state=42\n)\n\ntrain_gen = datagen.flow(x_tr, y_tr, batch_size=64)\nval_gen   = datagen.flow(x_val, y_val, batch_size=64, shuffle=False)","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:21:08.724048Z","iopub.status.busy":"2026-01-07T22:21:08.723466Z","iopub.status.idle":"2026-01-07T22:21:08.880365Z","shell.execute_reply":"2026-01-07T22:21:08.879750Z"},"papermill":{"duration":0.166808,"end_time":"2026-01-07T22:21:08.882051","exception":false,"start_time":"2026-01-07T22:21:08.715243","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"403f2fc2","cell_type":"code","source":"# model.load_weights('/kaggle/input/handwritten-digits-test-set/model weights/model_weights.weights.h5')","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:21:08.898479Z","iopub.status.busy":"2026-01-07T22:21:08.898235Z","iopub.status.idle":"2026-01-07T22:21:08.901490Z","shell.execute_reply":"2026-01-07T22:21:08.900967Z"},"papermill":{"duration":0.013145,"end_time":"2026-01-07T22:21:08.903009","exception":false,"start_time":"2026-01-07T22:21:08.889864","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"7b01a6bf","cell_type":"code","source":"history = model.fit(    \n    train_gen,\n    validation_data=val_gen,\n    epochs=200,\n    callbacks=callbacks\n)","metadata":{"execution":{"iopub.execute_input":"2026-01-07T22:21:08.920299Z","iopub.status.busy":"2026-01-07T22:21:08.920017Z","iopub.status.idle":"2026-01-07T23:49:02.783923Z","shell.execute_reply":"2026-01-07T23:49:02.783057Z"},"papermill":{"duration":5277.085214,"end_time":"2026-01-07T23:49:05.996811","exception":false,"start_time":"2026-01-07T22:21:08.911597","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"1d6827cc","cell_type":"markdown","source":"# Testing Model","metadata":{"papermill":{"duration":3.250316,"end_time":"2026-01-07T23:49:12.375410","exception":false,"start_time":"2026-01-07T23:49:09.125094","status":"completed"},"tags":[]}},{"id":"5f0f392d","cell_type":"code","source":"test_loss, test_acc = model.evaluate(x_test, y_test)\nprint(f\"Test accuracy: {test_acc}, Test Loss: {test_loss}\")","metadata":{"execution":{"iopub.execute_input":"2026-01-07T23:49:18.802182Z","iopub.status.busy":"2026-01-07T23:49:18.801567Z","iopub.status.idle":"2026-01-07T23:49:26.803697Z","shell.execute_reply":"2026-01-07T23:49:26.802947Z"},"papermill":{"duration":11.256807,"end_time":"2026-01-07T23:49:26.805152","exception":false,"start_time":"2026-01-07T23:49:15.548345","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"5bef48ea-97d8-44bf-9120-c29a05b2d42e","cell_type":"markdown","source":"## Per-Class Diagnostics (Confusion Matrix)\n\nThis is the key diagnostic missing from the original notebook: it tells you *which* classes/scripts are actually being confused, rather than just an overall accuracy number.\n","metadata":{}},{"id":"709f6e8a-2416-4e38-be56-be902d5730ae","cell_type":"code","source":"from sklearn.metrics import confusion_matrix, classification_report\nimport seaborn as sns\n\ny_pred_probs = model.predict(x_test)\ny_pred = np.argmax(y_pred_probs, axis=1)\n\nprint(classification_report(y_test, y_pred, digits=4))\n\ncm = confusion_matrix(y_test, y_pred)\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues')\nplt.xlabel('Predicted')\nplt.ylabel('True')\nplt.title('Confusion Matrix (digits 0-9, Arabic+English combined)')\nplt.show()\n\n# Per-class accuracy, sorted worst-first so weak classes jump out\nper_class_acc = cm.diagonal() / cm.sum(axis=1)\nfor cls, acc in sorted(enumerate(per_class_acc), key=lambda t: t[1]):\n    print(f\"Class {cls}: {acc:.4f}\")\n","metadata":{},"outputs":[],"execution_count":null},{"id":"2bd43d46","cell_type":"code","source":"# plt.figure()\n\n# plt.subplot(1, 2, 1)\n# plt.plot(history.history['accuracy'])\n# plt.plot(history.history['val_accuracy'])\n# plt.title('Accuracy')\n# plt.legend(['Train', 'Val'])\n\n# plt.subplot(1, 2, 2)\n# plt.plot(history.history['loss'])\n# plt.plot(history.history['val_loss'])\n# plt.title('Loss')\n# plt.legend(['Train', 'Val'])\n\n# plt.show()","metadata":{"execution":{"iopub.execute_input":"2026-01-07T23:49:33.050792Z","iopub.status.busy":"2026-01-07T23:49:33.050311Z","iopub.status.idle":"2026-01-07T23:49:33.053765Z","shell.execute_reply":"2026-01-07T23:49:33.053127Z"},"papermill":{"duration":3.147409,"end_time":"2026-01-07T23:49:33.055228","exception":false,"start_time":"2026-01-07T23:49:29.907819","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"3b4db979","cell_type":"markdown","source":"# Preparing Real Data Model Will Be Performing On\n## Helping Functions","metadata":{"papermill":{"duration":3.332758,"end_time":"2026-01-07T23:49:39.656982","exception":false,"start_time":"2026-01-07T23:49:36.324224","status":"completed"},"tags":[]}},{"id":"2b68c8e2","cell_type":"code","source":"# Builds our dataframe\ndef load_df(path, _extensions = ['png']):\n    x = []\n    \n    for dirname, _, filenames in (os.walk(path)):\n        for filename in tqdm(filenames):\n            extension = filename.split('.')\n            if(len(extension) > 1 and extension[1] in _extensions):\n                x.append(os.path.join(dirname, filename))\n            \n    dataset = {\n        'image': x\n    }\n    \n    return pd.DataFrame(dataset)\n\n# Show image and return it\ndef read_image(path, show_plt=False):\n    img = plt.imread(path)\n    if show_plt:\n        show_image_heatmap(img)\n    \n    return img\n\n# Preprocessing\n# FIX: this previously used a completely different pipeline than training\n# (fixed threshold=0.85 on a raw grayscale conversion, no blur, no Otsu,\n# and an inverted polarity convention `binary < threshold`). A model\n# trained on Otsu-binarized, blurred images was being evaluated in\n# production on images processed a totally different way, which will\n# hurt real-world accuracy even when val/test accuracy looks fine.\n# This now mirrors preprocess_image exactly, just adding a resize step\n# for real-world images that aren't already 28x28.\ndef preprocess_prediction_image(img, target_size=(28, 28)):\n    \"\"\"\n    img: (H, W, 3) any dtype, any size\n    returns: (1, 28, 28, 1) float32, batch dim included\n    digit = 1, background = 0 (same convention as training)\n    \"\"\"\n    img = img.astype(np.uint8)\n\n    # Resize to model input size first (real photos won't already be 28x28)\n    img = cv2.resize(img, target_size, interpolation=cv2.INTER_AREA)\n\n    # From here on, identical to preprocess_image used at training time\n    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n    gray = cv2.GaussianBlur(gray, (3, 3), 0)\n    _, gray = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n    gray = gray / 255.0\n    gray = gray[..., np.newaxis].astype(np.float32)\n\n    # add batch dim\n    gray = np.expand_dims(gray, axis=0)\n\n    return gray\n\n#  Prediction Function\ndef predict(img, model):\n    # Read image\n    img = read_image(img)\n    img = preprocess_prediction_image(img)\n    \n    # Make prediction\n    prediction = model.predict(img)\n    digit = np.argmax(prediction)\n    return digit, np.max(prediction)\n","metadata":{"execution":{"iopub.execute_input":"2026-01-07T23:49:45.943150Z","iopub.status.busy":"2026-01-07T23:49:45.942438Z","iopub.status.idle":"2026-01-07T23:49:45.950215Z","shell.execute_reply":"2026-01-07T23:49:45.949589Z"},"papermill":{"duration":3.140256,"end_time":"2026-01-07T23:49:45.951602","exception":false,"start_time":"2026-01-07T23:49:42.811346","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"c9f1a2fb","cell_type":"markdown","source":"## Reading Data","metadata":{"papermill":{"duration":3.233942,"end_time":"2026-01-07T23:49:52.317183","exception":false,"start_time":"2026-01-07T23:49:49.083241","status":"completed"},"tags":[]}},{"id":"e0e87e7c","cell_type":"code","source":"df = load_df('/kaggle/input/handwritten-digits-test-set/digits/digits')\ndf.head()","metadata":{"execution":{"iopub.execute_input":"2026-01-07T23:49:58.690928Z","iopub.status.busy":"2026-01-07T23:49:58.690102Z","iopub.status.idle":"2026-01-07T23:49:58.792638Z","shell.execute_reply":"2026-01-07T23:49:58.791953Z"},"papermill":{"duration":3.344542,"end_time":"2026-01-07T23:49:58.794090","exception":false,"start_time":"2026-01-07T23:49:55.449548","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"c707de2c","cell_type":"markdown","source":"## Labeling data ","metadata":{"papermill":{"duration":3.272631,"end_time":"2026-01-07T23:50:05.200198","exception":false,"start_time":"2026-01-07T23:50:01.927567","status":"completed"},"tags":[]}},{"id":"e386d35a","cell_type":"code","source":"# Picking random number\nrng = np.random.default_rng()\nrandom_integers = rng.integers(0, df.shape[0], size=1)[0]\n\n# Displaying sample\nimg = read_image(df.iloc[random_integers].image)\nimg = preprocess_prediction_image(img)\nshow_image_heatmap(img)","metadata":{"execution":{"iopub.execute_input":"2026-01-07T23:50:11.596670Z","iopub.status.busy":"2026-01-07T23:50:11.596385Z","iopub.status.idle":"2026-01-07T23:50:11.733500Z","shell.execute_reply":"2026-01-07T23:50:11.732724Z"},"papermill":{"duration":3.389854,"end_time":"2026-01-07T23:50:11.734857","exception":false,"start_time":"2026-01-07T23:50:08.345003","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"a7f8a7e4","cell_type":"code","source":"df_labels = []\nlabel_confidence = []\nfor img in df.image:\n    digit, confidence = predict(img, model)\n    df_labels.append(digit)\n    label_confidence.append(confidence)","metadata":{"execution":{"iopub.execute_input":"2026-01-07T23:50:18.180865Z","iopub.status.busy":"2026-01-07T23:50:18.180604Z","iopub.status.idle":"2026-01-07T23:50:21.971051Z","shell.execute_reply":"2026-01-07T23:50:21.970446Z"},"papermill":{"duration":6.937162,"end_time":"2026-01-07T23:50:21.972568","exception":false,"start_time":"2026-01-07T23:50:15.035406","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"1510cdc0","cell_type":"code","source":"df['label'] = df_labels\ndf['confidence'] = label_confidence","metadata":{"execution":{"iopub.execute_input":"2026-01-07T23:50:28.283531Z","iopub.status.busy":"2026-01-07T23:50:28.282806Z","iopub.status.idle":"2026-01-07T23:50:28.287524Z","shell.execute_reply":"2026-01-07T23:50:28.286875Z"},"papermill":{"duration":3.140748,"end_time":"2026-01-07T23:50:28.288945","exception":false,"start_time":"2026-01-07T23:50:25.148197","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"91fad6d2","cell_type":"markdown","source":"## Plotting Sample result","metadata":{"papermill":{"duration":3.152884,"end_time":"2026-01-07T23:50:34.699153","exception":false,"start_time":"2026-01-07T23:50:31.546269","status":"completed"},"tags":[]}},{"id":"ab07f4f3","cell_type":"code","source":"idx = np.random.randint(0, df.shape[0])\nimg = read_image(df.iloc[idx].image, True)\nprint(f'==========={df.iloc[idx].label}=============')","metadata":{"execution":{"iopub.execute_input":"2026-01-07T23:50:41.106397Z","iopub.status.busy":"2026-01-07T23:50:41.105644Z","iopub.status.idle":"2026-01-07T23:50:41.241485Z","shell.execute_reply":"2026-01-07T23:50:41.240747Z"},"papermill":{"duration":3.257907,"end_time":"2026-01-07T23:50:41.242879","exception":false,"start_time":"2026-01-07T23:50:37.984972","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"8ab2ab45","cell_type":"markdown","source":"## Saving Results","metadata":{"papermill":{"duration":3.132195,"end_time":"2026-01-07T23:50:47.686277","exception":false,"start_time":"2026-01-07T23:50:44.554082","status":"completed"},"tags":[]}},{"id":"8edf5930","cell_type":"code","source":"df.to_csv('df_data.csv', index=False)","metadata":{"execution":{"iopub.execute_input":"2026-01-07T23:50:54.247406Z","iopub.status.busy":"2026-01-07T23:50:54.246795Z","iopub.status.idle":"2026-01-07T23:50:54.255435Z","shell.execute_reply":"2026-01-07T23:50:54.254911Z"},"papermill":{"duration":3.314726,"end_time":"2026-01-07T23:50:54.256835","exception":false,"start_time":"2026-01-07T23:50:50.942109","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"1c59c375","cell_type":"code","source":"# Save weights to a file\nmodel.save_weights('model_weights.weights.h5')","metadata":{"execution":{"iopub.execute_input":"2026-01-07T23:51:00.604211Z","iopub.status.busy":"2026-01-07T23:51:00.603846Z","iopub.status.idle":"2026-01-07T23:51:00.822933Z","shell.execute_reply":"2026-01-07T23:51:00.822137Z"},"papermill":{"duration":3.397492,"end_time":"2026-01-07T23:51:00.824618","exception":false,"start_time":"2026-01-07T23:50:57.427126","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"9f216f11","cell_type":"code","source":"","metadata":{"papermill":{"duration":3.26606,"end_time":"2026-01-07T23:51:07.262718","exception":false,"start_time":"2026-01-07T23:51:03.996658","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}