{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# CIFAR-10 assignment — complete code\n# Paste this entire block into ONE cell in a NEW Kaggle notebook.\n# Attach CIFAR-10 competition data. Enable GPU and Internet.\n\nimport os\nimport math\nimport time\nimport random\nimport json\nimport shutil\nimport subprocess\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torchvision.models import resnet18\n\n\n# ============================================================\n# 1. SETTINGS\n# ============================================================\n\nSEED = 42\nEPOCHS = 40\nBATCH_SIZE = 128\nWORKERS = 2\nBASE_LR = 0.1\nUSE_TTA = True\nEXTRACTION_LIMIT_MINUTES = 180\n\n# Large image folders go into temporary storage.\nTEMP = Path(\"/tmp/cifar10_assignment\")\n\n# Only model/results/submission go into Kaggle outputs.\nOUT = Path(\"/kaggle/working\")\n\nTEMP.mkdir(parents=True, exist_ok=True)\nOUT.mkdir(parents=True, exist_ok=True)\n\nif not torch.cuda.is_available():\n    raise RuntimeError(\"Enable a GPU before running this notebook.\")\n\nDEVICE = torch.device(\"cuda\")\n\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\ntorch.backends.cudnn.benchmark = True\n\nprint(\"GPU:\", torch.cuda.get_device_name(0), flush=True)\nprint(\"Training epochs:\", EPOCHS, flush=True)\n\nCLASSES = [\n    \"airplane\", \"automobile\", \"bird\", \"cat\", \"deer\",\n    \"dog\", \"frog\", \"horse\", \"ship\", \"truck\"\n]\n\nclass_to_idx = {\n    label: index for index, label in enumerate(CLASSES)\n}\n\n\n# ============================================================\n# 2. LOCATE COMPETITION DATA\n# ============================================================\n\nmatches = [\n    path.parent\n    for path in Path(\"/kaggle/input\").rglob(\"trainLabels.csv\")\n    if (path.parent / \"test.7z\").exists()\n]\n\nif len(matches) != 1:\n    raise RuntimeError(\n        \"Attach exactly one CIFAR-10 competition dataset.\"\n    )\n\nDATA = matches[0]\n\nlabels = pd.read_csv(DATA / \"trainLabels.csv\")\nsample = pd.read_csv(DATA / \"sampleSubmission.csv\")\n\nassert len(labels) == 50000 and labels[\"id\"].is_unique\nassert len(sample) == 300000 and sample[\"id\"].is_unique\nassert set(labels[\"label\"]) == set(CLASSES)\n\nprint(\"Input:\", DATA, flush=True)\n\n\n# ============================================================\n# 3. EXTRACT TO TEMPORARY STORAGE BEFORE TRAINING\n# ============================================================\n\nsevenzip = shutil.which(\"7z\") or shutil.which(\"7zz\")\n\nif sevenzip is None:\n    subprocess.run(\n        [\"apt-get\", \"update\", \"-qq\"],\n        check=True,\n        timeout=180\n    )\n    subprocess.run(\n        [\"apt-get\", \"install\", \"-y\", \"-qq\", \"p7zip-full\"],\n        check=True,\n        timeout=180\n    )\n    sevenzip = shutil.which(\"7z\")\n\nif not sevenzip:\n    raise RuntimeError(\"7-Zip was not installed successfully.\")\n\n\ndef extract(name, expected_ids):\n    folder = TEMP / name\n    expected = {f\"{int(i)}.png\" for i in expected_ids}\n    marker = TEMP / f\"{name}.complete\"\n\n    def names():\n        if not folder.exists():\n            return set()\n\n        with os.scandir(folder) as entries:\n            return {\n                entry.name\n                for entry in entries\n                if entry.name.endswith(\".png\")\n            }\n\n    if marker.exists() and names() == expected:\n        print(f\"{name}: reusing verified images\", flush=True)\n        return folder\n\n    logfile = TEMP / f\"{name}_extraction.log\"\n    start = time.monotonic()\n\n    print(\n        f\"Extracting {name} to temporary storage...\",\n        flush=True\n    )\n\n    with logfile.open(\"w\") as log:\n        process = subprocess.Popen(\n            [\n                sevenzip,\n                \"x\",\n                str(DATA / f\"{name}.7z\"),\n                f\"-o{TEMP}\",\n                \"-aoa\",\n                \"-y\",\n                \"-bso0\",\n                \"-bsp0\",\n            ],\n            stdout=log,\n            stderr=subprocess.STDOUT\n        )\n\n        try:\n            while True:\n                try:\n                    result = process.wait(timeout=30)\n                    break\n\n                except subprocess.TimeoutExpired:\n                    elapsed = (time.monotonic() - start) / 60\n\n                    print(\n                        f\"{name}: {len(names()):,}/{len(expected):,} \"\n                        f\"files; {elapsed:.1f} minutes\",\n                        flush=True\n                    )\n\n                    if elapsed >= EXTRACTION_LIMIT_MINUTES:\n                        raise TimeoutError(\n                            f\"{name} extraction exceeded \"\n                            f\"{EXTRACTION_LIMIT_MINUTES} minutes. \"\n                            \"Stopped before training. \"\n                            \"Inspect the progress printed above.\"\n                        )\n\n        finally:\n            if process.poll() is None:\n                process.kill()\n                process.wait()\n\n    if result != 0:\n        raise RuntimeError(\n            logfile.read_text(errors=\"replace\")[-2000:]\n        )\n\n    if names() != expected:\n        raise RuntimeError(\n            f\"{name}: extracted image IDs do not match the input.\"\n        )\n\n    marker.write_text(\"complete\")\n\n    print(\n        f\"{name}: verified {len(expected):,} images\",\n        flush=True\n    )\n\n    return folder\n\n\nTRAIN_DIR = extract(\"train\", labels[\"id\"])\nTEST_DIR = extract(\"test\", sample[\"id\"])\n\n\n# ============================================================\n# 4. TRAINING AND VALIDATION DATASETS\n# ============================================================\n\nmean = (0.4914, 0.4822, 0.4465)\nstd = (0.2470, 0.2435, 0.2616)\n\ntrain_transform = transforms.Compose([\n    transforms.RandomCrop(32, padding=4),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize(mean, std),\n])\n\neval_transform = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(mean, std),\n])\n\n\nclass Images(Dataset):\n    def __init__(self, frame, folder, transform, test=False):\n        self.ids = frame[\"id\"].astype(int).tolist()\n        self.targets = (\n            None if test\n            else [class_to_idx[label] for label in frame[\"label\"]]\n        )\n        self.folder = folder\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.ids)\n\n    def __getitem__(self, index):\n        path = self.folder / f\"{self.ids[index]}.png\"\n\n        with Image.open(path) as image:\n            x = self.transform(image.convert(\"RGB\"))\n\n        y = (\n            self.ids[index]\n            if self.targets is None\n            else self.targets[index]\n        )\n\n        return x, y\n\n\n# Exactly 500 validation images per class.\nrng = np.random.default_rng(SEED)\ntrain_idx = []\nval_idx = []\n\nfor label in CLASSES:\n    indices = labels.index[\n        labels[\"label\"] == label\n    ].to_numpy().copy()\n\n    rng.shuffle(indices)\n\n    val_idx.extend(indices[:500])\n    train_idx.extend(indices[500:])\n\nassert not set(train_idx) & set(val_idx)\n\n\ndef seed_worker(worker_id):\n    worker_seed = torch.initial_seed() % (2**32)\n    np.random.seed(worker_seed)\n    random.seed(worker_seed)\n\n\ndef make_loader(dataset, shuffle=False):\n    return DataLoader(\n        dataset,\n        batch_size=BATCH_SIZE,\n        shuffle=shuffle,\n        num_workers=WORKERS,\n        pin_memory=True,\n        timeout=120 if WORKERS else 0,\n        worker_init_fn=seed_worker,\n        generator=torch.Generator().manual_seed(SEED),\n    )\n\n\ntrain_loader = make_loader(\n    Images(\n        labels.loc[train_idx],\n        TRAIN_DIR,\n        train_transform\n    ),\n    shuffle=True\n)\n\nval_loader = make_loader(\n    Images(\n        labels.loc[val_idx],\n        TRAIN_DIR,\n        eval_transform\n    )\n)\n\ntest_loader = make_loader(\n    Images(\n        sample,\n        TEST_DIR,\n        eval_transform,\n        test=True\n    )\n)\n\nprint(\n    \"Training: 45,000 images | Validation: 5,000 images\",\n    flush=True\n)\n\n\n# ============================================================\n# 5. MODEL — RANDOM INITIALIZATION, NO PRETRAINED WEIGHTS\n# ============================================================\n\nmodel = resnet18(weights=None)\n\nmodel.conv1 = nn.Conv2d(\n    3, 64,\n    kernel_size=3,\n    stride=1,\n    padding=1,\n    bias=False\n)\n\nmodel.maxpool = nn.Identity()\nmodel.fc = nn.Linear(model.fc.in_features, 10)\nmodel = model.to(DEVICE)\n\noptimizer = torch.optim.SGD(\n    model.parameters(),\n    lr=BASE_LR,\n    momentum=0.9,\n    weight_decay=5e-4,\n    nesterov=True\n)\n\ncriterion = nn.CrossEntropyLoss(label_smoothing=0.1)\nscaler = torch.amp.GradScaler(\"cuda\")\n\nsteps = len(train_loader)\nwarmup = 5 * steps\ntotal_steps = EPOCHS * steps\n\nBEST = OUT / \"best_model.pth\"\n\n\n@torch.inference_mode()\ndef validate():\n    model.eval()\n    correct = 0\n    total = 0\n\n    for x, y in val_loader:\n        x = x.to(DEVICE)\n        y = y.to(DEVICE)\n\n        with torch.amp.autocast(\"cuda\"):\n            logits = model(x)\n\n        correct += (logits.argmax(1) == y).sum().item()\n        total += len(y)\n\n    return correct / total\n\n\n# ============================================================\n# 6. TRAIN AND SAVE THE BEST MODEL\n# ============================================================\n\nhistory = []\nbest = -1.0\nbest_epoch = 0\n\nfor epoch in range(EPOCHS):\n    epoch_start = time.monotonic()\n    model.train()\n    loss_sum = 0.0\n\n    for batch, (x, y) in enumerate(train_loader):\n        step = epoch * steps + batch\n\n        if step < warmup:\n            lr = BASE_LR * (step + 1) / warmup\n        else:\n            fraction = (\n                (step - warmup)\n                / max(1, total_steps - warmup - 1)\n            )\n            lr = BASE_LR * 0.5 * (\n                1 + math.cos(math.pi * fraction)\n            )\n\n        for group in optimizer.param_groups:\n            group[\"lr\"] = lr\n\n        x = x.to(DEVICE, non_blocking=True)\n        y = y.to(DEVICE, non_blocking=True)\n\n        optimizer.zero_grad(set_to_none=True)\n\n        with torch.amp.autocast(\"cuda\"):\n            loss = criterion(model(x), y)\n\n        if not torch.isfinite(loss).item():\n            raise RuntimeError(\"Training loss is not finite.\")\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        loss_sum += loss.item() * len(y)\n\n    accuracy = validate()\n\n    if accuracy > best:\n        best = accuracy\n        best_epoch = epoch + 1\n        torch.save(model.state_dict(), BEST)\n\n    history.append({\n        \"epoch\": epoch + 1,\n        \"train_loss\": loss_sum / len(train_loader.dataset),\n        \"validation_accuracy\": accuracy,\n    })\n\n    pd.DataFrame(history).to_csv(\n        OUT / \"training_history.csv\",\n        index=False\n    )\n\n    print(\n        f\"Epoch {epoch + 1}/{EPOCHS} | \"\n        f\"validation={accuracy:.4f} | \"\n        f\"best={best:.4f} | \"\n        f\"{time.monotonic() - epoch_start:.0f}s\",\n        flush=True\n    )\n\n\n# ============================================================\n# 7. PREDICT ALL KAGGLE TEST IMAGES\n# ============================================================\n\n# Load the checkpoint trained above, not an external model.\nmodel.load_state_dict(\n    torch.load(\n        BEST,\n        map_location=DEVICE,\n        weights_only=True\n    )\n)\n\nmodel.eval()\n\nids = []\npredictions = []\nstart = time.monotonic()\n\nwith torch.inference_mode():\n    for batch, (x, image_ids) in enumerate(test_loader):\n        x = x.to(DEVICE, non_blocking=True)\n\n        with torch.amp.autocast(\"cuda\"):\n            logits = model(x)\n            flipped = model(x.flip(3)) if USE_TTA else None\n\n        scores = logits.float().softmax(1)\n\n        if USE_TTA:\n            scores = (\n                scores + flipped.float().softmax(1)\n            ) / 2\n\n        predicted_indices = scores.argmax(1).cpu().tolist()\n\n        predictions.extend(\n            CLASSES[index] for index in predicted_indices\n        )\n        ids.extend(image_ids.tolist())\n\n        if (batch + 1) % 100 == 0 or len(ids) == len(sample):\n            elapsed = time.monotonic() - start\n            remaining = (\n                elapsed / len(ids)\n                * (len(sample) - len(ids))\n                / 60\n            )\n\n            print(\n                f\"Predicted {len(ids):,}/{len(sample):,}; \"\n                f\"estimated {remaining:.1f} minutes remaining\",\n                flush=True\n            )\n\n\n# ============================================================\n# 8. VERIFY AND SAVE SUBMISSION\n# ============================================================\n\nsubmission = pd.DataFrame({\n    \"id\": ids,\n    \"label\": predictions,\n})\n\nassert len(submission) == 300000\nassert submission[\"id\"].is_unique\nassert set(submission[\"id\"]) == set(sample[\"id\"])\n\nsubmission = (\n    submission\n    .set_index(\"id\")\n    .loc[sample[\"id\"]]\n    .reset_index()\n)\n\nsubmission.to_csv(\n    OUT / \"submission.csv\",\n    index=False\n)\n\ncheck = pd.read_csv(OUT / \"submission.csv\")\n\nassert check.columns.tolist() == [\"id\", \"label\"]\nassert check[\"id\"].tolist() == sample[\"id\"].astype(int).tolist()\nassert pd.api.types.is_integer_dtype(check[\"id\"])\nassert not check.isna().any().any()\nassert set(check[\"label\"]).issubset(CLASSES)\n\nmetrics = {\n    \"epochs\": EPOCHS,\n    \"best_epoch\": best_epoch,\n    \"best_validation_accuracy\": best,\n    \"random_initialization\": True,\n    \"flip_tta\": USE_TTA,\n    \"train_images\": 45000,\n    \"validation_images\": 5000,\n}\n\n(OUT / \"metrics.json\").write_text(\n    json.dumps(metrics, indent=2)\n)\n\nprint(\"\\nDONE: /kaggle/working/submission.csv\", flush=True)\nprint(\"Upload this CSV to Kaggle to obtain your competition score.\")\nprint(check.head().to_string(index=False))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-09-28T10:27:15.829416Z","iopub.execute_input":"2026-09-28T10:27:15.830034Z"}},"outputs":[],"execution_count":null}]}