{"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 the GPU and training settings\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 = 80\nBATCH_SIZE = 128  # Reduce to 64 if GPU memory runs out.\nNUM_WORKERS = 2\nBASE_LR = 0.1\nWARMUP_EPOCHS = 5\n\nUSE_TTA = True\nEVALUATE_OFFICIAL_TEST = True\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\n# GPU runs can still vary slightly despite setting the random seeds.\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-23T14:04:20.619306Z","iopub.execute_input":"2026-09-23T14:04:20.619797Z","iopub.status.idle":"2026-09-23T14:04:28.921226Z","shell.execute_reply.started":"2026-09-23T14:04:20.61977Z","shell.execute_reply":"2026-09-23T14:04:28.919641Z"},"jupyter":{"source_hidden":true}},"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-23T14:04:39.742074Z","iopub.execute_input":"2026-09-23T14:04:39.742368Z","iopub.status.idle":"2026-09-23T14:11:05.857156Z","shell.execute_reply.started":"2026-09-23T14:04:39.742344Z","shell.execute_reply":"2026-09-23T14:11:05.85624Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 3: Apply training augmentation and create separate training and validation datasets\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\n# Stratified split: 500 validation images from each class.\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\n    val_indices.extend(indices[:500].tolist())\n    train_indices.extend(indices[500:].tolist())\n\nassert not set(train_indices) & set(val_indices)\n\n# Separate dataset objects ensure separate transforms.\ntrain_ds = TrainDataset(\n    labels_df.loc[train_indices],\n    train_tfms\n)\n\nval_ds = TrainDataset(\n    labels_df.loc[val_indices],\n    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-23T14:11:05.933475Z","iopub.execute_input":"2026-09-23T14:11:05.933816Z","iopub.status.idle":"2026-09-23T14:11:06.002946Z","shell.execute_reply.started":"2026-09-23T14:11:05.933786Z","shell.execute_reply":"2026-09-23T14:11:06.002376Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 4: Build a randomly initialized ResNet-18 and configure optimization and evaluation\n\n# IMPORTANT: No pretrained weights.\nmodel = resnet18(weights=None)\n\n# Adapt ResNet-18 to 32 × 32 CIFAR-10 images.\nmodel.conv1 = nn.Conv2d(\n    3,\n    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)\n\nscaler = torch.amp.GradScaler(\"cuda\", enabled=AMP)\n\nBEST_PATH = WORK / \"best_model.pth\"\n\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\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\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\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},"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},"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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 7: Predict Kaggle test labels with horizontal-flip augmentation and save submission.csv\n\nTEST_DIR = extract_and_verify(\"test\", sample[\"id\"])\n\n\nclass TestDataset(Dataset):\n    def __init__(self, ids):\n        self.ids = [int(i) for i in ids]\n\n    def __len__(self):\n        return len(self.ids)\n\n    def __getitem__(self, index):\n        image_id = self.ids[index]\n        image_path = TEST_DIR / f\"{image_id}.png\"\n\n        with Image.open(image_path) as image:\n            x = eval_tfms(image.convert(\"RGB\"))\n\n        return x, image_id\n\n\ntest_loader = loader(TestDataset(sample[\"id\"]))\n\nids = []\npredictions = []\n\nmodel.eval()\n\nwith torch.inference_mode():\n    for x, image_ids in tqdm(\n        test_loader,\n        desc=\"Predicting Kaggle test images\"\n    ):\n        x = x.to(DEVICE, non_blocking=True)\n\n        predicted_indices = (\n            probabilities(x, tta=USE_TTA)\n            .argmax(dim=1)\n            .cpu()\n            .tolist()\n        )\n\n        # Convert tensor IDs to ordinary Python integers.\n        ids.extend(image_ids.tolist())\n\n        predictions.extend(\n            CLASSES[index] for index in predicted_indices\n        )\n\nsubmission = pd.DataFrame({\n    \"id\": ids,\n    \"label\": predictions,\n})\n\nassert submission[\"id\"].is_unique\nassert len(submission) == len(sample) == 300000\nassert set(submission[\"id\"]) == set(sample[\"id\"])\nassert set(submission[\"label\"]).issubset(CLASSES)\n\n# Match the sample submission's row order.\nsubmission = (\n    submission\n    .set_index(\"id\")\n    .loc[sample[\"id\"]]\n    .reset_index()\n)\n\nsubmission = submission[[\"id\", \"label\"]]\n\nSUBMISSION_PATH = WORK / \"submission.csv\"\nsubmission.to_csv(SUBMISSION_PATH, index=False)\n\n# Verify the saved CSV, including its ID format.\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 not check.isna().any().any()\nassert len(check) == 300000\n\nprint(\"\\nSubmission verified:\", SUBMISSION_PATH)\nprint(\"Shape:\", check.shape)\nprint(check.head().to_string(index=False))\nprint(\"\\nUpload submission.csv to the Kaggle competition.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}