{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.13.15","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":"# ============================================================\n# DSEG660 Challenge Assignment\n# Custom ConvNet Training on CIFAR-10\n# Step 1: Environment and Dataset Verification\n# ============================================================\n\nimport os\nimport sys\nimport torch\nimport numpy as np\nimport pandas as pd\n\nprint(\"=\" * 60)\nprint(\"ENVIRONMENT\")\nprint(\"=\" * 60)\n\nprint(f\"Python version : {sys.version.split()[0]}\")\nprint(f\"PyTorch version: {torch.__version__}\")\nprint(f\"CUDA available : {torch.cuda.is_available()}\")\n\nif torch.cuda.is_available():\n    print(f\"Number of GPUs : {torch.cuda.device_count()}\")\n    \n    for i in range(torch.cuda.device_count()):\n        print(f\"GPU {i}: {torch.cuda.get_device_name(i)}\")\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"KAGGLE INPUT FILES\")\nprint(\"=\" * 60)\n\nfor dirname, _, filenames in os.walk(\"/kaggle/input\"):\n    for filename in filenames:\n        path = os.path.join(dirname, filename)\n        size_mb = os.path.getsize(path) / (1024 ** 2)\n        print(f\"{path}  ({size_mb:.2f} MB)\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:25:27.422991Z","iopub.execute_input":"2026-10-05T04:25:27.423466Z","iopub.status.idle":"2026-10-05T04:25:36.175896Z","shell.execute_reply.started":"2026-10-05T04:25:27.423443Z","shell.execute_reply":"2026-10-05T04:25:36.175031Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 2: Extract CIFAR-10 Competition Images\n# ============================================================\n\nimport os\nimport subprocess\n\nDATA_DIR = \"/kaggle/input/competitions/cifar-10\"\nWORK_DIR = \"/kaggle/working\"\n\nTRAIN_ARCHIVE = os.path.join(DATA_DIR, \"train.7z\")\nTEST_ARCHIVE = os.path.join(DATA_DIR, \"test.7z\")\n\nTRAIN_DIR = os.path.join(WORK_DIR, \"train\")\nTEST_DIR = os.path.join(WORK_DIR, \"test\")\n\n# Extract only if the folders do not already exist\nif not os.path.exists(TRAIN_DIR):\n    print(\"Extracting training images...\")\n    subprocess.run(\n        [\"7z\", \"x\", TRAIN_ARCHIVE, f\"-o{WORK_DIR}\", \"-y\"],\n        check=True,\n        stdout=subprocess.DEVNULL\n    )\n    print(\"Training extraction complete.\")\nelse:\n    print(\"Training images already extracted.\")\n\nif not os.path.exists(TEST_DIR):\n    print(\"Extracting test images...\")\n    subprocess.run(\n        [\"7z\", \"x\", TEST_ARCHIVE, f\"-o{WORK_DIR}\", \"-y\"],\n        check=True,\n        stdout=subprocess.DEVNULL\n    )\n    print(\"Test extraction complete.\")\nelse:\n    print(\"Test images already extracted.\")\n\n# Verify image counts\ntrain_files = [\n    f for f in os.listdir(TRAIN_DIR)\n    if f.lower().endswith(\".png\")\n]\n\ntest_files = [\n    f for f in os.listdir(TEST_DIR)\n    if f.lower().endswith(\".png\")\n]\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"DATASET VERIFICATION\")\nprint(\"=\" * 60)\n\nprint(f\"Training images: {len(train_files):,}\")\nprint(f\"Test images    : {len(test_files):,}\")\n\nassert len(train_files) == 50_000, \\\n    f\"Expected 50,000 training images, found {len(train_files)}\"\n\nassert len(test_files) == 300_000, \\\n    f\"Expected 300,000 test images, found {len(test_files)}\"\n\nprint(\"\\nDataset extraction verified successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:26:56.671172Z","iopub.execute_input":"2026-10-05T04:26:56.67193Z","iopub.status.idle":"2026-10-05T04:28:05.342913Z","shell.execute_reply.started":"2026-10-05T04:26:56.671897Z","shell.execute_reply":"2026-10-05T04:28:05.342141Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 3: Load Labels and Create Stratified Train/Val Split\n# ============================================================\n\nfrom sklearn.model_selection import train_test_split\n\nLABELS_PATH = os.path.join(DATA_DIR, \"trainLabels.csv\")\n\n# Load labels\nlabels_df = pd.read_csv(LABELS_PATH)\n\nprint(\"=\" * 60)\nprint(\"LABEL DATA\")\nprint(\"=\" * 60)\n\nprint(f\"Number of labeled images: {len(labels_df):,}\")\nprint(\"\\nFirst 10 rows:\")\ndisplay(labels_df.head(10))\n\n# CIFAR-10 classes\nclasses = sorted(labels_df[\"label\"].unique())\nnum_classes = len(classes)\n\nprint(\"\\nClasses:\")\nfor i, class_name in enumerate(classes):\n    print(f\"{i}: {class_name}\")\n\nprint(f\"\\nNumber of classes: {num_classes}\")\n\n# Check class distribution\nprint(\"\\nClass distribution:\")\nprint(labels_df[\"label\"].value_counts().sort_index())\n\n# Create label <-> integer mappings\nclass_to_idx = {class_name: idx for idx, class_name in enumerate(classes)}\nidx_to_class = {idx: class_name for class_name, idx in class_to_idx.items()}\n\nlabels_df[\"target\"] = labels_df[\"label\"].map(class_to_idx)\n\n# Add exact image paths\nlabels_df[\"path\"] = labels_df[\"id\"].apply(\n    lambda image_id: os.path.join(TRAIN_DIR, f\"{image_id}.png\")\n)\n\n# Verify that every labeled image exists\nassert labels_df[\"path\"].apply(os.path.exists).all(), \\\n    \"Some training images could not be found!\"\n\n# ------------------------------------------------------------\n# Stratified 90/10 split\n# ------------------------------------------------------------\n\ntrain_df, val_df = train_test_split(\n    labels_df,\n    test_size=0.10,\n    random_state=42,\n    stratify=labels_df[\"target\"]\n)\n\ntrain_df = train_df.reset_index(drop=True)\nval_df = val_df.reset_index(drop=True)\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"TRAIN / VALIDATION SPLIT\")\nprint(\"=\" * 60)\n\nprint(f\"Training samples  : {len(train_df):,}\")\nprint(f\"Validation samples: {len(val_df):,}\")\n\nprint(\"\\nTraining class counts:\")\nprint(train_df[\"label\"].value_counts().sort_index())\n\nprint(\"\\nValidation class counts:\")\nprint(val_df[\"label\"].value_counts().sort_index())\n\nassert len(train_df) == 45_000\nassert len(val_df) == 5_000\n\nprint(\"\\nStratified split verified successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:29:19.014107Z","iopub.execute_input":"2026-10-05T04:29:19.014842Z","iopub.status.idle":"2026-10-05T04:29:20.179324Z","shell.execute_reply.started":"2026-10-05T04:29:19.014815Z","shell.execute_reply":"2026-10-05T04:29:20.178465Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 4: Dataset, Data Augmentation, and DataLoaders\n# ============================================================\n\nimport random\nfrom PIL import Image\n\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\n# ------------------------------------------------------------\n# Reproducibility\n# ------------------------------------------------------------\n\nSEED = 42\n\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\n\n# ------------------------------------------------------------\n# CIFAR-10 normalization statistics\n# ------------------------------------------------------------\n\nCIFAR10_MEAN = (0.4914, 0.4822, 0.4465)\nCIFAR10_STD = (0.2470, 0.2435, 0.2616)\n\n# ------------------------------------------------------------\n# Training augmentation\n# ------------------------------------------------------------\n\ntrain_transform = transforms.Compose([\n    transforms.RandomCrop(32, padding=4),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.ToTensor(),\n    transforms.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n])\n\n# Validation receives NO random augmentation\nval_transform = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n])\n\n# ------------------------------------------------------------\n# Custom CIFAR-10 Dataset\n# ------------------------------------------------------------\n\nclass CIFAR10CompetitionDataset(Dataset):\n    def __init__(self, dataframe, transform=None):\n        self.dataframe = dataframe.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.dataframe)\n\n    def __getitem__(self, index):\n        row = self.dataframe.iloc[index]\n\n        image = Image.open(row[\"path\"]).convert(\"RGB\")\n        target = int(row[\"target\"])\n\n        if self.transform is not None:\n            image = self.transform(image)\n\n        return image, target\n\n# ------------------------------------------------------------\n# Create datasets\n# ------------------------------------------------------------\n\ntrain_dataset = CIFAR10CompetitionDataset(\n    train_df,\n    transform=train_transform\n)\n\nval_dataset = CIFAR10CompetitionDataset(\n    val_df,\n    transform=val_transform\n)\n\n# ------------------------------------------------------------\n# DataLoaders\n# ------------------------------------------------------------\n\nBATCH_SIZE = 256\nNUM_WORKERS = 4\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=NUM_WORKERS,\n    pin_memory=True,\n    persistent_workers=True,\n    drop_last=True\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=NUM_WORKERS,\n    pin_memory=True,\n    persistent_workers=True\n)\n\nprint(\"=\" * 60)\nprint(\"DATA PIPELINE\")\nprint(\"=\" * 60)\n\nprint(f\"Training dataset  : {len(train_dataset):,}\")\nprint(f\"Validation dataset: {len(val_dataset):,}\")\nprint(f\"Batch size        : {BATCH_SIZE}\")\nprint(f\"Training batches  : {len(train_loader):,}\")\nprint(f\"Validation batches: {len(val_loader):,}\")\n\n# Test one batch\nimages, targets = next(iter(train_loader))\n\nprint(\"\\nBatch verification:\")\nprint(f\"Image batch shape : {images.shape}\")\nprint(f\"Target batch shape: {targets.shape}\")\nprint(f\"Image dtype       : {images.dtype}\")\nprint(f\"Target dtype      : {targets.dtype}\")\n\nassert images.shape == (BATCH_SIZE, 3, 32, 32)\nassert targets.shape == (BATCH_SIZE,)\n\nprint(\"\\nData pipeline verified successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:30:55.974379Z","iopub.execute_input":"2026-10-05T04:30:55.97523Z","iopub.status.idle":"2026-10-05T04:31:00.871214Z","shell.execute_reply.started":"2026-10-05T04:30:55.975199Z","shell.execute_reply":"2026-10-05T04:31:00.869582Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 5: Custom Residual ConvNet\n# Designed from scratch for CIFAR-10\n# NO pre-trained weights\n# NO off-the-shelf architecture\n# ============================================================\n\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n\n# ------------------------------------------------------------\n# Custom Residual Block\n# ------------------------------------------------------------\n\nclass ResidualBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, stride=1, dropout=0.0):\n        super().__init__()\n\n        self.bn1 = nn.BatchNorm2d(in_channels)\n        self.relu1 = nn.ReLU(inplace=True)\n\n        self.conv1 = nn.Conv2d(\n            in_channels,\n            out_channels,\n            kernel_size=3,\n            stride=stride,\n            padding=1,\n            bias=False\n        )\n\n        self.bn2 = nn.BatchNorm2d(out_channels)\n        self.relu2 = nn.ReLU(inplace=True)\n\n        self.conv2 = nn.Conv2d(\n            out_channels,\n            out_channels,\n            kernel_size=3,\n            stride=1,\n            padding=1,\n            bias=False\n        )\n\n        self.dropout = nn.Dropout2d(p=dropout) if dropout > 0 else nn.Identity()\n\n        # Projection shortcut when dimensions change\n        if stride != 1 or in_channels != out_channels:\n            self.shortcut = nn.Conv2d(\n                in_channels,\n                out_channels,\n                kernel_size=1,\n                stride=stride,\n                bias=False\n            )\n        else:\n            self.shortcut = nn.Identity()\n\n    def forward(self, x):\n        shortcut = self.shortcut(x)\n\n        out = self.bn1(x)\n        out = self.relu1(out)\n        out = self.conv1(out)\n\n        out = self.bn2(out)\n        out = self.relu2(out)\n        out = self.dropout(out)\n        out = self.conv2(out)\n\n        out = out + shortcut\n\n        return out\n\n\n# ------------------------------------------------------------\n# Custom CIFAR Residual Network\n# ------------------------------------------------------------\n\nclass CustomCIFARNet(nn.Module):\n    def __init__(\n        self,\n        num_classes=10,\n        base_width=64,\n        blocks_per_stage=(4, 4, 4),\n        dropout=0.10\n    ):\n        super().__init__()\n\n        self.stem = nn.Conv2d(\n            3,\n            base_width,\n            kernel_size=3,\n            stride=1,\n            padding=1,\n            bias=False\n        )\n\n        # Stage 1: 32x32\n        self.stage1 = self._make_stage(\n            base_width,\n            base_width,\n            blocks_per_stage[0],\n            stride=1,\n            dropout=dropout\n        )\n\n        # Stage 2: 16x16\n        self.stage2 = self._make_stage(\n            base_width,\n            base_width * 2,\n            blocks_per_stage[1],\n            stride=2,\n            dropout=dropout\n        )\n\n        # Stage 3: 8x8\n        self.stage3 = self._make_stage(\n            base_width * 2,\n            base_width * 4,\n            blocks_per_stage[2],\n            stride=2,\n            dropout=dropout\n        )\n\n        self.final_bn = nn.BatchNorm2d(base_width * 4)\n        self.final_relu = nn.ReLU(inplace=True)\n\n        self.pool = nn.AdaptiveAvgPool2d(1)\n\n        self.classifier = nn.Linear(\n            base_width * 4,\n            num_classes\n        )\n\n        # Explicit random initialization\n        self.apply(self._initialize_weights)\n\n    def _make_stage(\n        self,\n        in_channels,\n        out_channels,\n        num_blocks,\n        stride,\n        dropout\n    ):\n        layers = []\n\n        layers.append(\n            ResidualBlock(\n                in_channels,\n                out_channels,\n                stride=stride,\n                dropout=dropout\n            )\n        )\n\n        for _ in range(1, num_blocks):\n            layers.append(\n                ResidualBlock(\n                    out_channels,\n                    out_channels,\n                    stride=1,\n                    dropout=dropout\n                )\n            )\n\n        return nn.Sequential(*layers)\n\n    @staticmethod\n    def _initialize_weights(module):\n        \"\"\"\n        All network weights are initialized randomly.\n        No pretrained parameters are used.\n        \"\"\"\n\n        if isinstance(module, nn.Conv2d):\n            nn.init.kaiming_normal_(\n                module.weight,\n                mode=\"fan_out\",\n                nonlinearity=\"relu\"\n            )\n\n        elif isinstance(module, nn.BatchNorm2d):\n            nn.init.ones_(module.weight)\n            nn.init.zeros_(module.bias)\n\n        elif isinstance(module, nn.Linear):\n            nn.init.normal_(module.weight, mean=0.0, std=0.01)\n\n            if module.bias is not None:\n                nn.init.zeros_(module.bias)\n\n    def forward(self, x):\n        x = self.stem(x)\n\n        x = self.stage1(x)\n        x = self.stage2(x)\n        x = self.stage3(x)\n\n        x = self.final_bn(x)\n        x = self.final_relu(x)\n\n        x = self.pool(x)\n        x = torch.flatten(x, 1)\n\n        x = self.classifier(x)\n\n        return x\n\n\n# ------------------------------------------------------------\n# Instantiate model\n# ------------------------------------------------------------\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nmodel = CustomCIFARNet(\n    num_classes=10,\n    base_width=64,\n    blocks_per_stage=(4, 4, 4),\n    dropout=0.10\n)\n\nmodel = model.to(device)\n\n# ------------------------------------------------------------\n# Verify model\n# ------------------------------------------------------------\n\ntotal_params = sum(\n    p.numel() for p in model.parameters()\n)\n\ntrainable_params = sum(\n    p.numel() for p in model.parameters()\n    if p.requires_grad\n)\n\nprint(\"=\" * 60)\nprint(\"CUSTOM CIFAR-10 RESIDUAL NETWORK\")\nprint(\"=\" * 60)\n\nprint(f\"Device              : {device}\")\nprint(f\"Total parameters    : {total_params:,}\")\nprint(f\"Trainable parameters: {trainable_params:,}\")\n\n# Test forward pass\ndummy_input = torch.randn(\n    8, 3, 32, 32,\n    device=device\n)\n\nwith torch.no_grad():\n    dummy_output = model(dummy_input)\n\nprint(f\"\\nInput shape : {dummy_input.shape}\")\nprint(f\"Output shape: {dummy_output.shape}\")\n\nassert dummy_output.shape == (8, 10)\n\nprint(\"\\nCustom network forward pass verified successfully!\")\nprint(\"All weights were randomly initialized — no pretrained weights used.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:41:39.615645Z","iopub.execute_input":"2026-10-05T04:41:39.616315Z","iopub.status.idle":"2026-10-05T04:41:41.165806Z","shell.execute_reply.started":"2026-10-05T04:41:39.61628Z","shell.execute_reply":"2026-10-05T04:41:41.165028Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 6: CutMix Advanced Data Augmentation\n# ============================================================\n\nimport math\nimport matplotlib.pyplot as plt\n\n\ndef rand_bbox(size, lam):\n    \"\"\"\n    Generate a random CutMix bounding box.\n\n    size: image tensor shape [B, C, H, W]\n    lam : sampled mixing coefficient\n    \"\"\"\n\n    H = size[2]\n    W = size[3]\n\n    cut_ratio = math.sqrt(1.0 - lam)\n\n    cut_w = int(W * cut_ratio)\n    cut_h = int(H * cut_ratio)\n\n    # Random center point\n    cx = np.random.randint(W)\n    cy = np.random.randint(H)\n\n    x1 = np.clip(cx - cut_w // 2, 0, W)\n    x2 = np.clip(cx + cut_w // 2, 0, W)\n\n    y1 = np.clip(cy - cut_h // 2, 0, H)\n    y2 = np.clip(cy + cut_h // 2, 0, H)\n\n    return x1, y1, x2, y2\n\n\ndef apply_cutmix(images, targets, alpha=1.0):\n    \"\"\"\n    Apply CutMix to a batch.\n\n    Returns:\n        mixed_images\n        target_a\n        target_b\n        adjusted_lambda\n    \"\"\"\n\n    if alpha <= 0:\n        return images, targets, targets, 1.0\n\n    # Sample lambda from Beta distribution\n    lam = np.random.beta(alpha, alpha)\n\n    batch_size = images.size(0)\n\n    # Randomly shuffle samples inside the batch\n    indices = torch.randperm(\n        batch_size,\n        device=images.device\n    )\n\n    target_a = targets\n    target_b = targets[indices]\n\n    # Generate CutMix rectangle\n    x1, y1, x2, y2 = rand_bbox(\n        images.size(),\n        lam\n    )\n\n    # Clone so the original batch remains unchanged\n    mixed_images = images.clone()\n\n    # Replace rectangular region\n    mixed_images[:, :, y1:y2, x1:x2] = \\\n        images[indices, :, y1:y2, x1:x2]\n\n    # Recalculate lambda using the actual clipped area\n    box_area = (x2 - x1) * (y2 - y1)\n    image_area = images.size(2) * images.size(3)\n\n    lam_adjusted = 1.0 - (box_area / image_area)\n\n    return mixed_images, target_a, target_b, lam_adjusted\n\n\ndef cutmix_loss(criterion, predictions, target_a, target_b, lam):\n    \"\"\"\n    Weighted CutMix classification loss.\n    \"\"\"\n\n    return (\n        lam * criterion(predictions, target_a)\n        +\n        (1.0 - lam) * criterion(predictions, target_b)\n    )\n\n\n# ------------------------------------------------------------\n# Verify CutMix on a training batch\n# ------------------------------------------------------------\n\nsample_images, sample_targets = next(iter(train_loader))\n\nsample_images = sample_images.to(device)\nsample_targets = sample_targets.to(device)\n\nmixed_images, target_a, target_b, lam = apply_cutmix(\n    sample_images,\n    sample_targets,\n    alpha=1.0\n)\n\nprint(\"=\" * 60)\nprint(\"CUTMIX VERIFICATION\")\nprint(\"=\" * 60)\n\nprint(f\"Original batch shape : {sample_images.shape}\")\nprint(f\"CutMix batch shape   : {mixed_images.shape}\")\nprint(f\"Lambda               : {lam:.4f}\")\n\nprint(\"\\nFirst 8 mixed labels:\")\n\nfor i in range(8):\n    label_a = idx_to_class[target_a[i].item()]\n    label_b = idx_to_class[target_b[i].item()]\n\n    print(\n        f\"Image {i + 1}: \"\n        f\"{lam:.3f} × {label_a} + \"\n        f\"{1-lam:.3f} × {label_b}\"\n    )\n\nassert mixed_images.shape == sample_images.shape\n\nprint(\"\\nCutMix augmentation verified successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:43:07.674481Z","iopub.execute_input":"2026-10-05T04:43:07.674985Z","iopub.status.idle":"2026-10-05T04:43:08.235153Z","shell.execute_reply.started":"2026-10-05T04:43:07.674959Z","shell.execute_reply":"2026-10-05T04:43:08.232668Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 7: Visualize CutMix Examples\n# ============================================================\n\ndef denormalize_image(tensor):\n    \"\"\"\n    Undo CIFAR-10 normalization for visualization.\n    \"\"\"\n\n    mean = torch.tensor(\n        CIFAR10_MEAN,\n        device=tensor.device\n    ).view(3, 1, 1)\n\n    std = torch.tensor(\n        CIFAR10_STD,\n        device=tensor.device\n    ).view(3, 1, 1)\n\n    image = tensor * std + mean\n\n    return image.clamp(0, 1)\n\n\nfig, axes = plt.subplots(2, 4, figsize=(12, 6))\n\nfor i, ax in enumerate(axes.flat):\n\n    image = denormalize_image(\n        mixed_images[i]\n    )\n\n    image = (\n        image\n        .detach()\n        .cpu()\n        .permute(1, 2, 0)\n        .numpy()\n    )\n\n    ax.imshow(image)\n\n    label_a = idx_to_class[target_a[i].item()]\n    label_b = idx_to_class[target_b[i].item()]\n\n    ax.set_title(\n        f\"{lam:.2f} {label_a}\\n\"\n        f\"+ {1-lam:.2f} {label_b}\",\n        fontsize=9\n    )\n\n    ax.axis(\"off\")\n\nplt.suptitle(\n    \"CutMix Advanced Data Augmentation\",\n    fontsize=14,\n    fontweight=\"bold\"\n)\n\nplt.tight_layout()\n\n# Save for the final report\nplt.savefig(\n    \"/kaggle/working/cutmix_examples.png\",\n    dpi=200,\n    bbox_inches=\"tight\"\n)\n\nplt.show()\n\nprint(\"CutMix visualization saved as:\")\nprint(\"/kaggle/working/cutmix_examples.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:43:36.501649Z","iopub.execute_input":"2026-10-05T04:43:36.502192Z","iopub.status.idle":"2026-10-05T04:43:37.683254Z","shell.execute_reply.started":"2026-10-05T04:43:36.502167Z","shell.execute_reply":"2026-10-05T04:43:37.68253Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 8: AdamW Optimizer + Linear Warmup + Cosine Annealing\n# ============================================================\n\nimport math\nfrom torch.optim import AdamW\nfrom torch.optim.lr_scheduler import LambdaLR\n\n# ------------------------------------------------------------\n# Training configuration\n# ------------------------------------------------------------\n\nEPOCHS = 180\nWARMUP_EPOCHS = 5\n\nBASE_LR = 3e-4\nMIN_LR_RATIO = 0.01\nWEIGHT_DECAY = 5e-4\n\n# ------------------------------------------------------------\n# AdamW\n# ------------------------------------------------------------\n\noptimizer = AdamW(\n    model.parameters(),\n    lr=BASE_LR,\n    weight_decay=WEIGHT_DECAY,\n    betas=(0.9, 0.999)\n)\n\n# ------------------------------------------------------------\n# Linear Warmup + Cosine Annealing\n# ------------------------------------------------------------\n\ndef lr_lambda(epoch):\n    \"\"\"\n    Linear warmup for WARMUP_EPOCHS, followed by\n    cosine annealing for the remaining epochs.\n    \"\"\"\n\n    # Linear warmup\n    if epoch < WARMUP_EPOCHS:\n        return float(epoch + 1) / float(WARMUP_EPOCHS)\n\n    # Cosine annealing\n    progress = (\n        (epoch - WARMUP_EPOCHS)\n        /\n        max(1, EPOCHS - WARMUP_EPOCHS - 1)\n    )\n\n    cosine_factor = 0.5 * (\n        1.0 + math.cos(math.pi * progress)\n    )\n\n    return (\n        MIN_LR_RATIO\n        +\n        (1.0 - MIN_LR_RATIO) * cosine_factor\n    )\n\n\nscheduler = LambdaLR(\n    optimizer,\n    lr_lambda=lr_lambda\n)\n\n# ------------------------------------------------------------\n# Print configuration\n# ------------------------------------------------------------\n\nprint(\"=\" * 60)\nprint(\"TRAINING CONFIGURATION\")\nprint(\"=\" * 60)\n\nprint(\"Optimizer            : AdamW\")\nprint(f\"Base learning rate   : {BASE_LR}\")\nprint(f\"Weight decay         : {WEIGHT_DECAY}\")\nprint(f\"Total epochs         : {EPOCHS}\")\nprint(f\"Warmup epochs        : {WARMUP_EPOCHS}\")\nprint(\"LR schedule          : Linear Warmup + Cosine Annealing\")\nprint(f\"Minimum LR ratio     : {MIN_LR_RATIO}\")\nprint(\"Advanced augmentation: CutMix\")\nprint(\"Pretrained weights   : NONE\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:45:09.194129Z","iopub.execute_input":"2026-10-05T04:45:09.195017Z","iopub.status.idle":"2026-10-05T04:45:09.204536Z","shell.execute_reply.started":"2026-10-05T04:45:09.194987Z","shell.execute_reply":"2026-10-05T04:45:09.203759Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 9: Visualize Warmup + Cosine Annealing Schedule\n# ============================================================\n\nschedule_epochs = np.arange(EPOCHS)\n\nschedule_lrs = [\n    BASE_LR * lr_lambda(epoch)\n    for epoch in schedule_epochs\n]\n\nplt.figure(figsize=(10, 5))\n\nplt.plot(\n    schedule_epochs + 1,\n    schedule_lrs,\n    linewidth=2\n)\n\nplt.axvline(\n    WARMUP_EPOCHS,\n    linestyle=\"--\",\n    label=f\"End of warmup (epoch {WARMUP_EPOCHS})\"\n)\n\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Learning Rate\")\n\nplt.title(\n    \"Linear Warmup + Cosine Annealing Learning Rate Schedule\"\n)\n\nplt.legend()\nplt.grid(alpha=0.3)\n\nplt.tight_layout()\n\nplt.savefig(\n    \"/kaggle/working/lr_schedule.png\",\n    dpi=200,\n    bbox_inches=\"tight\"\n)\n\nplt.show()\n\nprint(\"First 10 learning rates:\")\n\nfor epoch in range(10):\n    print(\n        f\"Epoch {epoch + 1:3d}: \"\n        f\"{schedule_lrs[epoch]:.8f}\"\n    )\n\nprint(\"\\nFinal learning rate:\")\nprint(\n    f\"Epoch {EPOCHS}: \"\n    f\"{schedule_lrs[-1]:.8f}\"\n)\n\nprint(\"\\nLearning-rate schedule saved as:\")\nprint(\"/kaggle/working/lr_schedule.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:45:25.943548Z","iopub.execute_input":"2026-10-05T04:45:25.943839Z","iopub.status.idle":"2026-10-05T04:45:26.403015Z","shell.execute_reply.started":"2026-10-05T04:45:25.943817Z","shell.execute_reply":"2026-10-05T04:45:26.402253Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 10: Training and Validation System\n# ============================================================\n\nimport time\nfrom tqdm.auto import tqdm\n\n# ------------------------------------------------------------\n# Multi-GPU\n# ------------------------------------------------------------\n\nif torch.cuda.device_count() > 1:\n    print(f\"Using {torch.cuda.device_count()} GPUs with DataParallel.\")\n    model = nn.DataParallel(model)\nelse:\n    print(\"Using a single GPU.\")\n\nmodel = model.to(device)\n\n# ------------------------------------------------------------\n# Loss\n# ------------------------------------------------------------\n\n# Label smoothing adds additional regularization.\ncriterion = nn.CrossEntropyLoss(\n    label_smoothing=0.1\n)\n\n# ------------------------------------------------------------\n# Mixed precision\n# ------------------------------------------------------------\n\nscaler = torch.amp.GradScaler(\n    \"cuda\",\n    enabled=torch.cuda.is_available()\n)\n\n# ------------------------------------------------------------\n# Training configuration\n# ------------------------------------------------------------\n\nCUTMIX_PROB = 0.5\nCUTMIX_ALPHA = 1.0\nMAX_GRAD_NORM = 5.0\n\nBEST_MODEL_PATH = \"/kaggle/working/best_custom_cifar_net.pth\"\n\n# ------------------------------------------------------------\n# Training function\n# ------------------------------------------------------------\n\ndef train_one_epoch(\n    model,\n    loader,\n    optimizer,\n    criterion,\n    scaler,\n    device\n):\n    model.train()\n\n    running_loss = 0.0\n    running_correct = 0.0\n    total_samples = 0\n\n    progress_bar = tqdm(\n        loader,\n        desc=\"Training\",\n        leave=False\n    )\n\n    for images, targets in progress_bar:\n\n        images = images.to(\n            device,\n            non_blocking=True\n        )\n\n        targets = targets.to(\n            device,\n            non_blocking=True\n        )\n\n        optimizer.zero_grad(\n            set_to_none=True\n        )\n\n        # Decide whether to apply CutMix\n        use_cutmix = (\n            np.random.rand() < CUTMIX_PROB\n        )\n\n        if use_cutmix:\n\n            inputs, target_a, target_b, lam = apply_cutmix(\n                images,\n                targets,\n                alpha=CUTMIX_ALPHA\n            )\n\n        else:\n\n            inputs = images\n            target_a = targets\n            target_b = targets\n            lam = 1.0\n\n        # Mixed-precision forward pass\n        with torch.amp.autocast(\n            device_type=\"cuda\",\n            dtype=torch.float16,\n            enabled=torch.cuda.is_available()\n        ):\n\n            outputs = model(inputs)\n\n            if use_cutmix:\n                loss = cutmix_loss(\n                    criterion,\n                    outputs,\n                    target_a,\n                    target_b,\n                    lam\n                )\n            else:\n                loss = criterion(\n                    outputs,\n                    targets\n                )\n\n        # Backpropagation\n        scaler.scale(loss).backward()\n\n        # Unscale before gradient clipping\n        scaler.unscale_(optimizer)\n\n        torch.nn.utils.clip_grad_norm_(\n            model.parameters(),\n            MAX_GRAD_NORM\n        )\n\n        scaler.step(optimizer)\n        scaler.update()\n\n        # ----------------------------------------------------\n        # Statistics\n        # ----------------------------------------------------\n\n        batch_size = targets.size(0)\n\n        running_loss += (\n            loss.item() * batch_size\n        )\n\n        predictions = outputs.argmax(dim=1)\n\n        if use_cutmix:\n\n            correct_a = (\n                predictions == target_a\n            ).sum().item()\n\n            correct_b = (\n                predictions == target_b\n            ).sum().item()\n\n            running_correct += (\n                lam * correct_a\n                +\n                (1.0 - lam) * correct_b\n            )\n\n        else:\n\n            running_correct += (\n                predictions == targets\n            ).sum().item()\n\n        total_samples += batch_size\n\n        progress_bar.set_postfix(\n            loss=f\"{running_loss / total_samples:.4f}\",\n            acc=f\"{100 * running_correct / total_samples:.2f}%\"\n        )\n\n    epoch_loss = (\n        running_loss / total_samples\n    )\n\n    epoch_accuracy = (\n        100.0 * running_correct / total_samples\n    )\n\n    return epoch_loss, epoch_accuracy\n\n\n# ------------------------------------------------------------\n# Validation function\n# ------------------------------------------------------------\n\n@torch.no_grad()\ndef validate(\n    model,\n    loader,\n    criterion,\n    device\n):\n    model.eval()\n\n    running_loss = 0.0\n    running_correct = 0\n    total_samples = 0\n\n    for images, targets in loader:\n\n        images = images.to(\n            device,\n            non_blocking=True\n        )\n\n        targets = targets.to(\n            device,\n            non_blocking=True\n        )\n\n        with torch.amp.autocast(\n            device_type=\"cuda\",\n            dtype=torch.float16,\n            enabled=torch.cuda.is_available()\n        ):\n\n            outputs = model(images)\n\n            loss = criterion(\n                outputs,\n                targets\n            )\n\n        predictions = outputs.argmax(dim=1)\n\n        batch_size = targets.size(0)\n\n        running_loss += (\n            loss.item() * batch_size\n        )\n\n        running_correct += (\n            predictions == targets\n        ).sum().item()\n\n        total_samples += batch_size\n\n    epoch_loss = (\n        running_loss / total_samples\n    )\n\n    epoch_accuracy = (\n        100.0 * running_correct / total_samples\n    )\n\n    return epoch_loss, epoch_accuracy\n\n\nprint(\"=\" * 60)\nprint(\"TRAINING SYSTEM\")\nprint(\"=\" * 60)\n\nprint(f\"GPUs available       : {torch.cuda.device_count()}\")\nprint(f\"CutMix probability   : {CUTMIX_PROB}\")\nprint(f\"CutMix alpha         : {CUTMIX_ALPHA}\")\nprint(\"Label smoothing      : 0.1\")\nprint(\"Mixed precision      : Enabled\")\nprint(f\"Gradient clipping    : {MAX_GRAD_NORM}\")\nprint(f\"Best checkpoint path : {BEST_MODEL_PATH}\")\n\nprint(\"\\nTraining and validation functions created successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:46:42.630229Z","iopub.execute_input":"2026-10-05T04:46:42.630756Z","iopub.status.idle":"2026-10-05T04:46:42.728905Z","shell.execute_reply.started":"2026-10-05T04:46:42.630715Z","shell.execute_reply":"2026-10-05T04:46:42.72805Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 11: One-Batch Training Sanity Check\n# ============================================================\n\nmodel.train()\n\ntest_images, test_targets = next(iter(train_loader))\n\ntest_images = test_images.to(\n    device,\n    non_blocking=True\n)\n\ntest_targets = test_targets.to(\n    device,\n    non_blocking=True\n)\n\noptimizer.zero_grad(set_to_none=True)\n\ntest_inputs, test_a, test_b, test_lam = apply_cutmix(\n    test_images,\n    test_targets,\n    alpha=CUTMIX_ALPHA\n)\n\nwith torch.amp.autocast(\n    device_type=\"cuda\",\n    dtype=torch.float16,\n    enabled=torch.cuda.is_available()\n):\n\n    test_outputs = model(test_inputs)\n\n    test_loss = cutmix_loss(\n        criterion,\n        test_outputs,\n        test_a,\n        test_b,\n        test_lam\n    )\n\nscaler.scale(test_loss).backward()\n\nscaler.unscale_(optimizer)\n\ntorch.nn.utils.clip_grad_norm_(\n    model.parameters(),\n    MAX_GRAD_NORM\n)\n\nscaler.step(optimizer)\nscaler.update()\n\nprint(\"=\" * 60)\nprint(\"ONE-BATCH SANITY CHECK\")\nprint(\"=\" * 60)\n\nprint(f\"Input shape : {test_inputs.shape}\")\nprint(f\"Output shape: {test_outputs.shape}\")\nprint(f\"Loss        : {test_loss.item():.4f}\")\nprint(f\"CutMix λ    : {test_lam:.4f}\")\n\nassert torch.isfinite(test_loss), \\\n    \"Loss is NaN or infinite!\"\n\nassert test_outputs.shape == (\n    BATCH_SIZE,\n    10\n)\n\nprint(\"\\nForward pass     : PASSED\")\nprint(\"Loss computation : PASSED\")\nprint(\"Backward pass    : PASSED\")\nprint(\"Optimizer step   : PASSED\")\nprint(\"Mixed precision  : PASSED\")\nprint(\"\\nTraining sanity check successful!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:47:04.833825Z","iopub.execute_input":"2026-10-05T04:47:04.83438Z","iopub.status.idle":"2026-10-05T04:47:07.834638Z","shell.execute_reply.started":"2026-10-05T04:47:04.834352Z","shell.execute_reply":"2026-10-05T04:47:07.833918Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 12: Clean Reset Before Full Training\n# ============================================================\n\n# Reset random seeds\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\n\n# Delete sanity-check model/optimizer state\ndel model\ntorch.cuda.empty_cache()\n\n# ------------------------------------------------------------\n# Recreate model FROM SCRATCH\n# ------------------------------------------------------------\n\nmodel = CustomCIFARNet(\n    num_classes=10,\n    base_width=64,\n    blocks_per_stage=(4, 4, 4),\n    dropout=0.10\n)\n\nmodel = model.to(device)\n\n# Use both T4 GPUs\nif torch.cuda.device_count() > 1:\n    model = nn.DataParallel(model)\n\nmodel = model.to(device)\n\n# ------------------------------------------------------------\n# Fresh AdamW optimizer\n# ------------------------------------------------------------\n\noptimizer = AdamW(\n    model.parameters(),\n    lr=BASE_LR,\n    weight_decay=WEIGHT_DECAY,\n    betas=(0.9, 0.999)\n)\n\n# Fresh scheduler\nscheduler = LambdaLR(\n    optimizer,\n    lr_lambda=lr_lambda\n)\n\n# Fresh mixed-precision scaler\nscaler = torch.amp.GradScaler(\n    \"cuda\",\n    enabled=torch.cuda.is_available()\n)\n\n# Fresh loss\ncriterion = nn.CrossEntropyLoss(\n    label_smoothing=0.1\n)\n\nprint(\"=\" * 60)\nprint(\"CLEAN TRAINING RESET\")\nprint(\"=\" * 60)\n\nprint(\"Model              : recreated from scratch\")\nprint(\"Weights            : randomly initialized\")\nprint(\"Pretrained weights : NONE\")\nprint(\"Optimizer          : fresh AdamW\")\nprint(\"Scheduler          : fresh Warmup + Cosine\")\nprint(\"AMP scaler         : reset\")\nprint(f\"GPUs               : {torch.cuda.device_count()}\")\n\nprint(\"\\nReady for the real training run.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:48:39.796102Z","iopub.execute_input":"2026-10-05T04:48:39.796725Z","iopub.status.idle":"2026-10-05T04:48:39.968748Z","shell.execute_reply.started":"2026-10-05T04:48:39.796699Z","shell.execute_reply":"2026-10-05T04:48:39.967938Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 13: Full Training Run\n# 180 epochs - AdamW - Warmup + Cosine - CutMix\n# ============================================================\n\nhistory = {\n    \"train_loss\": [],\n    \"train_acc\": [],\n    \"val_loss\": [],\n    \"val_acc\": [],\n    \"lr\": []\n}\n\nbest_val_acc = 0.0\nbest_epoch = 0\n\ntraining_start_time = time.time()\n\nprint(\"=\" * 72)\nprint(\"STARTING FULL CIFAR-10 TRAINING\")\nprint(\"=\" * 72)\n\nprint(f\"Epochs          : {EPOCHS}\")\nprint(f\"Batch size      : {BATCH_SIZE}\")\nprint(\"Optimizer       : AdamW\")\nprint(\"LR schedule     : Linear Warmup + Cosine Annealing\")\nprint(\"Augmentation    : RandomCrop + HorizontalFlip + CutMix\")\nprint(\"Initialization  : Random\")\nprint(\"Pretrained      : NO\")\nprint(\"=\" * 72)\n\nfor epoch in range(EPOCHS):\n\n    epoch_start_time = time.time()\n\n    current_lr = optimizer.param_groups[0][\"lr\"]\n\n    # --------------------------------------------------------\n    # Train\n    # --------------------------------------------------------\n\n    train_loss, train_acc = train_one_epoch(\n        model=model,\n        loader=train_loader,\n        optimizer=optimizer,\n        criterion=criterion,\n        scaler=scaler,\n        device=device\n    )\n\n    # --------------------------------------------------------\n    # Validate\n    # --------------------------------------------------------\n\n    val_loss, val_acc = validate(\n        model=model,\n        loader=val_loader,\n        criterion=criterion,\n        device=device\n    )\n\n    # --------------------------------------------------------\n    # Store history\n    # --------------------------------------------------------\n\n    history[\"train_loss\"].append(train_loss)\n    history[\"train_acc\"].append(train_acc)\n    history[\"val_loss\"].append(val_loss)\n    history[\"val_acc\"].append(val_acc)\n    history[\"lr\"].append(current_lr)\n\n    # --------------------------------------------------------\n    # Save best checkpoint\n    # --------------------------------------------------------\n\n    improved = val_acc > best_val_acc\n\n    if improved:\n\n        best_val_acc = val_acc\n        best_epoch = epoch + 1\n\n        # DataParallel adds \"module.\", so save underlying model\n        model_to_save = (\n            model.module\n            if isinstance(model, nn.DataParallel)\n            else model\n        )\n\n        torch.save(\n            {\n                \"epoch\": epoch + 1,\n                \"model_state_dict\": model_to_save.state_dict(),\n                \"optimizer_state_dict\": optimizer.state_dict(),\n                \"val_accuracy\": val_acc,\n                \"val_loss\": val_loss,\n                \"class_to_idx\": class_to_idx\n            },\n            BEST_MODEL_PATH\n        )\n\n    # --------------------------------------------------------\n    # Update learning rate\n    # --------------------------------------------------------\n\n    scheduler.step()\n\n    epoch_time = time.time() - epoch_start_time\n\n    marker = \"  <-- BEST\" if improved else \"\"\n\n    print(\n        f\"Epoch {epoch + 1:03d}/{EPOCHS} | \"\n        f\"LR {current_lr:.7f} | \"\n        f\"Train Loss {train_loss:.4f} | \"\n        f\"Train Acc {train_acc:6.2f}% | \"\n        f\"Val Loss {val_loss:.4f} | \"\n        f\"Val Acc {val_acc:6.2f}% | \"\n        f\"{epoch_time:.1f}s\"\n        f\"{marker}\"\n    )\n\n    # Periodic backup of history\n    if (epoch + 1) % 10 == 0:\n\n        history_df_temp = pd.DataFrame(history)\n\n        history_df_temp.to_csv(\n            \"/kaggle/working/training_history.csv\",\n            index=False\n        )\n\n# ------------------------------------------------------------\n# Finished\n# ------------------------------------------------------------\n\ntotal_training_time = (\n    time.time() - training_start_time\n)\n\nhistory_df = pd.DataFrame(history)\n\nhistory_df.to_csv(\n    \"/kaggle/working/training_history.csv\",\n    index=False\n)\n\nprint(\"\\n\" + \"=\" * 72)\nprint(\"TRAINING COMPLETE\")\nprint(\"=\" * 72)\n\nprint(\n    f\"Total training time : \"\n    f\"{total_training_time / 60:.2f} minutes\"\n)\n\nprint(\n    f\"Best epoch          : \"\n    f\"{best_epoch}\"\n)\n\nprint(\n    f\"Best validation acc : \"\n    f\"{best_val_acc:.2f}%\"\n)\n\nprint(\n    f\"Best model saved to : \"\n    f\"{BEST_MODEL_PATH}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T04:49:18.340431Z","iopub.execute_input":"2026-10-05T04:49:18.341006Z","iopub.status.idle":"2026-10-05T06:04:17.002031Z","shell.execute_reply.started":"2026-10-05T04:49:18.34098Z","shell.execute_reply":"2026-10-05T06:04:17.001022Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 15: Load and Verify Best Model\n# ============================================================\n\n# Load the checkpoint containing the best validation model\ncheckpoint = torch.load(\n    BEST_MODEL_PATH,\n    map_location=device,\n    weights_only=False\n)\n\n# ------------------------------------------------------------\n# Recreate the exact same custom architecture\n# ------------------------------------------------------------\n\nbest_model = CustomCIFARNet(\n    num_classes=10,\n    base_width=64,\n    blocks_per_stage=(4, 4, 4),\n    dropout=0.10\n)\n\n# Load the weights from the best checkpoint\nbest_model.load_state_dict(\n    checkpoint[\"model_state_dict\"]\n)\n\nbest_model = best_model.to(device)\nbest_model.eval()\n\n# ------------------------------------------------------------\n# Display saved checkpoint information\n# ------------------------------------------------------------\n\nprint(\"=\" * 60)\nprint(\"BEST MODEL LOADED\")\nprint(\"=\" * 60)\n\nprint(f\"Checkpoint epoch     : {checkpoint['epoch']}\")\nprint(f\"Checkpoint val acc   : {checkpoint['val_accuracy']:.2f}%\")\nprint(f\"Checkpoint val loss  : {checkpoint['val_loss']:.4f}\")\n\n# ------------------------------------------------------------\n# Independently evaluate the loaded checkpoint\n# ------------------------------------------------------------\n\nverified_loss, verified_acc = validate(\n    best_model,\n    val_loader,\n    criterion,\n    device\n)\n\nprint(\"\\nIndependent verification:\")\nprint(f\"Validation loss      : {verified_loss:.4f}\")\nprint(f\"Validation accuracy  : {verified_acc:.2f}%\")\n\n# ------------------------------------------------------------\n# Verify that the loaded model reproduces the saved result\n#\n# A tiny difference can occur because validation uses\n# GPU mixed-precision (FP16) inference.\n# ------------------------------------------------------------\n\naccuracy_difference = abs(\n    verified_acc - checkpoint[\"val_accuracy\"]\n)\n\nprint(\n    f\"Accuracy difference  : \"\n    f\"{accuracy_difference:.4f} percentage points\"\n)\n\nassert accuracy_difference < 0.10, (\n    \"Loaded checkpoint differs unexpectedly from \"\n    \"the saved validation result.\"\n)\n\nprint(\"\\nBest checkpoint verified successfully!\")\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"FINAL BEST MODEL\")\nprint(\"=\" * 60)\n\nprint(f\"Best epoch           : {checkpoint['epoch']}\")\nprint(f\"Saved validation acc : {checkpoint['val_accuracy']:.2f}%\")\nprint(f\"Verified val acc     : {verified_acc:.2f}%\")\nprint(\"Pretrained weights   : NONE\")\nprint(\"Status               : READY FOR EVALUATION\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T06:06:43.306465Z","iopub.execute_input":"2026-10-05T06:06:43.30715Z","iopub.status.idle":"2026-10-05T06:06:44.823056Z","shell.execute_reply.started":"2026-10-05T06:06:43.307123Z","shell.execute_reply":"2026-10-05T06:06:44.822358Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 16: Training Curves\n# ============================================================\n\nhistory_df = pd.read_csv(\n    \"/kaggle/working/training_history.csv\"\n)\n\nepochs_axis = np.arange(\n    1,\n    len(history_df) + 1\n)\n\n# ------------------------------------------------------------\n# Accuracy\n# ------------------------------------------------------------\n\nplt.figure(figsize=(10, 5))\n\nplt.plot(\n    epochs_axis,\n    history_df[\"train_acc\"],\n    label=\"Training Accuracy\"\n)\n\nplt.plot(\n    epochs_axis,\n    history_df[\"val_acc\"],\n    label=\"Validation Accuracy\"\n)\n\nplt.axhline(\n    94.2,\n    linestyle=\"--\",\n    label=\"Full-score threshold (94.2%)\"\n)\n\nplt.scatter(\n    best_epoch,\n    best_val_acc,\n    s=80,\n    zorder=5,\n    label=f\"Best: {best_val_acc:.2f}%\"\n)\n\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Accuracy (%)\")\n\nplt.title(\n    \"Custom CIFAR-10 ConvNet — Training and Validation Accuracy\"\n)\n\nplt.legend()\nplt.grid(alpha=0.3)\nplt.tight_layout()\n\nplt.savefig(\n    \"/kaggle/working/accuracy_curve.png\",\n    dpi=200,\n    bbox_inches=\"tight\"\n)\n\nplt.show()\n\n\n# ------------------------------------------------------------\n# Loss\n# ------------------------------------------------------------\n\nplt.figure(figsize=(10, 5))\n\nplt.plot(\n    epochs_axis,\n    history_df[\"train_loss\"],\n    label=\"Training Loss\"\n)\n\nplt.plot(\n    epochs_axis,\n    history_df[\"val_loss\"],\n    label=\"Validation Loss\"\n)\n\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Cross-Entropy Loss\")\n\nplt.title(\n    \"Custom CIFAR-10 ConvNet — Training and Validation Loss\"\n)\n\nplt.legend()\nplt.grid(alpha=0.3)\nplt.tight_layout()\n\nplt.savefig(\n    \"/kaggle/working/loss_curve.png\",\n    dpi=200,\n    bbox_inches=\"tight\"\n)\n\nplt.show()\n\nprint(\"=\" * 60)\nprint(\"FINAL TRAINING SUMMARY\")\nprint(\"=\" * 60)\n\nprint(f\"Best epoch               : {best_epoch}\")\nprint(f\"Best validation accuracy : {best_val_acc:.2f}%\")\nprint(f\"Full-score threshold     : 94.20%\")\nprint(\n    f\"Margin above threshold   : \"\n    f\"{best_val_acc - 94.2:+.2f} percentage points\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T06:07:19.22878Z","iopub.execute_input":"2026-10-05T06:07:19.229045Z","iopub.status.idle":"2026-10-05T06:07:20.285132Z","shell.execute_reply.started":"2026-10-05T06:07:19.229025Z","shell.execute_reply":"2026-10-05T06:07:20.284551Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 17: Kaggle Test Dataset and DataLoader\n# ============================================================\n\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\n\n\nclass CIFAR10TestDataset(Dataset):\n    def __init__(self, test_dir, transform=None):\n        self.test_dir = test_dir\n        self.transform = transform\n\n        # IMPORTANT:\n        # Sort numerically: 1.png, 2.png, 3.png, ...\n        self.image_ids = sorted(\n            [\n                int(os.path.splitext(filename)[0])\n                for filename in os.listdir(test_dir)\n                if filename.lower().endswith(\".png\")\n            ]\n        )\n\n    def __len__(self):\n        return len(self.image_ids)\n\n    def __getitem__(self, index):\n\n        image_id = self.image_ids[index]\n\n        image_path = os.path.join(\n            self.test_dir,\n            f\"{image_id}.png\"\n        )\n\n        image = Image.open(image_path).convert(\"RGB\")\n\n        if self.transform is not None:\n            image = self.transform(image)\n\n        return image, image_id\n\n\n# ------------------------------------------------------------\n# Test transform\n# NO random augmentation during inference\n# ------------------------------------------------------------\n\ntest_transform = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(\n        CIFAR10_MEAN,\n        CIFAR10_STD\n    )\n])\n\n\n# ------------------------------------------------------------\n# Dataset\n# ------------------------------------------------------------\n\ntest_dataset = CIFAR10TestDataset(\n    TEST_DIR,\n    transform=test_transform\n)\n\n\n# ------------------------------------------------------------\n# DataLoader\n# ------------------------------------------------------------\n\nTEST_BATCH_SIZE = 512\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=TEST_BATCH_SIZE,\n    shuffle=False,\n    num_workers=NUM_WORKERS,\n    pin_memory=True,\n    persistent_workers=True\n)\n\n\n# ------------------------------------------------------------\n# Verification\n# ------------------------------------------------------------\n\nprint(\"=\" * 60)\nprint(\"KAGGLE TEST DATASET\")\nprint(\"=\" * 60)\n\nprint(f\"Test images       : {len(test_dataset):,}\")\nprint(f\"Test batch size   : {TEST_BATCH_SIZE}\")\nprint(f\"Number of batches : {len(test_loader):,}\")\n\nprint(f\"\\nFirst image ID    : {test_dataset.image_ids[0]}\")\nprint(f\"Last image ID     : {test_dataset.image_ids[-1]}\")\n\nassert len(test_dataset) == 300_000\nassert test_dataset.image_ids[0] == 1\nassert test_dataset.image_ids[-1] == 300_000\n\ntest_images_batch, test_ids_batch = next(iter(test_loader))\n\nprint(f\"\\nBatch image shape : {test_images_batch.shape}\")\nprint(f\"Batch ID shape    : {test_ids_batch.shape}\")\n\nassert test_images_batch.shape[1:] == (3, 32, 32)\n\nprint(\"\\nKaggle test dataset verified successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T06:10:29.247988Z","iopub.execute_input":"2026-10-05T06:10:29.248466Z","iopub.status.idle":"2026-10-05T06:10:31.458626Z","shell.execute_reply.started":"2026-10-05T06:10:29.248435Z","shell.execute_reply":"2026-10-05T06:10:31.457379Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 18: Test-Time Augmentation (TTA) Inference\n# Original + Horizontal Flip\n# ============================================================\n\nbest_model.eval()\n\nall_ids = []\nall_predictions = []\nall_confidences = []\n\nprint(\"=\" * 60)\nprint(\"STARTING KAGGLE TEST INFERENCE\")\nprint(\"=\" * 60)\n\nprint(\"Model : Best checkpoint from epoch 165\")\nprint(\"TTA   : Original + Horizontal Flip\")\nprint(f\"Images: {len(test_dataset):,}\")\nprint()\n\n\nwith torch.no_grad():\n\n    for images, image_ids in tqdm(\n        test_loader,\n        desc=\"TTA inference\"\n    ):\n\n        images = images.to(\n            device,\n            non_blocking=True\n        )\n\n        # ----------------------------------------------------\n        # Prediction 1: original image\n        # ----------------------------------------------------\n\n        with torch.amp.autocast(\n            device_type=\"cuda\",\n            dtype=torch.float16,\n            enabled=torch.cuda.is_available()\n        ):\n\n            logits_original = best_model(images)\n\n            # ------------------------------------------------\n            # Prediction 2: horizontally flipped image\n            # ------------------------------------------------\n\n            flipped_images = torch.flip(\n                images,\n                dims=[3]\n            )\n\n            logits_flipped = best_model(\n                flipped_images\n            )\n\n        # ----------------------------------------------------\n        # Average probabilities\n        # ----------------------------------------------------\n\n        probs_original = torch.softmax(\n            logits_original.float(),\n            dim=1\n        )\n\n        probs_flipped = torch.softmax(\n            logits_flipped.float(),\n            dim=1\n        )\n\n        probs_tta = (\n            probs_original + probs_flipped\n        ) / 2.0\n\n        confidences, predictions = torch.max(\n            probs_tta,\n            dim=1\n        )\n\n        all_ids.extend(\n            image_ids.cpu().numpy().tolist()\n        )\n\n        all_predictions.extend(\n            predictions.cpu().numpy().tolist()\n        )\n\n        all_confidences.extend(\n            confidences.cpu().numpy().tolist()\n        )\n\n\nprint(\"\\nInference complete!\")\n\nprint(f\"Predictions generated: {len(all_predictions):,}\")\n\nassert len(all_predictions) == 300_000\nassert len(all_ids) == 300_000\n\nprint(\n    f\"Mean TTA confidence : \"\n    f\"{np.mean(all_confidences):.4f}\"\n)\n\nprint(\"\\nTTA inference verified successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T06:10:46.198391Z","iopub.execute_input":"2026-10-05T06:10:46.198931Z","iopub.status.idle":"2026-10-05T06:12:37.941094Z","shell.execute_reply.started":"2026-10-05T06:10:46.198899Z","shell.execute_reply":"2026-10-05T06:12:37.940463Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Step 19: Generate Kaggle Submission\n# ============================================================\n\n# Convert predicted class indices back to class names\npredicted_labels = [\n    idx_to_class[pred]\n    for pred in all_predictions\n]\n\nsubmission = pd.DataFrame({\n    \"id\": all_ids,\n    \"label\": predicted_labels\n})\n\n# Ensure numeric ordering\nsubmission = submission.sort_values(\n    \"id\"\n).reset_index(drop=True)\n\nSUBMISSION_PATH = (\n    \"/kaggle/working/submission.csv\"\n)\n\nsubmission.to_csv(\n    SUBMISSION_PATH,\n    index=False\n)\n\nprint(\"=\" * 60)\nprint(\"KAGGLE SUBMISSION\")\nprint(\"=\" * 60)\n\nprint(f\"Rows        : {len(submission):,}\")\nprint(f\"Columns     : {list(submission.columns)}\")\nprint(f\"First ID    : {submission.iloc[0]['id']}\")\nprint(f\"Last ID     : {submission.iloc[-1]['id']}\")\n\nprint(\"\\nFirst 10 predictions:\")\ndisplay(submission.head(10))\n\nprint(\"\\nPredicted class distribution:\")\nprint(\n    submission[\"label\"]\n    .value_counts()\n    .sort_index()\n)\n\n# ------------------------------------------------------------\n# Final integrity checks\n# ------------------------------------------------------------\n\nassert len(submission) == 300_000\nassert list(submission.columns) == [\"id\", \"label\"]\nassert submission[\"id\"].iloc[0] == 1\nassert submission[\"id\"].iloc[-1] == 300_000\nassert submission[\"id\"].is_unique\nassert submission[\"label\"].isin(classes).all()\n\n# Compare format with Kaggle's sample submission\nsample_submission = pd.read_csv(\n    os.path.join(\n        DATA_DIR,\n        \"sampleSubmission.csv\"\n    )\n)\n\nassert list(submission.columns) == \\\n       list(sample_submission.columns)\n\nassert len(submission) == \\\n       len(sample_submission)\n\nprint(\"\\nAll submission integrity checks PASSED.\")\n\nprint(\n    f\"\\nSubmission saved to:\\n\"\n    f\"{SUBMISSION_PATH}\"\n)\n\nprint(\"\\nREADY TO SUBMIT TO KAGGLE!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T06:13:12.088783Z","iopub.execute_input":"2026-10-05T06:13:12.089606Z","iopub.status.idle":"2026-10-05T06:13:12.663002Z","shell.execute_reply.started":"2026-10-05T06:13:12.089569Z","shell.execute_reply":"2026-10-05T06:13:12.662205Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cleanup large temporary extracted datasets before Quick Save\n# ============================================================\n\nimport os\nimport shutil\n\nfolders_to_remove = [\n    \"/kaggle/working/train\",\n    \"/kaggle/working/test\"\n]\n\nfor folder in folders_to_remove:\n    if os.path.exists(folder):\n        print(f\"Removing temporary folder: {folder}\")\n        shutil.rmtree(folder)\n        print(\"Removed successfully.\")\n    else:\n        print(f\"Already absent: {folder}\")\n\nprint(\"\\nIMPORTANT OUTPUT FILES:\")\n\nimportant_files = [\n    \"/kaggle/working/best_custom_cifar_net.pth\",\n    \"/kaggle/working/training_history.csv\",\n    \"/kaggle/working/submission.csv\",\n    \"/kaggle/working/accuracy_curve.png\",\n    \"/kaggle/working/loss_curve.png\",\n    \"/kaggle/working/lr_schedule.png\",\n    \"/kaggle/working/cutmix_examples.png\"\n]\n\nfor file in important_files:\n    if os.path.exists(file):\n        size_mb = os.path.getsize(file) / (1024 ** 2)\n        print(f\"✓ {os.path.basename(file)} ({size_mb:.2f} MB)\")\n    else:\n        print(f\"✗ MISSING: {os.path.basename(file)}\")\n\nprint(\"\\nCleanup complete.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-05T06:41:42.321552Z","iopub.execute_input":"2026-10-05T06:41:42.321962Z","iopub.status.idle":"2026-10-05T06:41:54.343558Z","shell.execute_reply.started":"2026-10-05T06:41:42.321927Z","shell.execute_reply":"2026-10-05T06:41:54.342573Z"}},"outputs":[],"execution_count":null}]}