{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":4117,"databundleVersionId":46665},{"sourceType":"datasetVersion","sourceId":15522655,"datasetId":9931127,"databundleVersionId":16449701},{"sourceType":"datasetVersion","sourceId":16179595,"datasetId":10374179,"databundleVersionId":17156826},{"sourceType":"datasetVersion","sourceId":15743447,"datasetId":10088103,"databundleVersionId":16686091},{"sourceType":"datasetVersion","sourceId":16172742,"datasetId":10369807,"databundleVersionId":17149350}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# B4.1 — Per-Channel Ablation (6 cấu hình)\n**PIXEL Pipeline v3 — ResNet-50, 3 seeds × 6 configs × 2 datasets**\n\n| Config | R (Byte) | G (Entropy) | B (LBP) | Ghi chú |\n|--------|----------|-------------|---------|--------|\n| R      | ✓ | ✗ | ✗ | = Grayscale baseline (đã có) |\n| G      | ✗ | ✓ | ✗ | Entropy đơn thuần |\n| B      | ✗ | ✗ | ✓ | LBP đơn thuần |\n| R+G    | ✓ | ✓ | ✗ | Byte + Entropy, không texture |\n| R+B    | ✓ | ✗ | ✓ | Byte + Texture, không entropy |\n| RGB    | ✓ | ✓ | ✓ | Full PPS (đã có) |\n\n**Mỗi session** chạy một tập configs trên một dataset. Thay `CURRENT_DATASET` và `CONFIGS_TO_RUN` trước khi chạy.\n\n- **Session 2.1** (Malimg): `CONFIGS_TO_RUN = ['G', 'B']`  \n- **Session 2.2** (BIG-2015 song song): `CONFIGS_TO_RUN = ['G', 'B']`  \n- **Session 2.3** (Malimg tiếp): `CONFIGS_TO_RUN = ['RG', 'RB']`  \n- **Session 2.4** (BIG-2015 tiếp): `CONFIGS_TO_RUN = ['RG', 'RB']`","metadata":{}},{"cell_type":"code","source":"# ============================================================\n# CELL 1: Imports\n# ============================================================\nimport os, json, gc, copy, warnings\nimport numpy as np\nimport pandas as pd\nimport psutil\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom contextlib import nullcontext\nfrom torch.amp import autocast, GradScaler\nfrom torch.utils.data import (DataLoader, random_split,\n                               WeightedRandomSampler, Subset, Dataset)\nfrom torchvision import datasets, transforms, models\nfrom sklearn.metrics import f1_score, classification_report\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom PIL import Image\n\nwarnings.filterwarnings(\"ignore\")\nos.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\"\nos.environ[\"OMP_NUM_THREADS\"]        = \"2\"\nos.environ[\"MKL_NUM_THREADS\"]        = \"2\"\n\nDEVICE      = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nAMP_ENABLED = torch.cuda.is_available()\nprint(f\"Device : {DEVICE}\")\nprint(f\"GPU    : {torch.cuda.device_count()} × {torch.cuda.get_device_name(0) if AMP_ENABLED else 'N/A'}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-07T17:08:04.273120Z","iopub.execute_input":"2026-07-07T17:08:04.273960Z","iopub.status.idle":"2026-07-07T17:08:13.413245Z","shell.execute_reply.started":"2026-07-07T17:08:04.273925Z","shell.execute_reply":"2026-07-07T17:08:13.412524Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 2: Config — THAY ĐỔI TRƯỚC MỖI SESSION\n# ============================================================\n\n# ── Chọn dataset cho session này ─────────────────────────────\nCURRENT_DATASET = \"microsoft_rgb\"   # hoặc \"microsoft_rgb\"\n\n# ── Configs cần train trong session này ──────────────────────\n# Session 2.1 / 2.2: ['G', 'B']\n# Session 2.3 / 2.4: ['RG', 'RB']\n# Để train tất cả:   ['R', 'G', 'B', 'RG', 'RB', 'RGB']\nCONFIGS_TO_RUN = ['RGB']\n\n# ── Dataset paths (từ b4_2 occlusion notebook) ───────────────\nDATASET_CONFIGS = {\n    \"malimg_rgb\": {\n        \"path\"        : \"/kaggle/input/datasets/dongquan/malimg-rgb/kaggle/working/malimg_rgb\",\n        \"csv_path\"    : None,\n        \"is_rgb\"      : True,\n        \"num_classes\" : 25,\n        \"description\" : \"Malimg\",\n    },\n    \"microsoft_rgb\": {\n        \"path\"        : \"/kaggle/input/datasets/vnhtbo/microsoft/train_rgb/train_rgb\",\n        \"csv_path\"    : \"/kaggle/input/competitions/malware-classification/trainLabels.csv\",\n        \"is_rgb\"      : True,\n        \"num_classes\" : 9,\n        \"description\" : \"Microsoft BIG-2015\",\n    },\n}\n\n# ── Hyperparams (khớp với resnet-50-training.ipynb) ───────────\nIMG_SIZE           = 224\nBATCH_SIZE         = 32\nACCUMULATION_STEPS = 2\nFREEZE_EPOCHS      = 7\nUNFREEZE_EPOCHS    = 18\nTOTAL_EPOCHS       = FREEZE_EPOCHS + UNFREEZE_EPOCHS   # 25\nEARLY_STOP_PATIENCE= 7\nN_RUNS             = 3\nLR_HEAD            = 1e-3\nLR_FINETUNE        = 1e-4\nVAL_RATIO          = 0.15\nTEST_RATIO         = 0.15\nSEED               = 42\n\nCKPT_DIR   = \"/kaggle/working/checkpoints\"\nOUTPUT_DIR = \"/kaggle/working\"\nos.makedirs(CKPT_DIR, exist_ok=True)\n\n# Kết quả tích lũy qua các sessions\nRESULTS_CSV  = os.path.join(OUTPUT_DIR, f\"ablation_results_{CURRENT_DATASET}.csv\")\nPROGRESS_FILE= os.path.join(OUTPUT_DIR, f\"ablation_progress_{CURRENT_DATASET}.json\")\n\ncfg = DATASET_CONFIGS[CURRENT_DATASET]\nprint(f\"Dataset  : {cfg['description']}\")\nprint(f\"Configs  : {CONFIGS_TO_RUN}\")\nprint(f\"Total runs: {len(CONFIGS_TO_RUN)} configs × {N_RUNS} seeds = {len(CONFIGS_TO_RUN)*N_RUNS}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-07T17:08:13.415115Z","iopub.execute_input":"2026-07-07T17:08:13.415654Z","iopub.status.idle":"2026-07-07T17:08:13.425235Z","shell.execute_reply.started":"2026-07-07T17:08:13.415627Z","shell.execute_reply":"2026-07-07T17:08:13.424300Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 3: ChannelAblationDataset + data loading utils\n# ============================================================\n\nclass ChannelAblationDataset(Dataset):\n    \"\"\"\n    Áp dụng make_channel_image logic lên ảnh RGB-PPS đã có:\n      R channel (idx 0) = Raw byte amplitude\n      G channel (idx 1) = Shannon entropy\n      B channel (idx 2) = LBP texture\n    Kênh không có trong config → set về 0 (raw uint8 space, trước normalize).\n    \"\"\"\n    def __init__(self, samples, classes, config: str, transform=None):\n        self.samples   = samples\n        self.targets   = [lbl for _, lbl in samples]\n        self.classes   = classes\n        self.use_R     = 'R' in config\n        self.use_G     = 'G' in config\n        self.use_B     = 'B' in config\n        self.transform = transform\n\n    def __len__(self): return len(self.samples)\n\n    def __getitem__(self, idx):\n        path, label = self.samples[idx]\n        img_np = np.array(Image.open(path).convert(\"RGB\"))\n        # make_channel_image: tạo zeros rồi điền kênh active\n        masked = np.zeros_like(img_np)          # dtype=uint8, range 0-255\n        if self.use_R: masked[:, :, 0] = img_np[:, :, 0]\n        if self.use_G: masked[:, :, 1] = img_np[:, :, 1]\n        if self.use_B: masked[:, :, 2] = img_np[:, :, 2]\n        img = Image.fromarray(masked)\n        if self.transform:\n            img = self.transform(img)\n        return img, label\n\n\nclass CSVImageDataset(Dataset):\n    \"\"\"Microsoft BIG-2015: ảnh phẳng + CSV label (copy từ eb3-training.ipynb)\"\"\"\n    def __init__(self, root, csv_path, transform=None):\n        df = pd.read_csv(csv_path)\n        unique_classes    = sorted(df[\"Class\"].unique())\n        self.class_to_idx = {c: i for i, c in enumerate(unique_classes)}\n        self.classes      = [str(c) for c in unique_classes]\n        existing          = set(os.listdir(root))\n        self.samples, self.targets = [], []\n        for _, row in df.iterrows():\n            fname = f\"{row['Id']}.png\"\n            if fname in existing:\n                lbl = self.class_to_idx[row[\"Class\"]]\n                self.samples.append((os.path.join(root, fname), lbl))\n                self.targets.append(lbl)\n        self.transform = transform\n        print(f\"  Loaded {len(self.samples)}/{len(df)} images, {len(self.classes)} classes\")\n\n    def __len__(self): return len(self.samples)\n\n    def __getitem__(self, idx):\n        path, label = self.samples[idx]\n        img = Image.open(path).convert(\"RGB\")\n        if self.transform: img = self.transform(img)\n        return img, label\n\n\ndef load_base_info(cfg):\n    \"\"\"Load (samples, classes, targets) từ dataset config.\"\"\"\n    if cfg.get(\"csv_path\"):\n        ds = CSVImageDataset(cfg[\"path\"], cfg[\"csv_path\"], transform=None)\n    else:\n        base = datasets.ImageFolder(cfg[\"path\"])\n        ds   = base\n    print(f\"  Base dataset: {len(ds.samples)} samples, {len(ds.classes)} classes\")\n    return list(ds.samples), list(ds.classes), list(ds.targets)\n\n\ndef get_transforms(is_rgb: bool):\n    norm_mean = [0.485, 0.456, 0.406] if is_rgb else [0.5, 0.5, 0.5]\n    norm_std  = [0.229, 0.224, 0.225] if is_rgb else [0.5, 0.5, 0.5]\n    base      = [transforms.Resize((IMG_SIZE, IMG_SIZE))]\n    if not is_rgb:\n        base.append(transforms.Grayscale(num_output_channels=3))\n    train_tf = transforms.Compose(base + [\n        transforms.ColorJitter(brightness=0.2, contrast=0.2),\n        transforms.ToTensor(),\n        transforms.Normalize(norm_mean, norm_std),\n    ])\n    val_tf = transforms.Compose(base + [\n        transforms.ToTensor(),\n        transforms.Normalize(norm_mean, norm_std),\n    ])\n    return train_tf, val_tf\n\n\ndef get_or_create_split(all_targets, dataset_name):\n    \"\"\"Load split JSON nếu có; nếu không → tạo mới và lưu.\"\"\"\n    spath = os.path.join(CKPT_DIR, f\"split_{dataset_name}.json\")\n    n       = len(all_targets)\n    n_test  = int(n * TEST_RATIO)\n    n_val   = int(n * VAL_RATIO)\n    n_train = n - n_val - n_test\n    if os.path.exists(spath):\n        saved = json.load(open(spath))\n        print(f\"  📂 Loaded split (seed={saved['seed']}, total={saved['total']})\")\n        return saved\n    # Tạo mới\n    indices = list(range(n))\n    gen = torch.Generator().manual_seed(SEED)\n    perm = torch.randperm(n, generator=gen).tolist()\n    split = {\n        \"train\": perm[:n_train],\n        \"val\"  : perm[n_train:n_train+n_val],\n        \"test\" : perm[n_train+n_val:],\n        \"seed\" : SEED, \"total\": n,\n    }\n    json.dump(split, open(spath, \"w\"))\n    print(f\"  💾 Created new split → {spath}\")\n    return split\n\n\ndef clear_memory():\n    gc.collect()\n    if AMP_ENABLED:\n        torch.cuda.empty_cache()\n        torch.cuda.synchronize()\n\n\nprint(\"✅ Dataset utils ready\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-07T17:08:13.426308Z","iopub.execute_input":"2026-07-07T17:08:13.426720Z","iopub.status.idle":"2026-07-07T17:08:13.469631Z","shell.execute_reply.started":"2026-07-07T17:08:13.426687Z","shell.execute_reply":"2026-07-07T17:08:13.468787Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 4: ResNet-50 model (khớp với resnet-50-training.ipynb)\n# ============================================================\n\ndef build_resnet50(num_classes: int) -> nn.Module:\n    model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)\n    for param in model.parameters():\n        param.requires_grad = False\n    model.fc = nn.Sequential(\n        nn.Dropout(0.3),\n        nn.Linear(2048, num_classes),\n    )\n    return model.to(DEVICE)\n\n\ndef unfreeze_last_blocks(base_model):\n    for param in base_model.layer3.parameters(): param.requires_grad = True\n    for param in base_model.layer4.parameters(): param.requires_grad = True\n    trainable = sum(p.numel() for p in base_model.parameters() if p.requires_grad)\n    print(f\"  >> Unfreeze layer3+layer4 | Trainable: {trainable/1e6:.1f}M\")\n\n\ndef train_one_epoch(model, loader, optimizer, criterion, scaler, accumulation_steps=2):\n    model.train()\n    amp_ctx    = autocast('cuda') if AMP_ENABLED else nullcontext()\n    total_loss = correct = total = 0\n    optimizer.zero_grad()\n    for step, (imgs, labels) in enumerate(loader):\n        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n        with amp_ctx:\n            outputs = model(imgs)\n            loss    = criterion(outputs, labels) / accumulation_steps\n        scaler.scale(loss).backward()\n        if (step + 1) % accumulation_steps == 0:\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n        total_loss += loss.item() * accumulation_steps\n        correct    += (outputs.detach().argmax(1) == labels).sum().item()\n        total      += labels.size(0)\n        del imgs, labels, outputs, loss\n    return total_loss / len(loader), correct / total\n\n\ndef evaluate(model, loader, criterion):\n    model.eval()\n    amp_ctx    = autocast('cuda') if AMP_ENABLED else nullcontext()\n    total_loss = 0\n    preds, trues = [], []\n    with torch.no_grad():\n        for imgs, labels in loader:\n            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n            with amp_ctx:\n                out        = model(imgs)\n                total_loss += criterion(out, labels).item()\n            preds.extend(out.argmax(1).cpu().numpy())\n            trues.extend(labels.cpu().numpy())\n            del imgs, labels, out\n    acc = np.mean(np.array(preds) == np.array(trues))\n    f1  = f1_score(trues, preds, average=\"macro\", zero_division=0)\n    return total_loss / len(loader), acc, f1, preds, trues\n\n\nprint(\"✅ Model + train/eval functions ready\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-07T17:08:13.471369Z","iopub.execute_input":"2026-07-07T17:08:13.471636Z","iopub.status.idle":"2026-07-07T17:08:13.490086Z","shell.execute_reply.started":"2026-07-07T17:08:13.471600Z","shell.execute_reply":"2026-07-07T17:08:13.489215Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 5: Single-run training function\n# ============================================================\n\ndef ckpt_path(name, run):   return os.path.join(CKPT_DIR, f\"best_{name}_run{run}.pt\")\ndef resume_path(name, run): return os.path.join(CKPT_DIR, f\"resume_{name}_run{run}.pt\")\n\n\ndef train_config_run(config, run, base_samples, base_classes,\n                     class_counts, split_json, cfg, train_tf, val_tf):\n    \"\"\"\n    Train ResNet-50 cho một config × một run.\n    Trả về (test_acc, test_f1).\n    \"\"\"\n    torch.manual_seed(SEED + run)\n    np.random.seed(SEED + run)\n\n    run_name = f\"{CURRENT_DATASET}_{config}\"\n    best_pt  = ckpt_path(run_name, run)\n    res_pt   = resume_path(run_name, run)\n\n    # ── Datasets ──────────────────────────────────────────────\n    train_full = ChannelAblationDataset(base_samples, base_classes, config, transform=train_tf)\n    val_full   = ChannelAblationDataset(base_samples, base_classes, config, transform=val_tf)\n\n    train_ds   = Subset(train_full, split_json[\"train\"])\n    val_ds     = Subset(val_full,   split_json[\"val\"])\n    test_ds    = Subset(val_full,   split_json[\"test\"])\n\n    sample_weights = np.array([\n        1.0 / class_counts[base_samples[i][1]] for i in split_json[\"train\"]\n    ])\n    sampler = WeightedRandomSampler(\n        torch.FloatTensor(sample_weights), len(sample_weights), replacement=True\n    )\n\n    train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, sampler=sampler,\n                              num_workers=2, pin_memory=True)\n    val_loader   = DataLoader(val_ds,   batch_size=BATCH_SIZE, shuffle=False,\n                              num_workers=2, pin_memory=True)\n    test_loader  = DataLoader(test_ds,  batch_size=BATCH_SIZE, shuffle=False,\n                              num_workers=2, pin_memory=True)\n\n    # ── Criterion ─────────────────────────────────────────────\n    w         = 1.0 / class_counts\n    w         = torch.FloatTensor(w / w.sum() * len(w)).to(DEVICE)\n    criterion = nn.CrossEntropyLoss(weight=w)\n\n    # ── Model ─────────────────────────────────────────────────\n    model      = build_resnet50(cfg[\"num_classes\"])\n    base_model = model\n    scaler     = GradScaler('cuda') if AMP_ENABLED else None\n    optimizer  = optim.AdamW(base_model.fc.parameters(), lr=LR_HEAD, weight_decay=1e-4)\n    scheduler  = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=FREEZE_EPOCHS)\n\n    best_val_f1 = 0.0\n    best_epoch  = 0\n    no_improve  = 0\n    start_epoch = 1\n    phase       = 1\n    history     = []\n\n    # ── Resume ────────────────────────────────────────────────\n    if os.path.exists(res_pt):\n        ckpt          = torch.load(res_pt, map_location=DEVICE)\n        base_model.load_state_dict(ckpt[\"model\"])\n        best_val_f1   = ckpt[\"best_val_f1\"]\n        best_epoch    = ckpt[\"best_epoch\"]\n        no_improve    = ckpt[\"no_improve\"]\n        start_epoch   = ckpt[\"epoch\"] + 1\n        history       = ckpt[\"history\"]\n        phase         = ckpt[\"phase\"]\n        if phase == 2:\n            unfreeze_last_blocks(base_model)\n            optimizer = optim.AdamW([\n                {\"params\": base_model.layer3.parameters(), \"lr\": LR_FINETUNE / 2},\n                {\"params\": base_model.layer4.parameters(), \"lr\": LR_FINETUNE},\n                {\"params\": base_model.fc.parameters(),     \"lr\": LR_FINETUNE * 2},\n            ], weight_decay=1e-4)\n            scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=UNFREEZE_EPOCHS)\n        optimizer.load_state_dict(ckpt[\"optimizer\"])\n        scheduler.load_state_dict(ckpt[\"scheduler\"])\n        if scaler: scaler.load_state_dict(ckpt[\"scaler\"])\n        print(f\"  🔄 Resume epoch {start_epoch} | Phase {phase} | Best F1: {best_val_f1:.4f}\")\n\n    # ── Header ────────────────────────────────────────────────\n    print(f\"\\n  {'Ep':>4} {'Ph':>3} │ \"\n          f\"{'TrLoss':>7} {'TrAcc':>7} │ \"\n          f\"{'ValLoss':>7} {'ValF1':>7} │ \"\n          f\"{'Best':>7} {'NoImp':>5}\")\n    print(f\"  {'─'*4} {'─'*3} ┼ {'─'*7} {'─'*7} ┼ {'─'*7} {'─'*7} ┼ {'─'*7} {'─'*5}\")\n\n    # ── Training loop ─────────────────────────────────────────\n    for epoch in range(start_epoch, TOTAL_EPOCHS + 1):\n\n        if epoch == FREEZE_EPOCHS + 1 and phase == 1:\n            phase = 2\n            unfreeze_last_blocks(base_model)\n            optimizer = optim.AdamW([\n                {\"params\": base_model.layer3.parameters(), \"lr\": LR_FINETUNE / 2},\n                {\"params\": base_model.layer4.parameters(), \"lr\": LR_FINETUNE},\n                {\"params\": base_model.fc.parameters(),     \"lr\": LR_FINETUNE * 2},\n            ], weight_decay=1e-4)\n            scheduler  = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=UNFREEZE_EPOCHS)\n            if AMP_ENABLED: scaler = GradScaler('cuda')\n            no_improve = 0\n            print(f\"\\n  ── Phase 2 (epoch {epoch}–{TOTAL_EPOCHS}) ──\\n\")\n\n        train_loss, train_acc = train_one_epoch(\n            model, train_loader, optimizer, criterion, scaler, ACCUMULATION_STEPS\n        )\n        val_loss, val_acc, val_f1, _, _ = evaluate(model, val_loader, criterion)\n        scheduler.step()\n\n        history.append({\n            \"epoch\": epoch, \"phase\": phase,\n            \"train_loss\": round(float(train_loss), 4),\n            \"val_loss\"  : round(float(val_loss),   4),\n            \"val_f1\"    : round(float(val_f1),      4),\n        })\n\n        if val_f1 > best_val_f1:\n            best_val_f1 = val_f1\n            best_epoch  = epoch\n            no_improve  = 0\n            torch.save(base_model.state_dict(), best_pt)\n            mark = \"💾\"\n        else:\n            no_improve += 1\n            mark = \"  \"\n\n        print(f\"  {epoch:>4} {phase:>3} │ \"\n              f\"{train_loss:>7.4f} {train_acc:>7.4f} │ \"\n              f\"{val_loss:>7.4f} {val_f1:>7.4f} │ \"\n              f\"{best_val_f1:>7.4f} {no_improve:>5} {mark}\")\n\n        # Resume checkpoint mỗi 5 epoch\n        if epoch % 5 == 0:\n            ckpt_state = {\n                \"epoch\": epoch, \"phase\": phase,\n                \"model\": base_model.state_dict(),\n                \"optimizer\": optimizer.state_dict(),\n                \"scheduler\": scheduler.state_dict(),\n                \"best_val_f1\": best_val_f1, \"best_epoch\": best_epoch,\n                \"no_improve\": no_improve, \"history\": history,\n            }\n            if scaler: ckpt_state[\"scaler\"] = scaler.state_dict()\n            torch.save(ckpt_state, res_pt)\n\n        if no_improve >= EARLY_STOP_PATIENCE:\n            print(f\"\\n  🛑 Early stop: F1 không tăng {no_improve} epochs\")\n            break\n\n        clear_memory()\n\n    # ── Test ──────────────────────────────────────────────────\n    print(f\"\\n  Best epoch: {best_epoch} | Best val F1: {best_val_f1:.4f}\")\n    base_model.load_state_dict(torch.load(best_pt, map_location=DEVICE))\n    _, test_acc, test_f1, preds, trues = evaluate(model, test_loader, criterion)\n    print(f\"  ✅ TEST — Acc: {test_acc:.4f} | F1-Macro: {test_f1:.4f}\")\n\n    # Cleanup\n    if os.path.exists(res_pt): os.remove(res_pt)\n    del model, optimizer, scheduler, train_loader, val_loader, test_loader\n    del train_full, val_full, train_ds, val_ds, test_ds, sampler\n    clear_memory()\n\n    return float(test_acc), float(test_f1)\n\n\nprint(\"✅ train_config_run() ready\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-07T17:08:13.490988Z","iopub.execute_input":"2026-07-07T17:08:13.491348Z","iopub.status.idle":"2026-07-07T17:08:13.514049Z","shell.execute_reply.started":"2026-07-07T17:08:13.491313Z","shell.execute_reply":"2026-07-07T17:08:13.513234Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 6: Chuẩn bị — load base info + split\n# ============================================================\n\nprint(f\"\\n{'='*60}\")\nprint(f\"  {cfg['description']}\")\nprint(f\"{'='*60}\")\n\nbase_samples, base_classes, base_targets = load_base_info(cfg)\nclass_counts = np.bincount(base_targets)\nsplit_json   = get_or_create_split(base_targets, CURRENT_DATASET)\ntrain_tf, val_tf = get_transforms(cfg[\"is_rgb\"])\n\nprint(f\"  Classes: {len(base_classes)} | \"\n      f\"Train: {len(split_json['train'])} | \"\n      f\"Val: {len(split_json['val'])} | \"\n      f\"Test: {len(split_json['test'])}\")\nprint(f\"  Imbalance — Min: {class_counts.min()} | Max: {class_counts.max()}\")\n\n# Load progress từ session trước (nếu có)\nif os.path.exists(PROGRESS_FILE):\n    with open(PROGRESS_FILE) as f:\n        progress = json.load(f)\n    print(f\"  📂 Loaded progress: {len(progress)} config(s) already done\")\nelse:\n    progress = {}\n    print(f\"  ⚡ No progress file — starting fresh\")\n\n# Load kết quả đã có (nếu CSV tồn tại)\nif os.path.exists(RESULTS_CSV):\n    existing_df = pd.read_csv(RESULTS_CSV)\n    all_results = existing_df.to_dict('records')\n    print(f\"  📂 Loaded {len(all_results)} existing rows from {RESULTS_CSV}\")\nelse:\n    all_results = []","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-07T17:08:13.514997Z","iopub.execute_input":"2026-07-07T17:08:13.515312Z","iopub.status.idle":"2026-07-07T17:08:14.266715Z","shell.execute_reply.started":"2026-07-07T17:08:13.515292Z","shell.execute_reply":"2026-07-07T17:08:14.265976Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 7: Main ablation loop\n# ============================================================\n\nfor config in CONFIGS_TO_RUN:\n\n    if config in progress:\n        print(f\"\\n⏭️  SKIP {config} — đã có {len(progress[config])} runs trong progress file\")\n        continue\n\n    print(f\"\\n{'='*60}\")\n    print(f\"  Config: [{config}]  |  Dataset: {cfg['description']}\")\n    print(f\"  Channels active: \"\n          f\"R={'✓' if 'R' in config else '✗'}  \"\n          f\"G={'✓' if 'G' in config else '✗'}  \"\n          f\"B={'✓' if 'B' in config else '✗'}\")\n    print(f\"{'='*60}\")\n\n    config_runs = []\n\n    for run in range(1, N_RUNS + 1):\n        print(f\"\\n  {'─'*54}\")\n        print(f\"  RUN {run}/{N_RUNS}  [{config}]\")\n        print(f\"  {'─'*54}\")\n\n        test_acc, test_f1 = train_config_run(\n            config, run,\n            base_samples, base_classes,\n            class_counts, split_json,\n            cfg, train_tf, val_tf,\n        )\n\n        row = {\n            \"config\"   : config,\n            \"dataset\"  : cfg[\"description\"],\n            \"seed\"     : run,\n            \"accuracy\" : round(test_acc, 4),\n            \"f1_macro\" : round(test_f1,  4),\n        }\n        config_runs.append(row)\n        all_results.append(row)\n\n        # Lưu CSV ngay sau mỗi run\n        pd.DataFrame(all_results).to_csv(RESULTS_CSV, index=False)\n        print(f\"  💾 Saved row → {RESULTS_CSV}\")\n\n    # Lưu progress sau khi xong toàn bộ N_RUNS cho config này\n    progress[config] = config_runs\n    with open(PROGRESS_FILE, \"w\") as f:\n        json.dump(progress, f, indent=2, ensure_ascii=False)\n\n    # Print per-config summary\n    accs = [r[\"accuracy\"] for r in config_runs]\n    f1s  = [r[\"f1_macro\"]  for r in config_runs]\n    print(f\"\\n  [{config}] Summary — \"\n          f\"Acc: {np.mean(accs):.4f}±{np.std(accs):.4f}  \"\n          f\"F1: {np.mean(f1s):.4f}±{np.std(f1s):.4f}\")\n\nprint(f\"\\n✅ Session complete — {len(CONFIGS_TO_RUN)} config(s) processed\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-07T17:08:14.267780Z","iopub.execute_input":"2026-07-07T17:08:14.268038Z","execution_failed":"2026-07-07T17:12:05.238Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 8: Tổng hợp kết quả hiện có\n# ============================================================\nRESULTS_CSV = '/kaggle/input/datasets/vnhtbo/rgboutput/ablation_results_microsoft_rgb_R_G_B.csv'\nif not os.path.exists(RESULTS_CSV):\n    print(\"Chưa có kết quả.\")\nelse:\n    df = pd.read_csv(RESULTS_CSV)\n\n    summary = (\n        df.groupby([\"dataset\", \"config\"])[[\"accuracy\", \"f1_macro\"]]\n        .agg([\"mean\", \"std\"])\n        .round(4)\n    )\n    summary.columns = [\"Acc mean\", \"Acc std\", \"F1 mean\", \"F1 std\"]\n\n    # Sắp xếp theo thứ tự config hợp lý\n    CONFIG_ORDER = ['R', 'G', 'B', 'RG', 'RB', 'RGB']\n    summary = summary.reset_index()\n    summary[\"config_order\"] = summary[\"config\"].map(\n        {c: i for i, c in enumerate(CONFIG_ORDER)}\n    ).fillna(99)\n    summary = summary.sort_values([\"dataset\", \"config_order\"]).drop(columns=\"config_order\")\n\n    print(\"=\" * 70)\n    print(\"  ABLATION RESULTS — MEAN ± STD (3 RUNS)\")\n    print(\"=\" * 70)\n    print(summary.to_string(index=False))\n\n    summary_path = os.path.join(OUTPUT_DIR, f\"ablation_summary_{CURRENT_DATASET}.csv\")\n    summary.to_csv(summary_path, index=False)\n    print(f\"\\n💾 Saved: {summary_path}\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-07T17:12:05.239Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 9: Visualization — bar chart ablation\n# ============================================================\n\nif not os.path.exists(RESULTS_CSV):\n    print(\"Chưa có kết quả để vẽ.\")\nelse:\n    df = pd.read_csv(RESULTS_CSV)\n    CONFIG_ORDER = ['R', 'G', 'B', 'RG', 'RB', 'RGB']\n\n    pivot_f1  = (\n        df.groupby([\"dataset\", \"config\"])[\"f1_macro\"]\n        .agg([\"mean\", \"std\"])\n        .reset_index()\n    )\n    pivot_f1[\"config_order\"] = pivot_f1[\"config\"].map(\n        {c: i for i, c in enumerate(CONFIG_ORDER)}\n    ).fillna(99)\n    pivot_f1 = pivot_f1.sort_values([\"dataset\", \"config_order\"])\n\n    datasets_present = pivot_f1[\"dataset\"].unique()\n    fig, axes = plt.subplots(1, len(datasets_present),\n                              figsize=(6 * len(datasets_present), 5),\n                              sharey=False)\n    if len(datasets_present) == 1:\n        axes = [axes]\n\n    colors = ['#e74c3c','#2ecc71','#3498db','#f39c12','#9b59b6','#1abc9c']\n\n    for ax, ds_name in zip(axes, datasets_present):\n        sub = pivot_f1[pivot_f1[\"dataset\"] == ds_name].copy()\n        bars = ax.bar(\n            sub[\"config\"], sub[\"mean\"],\n            yerr=sub[\"std\"],\n            capsize=5, color=colors[:len(sub)],\n            edgecolor=\"white\", linewidth=0.8,\n            error_kw={\"elinewidth\": 1.5, \"ecolor\": \"gray\"},\n        )\n        for bar, (_, row) in zip(bars, sub.iterrows()):\n            ax.text(\n                bar.get_x() + bar.get_width() / 2,\n                bar.get_height() + row[\"std\"] + 0.003,\n                f\"{row['mean']:.3f}\",\n                ha=\"center\", va=\"bottom\", fontsize=8,\n            )\n        ax.set_title(f\"{ds_name}\\nPer-Channel Ablation — F1-Macro\",\n                     fontsize=11, fontweight=\"bold\")\n        ax.set_xlabel(\"Channel Config\", fontsize=10)\n        ax.set_ylabel(\"F1-Macro (mean ± std)\", fontsize=10)\n        ax.set_ylim(0, 1.05)\n        ax.grid(axis=\"y\", alpha=0.3)\n\n    plt.tight_layout()\n    fig_path = os.path.join(OUTPUT_DIR, f\"ablation_bar_{CURRENT_DATASET}.png\")\n    plt.savefig(fig_path, dpi=200, bbox_inches=\"tight\")\n    plt.show()\n    print(f\"💾 {fig_path}\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-07T17:12:05.239Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## Files đầu ra\n\n| File | Nội dung |\n|---|---|\n| `ablation_results_{dataset}.csv` | Per-run: config, dataset, seed, accuracy, f1_macro |\n| `ablation_summary_{dataset}.csv` | Mean ± std per config (dùng cho Table V.B trong paper) |\n| `ablation_progress_{dataset}.json` | Resume state — xóa nếu muốn retrain từ đầu |\n| `checkpoints/best_{dataset}_{config}_run{N}.pt` | Best checkpoint mỗi run |\n| `ablation_bar_{dataset}.png` | Bar chart F1-Macro per config |\n\n**Quy trình nhiều session:**\n1. Session 2.1 → `CONFIGS_TO_RUN = ['G', 'B']`, `CURRENT_DATASET = 'malimg_rgb'`\n2. Session 2.2 (song song) → `CONFIGS_TO_RUN = ['G', 'B']`, `CURRENT_DATASET = 'microsoft_rgb'`\n3. Session 2.3 → `CONFIGS_TO_RUN = ['RG', 'RB']`, `CURRENT_DATASET = 'malimg_rgb'`\n4. Session 2.4 (song song) → `CONFIGS_TO_RUN = ['RG', 'RB']`, `CURRENT_DATASET = 'microsoft_rgb'`\n\nKết quả R và RGB lấy từ paper gốc; notebook này train 4 configs còn lại.","metadata":{}}]}