{"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":"import os, time, glob, shutil, subprocess, sys\nimport numpy as np, pandas as pd\nimport torch, torch.nn as nn, torch.nn.functional as F\nimport torchvision\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\n\ntorch.manual_seed(42); np.random.seed(42)\ndevice = torch.device('cuda')\ntorch.backends.cudnn.benchmark = True\nprint(torch.__version__, torch.cuda.get_device_name(0))\n\nCLASSES = ['airplane', 'automobile', 'bird', 'cat', 'deer',\n           'dog', 'frog', 'horse', 'ship', 'truck']","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-10-04T13:28:45.124379Z","iopub.execute_input":"2026-10-04T13:28:45.125124Z","iopub.status.idle":"2026-10-04T13:28:48.588873Z","shell.execute_reply.started":"2026-10-04T13:28:45.125099Z","shell.execute_reply":"2026-10-04T13:28:48.588183Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_set = torchvision.datasets.CIFAR10('/tmp/cifar', train=True, download=True)\ntest_set  = torchvision.datasets.CIFAR10('/tmp/cifar', train=False, download=True)\nassert train_set.classes == CLASSES\n\ndef to_tensor(ds):\n    x = torch.tensor(ds.data).permute(0, 3, 1, 2).float().div(255)   # N,3,32,32 in [0,1]\n    return x, torch.tensor(ds.targets)\n\nxtr, ytr = to_tensor(train_set)\nxte, yte = to_tensor(test_set)\n\nMEAN = xtr.mean((0, 2, 3)).view(1, 3, 1, 1).to(device)\nSTD  = xtr.std((0, 2, 3)).view(1, 3, 1, 1).to(device)\nnorm = lambda x: (x - MEAN) / STD\n\nxtr, ytr = norm(xtr.to(device)), ytr.to(device)\nxte, yte = norm(xte.to(device)), yte.to(device)\nprint(xtr.shape, xte.shape)\nprint('mean:', MEAN.flatten().tolist())\nprint('std: ', STD.flatten().tolist())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-04T13:28:48.589905Z","iopub.execute_input":"2026-10-04T13:28:48.590387Z","iopub.status.idle":"2026-10-04T13:28:52.040445Z","shell.execute_reply.started":"2026-10-04T13:28:48.590359Z","shell.execute_reply":"2026-10-04T13:28:52.039837Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"AR = torch.arange(32, device=device)\n\ndef augment(x):\n    B = x.size(0)\n    # 1) random horizontal flip (50% chance per image)\n    flip = torch.rand(B, device=device) < 0.5\n    x = torch.where(flip[:, None, None, None], x.flip(3), x)\n    # 2) random crop: pad to 40x40 (mirror the edges), then cut a random 32x32 window\n    xp = F.pad(x, (4, 4, 4, 4), mode='reflect')\n    i = torch.randint(0, 9, (B,), device=device)\n    j = torch.randint(0, 9, (B,), device=device)\n    rows = (i[:, None] + AR)[:, None, :, None]\n    cols = (j[:, None] + AR)[:, None, None, :]\n    b = torch.arange(B, device=device)[:, None, None, None]\n    c = torch.arange(3, device=device)[None, :, None, None]\n    return xp[b, c, rows, cols]\n\n# quick visual check\nimport matplotlib.pyplot as plt\nsample = xtr[:8]\naug = augment(sample)\nshow = lambda t: (t * STD + MEAN).clamp(0, 1).permute(0, 2, 3, 1).cpu()\nfig, ax = plt.subplots(2, 8, figsize=(14, 4))\nfor k in range(8):\n    ax[0, k].imshow(show(sample)[k]); ax[0, k].axis('off')\n    ax[1, k].imshow(show(aug)[k]);    ax[1, k].axis('off')\nax[0, 0].set_title('original', loc='left'); ax[1, 0].set_title('augmented', loc='left')\nplt.show()\nprint(aug.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-04T13:28:52.042235Z","iopub.execute_input":"2026-10-04T13:28:52.042446Z","iopub.status.idle":"2026-10-04T13:28:52.325948Z","shell.execute_reply.started":"2026-10-04T13:28:52.042426Z","shell.execute_reply":"2026-10-04T13:28:52.325185Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def conv_bn(ci, co, pool=False):\n    layers = [nn.Conv2d(ci, co, 3, padding=1, bias=False),\n              nn.BatchNorm2d(co),\n              nn.ReLU(inplace=True)]\n    if pool:\n        layers.append(nn.MaxPool2d(2))\n    return nn.Sequential(*layers)\n\nclass Residual(nn.Module):\n    def __init__(self, c):\n        super().__init__()\n        self.body = nn.Sequential(conv_bn(c, c), conv_bn(c, c))\n    def forward(self, x):\n        return x + self.body(x)          # skip connection\n\ndef resnet9(num_classes=10):\n    return nn.Sequential(\n        conv_bn(3, 64),                                   # 32x32\n        conv_bn(64, 128, pool=True),  Residual(128),      # 16x16\n        conv_bn(128, 256, pool=True),                     # 8x8\n        conv_bn(256, 512, pool=True), Residual(512),      # 4x4\n        nn.AdaptiveMaxPool2d(1), nn.Flatten(),\n        nn.Linear(512, num_classes))\n\nmodel = resnet9().to(device)\nprint(f'{sum(p.numel() for p in model.parameters())/1e6:.2f}M parameters')\n\n# sanity check: pass a batch through the untrained model\nwith torch.no_grad():\n    out = model(xtr[:4])\nprint(out.shape)\nprint('untrained test accuracy:', (model(xte[:1000]).argmax(1) == yte[:1000]).float().mean().item())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-04T13:28:52.326978Z","iopub.execute_input":"2026-10-04T13:28:52.327277Z","iopub.status.idle":"2026-10-04T13:28:56.730687Z","shell.execute_reply.started":"2026-10-04T13:28:52.327246Z","shell.execute_reply":"2026-10-04T13:28:56.729869Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# fresh model (so the sanity-check run above doesn't affect anything)\ntorch.manual_seed(42)\nmodel = resnet9().to(device)\n\nEPOCHS, BS, MAX_LR, WD = 30, 512, 0.2, 5e-4\nsteps_per_epoch = (len(xtr) + BS - 1) // BS\n\nopt = torch.optim.SGD(model.parameters(), lr=MAX_LR, momentum=0.9,\n                      nesterov=True, weight_decay=WD)\nsched = torch.optim.lr_scheduler.OneCycleLR(\n    opt, max_lr=MAX_LR, epochs=EPOCHS, steps_per_epoch=steps_per_epoch,\n    pct_start=0.25, anneal_strategy='linear')\ncriterion = nn.CrossEntropyLoss(label_smoothing=0.1)\nscaler = torch.amp.GradScaler('cuda')\n\n@torch.no_grad()\ndef predict_logits(model, x, bs=1000, tta=True):\n    model.eval()\n    out = []\n    for k in range(0, len(x), bs):\n        xb = x[k:k + bs]\n        with torch.autocast('cuda', dtype=torch.float16):\n            logits = model(xb)\n            if tta:\n                logits = logits + model(xb.flip(3))   # also look at the mirrored image\n        out.append(logits.float())\n    return torch.cat(out)\n\ndef accuracy(model, x, y, tta=True):\n    return (predict_logits(model, x, tta=tta).argmax(1) == y).float().mean().item()\n\nhistory = []\nfor epoch in range(EPOCHS):\n    model.train(); t0 = time.time()\n    perm = torch.randperm(len(xtr), device=device)\n    loss_sum, correct = 0.0, 0\n    for k in range(steps_per_epoch):\n        idx = perm[k * BS:(k + 1) * BS]\n        xb, yb = augment(xtr[idx]), ytr[idx]\n        with torch.autocast('cuda', dtype=torch.float16):\n            out = model(xb)\n            loss = criterion(out, yb)\n        opt.zero_grad(set_to_none=True)\n        scaler.scale(loss).backward()\n        scaler.step(opt); scaler.update(); sched.step()\n        loss_sum += loss.item() * len(idx)\n        correct += (out.argmax(1) == yb).sum().item()\n    test_acc = accuracy(model, xte, yte, tta=False)\n    history.append((epoch + 1, loss_sum / len(xtr), correct / len(xtr), test_acc))\n    print(f'epoch {epoch+1:2d} | loss {loss_sum/len(xtr):.3f} | train acc {correct/len(xtr):.4f} '\n          f'| test acc {test_acc:.4f} | lr {sched.get_last_lr()[0]:.4f} | {time.time()-t0:.1f}s')\n\nfinal_acc = accuracy(model, xte, yte, tta=True)\nprint(f'\\nOfficial CIFAR-10 test accuracy (with flip TTA) = {final_acc:.4f}  <- estimated Kaggle score')\ntorch.save(model.state_dict(), '/kaggle/working/resnet9_cifar10.pt')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-04T13:28:56.731766Z","iopub.execute_input":"2026-10-04T13:28:56.732105Z","iopub.status.idle":"2026-10-04T13:35:03.123576Z","shell.execute_reply.started":"2026-10-04T13:28:56.732079Z","shell.execute_reply":"2026-10-04T13:35:03.1226Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TEST_7Z = glob.glob('/kaggle/input/**/test.7z', recursive=True)[0]\nprint('Found:', TEST_7Z)\n\nimport io\nshutil.rmtree('/tmp/test', ignore_errors=True)   # remove the half-extracted folder\nsubprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'libarchive-c'], check=True)\nimport libarchive\n\nN = 300000\nxk = np.zeros((N, 32, 32, 3), dtype=np.uint8)   # all Kaggle test images, in memory\nseen = np.zeros(N, dtype=bool)\n\nt0, cnt = time.time(), 0\nwith libarchive.file_reader(TEST_7Z) as archive:\n    for entry in archive:\n        name = entry.pathname\n        if not name.endswith('.png'):\n            continue\n        i = int(os.path.basename(name)[:-4]) - 1        # '123.png' -> row 122\n        data = b''.join(entry.get_blocks())\n        xk[i] = np.array(Image.open(io.BytesIO(data)).convert('RGB'))\n        seen[i] = True; cnt += 1\n        if cnt % 25000 == 0:\n            print(f'{cnt} images  ({time.time()-t0:.0f}s)')\n\nprint(f'Done: {cnt} images, all present: {seen.all()}, total {time.time()-t0:.0f}s')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-04T13:35:03.124768Z","iopub.execute_input":"2026-10-04T13:35:03.125806Z","iopub.status.idle":"2026-10-04T13:36:53.79717Z","shell.execute_reply.started":"2026-10-04T13:35:03.125766Z","shell.execute_reply":"2026-10-04T13:36:53.796258Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"h = np.array(history)\nfig, ax = plt.subplots(1, 2, figsize=(12, 4))\n\nax[0].plot(h[:, 0], h[:, 1])\nax[0].set_title('Training loss'); ax[0].set_xlabel('epoch'); ax[0].grid(alpha=0.3)\n\nax[1].plot(h[:, 0], h[:, 2], label='train (augmented)')\nax[1].plot(h[:, 0], h[:, 3], label='test (official CIFAR-10)')\nax[1].set_title('Accuracy'); ax[1].set_xlabel('epoch'); ax[1].grid(alpha=0.3); ax[1].legend()\n\nplt.tight_layout()\nplt.savefig('/kaggle/working/training_curves.png', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-04T13:36:53.798553Z","iopub.execute_input":"2026-10-04T13:36:53.798949Z","iopub.status.idle":"2026-10-04T13:36:54.343526Z","shell.execute_reply.started":"2026-10-04T13:36:53.798922Z","shell.execute_reply":"2026-10-04T13:36:54.342716Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds = []\nCH = 10000                                        # process 10k images at a time\nfor s in range(0, N, CH):\n    xb = torch.from_numpy(xk[s:s + CH]).to(device)       # uint8, N,32,32,3\n    xb = norm(xb.permute(0, 3, 1, 2).float().div(255))   # same preprocessing as training\n    preds.append(predict_logits(model, xb, tta=True).argmax(1).cpu())\npreds = torch.cat(preds).numpy()\n\nsub = pd.DataFrame({'id': np.arange(1, N + 1),\n                    'label': [CLASSES[p] for p in preds]})\nsub.to_csv('/kaggle/working/submission.csv', index=False)\n\nprint(sub.shape)\nprint(sub.head())\nprint(sub.label.value_counts())\n\n# visual spot-check: a few test images with their predicted labels\nfig, ax = plt.subplots(1, 10, figsize=(16, 2.2))\nfor k in range(10):\n    ax[k].imshow(xk[k]); ax[k].set_title(sub.label[k], fontsize=9); ax[k].axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-04T13:36:54.344383Z","iopub.execute_input":"2026-10-04T13:36:54.344795Z","iopub.status.idle":"2026-10-04T13:37:31.965846Z","shell.execute_reply.started":"2026-10-04T13:36:54.344771Z","shell.execute_reply":"2026-10-04T13:37:31.965227Z"}},"outputs":[],"execution_count":null}]}