{"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":"# Cell 1: Import libraries, set random seeds, and configure GPU training\n\nimport math\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\nfrom tqdm.auto import tqdm\n\n\nSEED = 42\nEPOCHS = 40\nBATCH_SIZE = 128\nNUM_WORKERS = 2\nBASE_LR = 0.1\nWARMUP_EPOCHS = 5\n\nUSE_TTA = True\n\n# Skip the optional official test download to save time.\nEVALUATE_OFFICIAL_TEST = False\n\nWORK = Path(\"/kaggle/working\")\nWORK.mkdir(parents=True, exist_ok=True)\n\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nAMP = DEVICE.type == \"cuda\"\n\ntorch.backends.cudnn.benchmark = True\n\nprint(\"Device:\", DEVICE)\nprint(\"PyTorch:\", torch.__version__)\n\nif not AMP:\n    raise RuntimeError(\n        \"Enable a GPU in Kaggle notebook settings before training.\"\n    )\n\nprint(\"GPU:\", torch.cuda.get_device_name(0))\nprint(\"Training epochs:\", EPOCHS)\nprint(\"Official test download: skipped\")\n\nCLASSES = [\n    \"airplane\", \"automobile\", \"bird\", \"cat\", \"deer\",\n    \"dog\", \"frog\", \"horse\", \"ship\", \"truck\"\n]\n\nCLASS_TO_IDX = {name: i for i, name in enumerate(CLASSES)}\n\nMEAN = (0.4914, 0.4822, 0.4465)\nSTD = (0.2470, 0.2435, 0.2616)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:53:02.02778Z","iopub.status.idle":"2026-09-23T18:53:02.028222Z","shell.execute_reply.started":"2026-09-23T18:53:02.02802Z","shell.execute_reply":"2026-09-23T18:53:02.028055Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 2: Locate the CIFAR-10 competition dataset, extract training images, and verify the files\n\ncandidates = list(Path(\"/kaggle/input\").rglob(\"trainLabels.csv\"))\ncandidates = [\n    p.parent\n    for p in candidates\n    if (p.parent / \"test.7z\").exists()\n]\n\nif len(candidates) != 1:\n    raise RuntimeError(\n        \"Attach exactly one CIFAR-10 competition dataset. \"\n        f\"Found: {candidates}\"\n    )\n\nDATA = candidates[0]\nprint(\"Dataset location:\", DATA)\n\nlabels_df = pd.read_csv(DATA / \"trainLabels.csv\")\nsample = pd.read_csv(DATA / \"sampleSubmission.csv\")\n\nassert len(labels_df) == 50000\nassert labels_df[\"id\"].is_unique\nassert len(sample) == 300000\nassert sample[\"id\"].is_unique\nassert set(labels_df[\"label\"]) == set(CLASSES)\n\nextractor = (\n    shutil.which(\"7z\")\n    or shutil.which(\"7zz\")\n    or shutil.which(\"7za\")\n)\n\nif extractor is None:\n    subprocess.run(\n        [\"apt-get\", \"update\", \"-qq\"],\n        check=True\n    )\n    subprocess.run(\n        [\"apt-get\", \"install\", \"-y\", \"-qq\", \"p7zip-full\"],\n        check=True\n    )\n    extractor = shutil.which(\"7z\")\n\nif extractor is None:\n    raise RuntimeError(\"7-Zip installation failed.\")\n\n\ndef extract_and_verify(name, expected_ids):\n    folder = WORK / name\n    expected = {f\"{int(i)}.png\" for i in expected_ids}\n\n    present = (\n        {p.name for p in folder.glob(\"*.png\")}\n        if folder.exists()\n        else set()\n    )\n\n    if not expected.issubset(present):\n        print(f\"Extracting {name}.7z ...\")\n\n        subprocess.run(\n            [\n                extractor,\n                \"x\",\n                str(DATA / f\"{name}.7z\"),\n                f\"-o{WORK}\",\n                \"-y\",\n                \"-bsp0\",\n                \"-bso0\",\n            ],\n            check=True\n        )\n\n    present = {p.name for p in folder.glob(\"*.png\")}\n\n    if present != expected:\n        raise RuntimeError(\n            f\"{name}: {len(expected - present)} missing and \"\n            f\"{len(present - expected)} extra images.\"\n        )\n\n    print(f\"{name}: verified {len(present):,} images\")\n    return folder\n\n\nTRAIN_DIR = extract_and_verify(\"train\", labels_df[\"id\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:53:02.029755Z","iopub.status.idle":"2026-09-23T18:53:02.03015Z","shell.execute_reply.started":"2026-09-23T18:53:02.029917Z","shell.execute_reply":"2026-09-23T18:53:02.029943Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 3: Create separate training and validation datasets and data loaders\n\ntrain_tfms = transforms.Compose([\n    transforms.RandomCrop(32, padding=4),\n    transforms.RandomHorizontalFlip(),\n    transforms.ColorJitter(0.2, 0.2, 0.2, 0.1),\n    transforms.ToTensor(),\n    transforms.Normalize(MEAN, STD),\n    transforms.RandomErasing(\n        p=0.25,\n        scale=(0.02, 0.15),\n        ratio=(0.3, 3.3),\n        value=\"random\"\n    ),\n])\n\neval_tfms = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(MEAN, STD),\n])\n\n\nclass TrainDataset(Dataset):\n    def __init__(self, frame, transform):\n        self.ids = frame[\"id\"].astype(int).tolist()\n        self.targets = [\n            CLASS_TO_IDX[label] for label in frame[\"label\"]\n        ]\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.ids)\n\n    def __getitem__(self, index):\n        image_path = TRAIN_DIR / f\"{self.ids[index]}.png\"\n\n        with Image.open(image_path) as image:\n            x = self.transform(image.convert(\"RGB\"))\n\n        return x, self.targets[index]\n\n\nrng = np.random.default_rng(SEED)\ntrain_indices = []\nval_indices = []\n\nfor name in CLASSES:\n    indices = labels_df.index[\n        labels_df[\"label\"] == name\n    ].to_numpy().copy()\n\n    rng.shuffle(indices)\n    val_indices.extend(indices[:500].tolist())\n    train_indices.extend(indices[500:].tolist())\n\nassert not set(train_indices) & set(val_indices)\n\ntrain_ds = TrainDataset(\n    labels_df.loc[train_indices], train_tfms\n)\nval_ds = TrainDataset(\n    labels_df.loc[val_indices], eval_tfms\n)\n\n\ndef seed_worker(worker_id):\n    worker_seed = torch.initial_seed() % (2**32)\n    random.seed(worker_seed)\n    np.random.seed(worker_seed)\n\n\ndef loader(dataset, shuffle=False):\n    return DataLoader(\n        dataset,\n        batch_size=BATCH_SIZE,\n        shuffle=shuffle,\n        num_workers=NUM_WORKERS,\n        pin_memory=AMP,\n        worker_init_fn=seed_worker,\n        generator=torch.Generator().manual_seed(SEED),\n        persistent_workers=NUM_WORKERS > 0,\n    )\n\n\ntrain_loader = loader(train_ds, shuffle=True)\nval_loader = loader(val_ds)\n\nprint(\"Training images:\", len(train_ds))\nprint(\"Validation images:\", len(val_ds))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:53:02.031942Z","iopub.status.idle":"2026-09-23T18:53:02.032294Z","shell.execute_reply.started":"2026-09-23T18:53:02.032153Z","shell.execute_reply":"2026-09-23T18:53:02.032174Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 4: Build the randomly initialized model and configure training\n\nmodel = resnet18(weights=None)  # No pretrained weights.\n\nmodel.conv1 = nn.Conv2d(\n    3, 64,\n    kernel_size=3,\n    stride=1,\n    padding=1,\n    bias=False\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\", enabled=AMP)\n\nBEST_PATH = WORK / \"best_model.pth\"\nsteps_per_epoch = len(train_loader)\ntotal_steps = EPOCHS * steps_per_epoch\nwarmup_steps = WARMUP_EPOCHS * steps_per_epoch\n\nassert EPOCHS > WARMUP_EPOCHS\n\n\ndef learning_rate(step):\n    if step < warmup_steps:\n        return BASE_LR * (step + 1) / warmup_steps\n\n    progress = (\n        (step - warmup_steps)\n        / max(1, total_steps - warmup_steps - 1)\n    )\n    return BASE_LR * 0.5 * (\n        1 + math.cos(math.pi * progress)\n    )\n\n\ndef probabilities(x, tta=False):\n    with torch.amp.autocast(\n        device_type=DEVICE.type,\n        enabled=AMP\n    ):\n        logits = model(x)\n        if tta:\n            flipped_logits = model(torch.flip(x, dims=[3]))\n\n    probs = logits.float().softmax(dim=1)\n\n    if tta:\n        flipped_probs = flipped_logits.float().softmax(dim=1)\n        probs = (probs + flipped_probs) / 2\n\n    return probs\n\n\n@torch.inference_mode()\ndef evaluate(data_loader, tta=False):\n    model.eval()\n    correct = 0\n    total = 0\n\n    for x, y in data_loader:\n        x = x.to(DEVICE, non_blocking=True)\n        y = y.to(DEVICE, non_blocking=True)\n\n        predictions = probabilities(x, tta=tta).argmax(dim=1)\n\n        correct += (predictions == y).sum().item()\n        total += y.size(0)\n\n    return correct / total\n\n\nprint(\"Model ready: ResNet-18 with randomly initialized weights.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:53:02.03304Z","iopub.status.idle":"2026-09-23T18:53:02.033586Z","shell.execute_reply.started":"2026-09-23T18:53:02.033379Z","shell.execute_reply":"2026-09-23T18:53:02.033404Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 5: Train the model and save the checkpoint with the highest validation accuracy\n\nbest_val = -1.0\nbest_epoch = 0\nhistory = []\n\nfor epoch in range(1, EPOCHS + 1):\n    model.train()\n\n    loss_sum = 0.0\n    seen = 0\n\n    progress = tqdm(\n        train_loader,\n        desc=f\"Epoch {epoch}/{EPOCHS}\",\n        leave=False\n    )\n\n    for batch_index, (x, y) in enumerate(progress):\n        step = (epoch - 1) * steps_per_epoch + batch_index\n        lr = learning_rate(step)\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(\n            device_type=DEVICE.type,\n            enabled=AMP\n        ):\n            logits = model(x)\n            loss = criterion(logits, y)\n\n        if not torch.isfinite(loss).item():\n            raise RuntimeError(\n                \"Non-finite training loss. Stop and investigate.\"\n            )\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        loss_sum += loss.item() * y.size(0)\n        seen += y.size(0)\n\n    val_acc = evaluate(val_loader)\n\n    if val_acc > best_val:\n        best_val = val_acc\n        best_epoch = epoch\n        torch.save(model.state_dict(), BEST_PATH)\n\n    history.append({\n        \"epoch\": epoch,\n        \"train_loss\": loss_sum / seen,\n        \"val_accuracy\": val_acc,\n        \"learning_rate\": lr,\n    })\n\n    pd.DataFrame(history).to_csv(\n        WORK / \"training_history.csv\",\n        index=False\n    )\n\n    print(\n        f\"Epoch {epoch:03d} | \"\n        f\"train_loss={loss_sum / seen:.4f} | \"\n        f\"val_acc={val_acc:.4f} | \"\n        f\"best={best_val:.4f} | \"\n        f\"lr={lr:.6f}\"\n    )\n\n# Reload only the checkpoint trained above from random initialization.\nmodel.load_state_dict(\n    torch.load(\n        BEST_PATH,\n        map_location=DEVICE,\n        weights_only=True\n    )\n)\n\nmodel.eval()\n\nmetrics = {\n    \"best_epoch\": best_epoch,\n    \"best_validation_accuracy\": best_val,\n    \"epochs\": EPOCHS,\n    \"seed\": SEED,\n    \"pretrained_weights\": False,\n    \"horizontal_flip_tta\": USE_TTA,\n}\n\nprint(\"\\nBest validation accuracy:\", best_val)\nprint(\"Best epoch:\", best_epoch)\nprint(\"Saved model:\", BEST_PATH)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:53:02.034984Z","iopub.status.idle":"2026-09-23T18:53:02.035344Z","shell.execute_reply.started":"2026-09-23T18:53:02.035161Z","shell.execute_reply":"2026-09-23T18:53:02.035182Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 6: Evaluate the best checkpoint on the official CIFAR-10 test set\n\nif EVALUATE_OFFICIAL_TEST:\n    from torchvision.datasets import CIFAR10\n\n    try:\n        official_ds = CIFAR10(\n            root=str(WORK / \"data\"),\n            train=False,\n            download=True,\n            transform=eval_tfms\n        )\n\n    except (OSError, RuntimeError) as error:\n        print(\n            \"Official test download unavailable. \"\n            \"Continuing to Kaggle prediction.\"\n        )\n        print(error)\n\n    else:\n        official_loader = loader(official_ds)\n\n        metrics[\"official_test_accuracy\"] = evaluate(\n            official_loader,\n            tta=False\n        )\n\n        print(\n            \"Official CIFAR-10 test accuracy:\",\n            metrics[\"official_test_accuracy\"]\n        )\n\n        if USE_TTA:\n            metrics[\"official_test_accuracy_tta\"] = evaluate(\n                official_loader,\n                tta=True\n            )\n\n            print(\n                \"Official CIFAR-10 test accuracy with flip TTA:\",\n                metrics[\"official_test_accuracy_tta\"]\n            )\n\n(WORK / \"metrics.json\").write_text(\n    json.dumps(metrics, indent=2)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:53:02.036354Z","iopub.status.idle":"2026-09-23T18:53:02.036633Z","shell.execute_reply.started":"2026-09-23T18:53:02.036515Z","shell.execute_reply":"2026-09-23T18:53:02.03653Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 7: Read test images directly from the archive and create submission.csv\n\nimport io\nimport sys\nimport time\nimport subprocess\nimport ctypes.util\nfrom pathlib import Path\n\n# Install the archive reader.\nif ctypes.util.find_library(\"archive\") is None:\n    subprocess.run([\"apt-get\", \"update\", \"-qq\"], check=True)\n    subprocess.run(\n        [\"apt-get\", \"install\", \"-y\", \"-qq\", \"libarchive-dev\"],\n        check=True\n    )\n\nsubprocess.run(\n    [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"libarchive-c\"],\n    check=True\n)\n\nimport libarchive\n\n# Load the model trained in Cell 5 of THIS notebook.\nmodel.load_state_dict(\n    torch.load(BEST_PATH, map_location=DEVICE, weights_only=True)\n)\nmodel.eval()\n\narchive_path = DATA / \"test.7z\"\nexpected_ids = set(sample[\"id\"].astype(int))\npredictions_by_id = {}\n\nbatch_images = []\nbatch_ids = []\nstarted = time.monotonic()\nlast_report = 0\n\n\n@torch.inference_mode()\ndef predict_batch(images, image_ids):\n    x = torch.stack(images).to(DEVICE)\n\n    predicted = (\n        probabilities(x, tta=USE_TTA)\n        .argmax(dim=1)\n        .cpu()\n        .tolist()\n    )\n\n    for image_id, class_index in zip(image_ids, predicted):\n        if image_id in predictions_by_id:\n            raise RuntimeError(f\"Duplicate image ID: {image_id}\")\n\n        predictions_by_id[image_id] = CLASSES[class_index]\n\n\nprint(\n    \"Opening test archive and predicting directly; \"\n    \"no test-image folder will be created.\",\n    flush=True\n)\n\nwith libarchive.file_reader(str(archive_path)) as archive:\n    for entry in archive:\n        path = Path(entry.pathname)\n\n        if path.suffix.lower() != \".png\":\n            continue\n\n        image_id = int(path.stem)\n\n        if image_id not in expected_ids:\n            raise RuntimeError(f\"Unexpected image ID: {image_id}\")\n\n        image_bytes = b\"\".join(entry.get_blocks())\n\n        with Image.open(io.BytesIO(image_bytes)) as image:\n            tensor = eval_tfms(image.convert(\"RGB\"))\n\n        batch_images.append(tensor)\n        batch_ids.append(image_id)\n\n        if len(batch_images) >= BATCH_SIZE:\n            predict_batch(batch_images, batch_ids)\n            batch_images.clear()\n            batch_ids.clear()\n\n            completed = len(predictions_by_id)\n\n            if completed - last_report >= 5000:\n                elapsed = time.monotonic() - started\n                rate = completed / max(elapsed, 1)\n                remaining = (\n                    len(expected_ids) - completed\n                ) / max(rate, 0.001) / 60\n\n                print(\n                    f\"{completed:,}/{len(expected_ids):,} predicted | \"\n                    f\"{elapsed / 60:.1f} minutes elapsed | \"\n                    f\"estimated {remaining:.1f} minutes remaining\",\n                    flush=True\n                )\n\n                last_report = completed\n\n# Process the final partial batch.\nif batch_images:\n    predict_batch(batch_images, batch_ids)\n\nassert set(predictions_by_id) == expected_ids, (\n    \"Missing or unexpected test predictions.\"\n)\n\nsubmission = pd.DataFrame({\n    \"id\": sample[\"id\"].astype(int),\n    \"label\": [\n        predictions_by_id[int(image_id)]\n        for image_id in sample[\"id\"]\n    ],\n})\n\nassert len(submission) == 300000\nassert submission[\"id\"].is_unique\nassert not submission.isna().any().any()\nassert set(submission[\"label\"]).issubset(CLASSES)\n\nsubmission_path = WORK / \"submission.csv\"\nsubmission.to_csv(submission_path, index=False)\n\ncheck = pd.read_csv(submission_path)\n\nassert check.columns.tolist() == [\"id\", \"label\"]\nassert pd.api.types.is_integer_dtype(check[\"id\"])\nassert check[\"id\"].tolist() == sample[\"id\"].astype(int).tolist()\nassert len(check) == 300000\nassert not check.isna().any().any()\n\nprint(check.head().to_string(index=False))\nprint(f\"\\nREADY: {submission_path}\", flush=True)\nprint(\"Download this CSV and submit it to Kaggle.\", flush=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:53:02.037631Z","iopub.status.idle":"2026-09-23T18:53:02.037883Z","shell.execute_reply.started":"2026-09-23T18:53:02.037763Z","shell.execute_reply":"2026-09-23T18:53:02.037778Z"}},"outputs":[],"execution_count":null}]}