{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# ResNet-18 on ImageNet-1k — Continued training with CosineAnnealingLR (Kaggle, 2x GPU)\nContinues from the `best.pt` produced by the earlier OneCycleLR run (5 epochs), for 5 more\nepochs with a fresh AdamW optimizer and `CosineAnnealingLR`. Same memory/multi-GPU setup as\nbefore (uint8->GPU normalisation, fp16 autocast, channels_last, DataParallel), plus TensorBoard.\n\n**Kaggle setup:** Accelerator = GPU T4 x2. Inputs needed:\n- Competition: `imagenet-object-localization-challenge`\n- Notebook output: `imagenet-classification-v1` (must contain `best.pt` and `file_index.npz`)","metadata":{}},{"cell_type":"code","source":"import os, time, random\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torch.utils.tensorboard import SummaryWriter","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-30T21:02:40.454553Z","iopub.execute_input":"2026-09-30T21:02:40.454996Z","iopub.status.idle":"2026-09-30T21:02:40.657818Z","shell.execute_reply.started":"2026-09-30T21:02:40.454958Z","shell.execute_reply":"2026-09-30T21:02:40.657109Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CONFIG\n# ============================================================\nSEED           = 42\nBATCH_SIZE     = 256\nNUM_EPOCHS     = 5\nNUM_WORKERS    = 4\nVAL_PER_CLASS  = 10\nLR             = 1e-4          # lower LR since we're continuing from a trained checkpoint\nETA_MIN        = 1e-6\nWEIGHT_DECAY   = 0.02\nIMG_SIZE       = 224\nDEBUG_SUBSET   = None           # e.g. 5000 for a smoke test; None = full data\n\n# ---- hardcoded paths (fixed for this competition/notebook setup - no scanning) ----\nCOMP_DIR   = \"/kaggle/input/competitions/imagenet-object-localization-challenge\"\nROOT       = COMP_DIR\nTRAIN_DIR  = os.path.join(ROOT, \"ILSVRC\", \"Data\", \"CLS-LOC\", \"train\")\nVAL_DIR    = os.path.join(ROOT, \"ILSVRC\", \"Data\", \"CLS-LOC\", \"val\")\nSYNSET_TXT = os.path.join(ROOT, \"LOC_synset_mapping.txt\")\nVAL_CSV    = os.path.join(ROOT, \"LOC_val_solution.csv\")\n\nPREV_RUN_DIR   = \"/kaggle/input/notebooks/jhaipranesh/imagenet-classification-v1\"\nPREV_BEST_PT   = os.path.join(PREV_RUN_DIR, \"best.pt\")\nPREV_INDEX_NPZ = os.path.join(PREV_RUN_DIR, \"file_index.npz\")\n\nCKPT_DIR    = \"/kaggle/working\"\nLAST_PATH   = os.path.join(CKPT_DIR, \"last_cosine.pt\")\nBEST_PATH   = os.path.join(CKPT_DIR, \"best_cosine.pt\")\nTB_LOGDIR   = os.path.join(CKPT_DIR, \"runs\", \"cosine_run\")\n\nrandom.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)\ntorch.backends.cudnn.benchmark = True\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nNUM_GPUS = torch.cuda.device_count()\nprint(\"device:\", device, \"| GPUs:\", NUM_GPUS)\nfor i in range(NUM_GPUS):\n    print(f\"  GPU {i}: {torch.cuda.get_device_name(i)}\")\n\nwriter = SummaryWriter(TB_LOGDIR)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CLASSES (1000 synsets, sorted order = label index)\n# ============================================================\nsynsets, class_names = [], []\nwith open(SYNSET_TXT) as f:\n    for line in f:\n        wnid, name = line.strip().split(\" \", 1)\n        synsets.append(wnid)\n        class_names.append(name.split(\",\")[0])\nwnid_to_idx = {w: i for i, w in enumerate(synsets)}\nNUM_CLASSES = len(synsets)\nprint(NUM_CLASSES, \"classes | e.g.\", class_names[:3])","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# FILE INDEX  — reused directly from the previous run, no rescanning\n# ============================================================\nz = np.load(PREV_INDEX_NPZ)\nall_train_files, all_train_labels = z[\"files\"], z[\"labels\"]\nprint(\"train images (from previous run's index):\", len(all_train_files))\n\n# same seed -> identical held-out validation split as the first run\nrng = np.random.default_rng(SEED)\nis_val = np.zeros(len(all_train_labels), dtype=bool)\nfor c in range(NUM_CLASSES):\n    idxs = np.flatnonzero(all_train_labels == c)\n    is_val[rng.choice(idxs, size=VAL_PER_CLASS, replace=False)] = True\n\ntrain_files, train_labels = all_train_files[~is_val], all_train_labels[~is_val]\nval_files,   val_labels   = all_train_files[is_val],  all_train_labels[is_val]\ndel all_train_files, all_train_labels, is_val\n\n# ---- TEST = official ImageNet validation set (50k) ----\ndf = pd.read_csv(VAL_CSV)\ndf[\"label\"] = df[\"PredictionString\"].str.split(\" \").str[0].map(wnid_to_idx)\ndf = df.sort_values(\"ImageId\").reset_index(drop=True)\ntest_files  = (df[\"ImageId\"] + \".JPEG\").to_numpy().astype(\"S\")\ntest_labels = df[\"label\"].to_numpy().astype(np.int16)\ndel df\n\nif DEBUG_SUBSET:\n    p = np.random.default_rng(0).permutation(len(train_files))[:DEBUG_SUBSET]\n    train_files, train_labels = train_files[p], train_labels[p]\n    val_files, val_labels   = val_files[:DEBUG_SUBSET // 5], val_labels[:DEBUG_SUBSET // 5]\n    test_files, test_labels = test_files[:DEBUG_SUBSET // 5], test_labels[:DEBUG_SUBSET // 5]\n\nprint(f\"train {len(train_files):,} | val {len(val_files):,} | test {len(test_files):,}\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# TRANSFORMS  (output uint8 tensors; normalisation is done on GPU)\n# ============================================================\ntrain_transform = transforms.Compose([\n    transforms.RandomResizedCrop(IMG_SIZE),\n    transforms.RandomHorizontalFlip(),\n    transforms.PILToTensor(),\n])\n\neval_transform = transforms.Compose([\n    transforms.Resize(256),\n    transforms.CenterCrop(IMG_SIZE),\n    transforms.PILToTensor(),\n])\n\nMEAN = torch.tensor([0.485, 0.456, 0.406], device=device).view(1, 3, 1, 1) * 255\nSTD  = torch.tensor([0.229, 0.224, 0.225], device=device).view(1, 3, 1, 1) * 255\n\ndef prep(images):\n    x = images.to(device, non_blocking=True).float()\n    x = (x - MEAN) / STD\n    return x.contiguous(memory_format=torch.channels_last)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# DATASET  (lazy: one image decoded per __getitem__)\n# ============================================================\nclass ImageNetFiles(Dataset):\n    def __init__(self, folder, files, labels, transform, per_class_dir, draft_size):\n        self.folder, self.files, self.labels = folder, files, labels\n        self.transform, self.per_class_dir, self.draft = transform, per_class_dir, draft_size\n\n    def __len__(self):\n        return len(self.files)\n\n    def __getitem__(self, i):\n        label = int(self.labels[i])\n        fname = self.files[i].decode()\n        d = os.path.join(self.folder, synsets[label]) if self.per_class_dir else self.folder\n        with Image.open(os.path.join(d, fname)) as img:\n            img.draft(\"RGB\", (self.draft, self.draft))\n            img = img.convert(\"RGB\")\n        return self.transform(img), label\n\ntrain_dataset = ImageNetFiles(TRAIN_DIR, train_files, train_labels, train_transform, True,  320)\nval_dataset   = ImageNetFiles(TRAIN_DIR, val_files,   val_labels,   eval_transform,  True,  256)\ntest_dataset  = ImageNetFiles(VAL_DIR,   test_files,  test_labels,  eval_transform,  False, 256)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(\n    train_dataset, batch_size=BATCH_SIZE, shuffle=True, drop_last=True,\n    num_workers=NUM_WORKERS, pin_memory=True,\n    persistent_workers=True, prefetch_factor=2,\n)\nval_loader = DataLoader(\n    val_dataset, batch_size=BATCH_SIZE, shuffle=False,\n    num_workers=NUM_WORKERS, pin_memory=True, prefetch_factor=2,\n)\ntest_loader = DataLoader(\n    test_dataset, batch_size=BATCH_SIZE, shuffle=False,\n    num_workers=NUM_WORKERS, pin_memory=True, prefetch_factor=2,\n)\n\nimages, labels = next(iter(val_loader))\nprint(images.shape, images.dtype, labels[:8], [class_names[l] for l in labels[:3]])\ndel images, labels","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# MODEL  (unchanged ResNet-18; autocast applied inside forward for DataParallel)\n# ============================================================\nclass BasicBlock(nn.Module):\n\n    expansion = 1\n\n    def __init__(self, in_channels, out_channels, stride=1):\n        super().__init__()\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        self.relu = nn.ReLU(inplace=True)\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        self.shortcut = nn.Sequential()\n        if stride != 1 or in_channels != out_channels:\n            self.shortcut = nn.Sequential(\n                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),\n                nn.BatchNorm2d(out_channels),\n            )\n\n    def forward(self, x):\n        identity = self.shortcut(x)\n        out = self.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        out = out + identity\n        return self.relu(out)\n\n\nclass ResNet18(nn.Module):\n\n    def __init__(self, num_classes=1000):\n        super().__init__()\n\n        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)\n        self.bn1 = nn.BatchNorm2d(64)\n        self.relu = nn.ReLU(inplace=True)\n        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)\n\n        self.layer1 = self._make_layer(64,  64,  blocks=2, stride=1)\n        self.layer2 = self._make_layer(64,  128, blocks=2, stride=2)\n        self.layer3 = self._make_layer(128, 256, blocks=2, stride=2)\n        self.layer4 = self._make_layer(256, 512, blocks=2, stride=2)\n\n        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))\n        self.fc = nn.Linear(512 * BasicBlock.expansion, num_classes)\n\n        self._initialize_weights()\n\n    def _make_layer(self, in_channels, out_channels, blocks, stride):\n        layers = [BasicBlock(in_channels, out_channels, stride)]\n        for _ in range(1, blocks):\n            layers.append(BasicBlock(out_channels, out_channels, 1))\n        return nn.Sequential(*layers)\n\n    def _initialize_weights(self):\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            elif isinstance(m, nn.Linear):\n                nn.init.normal_(m.weight, mean=0, std=0.01)\n                nn.init.constant_(m.bias, 0)\n\n    @torch.autocast(device_type=\"cuda\", dtype=torch.float16)\n    def forward(self, x):\n        x = self.maxpool(self.relu(self.bn1(self.conv1(x))))\n        x = self.layer1(x)\n        x = self.layer2(x)\n        x = self.layer3(x)\n        x = self.layer4(x)\n        x = torch.flatten(self.avgpool(x), 1)\n        return self.fc(x)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# MODEL / OPTIMISER / COSINE SCHEDULER  (2 GPUs via DataParallel)\n# ============================================================\nmodel = ResNet18(NUM_CLASSES).to(device).to(memory_format=torch.channels_last)\n\n# start from the previous run's best weights (weights only - no optimizer/scheduler state,\n# since we're switching from OneCycleLR to CosineAnnealingLR)\nmodel.load_state_dict(torch.load(PREV_BEST_PT, map_location=\"cpu\"))\nprint(\"Loaded starting weights from\", PREV_BEST_PT)\n\nif NUM_GPUS > 1:\n    model = nn.DataParallel(model)\n\ndef unwrap(m):\n    return m.module if isinstance(m, nn.DataParallel) else m\n\nprint(f\"params: {sum(p.numel() for p in model.parameters()) / 1e6:.2f}M\")\n\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.AdamW(model.parameters(), lr=LR, weight_decay=WEIGHT_DECAY)\n\n# CosineAnnealingLR steps once per EPOCH (not per batch, unlike the earlier OneCycleLR)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=NUM_EPOCHS, eta_min=ETA_MIN)\nscaler = torch.amp.GradScaler(\"cuda\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# TRAIN / EVALUATE  (with TensorBoard logging)\n# ============================================================\ndef train_one_epoch(model, loader, criterion, optimizer, scaler, writer, global_step, epoch, num_epochs):\n    model.train()\n    loss_sum = torch.zeros((), device=device)\n    correct1 = torch.zeros((), device=device)\n    correct5 = torch.zeros((), device=device)\n    total = 0\n\n    bar = tqdm(loader, desc=f\"Epoch {epoch}/{num_epochs} [Train]\")\n    for step, (images, labels) in enumerate(bar):\n        images = prep(images)\n        labels = labels.to(device, non_blocking=True)\n\n        optimizer.zero_grad(set_to_none=True)\n        outputs = model(images)\n        loss = criterion(outputs.float(), labels)\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        bs = labels.size(0)\n        top5 = outputs.topk(5, dim=1).indices\n        loss_sum += loss.detach() * bs\n        correct1 += (top5[:, 0] == labels).sum()\n        correct5 += (top5 == labels[:, None]).any(dim=1).sum()\n        total += bs\n        global_step += 1\n\n        if step % 50 == 0:\n            batch_loss = loss.item()\n            bar.set_postfix(loss=f\"{loss_sum.item() / total:.4f}\",\n                            top1=f\"{100 * correct1.item() / total:.2f}%\")\n            writer.add_scalar(\"Loss/train_step\", batch_loss, global_step)\n            writer.add_scalar(\"LR\", optimizer.param_groups[0][\"lr\"], global_step)\n\n    return loss_sum.item() / total, correct1.item() / total, correct5.item() / total, global_step\n\n\n@torch.no_grad()\ndef evaluate(model, loader, criterion, desc=\"[Eval]\", collect=False):\n    model.eval()\n    loss_sum = torch.zeros((), device=device)\n    correct1 = torch.zeros((), device=device)\n    correct5 = torch.zeros((), device=device)\n    total = 0\n    preds, labs = [], []\n\n    for images, labels in tqdm(loader, desc=desc):\n        images = prep(images)\n        labels = labels.to(device, non_blocking=True)\n\n        outputs = model(images).float()\n        loss = criterion(outputs, labels)\n\n        bs = labels.size(0)\n        top5 = outputs.topk(5, dim=1).indices\n        loss_sum += loss * bs\n        correct1 += (top5[:, 0] == labels).sum()\n        correct5 += (top5 == labels[:, None]).any(dim=1).sum()\n        total += bs\n\n        if collect:\n            preds.append(top5[:, 0].to(torch.int16))\n            labs.append(labels.to(torch.int16))\n\n    result = (loss_sum.item() / total, correct1.item() / total, correct5.item() / total)\n    if collect:\n        return result + (torch.cat(labs).cpu().numpy(), torch.cat(preds).cpu().numpy())\n    return result","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# TRAINING LOOP  (resumable within THIS cosine run only)\n# ============================================================\nstart_epoch, best_top1, history, global_step = 0, 0.0, [], 0\nif os.path.exists(LAST_PATH):\n    ck = torch.load(LAST_PATH, map_location=\"cpu\")\n    unwrap(model).load_state_dict(ck[\"model\"])\n    optimizer.load_state_dict(ck[\"optimizer\"])\n    scheduler.load_state_dict(ck[\"scheduler\"])\n    scaler.load_state_dict(ck[\"scaler\"])\n    start_epoch  = ck[\"epoch\"] + 1\n    best_top1    = ck[\"best_top1\"]\n    history      = ck[\"history\"]\n    global_step  = ck[\"global_step\"]\n    del ck\n    print(f\"Resumed cosine run from epoch {start_epoch}\")\n\nfor epoch in range(start_epoch, NUM_EPOCHS):\n    t0 = time.time()\n\n    tr_loss, tr_top1, tr_top5, global_step = train_one_epoch(\n        model, train_loader, criterion, optimizer, scaler, writer, global_step, epoch + 1, NUM_EPOCHS\n    )\n    va_loss, va_top1, va_top5 = evaluate(model, val_loader, criterion, \"[Validation]\")\n\n    scheduler.step()   # CosineAnnealingLR: once per epoch\n\n    epoch_time = time.time() - t0\n    history.append(dict(epoch=epoch + 1, train_loss=tr_loss, train_top1=tr_top1,\n                        val_loss=va_loss, val_top1=va_top1, val_top5=va_top5))\n\n    # ---- TensorBoard: per-epoch scalars ----\n    writer.add_scalars(\"Loss/epoch\", {\"train\": tr_loss, \"val\": va_loss}, epoch + 1)\n    writer.add_scalars(\"Accuracy/top1\", {\"train\": tr_top1, \"val\": va_top1}, epoch + 1)\n    writer.add_scalar(\"Accuracy/val_top5\", va_top5, epoch + 1)\n    writer.add_scalar(\"Epoch_time_sec\", epoch_time, epoch + 1)\n    writer.flush()\n\n    if va_top1 > best_top1:\n        best_top1 = va_top1\n        torch.save(unwrap(model).state_dict(), BEST_PATH)\n\n    torch.save({\n        \"model\": unwrap(model).state_dict(), \"optimizer\": optimizer.state_dict(),\n        \"scheduler\": scheduler.state_dict(), \"scaler\": scaler.state_dict(),\n        \"epoch\": epoch, \"best_top1\": best_top1, \"history\": history,\n        \"global_step\": global_step,\n    }, LAST_PATH)\n\n    print(\n        f\"\\nEpoch [{epoch + 1}/{NUM_EPOCHS}] ({epoch_time / 60:.1f} min) \"\n        f\"Train Loss: {tr_loss:.4f} Top1: {tr_top1 * 100:.2f}% | \"\n        f\"Val Loss: {va_loss:.4f} Top1: {va_top1 * 100:.2f}% Top5: {va_top5 * 100:.2f}%\"\n    )","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# FINAL TEST  (official ImageNet val set, using this run's best checkpoint)\n# ============================================================\nunwrap(model).load_state_dict(torch.load(BEST_PATH, map_location=\"cpu\"))\n\ntest_loss, test_top1, test_top5, labels, predictions = evaluate(\n    model, test_loader, criterion, \"[Test]\", collect=True\n)\nprint(\"\\nTest Results\")\nprint(f\"Test Loss : {test_loss:.4f}\")\nprint(f\"Top-1 Acc : {test_top1 * 100:.2f}%\")\nprint(f\"Top-5 Acc : {test_top5 * 100:.2f}%\")\n\nwriter.add_scalar(\"Test/loss\", test_loss)\nwriter.add_scalar(\"Test/top1\", test_top1)\nwriter.add_scalar(\"Test/top5\", test_top5)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# TRAINING CURVES  (matplotlib, for a quick look inline too)\n# ============================================================\nimport matplotlib.pyplot as plt\n\nh = pd.DataFrame(history)\nfig, ax = plt.subplots(1, 2, figsize=(11, 4))\nax[0].plot(h.epoch, h.train_loss, label=\"train\"); ax[0].plot(h.epoch, h.val_loss, label=\"val\")\nax[0].set_title(\"Loss\"); ax[0].set_xlabel(\"epoch\"); ax[0].legend()\nax[1].plot(h.epoch, h.train_top1 * 100, label=\"train top-1\"); ax[1].plot(h.epoch, h.val_top1 * 100, label=\"val top-1\")\nax[1].plot(h.epoch, h.val_top5 * 100, label=\"val top-5\")\nax[1].set_title(\"Accuracy (%)\"); ax[1].set_xlabel(\"epoch\"); ax[1].legend()\nplt.tight_layout(); plt.show()","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# PER-CLASS ANALYSIS  (1000 classes -> summarise instead of a full report)\n# ============================================================\nlabels = labels.astype(np.int64)\npredictions = predictions.astype(np.int64)\n\nper_class_total   = np.bincount(labels, minlength=NUM_CLASSES)\nper_class_correct = np.bincount(labels[labels == predictions], minlength=NUM_CLASSES)\nper_class_acc     = per_class_correct / np.maximum(per_class_total, 1)\n\norder = np.argsort(per_class_acc)\nprint(\"Worst 10 classes:\")\nfor c in order[:10]:\n    print(f\"  {class_names[c]:<30} {per_class_acc[c] * 100:5.1f}%\")\nprint(\"\\nBest 10 classes:\")\nfor c in order[::-1][:10]:\n    print(f\"  {class_names[c]:<30} {per_class_acc[c] * 100:5.1f}%\")\n\nwrong = labels != predictions\npair_counts = np.bincount(labels[wrong] * NUM_CLASSES + predictions[wrong],\n                          minlength=NUM_CLASSES * NUM_CLASSES)\nprint(\"\\nMost confused pairs (true -> predicted):\")\nfor k in np.argsort(pair_counts)[::-1][:15]:\n    print(f\"  {class_names[k // NUM_CLASSES]:<25} -> {class_names[k % NUM_CLASSES]:<25} x{pair_counts[k]}\")\n\n# ---- TensorBoard: per-class accuracy bar chart (worst 10 + best 10) ----\nsubset = np.concatenate([order[:10], order[::-1][:10]])\nfig2, ax2 = plt.subplots(figsize=(10, 5))\nax2.bar(range(len(subset)), per_class_acc[subset] * 100,\n        color=[\"crimson\"] * 10 + [\"seagreen\"] * 10)\nax2.set_xticks(range(len(subset)))\nax2.set_xticklabels([class_names[c] for c in subset], rotation=90)\nax2.set_ylabel(\"Accuracy (%)\")\nax2.set_title(\"Worst 10 (red) / Best 10 (green) classes\")\nplt.tight_layout()\nwriter.add_figure(\"PerClass/worst_best_accuracy\", fig2)\nplt.show()","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CONFUSION MATRIX FOR A SUBSET OF CLASSES  (also logged to TensorBoard)\n# ============================================================\nfrom sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay\n\ndef plot_confusion_matrix(labels, predictions, class_ids):\n    mask = np.isin(labels, class_ids) & np.isin(predictions, class_ids)\n    cm = confusion_matrix(labels[mask], predictions[mask], labels=class_ids)\n    disp = ConfusionMatrixDisplay(cm, display_labels=[class_names[c] for c in class_ids])\n    fig, ax = plt.subplots(figsize=(10, 10))\n    disp.plot(ax=ax, xticks_rotation=90, cmap=\"Blues\", colorbar=False)\n    plt.tight_layout()\n    return fig\n\nfig3 = plot_confusion_matrix(labels, predictions, subset)\nwriter.add_figure(\"PerClass/confusion_matrix\", fig3)\nplt.show()\n\n# ---- TensorBoard: hyperparameters for this run ----\nwriter.add_hparams(\n    {\"lr\": LR, \"batch_size\": BATCH_SIZE, \"weight_decay\": WEIGHT_DECAY,\n     \"scheduler\": \"CosineAnnealingLR\", \"num_epochs\": NUM_EPOCHS},\n    {\"hparam/test_top1\": test_top1, \"hparam/test_top5\": test_top5},\n)\nwriter.close()","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n```\n%load_ext tensorboard\n%tensorboard --logdir /kaggle/working/runs\n```","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}