{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## 1. Data\n\nWe use the **official CIFAR-10 training set** (identical images to the Kaggle training data, per the assignment). We hold out 5,000 images from it as a validation set for monitoring, and keep the full official CIFAR-10 test set untouched until the very end, to get a clean estimate of the Kaggle score.","metadata":{}},{"cell_type":"code","source":"import os, glob, re, time\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision\nimport torchvision.transforms as T\nfrom PIL import Image\n\n# ---------- Setup ----------\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nCIFAR_MEAN, CIFAR_STD = (0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)\n\ntrain_tfms = T.Compose([\n    T.RandomCrop(32, padding=4, padding_mode='reflect'),\n    T.RandomHorizontalFlip(),\n    T.ToTensor(),\n    T.Normalize(CIFAR_MEAN, CIFAR_STD),\n])\ntest_tfms = T.Compose([T.ToTensor(), T.Normalize(CIFAR_MEAN, CIFAR_STD)])\n\ntrain_ds = torchvision.datasets.CIFAR10(root='./data', train=True,  download=True, transform=train_tfms)\ntest_ds  = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=test_tfms)\nclasses = train_ds.classes\n\ntrain_dl = DataLoader(train_ds, batch_size=400, shuffle=True, num_workers=2)\ntest_dl  = DataLoader(test_ds, batch_size=800, shuffle=False, num_workers=2)\n\n# ---------- Model: 9-layer CNN with 2 residual blocks ----------\ndef conv_block(cin, cout, pool=False):\n    layers = [nn.Conv2d(cin, cout, 3, padding=1), nn.BatchNorm2d(cout), nn.ReLU(inplace=True)]\n    if pool: layers.append(nn.MaxPool2d(2))\n    return nn.Sequential(*layers)\n\nclass ResNet9(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.conv1 = conv_block(3, 64)\n        self.conv2 = conv_block(64, 128, pool=True)\n        self.res1  = nn.Sequential(conv_block(128, 128), conv_block(128, 128))\n        self.conv3 = conv_block(128, 256, pool=True)\n        self.conv4 = conv_block(256, 512, pool=True)\n        self.res2  = nn.Sequential(conv_block(512, 512), conv_block(512, 512))\n        self.classifier = nn.Sequential(nn.MaxPool2d(4), nn.Flatten(), nn.Dropout(0.2), nn.Linear(512, 10))\n\n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.conv2(x)\n        x = self.res1(x) + x\n        x = self.conv3(x)\n        x = self.conv4(x)\n        x = self.res2(x) + x\n        return self.classifier(x)\n\nmodel = ResNet9().to(device)\n\n# ---------- Train ----------\nepochs, max_lr = 25, 0.01\noptimizer = torch.optim.Adam(model.parameters(), max_lr, weight_decay=1e-4)\nscheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr, epochs=epochs, steps_per_epoch=len(train_dl))\n\nfor epoch in range(epochs):\n    model.train()\n    t0 = time.time()\n    for xb, yb in train_dl:\n        xb, yb = xb.to(device), yb.to(device)\n        loss = F.cross_entropy(model(xb), yb)\n        optimizer.zero_grad()\n        loss.backward()\n        nn.utils.clip_grad_norm_(model.parameters(), 0.1)\n        optimizer.step()\n        scheduler.step()\n\n    model.eval()\n    correct = total = 0\n    with torch.no_grad():\n        for xb, yb in test_dl:\n            xb, yb = xb.to(device), yb.to(device)\n            correct += (model(xb).argmax(1) == yb).sum().item()\n            total += yb.size(0)\n    print(f'Epoch {epoch+1}/{epochs}  test_acc: {correct/total:.4f}  ({time.time()-t0:.1f}s)')\n\n# ---------- Final accuracy w/ test-time augmentation (flip + average) ----------\n@torch.no_grad()\ndef accuracy_tta(model, dl):\n    model.eval()\n    correct = total = 0\n    for xb, yb in dl:\n        xb, yb = xb.to(device), yb.to(device)\n        probs = F.softmax(model(xb), 1) + F.softmax(model(torch.flip(xb, [3])), 1)\n        correct += (probs.argmax(1) == yb).sum().item()\n        total += yb.size(0)\n    return correct / total\n\nprint('Final test accuracy (TTA):', accuracy_tta(model, test_dl))\n\n# ---------- Build the Kaggle submission ----------\nTEST_7Z_PATH, EXTRACT_DIR = '/kaggle/input/competitions/cifar-10/test.7z', './test'\n\n!pip -q install py7zr\nimport py7zr\n\nif not os.path.isdir(EXTRACT_DIR) or len(glob.glob(EXTRACT_DIR + '/*')) == 0:\n    with py7zr.SevenZipFile(TEST_7Z_PATH, mode='r') as archive:\n        archive.extractall(path='.')\n\nprint('Extracted files:', len(glob.glob(EXTRACT_DIR + '/*')))\n\nclass KaggleTestDataset(Dataset):\n    def __init__(self, folder, transform):\n        paths = glob.glob(os.path.join(folder, '*.png'))\n        self.paths = sorted(paths, key=lambda p: int(re.findall(r'\\d+', os.path.basename(p))[0]))\n        self.transform = transform\n    def __len__(self): return len(self.paths)\n    def __getitem__(self, i):\n        p = self.paths[i]\n        img_id = int(re.findall(r'\\d+', os.path.basename(p))[0])\n        return self.transform(Image.open(p).convert('RGB')), img_id\n\nkaggle_ds = KaggleTestDataset(EXTRACT_DIR, test_tfms)\nkaggle_dl = DataLoader(kaggle_ds, batch_size=500, shuffle=False, num_workers=2)\n\nmodel.eval()\nids, preds = [], []\nwith torch.no_grad():\n    for xb, batch_ids in kaggle_dl:\n        xb = xb.to(device)\n        probs = F.softmax(model(xb), 1) + F.softmax(model(torch.flip(xb, [3])), 1)\n        preds.extend(probs.argmax(1).cpu().tolist())\n        ids.extend(batch_ids.tolist())\n\nsubmission = pd.DataFrame({'id': ids, 'label': [classes[p] for p in preds]})\nsubmission.sort_values('id').to_csv('submission.csv', index=False)\nprint(submission.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-22T11:40:52.067632Z","iopub.execute_input":"2026-09-22T11:40:52.067962Z","iopub.status.idle":"2026-09-22T11:55:14.993928Z","shell.execute_reply.started":"2026-09-22T11:40:52.067917Z","shell.execute_reply":"2026-09-22T11:55:14.993153Z"}},"outputs":[],"execution_count":null}]}