{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":3649,"databundleVersionId":46718,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-01T19:00:32.509064Z","iopub.execute_input":"2025-10-01T19:00:32.509449Z","iopub.status.idle":"2025-10-01T19:00:34.87858Z","shell.execute_reply.started":"2025-10-01T19:00:32.509418Z","shell.execute_reply":"2025-10-01T19:00:34.878Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!apt install libarchive-dev\n!pip install libarchive\n\nimport os, glob\nfrom libarchive.public import file_reader\n\nSRC = \"/kaggle/input/cifar-10\"\nWORK = \"/kaggle/working\"\nTRAIN_OUT = f\"{WORK}/train\"\nTEST_OUT = f\"{WORK}/test\"\nos.makedirs(TRAIN_OUT, exist_ok=True)\nos.makedirs(TEST_OUT, exist_ok=True)\n\ndef extract_7z(archive_path: str, out_dir: str, skip_if_present: bool = True):\n    \"\"\"Extracts a .7z archive using libarchive into out_dir.\"\"\"\n    if skip_if_present and any(p.name.endswith(\".png\") for p in os.scandir(out_dir)):\n        print(f\"✓ {out_dir} already contains files. Skipping: {os.path.basename(archive_path)}\")\n        return\n    \n    cnt = 0\n    print(f\"Extracting {archive_path} -> {out_dir}\")\n    with file_reader(archive_path) as entries:\n        for entry in entries:\n            rel = entry.pathname\n            out_path = os.path.join(out_dir, os.path.basename(rel.rstrip(\"/\")))\n            if rel.endswith(\"/\"):\n                os.makedirs(out_path, exist_ok=True)\n                continue\n            os.makedirs(os.path.dirname(out_path), exist_ok=True)\n            with open(out_path, \"wb\") as f:\n                for block in entry.get_blocks():\n                    f.write(block)\n            cnt += 1\n            if cnt % 1000 == 0:\n                print(f\"{cnt} files...\")\n    \n    print(f\"✓ Done: extracted {cnt} files to {out_dir}\")\n\nextract_7z(f\"{SRC}/train.7z\", TRAIN_OUT)\nextract_7z(f\"{SRC}/test.7z\", TEST_OUT)\n\ntrain_count = len(glob.glob(f\"{TRAIN_OUT}/*.png\"))\ntest_count = len(glob.glob(f\"{TEST_OUT}/*.png\"))\nprint(f\"Counts -> train: {train_count} | test: {test_count}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T19:02:19.409685Z","iopub.execute_input":"2025-10-01T19:02:19.410194Z","iopub.status.idle":"2025-10-01T19:04:03.256085Z","shell.execute_reply.started":"2025-10-01T19:02:19.410162Z","shell.execute_reply":"2025-10-01T19:04:03.255329Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import math, random, numpy as np, pandas as pd\nfrom PIL import Image\nfrom sklearn.model_selection import StratifiedShuffleSplit\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\ntorch.backends.cudnn.benchmark = True\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", DEVICE)\n\nTRAIN_DIR = f\"{WORK}/train\"\nTEST_DIR = f\"{WORK}/test\"\nassert os.path.exists(TRAIN_DIR) and os.path.exists(TEST_DIR)\n\n# CIFAR-10 normalization stats\nMEAN = (0.4914, 0.4822, 0.4465)\nSTD = (0.2023, 0.1994, 0.2010)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T19:04:03.25773Z","iopub.execute_input":"2025-10-01T19:04:03.258087Z","iopub.status.idle":"2025-10-01T19:04:14.158582Z","shell.execute_reply.started":"2025-10-01T19:04:03.258048Z","shell.execute_reply":"2025-10-01T19:04:14.157815Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 3: Load Labels and Create Train/Val Split\n# ============================================================================\ntrain_df = pd.read_csv(f\"{SRC}/trainLabels.csv\")\ntrain_df[\"file\"] = train_df[\"id\"].astype(str) + \".png\"\n\nCLASSES = sorted(train_df[\"label\"].unique().tolist())\ncls2idx = {c: i for i, c in enumerate(CLASSES)}\nidx2cls = {i: c for c, i in cls2idx.items()}\nnum_classes = len(CLASSES)\nprint(\"Classes:\", CLASSES)\nprint(\"Number of classes:\", num_classes)\n\nsss = StratifiedShuffleSplit(n_splits=1, test_size=5000, random_state=SEED)\ntr_idx, va_idx = next(sss.split(train_df[\"file\"], train_df[\"label\"]))\ntr_df = train_df.iloc[tr_idx].reset_index(drop=True)\nva_df = train_df.iloc[va_idx].reset_index(drop=True)\nprint(f\"Train: {len(tr_df)}, Val: {len(va_df)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T19:05:49.976579Z","iopub.execute_input":"2025-10-01T19:05:49.977046Z","iopub.status.idle":"2025-10-01T19:05:50.106421Z","shell.execute_reply.started":"2025-10-01T19:05:49.977019Z","shell.execute_reply":"2025-10-01T19:05:50.105793Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ============================================================================\n# CELL 4: Dataset and DataLoaders\n# ============================================================================\ntrain_tfms = transforms.Compose([\n    transforms.RandomCrop(32, padding=4, padding_mode=\"reflect\"),\n    transforms.RandomHorizontalFlip(),\n    transforms.AutoAugment(transforms.AutoAugmentPolicy.CIFAR10),\n    transforms.ToTensor(),\n    transforms.Normalize(MEAN, STD),\n    transforms.RandomErasing(p=0.1, scale=(0.02, 0.2), value=0)\n])\n\nvalid_tfms = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(MEAN, STD)\n])\n\nclass CIFAR10Files(Dataset):\n    def __init__(self, root_dir, df, transform):\n        self.root = root_dir\n        self.df = df.reset_index(drop=True)\n        self.t = transform\n        self.has_labels = \"label\" in df.columns\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, i):\n        r = self.df.iloc[i]\n        img = Image.open(f\"{self.root}/{r['file']}\").convert(\"RGB\")\n        img = self.t(img) if self.t else img\n        if self.has_labels:\n            return img, cls2idx[r[\"label\"]]\n        return img, r[\"file\"]\n\nBATCH_SIZE = 256\nNUM_WORKERS = 2\n\ntrain_loader = DataLoader(\n    CIFAR10Files(TRAIN_DIR, tr_df, train_tfms),\n    batch_size=BATCH_SIZE, shuffle=True, drop_last=True,\n    num_workers=NUM_WORKERS, pin_memory=True\n)\n\nvalid_loader = DataLoader(\n    CIFAR10Files(TRAIN_DIR, va_df, valid_tfms),\n    batch_size=BATCH_SIZE, shuffle=False,\n    num_workers=NUM_WORKERS, pin_memory=True\n)\n\nprint(f\"Train batches: {len(train_loader)}, Val batches: {len(valid_loader)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T19:06:28.620916Z","iopub.execute_input":"2025-10-01T19:06:28.621214Z","iopub.status.idle":"2025-10-01T19:06:28.631827Z","shell.execute_reply.started":"2025-10-01T19:06:28.621192Z","shell.execute_reply":"2025-10-01T19:06:28.631034Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BasicBlock(nn.Module):\n    \"\"\"\n    Basic ResNet block with residual connection.\n    Built using Conv2d, BatchNorm2d, and ReLU - NO pre-built ResNet imports.\n    \"\"\"\n    expansion = 1\n    \n    def __init__(self, in_channels, out_channels, stride=1):\n        super(BasicBlock, self).__init__()\n        \n        # First conv layer\n        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, \n                               stride=stride, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(out_channels)\n        \n        # Second conv layer\n        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,\n                               stride=1, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(out_channels)\n        \n        # Shortcut connection (identity or projection)\n        self.shortcut = nn.Sequential()\n        if stride != 1 or in_channels != out_channels * self.expansion:\n            self.shortcut = nn.Sequential(\n                nn.Conv2d(in_channels, out_channels * self.expansion,\n                         kernel_size=1, stride=stride, bias=False),\n                nn.BatchNorm2d(out_channels * self.expansion)\n            )\n    \n    def forward(self, x):\n        # Main path\n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        \n        # Add residual connection (shortcut)\n        out += self.shortcut(x)\n        out = F.relu(out)\n        return out\n\n\nclass CustomResNet(nn.Module):\n    \"\"\"\n    Custom ResNet built from scratch using basic layers.\n    Implements residual connections as required by assignment.\n    \"\"\"\n    def __init__(self, block, num_blocks, num_classes=10):\n        super(CustomResNet, self).__init__()\n        self.in_channels = 64\n        \n        # Initial convolution (adapted for CIFAR-10's 32x32 images)\n        self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, \n                               padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(64)\n        \n        # ResNet layers with residual blocks\n        self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)\n        self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)\n        self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)\n        self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)\n        \n        # Classification head\n        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))\n        self.fc = nn.Linear(512 * block.expansion, num_classes)\n        \n        # Initialize weights\n        self._initialize_weights()\n    \n    def _make_layer(self, block, out_channels, num_blocks, stride):\n        \"\"\"Create a layer with multiple residual blocks.\"\"\"\n        strides = [stride] + [1] * (num_blocks - 1)\n        layers = []\n        for stride in strides:\n            layers.append(block(self.in_channels, out_channels, stride))\n            self.in_channels = out_channels * block.expansion\n        return nn.Sequential(*layers)\n    \n    def _initialize_weights(self):\n        \"\"\"Initialize network weights.\"\"\"\n        for m in self.modules():\n            if isinstance(m, nn.Conv2d):\n                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')\n            elif isinstance(m, nn.BatchNorm2d):\n                nn.init.constant_(m.weight, 1)\n                nn.init.constant_(m.bias, 0)\n    \n    def forward(self, x):\n        # Initial conv\n        out = F.relu(self.bn1(self.conv1(x)))\n        \n        # Residual layers\n        out = self.layer1(out)\n        out = self.layer2(out)\n        out = self.layer3(out)\n        out = self.layer4(out)\n        \n        # Global pooling and classification\n        out = self.avgpool(out)\n        out = out.view(out.size(0), -1)\n        out = self.fc(out)\n        return out\n\n\ndef CustomResNet34(num_classes=10):\n    \"\"\"ResNet-34 architecture built from basic layers.\"\"\"\n    return CustomResNet(BasicBlock, [3, 4, 6, 3], num_classes)\n\n\n# Create model\nmodel = CustomResNet34(num_classes).to(DEVICE)\ntotal_params = sum(p.numel() for p in model.parameters())\nprint(f\"Model created: Custom ResNet-34\")\nprint(f\"Total parameters: {total_params:,} ({total_params/1e6:.2f}M)\")\nprint(f\"Trainable parameters: {sum(p.numel() for p in model.parameters() if p.requires_grad):,}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T19:06:56.095123Z","iopub.execute_input":"2025-10-01T19:06:56.095415Z","iopub.status.idle":"2025-10-01T19:06:56.698807Z","shell.execute_reply.started":"2025-10-01T19:06:56.095392Z","shell.execute_reply":"2025-10-01T19:06:56.698175Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 6: Training Utilities\n# ============================================================================\ndef rand_bbox(size, lam):\n    \"\"\"Generate random bounding box for CutMix.\"\"\"\n    W, H = size[2], size[3]\n    cut_rat = math.sqrt(1. - lam)\n    cut_w, cut_h = int(W * cut_rat), int(H * cut_rat)\n    cx, cy = np.random.randint(W), np.random.randint(H)\n    x1 = np.clip(cx - cut_w // 2, 0, W)\n    y1 = np.clip(cy - cut_h // 2, 0, H)\n    x2 = np.clip(cx + cut_w // 2, 0, W)\n    y2 = np.clip(cy + cut_h // 2, 0, H)\n    return x1, y1, x2, y2\n\ndef mix_criterion(criterion, pred, y_a, y_b, lam):\n    \"\"\"Loss function for MixUp/CutMix.\"\"\"\n    return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)\n\nclass EMA:\n    \"\"\"Exponential Moving Average for model weights.\"\"\"\n    def __init__(self, model, decay=0.999):\n        self.decay = decay\n        self.shadow = {k: v.detach().clone() for k, v in model.state_dict().items()}\n        self.backup = None\n    \n    def update(self, model):\n        with torch.no_grad():\n            for k, v in model.state_dict().items():\n                if v.is_floating_point():\n                    self.shadow[k].mul_(self.decay).add_(v.detach(), alpha=1.0 - self.decay)\n                else:\n                    self.shadow[k] = v.detach().clone()\n    \n    def apply_to(self, model):\n        self.backup = {k: v.detach().clone() for k, v in model.state_dict().items()}\n        model.load_state_dict(self.shadow, strict=True)\n    \n    def restore(self, model):\n        model.load_state_dict(self.backup, strict=True)\n        self.backup = None\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T19:07:37.7055Z","iopub.execute_input":"2025-10-01T19:07:37.706236Z","iopub.status.idle":"2025-10-01T19:07:37.716524Z","shell.execute_reply.started":"2025-10-01T19:07:37.706197Z","shell.execute_reply":"2025-10-01T19:07:37.715777Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 7: Training Configuration\n# ============================================================================\nEPOCHS = 80\nBASE_LR = 0.2\nWEIGHT_DECAY = 5e-4\nLABEL_SMOOTH = 0.1\nMIXUP_ALPHA = 0.2\nCUTMIX_ALPHA = 1.0\nMIX_PROB = 0.7\n\noptimizer = torch.optim.SGD(\n    model.parameters(), \n    lr=BASE_LR, \n    momentum=0.9,\n    nesterov=True, \n    weight_decay=WEIGHT_DECAY\n)\n\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\ncriterion = nn.CrossEntropyLoss(label_smoothing=LABEL_SMOOTH)\n\n# Mixed precision training\nif DEVICE.type == \"cuda\":\n    scaler = torch.amp.GradScaler(\"cuda\")\n    def autocast_ctx():\n        return torch.amp.autocast(device_type=\"cuda\")\nelse:\n    scaler = torch.amp.GradScaler(enabled=False)\n    def autocast_ctx():\n        from contextlib import nullcontext\n        return nullcontext()\n\nema = EMA(model, decay=0.999)\nbest_acc = 0.0\nBEST_PATH = f\"{WORK}/best_custom_resnet34.pth\"\nCKPT_DIR = f\"{WORK}/ckpts\"\nos.makedirs(CKPT_DIR, exist_ok=True)\n\nprint(\"Training configuration:\")\nprint(f\"  Epochs: {EPOCHS}\")\nprint(f\"  Base LR: {BASE_LR}\")\nprint(f\"  Batch size: {BATCH_SIZE}\")\nprint(f\"  Device: {DEVICE}\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T19:07:50.768008Z","iopub.execute_input":"2025-10-01T19:07:50.768304Z","iopub.status.idle":"2025-10-01T19:07:50.783924Z","shell.execute_reply.started":"2025-10-01T19:07:50.768281Z","shell.execute_reply":"2025-10-01T19:07:50.783257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 8: Training Loop\n# ============================================================================\nprint(\"\\nStarting training...\")\nfor epoch in range(1, EPOCHS + 1):\n    model.train()\n    tr_loss, tr_correct, seen = 0.0, 0, 0\n    \n    for xb, yb in train_loader:\n        xb, yb = xb.to(DEVICE, non_blocking=True), yb.to(DEVICE, non_blocking=True)\n        optimizer.zero_grad(set_to_none=True)\n        \n        use_mix = (np.random.rand() < MIX_PROB)\n        if use_mix:\n            r = np.random.rand()\n            if r < 0.5 and MIXUP_ALPHA > 0:\n                # MixUp\n                lam = np.random.beta(MIXUP_ALPHA, MIXUP_ALPHA)\n                idx = torch.randperm(xb.size(0), device=xb.device)\n                xb2 = lam * xb + (1 - lam) * xb[idx]\n                y_a, y_b = yb, yb[idx]\n                with autocast_ctx():\n                    logits = model(xb2)\n                    loss = mix_criterion(criterion, logits, y_a, y_b, lam)\n            else:\n                # CutMix\n                lam = np.random.beta(CUTMIX_ALPHA, CUTMIX_ALPHA)\n                idx = torch.randperm(xb.size(0), device=xb.device)\n                y_a, y_b = yb, yb[idx]\n                x1, y1, x2, y2 = rand_bbox(xb.size(), lam)\n                xbm = xb.clone()\n                xbm[:, :, y1:y2, x1:x2] = xb[idx, :, y1:y2, x1:x2]\n                lam = 1 - ((x2 - x1) * (y2 - y1) / (xb.size(-1) * xb.size(-2)))\n                with autocast_ctx():\n                    logits = model(xbm)\n                    loss = mix_criterion(criterion, logits, y_a, y_b, lam)\n        else:\n            # Standard training\n            with autocast_ctx():\n                logits = model(xb)\n                loss = criterion(logits, yb)\n        \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        ema.update(model)\n        \n        tr_loss += loss.item() * xb.size(0)\n        seen += xb.size(0)\n        if not use_mix:\n            tr_correct += (logits.argmax(1) == yb).float().sum().item()\n    \n    scheduler.step()\n    tr_loss /= seen\n    tr_acc = tr_correct / max(seen, 1)\n    \n    # Validation with EMA weights\n    model.eval()\n    ema.apply_to(model)\n    va_loss, va_correct, va_seen = 0.0, 0, 0\n    \n    with torch.no_grad():\n        for xb, yb in valid_loader:\n            xb, yb = xb.to(DEVICE), yb.to(DEVICE)\n            with autocast_ctx():\n                logits = model(xb)\n                loss = criterion(logits, yb)\n            va_loss += loss.item() * xb.size(0)\n            va_correct += (logits.argmax(1) == yb).float().sum().item()\n            va_seen += xb.size(0)\n    \n    ema.restore(model)\n    \n    va_loss /= va_seen\n    va_acc = va_correct / va_seen\n    \n    # Save best model\n    if va_acc > best_acc:\n        best_acc = va_acc\n        torch.save({\n            \"model\": model.state_dict(),\n            \"ema\": ema.shadow,\n            \"acc\": best_acc,\n            \"epoch\": epoch\n        }, BEST_PATH)\n    \n    # Save periodic checkpoints\n    if epoch % 5 == 0:\n        torch.save({\n            \"model\": model.state_dict(),\n            \"ema\": ema.shadow,\n            \"acc\": va_acc,\n            \"epoch\": epoch\n        }, f\"{CKPT_DIR}/ep{epoch:03d}.pth\")\n    \n    if epoch % 5 == 0 or epoch == 1:\n        print(f\"Epoch {epoch:02d}/{EPOCHS} | \"\n              f\"train loss: {tr_loss:.4f} | \"\n              f\"val loss: {va_loss:.4f} acc: {va_acc:.4f} | \"\n              f\"best: {best_acc:.4f}\")\n\nprint(f\"\\nTraining complete! Best validation accuracy: {best_acc:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T19:08:08.568611Z","iopub.execute_input":"2025-10-01T19:08:08.569366Z","iopub.status.idle":"2025-10-01T19:54:48.857213Z","shell.execute_reply.started":"2025-10-01T19:08:08.569338Z","shell.execute_reply":"2025-10-01T19:54:48.856037Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CIFAR10Test(Dataset):\n    def __init__(self, root):\n        self.root = root\n        self.files = sorted(\n            [p.name for p in os.scandir(root) if p.name.endswith(\".png\")],\n            key=lambda s: int(s.split(\".\")[0])\n        )\n    \n    def __len__(self):\n        return len(self.files)\n    \n    def __getitem__(self, i):\n        f = self.files[i]\n        img = Image.open(f\"{self.root}/{f}\").convert(\"RGB\")\n        return img, f\n\n# Load best model\nmodel = CustomResNet34(num_classes).to(DEVICE)\nckpt = torch.load(BEST_PATH, map_location=\"cpu\")\nmodel.load_state_dict(ckpt[\"model\"], strict=True)\n\n# Apply EMA weights\nema_shadow = ckpt[\"ema\"]\nmodel.load_state_dict(ema_shadow, strict=True)\n\n# Test-time augmentation setup\nhflip = transforms.RandomHorizontalFlip(p=1.0)\n\ntest_loader = DataLoader(\n    CIFAR10Test(TEST_DIR),\n    batch_size=512,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True,\n    collate_fn=lambda batch: batch\n)\n\n# Generate predictions\nids, preds = [], []\nmodel.eval()\n\nprint(\"Generating predictions...\")\nwith torch.no_grad():\n    for batch in test_loader:\n        imgs, names = zip(*batch)\n        \n        # Apply transforms and test-time augmentation\n        xb = torch.stack([valid_tfms(im) for im in imgs]).to(DEVICE, non_blocking=True)\n        xbF = torch.stack([valid_tfms(hflip(im)) for im in imgs]).to(DEVICE, non_blocking=True)\n        \n        # Average predictions from original and flipped\n        logits = model(xb) + model(xbF)\n        p = logits.argmax(1).tolist()\n        \n        ids.extend([n.split(\".\")[0] for n in names])\n        preds.extend([idx2cls[i] for i in p])\n\n# Create submission file\nsub = pd.DataFrame({\"id\": ids, \"label\": preds})\nsub.to_csv(f\"{WORK}/submission.csv\", index=False)\n\nprint(f\"✓ Submission created: submission.csv {sub.shape}\")\nprint(\"\\nFirst few predictions:\")\nprint(sub.head(10))\nprint(\"\\nPrediction distribution:\")\nprint(sub[\"label\"].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T19:55:41.359584Z","iopub.execute_input":"2025-10-01T19:55:41.359925Z","iopub.status.idle":"2025-10-01T20:02:01.392779Z","shell.execute_reply.started":"2025-10-01T19:55:41.359893Z","shell.execute_reply":"2025-10-01T20:02:01.391969Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 10: Neural Network Architecture Drawing\n# ============================================================================\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as mpatches\nfrom matplotlib.patches import FancyBboxPatch, Rectangle, FancyArrowPatch\n\nfig, ax = plt.subplots(figsize=(12, 16))\nax.set_xlim(0, 10)\nax.set_ylim(0, 40)\nax.axis('off')\n\ndef draw_box(x, y, w, h, text, color='lightblue', fontsize=9):\n    \"\"\"Draw a box with text.\"\"\"\n    rect = FancyBboxPatch((x, y), w, h, boxstyle=\"round,pad=0.05\", \n                          edgecolor='black', facecolor=color, linewidth=1.5)\n    ax.add_patch(rect)\n    ax.text(x + w/2, y + h/2, text, ha='center', va='center', \n           fontsize=fontsize, weight='bold')\n\ndef draw_arrow(x1, y1, x2, y2):\n    \"\"\"Draw an arrow.\"\"\"\n    arrow = FancyArrowPatch((x1, y1), (x2, y2), arrowstyle='->', \n                           color='black', linewidth=2, mutation_scale=20)\n    ax.add_patch(arrow)\n\n# Title\nax.text(5, 38, 'Custom ResNet-34 Architecture', ha='center', \n       fontsize=18, weight='bold')\n\ny_pos = 36\n\n# Input\ndraw_box(3, y_pos, 4, 1, \"Input: 3×32×32\", color='#E8E8E8')\ny_pos -= 1.5\n\n# Initial Conv\ndraw_arrow(5, y_pos + 1.5, 5, y_pos + 1)\ndraw_box(3, y_pos, 4, 1, \"Conv 3×3, stride=1\\n64 channels, BN, ReLU\", color='#AED6F1')\ny_pos -= 2\n\n# Layer 1\ndraw_arrow(5, y_pos + 2, 5, y_pos + 1.5)\ndraw_box(3, y_pos, 4, 1.5, \"Layer 1: 3 Residual Blocks\\n64 channels, stride=1\", color='#F9E79F')\nax.text(7.5, y_pos + 0.75, \"64×32×32\", fontsize=8, style='italic')\ny_pos -= 2.5\n\n# Layer 2\ndraw_arrow(5, y_pos + 2.5, 5, y_pos + 1.5)\ndraw_box(3, y_pos, 4, 1.5, \"Layer 2: 4 Residual Blocks\\n128 channels, stride=2\", color='#FADBD8')\nax.text(7.5, y_pos + 0.75, \"128×16×16\", fontsize=8, style='italic')\ny_pos -= 2.5\n\n# Layer 3\ndraw_arrow(5, y_pos + 2.5, 5, y_pos + 1.5)\ndraw_box(3, y_pos, 4, 1.5, \"Layer 3: 6 Residual Blocks\\n256 channels, stride=2\", color='#D7BDE2')\nax.text(7.5, y_pos + 0.75, \"256×8×8\", fontsize=8, style='italic')\ny_pos -= 2.5\n\n# Layer 4\ndraw_arrow(5, y_pos + 2.5, 5, y_pos + 1.5)\ndraw_box(3, y_pos, 4, 1.5, \"Layer 4: 3 Residual Blocks\\n512 channels, stride=2\", color='#A9DFBF')\nax.text(7.5, y_pos + 0.75, \"512×4×4\", fontsize=8, style='italic')\ny_pos -= 2.5\n\n# Global Average Pooling\ndraw_arrow(5, y_pos + 2.5, 5, y_pos + 1)\ndraw_box(3, y_pos, 4, 1, \"Global Avg Pool\\n512×1×1\", color='#D6EAF8')\ny_pos -= 1.8\n\n# Fully Connected\ndraw_arrow(5, y_pos + 1.8, 5, y_pos + 1)\ndraw_box(3, y_pos, 4, 1, f\"Fully Connected\\n512 → {num_classes}\", color='#ABEBC6')\ny_pos -= 1.8\n\n# Output\ndraw_arrow(5, y_pos + 1.8, 5, y_pos + 1)\ndraw_box(3, y_pos, 4, 1, f\"Output: {num_classes} classes\", color='#E8E8E8')\n\n# Residual Block Detail (inset)\ninset_x, inset_y = 0.5, 8\nax.text(inset_x + 2, inset_y + 6, 'Residual Block Detail:', \n       fontsize=11, weight='bold')\n\n# Input to block\ndraw_box(inset_x + 1, inset_y + 4.5, 2, 0.6, \"Input\", color='#E8E8E8', fontsize=8)\n\n# Main path\ndraw_arrow(inset_x + 2, inset_y + 4.5, inset_x + 2, inset_y + 4)\ndraw_box(inset_x + 1, inset_y + 3, 2, 0.8, \"Conv 3×3\\nBN, ReLU\", color='#FADBD8', fontsize=8)\ndraw_arrow(inset_x + 2, inset_y + 3, inset_x + 2, inset_y + 2.3)\ndraw_box(inset_x + 1, inset_y + 1.3, 2, 0.8, \"Conv 3×3\\nBN\", color='#FADBD8', fontsize=8)\ndraw_arrow(inset_x + 2, inset_y + 1.3, inset_x + 2, inset_y + 0.8)\n\n# Skip connection\nax.plot([inset_x + 0.5, inset_x + 0.5, inset_x + 1], \n        [inset_y + 4.8, inset_y + 0.5, inset_y + 0.5], \n        'b--', linewidth=2, label='Skip Connection')\ndraw_arrow(inset_x + 1, inset_y + 0.5, inset_x + 1.8, inset_y + 0.5)\n\n# Add\ndraw_box(inset_x + 1, inset_y - 0.2, 2, 0.6, \"Add + ReLU\", color='#ABEBC6', fontsize=8)\ndraw_arrow(inset_x + 2, inset_y + 0.4, inset_x + 2, inset_y - 0.2)\n\n# Output from block\ndraw_arrow(inset_x + 2, inset_y - 0.8, inset_x + 2, inset_y - 1.2)\ndraw_box(inset_x + 1, inset_y - 1.8, 2, 0.6, \"Output\", color='#E8E8E8', fontsize=8)\n\n# Architecture info\ninfo_box_text = f\"\"\"Architecture Summary:\n- Total layers: 34\n- Residual blocks: 16 (3+4+6+3)\n- Parameters: ~21M\n- Built from: Conv2d, BatchNorm2d, \n  ReLU (basic layers only)\n- Key feature: Residual connections \n  in every block\"\"\"\n\nax.text(0.3, 2, info_box_text, fontsize=9, verticalalignment='top',\n       bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.7))\n\nplt.tight_layout()\nplt.savefig(f\"{WORK}/neural_network_architecture.png\", dpi=300, bbox_inches='tight')\nprint(\"✓ Architecture drawing saved: neural_network_architecture.png\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T20:41:30.665918Z","iopub.execute_input":"2025-10-01T20:41:30.666198Z","iopub.status.idle":"2025-10-01T20:41:32.032516Z","shell.execute_reply.started":"2025-10-01T20:41:30.666174Z","shell.execute_reply":"2025-10-01T20:41:32.031764Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}