{"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":"%pip install lightning torchvision tqdm ipywidgets","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T00:56:53.316456Z","iopub.execute_input":"2026-07-04T00:56:53.317132Z","iopub.status.idle":"2026-07-04T00:56:56.835032Z","shell.execute_reply.started":"2026-07-04T00:56:53.317094Z","shell.execute_reply":"2026-07-04T00:56:56.834069Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data","metadata":{}},{"cell_type":"markdown","source":"## Dataframes","metadata":{}},{"cell_type":"code","source":"import numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nfrom sklearn.model_selection import train_test_split\ninput_folder = '/kaggle/input/competitions/Kannada-MNIST'\n\nfull = pd.read_csv(f\"{input_folder}/train.csv\")\ntrain, val = train_test_split(full, test_size = 0.2, stratify=full[\"label\"], random_state=7)\ntest = pd.read_csv(f\"{input_folder}/Dig-MNIST.csv\")\ninfer = pd.read_csv(f\"{input_folder}/test.csv\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2026-07-04T00:56:56.836589Z","iopub.execute_input":"2026-07-04T00:56:56.836958Z","iopub.status.idle":"2026-07-04T00:57:00.214841Z","shell.execute_reply.started":"2026-07-04T00:56:56.836927Z","shell.execute_reply":"2026-07-04T00:57:00.214206Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train.columns","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T00:57:00.215655Z","iopub.execute_input":"2026-07-04T00:57:00.215928Z","iopub.status.idle":"2026-07-04T00:57:00.222323Z","shell.execute_reply.started":"2026-07-04T00:57:00.215897Z","shell.execute_reply":"2026-07-04T00:57:00.221645Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Datasets","metadata":{}},{"cell_type":"code","source":"from torch.utils.data import Dataset\nimport torch\n\nclass KannadaMnist(Dataset):\n    def __init__(self, df, inference = False, transform = None):\n        super().__init__()\n        self.X, self.y = self._prepare_df(df, inference)\n        self.transform = transform\n        \n    @staticmethod\n    def _prepare_df(df, inference):\n        pixel_cols = [f\"pixel{i}\" for i in range(784)]\n        if inference:\n            y = torch.tensor(df[\"id\"].values, dtype=torch.long)\n        else:\n            y = torch.tensor(df[\"label\"].values, dtype=torch.long)\n            \n        X = torch.tensor(df[pixel_cols].values, dtype=torch.float32).reshape(-1,1,28,28)/255.0\n        return X,y\n    @property\n    def mean_std(self):\n        return self.X.mean(), self.X.std()\n    def __len__(self):\n        return len(self.X)\n    def __getitem__(self,idx):\n        x, y = self.X[idx], self.y[idx]\n        if self.transform is not None:\n            x = self.transform(x)\n        return x,y\n\ntrain_ds_raw = KannadaMnist(train)\nmu, sigma = train_ds_raw.mean_std\nmu, sigma","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T00:57:00.223985Z","iopub.execute_input":"2026-07-04T00:57:00.224226Z","iopub.status.idle":"2026-07-04T00:57:05.415431Z","shell.execute_reply.started":"2026-07-04T00:57:00.224206Z","shell.execute_reply":"2026-07-04T00:57:05.414714Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Transforms","metadata":{}},{"cell_type":"code","source":"from torchvision.transforms import v2\n\ntrain_transform = v2.Compose([\n    v2.RandomApply([\n        v2.RandomAffine(degrees=10, translate=(0.08, 0.08), scale=(0.92, 1.08)),\n    ], p=0.25),\n    v2.RandomApply([\n        v2.RandomRotation(5),\n    ], p=0.5),\n    v2.Normalize(mean=[mu], std=[sigma]),\n])\n\nval_transform = v2.Compose([\n    v2.Normalize(mean=[mu], std=[sigma])\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T00:57:05.416425Z","iopub.execute_input":"2026-07-04T00:57:05.416933Z","iopub.status.idle":"2026-07-04T00:57:09.010251Z","shell.execute_reply.started":"2026-07-04T00:57:05.416906Z","shell.execute_reply":"2026-07-04T00:57:09.009659Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Module","metadata":{}},{"cell_type":"code","source":"import lightning as L\nfrom torch.utils.data import DataLoader\n\nclass KannadaMnistDataModule(L.LightningDataModule):\n    def __init__(self, batch_size = 32, num_workers = 4):\n        super().__init__()\n        self.batch_size = batch_size\n        self.num_workers = num_workers\n\n    def setup(self, stage : str):\n        self.train_ds = KannadaMnist(train, transform = train_transform)\n        self.val_ds = KannadaMnist(val, transform = val_transform)\n        self.test_ds = KannadaMnist(test, transform = val_transform)\n        self.infer_ds = KannadaMnist(infer, inference = True, transform = val_transform)\n\n    def train_dataloader(self):\n        return DataLoader(dataset = self.train_ds, batch_size = self.batch_size, num_workers = self.num_workers)\n    def val_dataloader(self):\n        return DataLoader(dataset = self.val_ds, batch_size = self.batch_size, num_workers = self.num_workers)\n    def test_dataloader(self):\n        return DataLoader(dataset = self.test_ds, batch_size = self.batch_size, num_workers = self.num_workers)\n    def predict_dataloader(self):\n        return DataLoader(dataset = self.infer_ds, batch_size = self.batch_size, num_workers = self.num_workers)\n    \n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T00:57:09.011091Z","iopub.execute_input":"2026-07-04T00:57:09.011396Z","iopub.status.idle":"2026-07-04T00:57:23.967062Z","shell.execute_reply.started":"2026-07-04T00:57:09.011375Z","shell.execute_reply":"2026-07-04T00:57:23.966469Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model","metadata":{}},{"cell_type":"markdown","source":"## Residual blocks\n","metadata":{}},{"cell_type":"code","source":"import torch.nn as nn\n\nclass ResidualBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, stride=1):\n        super().__init__()\n        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias = False)\n        self.bn1 = nn.BatchNorm2d(out_channels)\n        self.relu = nn.ReLU()\n        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias = False)\n        self.bn2 = nn.BatchNorm2d(out_channels)\n        self.shortcut = nn.Identity()\n        if stride != 1 or in_channels != out_channels:\n            projection = nn.Sequential(\n                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),\n                nn.BatchNorm2d(out_channels)\n            )\n            self.shortcut = projection\n    \n    def forward(self, x):\n        out = self.conv1(x)\n        out = self.bn1(out)\n        out = self.relu(out)\n\n        out = self.conv2(out)\n        out = self.bn2(out)\n\n        out = out + self.shortcut(x)\n        out = self.relu(out)\n        return out\n\nclass Stem(nn.Module):\n    def __init__(self, in_channels, out_channels = 64, kernel_size = 3, stride = 1, padding = 1):\n        super().__init__()\n        self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=padding, bias = False)\n        self.bn = nn.BatchNorm2d(out_channels)\n        self.relu = nn.ReLU()\n    def forward(self, x):\n        out = self.conv(x)\n        out = self.bn(out)\n        out = self.relu(out)\n        return out\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T00:57:23.967923Z","iopub.execute_input":"2026-07-04T00:57:23.968380Z","iopub.status.idle":"2026-07-04T00:57:23.977198Z","shell.execute_reply.started":"2026-07-04T00:57:23.968343Z","shell.execute_reply":"2026-07-04T00:57:23.976441Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ResNet","metadata":{}},{"cell_type":"code","source":"class ResNet(nn.Module):\n    def __init__(self, num_blocks, num_classes, in_channels):\n        super().__init__()\n        self.stem = Stem(in_channels = in_channels, out_channels = 64)\n        \n        self.res_blocks = nn.ModuleList()\n        c_in = 64\n        c_out = 64\n        strides = [1 if i == 0 else 2 for i in range(len(num_blocks))]\n        for stage_idx, n in enumerate(num_blocks):\n            stride = strides[stage_idx]\n            for block_idx in range(n):\n                block_stride = stride if block_idx == 0 else 1\n                self.res_blocks.append(ResidualBlock(c_in, c_out, stride=block_stride))\n                c_in = c_out\n            c_out *= 2\n        self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))\n        self.dropout = nn.Dropout(p=0.2)\n        self.head = nn.Sequential(\n            nn.Linear(c_in, 64),\n            nn.BatchNorm1d(64),\n            nn.ReLU(),\n            nn.Dropout(p=0.2),\n            nn.Linear(64, num_classes),\n        )\n\n    def forward(self, x):\n        out = self.stem(x)\n\n        for block in self.res_blocks:\n            out = block(out) # shape: (batch_size, c_in, h, w)\n\n        out = self.avg_pool(out) # shape: (batch_size, c_in, 1, 1)\n        out = out.flatten(1) # shape: (batch_size, c_in)\n        out = self.dropout(out)\n        out = self.head(out)\n        return out\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T00:57:23.978137Z","iopub.execute_input":"2026-07-04T00:57:23.978453Z","iopub.status.idle":"2026-07-04T00:57:23.998590Z","shell.execute_reply.started":"2026-07-04T00:57:23.978419Z","shell.execute_reply":"2026-07-04T00:57:23.997754Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## LitModel","metadata":{}},{"cell_type":"code","source":"from torchmetrics import MetricCollection\nfrom torchmetrics.classification import (\n    MulticlassAccuracy, MulticlassPrecision, MulticlassRecall, MulticlassF1Score\n)\nimport torch\nclass LitResNet(L.LightningModule):\n    def __init__(self, model, num_classes, lr = 1e-3):\n        super().__init__()\n        self.model = model\n        self.lr = lr\n        self.loss_fn = nn.CrossEntropyLoss()\n        metrics = MetricCollection([\n            MulticlassAccuracy(num_classes=num_classes),\n            MulticlassPrecision(num_classes=num_classes, average=\"macro\"),\n            MulticlassRecall(num_classes=num_classes, average=\"macro\"),\n            MulticlassF1Score(num_classes=num_classes, average=\"macro\"),\n        ])\n        self.train_metrics = metrics.clone(prefix=\"train/\")\n        self.val_metrics = metrics.clone(prefix=\"val/\")\n        self.test_metrics = metrics.clone(prefix=\"test/\")\n    def forward(self,x):\n        return self.model(x)\n    def training_step(self, batch, batch_idx):\n        x, y = batch\n        logits = self(x)\n        loss = self.loss_fn(logits, y)\n        output = self.train_metrics(logits, y)\n        self.log(\"train/loss\", loss, prog_bar = True)\n        self.log_dict(output, on_step=False, on_epoch=True, prog_bar = True)\n        return loss\n    def validation_step(self, batch, batch_idx):\n        x, y = batch\n        logits = self(x)\n        loss = self.loss_fn(logits, y)\n        output = self.val_metrics(logits, y)\n        self.log(\"val/loss\", loss, prog_bar = True)\n        self.log_dict(output, on_step=False, on_epoch=True, prog_bar = True)\n        return loss\n    def test_step(self, batch, batch_idx):\n        x, y = batch\n        logits = self(x)\n        loss = self.loss_fn(logits, y)\n        output = self.train_metrics(logits, y)\n        self.log(\"test/loss\", loss)\n        self.log_dict(output, on_step=False, on_epoch=True)\n        return loss\n    def predict_step(self, batch, batch_idx):\n        x, y = batch\n        logits = self(x)\n        preds = logits.argmax(dim=1)\n        return preds\n\n    def configure_optimizers(self):\n        optimizer = torch.optim.Adam(self.model.parameters(), lr = self.lr)\n        scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode=\"min\", patience=2, factor=0.5)\n        lr_scheduler = {\n            \"scheduler\" : scheduler,\n            \"monitor\" : \"val/loss\",\n            \"interval\" : \"epoch\"\n        }\n        return {\n            \"optimizer\" : optimizer,\n            \"lr_scheduler\" : lr_scheduler\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T00:57:23.999558Z","iopub.execute_input":"2026-07-04T00:57:23.999883Z","iopub.status.idle":"2026-07-04T00:57:24.015379Z","shell.execute_reply.started":"2026-07-04T00:57:23.999852Z","shell.execute_reply":"2026-07-04T00:57:24.014849Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training","metadata":{}},{"cell_type":"markdown","source":"## Callback","metadata":{}},{"cell_type":"code","source":"from lightning.pytorch.callbacks import Callback\nfrom torchvision.utils import make_grid\nfrom IPython.display import display, Image\nimport matplotlib.pyplot as plt\nimport io\n\n\nclass LiveBatchPreview(Callback):\n    def __init__(self, mean = mu, std = sigma, every_n_batches=200, n_images=5):\n        self.every_n_batches = every_n_batches\n        self.n_images = n_images\n        self.to_displayable = v2.Compose([\n            v2.Normalize(mean=[-mean / std], std=[1 / std]),\n            v2.Lambda(lambda x: x.clamp(0, 1)),              \n            v2.ToPILImage()\n        ])\n        self._display_id = \"live_batch_preview\"\n        self._initialized = False\n\n    def _fig_to_image(self, fig):\n        buf = io.BytesIO()\n        fig.savefig(buf, format=\"png\", bbox_inches=\"tight\")\n        buf.seek(0)\n        return Image(data=buf.read())\n\n    def on_train_batch_end(self, trainer, pl_module, outputs, batch, batch_idx, dataloader_idx=0):\n        if batch_idx % self.every_n_batches != 0:\n            return\n\n        x, y = batch\n        n = min(self.n_images, x.shape[0])\n\n        fig, axes = plt.subplots(1, n, figsize=(1.6 * n, 1.6))\n        for i in range(n):\n            img = self.to_displayable(x[i].detach().cpu())\n            axes[i].imshow(img, cmap=\"gray\", vmin=0, vmax=1)\n            axes[i].set_title(str(y[i].item()), fontsize=9)\n            axes[i].axis(\"off\")\n        fig.suptitle(f\"epoch {trainer.current_epoch}  batch {batch_idx}\", fontsize=10)\n\n        img_widget = self._fig_to_image(fig)\n        plt.close(fig)\n\n        if not self._initialized:\n            display(img_widget, display_id=self._display_id)\n            self._initialized = True\n        else:\n            display(img_widget, display_id=self._display_id, update=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T00:57:24.017543Z","iopub.execute_input":"2026-07-04T00:57:24.017923Z","iopub.status.idle":"2026-07-04T00:57:24.035496Z","shell.execute_reply.started":"2026-07-04T00:57:24.017895Z","shell.execute_reply":"2026-07-04T00:57:24.034656Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Trainer","metadata":{}},{"cell_type":"code","source":"model = ResNet(num_blocks = [3,4,6,3], num_classes = 10 , in_channels = 1)\nlit_model = LitResNet(model = model, num_classes = 10)\ndm = KannadaMnistDataModule(batch_size = 128)\n\nfrom lightning.pytorch.callbacks import EarlyStopping, ModelCheckpoint\nearly_stop = EarlyStopping(\n    monitor=\"val/loss\",\n    patience=5,\n    mode=\"min\",\n)\ncheckpoint_callback = ModelCheckpoint(\n    monitor=\"val/loss\",\n    mode=\"min\",\n    save_top_k=1,\n    filename=\"best-model\",\n    dirpath=\"/kaggle/working/checkpoints\"\n)\nEPOCHS = 30\ntrainer = L.Trainer(max_epochs = EPOCHS, accelerator = \"gpu\", devices=1, callbacks = [early_stop, checkpoint_callback])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T00:57:24.036587Z","iopub.execute_input":"2026-07-04T00:57:24.037234Z","iopub.status.idle":"2026-07-04T00:57:24.315392Z","shell.execute_reply.started":"2026-07-04T00:57:24.037195Z","shell.execute_reply":"2026-07-04T00:57:24.314433Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training loop","metadata":{}},{"cell_type":"code","source":"trainer.fit(lit_model,datamodule = dm)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T00:57:24.316795Z","iopub.execute_input":"2026-07-04T00:57:24.317578Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_model = LitResNet.load_from_checkpoint(\n    checkpoint_callback.best_model_path,\n    model=ResNet(num_blocks=[3,4,6,3], num_classes=10, in_channels=1),\n    num_classes=10,\n)\nbest_model.eval()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds = torch.cat(trainer.predict(best_model, datamodule = dm))\n\ndef generate_submission(preds):\n    ids = infer[\"id\"]\n    cols = [\"id\", \"label\"]\n    submission = pd.DataFrame(\n        {\n            \"id\" : ids,\n            \"label\" : preds.numpy()\n        }\n    )\n    return submission\n\n\n    ","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission = generate_submission(preds)\nsubmission.to_csv('submission.csv', index = False)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"trainer.validate(best_model, dm)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"trainer.test(best_model, dm)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}