{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python"},"accelerator":"GPU"},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"63f02fc3","cell_type":"markdown","source":"# CIFAR-10 Classification with a ResNet-18 trained from scratch\n\n**Setup on Kaggle:** Settings → Accelerator = **GPU**, Internet = **On**, and add the **cifar-10** competition data (Add Input → Competitions → CIFAR-10).\n\nAll weights are **randomly initialized** — no pre-trained weights are used anywhere.","metadata":{}},{"id":"42d2f3d7","cell_type":"code","source":"import os, sys, glob, time, shutil, subprocess, random\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\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\n\ndef seed_everything(seed=42):\n    random.seed(seed); np.random.seed(seed)\n    torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)\nseed_everything(42)\n\ndevice = 'cuda' if torch.cuda.is_available() else 'cpu'\ntorch.backends.cudnn.benchmark = True\nprint('Device:', device, '| torch', torch.__version__)","metadata":{},"outputs":[],"execution_count":null},{"id":"39f58540","cell_type":"markdown","source":"## 1. Data (official CIFAR-10 via torchvision)\nThe training set is identical to Kaggle's; the official test set is used to estimate the Kaggle score.","metadata":{}},{"id":"9212370e","cell_type":"code","source":"MEAN, STD = (0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)\n\ntrain_tf = T.Compose([\n    T.RandomCrop(32, padding=4, padding_mode='reflect'),\n    T.RandomHorizontalFlip(),\n    T.ToTensor(),\n    T.Normalize(MEAN, STD),\n    T.RandomErasing(p=0.25, scale=(0.02, 0.15)),\n])\ntest_tf = T.Compose([T.ToTensor(), T.Normalize(MEAN, STD)])\n\ntrain_ds = torchvision.datasets.CIFAR10('/tmp/data', train=True,  download=True, transform=train_tf)\nval_ds   = torchvision.datasets.CIFAR10('/tmp/data', train=False, download=True, transform=test_tf)\nCLASSES = train_ds.classes   # same names/order as Kaggle labels\nprint(CLASSES)\n\nBATCH = 128\ntrain_loader = DataLoader(train_ds, batch_size=BATCH, shuffle=True, num_workers=4,\n                          pin_memory=True, drop_last=True, persistent_workers=True)\nval_loader   = DataLoader(val_ds, batch_size=512, shuffle=False, num_workers=4, pin_memory=True)","metadata":{},"outputs":[],"execution_count":null},{"id":"22a6e339","cell_type":"markdown","source":"## 2. Model: ResNet-18 (CIFAR variant)\n3×3 stem without max-pooling (suited to 32×32 images), 4 stages of 2 BasicBlocks each (64→128→256→512 channels), global average pooling, linear classifier. ~11.2M parameters, randomly initialized (Kaiming).","metadata":{}},{"id":"128ad0de","cell_type":"code","source":"class BasicBlock(nn.Module):\n    def __init__(self, in_c, out_c, stride=1):\n        super().__init__()\n        self.conv1 = nn.Conv2d(in_c, out_c, 3, stride, 1, bias=False)\n        self.bn1 = nn.BatchNorm2d(out_c)\n        self.conv2 = nn.Conv2d(out_c, out_c, 3, 1, 1, bias=False)\n        self.bn2 = nn.BatchNorm2d(out_c)\n        self.shortcut = nn.Sequential()\n        if stride != 1 or in_c != out_c:\n            self.shortcut = nn.Sequential(nn.Conv2d(in_c, out_c, 1, stride, bias=False),\n                                          nn.BatchNorm2d(out_c))\n    def forward(self, x):\n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        return F.relu(out + self.shortcut(x))\n\nclass ResNet18(nn.Module):\n    def __init__(self, num_classes=10):\n        super().__init__()\n        self.stem = nn.Sequential(nn.Conv2d(3, 64, 3, 1, 1, bias=False),\n                                  nn.BatchNorm2d(64), nn.ReLU(inplace=True))\n        cfg, layers, in_c = [(64, 1), (128, 2), (256, 2), (512, 2)], [], 64\n        for out_c, stride in cfg:\n            layers += [BasicBlock(in_c, out_c, stride), BasicBlock(out_c, out_c, 1)]\n            in_c = out_c\n        self.layers = nn.Sequential(*layers)\n        self.fc = nn.Linear(512, num_classes)\n        for m in self.modules():   # random initialization\n            if isinstance(m, nn.Conv2d):\n                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')\n            elif isinstance(m, nn.BatchNorm2d):\n                nn.init.ones_(m.weight); nn.init.zeros_(m.bias)\n    def forward(self, x):\n        x = self.layers(self.stem(x))\n        x = F.adaptive_avg_pool2d(x, 1).flatten(1)\n        return self.fc(x)\n\nmodel = ResNet18().to(device).to(memory_format=torch.channels_last)\nprint(f'Parameters: {sum(p.numel() for p in model.parameters())/1e6:.2f}M')","metadata":{},"outputs":[],"execution_count":null},{"id":"3e634b01","cell_type":"markdown","source":"## 3. Training\nSGD (momentum 0.9, Nesterov, weight decay 5e-4), One-Cycle LR schedule (peak 0.1), label smoothing 0.1, mixed precision, 40 epochs.","metadata":{}},{"id":"8f1af65e","cell_type":"code","source":"EPOCHS = 40\ncriterion = nn.CrossEntropyLoss(label_smoothing=0.1)\noptimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9,\n                            nesterov=True, weight_decay=5e-4)\nscheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr=0.1, epochs=EPOCHS,\n                                                steps_per_epoch=len(train_loader), pct_start=0.25)\nscaler = torch.amp.GradScaler('cuda')\n\n@torch.no_grad()\ndef predict_logits(model, loader, tta=True):\n    model.eval(); outs = []\n    for batch in loader:\n        x = batch[0] if isinstance(batch, (list, tuple)) else batch\n        x = x.to(device, non_blocking=True).to(memory_format=torch.channels_last)\n        with torch.amp.autocast('cuda'):\n            logits = model(x).float().softmax(1)\n            if tta:   # horizontal-flip test-time augmentation\n                logits += model(torch.flip(x, dims=[3])).float().softmax(1)\n        outs.append(logits.cpu())\n    return torch.cat(outs)\n\ndef evaluate(model, tta=False):\n    preds = predict_logits(model, val_loader, tta).argmax(1)\n    return (preds == torch.tensor(val_ds.targets)).float().mean().item()\n\nhistory = []\nfor epoch in range(EPOCHS):\n    model.train(); t0 = time.time(); tot_loss = correct = n = 0\n    for x, y in train_loader:\n        x = x.to(device, non_blocking=True).to(memory_format=torch.channels_last)\n        y = y.to(device, non_blocking=True)\n        optimizer.zero_grad(set_to_none=True)\n        with torch.amp.autocast('cuda'):\n            out = model(x); loss = criterion(out, y)\n        scaler.scale(loss).backward()\n        scaler.step(optimizer); scaler.update(); scheduler.step()\n        tot_loss += loss.item() * y.size(0); correct += (out.argmax(1) == y).sum().item(); n += y.size(0)\n    val_acc = evaluate(model)\n    history.append((tot_loss / n, correct / n, val_acc))\n    print(f'Epoch {epoch+1:2d}/{EPOCHS} | loss {tot_loss/n:.4f} | train acc {correct/n:.4f} '\n          f'| test acc {val_acc:.4f} | {time.time()-t0:.0f}s')\n\ntorch.save(model.state_dict(), '/kaggle/working/resnet18_cifar10.pth')","metadata":{},"outputs":[],"execution_count":null},{"id":"3d777bd1","cell_type":"code","source":"import matplotlib.pyplot as plt\nh = np.array(history)\nfig, ax = plt.subplots(1, 2, figsize=(11, 4))\nax[0].plot(h[:, 0]); ax[0].set_title('Training loss'); ax[0].set_xlabel('epoch')\nax[1].plot(h[:, 1], label='train'); ax[1].plot(h[:, 2], label='test'); ax[1].legend()\nax[1].set_title('Accuracy'); ax[1].set_xlabel('epoch')\nplt.show()\n\nfinal_acc = evaluate(model, tta=True)\nprint(f'Official CIFAR-10 test accuracy (with flip TTA): {final_acc:.4f}  -> expected Kaggle score')","metadata":{},"outputs":[],"execution_count":null},{"id":"38798b93","cell_type":"markdown","source":"## 4. Extract the Kaggle test set (300,000 images)","metadata":{}},{"id":"87c5083a","cell_type":"code","source":"src = glob.glob('/kaggle/input/**/test.7z', recursive=True)\nassert src, 'test.7z not found - add the CIFAR-10 competition data to the notebook'\nsrc = src[0]; OUT = '/tmp'          # extract outside /kaggle/working to keep outputs small\nif not os.path.isdir(f'{OUT}/test'):\n    if shutil.which('7z'):\n        subprocess.run(['7z', 'x', src, f'-o{OUT}', '-y'], check=True, stdout=subprocess.DEVNULL)\n    else:\n        subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'py7zr'], check=True)\n        import py7zr\n        with py7zr.SevenZipFile(src, 'r') as z:\n            z.extractall(OUT)\nprint('Test images:', len(glob.glob(f'{OUT}/test/*')))   # should be 300000","metadata":{},"outputs":[],"execution_count":null},{"id":"a4e8d152","cell_type":"markdown","source":"## 5. Predict and write `submission.csv`","metadata":{}},{"id":"09446b67","cell_type":"code","source":"class KaggleTest(Dataset):\n    def __init__(self, folder, tf):\n        self.ids = sorted(int(os.path.splitext(f)[0]) for f in os.listdir(folder))\n        self.folder, self.tf = folder, tf\n    def __len__(self): return len(self.ids)\n    def __getitem__(self, i):\n        img = Image.open(f'{self.folder}/{self.ids[i]}.png').convert('RGB')\n        return self.tf(img)\n\ntest_ds = KaggleTest(f'{OUT}/test', test_tf)\ntest_loader = DataLoader(test_ds, batch_size=1024, shuffle=False, num_workers=4, pin_memory=True)\n\npreds = predict_logits(model, test_loader, tta=True).argmax(1).numpy()\nsub = pd.DataFrame({'id': test_ds.ids, 'label': [CLASSES[p] for p in preds]})\nsub.to_csv('/kaggle/working/submission.csv', index=False)\nprint(sub.shape); sub.head()","metadata":{},"outputs":[],"execution_count":null}]}