{"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":"none","dataSources":[{"sourceId":8089,"databundleVersionId":44321,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!mkdir -p /kaggle/working/stage1_train\n!unzip -o -q /kaggle/input/data-science-bowl-2018/stage1_train.zip -d /kaggle/working/stage1_train\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T05:47:45.263710Z","iopub.execute_input":"2025-11-06T05:47:45.264194Z","iopub.status.idle":"2025-11-06T05:47:49.998925Z","shell.execute_reply.started":"2025-11-06T05:47:45.264163Z","shell.execute_reply":"2025-11-06T05:47:49.997518Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Setup: imports, device, small helpers\n#(What this cell does: imports libraries, sets device, seeds, and helper metrics + small image display util.)\n\nimport time, random, math, os\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, random_split\nimport torchvision\nfrom torchvision import transforms\nfrom torchvision.datasets import OxfordIIITPet\n\n# reproducible-ish\nseed = 42\nrandom.seed(seed); np.random.seed(seed); torch.manual_seed(seed)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device)\n\n# Metrics\ndef pixel_accuracy(pred, target):\n    pred = pred.argmax(1)\n    return (pred == target).float().mean().item()\n\ndef dice_score(pred, target, eps=1e-6):\n    # pred: logits or probabilities [B,C,H,W] ; target: [B,H,W]\n    if pred.dim() == 4:\n        pred = pred.argmax(1)\n    pred_flat = pred.view(-1).float()\n    tgt_flat = target.view(-1).float()\n    inter = (pred_flat * tgt_flat).sum()\n    return (2. * inter + eps) / (pred_flat.sum() + tgt_flat.sum() + eps)\n\n# A lighter Dice loss for multi-class (averaged)\nclass DiceLoss(nn.Module):\n    def __init__(self, eps=1e-6):\n        super().__init__()\n        self.eps = eps\n    def forward(self, logits, target):\n        # logits: [B,C,H,W], target: [B,H,W]\n        probs = F.softmax(logits, dim=1)\n        target_onehot = F.one_hot(target, num_classes=probs.shape[1]).permute(0,3,1,2).float()\n        num = 2*(probs*target_onehot).sum(dim=(2,3))\n        den = (probs + target_onehot).sum(dim=(2,3))\n        dice = (num + self.eps) / (den + self.eps)\n        return 1 - dice.mean()\n\n# Quick batch visualizer\ndef show_images(inputs, targets, preds=None, n=3, cmap='viridis'):\n    inputs = inputs.cpu().numpy().transpose(0,2,3,1)\n    targets = targets.cpu().numpy()\n    if preds is not None:\n        preds = preds.cpu().numpy()\n    fig,axs = plt.subplots(n,3,figsize=(9,3*n))\n    for i in range(n):\n        axs[i,0].imshow(inputs[i])\n        axs[i,0].set_title(\"input\"); axs[i,0].axis('off')\n        axs[i,1].imshow(targets[i])\n        axs[i,1].set_title(\"gt\"); axs[i,1].axis('off')\n        if preds is not None:\n            axs[i,2].imshow(preds[i])\n            axs[i,2].set_title(\"pred\"); axs[i,2].axis('off')\n    plt.tight_layout()\n\n\n\n# Purpose: read Data Science Bowl 2018 stage1_train structure (folders -> images/*.png and masks/*.png),\n# merge masks into one binary mask per image, resize to SIZE, sample SAMPLE_SIZE images,\n# create train_loader and test_loader (70/30 split). Falls back to Oxford-IIIT Pet if DSB not found.\n\nimport os, glob, random\nfrom PIL import Image\nimport numpy as np\nimport torch\nfrom torchvision import transforms\nfrom torch.utils.data import DataLoader, random_split\n\n#(change SAMPLE_SIZE for smaller/larger)\nSIZE = 128        # image resize (keep same as models)\nBATCH = 8         # batch size used earlier\nSAMPLE_SIZE = 100 # choose 20..100 for quick demos\n\nds_root = \"/kaggle/working/stage1_train\"\n\n\n\n# image transform (to tensor, normalized [0,1])\nimg_transform = transforms.Compose([\n    transforms.Resize((SIZE, SIZE)),\n    transforms.ToTensor(),\n])\n\n# resize mask with nearest neighbor to preserve label values\ndef load_and_resize_mask(mask_array, size=(SIZE, SIZE)):\n    # mask_array: numpy 2D uint8 (0/1 or 0..255)\n    pil = Image.fromarray((mask_array*255).astype(np.uint8))\n    pil = pil.resize(size, resample=Image.NEAREST)\n    arr = np.array(pil)\n    arr = (arr > 0).astype(np.uint8)\n    return torch.from_numpy(arr).long()  # shape [H,W], dtype long\n\ndata_items = []  # list of (tensor_image, tensor_mask)\n\nif os.path.exists(ds_root):\n    # find sample folders\n    sample_dirs = sorted(glob.glob(os.path.join(ds_root, \"*\")))\n    for s in sample_dirs:\n        # find image file inside images/ subfolder\n        img_files = glob.glob(os.path.join(s, \"images\", \"*.png\"))\n        if len(img_files) == 0:\n            continue\n        img_path = img_files[0]\n        # collect mask files (there may be many instance masks)\n        mask_files = glob.glob(os.path.join(s, \"masks\", \"*.png\"))\n        # load image\n        try:\n            img_pil = Image.open(img_path).convert(\"RGB\")\n        except Exception as e:\n            # skip if image corrupt\n            print(\"skip image:\", img_path, \"error:\", e); continue\n        img_t = img_transform(img_pil)  # [3,H,W] float\n\n        # build combined binary mask\n        if len(mask_files) == 0:\n            mask_t = torch.zeros((SIZE, SIZE), dtype=torch.long)\n        else:\n            # build combined mask at original resolution, then resize\n            w,h = img_pil.size\n            combined = np.zeros((h, w), dtype=np.uint8)\n            for mf in mask_files:\n                try:\n                    m_pil = Image.open(mf).convert(\"L\")\n                    m_arr = np.array(m_pil)\n                    combined = np.logical_or(combined, m_arr > 0)\n                except:\n                    continue\n            mask_t = load_and_resize_mask(combined, size=(SIZE, SIZE))\n        data_items.append((img_t, mask_t))\n    print(\"Found DSB samples (after reading):\", len(data_items))\nelse:\n    print(\"DSB path not found at\", ds_root)\n\n# FALLBACK: if DSB not available or empty, use a small Oxford-IIIT Pet subset\nif len(data_items) == 0:\n    print(\"Falling back to small Oxford-IIIT Pet subset (so pipeline stays runnable).\")\n    from torchvision.datasets import OxfordIIITPet\n    pet_root = \"/kaggle/working/data\"\n    os.makedirs(pet_root, exist_ok=True)\n    pet_ds = OxfordIIITPet(pet_root, download=True, target_types='segmentation',\n                           transform=transforms.Compose([transforms.Resize((SIZE,SIZE)), transforms.ToTensor()]),\n                           target_transform=transforms.Compose([transforms.Resize((SIZE,SIZE)), transforms.PILToTensor()]))\n    # sample up to SAMPLE_SIZE\n    idxs = list(range(len(pet_ds)))\n    random.shuffle(idxs)\n    idxs = idxs[:min(SAMPLE_SIZE, len(idxs))]\n    for i in idxs:\n        img, mask = pet_ds[i]\n        if isinstance(mask, torch.Tensor) and mask.ndim == 3:\n            mask = mask.squeeze(0)\n        mask = (mask > 0).long()\n        data_items.append((img, mask))\n\n# Create a small sampled dataset for demo speed\nrandom.shuffle(data_items)\nSAMPLE_SIZE = min(SAMPLE_SIZE, len(data_items))\ndata_items = data_items[:SAMPLE_SIZE]\nprint(\"Using SAMPLE_SIZE =\", SAMPLE_SIZE)\n\nclass SmallInMemoryDataset(torch.utils.data.Dataset):\n    def __init__(self, items):\n        self.items = items\n    def __len__(self):\n        return len(self.items)\n    def __getitem__(self, idx):\n        return self.items[idx]\n\ndataset_small = SmallInMemoryDataset(data_items)\n\n# 70/30 split (keeps variable names train_loader/test_loader)\nn_total = len(dataset_small)\ntrain_n = int(0.7 * n_total)\ntest_n = n_total - train_n\ntrain_ds, test_ds = random_split(dataset_small, [train_n, test_n])\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH, shuffle=True, num_workers=2, pin_memory=True)\ntest_loader  = DataLoader(test_ds,  batch_size=BATCH, shuffle=False, num_workers=2, pin_memory=True)\n\nprint(\"Train / Test sizes:\", len(train_ds), len(test_ds))\n# -----------------------------------------------------------------------------------------\n\n\n\n\n\n#2) Model definitions (UNet, Attention-UNet, UNet++, Mobile-UNet)\n#(What this cell does: defines 4 compact models. Each model is intentionally small and clear so you can understand. They all output logits with C=2 classes.)\n\n# --- What: define small clean model classes for UNet, AttentionUNet, UNet++(simple) and MobileUNet.\n# Double conv block\ndef conv3(in_ch, out_ch):\n    return nn.Sequential(nn.Conv2d(in_ch,out_ch,3,padding=1), nn.ReLU(inplace=True),\n                         nn.Conv2d(out_ch,out_ch,3,padding=1), nn.ReLU(inplace=True))\n\n# Depthwise separable conv for MobileUNet\nclass SeparableConv(nn.Module):\n    def __init__(self, in_ch, out_ch):\n        super().__init__()\n        self.op = nn.Sequential(\n            nn.Conv2d(in_ch, in_ch, kernel_size=3, padding=1, groups=in_ch, bias=False),\n            nn.Conv2d(in_ch, out_ch, kernel_size=1, bias=False),\n            nn.ReLU(inplace=True)\n        )\n    def forward(self,x): return self.op(x)\n\n# UNet baseline\nclass UNetSmall(nn.Module):\n    def __init__(self, in_ch=3, n_classes=2, base=32):\n        super().__init__()\n        self.d1 = conv3(in_ch, base)\n        self.d2 = conv3(base, base*2)\n        self.d3 = conv3(base*2, base*4)\n        self.b  = conv3(base*4, base*8)\n        self.up3 = nn.ConvTranspose2d(base*8, base*4, 2,2)\n        self.c3 = conv3(base*8, base*4)\n        self.up2 = nn.ConvTranspose2d(base*4, base*2, 2,2)\n        self.c2 = conv3(base*4, base*2)\n        self.up1 = nn.ConvTranspose2d(base*2, base, 2,2)\n        self.c1 = conv3(base*2, base)\n        self.out = nn.Conv2d(base, n_classes, 1)\n    def forward(self,x):\n        s1 = self.d1(x); p1 = F.max_pool2d(s1,2)\n        s2 = self.d2(p1); p2 = F.max_pool2d(s2,2)\n        s3 = self.d3(p2); p3 = F.max_pool2d(s3,2)\n        bn = self.b(p3)\n        x = self.up3(bn); x = torch.cat([x,s3],1); x = self.c3(x)\n        x = self.up2(x);  x = torch.cat([x,s2],1); x = self.c2(x)\n        x = self.up1(x);  x = torch.cat([x,s1],1); x = self.c1(x)\n        return self.out(x)\n\n# Attention Gate small\nclass AttentionGate(nn.Module):\n    def __init__(self, F_g, F_l, F_int):\n        \"\"\"F_g = channels of gating tensor you will pass in.\n           F_l = channels of the skip (x).\n           F_int = intermediate reduced channels (a smaller number).\n        \"\"\"\n        super().__init__()\n        self.W_g = nn.Conv2d(F_g, F_int, kernel_size=1, bias=True)\n        self.W_x = nn.Conv2d(F_l, F_int, kernel_size=1, bias=True)\n        self.psi = nn.Conv2d(F_int, 1, kernel_size=1, bias=True)\n        self.relu = nn.ReLU(inplace=True)\n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x, g):\n        # x: skip feature (B, F_l, Hx, Wx)\n        # g: gating feature (B, F_g, Hg, Wg)  - may be different spatial size\n        # If spatial sizes differ, upsample gating to match skip's spatial size\n        if g.shape[2:] != x.shape[2:]:\n            g = F.interpolate(g, size=x.shape[2:], mode='bilinear', align_corners=False)\n\n        # Now channel dims must match the convolutions' expected channels\n        g1 = self.W_g(g)      # -> B x F_int x Hx x Wx\n        x1 = self.W_x(x)      # -> B x F_int x Hx x Wx\n\n        psi = self.relu(g1 + x1)   # elementwise add in common spatial resolution\n        psi = self.psi(psi)        # -> B x 1 x Hx x Wx\n        alpha = self.sigmoid(psi)  # attention coefficients (0..1)\n        return x * alpha           # gated/filtered skip feature map\n\n\n# Attention UNet (small) uses same structure as UNet but with gates on skips\nclass AttUNetSmall(UNetSmall):\n    def __init__(self, in_ch=3, n_classes=2, base=32):\n        super().__init__(in_ch, n_classes, base)\n        # IMPORTANT:\n        # We will PASS the *upsampled decoder feature* (x after up conv) as gating.\n        # Therefore F_g must match the channel count of that upsampled feature.\n        # up3 produces channels base*4, up2 -> base*2, up1 -> base\n        self.ag3 = AttentionGate(F_g=base*4, F_l=base*4, F_int=base*2)  # gate between s3 (base*4) and up3(x) (base*4)\n        self.ag2 = AttentionGate(F_g=base*2, F_l=base*2, F_int=base)    # gate between s2 (base*2) and up2(x) (base*2)\n        # ensure F_int>=1\n        f_int1 = max(base//2, 1)\n        self.ag1 = AttentionGate(F_g=base, F_l=base, F_int=f_int1)      # gate between s1 (base) and up1(x) (base)\n\n    def forward(self, x):\n        s1 = self.d1(x); p1 = F.max_pool2d(s1,2)\n        s2 = self.d2(p1); p2 = F.max_pool2d(s2,2)\n        s3 = self.d3(p2); p3 = F.max_pool2d(s3,2)\n        bn = self.b(p3)\n\n        # Decoder: after each upsample we pass the upsampled feature as gating to the corresponding AG\n        x = self.up3(bn)                  # x channels = base*4 (matches ag3 F_g)\n        s3f = self.ag3(s3, x)             # gate: skip s3 filtered by gating x\n        x = torch.cat([x, s3f], 1); x = self.c3(x)\n\n        x = self.up2(x)                   # x channels = base*2 (matches ag2 F_g)\n        s2f = self.ag2(s2, x)\n        x = torch.cat([x, s2f], 1); x = self.c2(x)\n\n        x = self.up1(x)                   # x channels = base (matches ag1 F_g)\n        s1f = self.ag1(s1, x)\n        x = torch.cat([x, s1f], 1); x = self.c1(x)\n\n        return self.out(x)\n\n\n# UNet++ simplified: add intermediate conv to refine skip before concat\nclass UNetPP(nn.Module):\n    def __init__(self, in_ch=3, n_classes=2, base=32):\n        super().__init__()\n        self.d1 = conv3(in_ch, base)\n        self.d2 = conv3(base, base*2)\n        self.d3 = conv3(base*2, base*4)\n        self.b  = conv3(base*4, base*8)\n        # intermediate refinement convs for skip\n        self.r32 = nn.Conv2d(base*4, base*4, 3, padding=1)\n        self.r21 = nn.Conv2d(base*2, base*2, 3, padding=1)\n        # decoder\n        self.up3 = nn.ConvTranspose2d(base*8, base*4,2,2); self.c3=conv3(base*8, base*4)\n        self.up2 = nn.ConvTranspose2d(base*4, base*2,2,2); self.c2=conv3(base*4, base*2)\n        self.up1 = nn.ConvTranspose2d(base*2, base, 2,2);  self.c1=conv3(base*2, base)\n        self.out = nn.Conv2d(base, n_classes,1)\n    def forward(self,x):\n        s1 = self.d1(x); p1 = F.max_pool2d(s1,2)\n        s2 = self.d2(p1); p2 = F.max_pool2d(s2,2)\n        s3 = self.d3(p2); p3 = F.max_pool2d(s3,2)\n        bn = self.b(p3)\n        # refine skips slightly\n        s3r = F.relu(self.r32(s3))\n        s2r = F.relu(self.r21(s2))\n        x = self.up3(bn); x = torch.cat([x,s3r],1); x = self.c3(x)\n        x = self.up2(x);  x = torch.cat([x,s2r],1); x = self.c2(x)\n        x = self.up1(x);  x = torch.cat([x,s1],1);  x = self.c1(x)\n        return self.out(x)\n\n# Mobile UNet using separable convs\nclass MobileUNet(nn.Module):\n    def __init__(self, in_ch=3, n_classes=2, base=32):\n        super().__init__()\n        self.d1 = nn.Sequential(SeparableConv(in_ch, base), SeparableConv(base, base))\n        self.d2 = nn.Sequential(SeparableConv(base, base*2), SeparableConv(base*2, base*2))\n        self.d3 = nn.Sequential(SeparableConv(base*2, base*4), SeparableConv(base*4, base*4))\n        self.b  = nn.Sequential(SeparableConv(base*4, base*8), SeparableConv(base*8, base*8))\n        self.up3 = nn.ConvTranspose2d(base*8, base*4,2,2); self.c3 = nn.Sequential(SeparableConv(base*8, base*4))\n        self.up2 = nn.ConvTranspose2d(base*4, base*2,2,2); self.c2 = nn.Sequential(SeparableConv(base*4, base*2))\n        self.up1 = nn.ConvTranspose2d(base*2, base,2,2);   self.c1 = nn.Sequential(SeparableConv(base*2, base))\n        self.out = nn.Conv2d(base, n_classes, 1)\n    def forward(self,x):\n        s1 = self.d1(x); p1 = F.max_pool2d(s1,2)\n        s2 = self.d2(p1); p2 = F.max_pool2d(s2,2)\n        s3 = self.d3(p2); p3 = F.max_pool2d(s3,2)\n        bn = self.b(p3)\n        x = self.up3(bn); x = torch.cat([x,s3],1); x = self.c3(x)\n        x = self.up2(x);  x = torch.cat([x,s2],1); x = self.c2(x)\n        x = self.up1(x);  x = torch.cat([x,s1],1); x = self.c1(x)\n        return self.out(x)\n\n\n\n\n\n#3) Training utilities: one train/validate epoch function, timing\n#(What this cell does: defines train_epoch and eval_model functions which perform forward/backward, compute combined CE+Dice loss, track metrics, and measure inference time.)\n\n\n\n# --- What: train/val helpers using CE + Dice, measuring loss, accuracy, dice, and inference time\ndef train_epoch(model, loader, optim, ce_loss, dice_loss):\n    model.train()\n    running = {\"loss\":0.0, \"acc\":0.0, \"dice\":0.0}\n    n = 0\n    for x,y in loader:\n        x,y = x.to(device), y.to(device)\n        optim.zero_grad()\n        logits = model(x)\n        loss = ce_loss(logits, y) + dice_loss(logits, y)\n        loss.backward()\n        optim.step()\n        running[\"loss\"] += loss.item()\n        running[\"acc\"] += pixel_accuracy(logits.detach().cpu(), y.detach().cpu())\n        running[\"dice\"] += dice_score(logits.detach().cpu(), y.detach().cpu())\n        n += 1\n    return {k: v / n for k,v in running.items()}\n\n\n#eval_model (safe on CPU or GPU)\n@torch.no_grad()\ndef eval_model(model, loader, ce_loss, dice_loss, measure_speed=False):\n    model.eval()\n    running = {\"loss\":0.0, \"acc\":0.0, \"dice\":0.0}\n    n = 0\n    total_time = 0.0\n    total_samples = 0\n\n    use_cuda = torch.cuda.is_available()\n    for x,y in loader:\n        x,y = x.to(device), y.to(device)\n\n        if measure_speed and use_cuda:\n            # accurate GPU timing\n            torch.cuda.synchronize()\n            t0 = time.time()\n            logits = model(x)\n            torch.cuda.synchronize()\n            t1 = time.time()\n            total_time += (t1 - t0)\n            total_samples += x.size(0)\n        elif measure_speed and not use_cuda:\n            # CPU timing (no cuda synchronization possible)\n            t0 = time.time()\n            logits = model(x)\n            t1 = time.time()\n            total_time += (t1 - t0)\n            total_samples += x.size(0)\n        else:\n            logits = model(x)\n\n        loss = ce_loss(logits, y) + dice_loss(logits, y)\n        running[\"loss\"] += loss.item()\n        running[\"acc\"] += pixel_accuracy(logits.detach().cpu(), y.detach().cpu())\n        running[\"dice\"] += dice_score(logits.detach().cpu(), y.detach().cpu())\n        n += 1\n\n    stats = {k: v / n for k,v in running.items()} if n>0 else {\"loss\":0,\"acc\":0,\"dice\":0}\n    if measure_speed:\n        if total_samples > 0:\n            stats[\"time_per_image\"] = total_time / total_samples\n        else:\n            stats[\"time_per_image\"] = None\n    return stats\n\n\n\n\n\n\n\n#4) Train all models sequentially and collect stats (small epochs for demo)\n#(What this cell does: instantiates each model, trains for a few epochs, records per-epoch stats for plotting. For demo it uses EPOCHS=6. Increase for serious training.)\n\n\n# --- What: run short training for each model and collect history for plots. Increase EPOCHS in real runs.\nEPOCHS = 25\nmodels = {\n    \"UNet\": UNetSmall().to(device),\n    \"AttUNet\": AttUNetSmall().to(device),\n    \"UNet++\": UNetPP().to(device),\n    \"MobileUNet\": MobileUNet().to(device)\n}\n\nhist = {}\nfor name, model in models.items():\n    print(f\"\\n=== TRAINING {name} ===\")\n    optim = torch.optim.Adam(model.parameters(), lr=1e-3)\n    ce = nn.CrossEntropyLoss()\n    dice = DiceLoss()\n    hist[name] = {\"train_loss\":[],\"val_loss\":[],\"train_acc\":[], \"val_acc\":[], \"train_dice\":[], \"val_dice\":[]}\n    for ep in range(EPOCHS):\n        tr = train_epoch(model, train_loader, optim, ce, dice)\n        val = eval_model(model, test_loader, ce, dice)\n        print(f\"{name} E{ep}: tr_loss {tr['loss']:.4f} val_loss {val['loss']:.4f} tr_dice {tr['dice']:.3f} val_dice {val['dice']:.3f}\")\n        hist[name][\"train_loss\"].append(tr[\"loss\"]); hist[name][\"val_loss\"].append(val[\"loss\"])\n        hist[name][\"train_acc\"].append(tr[\"acc\"]); hist[name][\"val_acc\"].append(val[\"acc\"])\n        hist[name][\"train_dice\"].append(tr[\"dice\"]); hist[name][\"val_dice\"].append(val[\"dice\"])\n    # Save model\n    torch.save(model.state_dict(), f\"/kaggle/working/{name}.pth\")\n\n\n\n\n\n#5) Plots: training loss / dice curves and bar chart comparison (What this does: creates comparison plots across models)\n\n# --- What: plot loss and dice curves for each model and a summary bar chart of final metrics.\nplt.figure(figsize=(12,4))\nplt.subplot(1,2,1)\nfor name in hist:\n    plt.plot(hist[name][\"val_loss\"], label=name)\nplt.title(\"Val Loss\"); plt.legend()\n\nplt.subplot(1,2,2)\nfor name in hist:\n    plt.plot(hist[name][\"val_dice\"], label=name)\nplt.title(\"Val Dice\"); plt.legend()\nplt.show()\n\n# final summary bar chart (last epoch)\nnames = list(hist.keys())\nfinal_dice = [hist[n][\"val_dice\"][-1] for n in names]\nfinal_acc  = [hist[n][\"val_acc\"][-1] for n in names]\nx = np.arange(len(names))\nplt.figure(figsize=(8,4))\nplt.bar(x-0.15, final_dice, width=0.3, label=\"Dice\")\nplt.bar(x+0.15, final_acc,  width=0.3, label=\"PixelAcc\")\nplt.xticks(x, names)\nplt.ylim(0,1)\nplt.legend(); plt.title(\"Final validation metrics (last epoch)\")\nplt.show()\n\n\n\n\n\n\n#6) Per-model inference speed and sample outputs (What this does: measures inference time per image and shows several example input/gt/pred images for each model.)\n\n# --- What: measure inference speed (time per image) and display sample predictions from each model\nce = nn.CrossEntropyLoss(); dice = DiceLoss()\nsummary = {}\nfor name, model in models.items():\n    s = eval_model(model, test_loader, ce, dice, measure_speed=True)\n    summary[name] = s\n    print(f\"{name}: val_loss {s['loss']:.4f} val_dice {s['dice']:.3f} time_per_image {s.get('time_per_image',0):.4f}s\")\n\n# show sample predictions (first batch from test_loader)\nx,y = next(iter(test_loader))\nfor name, model in models.items():\n    model.eval()\n    with torch.no_grad():\n        preds = model(x.to(device)).argmax(1).cpu()\n    show_images(x, y, preds, n=min(3, x.size(0)))\n    plt.suptitle(name); plt.show()\n\n\n\n\n\n#7) Save a final comparison table and conclude\n#(What this cell does: prints a concise table of per-model metrics for reporting / slides.)\n\n\n# --- What: summarize metrics in a compact table (ready to copy into slides)\nprint(\"Model\\tValLoss\\tValDice\\tValAcc\\tTimePerImage(s)\")\nfor name in names:\n    s = summary[name]\n    print(f\"{name}\\t{s['loss']:.4f}\\t{s['dice']:.4f}\\t{s['acc']:.4f}\\t{s.get('time_per_image',0):.4f}\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-06T05:47:50.001426Z","iopub.execute_input":"2025-11-06T05:47:50.001753Z","iopub.status.idle":"2025-11-06T06:12:03.224812Z","shell.execute_reply.started":"2025-11-06T05:47:50.001726Z","shell.execute_reply":"2025-11-06T06:12:03.223790Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nnames = list(summary.keys())\naccs  = [summary[n][\"acc\"]*100 for n in names]   # convert to %\n\nplt.figure(figsize=(6,4))\nplt.bar(names, accs)\nplt.ylabel(\"Pixel Accuracy (%)\")\nplt.title(\"Pixel Level Accuracy Comparison Across Models\")\nfor i,v in enumerate(accs):\n    plt.text(i, v+0.5, f\"{v:.2f}%\", ha='center')  # show % on top of bar\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T06:25:34.398039Z","iopub.execute_input":"2025-11-06T06:25:34.398518Z","iopub.status.idle":"2025-11-06T06:25:34.557155Z","shell.execute_reply.started":"2025-11-06T06:25:34.398476Z","shell.execute_reply":"2025-11-06T06:25:34.556188Z"}},"outputs":[],"execution_count":null}]}