{"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":"gpu","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":"!ls -lah /kaggle/input/cifar-10","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:58:01.710713Z","iopub.execute_input":"2025-10-01T14:58:01.710978Z","iopub.status.idle":"2025-10-01T14:58:01.832547Z","shell.execute_reply.started":"2025-10-01T14:58:01.710946Z","shell.execute_reply":"2025-10-01T14:58:01.831599Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# A) imports, seed, device\nimport os, math, random, glob\nfrom pathlib import Path\nimport numpy as np, pandas as pd\nfrom PIL import Image\n\nimport torch, torch.nn as nn\nfrom torch.utils.data import DataLoader, random_split\nfrom torchvision import transforms\nfrom torchvision.models import resnet18\nfrom torch.cuda.amp import autocast, GradScaler\n\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)\ntorch.backends.cudnn.benchmark = True\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Device:\", DEVICE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:58:01.834768Z","iopub.execute_input":"2025-10-01T14:58:01.835174Z","iopub.status.idle":"2025-10-01T14:58:09.227748Z","shell.execute_reply.started":"2025-10-01T14:58:01.835138Z","shell.execute_reply":"2025-10-01T14:58:09.227124Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# B) extract Kaggle train.7z and build train/val loaders\n!apt -yq update >/dev/null\n!apt -yq install libarchive-dev >/dev/null\n!pip -q install libarchive >/dev/null\n\nimport libarchive.public\n\n# constants\nCIFAR10_MEAN = (0.4914, 0.4822, 0.4465)\nCIFAR10_STD  = (0.2470, 0.2435, 0.2616)\nCLASSES = ['airplane','automobile','bird','cat','deer','dog','frog','horse','ship','truck']\nclass_to_idx = {c:i for i,c in enumerate(CLASSES)}\n\n# extract train set to /kaggle/working/train (only once)\ntrain_out = \"/kaggle/working/train\"\nos.makedirs(train_out, exist_ok=True)\nif len(glob.glob(train_out + \"/*\")) < 1000:\n    for _ in libarchive.public.file_pour('/kaggle/input/cifar-10/train.7z'):\n        pass\n\n# labels\nlabels_df = pd.read_csv('/kaggle/input/cifar-10/trainLabels.csv')  # id,label\nlabels_df['path'] = labels_df['id'].astype(str).apply(lambda i: f\"{train_out}/{i}.png\")\nprint(\"Rows in labels:\", len(labels_df))\n\n# transforms\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(CIFAR10_MEAN, CIFAR10_STD),\n    transforms.RandomErasing(p=0.25, scale=(0.02,0.15), ratio=(0.3,3.3), value='random')\n])\ntest_tfms = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n])\n\n# dataset\nclass KaggleTrainDataset(torch.utils.data.Dataset):\n    def __init__(self, df, tfm):\n        self.df = df.reset_index(drop=True)\n        self.tfm = tfm\n    def __len__(self): return len(self.df)\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img = Image.open(row['path']).convert(\"RGB\")\n        y = class_to_idx[row['label']]\n        return self.tfm(img), y\n\nfull_ds = KaggleTrainDataset(labels_df, train_tfms)\n\n# split train/val\nval_size = 5000\ntrain_size = len(full_ds) - val_size\ntrain_ds, val_ds = random_split(full_ds, [train_size, val_size],\n                                generator=torch.Generator().manual_seed(SEED))\n\nBS = 128   # if OOM later, set 64 (also change in H)\ntrain_loader = DataLoader(train_ds, batch_size=BS, shuffle=True,  num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_ds,   batch_size=BS, shuffle=False, num_workers=2, pin_memory=True)\n\nprint(\"train/val sizes:\", len(train_ds), len(val_ds))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:58:09.228451Z","iopub.execute_input":"2025-10-01T14:58:09.228855Z","iopub.status.idle":"2025-10-01T14:58:41.862889Z","shell.execute_reply.started":"2025-10-01T14:58:09.228834Z","shell.execute_reply":"2025-10-01T14:58:41.861996Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# C') scratch-built residual CNN for CIFAR-10 (no torchvision models)\n\nimport torch.nn as nn\n\nclass BasicBlock(nn.Module):\n    def __init__(self, in_ch, out_ch, stride=1):\n        super().__init__()\n        self.conv1 = nn.Conv2d(in_ch, out_ch, 3, stride=stride, padding=1, bias=False)\n        self.bn1   = nn.BatchNorm2d(out_ch)\n        self.relu  = nn.ReLU(inplace=True)\n        self.conv2 = nn.Conv2d(out_ch, out_ch, 3, stride=1, padding=1, bias=False)\n        self.bn2   = nn.BatchNorm2d(out_ch)\n\n        self.down = None\n        if stride != 1 or in_ch != out_ch:\n            self.down = nn.Sequential(\n                nn.Conv2d(in_ch, out_ch, 1, stride=stride, bias=False),\n                nn.BatchNorm2d(out_ch)\n            )\n\n    def forward(self, x):\n        identity = x\n        out = self.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        if self.down is not None:\n            identity = self.down(identity)\n        out = self.relu(out + identity)\n        return out\n\ndef make_stage(in_ch, out_ch, num_blocks, first_stride):\n    layers = [BasicBlock(in_ch, out_ch, stride=first_stride)]\n    for _ in range(1, num_blocks):\n        layers.append(BasicBlock(out_ch, out_ch, stride=1))\n    return nn.Sequential(*layers)\n\nclass SmallResNetCIFAR(nn.Module):\n    # 3 stages: [64, 128, 256]; total ~11 layers with residuals\n    def __init__(self, num_classes=10):\n        super().__init__()\n        self.stem = nn.Sequential(\n            nn.Conv2d(3, 64, 3, stride=1, padding=1, bias=False),  # CIFAR stem\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True),\n        )\n        self.stage1 = make_stage(64,  64,  num_blocks=2, first_stride=1)  # 32x32\n        self.stage2 = make_stage(64,  128, num_blocks=2, first_stride=2)  # 16x16\n        self.stage3 = make_stage(128, 256, num_blocks=2, first_stride=2)  # 8x8\n        self.pool = nn.AdaptiveAvgPool2d(1)\n        self.fc   = nn.Linear(256, num_classes)\n\n        # kaiming initialization (still random, no pretrain)\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            if isinstance(m, nn.BatchNorm2d):\n                nn.init.ones_(m.weight); nn.init.zeros_(m.bias)\n\n    def forward(self, x):\n        x = self.stem(x)\n        x = self.stage1(x)\n        x = self.stage2(x)\n        x = self.stage3(x)\n        x = self.pool(x).flatten(1)\n        x = self.fc(x)\n        return x\n\nmodel = SmallResNetCIFAR().to(DEVICE)\nprint(\"Custom residual model ready (random init; no torchvision models used).\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:58:41.864734Z","iopub.execute_input":"2025-10-01T14:58:41.864972Z","iopub.status.idle":"2025-10-01T14:58:42.098934Z","shell.execute_reply.started":"2025-10-01T14:58:41.864949Z","shell.execute_reply":"2025-10-01T14:58:42.098359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# D) training setup\nEPOCHS = 40          # quick first run; raise to 80+ later for higher score\nBASE_LR = 0.1\n\noptimizer = torch.optim.SGD(model.parameters(), lr=BASE_LR, momentum=0.9,\n                            weight_decay=5e-4, nesterov=True)\ncriterion = nn.CrossEntropyLoss(label_smoothing=0.1)\nscaler = GradScaler()\n\nwarmup_epochs = 5\ntotal_steps = EPOCHS * len(train_loader)\n\ndef cosine_lr(step, total_steps, warmup_steps, base_lr):\n    if step < warmup_steps:\n        return base_lr * (step / warmup_steps)\n    progress = (step - warmup_steps) / (total_steps - warmup_steps)\n    return 0.5 * base_lr * (1.0 + math.cos(math.pi * progress))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:58:42.099629Z","iopub.execute_input":"2025-10-01T14:58:42.099879Z","iopub.status.idle":"2025-10-01T14:58:42.106149Z","shell.execute_reply.started":"2025-10-01T14:58:42.099857Z","shell.execute_reply":"2025-10-01T14:58:42.105571Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# E) train loop + validation\ndef evaluate(loader):\n    model.eval()\n    correct, total, loss_sum = 0, 0, 0.0\n    with torch.no_grad():\n        for x, y in loader:\n            x, y = x.to(DEVICE, non_blocking=True), y.to(DEVICE, non_blocking=True)\n            with autocast():\n                logits = model(x)\n                loss = criterion(logits, y)\n            loss_sum += loss.item() * x.size(0)\n            pred = logits.argmax(1)\n            correct += (pred == y).sum().item()\n            total += x.size(0)\n    return loss_sum/total, correct/total\n\nbest_val = 0.0\nglobal_step = 0\n\nfor epoch in range(1, EPOCHS+1):\n    model.train()\n    for x, y in train_loader:\n        x, y = x.to(DEVICE, non_blocking=True), y.to(DEVICE, non_blocking=True)\n        lr = cosine_lr(global_step, total_steps, warmup_epochs*len(train_loader), BASE_LR)\n        for g in optimizer.param_groups: g['lr'] = lr\n        optimizer.zero_grad(set_to_none=True)\n        with autocast():\n            logits = model(x)\n            loss = criterion(logits, y)\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        global_step += 1\n\n    val_loss, val_acc = evaluate(val_loader)\n    print(f\"Epoch {epoch:03d} | val_acc={val_acc:.4f} | val_loss={val_loss:.4f} | lr={lr:.5f}\")\n    if val_acc > best_val:\n        best_val = val_acc\n        torch.save(model.state_dict(), \"best_model.pth\")\n\nprint(\"Best val acc:\", best_val)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:58:42.106907Z","iopub.execute_input":"2025-10-01T14:58:42.107438Z","iopub.status.idle":"2025-10-01T15:19:34.988552Z","shell.execute_reply.started":"2025-10-01T14:58:42.107415Z","shell.execute_reply":"2025-10-01T15:19:34.987734Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# G) extract competition test set to /kaggle/working/test\nimport libarchive.public\nout_dir = \"/kaggle/working/test\"\nos.makedirs(out_dir, exist_ok=True)\nif len(glob.glob(out_dir + \"/*\")) < 1000:\n    for _ in libarchive.public.file_pour('/kaggle/input/cifar-10/test.7z'):\n        pass\nprint(\"Files in /kaggle/working/test:\", len(glob.glob(out_dir + \"/*\")))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T15:19:34.989571Z","iopub.execute_input":"2025-10-01T15:19:34.989789Z","iopub.status.idle":"2025-10-01T15:20:37.541340Z","shell.execute_reply.started":"2025-10-01T15:19:34.989765Z","shell.execute_reply":"2025-10-01T15:20:37.540480Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# H (fixed TTA): create a clean submission.csv\ntest_tfms = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n])\n\nclass ImageFolderFromDir(torch.utils.data.Dataset):\n    def __init__(self, folder, tfm):\n        self.paths = sorted(glob.glob(os.path.join(folder, \"*.png\")), key=lambda p: int(Path(p).stem))\n        self.tfm = tfm\n    def __len__(self): return len(self.paths)\n    def __getitem__(self, idx):\n        p = self.paths[idx]\n        img = Image.open(p).convert(\"RGB\")\n        return self.tfm(img), int(Path(p).stem)\n\nkaggle_test_ds = ImageFolderFromDir(\"/kaggle/working/test\", test_tfms)\nkaggle_test_loader = DataLoader(kaggle_test_ds, batch_size=BS, shuffle=False, num_workers=2, pin_memory=True)\n\nidx_to_label = ['airplane','automobile','bird','cat','deer','dog','frog','horse','ship','truck']\n\nmodel.load_state_dict(torch.load(\"best_model.pth\", map_location=DEVICE))\nmodel.eval()\n\nids, labels = [], []\nwith torch.no_grad():\n    for x, img_ids in kaggle_test_loader:\n        x = x.to(DEVICE, non_blocking=True)\n        with autocast():\n            logits1 = model(x)\n            logits2 = model(torch.flip(x, dims=[3]))  # horizontal flip\n            logits  = (logits1 + logits2) / 2\n        pred = logits.argmax(1).cpu().numpy()\n\n        # IMPORTANT: convert batch tensor -> list of Python ints\n        ids.extend([int(i) for i in img_ids])   # <-- this fixes the \"tensor(1)\" issue\n        labels.extend([idx_to_label[i] for i in pred])\n\nimport pandas as pd\nsub = pd.DataFrame({\"id\": ids, \"label\": labels}).sort_values(\"id\")\nsub.to_csv(\"submission.csv\", index=False)\nprint(sub.shape)  # should be (300000, 2)\nsub.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T15:20:37.542426Z","iopub.execute_input":"2025-10-01T15:20:37.542744Z","iopub.status.idle":"2025-10-01T15:21:52.489050Z","shell.execute_reply.started":"2025-10-01T15:20:37.542717Z","shell.execute_reply":"2025-10-01T15:21:52.488403Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\ns = pd.read_csv(\"submission.csv\")\nprint(s.shape, s.head(), s.dtypes)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T15:21:52.490102Z","iopub.execute_input":"2025-10-01T15:21:52.490416Z","iopub.status.idle":"2025-10-01T15:21:52.558155Z","shell.execute_reply.started":"2025-10-01T15:21:52.490379Z","shell.execute_reply":"2025-10-01T15:21:52.557569Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# F-exact: evaluate on the official CIFAR-10 test set (requires Internet = On)\nfrom torchvision.datasets import CIFAR10\nfrom torch.utils.data import DataLoader\n\ntest_official = CIFAR10(root=\"/kaggle/working/data\", train=False, download=True, transform=test_tfms)\ntest_loader_official = DataLoader(test_official, batch_size=BS, shuffle=False, num_workers=2, pin_memory=True)\n\nmodel.load_state_dict(torch.load(\"best_model.pth\", map_location=DEVICE))\ntl, ta = evaluate(test_loader_official)\nprint(\"Official CIFAR-10 test accuracy (≈ Kaggle score):\", ta)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T15:21:52.559916Z","iopub.execute_input":"2025-10-01T15:21:52.560187Z","iopub.status.idle":"2025-10-01T15:22:09.171321Z","shell.execute_reply.started":"2025-10-01T15:21:52.560171Z","shell.execute_reply":"2025-10-01T15:22:09.170539Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Graphviz diagram for the scratch residual CIFAR-10 CNN\n# Outputs: /kaggle/working/architecture_graphviz.png and .svg\n\n# Install graphviz engine + Python bindings (safe to re-run)\n!apt -yq install graphviz >/dev/null\n!pip -q install graphviz >/dev/null\n\nfrom graphviz import Digraph\nfrom IPython.display import Image, display\n\n# --- Theme (tweak if you like) ---\nPINK       = \"#ff8fab\"   # lines / text\nLIGHTPINK  = \"#ffe5ec\"   # node fill\nBORDER     = \"#ffc2d1\"   # cluster border\nFONT       = \"DejaVu Sans\"\n\ng = Digraph(\"CIFAR10_ResCNN\", format=\"png\")\ng.attr(rankdir=\"LR\", bgcolor=\"white\", pad=\"0.25\", splines=\"spline\",\n       ranksep=\"0.65\", nodesep=\"0.45\", dpi=\"300\")\ng.attr(\"node\", shape=\"box\", style=\"rounded,filled\",\n       color=PINK, fillcolor=LIGHTPINK, penwidth=\"2.2\",\n       fontname=FONT, fontsize=\"11\")\ng.attr(\"edge\", color=PINK, penwidth=\"1.8\", arrowsize=\"0.85\")\n\n# ---- Nodes & clusters ----\ng.node(\"inp\", \"Input\\n32×32×3\")\n\nwith g.subgraph(name=\"cluster_stem\") as c:\n    c.attr(label=\"Stem\", color=BORDER, penwidth=\"2\", style=\"rounded\", fontname=FONT)\n    c.node(\"stem\", \"3×3 conv\\nBN, ReLU\\n→ 32×32×64\")\n\nwith g.subgraph(name=\"cluster_s1\") as c:\n    c.attr(label=\"Stage 1 (64)\", color=BORDER, penwidth=\"2\", style=\"rounded\", fontname=FONT)\n    c.node(\"s1b1\", \"Block 1\\n3×3 → 3×3\")\n    c.node(\"plus1\", \"+\", shape=\"circle\", width=\"0.35\", height=\"0.35\",\n           style=\"filled\", fillcolor=LIGHTPINK, color=PINK, fontsize=\"10\")\n    c.node(\"s1b2\", \"Block 2\\n3×3 → 3×3\")\n\nwith g.subgraph(name=\"cluster_s2\") as c:\n    c.attr(label=\"Stage 2 (128, stride 2)\", color=BORDER, penwidth=\"2\", style=\"rounded\", fontname=FONT)\n    c.node(\"s2b1\", \"Block 1\\n3×3 → 3×3\")\n    c.node(\"plus2\", \"+\", shape=\"circle\", width=\"0.35\", height=\"0.35\",\n           style=\"filled\", fillcolor=LIGHTPINK, color=PINK, fontsize=\"10\")\n    c.node(\"s2b2\", \"Block 2\\n3×3 → 3×3\")\n\nwith g.subgraph(name=\"cluster_s3\") as c:\n    c.attr(label=\"Stage 3 (256, stride 2)\", color=BORDER, penwidth=\"2\", style=\"rounded\", fontname=FONT)\n    c.node(\"s3b1\", \"Block 1\\n3×3 → 3×3\")\n    c.node(\"plus3\", \"+\", shape=\"circle\", width=\"0.35\", height=\"0.35\",\n           style=\"filled\", fillcolor=LIGHTPINK, color=PINK, fontsize=\"10\")\n    c.node(\"s3b2\", \"Block 2\\n3×3 → 3×3\")\n\ng.node(\"head\", \"GAP → FC(256→10)\")\n\n# ---- Main forward path ----\ng.edges([\n    (\"inp\",\"stem\"),\n    (\"stem\",\"s1b1\"),\n    (\"s1b1\",\"s1b2\"),\n    (\"s1b2\",\"s2b1\"),\n    (\"s2b1\",\"s2b2\"),\n    (\"s2b2\",\"s3b1\"),\n    (\"s3b1\",\"s3b2\"),\n    (\"s3b2\",\"head\"),\n])\n\n# ---- Residual (skip) visuals (dashed, non-constraining) ----\n# Stage 1: input to block1 added before block2\ng.edge(\"stem\",\"plus1\", style=\"dashed\", xlabel=\"skip\", constraint=\"false\")\ng.edge(\"s1b1\",\"plus1\", constraint=\"false\")\ng.edge(\"plus1\",\"s1b2\", arrowhead=\"none\", constraint=\"false\")\n\n# Stage 2: downsample skip (typically 1×1 conv); add before block2\ng.edge(\"s1b2\",\"plus2\", style=\"dashed\", xlabel=\"skip/1×1\", constraint=\"false\")\ng.edge(\"s2b1\",\"plus2\", constraint=\"false\")\ng.edge(\"plus2\",\"s2b2\", arrowhead=\"none\", constraint=\"false\")\n\n# Stage 3: downsample skip; add before block2\ng.edge(\"s2b2\",\"plus3\", style=\"dashed\", xlabel=\"skip/1×1\", constraint=\"false\")\ng.edge(\"s3b1\",\"plus3\", constraint=\"false\")\ng.edge(\"plus3\",\"s3b2\", arrowhead=\"none\", constraint=\"false\")\n\n# ---- Render & show ----\npng_path = g.render(filename=\"architecture_graphviz\")\ng.format = \"svg\"; svg_path = g.render(filename=\"architecture_graphviz\")\nprint(\"Saved:\", png_path, \"and\", svg_path)\ndisplay(Image(filename=png_path))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T15:46:51.726308Z","iopub.execute_input":"2025-10-01T15:46:51.726768Z","iopub.status.idle":"2025-10-01T15:46:57.389202Z","shell.execute_reply.started":"2025-10-01T15:46:51.726737Z","shell.execute_reply":"2025-10-01T15:46:57.388318Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cute pink Graphviz diagram (explicit Conv2d labels)\n# Outputs: /kaggle/working/architecture_graphviz.png and .svg\n\n# Install Graphviz engine + Python bindings (safe to re-run)\n!apt -yq install graphviz >/dev/null\n!pip -q install graphviz >/dev/null\n\nfrom graphviz import Digraph\nfrom IPython.display import Image, display\n\n# --- Theme ---\nPINK, LIGHTPINK, BORDER = \"#ff8fab\", \"#ffe5ec\", \"#ffc2d1\"\nFONT = \"DejaVu Sans\"\n\ng = Digraph(\"CIFAR10_ResCNN\", format=\"png\")\ng.attr(rankdir=\"LR\", bgcolor=\"white\", pad=\"0.25\", splines=\"spline\",\n       ranksep=\"0.65\", nodesep=\"0.45\", dpi=\"300\")\ng.attr(\"node\", shape=\"box\", style=\"rounded,filled\",\n       color=PINK, fillcolor=LIGHTPINK, penwidth=\"2.2\",\n       fontname=FONT, fontsize=\"11\")\ng.attr(\"edge\", color=PINK, penwidth=\"1.8\", arrowsize=\"0.85\")\n\n# ----- Nodes & clusters -----\ng.node(\"inp\", \"Input\\n32×32×3\")\n\nwith g.subgraph(name=\"cluster_stem\") as c:\n    c.attr(label=\"Stem\", color=BORDER, penwidth=\"2\", style=\"rounded\", fontname=FONT)\n    c.node(\"stem\", \"Conv2d 3×3\\nBatchNorm2d, ReLU\\n→ 32×32×64\")\n\nwith g.subgraph(name=\"cluster_s1\") as c:\n    c.attr(label=\"Stage 1 (64)\", color=BORDER, penwidth=\"2\", style=\"rounded\", fontname=FONT)\n    c.node(\"s1b1\", \"Block 1\\nConv2d 3×3 → 3×3\")\n    c.node(\"plus1\", \"+\", shape=\"circle\", width=\"0.35\", height=\"0.35\",\n           style=\"filled\", fillcolor=LIGHTPINK, color=PINK, fontsize=\"10\")\n    c.node(\"s1b2\", \"Block 2\\nConv2d 3×3 → 3×3\")\n\nwith g.subgraph(name=\"cluster_s2\") as c:\n    c.attr(label=\"Stage 2 (128, stride 2)\", color=BORDER, penwidth=\"2\", style=\"rounded\", fontname=FONT)\n    c.node(\"s2b1\", \"Block 1\\nConv2d 3×3 → 3×3\")\n    c.node(\"plus2\", \"+\", shape=\"circle\", width=\"0.35\", height=\"0.35\",\n           style=\"filled\", fillcolor=LIGHTPINK, color=PINK, fontsize=\"10\")\n    c.node(\"s2b2\", \"Block 2\\nConv2d 3×3 → 3×3\")\n\nwith g.subgraph(name=\"cluster_s3\") as c:\n    c.attr(label=\"Stage 3 (256, stride 2)\", color=BORDER, penwidth=\"2\", style=\"rounded\", fontname=FONT)\n    c.node(\"s3b1\", \"Block 1\\nConv2d 3×3 → 3×3\")\n    c.node(\"plus3\", \"+\", shape=\"circle\", width=\"0.35\", height=\"0.35\",\n           style=\"filled\", fillcolor=LIGHTPINK, color=PINK, fontsize=\"10\")\n    c.node(\"s3b2\", \"Block 2\\nConv2d 3×3 → 3×3\")\n\ng.node(\"head\", \"GAP → Linear(256→10)\")\n\n# ----- Main forward path -----\ng.edges([\n    (\"inp\",\"stem\"),\n    (\"stem\",\"s1b1\"),\n    (\"s1b1\",\"s1b2\"),\n    (\"s1b2\",\"s2b1\"),\n    (\"s2b1\",\"s2b2\"),\n    (\"s2b2\",\"s3b1\"),\n    (\"s3b1\",\"s3b2\"),\n    (\"s3b2\",\"head\"),\n])\n\n# ----- Residual/skip visuals (dashed, non-constraining arcs) -----\ng.edge(\"stem\",\"plus1\", style=\"dashed\", xlabel=\"skip\", constraint=\"false\")\ng.edge(\"s1b1\",\"plus1\", constraint=\"false\")\ng.edge(\"plus1\",\"s1b2\", arrowhead=\"none\", constraint=\"false\")\n\ng.edge(\"s1b2\",\"plus2\", style=\"dashed\", xlabel=\"skip / 1×1 Conv2d\", constraint=\"false\")\ng.edge(\"s2b1\",\"plus2\", constraint=\"false\")\ng.edge(\"plus2\",\"s2b2\", arrowhead=\"none\", constraint=\"false\")\n\ng.edge(\"s2b2\",\"plus3\", style=\"dashed\", xlabel=\"skip / 1×1 Conv2d\", constraint=\"false\")\ng.edge(\"s3b1\",\"plus3\", constraint=\"false\")\ng.edge(\"plus3\",\"s3b2\", arrowhead=\"none\", constraint=\"false\")\n\n# ----- Render & preview -----\npng_path = g.render(filename=\"architecture_graphviz\")\ng.format = \"svg\"; svg_path = g.render(filename=\"architecture_graphviz\")\nprint(\"Saved:\", png_path, \"and\", svg_path)\ndisplay(Image(filename=png_path))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T15:56:39.947867Z","iopub.execute_input":"2025-10-01T15:56:39.948674Z","iopub.status.idle":"2025-10-01T15:56:45.551724Z","shell.execute_reply.started":"2025-10-01T15:56:39.948642Z","shell.execute_reply":"2025-10-01T15:56:45.550860Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Verify residual connections exist in each block\nfrom inspect import signature\n\ntotal = 0\nfor name, m in model.named_modules():\n    if m.__class__.__name__ == \"BasicBlock\":\n        total += 1\n        has_proj = getattr(m, \"down\", None) is not None\n        print(f\"{name}: residual add ✓  |  skip = {'1x1 Conv2d' if has_proj else 'identity'}\")\nprint(\"Total residual blocks:\", total)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T15:59:05.987884Z","iopub.execute_input":"2025-10-01T15:59:05.988187Z","iopub.status.idle":"2025-10-01T15:59:05.993946Z","shell.execute_reply.started":"2025-10-01T15:59:05.988163Z","shell.execute_reply":"2025-10-01T15:59:05.993326Z"}},"outputs":[],"execution_count":null}]}