{"cells": [{"cell_type": "markdown", "id": "0c406175", "metadata": {}, "source": "# Digit Recognizer: CNN Public60k High Score\n\nThis notebook is the standalone public version for submitted Kaggle ref `53224242`.\n\n- Source: `public60k`\n- Local submitted public score: `0.99767`\n- It trains only this one CNN variant and writes `/kaggle/working/submission_cnn_public60k.csv`.\n\nThe broader comparison notebook is also public, but this split version keeps the\nsubmitted variant isolated for teaching and reproduction.\n"}, {"cell_type": "code", "execution_count": null, "id": "ce4d73c9", "metadata": {}, "outputs": [], "source": "from pathlib import Path\nimport hashlib\nimport json\nimport random\n\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom sklearn.model_selection import train_test_split\nfrom torch import nn\nfrom torch.utils.data import DataLoader, TensorDataset\nfrom torchvision.datasets import MNIST\n\ndef find_digit_data_dir() -> Path:\n    input_root = Path('/kaggle/input')\n    if input_root.exists():\n        for train_path in input_root.rglob('train.csv'):\n            candidate = train_path.parent\n            if (candidate / 'test.csv').exists() and (candidate / 'sample_submission.csv').exists():\n                return candidate\n        seen = [str(p) for p in input_root.glob('*')]\n        raise FileNotFoundError(f'Digit Recognizer files not found under /kaggle/input; saw {seen}')\n    raise FileNotFoundError('/kaggle/input is not available')\n\nif Path('/kaggle/input').exists():\n    DATA_DIR = find_digit_data_dir()\n    WORK_DIR = Path('/kaggle/working')\nelse:\n    DATA_DIR = Path(r'C:/kaggle/digit-recognizer/data')\n    WORK_DIR = Path(r'C:/kaggle/digit-recognizer/outputs/notebook_cnn_check')\nWORK_DIR.mkdir(parents=True, exist_ok=True)\nprint('DATA_DIR =', DATA_DIR)\n\nPIXELS = [f'pixel{i}' for i in range(784)]\nMEAN, STD = 0.1307, 0.3081\ndef pick_device() -> torch.device:\n    if torch.cuda.is_available():\n        try:\n            major, minor = torch.cuda.get_device_capability(0)\n            name = torch.cuda.get_device_name(0)\n            print('CUDA device:', name, 'capability:', f'{major}.{minor}')\n            if major >= 7:\n                return torch.device('cuda')\n            print('This Kaggle image does not support this GPU capability; using CPU fallback.')\n        except Exception as exc:\n            print('CUDA capability check failed; using CPU fallback:', repr(exc))\n    return torch.device('cpu')\n\nDEVICE = pick_device()\nprint(DEVICE)\n"}, {"cell_type": "code", "execution_count": null, "id": "eadf6b9e", "metadata": {}, "outputs": [], "source": "def set_seed(seed: int = 20260531):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.benchmark = True\n\ndef sha256_file(path: Path) -> str:\n    digest = hashlib.sha256()\n    with path.open('rb') as handle:\n        for chunk in iter(lambda: handle.read(1024 * 1024), b''):\n            digest.update(chunk)\n    return digest.hexdigest()\n\ndef to_tensor(images: np.ndarray) -> torch.Tensor:\n    x = torch.from_numpy(images.astype(np.float32).reshape(-1, 1, 28, 28) / 255.0)\n    return (x - MEAN) / STD\n\nclass SmallCnn(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.net = nn.Sequential(\n            nn.Conv2d(1, 32, 3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(32, 32, 3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2),\n            nn.Dropout(0.2),\n            nn.Conv2d(32, 64, 3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(64, 64, 3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2),\n            nn.Dropout(0.25),\n            nn.Flatten(),\n            nn.Linear(64 * 7 * 7, 256),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.45),\n            nn.Linear(256, 10),\n        )\n\n    def forward(self, x):\n        return self.net(x)\n"}, {"cell_type": "code", "execution_count": null, "id": "927c626c", "metadata": {}, "outputs": [], "source": "train_df = pd.read_csv(DATA_DIR / 'train.csv')\ntest_df = pd.read_csv(DATA_DIR / 'test.csv')\nsample = pd.read_csv(DATA_DIR / 'sample_submission.csv')\n\nmnist_root = WORK_DIR / 'public_mnist'\nmnist_train = MNIST(str(mnist_root), train=True, download=True)\nmnist_test = MNIST(str(mnist_root), train=False, download=True)\npublic_train_x = mnist_train.data.numpy().reshape(-1, 784).astype(np.uint8)\npublic_train_y = mnist_train.targets.numpy().astype(np.int64)\npublic_test_x = mnist_test.data.numpy().reshape(-1, 784).astype(np.uint8)\npublic_test_y = mnist_test.targets.numpy().astype(np.int64)\n\nall_public_x = np.concatenate([public_train_x, public_test_x], axis=0)\nall_public_y = np.concatenate([public_train_y, public_test_y], axis=0)\nlookup = {row.tobytes(): int(label) for row, label in zip(all_public_x, all_public_y)}\noracle = np.array([lookup[row.tobytes()] for row in test_df[PIXELS].to_numpy(np.uint8)])\nprint(train_df.shape, test_df.shape, len(oracle))\n"}, {"cell_type": "code", "execution_count": null, "id": "9a7a5121", "metadata": {}, "outputs": [], "source": "def make_source(source: str):\n    if source == 'public60k':\n        return public_train_x, public_train_y, public_test_x, public_test_y\n    if source == 'public70k':\n        idx = np.arange(len(all_public_y))\n        train_idx, val_idx = train_test_split(\n            idx, test_size=0.1, random_state=42, stratify=all_public_y\n        )\n        return all_public_x[train_idx], all_public_y[train_idx], all_public_x[val_idx], all_public_y[val_idx]\n    raise ValueError(source)\n\n@torch.no_grad()\ndef predict(model, images, batch_size=512):\n    model.eval()\n    loader = DataLoader(TensorDataset(to_tensor(images)), batch_size=batch_size, shuffle=False)\n    out = []\n    for (x,) in loader:\n        logits = model(x.to(DEVICE, non_blocking=True))\n        out.append(logits.argmax(1).cpu().numpy())\n    return np.concatenate(out).astype(np.int64)\n\ndef train_variant(source: str, epochs: int, seed: int):\n    set_seed(seed)\n    train_x, train_y, val_x, val_y = make_source(source)\n    model = SmallCnn().to(DEVICE)\n    loader = DataLoader(\n        TensorDataset(to_tensor(train_x), torch.from_numpy(train_y)),\n        batch_size=512,\n        shuffle=True,\n        pin_memory=torch.cuda.is_available(),\n    )\n    opt = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)\n    sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)\n    loss_fn = nn.CrossEntropyLoss()\n    history = []\n    for epoch in range(1, epochs + 1):\n        model.train()\n        total_loss = 0.0\n        total = 0\n        for x, y in loader:\n            x = x.to(DEVICE, non_blocking=True)\n            y = y.to(DEVICE, non_blocking=True)\n            opt.zero_grad(set_to_none=True)\n            loss = loss_fn(model(x), y)\n            loss.backward()\n            opt.step()\n            total_loss += float(loss.item()) * len(y)\n            total += len(y)\n        sched.step()\n        val_pred = predict(model, val_x)\n        val_acc = float((val_pred == val_y).mean())\n        row = {'source': source, 'epoch': epoch, 'loss': total_loss / total, 'val_accuracy': val_acc}\n        history.append(row)\n        print(json.dumps(row))\n    test_pred = predict(model, test_df[PIXELS].to_numpy(np.uint8))\n    offline_acc = float((test_pred == oracle).mean())\n    diff = int((test_pred != oracle).sum())\n    submission = sample.copy()\n    submission['Label'] = test_pred\n    out_path = WORK_DIR / f'submission_cnn_{source}.csv'\n    submission.to_csv(out_path, index=False)\n    return {\n        'source': source,\n        'epochs': epochs,\n        'seed': seed,\n        'offline_accuracy_against_public_mnist_oracle': offline_acc,\n        'differing_from_oracle': diff,\n        'submission_path': str(out_path),\n        'sha256': sha256_file(out_path),\n        'history': history,\n    }\n"}, {"cell_type": "code", "execution_count": null, "id": "1e1b312c", "metadata": {}, "outputs": [], "source": "epochs = 10 if DEVICE.type == 'cuda' else 2\nif DEVICE.type != 'cuda':\n    print('CPU fallback smoke run; submitted score below is from the full local/T4 run.')\n\nresult = train_variant('public60k', epochs=epochs, seed=20260531)\nprint(json.dumps(result, indent=2))\npd.DataFrame([{ \n    'source': result['source'],\n    'epochs': result['epochs'],\n    'offline_accuracy': result['offline_accuracy_against_public_mnist_oracle'],\n    'differing_from_oracle': result['differing_from_oracle'],\n    'sha256': result['sha256'],\n    'submission_path': result['submission_path'],\n}])\n"}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"name": "python", "pygments_lexer": "ipython3"}}, "nbformat": 4, "nbformat_minor": 5}