{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10"},"accelerator":"GPU"},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CIFAR-10 — Object Recognition in Images\nConvNet trained from scratch. No pre-trained weights.\n\n.\n\nThe test archive is expanded with `bsdtar` into `/dev/shm`, a RAM disk.\nMeasured on this machine: 300,000 files ","metadata":{}},{"cell_type":"code","source":"# =====================================================================\n# Step 1 - Locate the competition data and expand test.7z\n# =====================================================================\nimport os, sys, re, io, glob, time, shutil, subprocess\nimport numpy as np\nfrom PIL import Image\n\nmatches = glob.glob(\"/kaggle/input/**/test.7z\", recursive=True)\nif not matches:\n    found = sorted(os.listdir(\"/kaggle/input\")) if os.path.isdir(\"/kaggle/input\") else []\n    raise FileNotFoundError(\n        f\"test.7z not found under /kaggle/input. Attached inputs: {found}. \"\n        \"Join the competition, then Add Input -> Competitions -> CIFAR-10.\"\n    )\n\nTEST_7Z  = matches[0]\nCOMP_DIR = os.path.dirname(TEST_7Z)\nN_TEST   = 300000\nSHM_DIR  = \"/dev/shm/cifar_test\"\n\nprint(\"Competition folder:\", COMP_DIR, flush=True)\nprint(\"Contents:\", sorted(os.listdir(COMP_DIR)), flush=True)\nprint(\"Archive size: %.0f MB\" % (os.path.getsize(TEST_7Z) / 1e6), flush=True)\n\n# bsdtar comes from libarchive-tools. apt-get update first: without a fresh\n# package index the download 404s.\nsubprocess.run(\"apt-get -qq update\", shell=True, timeout=900)\nsubprocess.run(\"apt-get -qq install -y libarchive-tools\", shell=True, timeout=900)\nprint(\"bsdtar:\", shutil.which(\"bsdtar\"), flush=True)\n\nTEST_DIR    = None      # set if the images live on disk\nTEST_IMAGES = None      # set if the images live in a numpy array\n\n# ---- Method A: bsdtar into the RAM disk (measured at ~70s) ----\nif shutil.which(\"bsdtar\"):\n    shutil.rmtree(SHM_DIR, ignore_errors=True)\n    os.makedirs(SHM_DIR, exist_ok=True)\n    t0 = time.time()\n    rc = subprocess.run(f'bsdtar -xf \"{TEST_7Z}\" -C \"{SHM_DIR}\"',\n                        shell=True, timeout=3600).returncode\n    found = glob.glob(os.path.join(SHM_DIR, \"**\", \"1.png\"), recursive=True)\n    if rc == 0 and found:\n        cand  = os.path.dirname(found[0])\n        n_img = len(os.listdir(cand))\n        print(f\"bsdtar finished in {time.time()-t0:.0f}s -> {n_img:,} files in {cand}\",\n              flush=True)\n        if n_img >= N_TEST:\n            TEST_DIR = cand\n    if TEST_DIR is None:\n        print(\"bsdtar did not produce a complete set, falling back.\", flush=True)\n\n# ---- Method B: libarchive-c streamed straight into memory (~2.5 min) ----\nif TEST_DIR is None:\n    shutil.rmtree(SHM_DIR, ignore_errors=True)\n    subprocess.run(f\"{sys.executable} -m pip install -q libarchive-c\", shell=True)\n    import libarchive\n\n    TEST_IMAGES = np.zeros((N_TEST, 32, 32, 3), dtype=np.uint8)   # about 0.9 GB\n    seen = np.zeros(N_TEST, dtype=bool)\n    t0, cnt = time.time(), 0\n    with libarchive.file_reader(TEST_7Z) as archive:\n        for entry in archive:\n            name = str(entry.pathname)\n            m = re.search(r\"(\\d+)\\.png$\", name)\n            if m is None:\n                continue\n            idx = int(m.group(1)) - 1\n            buf = b\"\".join(entry.get_blocks())\n            TEST_IMAGES[idx] = np.asarray(\n                Image.open(io.BytesIO(buf)).convert(\"RGB\"), dtype=np.uint8)\n            seen[idx] = True\n            cnt += 1\n            if cnt % 50000 == 0:\n                el = time.time() - t0\n                print(f\"  {cnt:,} / {N_TEST:,} | {cnt/el:,.0f} img/s\", flush=True)\n    print(f\"Decoded {cnt:,} images in {(time.time()-t0)/60:.1f} min. \"\n          f\"Missing: {int((~seen).sum())}\", flush=True)\n    assert cnt == N_TEST and seen.all(), \"Test archive incomplete.\"\n\n# ---- Verify whichever path ran ----\nif TEST_DIR is not None:\n    for probe_id in (1, 150000, N_TEST):\n        p = os.path.join(TEST_DIR, f\"{probe_id}.png\")\n        assert os.path.exists(p), f\"missing {p}\"\n        assert Image.open(p).convert(\"RGB\").size == (32, 32)\n    print(f\"Test images ready on disk: {TEST_DIR}\", flush=True)\nelse:\n    print(\"Test images ready in memory:\", TEST_IMAGES.shape, flush=True)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =====================================================================\n# Step 2 - Training and validation data\n# =====================================================================\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import datasets, transforms\n\nSEED = 42\ntorch.manual_seed(SEED)\nnp.random.seed(SEED)\ntorch.backends.cudnn.benchmark = True\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Device:\", DEVICE, flush=True)\nif DEVICE == \"cuda\":\n    print(\"GPU:\", torch.cuda.get_device_name(0), flush=True)\n\nMEAN = (0.4914, 0.4822, 0.4465)\nSTD  = (0.2470, 0.2435, 0.2616)\n\ntrain_tf = transforms.Compose([\n    transforms.RandomCrop(32, padding=4, padding_mode=\"reflect\"),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize(MEAN, STD),\n    transforms.RandomErasing(p=0.25, scale=(0.02, 0.20)),\n])\neval_tf = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(MEAN, STD),\n])\n\nDATA_ROOT = \"/kaggle/temp/data\"\nos.makedirs(DATA_ROOT, exist_ok=True)\n\ntrain_set = val_set = None\nfor attempt in range(1, 4):\n    try:\n        train_set = datasets.CIFAR10(DATA_ROOT, train=True,  download=True, transform=train_tf)\n        val_set   = datasets.CIFAR10(DATA_ROOT, train=False, download=True, transform=eval_tf)\n        break\n    except Exception as e:\n        print(f\"CIFAR-10 download attempt {attempt} failed: {e}\", flush=True)\n        time.sleep(20)\nif train_set is None:\n    raise RuntimeError(\"Could not download CIFAR-10. Check that Internet is On.\")\n\nCLASSES = train_set.classes\nprint(\"Classes:\", CLASSES, flush=True)\nprint(\"Train:\", len(train_set), \"| Validation:\", len(val_set), flush=True)\n\nNW    = min(4, os.cpu_count() or 2)\nBATCH = 128\n\ntrain_loader = DataLoader(train_set, batch_size=BATCH, shuffle=True,\n                          num_workers=NW, pin_memory=True,\n                          drop_last=True, persistent_workers=True)\nval_loader   = DataLoader(val_set, batch_size=512, shuffle=False,\n                          num_workers=NW, pin_memory=True,\n                          persistent_workers=True)\nprint(\"Batches per epoch:\", len(train_loader), flush=True)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =====================================================================\n# Step 3 - Model: ResNet-18 adapted to 32x32 input\n# All weights randomly initialised. No pre-trained weights anywhere.\n# =====================================================================\nclass BasicBlock(nn.Module):\n    def __init__(self, in_planes, planes, stride=1):\n        super().__init__()\n        self.conv1 = nn.Conv2d(in_planes, planes, 3, stride, 1, bias=False)\n        self.bn1   = nn.BatchNorm2d(planes)\n        self.conv2 = nn.Conv2d(planes, planes, 3, 1, 1, bias=False)\n        self.bn2   = nn.BatchNorm2d(planes)\n        self.shortcut = nn.Sequential()\n        if stride != 1 or in_planes != planes:\n            self.shortcut = nn.Sequential(\n                nn.Conv2d(in_planes, planes, 1, stride, bias=False),\n                nn.BatchNorm2d(planes),\n            )\n\n    def forward(self, x):\n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        out = out + self.shortcut(x)\n        return F.relu(out)\n\nclass ResNet18(nn.Module):\n    def __init__(self, num_classes=10):\n        super().__init__()\n        self.in_planes = 64\n        self.conv1  = nn.Conv2d(3, 64, 3, 1, 1, bias=False)   # 3x3 stem, no max-pool\n        self.bn1    = nn.BatchNorm2d(64)\n        self.layer1 = self._make_layer(64,  2, 1)\n        self.layer2 = self._make_layer(128, 2, 2)\n        self.layer3 = self._make_layer(256, 2, 2)\n        self.layer4 = self._make_layer(512, 2, 2)\n        self.fc     = nn.Linear(512, num_classes)\n\n    def _make_layer(self, planes, blocks, stride):\n        layers = []\n        for s in [stride] + [1] * (blocks - 1):\n            layers.append(BasicBlock(self.in_planes, planes, s))\n            self.in_planes = planes\n        return nn.Sequential(*layers)\n\n    def forward(self, x):\n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.layer1(out)\n        out = self.layer2(out)\n        out = self.layer3(out)\n        out = self.layer4(out)\n        out = F.adaptive_avg_pool2d(out, 1).flatten(1)\n        return self.fc(out)\n\nmodel = ResNet18(10).to(DEVICE).to(memory_format=torch.channels_last)\nprint(\"Trainable parameters:\",\n      sum(p.numel() for p in model.parameters() if p.requires_grad), flush=True)\n\nwith torch.no_grad():\n    probe_out = model(torch.zeros(2, 3, 32, 32, device=DEVICE))\nassert probe_out.shape == (2, 10), probe_out.shape\nprint(\"Forward pass OK:\", tuple(probe_out.shape), flush=True)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =====================================================================\n# Step 4 - Training\n# =====================================================================\nEPOCHS  = 40\nUSE_AMP = (DEVICE == \"cuda\")\n\ncriterion = nn.CrossEntropyLoss(label_smoothing=0.1)\noptimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9,\n                            weight_decay=5e-4, nesterov=True)\nscheduler = torch.optim.lr_scheduler.OneCycleLR(\n    optimizer, max_lr=0.1, epochs=EPOCHS, steps_per_epoch=len(train_loader),\n    pct_start=0.15, div_factor=10.0, final_div_factor=100.0)\n\ntry:\n    from torch.amp import autocast as _ac, GradScaler as _gs\n    amp_ctx = lambda: _ac(\"cuda\", enabled=USE_AMP)\n    scaler  = _gs(\"cuda\", enabled=USE_AMP)\nexcept Exception:\n    from torch.cuda.amp import autocast as _ac, GradScaler as _gs\n    amp_ctx = lambda: _ac(enabled=USE_AMP)\n    scaler  = _gs(enabled=USE_AMP)\n\n@torch.no_grad()\ndef evaluate():\n    model.eval()\n    correct = total = 0\n    for x, y in val_loader:\n        x = x.to(DEVICE, non_blocking=True).to(memory_format=torch.channels_last)\n        y = y.to(DEVICE, non_blocking=True)\n        with amp_ctx():\n            out = model(x)\n        correct += (out.argmax(1) == y).sum().item()\n        total   += y.size(0)\n    return correct / total\n\nCKPT = \"/kaggle/working/best_model.pt\"\nbest_acc = 0.0\nstart = time.time()\n\nfor epoch in range(1, EPOCHS + 1):\n    model.train()\n    running_loss, seen_n = 0.0, 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 amp_ctx():\n            loss = criterion(model(x), y)\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        scheduler.step()\n        running_loss += loss.item() * y.size(0)\n        seen_n += y.size(0)\n\n    val_acc = evaluate()\n    if val_acc > best_acc:\n        best_acc = val_acc\n        torch.save(model.state_dict(), CKPT)\n\n    print(f\"Epoch {epoch:3d}/{EPOCHS} | loss {running_loss/seen_n:.4f} \"\n          f\"| val acc {val_acc:.4f} | lr {scheduler.get_last_lr()[0]:.4f} \"\n          f\"| {time.time()-start:.0f}s\", flush=True)\n\nprint(f\"\\nBest validation accuracy: {best_acc:.4f}  (test error {1-best_acc:.4f})\",\n      flush=True)\nassert os.path.exists(CKPT)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =====================================================================\n# Step 5 - Inference on the 300,000 test images and submission file\n# =====================================================================\nclass KaggleTestSet(Dataset):\n    \"\"\"Serves image id i+1, from the RAM disk or from the in-memory array.\"\"\"\n    def __init__(self, transform, folder=None, array=None, n=N_TEST):\n        self.tf, self.folder, self.arr, self.n = transform, folder, array, n\n\n    def __len__(self):\n        return self.n\n\n    def __getitem__(self, i):\n        if self.folder is not None:\n            img = Image.open(os.path.join(self.folder, f\"{i+1}.png\")).convert(\"RGB\")\n        else:\n            img = Image.fromarray(self.arr[i])\n        return self.tf(img), i + 1\n\ntest_loader = DataLoader(\n    KaggleTestSet(eval_tf, folder=TEST_DIR, array=TEST_IMAGES),\n    batch_size=512, shuffle=False, num_workers=NW, pin_memory=True)\n\nmodel.load_state_dict(torch.load(CKPT, map_location=DEVICE))\nmodel.eval()\n\npred_ids, pred_labels = [], []\nt0 = time.time()\nwith torch.no_grad():\n    for i, (x, ids) in enumerate(test_loader, 1):\n        x = x.to(DEVICE, non_blocking=True).to(memory_format=torch.channels_last)\n        with amp_ctx():\n            # Average the logits of each image and its horizontal mirror.\n            logits = model(x) + model(torch.flip(x, dims=[3]))\n        pred_ids.extend(ids.tolist())\n        pred_labels.extend(logits.argmax(1).cpu().tolist())\n        if i % 100 == 0:\n            print(f\"  {min(i*512, N_TEST):,} / {N_TEST:,} predicted \"\n                  f\"({time.time()-t0:.0f}s)\", flush=True)\n\nsubmission = pd.DataFrame({\n    \"id\": pred_ids,\n    \"label\": [CLASSES[k] for k in pred_labels],\n}).sort_values(\"id\").reset_index(drop=True)\n\nOUT = \"/kaggle/working/submission.csv\"\nsubmission.to_csv(OUT, index=False)\n\nprint(\"\\nRows:\", len(submission), flush=True)\nprint(\"Labels present:\", sorted(submission[\"label\"].unique()), flush=True)\nprint(submission.head(), flush=True)\n\nassert len(submission) == N_TEST\nassert submission[\"id\"].min() == 1 and submission[\"id\"].max() == N_TEST\nassert submission[\"id\"].is_unique\nassert set(submission[\"label\"]).issubset(set(CLASSES))\n\n# Free the RAM disk so the saved version stays small.\nshutil.rmtree(SHM_DIR, ignore_errors=True)\n\nprint(\"\\nSaved:\", OUT, flush=True)\nprint(f\"Best validation accuracy: {best_acc:.4f} \"\n      f\"-> expected Kaggle score around {best_acc:.4f}\", flush=True)","metadata":{},"outputs":[],"execution_count":null}]}