{"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":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import math, random, os, time\nfrom dataclasses import dataclass\nfrom typing import Tuple\n\nimport numpy as np\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, random_split\nfrom torchvision import datasets, transforms\n\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED)\ntorch.manual_seed(SEED)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)\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# Train transforms (augmentation)\ntrain_tf = transforms.Compose([\n    transforms.RandomCrop(32, padding=4),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize(C10_MEAN, C10_STD),\n])\n\n# Validation/Test transforms (no augmentation)\neval_tf = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(C10_MEAN, C10_STD),\n])\n\nroot = \"./data\"\nfull_train = datasets.CIFAR10(root=root, train=True, download=True, transform=train_tf)\n\nVAL_SIZE = 5000\nTRAIN_SIZE = len(full_train) - VAL_SIZE\ntrain_set, val_set = random_split(\n    full_train, [TRAIN_SIZE, VAL_SIZE], generator=torch.Generator().manual_seed(SEED)\n)\n# IMPORTANT: use clean eval transforms for validation\nval_set.dataset.transform = eval_tf\n\nBATCH_SIZE = 128\nNUM_WORKERS = 2\n\ntrain_loader = DataLoader(\n    train_set, batch_size=BATCH_SIZE, shuffle=True,\n    num_workers=NUM_WORKERS, pin_memory=True, drop_last=True\n)\nval_loader   = DataLoader(\n    val_set,   batch_size=BATCH_SIZE, shuffle=False,\n    num_workers=NUM_WORKERS, pin_memory=True, drop_last=False\n)\n\nidx_to_class = ['airplane','automobile','bird','cat','deer','dog','frog','horse','ship','truck']\nprint(\"Train/Val sizes:\", len(train_set), len(val_set))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-08T15:42:36.203346Z","iopub.execute_input":"2025-10-08T15:42:36.203663Z","iopub.status.idle":"2025-10-08T15:42:51.797276Z","shell.execute_reply.started":"2025-10-08T15:42:36.203636Z","shell.execute_reply":"2025-10-08T15:42:51.796552Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_cutout(x, size=8):\n    # x: [B,3,32,32]\n    if size <= 0: return x\n    B, C, H, W = x.size()\n    y = x.clone()\n    cx = torch.randint(W, (B,), device=x.device)\n    cy = torch.randint(H, (B,), device=x.device)\n    x1 = (cx - size//2).clamp(0, W)\n    x2 = (cx + size//2).clamp(0, W)\n    y1 = (cy - size//2).clamp(0, H)\n    y2 = (cy + size//2).clamp(0, H)\n    for i in range(B):\n        y[i, :, y1[i]:y2[i], x1[i]:x2[i]] = 0.\n    return y\n\ndef rand_bbox(W, H, lam):\n    cut_rat = math.sqrt(1. - lam)\n    cut_w = int(W * cut_rat)\n    cut_h = int(H * cut_rat)\n    cx = np.random.randint(W)\n    cy = np.random.randint(H)\n    x1 = np.clip(cx - cut_w // 2, 0, W)\n    y1 = np.clip(cy - cut_h // 2, 0, H)\n    x2 = np.clip(cx + cut_w // 2, 0, W)\n    y2 = np.clip(cy + cut_h // 2, 0, H)\n    return x1, y1, x2, y2\n\ndef mixup_data(x, y, alpha=0.2):\n    if alpha <= 0: return x, y, y, 1.0\n    lam = np.random.beta(alpha, alpha)\n    index = torch.randperm(x.size(0), device=x.device)\n    mixed_x = lam * x + (1 - lam) * x[index, :]\n    y_a, y_b = y, y[index]\n    return mixed_x, y_a, y_b, lam\n\ndef cutmix_data(x, y, alpha=1.0):\n    if alpha <= 0: return x, y, y, 1.0\n    lam = np.random.beta(alpha, alpha)\n    index = torch.randperm(x.size(0), device=x.device)\n    y_a, y_b = y, y[index]\n    B, C, H, W = x.size()\n    x1, y1, x2, y2 = rand_bbox(W, H, lam)\n    new_x = x.clone()\n    new_x[:, :, y1:y2, x1:x2] = x[index, :, y1:y2, x1:x2]\n    lam_adj = 1 - ((x2 - x1) * (y2 - y1) / (W * H))\n    return new_x, y_a, y_b, lam_adj","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-08T15:42:58.195096Z","iopub.execute_input":"2025-10-08T15:42:58.195371Z","iopub.status.idle":"2025-10-08T15:42:58.205382Z","shell.execute_reply.started":"2025-10-08T15:42:58.195352Z","shell.execute_reply":"2025-10-08T15:42:58.204564Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def conv3x3(in_planes, out_planes, stride=1):\n    return nn.Conv2d(in_planes, out_planes, 3, stride=stride, padding=1, bias=False)\n\nclass WRNBasicBlock(nn.Module):\n    def __init__(self, in_planes, out_planes, stride, drop_rate):\n        super().__init__()\n        self.equal = (in_planes == out_planes)\n        self.bn1 = nn.BatchNorm2d(in_planes)\n        self.relu1 = nn.ReLU(inplace=True)\n        self.conv1 = conv3x3(in_planes, out_planes, stride)\n        self.bn2 = nn.BatchNorm2d(out_planes)\n        self.relu2 = nn.ReLU(inplace=True)\n        self.conv2 = conv3x3(out_planes, out_planes, 1)\n        self.dropout = nn.Dropout(p=drop_rate) if drop_rate > 0 else nn.Identity()\n        self.shortcut = nn.Identity() if self.equal else nn.Conv2d(in_planes, out_planes, 1, stride=stride, bias=False)\n\n    def forward(self, x):\n        out = self.relu1(self.bn1(x))\n        res = x if self.equal else self.shortcut(out)\n        out = self.conv1(out)\n        out = self.relu2(self.bn2(out))\n        out = self.dropout(out)\n        out = self.conv2(out)\n        return out + res\n\ndef make_group(block, in_ch, out_ch, n, stride, drop_rate):\n    layers = [block(in_ch, out_ch, stride, drop_rate)]\n    for _ in range(1, n):\n        layers.append(block(out_ch, out_ch, 1, drop_rate))\n    return nn.Sequential(*layers)\n\nclass WideResNet(nn.Module):\n    def __init__(self, depth=28, widen=10, drop_rate=0.3, num_classes=10):\n        super().__init__()\n        assert (depth - 4) % 6 == 0, \"depth should be 6n+4\"\n        n = (depth - 4) // 6\n        k = widen\n        self.stem   = conv3x3(3, 16, 1)\n        self.group1 = make_group(WRNBasicBlock, 16,    16*k, n, 1, drop_rate)\n        self.group2 = make_group(WRNBasicBlock, 16*k,  32*k, n, 2, drop_rate)\n        self.group3 = make_group(WRNBasicBlock, 32*k,  64*k, n, 2, drop_rate)\n        self.bn   = nn.BatchNorm2d(64*k)\n        self.relu = nn.ReLU(inplace=True)\n        self.fc   = nn.Linear(64*k, num_classes)\n\n        # Kaiming init\n        for m in self.modules():\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\n    def forward(self, x):\n        out = self.stem(x)\n        out = self.group1(out)\n        out = self.group2(out)\n        out = self.group3(out)\n        out = self.relu(self.bn(out))\n        out = F.adaptive_avg_pool2d(out, 1).flatten(1)\n        return self.fc(out)\n\nmodel = WideResNet(depth=28, widen=10, drop_rate=0.3, num_classes=10).to(device)\nn_params = sum(p.numel() for p in model.parameters())\nprint(f\"Model params: {n_params/1e6:.2f}M\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-08T15:43:04.034140Z","iopub.execute_input":"2025-10-08T15:43:04.034409Z","iopub.status.idle":"2025-10-08T15:43:04.702610Z","shell.execute_reply.started":"2025-10-08T15:43:04.034387Z","shell.execute_reply":"2025-10-08T15:43:04.701778Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 75\nBASE_LR = 3e-4\nWEIGHT_DECAY = 5e-2\nWARMUP_EPOCHS = 10\n\noptimizer = torch.optim.AdamW(model.parameters(), lr=BASE_LR, weight_decay=WEIGHT_DECAY)\n\ndef lr_schedule(t):\n    # Cosine after warmup\n    if t < WARMUP_EPOCHS:\n        return (t+1) / WARMUP_EPOCHS\n    T = EPOCHS - WARMUP_EPOCHS\n    tt = (t - WARMUP_EPOCHS + 1) / T\n    return 0.5 * (1 + math.cos(math.pi * tt))\n\ndef set_lr(optimizer, base, scale):\n    for g in optimizer.param_groups:\n        g['lr'] = base * scale\n\n# EMA for stability & a small boost\nclass EMA:\n    def __init__(self, m, decay=0.999):\n        self.m = m\n        self.shadow = {k: v.detach().clone() for k, v in m.state_dict().items()}\n        self.decay = decay\n    @torch.no_grad()\n    def update(self):\n        for k, v in self.m.state_dict().items():\n            self.shadow[k] = self.decay * self.shadow[k] + (1 - self.decay) * v.detach()\n    def state_dict(self):\n        return self.shadow\n\nema = EMA(model, decay=0.999)\n\nLABEL_SMOOTH = 0.1\nce = nn.CrossEntropyLoss(label_smoothing=LABEL_SMOOTH)\n\nMIXUP_ALPHA = 0.2\nCUTMIX_ALPHA = 1.0\nUSE_CUTOUT = True\nCUTOUT_SIZE = 8\n\nbest_acc = 0.0\n\ndef accuracy(loader, net):\n    net.eval()\n    tot = cor = 0\n    with torch.no_grad():\n        for x, y in loader:\n            x, y = x.to(device), y.to(device)\n            logits = net(x)\n            pred = logits.argmax(1)\n            cor += (pred == y).sum().item()\n            tot += y.size(0)\n    return cor / tot","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-08T15:44:10.688480Z","iopub.execute_input":"2025-10-08T15:44:10.689078Z","iopub.status.idle":"2025-10-08T15:44:10.701829Z","shell.execute_reply.started":"2025-10-08T15:44:10.689055Z","shell.execute_reply":"2025-10-08T15:44:10.701208Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Training loop \nfor epoch in range(EPOCHS):\n    model.train()\n    set_lr(optimizer, BASE_LR, lr_schedule(epoch))\n\n    t0 = time.time()\n    tr_cor = tr_tot = 0\n    tr_loss_sum = 0.\n\n    for x, y in train_loader:\n        x, y = x.to(device), y.to(device)\n        if USE_CUTOUT: x = apply_cutout(x, CUTOUT_SIZE)\n\n        # Randomly choose MixUp or CutMix (50/50)\n        if np.random.rand() < 0.5:\n            x_mix, y_a, y_b, lam = mixup_data(x, y, alpha=MIXUP_ALPHA)\n        else:\n            x_mix, y_a, y_b, lam = cutmix_data(x, y, alpha=CUTMIX_ALPHA)\n\n        logits = model(x_mix)\n        loss = lam * ce(logits, y_a) + (1-lam) * ce(logits, y_b)\n\n        optimizer.zero_grad(set_to_none=True)\n        loss.backward()\n        optimizer.step()\n        ema.update()\n\n        tr_loss_sum += loss.item() * x.size(0)\n        # quick progress metric (not exact for mixed labels)\n        preds = logits.argmax(1)\n        tr_cor += (preds == y).sum().item()\n        tr_tot += y.size(0)\n\n    # Evaluate with EMA weights\n    with torch.no_grad():\n        saved = {k: v.clone() for k, v in model.state_dict().items()}\n        model.load_state_dict(ema.state_dict(), strict=True)\n        val_acc = accuracy(val_loader, model)\n        model.load_state_dict(saved, strict=True)\n\n    tr_loss = tr_loss_sum / tr_tot\n    tr_acc = tr_cor / tr_tot\n    best_acc = max(best_acc, val_acc)\n    print(f\"Epoch {epoch+1:03d}/{EPOCHS} | lr {optimizer.param_groups[0]['lr']:.5f} | \"\n          f\"train {tr_acc:.4f}/{tr_loss:.4f} | val {val_acc:.4f} | best {best_acc:.4f} | \"\n          f\"{time.time()-t0:.1f}s\")\n\nprint(\"Best val acc:\", best_acc)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-08T15:44:24.561546Z","iopub.execute_input":"2025-10-08T15:44:24.562274Z","iopub.status.idle":"2025-10-08T19:32:24.201712Z","shell.execute_reply.started":"2025-10-08T15:44:24.562244Z","shell.execute_reply":"2025-10-08T19:32:24.200731Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch, time\n\ndef _unwrap_state_dict(m):\n    return m.state_dict()\n\nbest_acc = float(globals().get(\"best_acc\", 0.0))\n\n# Try to save EMA weights (if this cell is run right after training)\ntry:\n    if 'ema' in globals() and ema is not None:\n        ema_ckpt = {\n            \"ema\": True,\n            \"model\": {k: v.cpu() for k, v in ema.state_dict().items()},\n            \"best_acc\": best_acc,\n            \"saved_at\": time.strftime(\"%Y-%m-%d %H:%M:%S\"),\n        }\n        torch.save(ema_ckpt, \"best_from_session_ema.pth\")\n        print(\"✓ Saved EMA -> best_from_session_ema.pth\")\n    else:\n        print(\"EMA not present; skipping EMA save.\")\nexcept Exception as e:\n    print(\"EMA save skipped:\", e)\n\n# Always save plain model\nplain_ckpt = {\n    \"ema\": False,\n    \"model\": {k: v.cpu() for k, v in _unwrap_state_dict(model).items()},\n    \"best_acc\": best_acc,\n    \"saved_at\": time.strftime(\"%Y-%m-%d %H:%M:%S\"),\n}\ntorch.save(plain_ckpt, \"best_from_session_model.pth\")\nprint(\"✓ Saved plain model -> best_from_session_model.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-08T19:36:55.775557Z","iopub.execute_input":"2025-10-08T19:36:55.776399Z","iopub.status.idle":"2025-10-08T19:36:56.578311Z","shell.execute_reply.started":"2025-10-08T19:36:55.776370Z","shell.execute_reply":"2025-10-08T19:36:56.577637Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Offline accuracy on CIFAR-10 test set\nfrom torchvision import datasets, transforms\n\n# Rebuild model exactly like training\nnet = WideResNet(depth=28, widen=10, drop_rate=0.3, num_classes=10).to(device)\n\n# Prefer EMA checkpoint if available\nuse_ema = False\nif os.path.exists(\"best_from_session_ema.pth\"):\n    ckpt = torch.load(\"best_from_session_ema.pth\", map_location=device)\n    state = ckpt.get(\"model\", ckpt)\n    net.load_state_dict(state, strict=True)\n    use_ema = True\nelse:\n    ckpt = torch.load(\"best_from_session_model.pth\", map_location=device)\n    state = ckpt.get(\"model\", ckpt)\n    net.load_state_dict(state, strict=True)\n\nnet.eval()\n\ntest_tf = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(C10_MEAN, C10_STD),\n])\ntest_set = datasets.CIFAR10(root='./data', train=False, download=True, transform=test_tf)\ntest_loader = DataLoader(\n    test_set, batch_size=BATCH_SIZE, shuffle=False,\n    num_workers=NUM_WORKERS, pin_memory=True\n)\n\ncorrect = total = 0\nwith torch.no_grad():\n    for x, y in test_loader:\n        x, y = x.to(device), y.to(device)\n        pred = net(x).argmax(1)\n        correct += (pred == y).sum().item()\n        total   += y.size(0)\n\noffline_acc = correct / total\nprint(f\"Official CIFAR-10 test accuracy (offline estimate) [{ 'EMA' if use_ema else 'Plain' }]: {offline_acc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-08T19:37:01.029279Z","iopub.execute_input":"2025-10-08T19:37:01.029784Z","iopub.status.idle":"2025-10-08T19:37:09.150215Z","shell.execute_reply.started":"2025-10-08T19:37:01.029760Z","shell.execute_reply":"2025-10-08T19:37:09.149448Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Robust extract + verify + flatten, then recount\n!pip -q install py7zr\n\nimport os, glob, shutil, py7zr\nfrom pathlib import Path\n\n# 1) Locate the archive\ncands = glob.glob(\"/kaggle/input/**/test.7z\", recursive=True)\nprint(\"Candidates:\", cands)\nassert cands, \"Couldn't find test.7z. Make sure the CIFAR-10 competition is attached as an input.\"\ntest7z = cands[0]\nprint(\"Using:\", test7z, \"| size (bytes):\", os.path.getsize(test7z))\n\n# 2) Clean output dir\nOUTDIR = Path(\"test\")\nif OUTDIR.exists():\n    shutil.rmtree(OUTDIR)\nOUTDIR.mkdir(parents=True, exist_ok=True)\n\n# 3) Peek inside the archive, then extract\nwith py7zr.SevenZipFile(test7z, mode=\"r\") as z:\n    names = z.getnames()\n    print(\"First 10 archive entries:\", names[:10])\n    z.extractall(path=str(OUTDIR))\n\n# 4) Count recursively (handles nested folders inside the archive)\npngs_recursive = glob.glob(str(OUTDIR / \"**\" / \"*.png\"), recursive=True)\nprint(\"PNG files found (recursive):\", len(pngs_recursive))\n\n# 5) If files are nested (e.g., test/test/1.png), flatten into ./test\nmoved = 0\nfor p in pngs_recursive:\n    p = Path(p)\n    dest = OUTDIR / p.name\n    if p.resolve() != dest.resolve():\n        if not dest.exists():\n            shutil.move(str(p), str(dest))\n            moved += 1\nprint(\"Moved from nested dirs:\", moved)\n\n# 6) Remove empty subfolders under ./test (optional)\nfor root, dirs, files in os.walk(OUTDIR, topdown=False):\n    if Path(root) == OUTDIR:\n        continue\n    if not os.listdir(root):\n        os.rmdir(root)\n\n# 7) Final count should be ~300000\nfinal_count = len(list(OUTDIR.glob(\"*.png\")))\nprint(\"Final PNG count in ./test:\", final_count)\nassert final_count > 0, \"No PNGs found after extraction/flatten.\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-08T19:37:26.028509Z","iopub.execute_input":"2025-10-08T19:37:26.029258Z","iopub.status.idle":"2025-10-08T19:40:05.175925Z","shell.execute_reply.started":"2025-10-08T19:37:26.029233Z","shell.execute_reply":"2025-10-08T19:40:05.175215Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import csv, glob\nfrom pathlib import Path\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\n# --- 1) Load EMA (if present) or plain checkpoint ---\n# Try an EMA checkpoint first (common names), else fall back to plain\nckpt_path_ema_candidates = [\n    \"best_from_session_ema.pth\",\n    \"best_wrn2810.pth\",              # if your EMA was saved under this name previously\n]\nckpt_path_plain_candidates = [\n    \"best_from_session_model.pth\",\n    \"best_wrn2810.pth\",\n]\n\nstate_loaded = False\nif 'ema' in globals() and isinstance(ema, dict):\n    try:\n        model.load_state_dict(ema, strict=False)\n        state_loaded = True\n        print(\"Loaded model weights from in-memory EMA dict.\")\n    except Exception as e:\n        print(\"In-memory EMA load failed:\", e)\n\nif not state_loaded:\n    # Try file EMA\n    for p in ckpt_path_ema_candidates:\n        if Path(p).exists():\n            print(\"Loading EMA checkpoint:\", p)\n            ckpt = torch.load(p, map_location=device)\n            # allow either {'model': ...} or full EMA dict\n            state = ckpt.get('model', ckpt)\n            model.load_state_dict(state, strict=False)\n            state_loaded = True\n            break\n\nif not state_loaded:\n    # Fallback: plain checkpoint\n    for p in ckpt_path_plain_candidates:\n        if Path(p).exists():\n            print(\"Loading plain checkpoint:\", p)\n            ckpt = torch.load(p, map_location=device)\n            state = ckpt.get('model', ckpt)\n            model.load_state_dict(state, strict=True)\n            state_loaded = True\n            break\n\nassert state_loaded, \"No model weights found. Place an EMA or plain checkpoint in the working dir.\"\n\nmodel.eval()\n\n# --- 2) Dataset & loader ---\ntest_tf_final = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(C10_MEAN, C10_STD),\n])\n\n# Sorted by numeric filename so ids align (1.png, 2.png, ...)\ntest_files = sorted(glob.glob('test/*.png'), key=lambda p: int(Path(p).stem))\n\nclass TestFolder(Dataset):\n    def __init__(self, files, tfm):\n        self.files = files; self.tfm = tfm\n    def __len__(self): \n        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\n# Use the same constants you used during training; define them if needed\n# BATCH_SIZE = 256\n# NUM_WORKERS = 2\ntest_loader_big = DataLoader(\n    TestFolder(test_files, test_tf_final),\n    batch_size=BATCH_SIZE, shuffle=False,\n    num_workers=NUM_WORKERS, pin_memory=True\n)\n\nIDX_TO_CLASS = ['airplane','automobile','bird','cat','deer','dog','frog','horse','ship','truck']\n\n# --- 3) Batched inference and write CSV ---\nrows = []\nwith torch.no_grad():\n    for x, ids in test_loader_big:\n        x = x.to(device, non_blocking=True)\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])\n\nwith open('submission.csv', 'w', newline='') as f:\n    w = csv.writer(f)\n    w.writerow(['id','label'])\n    w.writerows(rows)\n\nprint(\"Wrote submission.csv with\", len(rows), \"rows.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-08T19:48:07.658780Z","iopub.execute_input":"2025-10-08T19:48:07.659549Z","iopub.status.idle":"2025-10-08T19:51:26.714233Z","shell.execute_reply.started":"2025-10-08T19:48:07.659522Z","shell.execute_reply":"2025-10-08T19:51:26.713274Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ndf = pd.read_csv(\"submission.csv\")\nprint(df.head())\nprint(\"Rows:\", len(df))\nprint(\"Columns:\", list(df.columns))\n\n# Must be exactly 300000 rows, two columns id,label\nassert list(df.columns) == [\"id\", \"label\"]\nassert len(df) == 300000\n\n# ids should be 1..300000 with no gaps\nassert df[\"id\"].is_monotonic_increasing\nassert df[\"id\"].iloc[0] == 1 and df[\"id\"].iloc[-1] == 300000\nassert df[\"id\"].nunique() == 300000\n\n# labels must be in the allowed set\nallowed = {'airplane','automobile','bird','cat','deer','dog','frog','horse','ship','truck'}\nassert set(df[\"label\"]).issubset(allowed)\n\nprint(\"✅ submission.csv looks good.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-08T19:57:17.399270Z","iopub.execute_input":"2025-10-08T19:57:17.399580Z","iopub.status.idle":"2025-10-08T19:57:17.803588Z","shell.execute_reply.started":"2025-10-08T19:57:17.399555Z","shell.execute_reply":"2025-10-08T19:57:17.802946Z"}},"outputs":[],"execution_count":null}]}