{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":16017,"databundleVersionId":708189}],"dockerImageVersionId":31328,"isInternetEnabled":false,"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)\nfrom pathlib import Path\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":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-11T06:36:35.862645Z","iopub.execute_input":"2026-04-11T06:36:35.862933Z","iopub.status.idle":"2026-04-11T06:36:35.875552Z","shell.execute_reply.started":"2026-04-11T06:36:35.862913Z","shell.execute_reply":"2026-04-11T06:36:35.874867Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"__author__ = \"Will Hinton\"\nprint(f\"Author: {__author__}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T06:22:27.120883Z","iopub.execute_input":"2026-04-11T06:22:27.121101Z","iopub.status.idle":"2026-04-11T06:22:27.128275Z","shell.execute_reply.started":"2026-04-11T06:22:27.121079Z","shell.execute_reply":"2026-04-11T06:22:27.127364Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 0 - Library Imports and Reproducibility\nimport os\nimport random\nimport warnings\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    accuracy_score,\n    classification_report,\n    confusion_matrix,\n    ConfusionMatrixDisplay,\n    log_loss\n)\nfrom sklearn.decomposition import PCA\nfrom sklearn.neural_network import MLPClassifier\n\nimport torch\n\nwarnings.filterwarnings(\"ignore\")\n\n# Reproducibility\nRANDOM_STATE = 42\nrandom.seed(RANDOM_STATE)\nnp.random.seed(RANDOM_STATE)\ntorch.manual_seed(RANDOM_STATE)\n\nplt.rcParams[\"figure.figsize\"] = (8, 5)\nplt.rcParams[\"axes.grid\"] = True\n\nDATA_DIR = Path(\".\")\nDATA_DIR = Path(\"/kaggle/input/competitions/Kannada-MNIST/\")\n\nprint(\"Libraries imported successfully.\")\nprint(\"Random seed set to:\", RANDOM_STATE)\nprint(\"PyTorch version:\", torch.__version__)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T06:28:11.377108Z","iopub.execute_input":"2026-04-11T06:28:11.377358Z","iopub.status.idle":"2026-04-11T06:28:11.386000Z","shell.execute_reply.started":"2026-04-11T06:28:11.377340Z","shell.execute_reply":"2026-04-11T06:28:11.385209Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 1 - Load and Preprocess Data Files\ntrain_path = DATA_DIR / \"train.csv\"\ntest_path = DATA_DIR / \"test.csv\"\nsubmission_path = DATA_DIR / \"sample_submission.csv\"\n\nassert train_path.exists(), f\"Missing file: {train_path}\"\nassert test_path.exists(), f\"Missing file: {test_path}\"\nassert submission_path.exists(), f\"Missing file: {submission_path}\"\n\ntrain_df = pd.read_csv(train_path)\ntest_df = pd.read_csv(test_path)\nsample_submission_df = pd.read_csv(submission_path)\n\nprint(\"train.csv shape:\", train_df.shape)\nprint(\"test.csv shape:\", test_df.shape)\n\ndisplay(train_df.head())\ndisplay(test_df.head())\n\n# Basic data quality checks\nmissing_summary = pd.DataFrame({\n    \"train_missing\": train_df.isna().sum(),\n})\nprint(\"Total missing values in train.csv:\", int(train_df.isna().sum().sum()))\nprint(\"Total missing values in test.csv:\", int(test_df.isna().sum().sum()))\n\n# Separate features and target\nX = train_df.drop(columns=[\"label\"]).copy()\ny = train_df[\"label\"].copy()\n\n# Competition test set has an id column plus pixel features\ntest_ids = test_df[\"id\"].copy()\nX_competition = test_df.drop(columns=[\"id\"]).copy()\n\n# Normalize pixel values to [0, 1]\nX = X.astype(\"float32\") / 255.0\nX_competition = X_competition.astype(\"float32\") / 255.0\n\n# Stratified split for internal model evaluation\nX_train, X_valid, y_train, y_valid = train_test_split(\n    X,\n    y,\n    test_size=0.20,\n    stratify=y,\n    random_state=RANDOM_STATE\n)\n\nprint(\"\\nSplit summary\")\nprint(\"X_train:\", X_train.shape, \"y_train:\", y_train.shape)\nprint(\"X_valid:\", X_valid.shape, \"y_valid:\", y_valid.shape)\nprint(\"X_competition:\", X_competition.shape)\n\n# Optional PyTorch-ready tensors for preprocessing compatibility / inspection\nX_train_tensor = torch.tensor(X_train.values, dtype=torch.float32)\ny_train_tensor = torch.tensor(y_train.values, dtype=torch.long)\n\nprint(\"\\nPyTorch-ready tensor shapes\")\nprint(\"X_train_tensor:\", tuple(X_train_tensor.shape))\nprint(\"y_train_tensor:\", tuple(y_train_tensor.shape))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T06:28:14.789625Z","iopub.execute_input":"2026-04-11T06:28:14.789909Z","iopub.status.idle":"2026-04-11T06:28:18.290143Z","shell.execute_reply.started":"2026-04-11T06:28:14.789889Z","shell.execute_reply":"2026-04-11T06:28:18.289469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 2 - Brief EDA\n\n# Class distribution\nlabel_counts = y.value_counts().sort_index()\n\nplt.figure()\nplt.bar(label_counts.index.astype(str), label_counts.values)\nplt.title(\"Class Distribution in Kannada-MNIST Training Set\")\nplt.xlabel(\"Digit Label\")\nplt.ylabel(\"Count\")\nplt.show()\n\n# Plot a few example images\nfig, axes = plt.subplots(2, 5, figsize=(12, 5))\nsample_indices = []\nfor digit in range(10):\n    idx = train_df.index[train_df[\"label\"] == digit][0]\n    sample_indices.append(idx)\n\nfor ax, idx in zip(axes.ravel(), sample_indices):\n    image = train_df.drop(columns=[\"label\"]).iloc[idx].values.reshape(28, 28)\n    label = train_df.iloc[idx][\"label\"]\n    ax.imshow(image, cmap=\"gray\")\n    ax.set_title(f\"Label: {label}\")\n    ax.axis(\"off\")\n\nplt.suptitle(\"Sample Kannada Digit Images\")\nplt.tight_layout()\nplt.show()\n\n# Histogram of normalized pixel intensities using a sample for speed and readability\npixel_sample = X_train.sample(n=min(5000, len(X_train)), random_state=RANDOM_STATE).to_numpy().ravel()\n\nplt.figure()\nplt.hist(pixel_sample, bins=30)\nplt.title(\"Histogram of Normalized Pixel Intensities\")\nplt.xlabel(\"Normalized Pixel Value\")\nplt.ylabel(\"Frequency\")\nplt.show()\n\n# PCA scatter plot on a subset for concise visualization\nsubset_size = min(3000, len(X_train))\nsubset_idx = np.random.choice(len(X_train), size=subset_size, replace=False)\nX_subset = X_train.iloc[subset_idx]\ny_subset = y_train.iloc[subset_idx]\n\npca = PCA(n_components=2, random_state=RANDOM_STATE)\nX_pca = pca.fit_transform(X_subset)\n\nplt.figure(figsize=(8, 6))\nscatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y_subset, s=10)\nplt.title(\"PCA Projection of Kannada-MNIST Training Sample\")\nplt.xlabel(\"Principal Component 1\")\nplt.ylabel(\"Principal Component 2\")\nplt.colorbar(scatter, label=\"Digit Label\")\nplt.show()\n\nprint(\"EDA observations:\")\nprint(\"- No missing values were detected in the provided files.\")\nprint(\"- Class frequencies appear balanced across the ten digit classes.\")\nprint(\"- Pixel intensity values are concentrated near zero because of image background.\")\nprint(\"- PCA suggests partial class separation, with some overlap expected for handwritten digits.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T06:28:52.163180Z","iopub.execute_input":"2026-04-11T06:28:52.163465Z","iopub.status.idle":"2026-04-11T06:28:53.913105Z","shell.execute_reply.started":"2026-04-11T06:28:52.163442Z","shell.execute_reply":"2026-04-11T06:28:53.912075Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 3 - Model Building, Training, and Evaluation\n\n# A compact but effective MLP/FNN architecture for flattened 28x28 images\nmlp = MLPClassifier(\n    hidden_layer_sizes=(256, 128),\n    activation=\"relu\",\n    solver=\"adam\",\n    alpha=1e-4,\n    batch_size=256,\n    learning_rate_init=0.001,\n    max_iter=1,          # one epoch at a time so we can track progress\n    warm_start=True,     # continue training across epochs\n    shuffle=True,\n    random_state=RANDOM_STATE,\n)\n\nepochs = 20\nclasses = np.unique(y_train)\n\nhistory = {\n    \"epoch\": [],\n    \"train_loss\": [],\n    \"train_accuracy\": [],\n    \"valid_accuracy\": [],\n    \"valid_log_loss\": [],\n}\n\nfor epoch in range(1, epochs + 1):\n    mlp.fit(X_train, y_train)\n\n    train_pred = mlp.predict(X_train)\n    valid_pred = mlp.predict(X_valid)\n    valid_proba = mlp.predict_proba(X_valid)\n\n    train_acc = accuracy_score(y_train, train_pred)\n    valid_acc = accuracy_score(y_valid, valid_pred)\n    valid_ll = log_loss(y_valid, valid_proba, labels=classes)\n\n    history[\"epoch\"].append(epoch)\n    history[\"train_loss\"].append(mlp.loss_)\n    history[\"train_accuracy\"].append(train_acc)\n    history[\"valid_accuracy\"].append(valid_acc)\n    history[\"valid_log_loss\"].append(valid_ll)\n\n    print(\n        f\"Epoch {epoch:02d} | \"\n        f\"train_loss={mlp.loss_:.4f} | \"\n        f\"train_acc={train_acc:.4f} | \"\n        f\"valid_acc={valid_acc:.4f} | \"\n        f\"valid_log_loss={valid_ll:.4f}\"\n    )\n\nhistory_df = pd.DataFrame(history)\ndisplay(history_df.tail())\n\nbest_valid_acc = history_df[\"valid_accuracy\"].max()\nbest_epoch = int(history_df.loc[history_df[\"valid_accuracy\"].idxmax(), \"epoch\"])\n\nprint(f\"Best validation accuracy: {best_valid_acc:.4f} at epoch {best_epoch}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T06:29:19.235557Z","iopub.execute_input":"2026-04-11T06:29:19.235833Z","iopub.status.idle":"2026-04-11T06:30:08.245747Z","shell.execute_reply.started":"2026-04-11T06:29:19.235794Z","shell.execute_reply":"2026-04-11T06:30:08.244953Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualize training loss and accuracy across epochs\nplt.figure()\nplt.plot(history_df[\"epoch\"], history_df[\"train_loss\"], marker=\"o\")\nplt.title(\"MLP Training Loss by Epoch\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.show()\n\nplt.figure()\nplt.plot(history_df[\"epoch\"], history_df[\"train_accuracy\"], marker=\"o\", label=\"Train Accuracy\")\nplt.plot(history_df[\"epoch\"], history_df[\"valid_accuracy\"], marker=\"o\", label=\"Validation Accuracy\")\nplt.title(\"MLP Accuracy by Epoch\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Accuracy\")\nplt.legend()\nplt.show()\n\n# Validation-set classification diagnostics\ny_valid_pred = mlp.predict(X_valid)\nprint(\"Validation Accuracy:\", round(accuracy_score(y_valid, y_valid_pred), 4))\nprint(\"\\nClassification Report:\\n\")\nprint(classification_report(y_valid, y_valid_pred))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T06:30:42.035331Z","iopub.execute_input":"2026-04-11T06:30:42.035604Z","iopub.status.idle":"2026-04-11T06:30:42.396858Z","shell.execute_reply.started":"2026-04-11T06:30:42.035583Z","shell.execute_reply":"2026-04-11T06:30:42.396198Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 4 - Visualize and Evaluate\n\ncm = confusion_matrix(y_valid, y_valid_pred, labels=mlp.classes_)\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=mlp.classes_)\nfig, ax = plt.subplots(figsize=(8, 8))\ndisp.plot(ax=ax, cmap=\"Blues\", colorbar=False)\nplt.title(\"Confusion Matrix - Validation Set\")\nplt.show()\n\nvalidation_accuracy = accuracy_score(y_valid, y_valid_pred)\nvalidation_error_rate = 1 - validation_accuracy\n\nprint(f\"Validation accuracy: {validation_accuracy:.4f}\")\nprint(f\"Validation error rate: {validation_error_rate:.4f}\")\n\n# Inspect a small set of correctly and incorrectly classified examples\nvalid_images = X_valid.reset_index(drop=True)\nvalid_labels = y_valid.reset_index(drop=True)\nvalid_preds = pd.Series(y_valid_pred).reset_index(drop=True)\n\nmis_idx = np.where(valid_labels != valid_preds)[0][:5]\ncor_idx = np.where(valid_labels == valid_preds)[0][:5]\n\nfig, axes = plt.subplots(2, 5, figsize=(12, 5))\nfor ax, idx in zip(axes[0], cor_idx):\n    ax.imshow(valid_images.iloc[idx].values.reshape(28, 28), cmap=\"gray\")\n    ax.set_title(f\"T:{valid_labels.iloc[idx]} P:{valid_preds.iloc[idx]}\")\n    ax.axis(\"off\")\nfor ax, idx in zip(axes[1], mis_idx):\n    ax.imshow(valid_images.iloc[idx].values.reshape(28, 28), cmap=\"gray\")\n    ax.set_title(f\"T:{valid_labels.iloc[idx]} P:{valid_preds.iloc[idx]}\")\n    ax.axis(\"off\")\n\naxes[0, 0].set_ylabel(\"Correct\", rotation=90, fontsize=10)\naxes[1, 0].set_ylabel(\"Errors\", rotation=90, fontsize=10)\nplt.suptitle(\"Correctly Classified and Misclassified Validation Images\")\nplt.tight_layout()\nplt.show()\n\nprint(\"Interpretive notes:\")\nprint(\"- Strength: the FNN/MLP is simple, fast to train, and fully aligned with the non-CNN assignment constraint.\")\nprint(\"- Strength: training loss generally declines as the model learns useful nonlinear patterns from flattened pixels.\")\nprint(\"- Limitation: unlike CNNs, the model does not explicitly exploit spatial locality in images.\")\nprint(\"- Limitation: flattened inputs can reduce robustness to slight translation, rotation, or stroke-style variation.\")\nprint(\"- Improvement idea: compare this MLP to an autoencoder + classifier pipeline or tune hidden-layer size, regularization, and epochs.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T06:31:06.617418Z","iopub.execute_input":"2026-04-11T06:31:06.617676Z","iopub.status.idle":"2026-04-11T06:31:07.203728Z","shell.execute_reply.started":"2026-04-11T06:31:06.617656Z","shell.execute_reply":"2026-04-11T06:31:07.202383Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 5 - Create fnn_submission.csv in competition format\n\ncompetition_predictions = mlp.predict(X_competition)\n\nfnn_submission = pd.DataFrame({\n    \"id\": test_ids,\n    \"label\": competition_predictions\n})\n\n# Validate output structure against sample submission\nassert list(fnn_submission.columns) == list(sample_submission_df.columns), \"Submission columns do not match sample_submission.csv\"\n\n##output_path = DATA_DIR / \"submission.csv\"\noutput_path = \"/kaggle/working/submission.csv\"\nfnn_submission.to_csv(output_path, index=False)\n\nprint(f\"Saved submission file to: {Path(output_path).resolve()}\")\ndisplay(fnn_submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T07:27:20.215588Z","iopub.execute_input":"2026-04-11T07:27:20.215868Z","iopub.status.idle":"2026-04-11T07:27:20.283151Z","shell.execute_reply.started":"2026-04-11T07:27:20.215846Z","shell.execute_reply":"2026-04-11T07:27:20.280652Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}