{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":3004,"databundleVersionId":861823,"sourceType":"competition"}],"dockerImageVersionId":31091,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:27:42.528076Z","iopub.execute_input":"2025-07-31T12:27:42.528230Z","iopub.status.idle":"2025-07-31T12:27:42.532887Z","shell.execute_reply.started":"2025-07-31T12:27:42.528216Z","shell.execute_reply":"2025-07-31T12:27:42.532220Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Digit Recognizer: A Comprehensive Walkthrough\n\nThis notebook details the process of building and training a convolutional neural network (CNN) to recognize handwritten digits from the famous MNIST dataset. We will cover everything from data loading and exploration to model building, training with cross-validation, and finally, generating a submission file for the Kaggle competition.\n\n## Table of Contents\n* [1. Introduction](#1.-Introduction)\n* [2. Setup and Dependencies](#2.-Setup-and-Dependencies)\n* [3. Reproducibility](#3.-Reproducibility)\n* [4. Data Loading](#4.-Data-Loading)\n* [5. Exploratory Data Analysis (EDA)](#5.-Exploratory-Data-Analysis-(EDA))\n* [6. Data Preprocessing](#6.-Data-Preprocessing)\n* [7. Data Visualization](#7.-Data-Visualization)\n* [8. Model Configuration and Training Strategy](#8.-Model-Configuration-and-Training-Strategy)\n* [9. Data Preprocessing Pipeline](#9.-Data-Preprocessing-Pipeline)\n* [10. Model Architecture](#10.-Model-Architecture)\n* [11. Model Training with Cross-Validation](#11.-Model-Training-with-Cross-Validation)\n* [12. Performance Evaluation](#12.-Performance-Evaluation)\n* [13. Visualizing Training History](#13.-Visualizing-Training-History)\n* [14. Preparing Test Data](#14.-Preparing-Test-Data)\n* [15. Generating Predictions](#15.-Generating-Predictions)\n* [16. Creating the Submission File](#16.-Creating-the-Submission-File)\n* [17. Final Submission](#17.-Final-Submission)\n* [18. Conclusion](#18.-Conclusion)","metadata":{}},{"cell_type":"markdown","source":"<a id='1.-Introduction'></a>\n## 1. Introduction\n\nWelcome to this comprehensive guide on building a digit recognizer using deep learning. In this notebook, we'll tackle the classic MNIST handwritten digit classification problem. Our goal is to create a model that can accurately identify digits from 0 to 9 based on pixel data from images. We will employ a **Convolutional Neural Network (CNN)**, a powerful type of neural network particularly well-suited for image-based tasks. This notebook will walk you through the entire machine learning workflow, from setting up the environment and exploring the data to building, training, and evaluating our model. We'll also cover best practices like cross-validation to ensure our model is robust and performs well on unseen data. Let's get started!","metadata":{}},{"cell_type":"markdown","source":"<a id='2.-Setup-and-Dependencies'></a>\n## 2. Setup and Dependencies\n\nBefore we dive into the project, it's crucial to import all the necessary libraries. This initial step ensures that we have all the tools we need for data manipulation, visualization, and building our deep learning model. Here, we import **NumPy** for numerical operations, **Pandas** for handling our datasets, **Matplotlib** for plotting and visualizing the digit images, and **TensorFlow**, the core library for building and training our neural network. We also import **StratifiedKFold** from **scikit-learn**, which will be instrumental in our cross-validation strategy, helping us to create balanced folds for training and validation.","metadata":{}},{"cell_type":"code","source":"# Import necessary libraries\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nfrom sklearn.model_selection import StratifiedKFold","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='3.-Reproducibility'></a>\n## 3. Reproducibility\n\nTo ensure that our results are consistent and can be reproduced by others (and by ourselves in future runs), we set a global random seed. Machine learning models often involve random processes, such as the initialization of model weights or the shuffling of data. By setting a seed for both **NumPy** and **TensorFlow**, we guarantee that these random operations will produce the same sequence of numbers every time the code is executed. This is a critical practice for debugging, sharing our work, and ensuring the reliability of our experiments.","metadata":{}},{"cell_type":"code","source":"# Set random seeds for reproducibility across NumPy and TensorFlow\nSEED = 28\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='4.-Data-Loading'></a>\n## 4. Data Loading\n\nThe first step in any machine learning project is to load the data. Here, we use the **Pandas** library to read our training and testing datasets from CSV files. The `train.csv` file contains the pixel values for each image along with a 'label' column that indicates the actual digit. The `test.csv` file contains the pixel values for the images we need to classify for our Kaggle submission. Loading these into pandas DataFrames allows for easy manipulation and inspection of the data, which is essential for the subsequent steps of our analysis.","metadata":{}},{"cell_type":"code","source":"df_train = pd.read_csv('../input/digit-recognizer/train.csv')\ndf_test = pd.read_csv('../input/digit-recognizer/test.csv')","metadata":{"trusted":true,"execution":{"iopub.status.idle":"2025-07-31T12:28:00.597486Z","shell.execute_reply.started":"2025-07-31T12:27:57.795053Z","shell.execute_reply":"2025-07-31T12:28:00.596695Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='5.-Exploratory-Data-Analysis-(EDA)'></a>\n## 5. Exploratory Data Analysis (EDA)\n\nA fundamental step in the machine learning pipeline is to get a feel for the data we're working with. In this section, we perform some initial exploratory data analysis (EDA) to understand the structure and characteristics of our datasets. We will inspect the last few rows of our training and test DataFrames using the `.tail()` method. This gives us a quick preview of the data, showing the pixel values and, for the training set, the corresponding labels. This initial look helps confirm that the data has been loaded correctly and gives us a sense of the format we'll be working with. We'll also check the dimensions of our datasets, examine the unique labels to ensure we have all ten digits (0-9), and check for any missing values. This foundational analysis is key to building an effective and robust model.","metadata":{}},{"cell_type":"code","source":"df_train.tail()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:28:00.599004Z","iopub.execute_input":"2025-07-31T12:28:00.599736Z","iopub.status.idle":"2025-07-31T12:28:00.611024Z","shell.execute_reply.started":"2025-07-31T12:28:00.599712Z","shell.execute_reply":"2025-07-31T12:28:00.610417Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_test.tail()","metadata":{"scrolled":true,"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:28:00.613853Z","iopub.execute_input":"2025-07-31T12:28:00.614135Z","iopub.status.idle":"2025-07-31T12:28:00.631300Z","shell.execute_reply.started":"2025-07-31T12:28:00.614112Z","shell.execute_reply":"2025-07-31T12:28:00.630763Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(df_train.shape, df_test.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:28:00.632709Z","iopub.execute_input":"2025-07-31T12:28:00.632893Z","iopub.status.idle":"2025-07-31T12:28:00.645678Z","shell.execute_reply.started":"2025-07-31T12:28:00.632878Z","shell.execute_reply":"2025-07-31T12:28:00.644994Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pd.unique(sorted(df_train.label))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:28:00.646307Z","iopub.execute_input":"2025-07-31T12:28:00.646469Z","iopub.status.idle":"2025-07-31T12:28:00.673744Z","shell.execute_reply.started":"2025-07-31T12:28:00.646455Z","shell.execute_reply":"2025-07-31T12:28:00.673136Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train['label'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:28:00.674360Z","iopub.execute_input":"2025-07-31T12:28:00.674561Z","iopub.status.idle":"2025-07-31T12:28:00.680522Z","shell.execute_reply.started":"2025-07-31T12:28:00.674546Z","shell.execute_reply":"2025-07-31T12:28:00.679755Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train.isna().sum().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:28:00.681454Z","iopub.execute_input":"2025-07-31T12:28:00.681657Z","iopub.status.idle":"2025-07-31T12:28:00.729003Z","shell.execute_reply.started":"2025-07-31T12:28:00.681634Z","shell.execute_reply":"2025-07-31T12:28:00.728341Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_test.isna().sum().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:28:00.729693Z","iopub.execute_input":"2025-07-31T12:28:00.729937Z","iopub.status.idle":"2025-07-31T12:28:00.755229Z","shell.execute_reply.started":"2025-07-31T12:28:00.729909Z","shell.execute_reply":"2025-07-31T12:28:00.754663Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='6.-Data-Preprocessing'></a>\n## 6. Data Preprocessing\n\nWith a better understanding of our data, we now move to the preprocessing stage. This is where we prepare the data for our machine learning model. The primary goal here is to separate our features (the pixel values of the images) from our target variable (the digit labels). We create `X_train` by dropping the 'label' column from our training DataFrame and `y_train` by selecting only the 'label' column. For the test set, since there are no labels, `X_test` will consist of all the pixel values. We also convert these pandas DataFrames into **NumPy** arrays using the `.values` attribute, as this is the format expected by **TensorFlow**. Finally, we print the shapes of our newly created arrays to verify that the dimensions are correct and that the preprocessing step was successful.","metadata":{}},{"cell_type":"code","source":"X_train = df_train.drop('label', axis= 1).values\nX_test = df_test.values\ny_train = df_train['label'].values\nprint(X_train.shape, y_train.shape, X_test.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:28:00.755824Z","iopub.execute_input":"2025-07-31T12:28:00.756004Z","iopub.status.idle":"2025-07-31T12:28:00.837621Z","shell.execute_reply.started":"2025-07-31T12:28:00.755989Z","shell.execute_reply":"2025-07-31T12:28:00.836984Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='7.-Data-Visualization'></a>\n## 7. Data Visualization\n\nTo gain a more intuitive understanding of our dataset, it's helpful to visualize some of the images. In this step, we'll take a look at a sample image from both the training and test sets. We select an image by its index, reshape the 1D array of 784 pixels into a 2D 28x28 array, and then use **Matplotlib** to display it. For the training image, we also display its corresponding label as the title of the plot. This not only confirms that our data represents actual handwritten digits but also gives us a qualitative sense of the data's characteristics, such as the variation in writing styles.","metadata":{}},{"cell_type":"code","source":"# Select and reshape a sample training image (28x28) for visualization\nINDEX = 1234\n\nnp.set_printoptions(linewidth=120)\nimg_train = X_train[INDEX].reshape(28, 28)\nprint(img_train)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:29:53.885096Z","iopub.status.idle":"2025-07-31T12:29:53.885767Z","shell.execute_reply.started":"2025-07-31T12:29:53.885593Z","shell.execute_reply":"2025-07-31T12:29:53.885609Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Do same thing for a test image\nimg_test = X_test[INDEX].reshape(28, 28)\nprint(img_test)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:29:53.886601Z","iopub.status.idle":"2025-07-31T12:29:53.886877Z","shell.execute_reply.started":"2025-07-31T12:29:53.886716Z","shell.execute_reply":"2025-07-31T12:29:53.886730Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Display the selected training image and its true label\nplt.imshow(img_train)\nplt.title(f'{y_train[INDEX]}')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:29:53.875663Z","iopub.status.idle":"2025-07-31T12:29:53.875978Z","shell.execute_reply.started":"2025-07-31T12:29:53.875825Z","shell.execute_reply":"2025-07-31T12:29:53.875839Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.imshow(img_test)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:29:53.877328Z","iopub.status.idle":"2025-07-31T12:29:53.877530Z","shell.execute_reply.started":"2025-07-31T12:29:53.877432Z","shell.execute_reply":"2025-07-31T12:29:53.877441Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='8.-Model-Configuration-and-Training-Strategy'></a>\n## 8. Model Configuration and Training Strategy\n\nBefore we start building and training our model, it's important to define our training strategy and set up some key hyperparameters. In this section, we establish the configuration for our training process. We define the number of folds for our cross-validation (`NUM_FOLD`), the batch size for training (`BATCH_SIZE`), and parameters for our data pipeline like the shuffle buffer size (`SHUFFLE_SIZE`) and prefetch size (`PREFETCH_SIZE`). We then initialize **StratifiedKFold** from scikit-learn, which will ensure that each fold of our data has a proportional representation of each digit class. We also create empty lists to store the accuracy and loss for each fold, as well as the training history. This systematic approach allows for a more organized and robust training process.","metadata":{}},{"cell_type":"code","source":"# Define cross-validation parameters and data pipeline constants\nNUM_FOLD = 10          # Number of folds for Stratified K-Fold CV\nBATCH_SIZE = 32        # Batch size for training\nSHUFFLE_SIZE = 1000    # Shuffle buffer size for dataset shuffling\nPREFETCH_SIZE = tf.data.AUTOTUNE  # Automatic prefetch tuning for performance\n\n# Stratified K-Fold object to preserve label distribution\nkf = StratifiedKFold(NUM_FOLD, shuffle=True, random_state=SEED)\n\n# Lists to store metrics across folds\nfold_acc_hist, fold_loss_hist = [], []\nhistories = []\n# A boolean variable for logging through KFold loop\nlog_print = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:30:02.783304Z","iopub.execute_input":"2025-07-31T12:30:02.783584Z","iopub.status.idle":"2025-07-31T12:30:02.788297Z","shell.execute_reply.started":"2025-07-31T12:30:02.783563Z","shell.execute_reply":"2025-07-31T12:30:02.787578Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='9.-Data-Preprocessing-Pipeline'></a>\n## 9. Data Preprocessing Pipeline\n\nTo streamline the preprocessing of our image data, we create a sequential model using **TensorFlow's Keras API**. This pipeline will take our raw input data (a 1D array of 784 pixels) and transform it into the format expected by our convolutional neural network. The pipeline consists of two main steps: first, a `Reshape` layer that converts the 1D array into a 2D image of size 28x28 with a single color channel (grayscale). Second, a `Rescaling` layer that normalizes the pixel values from the range [0, 255] to [0, 1]. This normalization is a crucial step that helps the model converge faster and more effectively during training.","metadata":{}},{"cell_type":"code","source":"# Preprocessing pipeline: reshape flat vectors into 28x28x1 and scale pixel values\npreprocessing_data = tf.keras.Sequential([\n    tf.keras.Input(shape= (784,)),\n    tf.keras.layers.Reshape((28, 28, 1)),\n    tf.keras.layers.Rescaling(1./255)\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:30:08.096149Z","iopub.execute_input":"2025-07-31T12:30:08.096626Z","iopub.status.idle":"2025-07-31T12:30:08.104926Z","shell.execute_reply.started":"2025-07-31T12:30:08.096601Z","shell.execute_reply":"2025-07-31T12:30:08.104293Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='10.-Model-Architecture'></a>\n## 10. Model Architecture\n\nHere, we define the architecture of our **Convolutional Neural Network (CNN)**. A CNN is a type of deep learning model that is particularly effective for image classification tasks. Our model is built using the Keras functional API, which offers a flexible way to create complex models.\n\nThe architecture consists of the following layers:\n* **Input Layer**: Specifies the input shape of our data, which is a 28x28 grayscale image.\n* **Convolutional Layer (`Conv2D`)**: This is the core building block of a CNN. It applies a set of learnable filters to the input image, allowing the model to detect features like edges, corners, and textures. We use 32 filters of size 3x3 and a 'relu' activation function.\n* **Max Pooling Layer (`MaxPooling2D`)**: This layer downsamples the feature maps, reducing their spatial dimensions. This helps to make the model more robust to variations in the position of features in the image and also reduces the computational load.\n* **Flatten Layer**: This layer converts the 2D feature maps into a 1D vector, preparing the data to be fed into the dense layers.\n* **Dense Layer**: A fully connected layer with 128 neurons and a 'relu' activation function. This layer learns to combine the features extracted by the convolutional layers to make classifications.\n* **Output Layer**: The final dense layer with 10 neurons, one for each digit class (0-9). We use a 'linear' activation function here because we will be using `SparseCategoricalCrossentropy(from_logits=True)` as our loss function, which is more numerically stable.\n\nThis architecture is a simple yet effective design for the MNIST digit recognition task.","metadata":{}},{"cell_type":"code","source":"# Build a simple CNN model for digit classification\ndef build_digit_model(input_size=(28, 28, 1), num_classes=10):\n    inputs = tf.keras.Input(shape=input_size)\n    x = tf.keras.layers.Conv2D(32, (3, 3), activation='relu')(inputs)\n    x = tf.keras.layers.MaxPooling2D((2, 2))(x)\n    x = tf.keras.layers.Flatten()(x)\n    x = tf.keras.layers.Dense(128, activation='relu')(x)\n    outputs = tf.keras.layers.Dense(num_classes, activation='linear')(x)  # logits output\n\n    model = tf.keras.Model(inputs, outputs)\n    return model\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:30:11.178037Z","iopub.execute_input":"2025-07-31T12:30:11.178304Z","iopub.status.idle":"2025-07-31T12:30:11.183430Z","shell.execute_reply.started":"2025-07-31T12:30:11.178281Z","shell.execute_reply":"2025-07-31T12:30:11.182730Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='11.-Model-Training-with-Cross-Validation'></a>\n## 11. Model Training with Cross-Validation\n\nThis is the core of our notebook, where we train our CNN model using a robust cross-validation strategy. We use **StratifiedKFold** to split our training data into 10 folds. For each fold, we:\n\n1.  **Split the data**: We divide the data into a training set and a validation set.\n2.  **Create `tf.data.Dataset` objects**: We convert our NumPy arrays into `tf.data.Dataset` objects, which are highly efficient for building input pipelines in TensorFlow.\n3.  **Apply preprocessing**: We apply our `preprocessing_data` pipeline to both the training and validation datasets.\n4.  **Build and compile the model**: We create a new instance of our `build_digit_model()` and compile it with the Adam optimizer, sparse categorical crossentropy loss, and accuracy as our metric.\n5.  **Define callbacks**: We use two important callbacks: `EarlyStopping` to prevent overfitting by stopping the training when the validation loss stops improving, and `ReduceLROnPlateau` to reduce the learning rate when the model's performance plateaus.\n6.  **Train the model**: We train the model for up to 50 epochs, using our prepared datasets and callbacks.\n7.  **Evaluate the model**: After training, we evaluate the model on the validation set and record the loss and accuracy for that fold.\n\nThis process is repeated for all 10 folds, ensuring that our model's performance is not dependent on a specific random split of the data.","metadata":{}},{"cell_type":"code","source":"# Perform 10-fold stratified cross-validation\nfor i, (train_idx, val_idx) in enumerate(kf.split(X_train, y_train)):\n    \n    # Split data into training and validation subsets for the current fold\n    X_train_fold, X_val_fold = X_train[train_idx], X_train[val_idx]\n    y_train_fold, y_val_fold = y_train[train_idx], y_train[val_idx]\n    \n    # Convert numpy arrays to TensorFlow Datasets\n    train_ds = tf.data.Dataset.from_tensor_slices((X_train_fold, y_train_fold))\n    val_ds = tf.data.Dataset.from_tensor_slices((X_val_fold, y_val_fold))\n    if log_print:\n        print(f'Train and Validation Datasets created successfully!')\n    \n    # Optimize dataset pipelines: shuffle, batch, prefetch\n    train_ds = (train_ds\n                .cache()\n                .shuffle(SHUFFLE_SIZE)\n                .batch(BATCH_SIZE)\n                .prefetch(PREFETCH_SIZE))\n    val_ds = (val_ds\n              .cache()\n              .batch(BATCH_SIZE)\n              .prefetch(PREFETCH_SIZE))\n    \n    # Apply preprocessing (reshape + rescaling) to datasets\n    processed_train_ds = train_ds.map(lambda x, y: (preprocessing_data(x), y))\n    processed_val_ds = val_ds.map(lambda x, y: (preprocessing_data(x), y))\n    if log_print:\n        print(f'Processing datasets done!')\n    \n    \n    model = build_digit_model()\n    if log_print:\n        model.summary()\n        log_print = False\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n        loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),\n        metrics=['accuracy']\n    )\n    \n    # Define callbacks for early stopping and learning rate reduction\n    early_stopping = tf.keras.callbacks.EarlyStopping(patience=5, restore_best_weights=True)\n    reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(patience=3, factor=0.2, min_lr=1e-5)\n    \n    # Train the model\n    print(f\"----- Fold {i+1}/{NUM_FOLD} -----\")\n    history = model.fit(\n        processed_train_ds,\n        epochs=50,\n        validation_data=processed_val_ds,\n        callbacks=[early_stopping, reduce_lr],\n        verbose=2\n    )\n    \n    # Save training history and evaluate the model on validation set\n    histories.append(history.history)\n    val_loss, val_accuracy = model.evaluate(processed_val_ds, verbose=0)\n    \n    # Record performance metrics for the current fold\n    print(f'Fold {i+1} Loss: {val_loss:.4f}')\n    print(f'Fold {i+1} Accuracy: {val_accuracy:.4f}')\n    fold_acc_hist.append(val_accuracy)\n    fold_loss_hist.append(val_loss)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:30:14.159617Z","iopub.execute_input":"2025-07-31T12:30:14.160132Z","iopub.status.idle":"2025-07-31T12:35:48.494824Z","shell.execute_reply.started":"2025-07-31T12:30:14.160109Z","shell.execute_reply":"2025-07-31T12:35:48.494017Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='12.-Performance-Evaluation'></a>\n## 12. Performance Evaluation\n\nAfter completing our 10-fold cross-validation, we can now assess the overall performance of our model. We calculate the average validation accuracy and loss across all the folds. This gives us a more reliable estimate of how our model is likely to perform on unseen data compared to a single train-validation split. A high average accuracy and low average loss indicate that our model has learned the underlying patterns in the data well and is able to generalize to new examples.","metadata":{}},{"cell_type":"code","source":"# Display average validation accuracy and loss across all folds\nprint(f'Average Validation Accuracy: {np.mean(fold_acc_hist):.4f}')\nprint(f'Average Validation Loss: {np.mean(fold_loss_hist):.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:35:56.743755Z","iopub.execute_input":"2025-07-31T12:35:56.744354Z","iopub.status.idle":"2025-07-31T12:35:56.748607Z","shell.execute_reply.started":"2025-07-31T12:35:56.744327Z","shell.execute_reply":"2025-07-31T12:35:56.747785Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='13.-Visualizing-Training-History'></a>\n## 13. Visualizing Training History\n\nTo get a more detailed look at our model's learning process, we visualize the training and validation metrics from the last fold. We plot the accuracy and loss for both the training and validation sets over the course of the epochs.\n\n* **Accuracy Plot**: This plot shows how the model's accuracy on the training and validation data changes with each epoch. Ideally, both lines should increase and converge. A large gap between the two lines can be a sign of overfitting.\n* **Loss Plot**: This plot shows the model's loss on the training and validation data. We want to see both lines decrease and converge. An increasing validation loss while the training loss decreases is a clear indicator of overfitting.\n\nThese plots are invaluable for diagnosing training issues and understanding the behavior of our model.","metadata":{}},{"cell_type":"code","source":"# Convert the last training history to a DataFrame for easy plotting\nlast_hist = histories[-1]\nhist_df = pd.DataFrame(last_hist)\n\n# Plot training vs validation accuracy and loss\nfig, axs = plt.subplots(1, 2, figsize=(12, 6))\n\naxs[0].plot(hist_df['accuracy'], label='Train Accuracy')\naxs[0].plot(hist_df['val_accuracy'], label='Validation Accuracy')\naxs[0].set_title('Model Accuracy (Last Fold)')\naxs[0].set_xlabel('Epoch', size=10)\naxs[0].set_ylabel('Accuracy', size=10)\naxs[0].legend(loc='lower right')\n\naxs[1].plot(hist_df['loss'], label='Train Loss')\naxs[1].plot(hist_df['val_loss'], label='Validation Loss')\naxs[1].set_title('Model Loss (Last Fold)')\naxs[1].set_xlabel('Epoch', size=10)\naxs[1].set_ylabel('Loss', size=10)\naxs[1].legend(loc='upper right')\n\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:36:01.036286Z","iopub.execute_input":"2025-07-31T12:36:01.036873Z","iopub.status.idle":"2025-07-31T12:36:01.355694Z","shell.execute_reply.started":"2025-07-31T12:36:01.036843Z","shell.execute_reply":"2025-07-31T12:36:01.355016Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='14.-Preparing-Test-Data'></a>\n## 14. Preparing Test Data\n\nNow that we have a trained model, it's time to prepare the test data for prediction. Similar to our training data pipeline, we first convert the test data into a `tf.data.Dataset`. We then apply the same preprocessing steps, including reshaping and rescaling, to ensure that the test data is in the exact same format as the data our model was trained on. This consistency is crucial for obtaining accurate predictions.","metadata":{}},{"cell_type":"code","source":"# Create and preprocess test dataset for inference\ntest_ds = tf.data.Dataset.from_tensor_slices(X_test)\ntest_ds = (test_ds\n           .batch(BATCH_SIZE)\n           .prefetch(PREFETCH_SIZE))\nprocessed_test_ds = test_ds.map(lambda x: preprocessing_data(x))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:36:07.343049Z","iopub.execute_input":"2025-07-31T12:36:07.343603Z","iopub.status.idle":"2025-07-31T12:36:07.809506Z","shell.execute_reply.started":"2025-07-31T12:36:07.343579Z","shell.execute_reply":"2025-07-31T12:36:07.808726Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='15.-Generating-Predictions'></a>\n## 15. Generating Predictions\n\nWith our test data preprocessed, we can now use our trained model to make predictions. We call the `model.predict()` method on our processed test dataset. This will return an array of logits, where each row corresponds to an image and each column represents the model's confidence for a particular digit class. To get the final predicted label, we use `tf.argmax()` to find the index of the highest logit for each image. This index corresponds to the digit that the model believes is represented in the image. We then print a sample prediction to see our model in action.","metadata":{}},{"cell_type":"code","source":"# Generate predictions (logits) on the test dataset and convert to labels\npreds = model.predict(processed_test_ds)\npreds_label = tf.argmax(preds, axis=1)\nprint(f'Sample Prediction: {preds_label[:10]}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:36:10.535565Z","iopub.execute_input":"2025-07-31T12:36:10.536126Z","iopub.status.idle":"2025-07-31T12:36:12.530136Z","shell.execute_reply.started":"2025-07-31T12:36:10.536102Z","shell.execute_reply":"2025-07-31T12:36:12.529338Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='16.-Creating-the-Submission-File'></a>\n## 16. Creating the Submission File\n\nFor the Kaggle competition, we need to format our predictions into a specific CSV file format. The submission file should have two columns: 'ImageId' and 'Label'. The 'ImageId' should be a 1-based index for each image in the test set, and the 'Label' column should contain our model's prediction for that image. We create a pandas DataFrame with these two columns, using the index of the test DataFrame for the 'ImageId' and our predicted labels.","metadata":{}},{"cell_type":"code","source":"# Prepare submission DataFrame with image indices and predicted labels\nsubmission = pd.DataFrame(\n    {'ImageId': df_test.index + 1,  # Kaggle requires 1-based index usually\n     'Label': preds_label.numpy()}\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:36:43.896920Z","iopub.execute_input":"2025-07-31T12:36:43.897278Z","iopub.status.idle":"2025-07-31T12:36:43.902442Z","shell.execute_reply.started":"2025-07-31T12:36:43.897251Z","shell.execute_reply":"2025-07-31T12:36:43.901707Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='17.-Final-Submission'></a>\n## 17. Final Submission\n\nThe final step is to save our submission DataFrame to a CSV file. We use the `.to_csv()` method from pandas, making sure to set `index=False` to avoid writing the DataFrame's index as an extra column. We then print a confirmation message and display the first few rows of our submission file using `.head()` to verify that it has been created correctly. This file is now ready to be uploaded to the Kaggle competition for scoring.","metadata":{}},{"cell_type":"code","source":"# Save predictions to CSV for Kaggle submission\nsubmission.to_csv('/kaggle/working/submission.csv', index= False)\nprint(f'Submission CSV file created successfully!')\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T12:36:45.903879Z","iopub.execute_input":"2025-07-31T12:36:45.904162Z","iopub.status.idle":"2025-07-31T12:36:45.929843Z","shell.execute_reply.started":"2025-07-31T12:36:45.904142Z","shell.execute_reply":"2025-07-31T12:36:45.929234Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id='18.-Conclusion'></a>\n## 18. Conclusion\n\nIn this notebook, we have successfully built, trained, and evaluated a convolutional neural network for handwritten digit recognition. We followed a structured approach, starting with data exploration and preprocessing, followed by model building and a robust cross-validation training strategy. Our final model achieves a high average validation accuracy, demonstrating its effectiveness in classifying the MNIST digits.\n\n**Potential Next Steps:**\n\n* **Data Augmentation**: To make our model even more robust, we could apply data augmentation techniques like random rotations, shifts, and zooms to the training images. This would expose the model to more variations and could improve its generalization performance.\n* **Model Architecture Tuning**: We could experiment with different CNN architectures, such as adding more convolutional or dense layers, using different filter sizes, or incorporating techniques like batch normalization and dropout.\n* **Hyperparameter Optimization**: A more systematic approach to hyperparameter tuning, using techniques like grid search or Bayesian optimization, could help us find the optimal combination of learning rate, batch size, and other parameters.\n* **Ensemble Methods**: Combining the predictions of multiple models (an ensemble) can often lead to better performance than any single model. We could train several different models and average their predictions.\n\nThank you for following along with this digit recognizer project. We hope this comprehensive walkthrough has been both informative and practical.","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}