{"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":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":16017,"databundleVersionId":708189}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -q onnx onnxscript\n\n# Kannada-MNIST CNN Training & TS File Generation\n# Run on Kaggle with GPU T4 accelerator enabled\n\nimport os, time, warnings\nwarnings.filterwarnings(\"ignore\")\n\nimport numpy as np\nimport pandas as pd\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, TensorDataset\nfrom torchvision import transforms\n\nSEED = 42\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.backends.cudnn.benchmark = True\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Device : {DEVICE}\")\n\nt_global_start = time.time()\n\n# ── 1. LOAD DATA ──────────────────────────────────────────────────────────────\nDATA_DIR = \"/kaggle/input/competitions/Kannada-MNIST\"\n\ntrain_df = pd.read_csv(os.path.join(DATA_DIR, \"train.csv\"))\ntest_df  = pd.read_csv(os.path.join(DATA_DIR, \"test.csv\"))\ndig_df   = pd.read_csv(os.path.join(DATA_DIR, \"Dig-MNIST.csv\"))\n\ndef df_to_tensors(df, has_label=True):\n    pixel_cols = [c for c in df.columns if c.startswith(\"pixel\")]\n    X = df[pixel_cols].values.astype(np.float32) / 255.0\n    X = X.reshape(-1, 1, 28, 28)\n    X_t = torch.tensor(X, dtype=torch.float32)\n    if has_label:\n        y = df[\"label\"].values.astype(np.int64)\n        return X_t, torch.tensor(y, dtype=torch.long)\n    return X_t\n\n# Merge train + Dig-MNIST for more training data (~70 k samples)\ncombined = pd.concat([train_df, dig_df], ignore_index=True)\nX_all, y_all = df_to_tensors(combined, has_label=True)\nX_test       = df_to_tensors(test_df,  has_label=False)\n\n# 90/10 split\nn_val  = int(0.10 * len(X_all))\nn_train= len(X_all) - n_val\nperm   = torch.randperm(len(X_all), generator=torch.Generator().manual_seed(SEED))\nidx_tr, idx_val = perm[:n_train], perm[n_train:]\n\nX_tr, y_tr = X_all[idx_tr], y_all[idx_tr]\nX_val, y_val = X_all[idx_val], y_all[idx_val]\n\n# ── 2. DATA AUGMENTATION & DATALOADERS ────────────────────────────────────────\ntrain_transform = nn.Sequential(\n    transforms.RandomAffine(degrees=10, translate=(0.1, 0.1), scale=(0.9, 1.1)),\n    transforms.RandomErasing(p=0.1, scale=(0.02, 0.1)),\n)\n\ndef make_loader(X, y, batch_size=256, shuffle=True):\n    ds = TensorDataset(X, y)\n    return DataLoader(ds, batch_size=batch_size, shuffle=shuffle, num_workers=2, pin_memory=True)\n\ntrain_loader = make_loader(X_tr,  y_tr,  batch_size=256, shuffle=True)\nval_loader   = make_loader(X_val, y_val, batch_size=512, shuffle=False)\ntest_loader  = DataLoader(TensorDataset(X_test), batch_size=512, shuffle=False, num_workers=2, pin_memory=True)\n\n# ── 3. MODEL DEFINITION ───────────────────────────────────────────────────────\nclass KannadaCNN(nn.Module):\n    def __init__(self, dropout=0.3):\n        super().__init__()\n        self.conv1 = nn.Conv2d(1,  32, kernel_size=3, padding=1)\n        self.bn1   = nn.BatchNorm2d(32)\n        self.pool1 = nn.MaxPool2d(2)\n\n        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)\n        self.bn2   = nn.BatchNorm2d(64)\n        self.pool2 = nn.MaxPool2d(2)\n\n        self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)\n        self.bn3   = nn.BatchNorm2d(128)\n        self.pool3 = nn.AdaptiveAvgPool2d(3)\n\n        self.drop  = nn.Dropout(dropout)\n        self.fc1   = nn.Linear(128 * 3 * 3, 256)\n        self.fc2   = nn.Linear(256, 10)\n\n    def forward(self, x, augment=False):\n        if augment and self.training:\n            x = train_transform(x)\n        x = self.pool1(F.relu(self.bn1(self.conv1(x))))\n        x = self.pool2(F.relu(self.bn2(self.conv2(x))))\n        x = self.pool3(F.relu(self.bn3(self.conv3(x))))\n        x = x.flatten(1)\n        x = self.drop(F.relu(self.fc1(x)))\n        return self.fc2(x)\n\nmodel = KannadaCNN(dropout=0.3).to(DEVICE)\n\n# ── 4. TRAINING ───────────────────────────────────────────────────────────────\nEPOCHS   = 20\nLR       = 3e-3\nWD       = 1e-4\n\noptimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=WD)\nscheduler = torch.optim.lr_scheduler.OneCycleLR(\n    optimizer, max_lr=LR,\n    steps_per_epoch=len(train_loader), epochs=EPOCHS, pct_start=0.2,\n)\ncriterion = nn.CrossEntropyLoss(label_smoothing=0.05)\n\nbest_val_acc = 0.0\nbest_state   = None\n\nprint(\"Training started...\")\nfor epoch in range(1, EPOCHS + 1):\n    model.train()\n    for X_b, y_b in train_loader:\n        X_b, y_b = X_b.to(DEVICE), y_b.to(DEVICE)\n        optimizer.zero_grad(set_to_none=True)\n        loss = criterion(model(X_b, augment=True), y_b)\n        loss.backward()\n        nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n        optimizer.step()\n        scheduler.step()\n\n    model.eval()\n    correct = total = 0\n    with torch.no_grad():\n        for X_b, y_b in val_loader:\n            X_b, y_b = X_b.to(DEVICE), y_b.to(DEVICE)\n            preds = model(X_b).argmax(1)\n            correct += (preds == y_b).sum().item()\n            total   += len(y_b)\n\n    val_acc = correct / total\n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        best_state   = {k: v.clone() for k, v in model.state_dict().items()}\n\nprint(f\"Best val accuracy : {best_val_acc*100:.3f}%\")\nmodel.load_state_dict(best_state)\nmodel.eval()\n\n# ── 5. SUBMISSION & EXPORT ────────────────────────────────────────────────────\ntest_preds = []\nwith torch.no_grad():\n    for (X_b,) in test_loader:\n        X_b = X_b.to(DEVICE)\n        test_preds.extend(model(X_b).argmax(1).cpu().numpy())\n\npd.DataFrame({\"id\": test_df[\"id\"], \"label\": test_preds}).to_csv(\"submission.csv\", index=False)\n\ndummy_input = torch.randn(1, 1, 28, 28, device=DEVICE)\ntorch.onnx.export(\n    model, dummy_input, \"kannada_cnn.onnx\",\n    input_names=[\"input\"], output_names=[\"output\"],\n    dynamic_axes={\"input\": {0: \"batch\"}, \"output\": {0: \"batch\"}},\n    opset_version=18, do_constant_folding=True,\n)\nprint(\"ONNX model saved!\")\n\n# ── 6. GENERATE .TS FILES FOR LOVABLE APP ─────────────────────────────────────\n\nmodel_config_ts = f\"\"\"export const MODEL_CONFIG = {{\n  modelName: \"KannadaCNN\",\n  version: \"1.0.0\",\n  inputShape: [1, 1, 28, 28],\n  inputSize: 28,\n  numClasses: 10,\n  classLabels: [\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\"],\n  bestValAccuracy: {best_val_acc * 100:.3f},\n  modelPath: \"/kannada_cnn.onnx\" // Make sure to place the model in the public folder\n}};\n\"\"\"\n\npreprocessing_ts = \"\"\"export function preprocessCanvas(imageData: ImageData): Float32Array {\n  const { data, width, height } = imageData;\n  const tensor = new Float32Array(width * height);\n  \n  // Convert RGBA to Grayscale and normalize to [0, 1]\n  for (let i = 0; i < data.length; i += 4) {\n    const r = data[i];\n    const g = data[i + 1];\n    const b = data[i + 2];\n    const a = data[i + 3];\n    \n    // Grayscale conversion based on luminance\n    const gray = (r * 0.299 + g * 0.587 + b * 0.114);\n    \n    // Since images in Kannada-MNIST are typically dark strokes on light background\n    // If your canvas is white-on-black, no inversion is needed.\n    // Assuming user draws black on white canvas:\n    let pixelValue = 255 - gray; \n    \n    // Normalize to [0, 1] as expected by the model\n    tensor[i / 4] = pixelValue / 255.0;\n  }\n  \n  return tensor;\n}\n\"\"\"\n\nmodel_loader_ts = \"\"\"import * as ort from 'onnxruntime-web';\nimport { MODEL_CONFIG } from './model-config';\nimport { preprocessCanvas } from './preprocessing';\n\nexport class ModelLoader {\n  private session: ort.InferenceSession | null = null;\n\n  async loadModel() {\n    try {\n      this.session = await ort.InferenceSession.create(MODEL_CONFIG.modelPath, {\n        executionProviders: ['wasm']\n      });\n      console.log(\"ONNX Model loaded successfully.\");\n    } catch (e) {\n      console.error(\"Failed to load ONNX model\", e);\n    }\n  }\n\n  async predict(imageData: ImageData): Promise<number> {\n    if (!this.session) {\n      throw new Error(\"Model is not loaded yet.\");\n    }\n\n    const preprocessedData = preprocessCanvas(imageData);\n    const tensor = new ort.Tensor('float32', preprocessedData, MODEL_CONFIG.inputShape);\n\n    const feeds: Record<string, ort.Tensor> = {};\n    feeds[this.session.inputNames[0]] = tensor;\n\n    const results = await this.session.run(feeds);\n    const outputTensor = results[this.session.outputNames[0]];\n    const outputData = outputTensor.data as Float32Array;\n\n    // Argmax\n    let maxVal = -Infinity;\n    let maxIdx = -1;\n    for (let i = 0; i < outputData.length; i++) {\n      if (outputData[i] > maxVal) {\n        maxVal = outputData[i];\n        maxIdx = i;\n      }\n    }\n\n    return maxIdx;\n  }\n}\n\"\"\"\n\nwith open(\"model-config.ts\", \"w\") as f: f.write(model_config_ts)\nwith open(\"preprocessing.ts\", \"w\") as f: f.write(preprocessing_ts)\nwith open(\"model-loader.ts\", \"w\") as f: f.write(model_loader_ts)\n\nprint(\"TS files (model-config.ts, preprocessing.ts, model-loader.ts) generated successfully!\")\nprint(f\"Total notebook time: {(time.time()-t_global_start)/60:.1f} min\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-05T15:20:24.067886Z","iopub.execute_input":"2026-05-05T15:20:24.068897Z","iopub.status.idle":"2026-05-05T15:21:32.294574Z","shell.execute_reply.started":"2026-05-05T15:20:24.068837Z","shell.execute_reply":"2026-05-05T15:21:32.293840Z"}},"outputs":[],"execution_count":null}]}