{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":3649,"databundleVersionId":46718,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Basics\nimport os, glob, random, csv, shutil\nfrom pathlib import Path\nimport numpy as np\nfrom PIL import Image\n\n# Torch\nimport torch, torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, Dataset, random_split\nfrom torchvision import datasets, transforms\n\n# Reproducibility\nseed = 42\nrandom.seed(seed); np.random.seed(seed); torch.manual_seed(seed)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.benchmark = True\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device)\n\n# CIFAR-10 normalization\nC10_MEAN = (0.4914, 0.4822, 0.4465)\nC10_STD  = (0.2470, 0.2435, 0.2616)\n\n# Class order used for submission\nIDX_TO_CLASS = ['airplane','automobile','bird','cat','deer','dog','frog','horse','ship','truck']\nprint(\"Class order:\", IDX_TO_CLASS)\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-24T16:42:10.984324Z","iopub.execute_input":"2025-09-24T16:42:10.984488Z","iopub.status.idle":"2025-09-24T16:42:17.768008Z","shell.execute_reply.started":"2025-09-24T16:42:10.984471Z","shell.execute_reply":"2025-09-24T16:42:17.767245Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Transforms\ntrain_tf = transforms.Compose([\n    transforms.RandomCrop(32, padding=4),\n    transforms.RandomHorizontalFlip(),\n    transforms.ColorJitter(0.2, 0.2, 0.2, 0.05),\n    transforms.ToTensor(),\n    transforms.Normalize(C10_MEAN, C10_STD),\n    transforms.RandomErasing(p=0.25, scale=(0.02, 0.15), ratio=(0.3, 3.3)),\n])\neval_tf = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(C10_MEAN, C10_STD),\n])\n\n# Load official CIFAR-10 train (50k)\nroot = \"./data\"\nfull_train = datasets.CIFAR10(root=root, train=True, download=True, transform=train_tf)\n\n# Split 45k/5k\nval_size = 5000\ntrain_size = len(full_train) - val_size\ng = torch.Generator().manual_seed(seed)\ntrain_set, val_set = random_split(full_train, [train_size, val_size], generator=g)\nval_set.dataset.transform = eval_tf  # no augs on val\n\n# Dataloaders\nBATCH_SIZE = 256\nNUM_WORKERS = 2\ntrain_loader = DataLoader(train_set, batch_size=BATCH_SIZE, shuffle=True,\n                          num_workers=NUM_WORKERS, pin_memory=True, drop_last=True)\nval_loader   = DataLoader(val_set,   batch_size=BATCH_SIZE, shuffle=False,\n                          num_workers=NUM_WORKERS, pin_memory=True)\n\nxb, yb = next(iter(train_loader))\nprint(\"Train batch:\", xb.shape, yb.shape, \"| sizes:\", len(train_set), len(val_set))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T16:42:30.640661Z","iopub.execute_input":"2025-09-24T16:42:30.641218Z","iopub.status.idle":"2025-09-24T16:42:40.429774Z","shell.execute_reply.started":"2025-09-24T16:42:30.641194Z","shell.execute_reply":"2025-09-24T16:42:40.429081Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# scratch residual network for CIFAR-10 \n\nclass BasicBlock(nn.Module):\n    \"\"\"Two 3x3 convs + residual skip. Downsample via stride=2 and 1x1 conv if needed.\"\"\"\n    expansion = 1\n    def __init__(self, in_ch, out_ch, stride=1):\n        super().__init__()\n        self.conv1 = nn.Conv2d(in_ch, out_ch, kernel_size=3, stride=stride, padding=1, bias=False)\n        self.bn1   = nn.BatchNorm2d(out_ch)\n        self.conv2 = nn.Conv2d(out_ch, out_ch, kernel_size=3, stride=1, padding=1, bias=False)\n        self.bn2   = nn.BatchNorm2d(out_ch)\n        self.act   = nn.ReLU(inplace=True)\n\n        if stride != 1 or in_ch != out_ch:\n            self.down = nn.Sequential(\n                nn.Conv2d(in_ch, out_ch, kernel_size=1, stride=stride, bias=False),\n                nn.BatchNorm2d(out_ch),\n            )\n        else:\n            self.down = nn.Identity()\n\n    def forward(self, x):\n        identity = self.down(x)\n        out = self.act(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        out = self.act(out + identity)   # residual connection\n        return out\n\nclass CifarResNet(nn.Module):\n    \"\"\"\n    Stem: 3x3 conv (s=1), BN, ReLU (no maxpool).\n    Stages: [L1, L2, L3] BasicBlocks with channel widths [64,128,256].\n    Head: GAP -> Linear(10).\n    \"\"\"\n    def __init__(self, layers=(3,3,3), widths=(64,128,256), num_classes=10):\n        super().__init__()\n        self.stem = nn.Sequential(\n            nn.Conv2d(3, widths[0], kernel_size=3, stride=1, padding=1, bias=False),\n            nn.BatchNorm2d(widths[0]),\n            nn.ReLU(inplace=True),\n        )\n        self.layer1 = self._make_stage(widths[0], widths[0], layers[0], stride=1)  # 32x32\n        self.layer2 = self._make_stage(widths[0], widths[1], layers[1], stride=2)  # 16x16\n        self.layer3 = self._make_stage(widths[1], widths[2], layers[2], stride=2)  # 8x8\n        self.pool = nn.AdaptiveAvgPool2d(1)\n        self.fc   = nn.Linear(widths[2], num_classes)\n\n        self.apply(self._init_kaiming)\n\n    def _make_stage(self, in_ch, out_ch, n_blocks, stride):\n        blocks = [BasicBlock(in_ch, out_ch, stride=stride)]\n        for _ in range(1, n_blocks):\n            blocks.append(BasicBlock(out_ch, out_ch, stride=1))\n        return nn.Sequential(*blocks)\n\n    @staticmethod\n    def _init_kaiming(m):\n        if isinstance(m, nn.Conv2d):\n            nn.init.kaiming_normal_(m.weight, nonlinearity='relu')\n        elif isinstance(m, nn.Linear):\n            nn.init.kaiming_normal_(m.weight, nonlinearity='relu')\n            if m.bias is not None: nn.init.zeros_(m.bias)\n        elif isinstance(m, nn.BatchNorm2d):\n            nn.init.ones_(m.weight); nn.init.zeros_(m.bias)\n\n    def forward(self, x):\n        x = self.stem(x)\n        x = self.layer1(x)\n        x = self.layer2(x)\n        x = self.layer3(x)\n        x = self.pool(x).flatten(1)\n        x = self.fc(x)\n        return x\n\nmodel = CifarResNet(layers=(3,3,3), widths=(64,128,256), num_classes=10)\nmodel = model.to(device)\nprint(\"Custom residual CIFAR model ready. Params:\",\n      sum(p.numel() for p in model.parameters())/1e6, \"M\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T16:42:46.198426Z","iopub.execute_input":"2025-09-24T16:42:46.199210Z","iopub.status.idle":"2025-09-24T16:42:46.296452Z","shell.execute_reply.started":"2025-09-24T16:42:46.199175Z","shell.execute_reply":"2025-09-24T16:42:46.295727Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 10\nbase_lr = 0.2 * (BATCH_SIZE / 256)\noptimizer = torch.optim.SGD(model.parameters(), lr=base_lr, momentum=0.9,\n                            weight_decay=5e-4, nesterov=True)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\ncriterion = nn.CrossEntropyLoss(label_smoothing=0.1)\n\n# Safe EMA (only float tensors)\nuse_ema = True\nema_decay = 0.999\nif use_ema:\n    ema = {k: v.detach().clone() for k, v in model.state_dict().items()}\n\n@torch.no_grad()\ndef ema_update():\n    if not use_ema: return\n    for k, v in model.state_dict().items():\n        if v.dtype.is_floating_point:\n            ema[k].mul_(ema_decay).add_(v.detach(), alpha=1.0 - ema_decay)\n        else:\n            ema[k] = v.detach().clone()\n\nbest_acc, best_path = 0.0, \"best_custom_cifar_resnet.pth\"\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T16:48:05.569097Z","iopub.execute_input":"2025-09-24T16:48:05.569403Z","iopub.status.idle":"2025-09-24T16:48:05.579856Z","shell.execute_reply.started":"2025-09-24T16:48:05.569374Z","shell.execute_reply":"2025-09-24T16:48:05.579350Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_epoch(loader, train=True):\n    model.train(train)\n    total, correct, loss_sum = 0, 0, 0.0\n    for x, y in loader:\n        x, y = x.to(device, non_blocking=True), y.to(device, non_blocking=True)\n        if train: optimizer.zero_grad(set_to_none=True)\n        with torch.set_grad_enabled(train):\n            logits = model(x)\n            loss = criterion(logits, y)\n            if train:\n                loss.backward()\n                optimizer.step()\n                ema_update()\n        loss_sum += loss.item() * y.size(0)\n        correct += (logits.argmax(1) == y).sum().item()\n        total += y.size(0)\n    return loss_sum/total, correct/total\n\nwarmup = 5\nfor epoch in range(EPOCHS):\n    if epoch < warmup:\n        for pg in optimizer.param_groups:\n            pg['lr'] = base_lr * (epoch + 1) / warmup\n\n    tr_loss, tr_acc = run_epoch(train_loader, train=True)\n    va_loss, va_acc = run_epoch(val_loader,   train=False)\n    scheduler.step()\n\n    if va_acc > best_acc:\n        best_acc = va_acc\n        torch.save({'model': model.state_dict()}, best_path)\n\n    print(f\"Epoch {epoch+1:03d}/{EPOCHS} | \"\n          f\"train {tr_acc:.4f}/{tr_loss:.4f} | \"\n          f\"val {va_acc:.4f}/{va_loss:.4f} | best {best_acc:.4f}\")\n\nprint(\"Best val acc:\", best_acc)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T16:48:09.219787Z","iopub.execute_input":"2025-09-24T16:48:09.220449Z","iopub.status.idle":"2025-09-24T16:51:25.518742Z","shell.execute_reply.started":"2025-09-24T16:48:09.220420Z","shell.execute_reply":"2025-09-24T16:51:25.517746Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_tf = transforms.Compose([transforms.ToTensor(), transforms.Normalize(C10_MEAN, C10_STD)])\ntest_official = datasets.CIFAR10(root='./data', train=False, download=True, transform=test_tf)\ntest_loader = DataLoader(test_official, batch_size=BATCH_SIZE, shuffle=False,\n                         num_workers=NUM_WORKERS, pin_memory=True)\n\n# Prefer EMA weights; else best checkpoint\nif 'ema' in globals():\n    model.load_state_dict(ema, strict=False)\nelse:\n    ckpt = torch.load(best_path, map_location=device)\n    model.load_state_dict(ckpt['model'], strict=True)\n\nmodel.eval()\ntotal = correct = 0\nwith torch.no_grad():\n    for x, y in test_loader:\n        x, y = x.to(device), y.to(device)\n        pred = model(x).argmax(1)\n        correct += (pred == y).sum().item()\n        total   += y.size(0)\n\noffline_acc = correct / total\nprint(\"Official CIFAR-10 test accuracy (offline estimate):\", offline_acc)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T16:51:31.475846Z","iopub.execute_input":"2025-09-24T16:51:31.476264Z","iopub.status.idle":"2025-09-24T16:51:33.836467Z","shell.execute_reply.started":"2025-09-24T16:51:31.476228Z","shell.execute_reply":"2025-09-24T16:51:33.835681Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip -q install py7zr\nimport py7zr\n\n# Find archive\ncands = glob.glob(\"/kaggle/input/**/test.7z\", recursive=True)\nprint(\"Candidates:\", cands)\nassert cands, \"Attach the 'CIFAR-10 - Object Recognition in Images' competition as input.\"\ntest7z = cands[0]\nprint(\"Using:\", test7z, \"| size:\", os.path.getsize(test7z))\n\n# Clean and extract\noutdir = \"test\"\nif os.path.exists(outdir):\n    shutil.rmtree(outdir)\nos.makedirs(outdir, exist_ok=True)\n\nwith py7zr.SevenZipFile(test7z, mode=\"r\") as z:\n    z.extractall(path=outdir)\n\n# Flatten if nested and count\npngs_recursive = glob.glob(os.path.join(outdir, \"**\", \"*.png\"), recursive=True)\nfor p in pngs_recursive:\n    dest = os.path.join(outdir, os.path.basename(p))\n    if os.path.abspath(p) != os.path.abspath(dest) and not os.path.exists(dest):\n        shutil.move(p, dest)\n# remove empty dirs\nfor root, dirs, files in os.walk(outdir, topdown=False):\n    if root != outdir and not os.listdir(root):\n        os.rmdir(root)\n\nfinal_count = len(glob.glob(os.path.join(outdir, \"*.png\")))\nprint(\"Final PNG count in ./test:\", final_count)  # expect ~300000\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T16:51:41.520037Z","iopub.execute_input":"2025-09-24T16:51:41.520880Z","iopub.status.idle":"2025-09-24T16:54:05.759304Z","shell.execute_reply.started":"2025-09-24T16:51:41.520847Z","shell.execute_reply":"2025-09-24T16:54:05.758498Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load EMA (if present) or best checkpoint; eval mode\nif 'ema' in globals():\n    model.load_state_dict(ema, strict=False)\nelse:\n    ckpt = torch.load(best_path, map_location=device)\n    model.load_state_dict(ckpt['model'], strict=True)\nmodel.eval()\n\ntest_tf_final = transforms.Compose([transforms.ToTensor(), transforms.Normalize(C10_MEAN, C10_STD)])\n\nclass TestFolder(Dataset):\n    def __init__(self, files, tfm):\n        self.files = files; self.tfm = tfm\n    def __len__(self): return len(self.files)\n    def __getitem__(self, i):\n        fp = self.files[i]\n        img = Image.open(fp).convert('RGB')\n        return self.tfm(img), int(Path(fp).stem)\n\ntest_files = sorted(glob.glob('test/*.png'), key=lambda p: int(Path(p).stem))\ntest_loader_big = DataLoader(TestFolder(test_files, test_tf_final),\n                             batch_size=BATCH_SIZE, shuffle=False,\n                             num_workers=NUM_WORKERS, pin_memory=True)\n\nrows = []\nwith torch.no_grad():\n    for x, ids in test_loader_big:\n        x = x.to(device)\n        pred = model(x).argmax(1).cpu().tolist()\n        for i, p in zip(ids.tolist(), pred):\n            rows.append((i, IDX_TO_CLASS[p]))\n\nrows.sort(key=lambda t: t[0])\nwith open('submission.csv', 'w', newline='') as f:\n    w = csv.writer(f); w.writerow(['id','label']); w.writerows(rows)\n\nprint(\"Wrote submission.csv with\", len(rows), \"rows.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T16:54:14.794002Z","iopub.execute_input":"2025-09-24T16:54:14.794317Z","iopub.status.idle":"2025-09-24T16:55:25.643676Z","shell.execute_reply.started":"2025-09-24T16:54:14.794293Z","shell.execute_reply":"2025-09-24T16:55:25.642783Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\ndf = pd.read_csv(\"submission.csv\")\nprint(\"shape:\", df.shape)\nprint(\"columns:\", list(df.columns))\nprint(\"id range:\", df['id'].min(), \"…\", df['id'].max())\nprint(\"unique labels:\", sorted(df['label'].unique()))\nassert set(df['label'].unique()) <= set(IDX_TO_CLASS)\nassert df['id'].is_monotonic_increasing\nassert not df.isna().any().any()\nprint(\"✅ CSV looks good.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T17:35:52.299215Z","iopub.execute_input":"2025-09-24T17:35:52.299554Z","iopub.status.idle":"2025-09-24T17:35:52.711942Z","shell.execute_reply.started":"2025-09-24T17:35:52.299525Z","shell.execute_reply":"2025-09-24T17:35:52.711282Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===== Cell 9: Quick preview =====\nimport pandas as pd\npd.read_csv(\"submission.csv\").head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T17:40:21.382758Z","iopub.execute_input":"2025-09-24T17:40:21.383081Z","iopub.status.idle":"2025-09-24T17:40:21.463710Z","shell.execute_reply.started":"2025-09-24T17:40:21.383057Z","shell.execute_reply":"2025-09-24T17:40:21.462942Z"}},"outputs":[],"execution_count":null}]}