{"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":"# \"\"\"\n# Split HAM10000 thành train/val/test, lưu 3 file CSV cho các mô hình khác.\n# Mỗi CSV có 2 cột: image_path, mask_path\n# Tỉ lệ: 70/15/15\n# \"\"\"\n\n# import os\n# import glob\n# import pandas as pd\n# from sklearn.model_selection import train_test_split\n\n# # -------------------------------------------------------------------------\n# IMAGE_DIR  = '/kaggle/input/datasets/surajghuwalewala/ham1000-segmentation-and-classification/images'\n# MASK_DIR   = '/kaggle/input/datasets/surajghuwalewala/ham1000-segmentation-and-classification/masks'\n# OUTPUT_DIR = '/kaggle/working/skin_cancer_split'\n\n# TRAIN_RATIO = 0.70\n# VAL_RATIO   = 0.15   # test = 1 - train - val = 0.15\n# SEED        = 42\n# # -------------------------------------------------------------------------\n\n# def build_df(image_dir, mask_dir):\n#     records = []\n#     for img_path in sorted(glob.glob(os.path.join(image_dir, '*.jpg'))):\n#         img_id    = os.path.splitext(os.path.basename(img_path))[0]\n#         mask_path = os.path.join(mask_dir, f'{img_id}_segmentation.png')\n#         if os.path.exists(mask_path):\n#             records.append({'image_path': img_path, 'mask_path': mask_path})\n#     df = pd.DataFrame(records)\n#     print(f\"Tong so cap hop le: {len(df)}\")\n#     return df\n\n\n# def split_and_save(df, output_dir,\n#                    train_ratio=TRAIN_RATIO,\n#                    val_ratio=VAL_RATIO,\n#                    seed=SEED):\n#     os.makedirs(output_dir, exist_ok=True)\n\n#     # Bước 1: tách test ra trước\n#     test_ratio = 1.0 - train_ratio - val_ratio\n#     train_val, test = train_test_split(\n#         df, test_size=test_ratio, random_state=seed)\n\n#     # Bước 2: tách train và val từ phần còn lại\n#     val_ratio_adjusted = val_ratio / (train_ratio + val_ratio)\n#     train, val = train_test_split(\n#         train_val, test_size=val_ratio_adjusted, random_state=seed)\n\n#     for split_name, split_df in [('train', train), ('val', val), ('test', test)]:\n#         path = os.path.join(output_dir, f'{split_name}.csv')\n#         split_df.reset_index(drop=True).to_csv(path, index=False)\n#         print(f\"  {split_name:<5s}: {len(split_df):>5d} mau  -> {path}\")\n\n#     return train, val, test\n\n\n# if __name__ == '__main__':\n#     df = build_df(IMAGE_DIR, MASK_DIR)\n#     train, val, test = split_and_save(df, OUTPUT_DIR)\n\n#     print(f\"\\nKiem tra:\")\n#     print(f\"  Train + Val + Test = {len(train) + len(val) + len(test)} (tong: {len(df)})\")\n#     print(pd.read_csv(os.path.join(OUTPUT_DIR, 'train.csv')).head(2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-02T00:46:17.810694Z","iopub.execute_input":"2026-07-02T00:46:17.811402Z","iopub.status.idle":"2026-07-02T00:46:17.817051Z","shell.execute_reply.started":"2026-07-02T00:46:17.811373Z","shell.execute_reply":"2026-07-02T00:46:17.816234Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!gdown 1RRGp2rUWENiIVuUR9tgyPsrScOjUByvs -O /kaggle/working/skin_cancer_split.zip","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-02T07:19:54.236580Z","iopub.execute_input":"2026-07-02T07:19:54.237280Z","iopub.status.idle":"2026-07-02T07:19:57.257091Z","shell.execute_reply.started":"2026-07-02T07:19:54.237246Z","shell.execute_reply":"2026-07-02T07:19:57.256357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!unzip /kaggle/working/skin_cancer_split.zip","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-02T07:19:57.258591Z","iopub.execute_input":"2026-07-02T07:19:57.258826Z","iopub.status.idle":"2026-07-02T07:19:57.388723Z","shell.execute_reply.started":"2026-07-02T07:19:57.258800Z","shell.execute_reply":"2026-07-02T07:19:57.387728Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!gdown 1Bp72bEyT657aC-a81tl_Ho_6Ll33-QfM","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-02T07:20:18.376251Z","iopub.execute_input":"2026-07-02T07:20:18.376701Z","iopub.status.idle":"2026-07-02T07:20:22.878456Z","shell.execute_reply.started":"2026-07-02T07:20:18.376667Z","shell.execute_reply":"2026-07-02T07:20:22.877464Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!unzip /kaggle/working/NestedUnet.zip","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-02T07:20:27.057025Z","iopub.execute_input":"2026-07-02T07:20:27.057644Z","iopub.status.idle":"2026-07-02T07:20:28.475186Z","shell.execute_reply.started":"2026-07-02T07:20:27.057611Z","shell.execute_reply":"2026-07-02T07:20:28.474190Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\n\nSEED=42\ndef seed_everything(seed=SEED):\n    random.seed(seed)\n    np.random.seed(seed)\n\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\n\ndef seed_worker(worker_id):\n    worker_seed = SEED + worker_id\n\n    random.seed(worker_seed)\n    np.random.seed(worker_seed)\n    torch.manual_seed(worker_seed)\n\n\nseed_everything(SEED) #bảo đảm công bằng loader cho các mô hình sau\n\n\nclass HAM10000Dataset(Dataset):\n    \"\"\"\n    CSV có 2 cột: image_path, mask_path (đường dẫn tuyệt đối).\n    Returns:\n        img  : FloatTensor (3, H, W)  — [0, 1], RGB\n        mask : FloatTensor (1, H, W)  — binary {0.0, 1.0}\n        meta : dict — image_id\n    \"\"\"\n\n    def __init__(self, csv_path, img_size=224, train=True):\n        self.df       = pd.read_csv(csv_path).reset_index(drop=True)\n        self.img_size = img_size\n        self.train    = train\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_image(self, path):\n        img = cv2.imread(path, cv2.IMREAD_COLOR)\n        if img is None:\n            raise IOError(f\"Khong doc duoc anh: {path}\")\n        return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n    def _load_mask(self, path):\n        raw = cv2.imread(path, cv2.IMREAD_GRAYSCALE)\n        if raw is None:\n            raise IOError(f\"Khong doc duoc mask: {path}\")\n        return (raw > 0).astype(np.float32)\n\n    def _pad_to_square(self, img, mask):\n        h, w = img.shape[:2]\n        diff = abs(h - w)\n        pad1, pad2 = diff // 2, diff - diff // 2\n        if h < w:\n            img  = np.pad(img,  ((pad1, pad2), (0, 0), (0, 0)), mode='reflect')\n            mask = np.pad(mask, ((pad1, pad2), (0, 0)),          mode='reflect')\n        else:\n            img  = np.pad(img,  ((0, 0), (pad1, pad2), (0, 0)), mode='reflect')\n            mask = np.pad(mask, ((0, 0), (pad1, pad2)),          mode='reflect')\n        img  = cv2.resize(img,  (self.img_size, self.img_size), interpolation=cv2.INTER_LINEAR)\n        mask = cv2.resize(mask, (self.img_size, self.img_size), interpolation=cv2.INTER_NEAREST)\n        return img, mask\n\n    def _augment(self, img, mask, seed):\n        rng = random.Random(seed)\n        if rng.random() < 0.5:\n            img  = np.flip(img,  axis=1).copy()\n            mask = np.flip(mask, axis=1).copy()\n        if rng.random() < 0.5:\n            img  = np.flip(img,  axis=0).copy()\n            mask = np.flip(mask, axis=0).copy()\n        k = rng.randint(0, 3)\n        if k:\n            img  = np.rot90(img,  k, axes=(0, 1)).copy()\n            mask = np.rot90(mask, k, axes=(0, 1)).copy()\n        return img, mask\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        img  = self._load_image(row['image_path'])\n        mask = self._load_mask(row['mask_path'])\n        img, mask = self._pad_to_square(img, mask)\n\n        img = img.astype(np.float32) / 255.0\n\n        if self.train:\n            img, mask = self._augment(img, mask, seed=idx)\n\n        img  = img.transpose(2, 0, 1)\n        mask = mask[np.newaxis, :, :]\n\n        meta = {'image_id': os.path.splitext(os.path.basename(row['image_path']))[0]}\n        return (\n            torch.from_numpy(img),\n            torch.from_numpy(mask),\n            meta,\n        )\n\n\ndef build_dataloaders(\n    split_dir='/kaggle/working/skin_cancer_split',\n    img_size=224,\n    batch_size=16,\n    num_workers=2,\n    seed=SEED,\n):\n    train_csv = os.path.join(split_dir, 'train.csv')\n    val_csv   = os.path.join(split_dir, 'val.csv')\n    test_csv  = os.path.join(split_dir, 'test.csv')\n\n    train_ds = HAM10000Dataset(train_csv, img_size, train=True)\n    val_ds   = HAM10000Dataset(val_csv,   img_size, train=False)\n    test_ds  = HAM10000Dataset(test_csv,  img_size, train=False)\n\n    print(f\"Train: {len(train_ds)} | Val: {len(val_ds)} | Test: {len(test_ds)}\")\n\n    generator = torch.Generator()\n    generator.manual_seed(seed)\n\n    train_loader = DataLoader(\n        train_ds,\n        batch_size=batch_size,\n        shuffle=True,\n        generator=generator,\n        num_workers=num_workers,\n        pin_memory=torch.cuda.is_available(),\n        drop_last=True,\n        worker_init_fn=seed_worker,\n    )\n\n    val_loader = DataLoader(\n        val_ds,\n        batch_size=batch_size,\n        shuffle=False,\n        generator=generator,\n        num_workers=num_workers,\n        pin_memory=torch.cuda.is_available(),\n        worker_init_fn=seed_worker,\n    )\n\n    test_loader = DataLoader(\n        test_ds,\n        batch_size=batch_size,\n        shuffle=False,\n        generator=generator,\n        num_workers=num_workers,\n        pin_memory=torch.cuda.is_available(),\n        worker_init_fn=seed_worker,\n    )\n\n    return train_loader, val_loader, test_loader\n\n\n# =========================================================================\n# Sanity check\n# =========================================================================\ndef sanity_check(split_dir='/kaggle/working/skin_cancer_split', n=5, img_size=256):\n    ds = HAM10000Dataset(os.path.join(split_dir, 'train.csv'), img_size, train=False)\n    print(f\"\\n--- Sanity check ({n} mau) ---\")\n    for i in range(n):\n        img, mask, meta = ds[i]\n        print(\n            f\"  [{meta['image_id']}]\"\n            f\"  img={tuple(img.shape)}  mask={tuple(mask.shape)}\"\n            f\"  mask_vals={sorted(mask.unique().tolist())}\"\n            f\"  fg={mask.mean().item()*100:.1f}%\"\n        )\n    print(\"Sanity check PASS.\")\n\n\n# =========================================================================\nif __name__ == \"__main__\":\n\n    seed_everything(SEED)\n\n    train_loader, val_loader, test_loader = build_dataloaders(\n        img_size=224,\n        batch_size=8,\n        seed=SEED,\n    )\n\n    imgs, masks, metas = next(iter(train_loader))\n\n    print(imgs.shape)\n    print(masks.shape)\n    print(masks.unique())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-02T07:21:10.348906Z","iopub.execute_input":"2026-07-02T07:21:10.349311Z","iopub.status.idle":"2026-07-02T07:21:11.255865Z","shell.execute_reply.started":"2026-07-02T07:21:10.349277Z","shell.execute_reply":"2026-07-02T07:21:11.255153Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\n=========================================================================\n NESTED U-NET (UNet++) \n=========================================================================\n\"\"\"\n\nimport os\nimport random\nfrom collections import OrderedDict\n\nimport numpy as np\nimport pandas as pd\nimport yaml\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.backends.cudnn as cudnn\nimport torch.optim as optim\nfrom torch.optim import lr_scheduler\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom tqdm import tqdm\n\n\n# =========================================================================\n# 0. REPRODUCIBILITY\n# =========================================================================\ndef seed_everything(seed: int = 42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\n# =========================================================================\n# 1. CONFIG — chỉnh trực tiếp trong dict này \n# =========================================================================\ndef get_config():\n    return {\n        'name': 'NestedUnet',                      \n        'work_dir': '/kaggle/working',\n\n        # model\n        'arch': 'NestedUNet',               # 'UNet' hoặc 'NestedUNet'\n        'deep_supervision': False, #tạo ra nhiều output ở các tầng , False thì mô hình sẽ forward từ đầu từ cuối chỉ có 1 output\n        'input_channels': 3,\n        'num_classes': 1, #foreground\n        'input_w': 224,\n        'input_h': 224,\n\n        # data (giả lập) — \n        'num_train_samples': None,\n        'num_val_samples': None,\n        'num_workers': None,\n\n        # train\n        'epochs': 100,\n        'loss': 'BCEDiceLoss',     #thêm dice vào để giải quyết mất cân bằng giữa foreground và background hay phổ biến trong ảnh y tế.         \n\n        # optimizer\n        'optimizer': 'Adam',                # 'Adam' hoặc 'SGD'\n        'lr': 3e-4,\n        'momentum': 0.9,\n        'weight_decay': 1e-4,\n        'nesterov': False,\n\n        # scheduler\n        'scheduler': 'CosineAnnealingLR',   # 'CosineAnnealingLR' | 'ReduceLROnPlateau' | 'MultiStepLR' | 'ConstantLR'\n        'min_lr': 1e-5,\n        'factor': 0.1,\n        'patience': 3,\n        'milestones': '10,20',\n        'gamma': 2 / 3,\n\n        'early_stopping': 10,               # -1 để tắt early stopping\n        'seed': 42,\n        'resume': None,                     # đường dẫn 'last_checkpoint.pth' để tiếp tục train\n    }\n# =========================================================================\n# 2. MODEL: VGGBlock / UNet / NestedUNet (UNet++)\n# =========================================================================\nclass VGGBlock(nn.Module):\n    def __init__(self, in_channels, middle_channels, out_channels):\n        super().__init__()\n        self.relu = nn.ReLU(inplace=True)\n        self.conv1 = nn.Conv2d(in_channels, middle_channels, 3, padding=1)\n        self.bn1 = nn.BatchNorm2d(middle_channels)\n        self.conv2 = nn.Conv2d(middle_channels, out_channels, 3, padding=1)\n        self.bn2 = nn.BatchNorm2d(out_channels)\n\n    def forward(self, x):\n        out = self.relu(self.bn1(self.conv1(x)))\n        out = self.relu(self.bn2(self.conv2(out)))\n        return out\n\n\nclass NestedUNet(nn.Module):\n    def __init__(self, num_classes, input_channels=3, deep_supervision=False, **kwargs):\n        super().__init__()\n        nb_filter = [32, 64, 128, 256, 512]\n        self.deep_supervision = deep_supervision\n\n        self.pool = nn.MaxPool2d(2, 2)\n        self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)\n\n        self.conv0_0 = VGGBlock(input_channels, nb_filter[0], nb_filter[0])\n        self.conv1_0 = VGGBlock(nb_filter[0], nb_filter[1], nb_filter[1])\n        self.conv2_0 = VGGBlock(nb_filter[1], nb_filter[2], nb_filter[2])\n        self.conv3_0 = VGGBlock(nb_filter[2], nb_filter[3], nb_filter[3])\n        self.conv4_0 = VGGBlock(nb_filter[3], nb_filter[4], nb_filter[4])\n\n        self.conv0_1 = VGGBlock(nb_filter[0] + nb_filter[1], nb_filter[0], nb_filter[0])\n        self.conv1_1 = VGGBlock(nb_filter[1] + nb_filter[2], nb_filter[1], nb_filter[1])\n        self.conv2_1 = VGGBlock(nb_filter[2] + nb_filter[3], nb_filter[2], nb_filter[2])\n        self.conv3_1 = VGGBlock(nb_filter[3] + nb_filter[4], nb_filter[3], nb_filter[3])\n\n        self.conv0_2 = VGGBlock(nb_filter[0] * 2 + nb_filter[1], nb_filter[0], nb_filter[0])\n        self.conv1_2 = VGGBlock(nb_filter[1] * 2 + nb_filter[2], nb_filter[1], nb_filter[1])\n        self.conv2_2 = VGGBlock(nb_filter[2] * 2 + nb_filter[3], nb_filter[2], nb_filter[2])\n\n        self.conv0_3 = VGGBlock(nb_filter[0] * 3 + nb_filter[1], nb_filter[0], nb_filter[0])\n        self.conv1_3 = VGGBlock(nb_filter[1] * 3 + nb_filter[2], nb_filter[1], nb_filter[1])\n\n        self.conv0_4 = VGGBlock(nb_filter[0] * 4 + nb_filter[1], nb_filter[0], nb_filter[0])\n\n        if self.deep_supervision:\n            self.final1 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1)\n            self.final2 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1)\n            self.final3 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1)\n            self.final4 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1)\n        else:\n            self.final = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1)\n\n    def forward(self, input):\n        x0_0 = self.conv0_0(input)\n        x1_0 = self.conv1_0(self.pool(x0_0))\n        x0_1 = self.conv0_1(torch.cat([x0_0, self.up(x1_0)], 1))\n\n        x2_0 = self.conv2_0(self.pool(x1_0))\n        x1_1 = self.conv1_1(torch.cat([x1_0, self.up(x2_0)], 1))\n        x0_2 = self.conv0_2(torch.cat([x0_0, x0_1, self.up(x1_1)], 1))\n\n        x3_0 = self.conv3_0(self.pool(x2_0))\n        x2_1 = self.conv2_1(torch.cat([x2_0, self.up(x3_0)], 1))\n        x1_2 = self.conv1_2(torch.cat([x1_0, x1_1, self.up(x2_1)], 1))\n        x0_3 = self.conv0_3(torch.cat([x0_0, x0_1, x0_2, self.up(x1_2)], 1))\n\n        x4_0 = self.conv4_0(self.pool(x3_0))\n        x3_1 = self.conv3_1(torch.cat([x3_0, self.up(x4_0)], 1))\n        x2_2 = self.conv2_2(torch.cat([x2_0, x2_1, self.up(x3_1)], 1))\n        x1_3 = self.conv1_3(torch.cat([x1_0, x1_1, x1_2, self.up(x2_2)], 1))\n        x0_4 = self.conv0_4(torch.cat([x0_0, x0_1, x0_2, x0_3, self.up(x1_3)], 1))\n\n        if self.deep_supervision:\n            return [self.final1(x0_1), self.final2(x0_2), self.final3(x0_3), self.final4(x0_4)]\n        return self.final(x0_4)\n\n\nARCHS = {'NestedUNet': NestedUNet}\n\n\n# =========================================================================\n# 3. LOSS & METRIC\n# =========================================================================\nclass BCEDiceLoss(nn.Module): # loss = 0,5* BCE with logits + DICE \n    def __init__(self):\n        super().__init__()\n\n    def forward(self, input, target):\n        bce = F.binary_cross_entropy_with_logits(input, target)\n        smooth = 1e-5\n        input = torch.sigmoid(input)\n        num = target.size(0)\n        input = input.view(num, -1)\n        target = target.view(num, -1)\n        intersection = input * target\n        dice = (2. * intersection.sum(1) + smooth) / (input.sum(1) + target.sum(1) + smooth)\n        dice = 1 - dice.sum() / num\n        return 0.5 * bce + dice\n\n\ndef iou_score(output, target):\n    smooth = 1e-5\n    if torch.is_tensor(output):\n        output = torch.sigmoid(output).detach().cpu().numpy()\n    if torch.is_tensor(target):\n        target = target.detach().cpu().numpy()\n    output_ = output > 0.5\n    target_ = target > 0.5\n    intersection = (output_ & target_).sum()\n    union = (output_ | target_).sum()\n    return (intersection + smooth) / (union + smooth)\n\n\n# =========================================================================\n# 4. UTILS\n# =========================================================================\ndef str2bool(v):\n    if isinstance(v, bool):\n        return v\n    if v.lower() in ['true', '1']:\n        return True\n    if v.lower() in ['false', '0']:\n        return False\n    raise ValueError('Boolean value expected.')\n\n\ndef count_params(model):\n    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n\n\nclass AverageMeter:\n    \"\"\"Tính và lưu giá trị trung bình / hiện tại.\"\"\"\n\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self.val = 0\n        self.avg = 0\n        self.sum = 0\n        self.count = 0\n\n    def update(self, val, n=1):\n        self.val = val\n        self.sum += val * n\n        self.count += n\n        self.avg = self.sum / self.count if self.count > 0 else 0\n\n\n# =========================================================================\n# 5. TRAIN / VALIDATE LOOP\n# =========================================================================\ndef train_one_epoch(config, loader, model, criterion, optimizer, device):\n    avg_meters = {'loss': AverageMeter(), 'iou': AverageMeter()}\n    model.train()\n\n    pbar = tqdm(total=len(loader), desc='train')\n    for input, target, _ in loader:\n        input = input.to(device)\n        target = target.to(device)\n\n        if config['deep_supervision']:\n            outputs = model(input)\n            loss = 0\n            for output in outputs:\n                loss += criterion(output, target)\n            loss /= len(outputs)\n            iou = iou_score(outputs[-1], target)\n        else:\n            output = model(input)\n            loss = criterion(output, target)\n            iou = iou_score(output, target)\n\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n\n        avg_meters['loss'].update(loss.item(), input.size(0))\n        avg_meters['iou'].update(iou, input.size(0))\n\n        pbar.set_postfix(OrderedDict([\n            ('loss', avg_meters['loss'].avg),\n            ('iou', avg_meters['iou'].avg),\n        ]))\n        pbar.update(1)\n    pbar.close()\n\n    return {'loss': avg_meters['loss'].avg, 'iou': avg_meters['iou'].avg}\n\n\ndef validate(config, loader, model, criterion, device):\n    avg_meters = {'loss': AverageMeter(), 'iou': AverageMeter()}\n    model.eval()\n\n    with torch.no_grad():\n        pbar = tqdm(total=len(loader), desc='valid')\n        for input, target, _ in loader:\n            input = input.to(device)\n            target = target.to(device)\n\n            if config['deep_supervision']:\n                outputs = model(input)\n                loss = 0\n                for output in outputs:\n                    loss += criterion(output, target)\n                loss /= len(outputs)\n                iou = iou_score(outputs[-1], target)\n            else:\n                output = model(input)\n                loss = criterion(output, target)\n                iou = iou_score(output, target)\n\n            avg_meters['loss'].update(loss.item(), input.size(0))\n            avg_meters['iou'].update(iou, input.size(0))\n\n            pbar.set_postfix(OrderedDict([\n                ('loss', avg_meters['loss'].avg),\n                ('iou', avg_meters['iou'].avg),\n            ]))\n            pbar.update(1)\n        pbar.close()\n\n    return {'loss': avg_meters['loss'].avg, 'iou': avg_meters['iou'].avg}\n\n\ndef gpu_self_test(device):\n    if device.type != 'cuda':\n        return device\n\n    print(f\"torch version      : {torch.__version__}\")\n    print(f\"torch built for CUDA: {torch.version.cuda}\")\n    try:\n        name = torch.cuda.get_device_name(0)\n        cap = torch.cuda.get_device_capability(0)\n        print(f\"GPU                 : {name} (compute capability sm_{cap[0]}{cap[1]})\")\n    except Exception as e:\n        print(f\"=> khong lay duoc thong tin GPU: {e}\")\n\n    try:\n        x = torch.randn(2, 3, 8, 8, device=device)\n        conv = nn.Conv2d(3, 4, 3, padding=1).to(device)\n        out = conv(x)\n        out.sum().backward()\n        torch.cuda.synchronize()\n        print(\"=> GPU self-test OK, train tren GPU.\")\n        return device\n    except RuntimeError as e:\n        print(\"!!! GPU self-test THAT BAI, fallback ve CPU. Loi:\")\n        return torch.device('cpu')\n\n\n# =========================================================================\n# 6. MAIN\n# =========================================================================\ndef main(config=None):\n    if config is None:\n        config = get_config()\n\n    seed_everything(config['seed'])\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    device = gpu_self_test(device)\n    print(f\"=> device: {device}\")\n\n    if config['name'] is None:\n        suffix = 'wDS' if config['deep_supervision'] else 'woDS'\n        config['name'] = f\"synthetic_{config['arch']}_{suffix}\"\n\n    model_dir = os.path.join(config['work_dir'], 'models', config['name'])\n    os.makedirs(model_dir, exist_ok=True)\n\n    print('-' * 20)\n    for k, v in config.items():\n        print(f'{k}: {v}')\n    print('-' * 20)\n\n    with open(os.path.join(model_dir, 'config.yml'), 'w') as f:\n        yaml.dump(config, f)\n\n    # ---- loss ----\n    if config['loss'] == 'BCEWithLogitsLoss':\n        criterion = nn.BCEWithLogitsLoss().to(device)\n    else:\n        criterion = BCEDiceLoss().to(device)\n\n    cudnn.benchmark = True\n\n    # ---- model ----\n    print(f\"=> creating model {config['arch']}\")\n    model = ARCHS[config['arch']](\n        config['num_classes'], config['input_channels'],\n        deep_supervision=config['deep_supervision'],\n    ).to(device)\n    print('Number of trainable params: %d' % count_params(model))\n\n    # ---- optimizer ----\n    params = filter(lambda p: p.requires_grad, model.parameters())\n    if config['optimizer'] == 'Adam':\n        optimizer = optim.Adam(params, lr=config['lr'], weight_decay=config['weight_decay'])\n    elif config['optimizer'] == 'SGD':\n        optimizer = optim.SGD(params, lr=config['lr'], momentum=config['momentum'],\n                               nesterov=config['nesterov'], weight_decay=config['weight_decay'])\n    else:\n        raise NotImplementedError(config['optimizer'])\n\n    # ---- scheduler ----\n    if config['scheduler'] == 'CosineAnnealingLR':\n        scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max=config['epochs'], eta_min=config['min_lr'])\n    elif config['scheduler'] == 'ReduceLROnPlateau':\n        scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, factor=config['factor'], patience=config['patience'],\n                                                     min_lr=config['min_lr'])\n    elif config['scheduler'] == 'MultiStepLR':\n        scheduler = lr_scheduler.MultiStepLR(\n            optimizer, milestones=[int(e) for e in config['milestones'].split(',')], gamma=config['gamma'])\n    elif config['scheduler'] == 'ConstantLR':\n        scheduler = None\n    else:\n        raise NotImplementedError(config['scheduler'])\n\n\n    log = OrderedDict([('epoch', []), ('lr', []), ('loss', []), ('iou', []), ('val_loss', []), ('val_iou', [])])\n\n    best_iou = 0.0\n    trigger = 0\n    start_epoch = 0\n\n    # ---- resume nếu có ----\n    if config.get('resume'):\n        print(f\"=> resuming from {config['resume']}\")\n        ckpt = torch.load(config['resume'], map_location=device)\n        model.load_state_dict(ckpt['model_state_dict'])\n        optimizer.load_state_dict(ckpt['optimizer_state_dict'])\n        start_epoch = ckpt['epoch'] + 1\n        best_iou = ckpt.get('best_iou', 0.0)\n        print(f\"=> resumed at epoch {start_epoch}, best_iou so far {best_iou:.4f}\")\n\n    for epoch in range(start_epoch, config['epochs']):\n        print(f\"Epoch [{epoch}/{config['epochs']}]\")\n\n        train_log = train_one_epoch(config, train_loader, model, criterion, optimizer, device)\n        val_log = validate(config, val_loader, model, criterion, device)\n\n        if scheduler is not None:\n            if config['scheduler'] == 'ReduceLROnPlateau':\n                scheduler.step(val_log['loss'])\n            else:\n                scheduler.step()\n\n        current_lr = optimizer.param_groups[0]['lr']\n        print('loss %.4f - iou %.4f - val_loss %.4f - val_iou %.4f - lr %.6f' %\n              (train_log['loss'], train_log['iou'], val_log['loss'], val_log['iou'], current_lr))\n\n        log['epoch'].append(epoch)\n        log['lr'].append(current_lr)\n        log['loss'].append(train_log['loss'])\n        log['iou'].append(train_log['iou'])\n        log['val_loss'].append(val_log['loss'])\n        log['val_iou'].append(val_log['iou'])\n        pd.DataFrame(log).to_csv(os.path.join(model_dir, 'log.csv'), index=False)\n\n        trigger += 1\n\n        # checkpoint \"last\" mỗi epoch \n        torch.save({\n            'epoch': epoch,\n            'model_state_dict': model.state_dict(),\n            'optimizer_state_dict': optimizer.state_dict(),\n            'best_iou': best_iou,\n            'config': config,\n        }, os.path.join(model_dir, 'last_checkpoint.pth'))\n\n        # checkpoint \"best\" theo val_iou\n        if val_log['iou'] > best_iou:\n            torch.save(model.state_dict(), os.path.join(model_dir, 'model_best.pth'))\n            best_iou = val_log['iou']\n            print(\"=> saved best model (val_iou=%.4f)\" % best_iou)\n            trigger = 0\n\n        if config['early_stopping'] >= 0 and trigger >= config['early_stopping']:\n            print(\"=> early stopping triggered\")\n            break\n\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n\n    print(\"\\nTraining finished. Best val IoU: %.4f\" % best_iou)\n    print(f\"Checkpoints & logs saved in: {model_dir}\")\n    return model, config\n\n\nif __name__ == '__main__':\n    cfg = get_config()\n    main(cfg)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-02T07:21:13.267814Z","iopub.execute_input":"2026-07-02T07:21:13.268643Z","iopub.status.idle":"2026-07-02T07:21:33.873016Z","shell.execute_reply.started":"2026-07-02T07:21:13.268606Z","shell.execute_reply":"2026-07-02T07:21:33.871780Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"/kaggle/working/kaggle/working/models/NestedUnet/model_best.pth","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\n=========================================================================\n Evaluate NestedUNet (UNet++) trên tập test\n Metrics: IoU, Dice, Precision, Recall\n=========================================================================\n\"\"\"\n\nimport os\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom tqdm import tqdm\nimport pandas as pd\n\n\n\n# =========================================================================\n# 1. METRICS\n# =========================================================================\n\ndef compute_metrics(output, target, threshold=0.5, smooth=1e-5):\n    \"\"\"\n    Tính IoU, Dice, Precision, Recall cho một batch.\n\n    Args:\n        output    : Tensor logits (B, 1, H, W) hoặc prob sau sigmoid\n        target    : Tensor mask  (B, 1, H, W), giá trị 0/1\n        threshold : ngưỡng binarize prediction\n        smooth    : tránh chia 0\n\n    Returns:\n        dict với các giá trị trung bình trên batch\n    \"\"\"\n    if torch.is_tensor(output):\n        prob = torch.sigmoid(output).detach().cpu().numpy()\n    else:\n        prob = output\n\n    if torch.is_tensor(target):\n        target = target.detach().cpu().numpy()\n\n    pred = (prob   > threshold).astype(np.float32)\n    gt   = (target > threshold).astype(np.float32)\n\n    B    = pred.shape[0]\n    pred = pred.reshape(B, -1)\n    gt   = gt.reshape(B, -1)\n\n    TP = (pred * gt).sum(axis=1)\n    FP = (pred * (1 - gt)).sum(axis=1)\n    FN = ((1 - pred) * gt).sum(axis=1)\n\n    iou       = (TP + smooth) / (TP + FP + FN + smooth)\n    dice      = (2 * TP + smooth) / (2 * TP + FP + FN + smooth)\n    precision = (TP + smooth) / (TP + FP + smooth)\n    recall    = (TP + smooth) / (TP + FN + smooth)\n\n    return {\n        'iou':       iou.mean(),\n        'dice':      dice.mean(),\n        'precision': precision.mean(),\n        'recall':    recall.mean(),\n    }\n\n\nclass MetricMeter:\n    \"\"\"Tích lũy metrics qua nhiều batch, trả về trung bình cuối cùng.\"\"\"\n\n    def __init__(self):\n        self.keys   = ['iou', 'dice', 'precision', 'recall']\n        self.sums   = {k: 0.0 for k in self.keys}\n        self.counts = {k: 0   for k in self.keys}\n\n    def update(self, metrics: dict, n: int = 1):\n        for k in self.keys:\n            self.sums[k]   += metrics[k] * n\n            self.counts[k] += n\n\n    def avg(self):\n        return {k: self.sums[k] / self.counts[k] for k in self.keys}\n\n\n# =========================================================================\n# 2. EVALUATE FUNCTION\n# =========================================================================\n\ndef evaluate(\n    model,\n    loader,\n    device,\n    config,\n    threshold: float = 0.5,\n    save_per_image: bool = False,\n    output_csv: str = None,\n):\n    \"\"\"\n    Chạy đánh giá trên toàn bộ test loader.\n\n    Args:\n        model          : model đã load weights\n        loader         : DataLoader trả về (input, target, img_id)\n        device         : torch.device\n        config         : dict config (cần config['deep_supervision'])\n        threshold      : ngưỡng binarize (mặc định 0.5)\n        save_per_image : nếu True, lưu metric từng ảnh\n        output_csv     : đường dẫn lưu kết quả CSV (None = không lưu)\n\n    Returns:\n        avg_metrics (dict), per_image_records (list of dict)\n    \"\"\"\n    model.eval()\n    meter   = MetricMeter()\n    records = []\n\n    with torch.no_grad():\n        pbar = tqdm(loader, desc='Evaluating', unit='batch')\n        for batch in pbar:\n            if len(batch) == 3:\n                inputs, targets, img_ids = batch\n                img_ids = list(img_ids)\n            else:\n                inputs, targets = batch\n                img_ids = [None] * inputs.size(0)\n\n            inputs  = inputs.to(device)\n            targets = targets.to(device)\n\n            # Deep supervision: lấy output cuối cùng để tính metric\n            # (giống validate() của train — dùng outputs[-1])\n            if config['deep_supervision']:\n                outputs = model(inputs)\n                output  = outputs[-1]\n            else:\n                output = model(inputs)\n\n            batch_metrics = compute_metrics(output, targets, threshold)\n            meter.update(batch_metrics, n=inputs.size(0))\n\n            pbar.set_postfix({k: f'{v:.4f}' for k, v in batch_metrics.items()})\n\n            if save_per_image:\n                prob = torch.sigmoid(output).detach().cpu().numpy()\n                gt   = targets.detach().cpu().numpy()\n                for i, img_id in enumerate(img_ids):\n                    img_metric = compute_metrics(\n                        prob[i:i+1], gt[i:i+1], threshold)\n                    records.append({\n                        'img_id':    img_id,\n                        'iou':       round(float(img_metric['iou']),       4),\n                        'dice':      round(float(img_metric['dice']),      4),\n                        'precision': round(float(img_metric['precision']), 4),\n                        'recall':    round(float(img_metric['recall']),    4),\n                    })\n\n    avg = meter.avg()\n\n    print('\\n' + '=' * 45)\n    print(f'{\"Metric\":<15} {\"Value\":>10}')\n    print('-' * 45)\n    for k, v in avg.items():\n        print(f'{k:<15} {v:>10.4f}')\n    print('=' * 45)\n\n    if output_csv is not None:\n        rows = records if save_per_image else []\n        rows.append({'img_id': 'AVERAGE', **{k: round(v, 4) for k, v in avg.items()}})\n        pd.DataFrame(rows).to_csv(output_csv, index=False)\n        print(f'=> Kết quả đã lưu tại: {output_csv}')\n\n    return avg, records\n\n\n# =========================================================================\n# 3. MAIN\n# =========================================================================\n\ndef main():\n    config = get_config()\n\n    CHECKPOINT  = '/kaggle/working/kaggle/working/models/NestedUnet/model_best.pth'\n    OUTPUT_CSV  = '/kaggle/working/test_metrics.csv'\n    THRESHOLD   = 0.5\n    BATCH_SIZE  = 8\n    NUM_WORKERS = config['num_workers'] or 4\n    INPUT_H, INPUT_W = config['input_h'], config['input_w']\n\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    print(f'=> device: {device}')\n\n    # ------------------------------------------------------------------\n    # Load model\n    # ------------------------------------------------------------------\n    arch = config['arch']\n    if arch not in ARCHS:\n        raise ValueError(f\"arch phải là một trong {list(ARCHS.keys())}, nhận được '{arch}'\")\n\n    print(f\"=> creating model {arch} \"\n          f\"(deep_supervision={config['deep_supervision']})\")\n    model = ARCHS[arch](\n        config['num_classes'],\n        config['input_channels'],\n        deep_supervision=config['deep_supervision'],\n    ).to(device)\n\n    print(f'=> loading checkpoint: {CHECKPOINT}')\n    state = torch.load(CHECKPOINT, map_location=device)\n    if 'model_state_dict' in state:\n        state = state['model_state_dict']\n    model.load_state_dict(state)\n    print('=> model loaded.')\n\n    # ------------------------------------------------------------------\n    # Dataset & DataLoader\n    # ------------------------------------------------------------------\n    # Thay bằng Dataset của bạn.\n    # Dataset phải trả về (image_tensor, mask_tensor, img_id).\n    # Ví dụ minh hoạ — uncomment và thay thế:\n    #\n    # from your_dataset import TestDataset\n    # import albumentations as A\n    # from albumentations.pytorch import ToTensorV2\n    #\n    # test_transform = A.Compose([\n    #     A.Resize(INPUT_H, INPUT_W),\n    #     A.Normalize(mean=(0.485, 0.456, 0.406),\n    #                 std =(0.229, 0.224, 0.225)),\n    #     ToTensorV2(),\n    # ])\n    # test_dataset = TestDataset(\n    #     img_dir  = '/path/to/test/images',\n    #     mask_dir = '/path/to/test/masks',\n    #     transform= test_transform,\n    # )\n    # test_loader = DataLoader(\n    #     test_dataset,\n    #     batch_size  = BATCH_SIZE,\n    #     shuffle     = False,\n    #     num_workers = NUM_WORKERS,\n    #     pin_memory  = True,\n    # )\n\n    # ------------------------------------------------------------------\n    # Chạy đánh giá\n    # ------------------------------------------------------------------\n    avg_metrics, per_image = evaluate(\n        model          = model,\n        loader         = test_loader,\n        device         = device,\n        config         = config,\n        threshold      = THRESHOLD,\n        save_per_image = True,\n        output_csv     = OUTPUT_CSV,\n    )\n\n    return avg_metrics\n\n\nif __name__ == '__main__':\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-02T07:28:05.326307Z","iopub.execute_input":"2026-07-02T07:28:05.327014Z","iopub.status.idle":"2026-07-02T07:28:30.669611Z","shell.execute_reply.started":"2026-07-02T07:28:05.326978Z","shell.execute_reply":"2026-07-02T07:28:30.668750Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}