{"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":"none","dataSources":[{"sourceType":"competition","sourceId":4117,"databundleVersionId":46665},{"sourceType":"datasetVersion","sourceId":15842073,"datasetId":10155541,"databundleVersionId":16792585},{"sourceType":"datasetVersion","sourceId":15850178,"datasetId":10161020,"databundleVersionId":16801351}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Phase 1 — Data Preparation\n\n> **Skip Phase 1** nếu đã có precomputed dataset — Phase 2 tự động nhận diện.\n","metadata":{}},{"cell_type":"markdown","source":"## A. Bootstrap\n","metadata":{}},{"cell_type":"code","source":"import sys, gc, time, logging, zipfile, shutil\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\n\nCOMP_DIR   = Path(\"/kaggle/input/competitions/malware-classification\")\nUTILS_PATH = Path(\"/kaggle/input/datasets/bnthanh/malware-utils-v4\")\nif str(UTILS_PATH) not in sys.path: sys.path.append(str(UTILS_PATH))\nfrom malware_utils_v4 import Config, GPU, FileOps, ImageOps, OpcodeOps, CacheOps\n\nlogging.basicConfig(level=logging.INFO,\n    format=\"%(asctime)s [%(levelname)s] %(message)s\", datefmt=\"%H:%M:%S\",\n    handlers=[logging.StreamHandler(sys.stdout),\n              logging.FileHandler(\"/kaggle/working/phase1.log\")])\nlog = logging.getLogger(\"phase1\")\n\ncfg = Config(base_dir=COMP_DIR, batch_n=100, unigram_k=500, bigram_k=1000, trigram_k=500, min_free_gb=5.0)\ncfg.make_dirs()\nGPU.setup()\nlog.info(\"train.7z exists=%s\", cfg.train_7z.exists())\nlog.info(\"Disk free: %.1f GB\", FileOps.get_free_gb())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T07:19:04.327880Z","iopub.execute_input":"2026-07-23T07:19:04.328160Z","iopub.status.idle":"2026-07-23T07:19:07.714201Z","shell.execute_reply.started":"2026-07-23T07:19:04.328125Z","shell.execute_reply":"2026-07-23T07:19:07.713399Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## B. Load Labels & Plan Batches\n","metadata":{}},{"cell_type":"code","source":"df_labels = pd.read_csv(cfg.label_file)\ndf_labels.columns = df_labels.columns.str.strip()\nfor col in df_labels.columns:\n    if col.lower() in (\"id\",\"name\"): df_labels = df_labels.rename(columns={col:\"Id\"})\n    if col.lower() == \"class\":        df_labels = df_labels.rename(columns={col:\"Class\"})\nALL_IDS = df_labels[\"Id\"].tolist()\n\ncached_ids       = CacheOps.get_cached_ids(cfg.cache_img_dir)\nexisting_records = CacheOps.load_raw_counts(cfg.raw_counts_file)\nexisting_raw_ids = {r[\"id\"] for r in existing_records}\ntodo_ids = [s for s in ALL_IDS if s not in cached_ids and s not in existing_raw_ids]\nlog.info(\"Total=%d | Done=%d | Todo=%d\", len(ALL_IDS), len(ALL_IDS)-len(todo_ids), len(todo_ids))\n\nif todo_ids:\n    archive_order = FileOps.get_archive_order(cfg.train_7z)\n    todo_ids      = FileOps.sort_by_archive_order(todo_ids, archive_order)\nbatches = list(FileOps.chunked(todo_ids, cfg.batch_n))\nlog.info(\"%d batches x %d files\", len(batches), cfg.batch_n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T07:19:07.715137Z","iopub.execute_input":"2026-07-23T07:19:07.715489Z","iopub.status.idle":"2026-07-23T07:19:08.094009Z","shell.execute_reply.started":"2026-07-23T07:19:07.715464Z","shell.execute_reply":"2026-07-23T07:19:08.093406Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## C. Extract & Build Features\n","metadata":{}},{"cell_type":"code","source":"# ── Pass 1: batch streaming ────────────────────────────────────────────────\nimport pickle\nt_start = time.time()\nCacheOps.clear_batch_records(cfg.batch_records_dir)\n\ndef format_counts(sid, bp, ap):\n    c = OpcodeOps.parse_advanced(ap) if ap else {\"id\":sid,\"uni\":{},\"bi\":{},\"tri\":{},\"sec\":{},\"api\":{},\"stats\":{}}\n    c[\"id\"] = sid\n    bs = bp.stat().st_size if bp else 0; as_ = ap.stat().st_size if ap else 0\n    c[\"meta\"] = {\"bytes_size\":bs,\"asm_size\":as_,\"size_ratio\":as_/bs if bs>0 else 0.}\n    return c\n\nfor batch_idx, batch_ids in enumerate(batches):\n    if FileOps.get_free_gb() < cfg.min_free_gb: break\n    log.info(\"Batch %03d/%03d\", batch_idx+1, len(batches))\n    pairs = FileOps.extract_batch(cfg.train_7z, batch_ids, cfg.scratch_dir)\n    recs  = []\n    for sid, bp, ap in pairs:\n        try:\n            CacheOps.save_image(ImageOps.process_image(bp,ap,cfg), sid, cfg.cache_img_dir)\n            recs.append(format_counts(sid, bp, ap))\n        except Exception as e: log.debug(\"Fail %s: %s\", sid, e)\n        FileOps.delete_raw(bp, ap)\n    CacheOps.save_batch_records(recs, batch_idx, cfg.batch_records_dir)\n    gc.collect(); GPU.free()\n    shutil.rmtree(cfg.scratch_dir, ignore_errors=True); cfg.scratch_dir.mkdir(exist_ok=True)\nlog.info(\"Pass 1 done: %.1f min\", (time.time()-t_start)/60)\n\n# ── Pass 2: Vocabulary + tabular CSV ────────────────────────────────────────\nvocab = OpcodeOps.build_vocabulary(\n    CacheOps.iter_all_records(cfg.batch_records_dir),\n    unigram_k=cfg.unigram_k, bigram_k=cfg.bigram_k, trigram_k=cfg.trigram_k)\nCacheOps.save_vocabulary(vocab[\"uni\"], vocab[\"bi\"], cfg.features_dir/\"vocabulary.pkl\")\nwith open(cfg.features_dir/\"vocab_full.pkl\",\"wb\") as f: pickle.dump(vocab,f,protocol=4)\nlog.info(\"vocab: uni=%d bi=%d tri=%d sec=%d api=%d\",\n         len(vocab[\"uni\"]),len(vocab[\"bi\"]),len(vocab[\"tri\"]),len(vocab[\"sec\"]),len(vocab[\"api\"]))\n\nSTAT_COLS = [\"n_funcs\",\"n_opcodes\",\"unique_ops\"]\ndf_tab = OpcodeOps.create_feature_df(\n    CacheOps.iter_all_records(cfg.batch_records_dir), vocab, STAT_COLS)\ndf_tab = df_tab.merge(df_labels[[\"Id\",\"Class\"]].set_index(\"Id\"),\n                      left_index=True, right_index=True, how=\"left\")\ndf_tab.to_csv(cfg.features_dir/\"tabular_features.csv\")\nlog.info(\"Saved tabular_features.csv: %s\", df_tab.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T07:19:08.095328Z","iopub.execute_input":"2026-07-23T07:19:08.095590Z","execution_failed":"2026-07-23T07:19:53.120Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## D. Verify & Package\n","metadata":{}},{"cell_type":"code","source":"npy_files   = list(cfg.cache_img_dir.glob(\"*.npy\"))\nmissing_ids = set(ALL_IDS) - {p.stem for p in npy_files}\nprint(f\"Images  : {len(npy_files):,} / {len(ALL_IDS):,}  Missing: {len(missing_ids)}\")\nprint(f\"Tabular : {df_tab.shape}  Disk: {FileOps.get_free_gb():.1f} GB\")\n\nzip_path = cfg.features_dir/\"image_features.zip\"\nwith zipfile.ZipFile(zip_path,\"w\",compression=zipfile.ZIP_STORED,allowZip64=True) as zf:\n    for p in tqdm(npy_files,desc=\"Zip\"): zf.write(p,arcname=p.name)\nwith zipfile.ZipFile(zip_path) as zf: n_zip = len(zf.namelist())\nif n_zip == len(npy_files):\n    shutil.rmtree(cfg.cache_img_dir, ignore_errors=True)\n    log.info(\"Zip OK (%d files) — Phase 1 DONE\", n_zip)\nelse:\n    log.error(\"Zip mismatch %d vs %d — GIU .npy!\", n_zip, len(npy_files))\ncfg.raw_counts_file.unlink(missing_ok=True)\n\nfor fname in [\"image_features.zip\",\"tabular_features.csv\",\"vocabulary.pkl\",\"vocab_full.pkl\"]:\n    p = cfg.features_dir/fname\n    mb = p.stat().st_size/1024**2 if p.exists() else -1\n    print(f\"  {chr(10003) if p.exists() else chr(10007)}  {fname:<35} {mb:>8.1f} MB\")\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-23T07:19:53.134Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Phase 2+3 — CNN + XGBoost + LightGBM + CatBoost\n\n## Mode auto-detection\n| Mode | Condition | Time |\n|------|-----------|------|\n| **FAST** | Precomputed dataset detected at `/kaggle/input/datasets/bnthanh/malwareclassificationpreprocessdata` | ~30 min |\n| **FULL** | No precomputed dataset — runs from Phase 1 outputs | ~9.5 hours |\n\n**Output files:** `submission_ensemble.csv` (best) + `submission_xgb.csv` + `submission_lgb.csv` + `submission_cb.csv`\n","metadata":{}},{"cell_type":"markdown","source":"## A. Setup — Imports, Constants, Auto-detect Mode\n","metadata":{}},{"cell_type":"code","source":"import sys, os, gc, time, logging, zipfile, shutil, pickle\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\nfrom scipy.optimize import minimize\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.models as models\nimport torchvision.transforms as T\n\nimport xgboost as xgb\nimport lightgbm as lgb\nfrom catboost import CatBoostClassifier, Pool\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.utils.class_weight import compute_sample_weight\nfrom sklearn.metrics import accuracy_score, log_loss\n\n# ── Logging ─────────────────────────────────────────────────────────────────\nfor h in logging.root.handlers[:]: logging.root.removeHandler(h)\nlogging.basicConfig(level=logging.INFO,\n    format=\"%(asctime)s [%(levelname)s] %(message)s\", datefmt=\"%H:%M:%S\",\n    handlers=[logging.StreamHandler(sys.stdout),\n              logging.FileHandler(\"/kaggle/working/pipeline.log\")])\nlog = logging.getLogger(\"pipeline\")\n\n# ── Constants ────────────────────────────────────────────────────────────────\nSEED, N_FOLDS, N_CLASSES = 42, 10, 9\nCLASS_NAMES = {1:\"Ramnit\",2:\"Lollipop\",3:\"Kelihos_ver3\",4:\"Vundo\",5:\"Simda\",\n               6:\"Tracur\",7:\"Kelihos_ver1\",8:\"Obfuscator.ACY\",9:\"Gatak\"}\n\n# ── Fixed paths ──────────────────────────────────────────────────────────────\nWORK_DIR  = Path(\"/kaggle/working\")\nCOMP_DIR  = Path(\"/kaggle/input/competitions/malware-classification\")\nIMG_DIR   = WORK_DIR / \"train_images\"\nSEL_PATH  = WORK_DIR / \"feature_selection.npy\"  # always writable\nUTILS_PATH = Path('/kaggle/input/datasets/bnthanh/malware-utils-v4')\nif str(UTILS_PATH) not in sys.path: sys.path.append(str(UTILS_PATH))\nfrom malware_utils_v4 import Config, GPU, FileOps, ImageOps, OpcodeOps, CacheOps\ncfg    = Config(base_dir=COMP_DIR)\nGPU.setup()\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nMEMORY_SAFE = True\nDL_TRAIN_KWARGS   = dict(batch_size=16, shuffle=True,  num_workers=0, pin_memory=False)\nDL_EXTRACT_KWARGS = dict(batch_size=8,  shuffle=False, num_workers=0, pin_memory=False)\nif not MEMORY_SAFE:\n    DL_TRAIN_KWARGS.update(batch_size=32, num_workers=4, pin_memory=True)\n    DL_EXTRACT_KWARGS.update(batch_size=32, num_workers=4, pin_memory=True)\n\n# ── AUTO-DETECT ──────────────────────────────────────────────────────────────\n# Thay doi ten dataset neu ban upload voi ten khac\n_PRECOMPUTED = Path(\"/kaggle/input/datasets/bnthanh/malwareclassificationpreprocessdata/features\")\nHAS_PRECOMPUTED = (\n    _PRECOMPUTED.exists() and\n    (_PRECOMPUTED / \"cnn_features_test.npy\").exists() and\n    (_PRECOMPUTED / \"tabular_features.csv\").exists()\n)\nif HAS_PRECOMPUTED:\n    FEAT_DIR = _PRECOMPUTED           # read-only: doc features tu precomputed\n    MODE     = \"FAST\"\nelse:\n    FEAT_DIR = WORK_DIR / \"features\"  # writable: Phase 1 da build vao day\n    FEAT_DIR.mkdir(parents=True, exist_ok=True)\n    MODE     = \"FULL\"\n\nIMG_DIR.mkdir(parents=True, exist_ok=True)\n(WORK_DIR / \"features\").mkdir(parents=True, exist_ok=True)\n\nlog.info(\"=\"*60)\nlog.info(\"  Mode     : %s\", MODE)\nlog.info(\"  FEAT_DIR : %s\", FEAT_DIR)\nlog.info(\"  Device   : %s\", DEVICE)\nlog.info(\"  Disk     : %.1f GB free\", FileOps.get_free_gb())\nlog.info(\"=\"*60)\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-23T07:19:53.135Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## B. Load Features\n","metadata":{}},{"cell_type":"code","source":"# ── Labels ──────────────────────────────────────────────────────────────────\ndf_labels = pd.read_csv(COMP_DIR / \"trainLabels.csv\")\ndf_labels.columns = df_labels.columns.str.strip()\nfor c in df_labels.columns:\n    if c.lower() in (\"id\",\"name\"): df_labels = df_labels.rename(columns={c:\"Id\"})\n    if c.lower() == \"class\":       df_labels = df_labels.rename(columns={c:\"Class\"})\ndf_labels[\"label\"] = df_labels[\"Class\"] - 1\nALL_IDS  = df_labels[\"Id\"].tolist()\nTEST_IDS = pd.read_csv(COMP_DIR / \"sampleSubmission.csv\")[\"Id\"].tolist()  # submission order\nlog.info(\"Train: %d  |  Test: %d\", len(ALL_IDS), len(TEST_IDS))\n\n# ── Vocabulary ───────────────────────────────────────────────────────────────\nMETA_COLS = [\"bytes_size\",\"asm_size\",\"size_ratio\"]\nSTAT_COLS = [\"n_funcs\",\"n_opcodes\",\"unique_ops\"]\n\nvocab_path = FEAT_DIR / \"vocab_full.pkl\"\nif vocab_path.exists():\n    with open(vocab_path,\"rb\") as f: vocab = pickle.load(f)\n    log.info(\"vocab: uni=%d bi=%d tri=%d sec=%d api=%d\",\n             len(vocab[\"uni\"]),len(vocab[\"bi\"]),len(vocab[\"tri\"]),len(vocab[\"sec\"]),len(vocab[\"api\"]))\nelse:\n    top_uni, top_bi = CacheOps.load_vocabulary(FEAT_DIR/\"vocabulary.pkl\")\n    vocab = {\"uni\":top_uni,\"bi\":top_bi,\"tri\":[],\"sec\":[],\"api\":[]}\n    log.warning(\"vocab_full.pkl not found - fallback to vocabulary.pkl (uni+bi only)\")\n\n# ── Tabular features (train) ─────────────────────────────────────────────────\ndf_tab = pd.read_csv(FEAT_DIR/\"tabular_features.csv\", index_col=\"Id\")\nlog.info(\"Tabular loaded: %s\", df_tab.shape)\ndf_meta = df_tab[META_COLS].copy()\ndf_tab  = df_tab.drop(columns=META_COLS)\n_       = df_tab.pop(\"Class\") if \"Class\" in df_tab.columns else None\n\nprint(f\"Tabular features: {df_tab.shape}  |  Mode: {MODE}\")\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-23T07:19:53.135Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## C. Extract Train Images\n","metadata":{}},{"cell_type":"code","source":"zip_path = FEAT_DIR / \"image_features.zip\"\nalready  = list(IMG_DIR.glob(\"*.npy\"))\nif len(already) >= len(ALL_IDS) > 0:\n    log.info(\"Train images OK: %d files - skip\", len(already))\nelif zip_path.exists():\n    t0 = time.time()\n    with zipfile.ZipFile(zip_path,\"r\") as zf: zf.extractall(IMG_DIR)\n    log.info(\"Extracted %d files in %.1fs\", len(list(IMG_DIR.glob(\"*.npy\"))), time.time()-t0)\nelse:\n    log.warning(\"image_features.zip not found; IMG_DIR has %d .npy files\", len(already))\nlog.info(\"Disk free: %.1f GB\", FileOps.get_free_gb())\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-23T07:19:53.135Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## D. CNN Fine-tune + TTA Extraction\n","metadata":{}},{"cell_type":"code","source":"class MalwareImageDataset(Dataset):\n    def __init__(self, sample_ids, img_dir, label_map=None):\n        self.ids, self.img_dir, self.label_map = sample_ids, img_dir, label_map\n        self.tf = T.Compose([T.ToTensor(),T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])])\n    def __len__(self): return len(self.ids)\n    def __getitem__(self, idx):\n        sid = self.ids[idx]\n        img = self.tf(np.load(self.img_dir / f\"{sid}.npy\"))\n        return (img, self.label_map[sid]) if self.label_map else (img, sid)\n\nclass TTADataset(Dataset):\n    N_AUG = 8\n    def __init__(self, sample_ids, img_dir):\n        self.ids, self.img_dir = sample_ids, img_dir\n        self.norm = T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n    def __len__(self): return len(self.ids)\n    def __getitem__(self, idx):\n        sid = self.ids[idx]\n        t = torch.from_numpy(np.load(self.img_dir/f\"{sid}.npy\")).permute(2,0,1).float()/255.\n        augs = [t, t.flip(-1), t.flip(-2),\n                t.rot90(1,[-2,-1]), t.rot90(2,[-2,-1]), t.rot90(3,[-2,-1]),\n                t.flip(-1).rot90(1,[-2,-1]), t.flip(-2).rot90(1,[-2,-1])]\n        return torch.stack([self.norm(a) for a in augs]), sid\n\ndef extract_with_tta(model, sample_ids, img_dir, device, dl_kwargs):\n    model.eval()\n    loader = DataLoader(TTADataset(sample_ids, img_dir), **dl_kwargs)\n    all_feats, all_probs, all_ids = [], [], []\n    with torch.no_grad():\n        for aug_batch, ids in tqdm(loader, desc=\"TTA\", ncols=80):\n            B, N, C, H, W = aug_batch.shape\n            flat  = aug_batch.view(B*N, C, H, W).to(device)\n            feats = model(flat, return_features=True)\n            probs = torch.softmax(model.head(feats), dim=-1)\n            feats = feats.view(B, N, -1).mean(1)\n            probs = probs.view(B, N, -1).mean(1)\n            all_feats.append(feats.cpu().numpy())\n            all_probs.append(probs.cpu().numpy())\n            all_ids.extend(list(ids))\n    return np.vstack(all_feats), np.vstack(all_probs), all_ids\n\nclass CNNExtractor(nn.Module):\n    def __init__(self, unfreeze_blocks=2, num_classes=9):\n        super().__init__()\n        bb            = models.efficientnet_b3(weights=\"IMAGENET1K_V1\")\n        self.features = bb.features\n        self.avgpool  = bb.avgpool\n        self.head     = nn.Linear(3072, num_classes)\n        for p in self.parameters(): p.requires_grad = False\n        for block in list(self.features.children())[-unfreeze_blocks:]:\n            for p in block.parameters(): p.requires_grad = True\n        for p in self.head.parameters(): p.requires_grad = True\n    def forward(self, x, return_features=True):\n        x    = self.features(x)\n        feat = torch.cat([self.avgpool(x).flatten(1),\n                          F.adaptive_max_pool2d(x,1).flatten(1)], dim=1)\n        return feat if return_features else self.head(feat)\n\n# ── File paths ────────────────────────────────────────────────────────────────\nCNN_FEAT_PATH  = FEAT_DIR / \"cnn_features_train.npy\"\nCNN_PROBS_PATH = FEAT_DIR / \"cnn_probs_train.npy\"\nCNN_IDS_PATH   = FEAT_DIR / \"cnn_ids_train.pkl\"\nCNN_MODEL_PATH = WORK_DIR / \"features\" / \"best_cnn.pth\"  # always writable\n\nif CNN_FEAT_PATH.exists() and CNN_PROBS_PATH.exists():\n    log.info(\"Train CNN found - loading\")\n    cnn_feat_train  = np.load(CNN_FEAT_PATH,  mmap_mode=\"r\")\n    cnn_probs_train = np.load(CNN_PROBS_PATH, mmap_mode=\"r\")\n    with open(CNN_IDS_PATH,\"rb\") as f: cnn_ids_train = pickle.load(f)\nelse:\n    label_map    = dict(zip(df_labels[\"Id\"], df_labels[\"label\"]))\n    train_loader = DataLoader(MalwareImageDataset(ALL_IDS, IMG_DIR, label_map), **DL_TRAIN_KWARGS)\n    model        = CNNExtractor(unfreeze_blocks=2).to(DEVICE)\n    optimizer    = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-5)\n    criterion    = nn.CrossEntropyLoss()\n    log.info(\"Fine-tuning EfficientNet-B3 - 3 epochs\")\n    model.train()\n    for epoch in range(3):\n        t0 = time.time(); loss_sum = 0\n        for imgs, labels in tqdm(train_loader, desc=f\"Epoch {epoch+1}\", ncols=80):\n            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n            optimizer.zero_grad()\n            l = criterion(model(imgs, return_features=False), labels)\n            l.backward(); optimizer.step(); loss_sum += l.item()\n        log.info(\"Epoch %d | loss=%.4f | %.1fs\", epoch+1, loss_sum/len(train_loader), time.time()-t0)\n    torch.save(model.state_dict(), CNN_MODEL_PATH)\n    model.eval()\n    cnn_feat_train, cnn_probs_train, cnn_ids_train = extract_with_tta(\n        model, ALL_IDS, IMG_DIR, DEVICE, DL_EXTRACT_KWARGS)\n    np.save(WORK_DIR/\"features\"/\"cnn_features_train.npy\", cnn_feat_train)\n    np.save(WORK_DIR/\"features\"/\"cnn_probs_train.npy\",    cnn_probs_train)\n    with open(WORK_DIR/\"features\"/\"cnn_ids_train.pkl\",\"wb\") as f: pickle.dump(cnn_ids_train,f,protocol=4)\n    log.info(\"Train CNN: feats=%s probs=%s\", cnn_feat_train.shape, cnn_probs_train.shape)\n    del model; torch.cuda.empty_cache(); gc.collect()\n\nprint(f\"Train CNN: feats={cnn_feat_train.shape}  probs={cnn_probs_train.shape}\")\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-23T07:19:53.136Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## E. Test Set Processing\n","metadata":{}},{"cell_type":"code","source":"TEST_TAB_PATH     = FEAT_DIR / \"tabular_features_test.csv\"\nTEST_CNN_PATH     = FEAT_DIR / \"cnn_features_test.npy\"\nTEST_PROBS_PATH   = FEAT_DIR / \"cnn_probs_test.npy\"\nCNN_IDS_TEST_PATH = WORK_DIR / \"features\" / \"cnn_ids_test.pkl\"  # always writable\nTEST_IMG_DIR      = WORK_DIR / \"test_images\"; TEST_IMG_DIR.mkdir(exist_ok=True)\n\n# ── Step 1: Tabular features ─────────────────────────────────────────────────\nif TEST_TAB_PATH.exists():\n    df_test_tab = pd.read_csv(TEST_TAB_PATH, index_col=\"Id\")\n    log.info(\"Test tabular loaded: %s\", df_test_tab.shape)\nelse:\n    test_archive  = COMP_DIR / \"test.7z\"\n    _arch_order   = FileOps.get_archive_order(test_archive)\n    _extr_order   = FileOps.sort_by_archive_order(TEST_IDS, _arch_order)  # efficient I/O\n    test_batch_dir = WORK_DIR / \"test_batches\"\n    CacheOps.clear_batch_records(test_batch_dir)\n    log.info(\"Extracting test batches...\")\n    for b_idx, b_ids in enumerate(tqdm(list(FileOps.chunked(_extr_order, cfg.batch_n)), desc=\"Test\")):\n        pairs = FileOps.extract_batch(test_archive, b_ids, cfg.scratch_dir)\n        recs  = []\n        for sid, bp, ap in pairs:\n            try:\n                np.save(TEST_IMG_DIR/f\"{sid}.npy\", ImageOps.process_image(bp,ap,cfg))\n                r = OpcodeOps.parse_advanced(ap) if ap else {\"id\":sid,\"uni\":{},\"bi\":{},\"tri\":{},\"sec\":{},\"api\":{},\"stats\":{}}\n                r[\"id\"] = sid\n                bs = bp.stat().st_size if bp else 0; as_ = ap.stat().st_size if ap else 0\n                r[\"meta\"] = {\"bytes_size\":bs,\"asm_size\":as_,\"size_ratio\":as_/bs if bs>0 else 0.}\n                recs.append(r)\n            except: pass\n            FileOps.delete_raw(bp, ap)\n        CacheOps.save_batch_records(recs, b_idx, test_batch_dir)\n        gc.collect(); GPU.free()\n    df_test_tab = OpcodeOps.create_feature_df(\n        CacheOps.iter_all_records(test_batch_dir), vocab, STAT_COLS)\n    df_test_tab.to_csv(WORK_DIR/\"features\"/\"tabular_features_test.csv\")\n    log.info(\"Test tabular built: %s\", df_test_tab.shape)\n\nif all(c in df_test_tab.columns for c in META_COLS):\n    df_test_meta = df_test_tab[META_COLS].copy()\n    df_test_tab  = df_test_tab.drop(columns=META_COLS)\nelse:\n    df_test_meta = pd.DataFrame(0, index=df_test_tab.index, columns=META_COLS)\n\n# ── Step 2: CNN features ─────────────────────────────────────────────────────\nif TEST_CNN_PATH.exists() and TEST_PROBS_PATH.exists():\n    cnn_feat_test  = np.load(TEST_CNN_PATH,   mmap_mode=\"r\")\n    cnn_probs_test = np.load(TEST_PROBS_PATH, mmap_mode=\"r\")\n    if CNN_IDS_TEST_PATH.exists():\n        with open(CNN_IDS_TEST_PATH,\"rb\") as f: cnn_ids_test = pickle.load(f)\n        log.info(\"Test CNN loaded: feats=%s  ids from pkl\", cnn_feat_test.shape)\n    else:\n        # Precomputed dataset: pkl not saved -> reconstruct archive order\n        _arch_order  = FileOps.get_archive_order(COMP_DIR / \"test.7z\")\n        cnn_ids_test = FileOps.sort_by_archive_order(TEST_IDS, _arch_order)\n        log.info(\"Test CNN loaded: feats=%s  ids reconstructed from archive order\", cnn_feat_test.shape)\nelse:\n    log.info(\"Extracting test CNN features with 8-fold TTA...\")\n    model = CNNExtractor(unfreeze_blocks=2, num_classes=N_CLASSES).to(DEVICE)\n    if CNN_MODEL_PATH.exists():\n        model.load_state_dict(torch.load(CNN_MODEL_PATH, map_location=DEVICE))\n    else:\n        raise FileNotFoundError(f\"CNN weights not found: {CNN_MODEL_PATH}\")\n    _arch_order   = FileOps.get_archive_order(COMP_DIR/\"test.7z\")\n    _extr_order   = FileOps.sort_by_archive_order(TEST_IDS, _arch_order)\n    cnn_feat_test, cnn_probs_test, cnn_ids_test = extract_with_tta(\n        model, _extr_order, TEST_IMG_DIR, DEVICE, DL_EXTRACT_KWARGS)\n    np.save(WORK_DIR/\"features\"/\"cnn_features_test.npy\",  cnn_feat_test)\n    np.save(WORK_DIR/\"features\"/\"cnn_probs_test.npy\",     cnn_probs_test)\n    with open(CNN_IDS_TEST_PATH,\"wb\") as f: pickle.dump(cnn_ids_test, f, protocol=4)\n    log.info(\"Test CNN done: feats=%s  ids saved\", cnn_feat_test.shape)\n    del model; torch.cuda.empty_cache(); gc.collect()\n\nprint(f\"Test CNN: feats={cnn_feat_test.shape}  probs={cnn_probs_test.shape}\")\nprint(f\"Test tab: {df_test_tab.shape}  meta: {df_test_meta.shape}\")\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-23T07:19:53.136Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## F. Assemble Feature Matrix\n","metadata":{}},{"cell_type":"code","source":"log.info(\"Assembling feature matrices...\")\n\n# ── Train: align CNN -> ALL_IDS order ────────────────────────────────────────\n_cnn_tr_idx       = [dict((s,i) for i,s in enumerate(cnn_ids_train))[sid] for sid in ALL_IDS]\nX_cnn_tr          = np.array(cnn_feat_train)[_cnn_tr_idx]   # (N, 3072)\nX_cnn_probs_tr    = np.array(cnn_probs_train)[_cnn_tr_idx]  # (N, 9)\nX_tab_tr          = df_tab.reindex(index=ALL_IDS).fillna(0).values\ndf_meta[\"unique_opcode_count\"] = (df_tab > 0).sum(axis=1)\nX_meta_tr         = df_meta.reindex(index=ALL_IDS).fillna(0).values\n\n# ── Test: align CNN -> TEST_IDS (submission order) ────────────────────────────\n# cnn_ids_test may be archive order (precomputed) or extraction order (fresh run)\n# Re-indexing to TEST_IDS always gives correct submission-order rows\n_cnn_te_map = {sid: i for i, sid in enumerate(cnn_ids_test)}\n_cnn_te_idx = [_cnn_te_map[sid] for sid in TEST_IDS]\nX_cnn_te        = np.array(cnn_feat_test)[_cnn_te_idx]   # (M, 3072)\nX_cnn_probs_te  = np.array(cnn_probs_test)[_cnn_te_idx]  # (M, 9)\nX_tab_te        = df_test_tab.reindex(index=TEST_IDS, columns=df_tab.columns).fillna(0).values\n_test_opc_cols  = [c for c in df_test_tab.columns if c not in META_COLS]\ndf_test_meta[\"unique_opcode_count\"] = (df_test_tab[_test_opc_cols] > 0).sum(axis=1)\nfor c in META_COLS:\n    if c not in df_test_meta.columns: df_test_meta[c] = 0\nX_meta_te = df_test_meta.reindex(index=TEST_IDS, columns=df_meta.columns).fillna(0).values\n\n# ── Labels & Final concat ────────────────────────────────────────────────────\ny_train = df_labels.set_index(\"Id\").reindex(ALL_IDS)[\"label\"].values\nX_train = np.hstack([X_cnn_tr, X_cnn_probs_tr, X_tab_tr, X_meta_tr])\nX_test  = np.hstack([X_cnn_te, X_cnn_probs_te, X_tab_te, X_meta_te])\n\nassert X_train.shape[1] == X_test.shape[1], f\"Column mismatch: {X_train.shape[1]} vs {X_test.shape[1]}\"\nlog.info(\"X_train: %s (CNN=%d+probs=%d+tab=%d+meta=%d)\",\n         X_train.shape, X_cnn_tr.shape[1], X_cnn_probs_tr.shape[1], X_tab_tr.shape[1], X_meta_tr.shape[1])\nlog.info(\"X_test : %s\", X_test.shape)\n\ndel X_cnn_tr, X_cnn_probs_tr, X_tab_tr, X_meta_tr\ndel X_cnn_te, X_cnn_probs_te, X_tab_te, X_meta_te\ngc.collect()\n\nprint(f\"X_train: {X_train.shape}  X_test: {X_test.shape}\")\nprint(\"Class distribution:\")\nfor c, n in sorted(zip(*np.unique(y_train, return_counts=True))):\n    print(f\"  Class {c+1} ({CLASS_NAMES[c+1]:<18}): {n:4d} ({n/len(y_train)*100:.1f}%)\")\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-23T07:19:53.136Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## G. 10-Fold CV: XGBoost + LightGBM + CatBoost\n","metadata":{}},{"cell_type":"code","source":"XGB_PARAMS = {\n    \"objective\":\"multi:softprob\",\"num_class\":N_CLASSES,\"eval_metric\":\"mlogloss\",\n    \"max_depth\":8,\"learning_rate\":0.025,\"subsample\":0.8,\"colsample_bytree\":0.8,\n    \"min_child_weight\":3,\"gamma\":0.1,\"tree_method\":\"hist\",\"device\":\"cuda\",\n    \"seed\":SEED,\"verbosity\":0,\n}\nLGB_PARAMS = {\n    \"objective\":\"multiclass\",\"num_class\":N_CLASSES,\"metric\":\"multi_logloss\",\n    \"learning_rate\":0.03,\"num_leaves\":64,\"min_child_samples\":20,\n    \"subsample\":0.8,\"colsample_bytree\":0.8,\"feature_fraction\":0.8,\n    \"reg_alpha\":0.1,\"reg_lambda\":0.1,\"seed\":SEED,\"verbosity\":-1,\n}\nCB_PARAMS = {\n    \"iterations\":1500,\"learning_rate\":0.03,\"depth\":8,\n    \"loss_function\":\"MultiClass\",\"eval_metric\":\"MultiClass\",\"task_type\":\"GPU\",\n    \"random_seed\":SEED,\"verbose\":200,\"early_stopping_rounds\":50,\"l2_leaf_reg\":3.0,\n}\n\n# ── Feature selection (once on 30% bootstrap) ────────────────────────────────\nif SEL_PATH.exists():\n    final_sel = np.load(SEL_PATH).tolist()\n    log.info(\"Feature selection loaded: %d features\", len(final_sel))\nelse:\n    log.info(\"Computing RF feature importance on 30%% bootstrap...\")\n    rng      = np.random.RandomState(SEED)\n    boot_idx = rng.choice(len(y_train), int(len(y_train)*0.30), replace=False)\n    rf       = RandomForestClassifier(n_estimators=200, max_depth=14, n_jobs=-1, random_state=SEED)\n    rf.fit(X_train[boot_idx], y_train[boot_idx])\n    final_sel = np.argsort(rf.feature_importances_)[::-1][:800].tolist()\n    np.save(SEL_PATH, np.array(final_sel))\n    log.info(\"Feature selection done: %d features\", len(final_sel))\n    del rf; gc.collect()\n\nX_train_sel = X_train[:, final_sel]\nX_test_sel  = X_test[:,  final_sel]\n\n# ── Stratified 10-fold CV ────────────────────────────────────────────────────\nsample_weights = compute_sample_weight(\"balanced\", y_train)\nskf = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=SEED)\n\noof_xgb=np.zeros((len(y_train),N_CLASSES)); tst_xgb=np.zeros((len(TEST_IDS),N_CLASSES))\noof_lgb=np.zeros((len(y_train),N_CLASSES)); tst_lgb=np.zeros((len(TEST_IDS),N_CLASSES))\noof_cb =np.zeros((len(y_train),N_CLASSES)); tst_cb =np.zeros((len(TEST_IDS),N_CLASSES))\nfold_models_xgb,fold_models_lgb,fold_models_cb=[],[],[]\nfold_scores=[]\nt_total=time.time()\nlog.info(\"Starting %d-fold CV (XGB + LGB + CB)...\", N_FOLDS)\n\nfor fold,(tr_idx,val_idx) in enumerate(skf.split(X_train_sel,y_train)):\n    t0=time.time()\n    X_tr,X_val=X_train_sel[tr_idx],X_train_sel[val_idx]\n    y_tr,y_val=y_train[tr_idx],y_train[val_idx]\n    w_tr=sample_weights[tr_idx]\n\n    m_xgb=xgb.train(XGB_PARAMS,xgb.DMatrix(X_tr,label=y_tr,weight=w_tr),\n                    num_boost_round=1500,evals=[(xgb.DMatrix(X_val,label=y_val),\"val\")],\n                    early_stopping_rounds=50,verbose_eval=200)\n    v_xgb=m_xgb.predict(xgb.DMatrix(X_val))\n    oof_xgb[val_idx]=v_xgb; tst_xgb+=m_xgb.predict(xgb.DMatrix(X_test_sel))/N_FOLDS\n    fold_models_xgb.append(m_xgb)\n\n    dtl=lgb.Dataset(X_tr,label=y_tr,weight=w_tr)\n    dvl=lgb.Dataset(X_val,label=y_val,reference=dtl)\n    m_lgb=lgb.train(LGB_PARAMS,dtl,num_boost_round=1500,valid_sets=[dvl],\n                    callbacks=[lgb.early_stopping(50),lgb.log_evaluation(200)])\n    v_lgb=m_lgb.predict(X_val,num_iteration=m_lgb.best_iteration)\n    oof_lgb[val_idx]=v_lgb; tst_lgb+=m_lgb.predict(X_test_sel,num_iteration=m_lgb.best_iteration)/N_FOLDS\n    fold_models_lgb.append(m_lgb)\n\n    m_cb=CatBoostClassifier(**CB_PARAMS)\n    m_cb.fit(Pool(X_tr,label=y_tr,weight=w_tr),eval_set=Pool(X_val,label=y_val),use_best_model=True)\n    v_cb=m_cb.predict_proba(Pool(X_val,label=y_val))\n    oof_cb[val_idx]=v_cb; tst_cb+=m_cb.predict_proba(X_test_sel)/N_FOLDS\n    fold_models_cb.append(m_cb)\n\n    l_x,l_l,l_c=log_loss(y_val,v_xgb),log_loss(y_val,v_lgb),log_loss(y_val,v_cb)\n    fold_scores.append({\"xgb\":l_x,\"lgb\":l_l,\"cb\":l_c})\n    log.info(\"Fold %2d | xgb=%.4f lgb=%.4f cb=%.4f | %.1fs\",fold+1,l_x,l_l,l_c,time.time()-t0)\n\nlog.info(\"CV done in %.1f min\", (time.time()-t_total)/60)\n\n# ── Optimal ensemble weights + temperature scaling (Nelder-Mead) ─────────────\ndef temperature_scale(probs, T):\n    eps=1e-10; logits=np.log(np.clip(probs,eps,1.))/max(T,0.1)\n    logits-=logits.max(axis=1,keepdims=True); e=np.exp(logits)\n    return e/e.sum(axis=1,keepdims=True)\n\ndef ensemble_loss(p):\n    w=np.abs(p[:3]); w/=w.sum(); T=np.abs(p[3:])+0.3\n    return log_loss(y_train,sum(wi*temperature_scale(oi,ti)\n                                for wi,oi,ti in zip(w,[oof_xgb,oof_lgb,oof_cb],T)))\n\nopt  =minimize(ensemble_loss,[1/3,1/3,1/3,1.,1.,1.],method=\"Nelder-Mead\",\n               options={\"maxiter\":2000,\"xatol\":1e-7,\"fatol\":1e-7,\"disp\":False})\nopt_w=np.abs(opt.x[:3]); opt_w/=opt_w.sum()\nopt_T=np.abs(opt.x[3:])+0.3\n\noof_preds  = sum(wi*temperature_scale(oi,ti) for wi,oi,ti in zip(opt_w,[oof_xgb,oof_lgb,oof_cb],opt_T))\ntest_preds = sum(wi*temperature_scale(ti,tt) for wi,ti,tt in zip(opt_w,[tst_xgb,tst_lgb,tst_cb],opt_T))\noof_acc    = accuracy_score(y_train,oof_preds.argmax(1))\noof_loss   = log_loss(y_train,oof_preds)\n\nprint(f\"\\n{chr(8212)*55}\")\nprint(f\"  OOF Accuracy : {oof_acc:.4f}\")\nprint(f\"  OOF Log-loss : {oof_loss:.6f}\")\nprint(f\"  Weights      : XGB={opt_w[0]:.3f} LGB={opt_w[1]:.3f} CB={opt_w[2]:.3f}\")\nprint(f\"  Temperatures : XGB={opt_T[0]:.3f} LGB={opt_T[1]:.3f} CB={opt_T[2]:.3f}\")\nprint(f\"{chr(8212)*55}\")\nprint(f\"  Individual: XGB={log_loss(y_train,oof_xgb):.6f} LGB={log_loss(y_train,oof_lgb):.6f} CB={log_loss(y_train,oof_cb):.6f}\")\nprint(f\"{chr(8212)*55}\")\nfor i,s in enumerate(fold_scores):\n    print(f\"  Fold {i+1:2d}: xgb={s['xgb']:.4f} lgb={s['lgb']:.4f} cb={s['cb']:.4f}\")\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-23T07:19:53.136Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## H. Pseudo-labeling\n","metadata":{}},{"cell_type":"code","source":"PSEUDO_THRESHOLD,MAX_PSEUDO_ROUNDS = 0.990, 2\nX_aug,y_aug,w_aug = X_train_sel.copy(),y_train.copy(),sample_weights.copy()\n\nfor pseudo_round in range(MAX_PSEUDO_ROUNDS):\n    mask  = test_preds.max(axis=1) >= PSEUDO_THRESHOLD\n    n_p   = mask.sum()\n    log.info(\"Pseudo round %d: %d/%d samples\", pseudo_round+1, n_p, len(TEST_IDS))\n    if n_p == 0: break\n\n    y_p   = test_preds[mask].argmax(axis=1)\n    w_p   = compute_sample_weight(\"balanced\", np.concatenate([y_aug,y_p]))[len(y_aug):]\n    X_aug = np.vstack([X_aug, X_test_sel[mask]])\n    y_aug = np.concatenate([y_aug, y_p])\n    w_aug = np.concatenate([w_aug, w_p])\n    log.info(\"Augmented size: %d\", len(y_aug))\n\n    r_x = int(np.mean([m.best_iteration  for m in fold_models_xgb]))\n    r_l = int(np.mean([m.best_iteration  for m in fold_models_lgb]))\n    r_c = int(np.mean([m.best_iteration_ for m in fold_models_cb]))\n\n    px=xgb.train({**XGB_PARAMS,\"verbosity\":0},xgb.DMatrix(X_aug,label=y_aug,weight=w_aug),num_boost_round=r_x)\n    pl=lgb.train({**LGB_PARAMS,\"verbose\":-1},lgb.Dataset(X_aug,label=y_aug,weight=w_aug),num_boost_round=r_l)\n    pc=CatBoostClassifier(**{**CB_PARAMS,\"iterations\":r_c,\"verbose\":0})\n    pc.fit(Pool(X_aug,label=y_aug,weight=w_aug))\n\n    new_t = sum(wi*temperature_scale(tp,tt) for wi,tp,tt in zip(opt_w,[\n        px.predict(xgb.DMatrix(X_test_sel)),\n        pl.predict(X_test_sel,num_iteration=r_l),\n        pc.predict_proba(X_test_sel)\n    ],opt_T))\n\n    skf3=StratifiedKFold(n_splits=3,shuffle=True,random_state=SEED+1)\n    p_oof=np.zeros((len(y_train),N_CLASSES))\n    for ptr,pval in skf3.split(X_aug[:len(y_train)],y_aug[:len(y_train)]):\n        _m=xgb.train({**XGB_PARAMS,\"verbosity\":0},\n                     xgb.DMatrix(X_aug[ptr],label=y_aug[ptr],weight=w_aug[ptr]),num_boost_round=r_x)\n        p_oof[pval]=_m.predict(xgb.DMatrix(X_aug[pval]))\n    new_loss=log_loss(y_train,p_oof)\n    log.info(\"Pseudo %d: loss=%.6f (was %.6f)\", pseudo_round+1, new_loss, oof_loss)\n\n    if new_loss < oof_loss - 0.0005:\n        test_preds=new_t; oof_loss=new_loss\n        log.info(\"Pseudo improved - updated\")\n    else:\n        log.info(\"Pseudo no improvement - stopping\"); break\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-23T07:19:53.137Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## I. Submissions — Ensemble + Per-model\n","metadata":{}},{"cell_type":"code","source":"from sklearn.metrics import classification_report\n\n# ── OOF Analysis ─────────────────────────────────────────────────────────────\noof_class = oof_preds.argmax(axis=1)\nprint(\"OOF Classification Report:\")\nprint(classification_report(y_train, oof_class, target_names=[CLASS_NAMES[i+1] for i in range(9)]))\n\nprint(\"Per-class accuracy:\")\nfor c in range(9):\n    mask = y_train == c\n    if mask.sum():\n        acc  = (oof_class[mask] == c).mean()\n        flag = chr(9989) if acc >= 0.99 else (chr(9888) if acc >= 0.95 else \"x\")\n        print(f\"  {flag} Class {c+1} ({CLASS_NAMES[c+1]:<18}): {acc:.4f}  (n={mask.sum()})\")\n\n# ── Helper: save submission ───────────────────────────────────────────────────\ndef save_sub(preds, fname, oof_loss_val=None):\n    col_names = [f\"Prediction{i}\" for i in range(1,10)]\n    df = pd.DataFrame(preds, columns=col_names)\n    df.insert(0, \"Id\", TEST_IDS)\n    df.to_csv(WORK_DIR / fname, index=False)\n    note = f\"OOF={oof_loss_val:.6f}\" if oof_loss_val else \"\"\n    log.info(\"Saved %-40s %s\", fname, note)\n    return df\n\n# ── 1. Ensemble (best — nop cai nay) ─────────────────────────────────────────\nsave_sub(test_preds, \"submission_ensemble.csv\", log_loss(y_train, oof_preds))\n\n# ── 2. XGBoost only ───────────────────────────────────────────────────────────\nsave_sub(temperature_scale(tst_xgb, opt_T[0]), \"submission_xgb.csv\", log_loss(y_train,oof_xgb))\n\n# ── 3. LightGBM only ──────────────────────────────────────────────────────────\nsave_sub(temperature_scale(tst_lgb, opt_T[1]), \"submission_lgb.csv\", log_loss(y_train,oof_lgb))\n\n# ── 4. CatBoost only ──────────────────────────────────────────────────────────\nsave_sub(temperature_scale(tst_cb, opt_T[2]),  \"submission_cb.csv\",  log_loss(y_train,oof_cb))\n\n# ── Summary ───────────────────────────────────────────────────────────────────\nsep = chr(9552)*60\nprint(f\"\\n{sep}\")\nprint(f\"  FINAL RESULTS\")\nprint(f\"{sep}\")\nprint(f\"  Model        OOF Log-loss   File\")\nprint(f\"  {chr(9472)*54}\")\nprint(f\"  XGBoost      {log_loss(y_train,oof_xgb):.6f}      submission_xgb.csv\")\nprint(f\"  LightGBM     {log_loss(y_train,oof_lgb):.6f}      submission_lgb.csv\")\nprint(f\"  CatBoost     {log_loss(y_train,oof_cb):.6f}       submission_cb.csv\")\nprint(f\"  {chr(9472)*54}\")\nprint(f\"  Ensemble     {log_loss(y_train,oof_preds):.6f}      submission_ensemble.csv  <- NOP\")\nprint(f\"{sep}\")\nprint(f\"  Weights: XGB={opt_w[0]:.3f}  LGB={opt_w[1]:.3f}  CB={opt_w[2]:.3f}\")\nprint(f\"  CNN: EfficientNet-B3 | TTA 8-fold | softmax features\")\nprint(f\"{sep}\")\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-23T07:19:53.137Z"}},"outputs":[],"execution_count":null}]}