{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# 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\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\n# Extract train images\nos.system(\"7z x /kaggle/input/competitions/cifar-10/train.7z -o/kaggle/working/\")\n\n# Verify extraction\nprint(os.listdir(\"/kaggle/working/train\")[:5])  # should show image filenames like 1.png, 2.png...","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-29T08:01:41.000562Z","iopub.execute_input":"2026-06-29T08:01:41.000819Z","iopub.status.idle":"2026-06-29T08:04:06.063185Z","shell.execute_reply.started":"2026-06-29T08:01:41.000795Z","shell.execute_reply":"2026-06-29T08:04:06.062532Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, classification_report\nimport seaborn as sns\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torchvision.transforms as transforms\nfrom torch.utils.data import Dataset, DataLoader","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS      = 20\nBATCH_SIZE  = 64\nRANDOM_SEED = 42\nDEVICE      = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nTRAIN_IMG_DIR   = \"/kaggle/working/train\"\nTRAIN_LABEL_CSV = \"/kaggle/input/competitions/cifar-10/trainLabels.csv\"\n\ntorch.manual_seed(RANDOM_SEED)\nnp.random.seed(RANDOM_SEED)\n\nprint(f\"Device : {DEVICE}\")\nprint(f\"Epochs : {EPOCHS}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class KaggleCIFAR10(Dataset):\n    def __init__(self, image_dir, labels_df, transform=None):\n        self.image_dir  = image_dir\n        self.labels_df  = labels_df.reset_index(drop=True)\n        self.transform  = transform\n        self.class_to_idx = {\n            c: i for i, c in enumerate(sorted(labels_df['label'].unique()))\n        }\n        self.idx_to_class = {v: k for k, v in self.class_to_idx.items()}\n\n    def __len__(self):\n        return len(self.labels_df)\n\n    def __getitem__(self, idx):\n        img_id = self.labels_df.loc[idx, 'id']\n        label  = self.labels_df.loc[idx, 'label']\n        image  = Image.open(os.path.join(self.image_dir, f\"{img_id}.png\")).convert(\"RGB\")\n        if self.transform:\n            image = self.transform(image)\n        return image, self.class_to_idx[label]\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---- Transforms ----\ntrain_transform = transforms.Compose([\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomCrop(32, padding=4),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize((0.4914, 0.4822, 0.4465),\n                         (0.2470, 0.2435, 0.2616))   # CIFAR-10 channel stats\n])\n\neval_transform = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize((0.4914, 0.4822, 0.4465),\n                         (0.2470, 0.2435, 0.2616))\n])\n\n# ---- Load & Split Labels ----\nlabels_df = pd.read_csv(TRAIN_LABEL_CSV)\n\n# Step 1: carve out test 15%\ntrain_val_df, test_df = train_test_split(\n    labels_df, test_size=0.15,\n    random_state=RANDOM_SEED, stratify=labels_df['label']\n)\n\n# Step 2: split remaining into train 70% / val 15%\ntrain_df, val_df = train_test_split(\n    train_val_df, test_size=0.176,       # 0.176 * 0.85 ≈ 0.15\n    random_state=RANDOM_SEED, stratify=train_val_df['label']\n)\n\ntrain_df = train_df.reset_index(drop=True)\nval_df   = val_df.reset_index(drop=True)\ntest_df  = test_df.reset_index(drop=True)\n\nprint(f\"\\nSplit Summary\")\nprint(f\"  Train : {len(train_df):>6}  ({len(train_df)/len(labels_df)*100:.1f}%)\")\nprint(f\"  Val   : {len(val_df):>6}  ({len(val_df)/len(labels_df)*100:.1f}%)\")\nprint(f\"  Test  : {len(test_df):>6}  ({len(test_df)/len(labels_df)*100:.1f}%)\")\n\n# ---- Verify class balance ----\nprint(\"\\nClass distribution (counts per split):\")\ndist = pd.DataFrame({\n    'Train' : train_df['label'].value_counts().sort_index(),\n    'Val'   : val_df['label'].value_counts().sort_index(),\n    'Test'  : test_df['label'].value_counts().sort_index(),\n})\nprint(dist)\n\n# ---- DataLoaders ----\ntrain_dataset = KaggleCIFAR10(TRAIN_IMG_DIR, train_df, transform=train_transform)\nval_dataset   = KaggleCIFAR10(TRAIN_IMG_DIR, val_df,   transform=eval_transform)\ntest_dataset  = KaggleCIFAR10(TRAIN_IMG_DIR, test_df,  transform=eval_transform)\n\nCLASS_NAMES = list(train_dataset.class_to_idx.keys())   # sorted alphabetically\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True,  num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_dataset,   batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\ntest_loader  = DataLoader(test_dataset,  batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 2. CNN MODEL\n# ============================================================\n\nclass SimpleCNN(nn.Module):\n    def __init__(self, num_classes=10):\n        super().__init__()\n\n        self.features = nn.Sequential(\n            # Block 1\n            nn.Conv2d(3, 32, kernel_size=3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(32, 32, kernel_size=3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2, 2),          # 32x32 -> 16x16\n            nn.Dropout2d(0.2),\n\n            # Block 2\n            nn.Conv2d(32, 64, kernel_size=3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(64, 64, kernel_size=3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2, 2),          # 16x16 -> 8x8\n            nn.Dropout2d(0.3),\n\n            # Block 3\n            nn.Conv2d(64, 128, kernel_size=3, padding=1),\n            nn.BatchNorm2d(128),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(128, 128, kernel_size=3, padding=1),\n            nn.BatchNorm2d(128),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2, 2),          # 8x8 -> 4x4\n            nn.Dropout2d(0.4),\n        )\n\n        self.classifier = nn.Sequential(\n            nn.Flatten(),\n            nn.Linear(128 * 4 * 4, 512),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.5),\n            nn.Linear(512, num_classes)\n        )\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.classifier(x)\n        return x\n\n\ndef count_parameters(model):\n    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n\n# Quick sanity check\n_m = SimpleCNN()\nprint(f\"\\nModel Parameters: {count_parameters(_m):,}\")\nprint(f\"Output shape test: {_m(torch.randn(2, 3, 32, 32)).shape}\")  # should be [2, 10]","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 3. TRAIN / EVAL FUNCTIONS\n# ============================================================\n\ndef train_one_epoch(model, loader, optimizer, criterion):\n    model.train()\n    total_loss, correct, total = 0.0, 0, 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        total_loss += loss.item()\n        correct    += outputs.argmax(1).eq(labels).sum().item()\n        total      += labels.size(0)\n\n    return total_loss / len(loader), 100.0 * correct / total\n\n\ndef evaluate(model, loader, criterion):\n    model.eval()\n    total_loss, correct, total = 0.0, 0, 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            total_loss += loss.item()\n            correct    += outputs.argmax(1).eq(labels).sum().item()\n            total      += labels.size(0)\n\n    return total_loss / len(loader), 100.0 * correct / total\n\n\ndef get_all_preds_and_labels(model, loader):\n    model.eval()\n    all_preds, all_labels = [], []\n    with torch.no_grad():\n        for images, labels in loader:\n            images = images.to(DEVICE)\n            preds  = model(images).argmax(1).cpu().numpy()\n            all_preds.extend(preds)\n            all_labels.extend(labels.numpy())\n    return np.array(all_labels), np.array(all_preds)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 4. EXPERIMENT DEFINITIONS\n# ============================================================\n\nexperiments = [\n    {\n        \"name\"        : \"SGD + CrossEntropy\",\n        \"optimizer_fn\": lambda p: optim.SGD(p, lr=0.01, momentum=0.9, weight_decay=1e-4),\n        \"criterion\"   : nn.CrossEntropyLoss(),\n        \"scheduler_fn\": lambda o: optim.lr_scheduler.StepLR(o, step_size=7, gamma=0.5),\n    },\n    {\n        \"name\"        : \"Adam + CrossEntropy\",\n        \"optimizer_fn\": lambda p: optim.Adam(p, lr=0.001, weight_decay=1e-4),\n        \"criterion\"   : nn.CrossEntropyLoss(),\n        \"scheduler_fn\": lambda o: optim.lr_scheduler.StepLR(o, step_size=7, gamma=0.5),\n    },\n    {\n        \"name\"        : \"RMSprop + CrossEntropy\",\n        \"optimizer_fn\": lambda p: optim.RMSprop(p, lr=0.001, weight_decay=1e-4),\n        \"criterion\"   : nn.CrossEntropyLoss(),\n        \"scheduler_fn\": lambda o: optim.lr_scheduler.StepLR(o, step_size=7, gamma=0.5),\n    },\n    {\n        \"name\"        : \"Adam + LabelSmoothing\",\n        \"optimizer_fn\": lambda p: optim.Adam(p, lr=0.001, weight_decay=1e-4),\n        \"criterion\"   : nn.CrossEntropyLoss(label_smoothing=0.1),\n        \"scheduler_fn\": lambda o: optim.lr_scheduler.StepLR(o, step_size=7, gamma=0.5),\n    },\n    {\n        \"name\"        : \"AdamW + CosineAnnealing\",\n        \"optimizer_fn\": lambda p: optim.AdamW(p, lr=0.001, weight_decay=1e-3),\n        \"criterion\"   : nn.CrossEntropyLoss(),\n        \"scheduler_fn\": lambda o: optim.lr_scheduler.CosineAnnealingLR(o, T_max=EPOCHS),\n    },\n]","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 5. RUN ALL EXPERIMENTS\n# ============================================================\n\nall_results = {}\n\nfor exp in experiments:\n    print(f\"\\n{'='*60}\")\n    print(f\"  {exp['name']}\")\n    print(f\"{'='*60}\")\n\n    model     = SimpleCNN().to(DEVICE)\n    optimizer = exp[\"optimizer_fn\"](model.parameters())\n    criterion = exp[\"criterion\"]\n    scheduler = exp[\"scheduler_fn\"](optimizer)\n\n    train_losses, val_losses   = [], []\n    train_accs,   val_accs     = [], []\n    lr_history                 = []\n\n    best_val_loss  = float('inf')\n    best_model_wts = None\n    patience_count = 0\n    PATIENCE       = 5          # early stopping patience\n\n    for epoch in range(1, EPOCHS + 1):\n        tr_loss, tr_acc   = train_one_epoch(model, train_loader, optimizer, criterion)\n        val_loss, val_acc = evaluate(model, val_loader, criterion)\n        scheduler.step()\n\n        train_losses.append(tr_loss)\n        val_losses.append(val_loss)\n        train_accs.append(tr_acc)\n        val_accs.append(val_acc)\n        lr_history.append(optimizer.param_groups[0]['lr'])\n\n        # Save best model weights\n        if val_loss < best_val_loss:\n            best_val_loss  = val_loss\n            best_model_wts = {k: v.clone() for k, v in model.state_dict().items()}\n            patience_count = 0\n        else:\n            patience_count += 1\n\n        if epoch % 5 == 0 or epoch == 1:\n            print(f\"  Epoch {epoch:02d}/{EPOCHS} | \"\n                  f\"Train Loss: {tr_loss:.4f}  Acc: {tr_acc:.1f}% | \"\n                  f\"Val Loss: {val_loss:.4f}  Acc: {val_acc:.1f}% | \"\n                  f\"LR: {lr_history[-1]:.6f}\")\n\n        # Early stopping\n        if patience_count >= PATIENCE:\n            print(f\"  Early stopping at epoch {epoch} (no val improvement for {PATIENCE} epochs)\")\n            break\n\n    # Load best weights back\n    model.load_state_dict(best_model_wts)\n\n    # Get predictions on test set (touch only once per experiment — for final report)\n    true_labels, pred_labels = get_all_preds_and_labels(model, test_loader)\n    test_acc = 100.0 * (true_labels == pred_labels).mean()\n\n    all_results[exp[\"name\"]] = {\n        \"train_losses\" : train_losses,\n        \"val_losses\"   : val_losses,\n        \"train_accs\"   : train_accs,\n        \"val_accs\"     : val_accs,\n        \"lr_history\"   : lr_history,\n        \"best_val_loss\": best_val_loss,\n        \"test_acc\"     : test_acc,\n        \"true_labels\"  : true_labels,\n        \"pred_labels\"  : pred_labels,\n        \"model\"        : model,\n    }\n\n    print(f\"\\n  Best Val Loss : {best_val_loss:.4f}\")\n    print(f\"  Test Accuracy : {test_acc:.2f}%\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 6. PLOTS\n# ============================================================\n\n# ---- 6a. Loss & Accuracy Curves per experiment ----\nn = len(all_results)\nfig, axes = plt.subplots(n, 3, figsize=(18, 4.5 * n))\nfig.suptitle(\"CIFAR-10 CNN — Experiment Comparison\", fontsize=15, fontweight='bold', y=1.01)\n\nfor i, (name, res) in enumerate(all_results.items()):\n    ep = range(1, len(res[\"train_losses\"]) + 1)\n\n    # Loss\n    ax = axes[i][0]\n    ax.plot(ep, res[\"train_losses\"], label=\"Train Loss\", color=\"steelblue\",  linewidth=2)\n    ax.plot(ep, res[\"val_losses\"],   label=\"Val Loss\",   color=\"tomato\",     linewidth=2, linestyle=\"--\")\n    ax.set_title(f\"{name}\\nLoss Curve\", fontsize=10)\n    ax.set_xlabel(\"Epoch\"); ax.set_ylabel(\"Loss\")\n    ax.legend(); ax.grid(True, alpha=0.3)\n\n    # Accuracy\n    ax = axes[i][1]\n    ax.plot(ep, res[\"train_accs\"], label=\"Train Acc\", color=\"steelblue\", linewidth=2)\n    ax.plot(ep, res[\"val_accs\"],   label=\"Val Acc\",   color=\"tomato\",    linewidth=2, linestyle=\"--\")\n    ax.set_title(f\"{name}\\nAccuracy  (Test: {res['test_acc']:.1f}%)\", fontsize=10)\n    ax.set_xlabel(\"Epoch\"); ax.set_ylabel(\"Accuracy (%)\")\n    ax.legend(); ax.grid(True, alpha=0.3)\n\n    # LR schedule\n    ax = axes[i][2]\n    ax.plot(ep, res[\"lr_history\"], color=\"darkorange\", linewidth=2)\n    ax.set_title(f\"{name}\\nLR Schedule\", fontsize=10)\n    ax.set_xlabel(\"Epoch\"); ax.set_ylabel(\"Learning Rate\")\n    ax.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.savefig(\"01_loss_accuracy_curves.png\", dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"Saved: 01_loss_accuracy_curves.png\")\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---- 6b. Overfitting Gap plot ----\nfig, ax = plt.subplots(figsize=(12, 5))\ncolors = ['steelblue', 'tomato', 'green', 'purple', 'orange']\n\nfor (name, res), color in zip(all_results.items(), colors):\n    ep  = range(1, len(res[\"train_accs\"]) + 1)\n    gap = [tr - v for tr, v in zip(res[\"train_accs\"], res[\"val_accs\"])]\n    ax.plot(ep, gap, label=name, color=color, linewidth=2)\n\nax.axhline(0, color='black', linestyle='--', linewidth=1)\nax.set_title(\"Overfitting Gap  (Train Acc − Val Acc)\\nHigher = more overfitting\", fontsize=12)\nax.set_xlabel(\"Epoch\"); ax.set_ylabel(\"Gap (%)\")\nax.legend(); ax.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.savefig(\"02_overfitting_gap.png\", dpi=150)\nplt.show()\nprint(\"Saved: 02_overfitting_gap.png\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---- 6c. Final summary bar chart ----\nnames     = list(all_results.keys())\ntest_accs = [all_results[n][\"test_acc\"] for n in names]\nval_accs_final = [all_results[n][\"val_accs\"][-1] for n in names]\n\nx = np.arange(len(names))\nwidth = 0.35\n\nfig, ax = plt.subplots(figsize=(13, 5))\nbars1 = ax.bar(x - width/2, val_accs_final, width, label='Final Val Acc',  color='steelblue', alpha=0.85)\nbars2 = ax.bar(x + width/2, test_accs,      width, label='Test Acc',       color='tomato',    alpha=0.85)\n\nax.set_xticks(x)\nax.set_xticklabels(names, rotation=15, ha='right', fontsize=9)\nax.set_ylabel(\"Accuracy (%)\")\nax.set_title(\"Final Val vs Test Accuracy — All Experiments\")\nax.legend(); ax.grid(True, alpha=0.3, axis='y')\n\nfor bar in bars1:\n    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,\n            f'{bar.get_height():.1f}', ha='center', va='bottom', fontsize=8)\nfor bar in bars2:\n    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,\n            f'{bar.get_height():.1f}', ha='center', va='bottom', fontsize=8)\n\nplt.tight_layout()\nplt.savefig(\"03_summary_bar.png\", dpi=150)\nplt.show()\nprint(\"Saved: 03_summary_bar.png\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ---- 6d. Confusion Matrix for best experiment ----\nbest_exp_name = max(all_results, key=lambda k: all_results[k][\"test_acc\"])\nres = all_results[best_exp_name]\n\ncm = confusion_matrix(res[\"true_labels\"], res[\"pred_labels\"])\ncm_norm = cm.astype(float) / cm.sum(axis=1, keepdims=True)   # row-normalize\n\nfig, axes = plt.subplots(1, 2, figsize=(16, 6))\nfig.suptitle(f\"Confusion Matrix — Best Experiment: {best_exp_name}\", fontsize=12, fontweight='bold')\n\n# Raw counts\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES, ax=axes[0])\naxes[0].set_title(\"Raw Counts\")\naxes[0].set_xlabel(\"Predicted\"); axes[0].set_ylabel(\"True\")\n\n# Normalized\nsns.heatmap(cm_norm, annot=True, fmt='.2f', cmap='Blues',\n            xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES, ax=axes[1])\naxes[1].set_title(\"Normalized (Recall per class)\")\naxes[1].set_xlabel(\"Predicted\"); axes[1].set_ylabel(\"True\")\n\nplt.tight_layout()\nplt.savefig(\"04_confusion_matrix.png\", dpi=150)\nplt.show()\nprint(\"Saved: 04_confusion_matrix.png\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---- 6e. Per-class accuracy bar ----\nper_class_acc = cm_norm.diagonal() * 100\n\nfig, ax = plt.subplots(figsize=(10, 4))\nbars = ax.bar(CLASS_NAMES, per_class_acc, color='steelblue', alpha=0.85)\nax.axhline(per_class_acc.mean(), color='tomato', linestyle='--', linewidth=2, label=f'Mean: {per_class_acc.mean():.1f}%')\nax.set_title(f\"Per-class Accuracy — {best_exp_name}\")\nax.set_ylabel(\"Accuracy (%)\"); ax.set_ylim(0, 100)\nax.legend(); ax.grid(True, alpha=0.3, axis='y')\n\nfor bar, acc in zip(bars, per_class_acc):\n    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,\n            f'{acc:.1f}', ha='center', va='bottom', fontsize=8)\n\nplt.tight_layout()\nplt.savefig(\"05_per_class_accuracy.png\", dpi=150)\nplt.show()\nprint(\"Saved: 05_per_class_accuracy.png\")\n\n\n# ---- 6f. Classification Report ----\nprint(f\"\\n{'='*60}\")\nprint(f\"Classification Report — {best_exp_name}\")\nprint(f\"{'='*60}\")\nprint(classification_report(res[\"true_labels\"], res[\"pred_labels\"], target_names=CLASS_NAMES))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ============================================================\n# 7. FINAL SUMMARY TABLE\n# ============================================================\n\nprint(f\"\\n{'='*60}\")\nprint(\"FINAL SUMMARY — ALL EXPERIMENTS\")\nprint(f\"{'='*60}\")\nprint(f\"{'Experiment':<35} {'Best Val Loss':>13} {'Val Acc':>9} {'Test Acc':>9}\")\nprint(\"-\" * 70)\nfor name, res in all_results.items():\n    print(f\"{name:<35} {res['best_val_loss']:>13.4f} \"\n          f\"{res['val_accs'][-1]:>8.1f}% \"\n          f\"{res['test_acc']:>8.1f}%\")\nprint(f\"\\nBest overall: {best_exp_name}  ({all_results[best_exp_name]['test_acc']:.2f}% test acc)\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}