{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":3649,"databundleVersionId":46718}],"dockerImageVersionId":31329,"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":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T03:49:24.051385Z","iopub.execute_input":"2026-04-12T03:49:24.051667Z","iopub.status.idle":"2026-04-12T03:49:24.058991Z","shell.execute_reply.started":"2026-04-12T03:49:24.051636Z","shell.execute_reply":"2026-04-12T03:49:24.058143Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Notebook 1 — Data & Standard Training\n\n**Goal:** Train a standard ResNet-18 on CIFAR-10 from scratch and save the best checkpoint.\n\n**Data source:** Kaggle competition dataset at `/kaggle/input/competitions/cifar-10/`\n- `train.7z` → 60,000 PNG images (32×32)\n- `trainLabels.csv` → image ID to class label mapping\n\n**Split:** 50,000 train / 5,000 test / 5,000 validation (stratified)\n\n**Environment:** Kaggle GPU (T4/P100) recommended.\n\n**Outputs:**\n- `outputs/checkpoints/standard_resnet18.pth` — best epoch weights\n- `outputs/training_curves/standard_train.csv` — per-epoch metrics\n\n**Expected result:** Clean test accuracy ≥ 90% (30 epochs on GPU)","metadata":{}},{"cell_type":"markdown","source":"---\n## Cell 1 — Imports & Config","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torchvision.transforms as transforms\nimport torchvision.models as models\nimport matplotlib.pyplot as plt\nimport pandas as pd\nimport numpy as np\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import train_test_split\nimport subprocess\nimport os\nimport time\nimport glob\n\n# ── Reproducibility ──\nSEED = 42\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\nnp.random.seed(SEED)\ntorch.backends.cudnn.deterministic = True\ntorch.backends.cudnn.benchmark = False\n\n# ── Hyperparameters ──\nBATCH_SIZE = 128\nEPOCHS = 30           # 20-40 epochs sufficient on GPU; targets ~90%+ clean accuracy\nLEARNING_RATE = 0.1\nMOMENTUM = 0.9\nWEIGHT_DECAY = 5e-4\n\n# ── Device ──\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# ── Kaggle input paths ──\nKAGGLE_INPUT = '/kaggle/input/competitions/cifar-10'\nTRAIN_ARCHIVE = f'{KAGGLE_INPUT}/train.7z'\nLABELS_CSV    = f'{KAGGLE_INPUT}/trainLabels.csv'\n\n# ── Extraction directory (writable area on Kaggle) ──\nEXTRACT_DIR = '/kaggle/working/cifar10_extracted'\nTRAIN_IMG_DIR = f'{EXTRACT_DIR}/train'\n\n# ── Output paths ──\nIS_KAGGLE = os.path.exists('/kaggle/working')\nOUTPUT_DIR = '/kaggle/working/outputs' if IS_KAGGLE else '../outputs'\n\nCHECKPOINT_DIR = f'{OUTPUT_DIR}/checkpoints'\nCURVES_DIR = f'{OUTPUT_DIR}/training_curves'\nCHECKPOINT_PATH = f'{CHECKPOINT_DIR}/standard_resnet18.pth'\nCURVES_PATH = f'{CURVES_DIR}/standard_train.csv'\n\nos.makedirs(CHECKPOINT_DIR, exist_ok=True)\nos.makedirs(CURVES_DIR, exist_ok=True)\n\nprint(f\"Training on: {DEVICE}\")\nprint(f\"Output directory: {OUTPUT_DIR}\")\nprint(f\"Kaggle environment: {IS_KAGGLE}\")","metadata":{"vscode":{"languageId":"markdown"},"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T03:49:27.655493Z","iopub.execute_input":"2026-04-12T03:49:27.656213Z","iopub.status.idle":"2026-04-12T03:49:27.668071Z","shell.execute_reply.started":"2026-04-12T03:49:27.656183Z","shell.execute_reply":"2026-04-12T03:49:27.667180Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## Cell 2 — Extract Dataset\n\nExtract `train.7z` from the Kaggle competition input. This produces individual PNG files (32×32) named `1.png`, `2.png`, ... inside the extraction directory.","metadata":{}},{"cell_type":"code","source":"# ── Extract train.7z if not already extracted ──\nos.makedirs(EXTRACT_DIR, exist_ok=True)\n\nif os.path.exists(TRAIN_IMG_DIR) and len(os.listdir(TRAIN_IMG_DIR)) > 0:\n    print(f\"Train images already extracted at {TRAIN_IMG_DIR}\")\nelse:\n    print(f\"Extracting {TRAIN_ARCHIVE} ...\")\n    result = subprocess.run(\n        ['7z', 'x', TRAIN_ARCHIVE, f'-o{EXTRACT_DIR}', '-y'],\n        capture_output=True, text=True\n    )\n    if result.returncode != 0:\n        print(f\"Error extracting: {result.stderr}\")\n        raise RuntimeError(\"Failed to extract train.7z\")\n    print(\"Extraction complete.\")\n\n# Verify extraction\nnum_images = len(glob.glob(f'{TRAIN_IMG_DIR}/*.png'))\nprint(f\"Total images found: {num_images:,}\")\n\n# Load labels CSV\nlabels_df = pd.read_csv(LABELS_CSV)\nprint(f\"Labels CSV shape: {labels_df.shape}\")\nprint(f\"\\nLabel distribution:\\n{labels_df['label'].value_counts().sort_index()}\")\nprint(f\"\\nSample rows:\")\nlabels_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T03:49:32.650861Z","iopub.execute_input":"2026-04-12T03:49:32.651261Z","iopub.status.idle":"2026-04-12T03:49:32.796407Z","shell.execute_reply.started":"2026-04-12T03:49:32.651232Z","shell.execute_reply":"2026-04-12T03:49:32.795702Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## Cell 3 — Custom Dataset & Stratified Split\n\n- Custom `CIFAR10Dataset` class that loads individual PNG files and maps text labels to integer indices.\n- **Stratified split:** 50,000 train / 5,000 test / 5,000 validation — preserves class balance across all splits.","metadata":{}},{"cell_type":"code","source":"# ── CIFAR-10 constants ──\nCIFAR10_MEAN = (0.4914, 0.4822, 0.4465)\nCIFAR10_STD  = (0.2023, 0.1994, 0.2010)\n\nCIFAR10_CLASSES = [\n    'airplane', 'automobile', 'bird', 'cat', 'deer',\n    'dog', 'frog', 'horse', 'ship', 'truck'\n]\nCLASS_TO_IDX = {cls: idx for idx, cls in enumerate(CIFAR10_CLASSES)}\n\n\nclass CIFAR10Dataset(Dataset):\n    \"\"\"\n    Custom PyTorch Dataset for CIFAR-10 PNG images from the Kaggle competition.\n    Each image is a 32x32 PNG file named by its ID (e.g., 1.png, 2.png, ...).\n    \"\"\"\n    def __init__(self, image_ids, labels, img_dir, transform=None):\n        self.image_ids = image_ids\n        self.labels = labels\n        self.img_dir = img_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.image_ids)\n\n    def __getitem__(self, idx):\n        img_id = self.image_ids[idx]\n        img_path = os.path.join(self.img_dir, f'{img_id}.png')\n        image = Image.open(img_path).convert('RGB')\n\n        if self.transform:\n            image = self.transform(image)\n\n        label = self.labels[idx]\n        return image, label\n\n\n# ── Prepare IDs and integer labels ──\nall_ids = labels_df['id'].values\nall_labels = labels_df['label'].map(CLASS_TO_IDX).values\n\nprint(f\"Total samples: {len(all_ids):,}\")\nprint(f\"Unique classes: {len(set(all_labels))}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T03:49:37.577104Z","iopub.execute_input":"2026-04-12T03:49:37.577893Z","iopub.status.idle":"2026-04-12T03:49:37.594473Z","shell.execute_reply.started":"2026-04-12T03:49:37.577858Z","shell.execute_reply":"2026-04-12T03:49:37.593504Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\n# Step 1: Split off 10,000 (val + test) from 50,000\ntrain_ids, temp_ids, train_labels, temp_labels = train_test_split(\n    all_ids, all_labels,\n    test_size=10000,   # 50k → 40k train + 10k temp\n    random_state=SEED,\n    stratify=all_labels\n)\n\n# Step 2: Split 10,000 → 5k val + 5k test\nval_ids, test_ids, val_labels, test_labels = train_test_split(\n    temp_ids, temp_labels,\n    test_size=5000,    # split evenly\n    random_state=SEED,\n    stratify=temp_labels\n)\n\nprint(f\"Train: {len(train_ids):,}  |  Val: {len(val_ids):,}  |  Test: {len(test_ids):,}\")\nprint(f\"Total: {len(train_ids) + len(val_ids) + len(test_ids):,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T03:49:39.908723Z","iopub.execute_input":"2026-04-12T03:49:39.909444Z","iopub.status.idle":"2026-04-12T03:49:39.940462Z","shell.execute_reply.started":"2026-04-12T03:49:39.909412Z","shell.execute_reply":"2026-04-12T03:49:39.939539Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## Cell 4 — DataLoaders\n\n- **Train:** Normalize + RandomCrop (padding=4) + RandomHorizontalFlip\n- **Test/Val:** Normalize only (no augmentation)","metadata":{}},{"cell_type":"code","source":"# ── Transforms ──\ntrain_transform = transforms.Compose([\n    transforms.RandomCrop(32, padding=4),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n])\n\ntest_transform = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n])\n\n# ── Datasets ──\ntrain_dataset = CIFAR10Dataset(train_ids, train_labels, TRAIN_IMG_DIR, transform=train_transform)\ntest_dataset  = CIFAR10Dataset(test_ids,  test_labels,  TRAIN_IMG_DIR, transform=test_transform)\nval_dataset   = CIFAR10Dataset(val_ids,   val_labels,   TRAIN_IMG_DIR, transform=test_transform)\n\n# ── DataLoaders ──\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True,  num_workers=2, pin_memory=True)\ntest_loader  = DataLoader(test_dataset,  batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_dataset,   batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\nprint(f\"Train:  {len(train_dataset):,} samples → {len(train_loader)} batches\")\nprint(f\"Test:   {len(test_dataset):,} samples → {len(test_loader)} batches\")\nprint(f\"Val:    {len(val_dataset):,} samples → {len(val_loader)} batches\")\nprint(f\"Batch size: {BATCH_SIZE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T03:49:41.785870Z","iopub.execute_input":"2026-04-12T03:49:41.786590Z","iopub.status.idle":"2026-04-12T03:49:41.795170Z","shell.execute_reply.started":"2026-04-12T03:49:41.786559Z","shell.execute_reply":"2026-04-12T03:49:41.794271Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Quick dataset preview","metadata":{}},{"cell_type":"code","source":"def denormalize(tensor, mean=CIFAR10_MEAN, std=CIFAR10_STD):\n    \"\"\"Reverse CIFAR-10 normalization for display.\"\"\"\n    mean = torch.tensor(mean).view(3, 1, 1)\n    std = torch.tensor(std).view(3, 1, 1)\n    return torch.clamp(tensor * std + mean, 0, 1)\n\n# Preview 16 images from the test set\npreview_loader = DataLoader(test_dataset, batch_size=16, shuffle=True)\nimages, labels = next(iter(preview_loader))\n\nfig, axes = plt.subplots(2, 8, figsize=(14, 5))\nfor i, ax in enumerate(axes.flat):\n    img = denormalize(images[i]).permute(1, 2, 0).numpy()\n    ax.imshow(img)\n    ax.set_title(CIFAR10_CLASSES[labels[i]], fontsize=9)\n    ax.axis('off')\nplt.suptitle('CIFAR-10 Sample Images (from test split)', fontsize=13, fontweight='bold')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T03:49:44.009821Z","iopub.execute_input":"2026-04-12T03:49:44.010125Z","iopub.status.idle":"2026-04-12T03:49:44.580760Z","shell.execute_reply.started":"2026-04-12T03:49:44.010100Z","shell.execute_reply":"2026-04-12T03:49:44.579992Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## Cell 5 — Model Definition\n\nResNet-18 adapted for CIFAR-10's 32×32 inputs:\n- **No pretrained weights** — trained from scratch on CIFAR-10\n- Replace `conv1` with a 3×3 kernel (stride=1) instead of 7×7 (designed for 224×224 ImageNet)\n- Remove `maxpool` (set to `nn.Identity()`) — the 32×32 images are too small for aggressive downsampling\n- Replace final FC layer: `model.fc = nn.Linear(512, 10)`\n\nThese modifications are standard practice for CIFAR-10 and improve clean accuracy by ~1-2%.","metadata":{}},{"cell_type":"code","source":"def create_resnet18_cifar10():\n    \"\"\"\n    Create a ResNet-18 model adapted for CIFAR-10 (32x32 images, 10 classes).\n    Modifications from standard ImageNet ResNet-18:\n      - conv1: 3x3 kernel, stride=1, padding=1 (instead of 7x7, stride=2)\n      - maxpool: removed (nn.Identity)\n      - fc: 512 -> 10 (instead of 512 -> 1000)\n    \"\"\"\n    model = models.resnet18(weights=None)  # train from scratch, no pretrained\n\n    # Replace first conv: 7x7 stride 2 → 3x3 stride 1 (for 32x32 inputs)\n    model.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)\n\n    # Remove maxpool — too aggressive for 32x32\n    model.maxpool = nn.Identity()\n\n    # Replace classifier head: 1000 → 10 classes\n    model.fc = nn.Linear(512, 10)\n\n    return model\n\nmodel = create_resnet18_cifar10().to(DEVICE)\n\n# Print model summary\ntotal_params = sum(p.numel() for p in model.parameters())\ntrainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f\"Model: ResNet-18 (CIFAR-10 adapted)\")\nprint(f\"Total parameters:     {total_params:,}\")\nprint(f\"Trainable parameters: {trainable_params:,}\")\nprint(f\"Device: {DEVICE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T03:49:47.766173Z","iopub.execute_input":"2026-04-12T03:49:47.766620Z","iopub.status.idle":"2026-04-12T03:49:47.940995Z","shell.execute_reply.started":"2026-04-12T03:49:47.766587Z","shell.execute_reply":"2026-04-12T03:49:47.940014Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## Cell 6 — Training Loop\n\n- **Optimizer:** SGD with momentum (0.9) and weight decay (5e-4)\n- **Scheduler:** CosineAnnealingLR — smoothly decays LR from 0.1 to near 0\n- **Checkpointing:** Save the best model by test accuracy (not just last epoch)\n- **Validation:** Used to monitor for overfitting (not used for model selection)","metadata":{}},{"cell_type":"code","source":"def train_one_epoch(model, loader, optimizer, criterion, device):\n    \"\"\"Train for one epoch. Returns (average_loss, accuracy%).\"\"\"\n    model.train()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n\n    for images, labels in loader:\n        images, labels = images.to(device), labels.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * images.size(0)\n        _, predicted = outputs.max(1)\n        total += labels.size(0)\n        correct += predicted.eq(labels).sum().item()\n\n    avg_loss = running_loss / total\n    accuracy = 100.0 * correct / total\n    return avg_loss, accuracy\n\n\ndef evaluate(model, loader, criterion, device):\n    \"\"\"Evaluate model on a dataset. Returns (average_loss, accuracy%).\"\"\"\n    model.eval()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n\n    with torch.no_grad():\n        for images, labels in loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            running_loss += loss.item() * images.size(0)\n            _, predicted = outputs.max(1)\n            total += labels.size(0)\n            correct += predicted.eq(labels).sum().item()\n\n    avg_loss = running_loss / total\n    accuracy = 100.0 * correct / total\n    return avg_loss, accuracy\n\n\nprint(\"Training and evaluation functions defined.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T03:50:26.189086Z","iopub.execute_input":"2026-04-12T03:50:26.189740Z","iopub.status.idle":"2026-04-12T03:50:26.198430Z","shell.execute_reply.started":"2026-04-12T03:50:26.189710Z","shell.execute_reply":"2026-04-12T03:50:26.197469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Setup ──\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.SGD(model.parameters(), lr=LEARNING_RATE, momentum=MOMENTUM, weight_decay=WEIGHT_DECAY)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\n\nhistory = {\n    'epoch': [], 'train_loss': [], 'train_acc': [],\n    'val_loss': [], 'val_acc': [], 'lr': []\n}\n\nbest_val_acc = 0.0\nstart_time   = time.time()\n\nprint(f\"Starting training for {EPOCHS} epochs...\")\nprint(f\"{'Epoch':>5} | {'Train Loss':>10} | {'Train Acc':>9} | {'Val Loss':>8} | {'Val Acc':>7} | {'LR':>8} | {'':>2}\")\nprint('-' * 75)\n\nfor epoch in range(1, EPOCHS + 1):\n    epoch_start = time.time()\n\n    train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, criterion, DEVICE)\n    val_loss,   val_acc   = evaluate(model, val_loader, criterion, DEVICE)  # ← val only\n\n    current_lr = optimizer.param_groups[0]['lr']\n    scheduler.step()\n\n    history['epoch'].append(epoch)\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    # Best checkpoint selected by val accuracy — test set never seen here\n    is_best = val_acc > best_val_acc\n    if is_best:\n        best_val_acc = val_acc\n        torch.save(model.state_dict(), CHECKPOINT_PATH)\n\n    elapsed = time.time() - epoch_start\n    print(f\"{epoch:5d} | {train_loss:10.4f} | {train_acc:8.2f}% | {val_loss:8.4f} | {val_acc:6.2f}% | {current_lr:.6f} | {elapsed:.1f}s\")\n\nprint(f\"\\nTraining complete in {(time.time()-start_time)/60:.1f} min\")\nprint(f\"Best val accuracy: {best_val_acc:.2f}%  (checkpoint saved)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T03:52:27.088682Z","iopub.execute_input":"2026-04-12T03:52:27.089209Z","iopub.status.idle":"2026-04-12T04:13:12.184122Z","shell.execute_reply.started":"2026-04-12T03:52:27.089173Z","shell.execute_reply":"2026-04-12T04:13:12.183050Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## Cell 7 — Save Checkpoint & Training Curves","metadata":{}},{"cell_type":"code","source":"# Save training curves to CSV\ndf_history = pd.DataFrame(history)\ndf_history.to_csv(CURVES_PATH, index=False)\n\nprint(f\"Best checkpoint saved: {best_acc:.2f}%\")\nprint(f\"  → {CHECKPOINT_PATH}\")\nprint(f\"Training curves saved:\")\nprint(f\"  → {CURVES_PATH}\")\nprint(f\"\\nTraining history (last 5 epochs):\")\nprint(df.tail().to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T04:20:53.704017Z","iopub.execute_input":"2026-04-12T04:20:53.704856Z","iopub.status.idle":"2026-04-12T04:20:53.715279Z","shell.execute_reply.started":"2026-04-12T04:20:53.704821Z","shell.execute_reply":"2026-04-12T04:20:53.714571Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## Cell 8 — Plot Training Curves","metadata":{}},{"cell_type":"code","source":"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n\nax1.plot(df_history['epoch'], df_history['train_loss'], 'b-', linewidth=2, label='Train Loss')\nax1.plot(df_history['epoch'], df_history['val_loss'],   'r-', linewidth=2, label='Val Loss')\nax1.set_xlabel('Epoch'); ax1.set_ylabel('Loss')\nax1.set_title('Loss Curves', fontweight='bold')\nax1.legend(); ax1.grid(True, alpha=0.3); ax1.set_xlim(1, EPOCHS)\n\nax2.plot(df_history['epoch'], df_history['train_acc'], 'b-', linewidth=2, label='Train Acc')\nax2.plot(df_history['epoch'], df_history['val_acc'],   'r-', linewidth=2, label='Val Acc')\nax2.axhline(y=best_val_acc, color='green', linestyle='--', alpha=0.6, label=f'Best Val: {best_val_acc:.2f}%')\nax2.set_xlabel('Epoch'); ax2.set_ylabel('Accuracy (%)')\nax2.set_title('Accuracy Curves', fontweight='bold')\nax2.legend(); ax2.grid(True, alpha=0.3); ax2.set_xlim(1, EPOCHS); ax2.set_ylim(0, 100)\n\nplt.suptitle('Standard ResNet-18 — CIFAR-10 Training', fontsize=14, fontweight='bold', y=1.02)\nplt.tight_layout()\nplt.savefig(f'{CURVES_DIR}/standard_train_plot.png', dpi=150, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T04:20:57.409521Z","iopub.execute_input":"2026-04-12T04:20:57.409944Z","iopub.status.idle":"2026-04-12T04:20:58.080081Z","shell.execute_reply.started":"2026-04-12T04:20:57.409914Z","shell.execute_reply":"2026-04-12T04:20:58.079447Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## Cell 9 — Final Evaluation\n\nLoad the best checkpoint and confirm final test accuracy + per-class breakdown.","metadata":{}},{"cell_type":"code","source":"best_model = create_resnet18_cifar10().to(DEVICE)\nbest_model.load_state_dict(torch.load(CHECKPOINT_PATH, map_location=DEVICE, weights_only=True))\nbest_model.eval()\n\nval_loss_final,  val_acc_final  = evaluate(best_model, val_loader,  criterion, DEVICE)\ntest_loss_final, test_acc_final = evaluate(best_model, test_loader, criterion, DEVICE)\n\nprint(\"=\" * 52)\nprint(\"  Standard Model — Final Evaluation\")\nprint(\"=\" * 52)\nprint(f\"  Val accuracy  (checkpoint basis) : {val_acc_final:.2f}%\")\nprint(f\"  Test accuracy (final result)     : {test_acc_final:.2f}%\")\nprint(f\"  Expected range                   : 90–93%\")\nprint(\"=\" * 52)\n\n# Per-class on test set\nclass_correct = [0] * 10\nclass_total   = [0] * 10\nwith torch.no_grad():\n    for images, labels in test_loader:\n        images, labels = images.to(DEVICE), labels.to(DEVICE)\n        _, predicted = best_model(images).max(1)\n        for i in range(labels.size(0)):\n            class_correct[labels[i]] += (predicted[i] == labels[i]).item()\n            class_total[labels[i]]   += 1\n\nprint(f\"\\nPer-class accuracy (test set):\")\nprint(f\"{'Class':<12} | {'Correct':>7} / {'Total':>5} | {'Acc':>7}\")\nprint('-' * 38)\nfor i in range(10):\n    acc = 100.0 * class_correct[i] / class_total[i]\n    print(f\"{CIFAR10_CLASSES[i]:<12} | {class_correct[i]:>7} / {class_total[i]:>5} | {acc:>6.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T04:21:11.158608Z","iopub.execute_input":"2026-04-12T04:21:11.159287Z","iopub.status.idle":"2026-04-12T04:21:16.379418Z","shell.execute_reply.started":"2026-04-12T04:21:11.159254Z","shell.execute_reply":"2026-04-12T04:21:16.378482Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## Summary\n\n| Item | Value |\n|------|-------|\n| Model | ResNet-18 (CIFAR-10 adapted) |\n| Dataset | Kaggle CIFAR-10 competition (60K images) |\n| Split | 50K train / 5K test / 5K val (stratified) |\n| Epochs | 30 |\n| Optimizer | SGD (lr=0.1, momentum=0.9, wd=5e-4) |\n| Scheduler | CosineAnnealingLR |\n| Best checkpoint | `outputs/checkpoints/standard_resnet18.pth` |\n| Training curves | `outputs/training_curves/standard_train.csv` |\n\n**Next step:** Run `02_attack_engine.ipynb` to attack this standard model.","metadata":{}},{"cell_type":"code","source":"import os\nprint(os.path.exists(CHECKPOINT_PATH))          # should print True\nprint(os.path.getsize(CHECKPOINT_PATH) / 1e6,  'MB')   # should be ~45MB","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T04:25:36.531830Z","iopub.execute_input":"2026-04-12T04:25:36.532173Z","iopub.status.idle":"2026-04-12T04:25:36.537889Z","shell.execute_reply.started":"2026-04-12T04:25:36.532141Z","shell.execute_reply":"2026-04-12T04:25:36.537105Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nshutil.copy(CHECKPOINT_PATH, '/kaggle/working/standard_resnet18.pth')\n\nfrom IPython.display import FileLink\nFileLink('/kaggle/working/standard_resnet18.pth')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-12T04:27:19.683543Z","iopub.execute_input":"2026-04-12T04:27:19.684521Z","iopub.status.idle":"2026-04-12T04:27:19.728690Z","shell.execute_reply.started":"2026-04-12T04:27:19.684488Z","shell.execute_reply":"2026-04-12T04:27:19.727980Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}