{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"databundleVersionId":46665,"sourceId":4117,"sourceType":"competition"},{"databundleVersionId":16703603,"datasetId":10099522,"sourceId":15759624,"sourceType":"datasetVersion"}],"dockerImageVersionId":31329,"isGpuEnabled":true,"isInternetEnabled":true,"language":"python","sourceType":"notebook"},"papermill":{"default_parameters":{},"duration":3197.957954,"end_time":"2026-04-22T12:53:13.122132+00:00","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-04-22T11:59:55.164178+00:00","version":"2.7.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"0f6ba5a0-f5ac-4740-9408-6f46450bcbcd","cell_type":"code","source":"# ╔══════════════════════════════════════════════════════════════╗\n# ║  MULTI-VIEW STACKING V3 — MALWARE CLASSIFICATION BIG2015 ║\n# ║                                                          ║\n# ║  Kiến trúc V3 (7 inputs → 9 Models):                     ║\n# ║    View 1: XGB  trên [107 tabular]                       ║\n# ║    View 2: LGB  trên [3000 byte n-gram]                  ║\n# ║    View 3: XGB  trên [2000 opcode n-gram]                ║\n# ║    View 4: XGB  trên [1024 pixel density]                ║\n# ║    View 5: XGB  trên [173 API/DLL counts]                ║\n# ║    View 6: XGB  trên [27 Section stats]                  ║\n# ║    View 7: LGB  trên [2000 Strings chi2]                 ║\n# ║    View 8: XGB  trên [ALL features]                      ║\n# ║    View 9: LGB  trên [ALL features]                      ║\n# ║         ↓ 81 OOF proba (9 views × 9 classes)             ║\n# ║    Meta-Feature Engineering: OOF proba + Entropy + Std   ║\n# ║         ↓ 126 Features (Level-1 Smart Input)             ║\n# ║    Level-1: Proper Nested CV (LR + XGB)                  ║\n# ║         ↓ SLSQP Optimize 4 Weights                       ║\n# ║    Final Submission Ensemble                             ║\n# ╚══════════════════════════════════════════════════════════════╝\n# ║                                                          ║\n# ║  CẢI TIẾN:                                               ║\n# ║    1. OOF-Calibrated Pseudo-Label Threshold              ║\n# ║    2. Class Weighting toàn pipeline (sample_weight XGB)  ║\n# ║    3. Cost Evaluation Report (Training/Inference/Memory) ║\n# ╚══════════════════════════════════════════════════════════════╝","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"2d0d2a55-a87c-4cb0-9123-ea2f51300dc5","cell_type":"code","source":"# ============================================================\n# CELL 1 — IMPORTS\n# ============================================================\nimport os\nimport gc\nimport time\nimport pickle\nimport joblib\nimport numpy as np\nimport pandas as pd\nimport scipy.sparse as sp\nimport lightgbm as lgb\nimport xgboost as xgb\nfrom sklearn.linear_model import LogisticRegression\n\nfrom sklearn.metrics import log_loss, accuracy_score\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.utils.class_weight import compute_sample_weight\nfrom sklearn.feature_extraction.text import CountVectorizer\nfrom sklearn.feature_selection import SelectKBest, chi2\nfrom scipy.optimize import minimize\n\nprint(\"Import xong!\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"388436a1-b53c-44a0-8e48-d09577955555","cell_type":"code","source":"# ============================================================\n# CELL 2 — CẤU HÌNH ĐƯỜNG DẪN\n# ============================================================\n\n# DATA_IN_DIR trỏ tới Dataset Kaggle đã xử lí data\nDATA_IN_DIR   = '/kaggle/input/datasets/trankimhuu/data-ml-big-2015-ver-3'\n\nWORKING_DIR   = '/kaggle/working'\nMODEL_DIR     = os.path.join(WORKING_DIR, 'models')\nCKPT_DIR      = os.path.join(WORKING_DIR, 'checkpoints')\n\nos.makedirs(MODEL_DIR, exist_ok=True)\nos.makedirs(CKPT_DIR,  exist_ok=True)\n\nSUBMISSION_CSV = '/kaggle/input/competitions/malware-classification/sampleSubmission.csv'\nNUM_CLASSES    = 9\nN_FOLDS        = 5\nRANDOM_STATE   = 42\n\nprint(f\"Input : {DATA_IN_DIR}\")\nprint(f\"Models: {MODEL_DIR}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"c33de40a-4618-4447-85e1-20819bc812d5","cell_type":"code","source":"# ============================================================\n# CELL 3 — LOAD DỮ LIỆU\n# ============================================================\n\nprint(\"=\" * 55)\nprint(\"BƯỚC 1: LOAD DỮ LIỆU\")\nprint(\"=\" * 55)\n\ndef load_file(filename):\n    fp = os.path.join(DATA_IN_DIR, filename)\n    if not os.path.exists(fp):\n        # Trả về dummy paths nếu chạy local test thử\n        print(f\"  [Warning] Missing {filename}. Skipping...\")\n        return None\n        \n    size_mb = os.path.getsize(fp) / 1e6\n    print(f\"  {filename:<30} ({size_mb:>5.0f} MB)...\", end=\"\", flush=True)\n    return fp\n\n# ── TRAIN ──\nprint(\"Train data:\")\nfp = load_file(\"X_train_tab.npy\");      X_train_tab   = np.load(fp) if fp else None; print(f\" {X_train_tab.shape}\" if fp else \"\")\nfp = load_file(\"X_train_ng.npz\");       X_train_ng    = sp.load_npz(fp) if fp else None; print(f\" {X_train_ng.shape}\" if fp else \"\")\nfp = load_file(\"X_train_pixel.npy\");    X_train_pixel = np.load(fp) if fp else None; print(f\" {X_train_pixel.shape}\" if fp else \"\")\nfp = load_file(\"X_train_opseq.pkl\")\nif fp:\n    with open(fp, \"rb\") as f: train_opseq = pickle.load(f)\n    print(f\" n={len(train_opseq):,}\")\n\nfp = load_file(\"X_train_api.npy\");      X_train_api   = np.load(fp) if fp else None; print(f\" {X_train_api.shape}\" if fp else \"\")\nfp = load_file(\"X_train_sections.npy\"); X_train_sect  = np.load(fp) if fp else None; print(f\" {X_train_sect.shape}\" if fp else \"\")\nfp = load_file(\"X_train_strings.npz\");  X_train_str   = sp.load_npz(fp) if fp else None; print(f\" {X_train_str.shape}\" if fp else \"\")\n\nfp = load_file(\"y_train.npy\");          y_train       = np.load(fp) if fp else None; print(f\" {y_train.shape}\" if fp else \"\")\n\n# ── TEST ──\nprint(\"\\nTest data:\")\nfp = load_file(\"X_test_tab.npy\");       X_test_tab    = np.load(fp) if fp else None; print(f\" {X_test_tab.shape}\" if fp else \"\")\nfp = load_file(\"X_test_pixel.npy\");     X_test_pixel  = np.load(fp) if fp else None; print(f\" {X_test_pixel.shape}\" if fp else \"\")\nfp = load_file(\"X_test_opseq.pkl\")\nif fp:\n    with open(fp, \"rb\") as f: test_opseq = pickle.load(f)\n    print(f\" n={len(test_opseq):,}\")\n\nfp = load_file(\"X_test_api.npy\");       X_test_api    = np.load(fp) if fp else None; print(f\" {X_test_api.shape}\" if fp else \"\")\nfp = load_file(\"X_test_sections.npy\");  X_test_sect   = np.load(fp) if fp else None; print(f\" {X_test_sect.shape}\" if fp else \"\")\nfp = load_file(\"X_test_strings.npz\");   X_test_str    = sp.load_npz(fp) if fp else None; print(f\" {X_test_str.shape}\" if fp else \"\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"f48b26de-c747-401d-b5a0-361108e63194","cell_type":"code","source":"# ============================================================\n# CELL 4 — XỬ LÝ TRAIN: Opcode, Byte NG, Strings NG (SelectKBest)\n# ============================================================\n\nUSE_OPCODE  = True\nK_BYTE_NG   = 3000\nK_OPCODE_NG = 2000\nK_STRINGS   = 2000\nCHUNK_SIZE  = 500\n\nprint(\"\\n\" + \"=\" * 55)\nprint(\"BƯỚC 2: XỬ LÝ TRAIN TỪNG VIEW (Feature Selection)\")\nprint(\"=\" * 55)\n\n# --- 1. Opcode Selection ---\nif USE_OPCODE and train_opseq is not None:\n    from sklearn.feature_extraction.text import HashingVectorizer as _HashVec\n    opcode_hash_vec = _HashVec(ngram_range=(2, 3), n_features=2**14, analyzer=\"word\", norm=None, alternate_sign=False)\n    print(\"  Vectorizing train opcode...\", end=\"\", flush=True)\n    X_train_opng = opcode_hash_vec.transform(train_opseq)\n    joblib.dump(opcode_hash_vec, os.path.join(MODEL_DIR, \"opcode_hash_vec.pkl\"))\n    del train_opseq; gc.collect()\n    \n    k_op = min(K_OPCODE_NG, X_train_opng.shape[1])\n    print(f\" -> chi2 {X_train_opng.shape[1]:,}→{k_op}...\", end=\"\", flush=True)\n    sel_opcode = SelectKBest(chi2, k=k_op)\n    X_train_opng_sel = sel_opcode.fit_transform(X_train_opng, y_train).toarray().astype(np.float32)\n    joblib.dump(sel_opcode, os.path.join(MODEL_DIR, \"selector_opcode.pkl\"))\n    del X_train_opng; gc.collect()\n    print(f\"{X_train_opng_sel.shape}\")\n\n# --- 2. Strings Selection ---\nif X_train_str is not None:\n    k_str = min(K_STRINGS, X_train_str.shape[1])\n    print(f\"  Strings chi2 {X_train_str.shape[1]:,}→{k_str}...\", end=\"\", flush=True)\n    sel_string = SelectKBest(chi2, k=k_str)\n    X_train_str_sel = sel_string.fit_transform(X_train_str, y_train).toarray().astype(np.float32)\n    joblib.dump(sel_string, os.path.join(MODEL_DIR, \"selector_string.pkl\"))\n    del X_train_str; gc.collect()\n    print(f\"{X_train_str_sel.shape}\")\n\n# --- 3. Byte NG Selection (Chunking) ---\nif X_train_ng is not None:\n    print(f\"  Byte N-gram chi2 train {X_train_ng.shape[1]:,}→{K_BYTE_NG} (chunking)...\", end=\"\", flush=True)\n    n_samples, n_features = X_train_ng.shape\n    n_classes = len(np.unique(y_train))\n\n    Y = np.zeros((n_samples, n_classes), dtype=np.float32)\n    Y[np.arange(n_samples), y_train] = 1.0\n\n    observed     = np.zeros((n_classes, n_features), dtype=np.float64)\n    feature_sum  = np.zeros(n_features, dtype=np.float64)\n\n    for start in range(0, n_samples, CHUNK_SIZE):\n        end      = min(start + CHUNK_SIZE, n_samples)\n        X_chunk  = X_train_ng[start:end]\n        observed += (X_chunk.T.dot(Y[start:end])).T\n        feature_sum += np.asarray(X_chunk.sum(axis=0)).ravel()\n\n    class_prob = Y.mean(axis=0)\n    expected   = np.outer(class_prob, feature_sum)\n    np.clip(expected, 1e-9, None, out=expected)\n    chi2_scores = np.sum((observed - expected) ** 2 / expected, axis=0)\n    top_k_idx   = np.argsort(chi2_scores)[-K_BYTE_NG:]\n    joblib.dump(top_k_idx, os.path.join(MODEL_DIR, \"top_k_byte_idx.pkl\"))\n\n    X_train_byteng = np.zeros((n_samples, K_BYTE_NG), dtype=np.float32)\n    for start in range(0, n_samples, CHUNK_SIZE):\n        end = min(start + CHUNK_SIZE, n_samples)\n        X_train_byteng[start:end] = X_train_ng[start:end].toarray()[:, top_k_idx]\n\n    del X_train_ng, Y, observed, feature_sum; gc.collect()\n    print(f\"{X_train_byteng.shape}\")\n\n# LƯU TỪNG VIEW TRAINING\nprint(\"\\n  Lưu từng view vào Checkpoints...\")\nif X_train_tab is not None:   X_train_tab   = X_train_tab.astype(np.float32);   np.save(os.path.join(CKPT_DIR, \"X_train_v_tab.npy\"), X_train_tab)\nif X_train_pixel is not None: X_train_pixel = X_train_pixel.astype(np.float32); np.save(os.path.join(CKPT_DIR, \"X_train_v_pixel.npy\"), X_train_pixel)\nif X_train_api is not None:   X_train_api   = X_train_api.astype(np.float32);   np.save(os.path.join(CKPT_DIR, \"X_train_v_api.npy\"), X_train_api)\nif X_train_sect is not None:  X_train_sect  = X_train_sect.astype(np.float32);  np.save(os.path.join(CKPT_DIR, \"X_train_v_sections.npy\"), X_train_sect)\n\nnp.save(os.path.join(CKPT_DIR, \"X_train_v_byteng.npy\"), X_train_byteng)\nnp.save(os.path.join(CKPT_DIR, \"X_train_v_opng.npy\"), X_train_opng_sel)\nnp.save(os.path.join(CKPT_DIR, \"X_train_v_strings.npy\"), X_train_str_sel)\n\n# Ghép ALL features\nX_train_all = np.hstack([\n    X_train_tab, \n    X_train_byteng, \n    X_train_opng_sel, \n    X_train_pixel,\n    X_train_api,\n    X_train_sect,\n    X_train_str_sel\n]).astype(np.float32)\nnp.save(os.path.join(CKPT_DIR, \"X_train_v_all.npy\"), X_train_all)\n\nprint(f\"Gộp ALL views: {X_train_all.shape}\")\ndel X_train_tab, X_train_byteng, X_train_opng_sel, X_train_pixel, X_train_api, X_train_sect, X_train_str_sel, X_train_all\ngc.collect()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"6a97cf3b-c24e-4f15-963c-1a35997d58ac","cell_type":"code","source":"# ============================================================\n# CELL 5 — XỬ LÝ TEST: Tương tự train\n# ============================================================\n\nprint(\"\\n\" + \"=\" * 55)\nprint(\"BƯỚC 3: XỬ LÝ TEST TỪNG VIEW\")\nprint(\"=\" * 55)\n\n# --- 1. Opcode ---\nif USE_OPCODE and test_opseq is not None:\n    opcode_hash_vec = joblib.load(os.path.join(MODEL_DIR, \"opcode_hash_vec.pkl\"))\n    sel_opcode      = joblib.load(os.path.join(MODEL_DIR, \"selector_opcode.pkl\"))\n    print(\"  Vectorizing test opcode...\", end=\"\", flush=True)\n    X_test_opng = opcode_hash_vec.transform(test_opseq)\n    X_test_opng_sel = sel_opcode.transform(X_test_opng).toarray().astype(np.float32)\n    del X_test_opng, test_opseq; gc.collect()\n    print(f\"{X_test_opng_sel.shape}\")\n\n# --- 2. Strings ---\nif X_test_str is not None:\n    sel_string = joblib.load(os.path.join(MODEL_DIR, \"selector_string.pkl\"))\n    print(\"  Transforming test strings...\", end=\"\", flush=True)\n    X_test_str_sel = sel_string.transform(X_test_str).toarray().astype(np.float32)\n    del X_test_str; gc.collect()\n    print(f\"{X_test_str_sel.shape}\")\n\n# --- 3. Byte N-gram ---\nfp_ng = load_file(\"X_test_ng.npz\")\nif fp_ng:\n    X_test_ng = sp.load_npz(fp_ng)\n    top_k_idx = joblib.load(os.path.join(MODEL_DIR, \"top_k_byte_idx.pkl\"))\n    n_test = X_test_ng.shape[0]\n    X_test_byteng = np.zeros((n_test, len(top_k_idx)), dtype=np.float32)\n    print(\"  Densify test byte NG...\", end=\"\", flush=True)\n    for start in range(0, n_test, CHUNK_SIZE):\n        end = min(start + CHUNK_SIZE, n_test)\n        X_test_byteng[start:end] = X_test_ng[start:end].toarray()[:, top_k_idx]\n    del X_test_ng; gc.collect()\n    print(f\"{X_test_byteng.shape}\")\n\n# LƯU TỪNG VIEW TEST\nprint(\"\\n  Lưu từng view test vào Checkpoints...\")\nif X_test_tab is not None:   X_test_tab   = X_test_tab.astype(np.float32);   np.save(os.path.join(CKPT_DIR, \"X_test_v_tab.npy\"), X_test_tab)\nif X_test_pixel is not None: X_test_pixel = X_test_pixel.astype(np.float32); np.save(os.path.join(CKPT_DIR, \"X_test_v_pixel.npy\"), X_test_pixel)\nif X_test_api is not None:   X_test_api   = X_test_api.astype(np.float32);   np.save(os.path.join(CKPT_DIR, \"X_test_v_api.npy\"), X_test_api)\nif X_test_sect is not None:  X_test_sect  = X_test_sect.astype(np.float32);  np.save(os.path.join(CKPT_DIR, \"X_test_v_sections.npy\"), X_test_sect)\n\nnp.save(os.path.join(CKPT_DIR, \"X_test_v_byteng.npy\"), X_test_byteng)\nnp.save(os.path.join(CKPT_DIR, \"X_test_v_opng.npy\"), X_test_opng_sel)\nnp.save(os.path.join(CKPT_DIR, \"X_test_v_strings.npy\"), X_test_str_sel)\n\nX_test_all = np.hstack([\n    X_test_tab, \n    X_test_byteng, \n    X_test_opng_sel, \n    X_test_pixel,\n    X_test_api,\n    X_test_sect,\n    X_test_str_sel\n]).astype(np.float32)\nnp.save(os.path.join(CKPT_DIR, \"X_test_v_all.npy\"), X_test_all)\n\nprint(f\"Gộp ALL views TEST: {X_test_all.shape}\")\ndel X_test_tab, X_test_byteng, X_test_opng_sel, X_test_pixel, X_test_api, X_test_sect, X_test_str_sel, X_test_all\ngc.collect()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"f8d08332-e093-43fb-b27a-41ac1436ea84","cell_type":"code","source":"# ============================================================\n# CELL 6 — LOAD CÁC VIEW TỪ CHECKPOINT VÀO MEMORY MỚI\n# ============================================================\n\nprint(\"\\n\" + \"=\" * 55)\nprint(\"BƯỚC 4: LOAD CÁC VIEWS (Tổng cộng 9 models)\")\nprint(\"=\" * 55)\n\nVIEWS = {\n    \"tab\":      {\"file\": \"v_tab\",      \"desc\": \"Tabular (meta features)\"},\n    \"byteng\":   {\"file\": \"v_byteng\",   \"desc\": \"Byte N-gram (3000 chi2)\"},\n    \"opng\":     {\"file\": \"v_opng\",     \"desc\": \"Opcode N-gram (2000 chi2)\"},\n    \"pixel\":    {\"file\": \"v_pixel\",    \"desc\": \"Pixel density (32x32)\"},\n    \"api\":      {\"file\": \"v_api\",      \"desc\": \"API Calls + DLLs\"},\n    \"sections\": {\"file\": \"v_sections\", \"desc\": \"Section Statistics\"},\n    \"strings\":  {\"file\": \"v_strings\",  \"desc\": \"Printable Strings HashVec\"},\n    \"all\":      {\"file\": \"v_all\",      \"desc\": \"ALL features combined (XGB)\"},\n    \"all_lgb\":  {\"file\": \"v_all\",      \"desc\": \"ALL features combined (LGB)\"},\n}\n\nVIEW_FEATURE_MAP = {\n    \"tab\": \"tab\", \"byteng\": \"byteng\", \"opng\": \"opng\",\n    \"pixel\": \"pixel\", \"api\": \"api\", \"sections\": \"sections\", \"strings\": \"strings\",\n    \"all\": \"all\", \"all_lgb\": \"all\",\n}\n\nX_train_views = {}\nX_test_views  = {}\n\nfor vname in set(VIEW_FEATURE_MAP.values()):\n    X_tr = np.load(os.path.join(CKPT_DIR, f\"X_train_v_{vname}.npy\"))\n    X_te = np.load(os.path.join(CKPT_DIR, f\"X_test_v_{vname}.npy\"))\n    X_train_views[vname] = X_tr\n    X_test_views[vname]  = X_te\n    print(f\"  {vname:8s} : train {X_tr.shape}  test {X_te.shape}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"d668282f-1ed1-485e-b842-027bf2a92e8e","cell_type":"code","source":"# ============================================================\n# CELL 7 — CẤU HÌNH VÀ TRAIN MULTI-VIEW MODELS\n# ============================================================\n\nprint(\"\\n\" + \"=\" * 55)\nprint(\"BƯỚC 5: CẤU HÌNH 9 MULTI-VIEW MODELS\")\nprint(\"=\" * 55)\n\nclasses = np.unique(y_train)\nweights = compute_class_weight(\"balanced\", classes=classes, y=y_train)\nclass_weights_dict = dict(zip(classes.astype(int), weights))\n\nVIEW_MODELS = {\n    \"tab\": xgb.XGBClassifier(\n        objective='multi:softprob', num_class=NUM_CLASSES,\n        max_depth=8, n_estimators=500, learning_rate=0.05,\n        subsample=0.8, colsample_bytree=0.9, min_child_weight=3,\n        tree_method='hist', device='cuda', eval_metric='mlogloss', random_state=RANDOM_STATE,\n    ),\n    \"byteng\": lgb.LGBMClassifier(\n        objective='multiclass', num_class=NUM_CLASSES, class_weight=class_weights_dict,\n        n_estimators=800, learning_rate=0.05, num_leaves=127, min_child_samples=10,\n        feature_fraction=0.5, bagging_fraction=0.8, bagging_freq=5,\n        lambda_l1=0.1, lambda_l2=0.1, device='gpu', random_state=RANDOM_STATE, verbose=-1,\n    ),\n    \"opng\": xgb.XGBClassifier(\n        objective='multi:softprob', num_class=NUM_CLASSES,\n        max_depth=7, n_estimators=500, learning_rate=0.05,\n        subsample=0.8, colsample_bytree=0.6, min_child_weight=3,\n        tree_method='hist', device='cuda', eval_metric='mlogloss', random_state=RANDOM_STATE,\n    ),\n    \"pixel\": xgb.XGBClassifier(\n        objective='multi:softprob', num_class=NUM_CLASSES,\n        max_depth=7, n_estimators=500, learning_rate=0.05,\n        subsample=0.8, colsample_bytree=0.7, min_child_weight=3,\n        tree_method='hist', device='cuda', eval_metric='mlogloss', random_state=RANDOM_STATE,\n    ),\n    \"api\": xgb.XGBClassifier(\n        objective='multi:softprob', num_class=NUM_CLASSES,\n        max_depth=7, n_estimators=600, learning_rate=0.05,\n        subsample=0.8, colsample_bytree=0.8, min_child_weight=3,\n        tree_method='hist', device='cuda', eval_metric='mlogloss', random_state=RANDOM_STATE,\n    ),\n    \"sections\": xgb.XGBClassifier(\n        objective='multi:softprob', num_class=NUM_CLASSES,\n        max_depth=6, n_estimators=400, learning_rate=0.05,\n        subsample=0.8, colsample_bytree=0.9, min_child_weight=3,\n        tree_method='hist', device='cuda', eval_metric='mlogloss', random_state=RANDOM_STATE,\n    ),\n    \"strings\": lgb.LGBMClassifier(\n        objective='multiclass', num_class=NUM_CLASSES, class_weight=class_weights_dict,\n        n_estimators=800, learning_rate=0.05, num_leaves=127, min_child_samples=10,\n        feature_fraction=0.5, bagging_fraction=0.8, bagging_freq=5,\n        lambda_l1=0.1, lambda_l2=0.1, device='gpu', random_state=RANDOM_STATE, verbose=-1,\n    ),\n    \"all\": xgb.XGBClassifier(\n        objective='multi:softprob', num_class=NUM_CLASSES,\n        max_depth=7, n_estimators=500, learning_rate=0.05,\n        subsample=0.8, colsample_bytree=0.7, min_child_weight=3,\n        tree_method='hist', device='cuda', eval_metric='mlogloss', random_state=RANDOM_STATE,\n    ),\n    \"all_lgb\": lgb.LGBMClassifier(\n        objective='multiclass', num_class=NUM_CLASSES, class_weight=class_weights_dict,\n        n_estimators=1000, learning_rate=0.05, num_leaves=127, min_child_samples=10,\n        feature_fraction=0.7, bagging_fraction=0.8, bagging_freq=5,\n        lambda_l1=0.1, lambda_l2=0.1, device='gpu', random_state=RANDOM_STATE, verbose=-1,\n    ),\n}\n\nskf = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=2026)\nN_VIEWS = len(VIEW_MODELS)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"b3ac5e89-96da-452f-bc71-1ce073d33d95","cell_type":"code","source":"# ============================================================\n# CELL 8 — LEVEL-0 TRAINING (OOF META-FEATURES)\n# ============================================================\n\nprint(\"\\n\" + \"=\" * 55)\nprint(\"BƯỚC 6: MULTI-VIEW LEVEL-0 (TẠO OOF)\")\nprint(\"=\" * 55)\n\n# Đảm bảo xóa checkpoint cũ khi chạy phiên bản mới này\nimport glob\nold_ckpts = glob.glob(os.path.join(CKPT_DIR, \"oof_train_*.npy\")) + glob.glob(os.path.join(CKPT_DIR, \"oof_test_*.npy\"))\nfor f in old_ckpts:\n    os.remove(f)\n\nmeta_train_list = []\nmeta_test_list  = []\nn_train = len(y_train)\nn_test  = X_test_views[\"tab\"].shape[0]\n\n# === Lưu thời gian training từng view để báo cáo ===\nlevel0_times = {}\n\nfor view_name, model in VIEW_MODELS.items():\n    feat_key = VIEW_FEATURE_MAP[view_name]\n    X_tr = X_train_views[feat_key]\n    X_te = X_test_views[feat_key]\n    \n    print(f\"\\n{'─'*50}\")\n    print(f\"[{view_name.upper()}] features={X_tr.shape[1]}\")\n\n    oof_train    = np.zeros((n_train, NUM_CLASSES), dtype=np.float32)\n    oof_test_avg = np.zeros((n_test,  NUM_CLASSES), dtype=np.float32)\n\n    view_t0 = time.time()\n    for fold, (tr_idx, val_idx) in enumerate(skf.split(X_tr, y_train)):\n        print(f\"  Fold {fold+1}/{N_FOLDS}...\", end=\"\", flush=True)\n        t0 = time.time()\n\n        # === [CLASS IMBALANCE] sample_weight cho XGB ===\n        if isinstance(model, xgb.XGBClassifier):\n            sw = compute_sample_weight(\"balanced\", y_train[tr_idx])\n            model.fit(X_tr[tr_idx], y_train[tr_idx], sample_weight=sw)\n        else:\n            # LGB đã có class_weight trong config\n            model.fit(X_tr[tr_idx], y_train[tr_idx])\n\n        oof_train[val_idx] = model.predict_proba(X_tr[val_idx])\n        oof_test_avg += model.predict_proba(X_te) / N_FOLDS\n        print(f\" ll={log_loss(y_train[val_idx], oof_train[val_idx]):.5f}  ({time.time()-t0:.1f}s)\")\n        gc.collect()\n\n    print(f\" OOF logloss: {log_loss(y_train, oof_train):.5f}\")\n    \n    meta_train_list.append(oof_train)\n    meta_test_list.append(oof_test_avg)\n\n    # Full fit\n    print(f\"  Full model...\", end=\"\", flush=True)\n    t0 = time.time()\n    if isinstance(model, xgb.XGBClassifier):\n        sw_full = compute_sample_weight(\"balanced\", y_train)\n        model.fit(X_tr, y_train, sample_weight=sw_full)\n    else:\n        model.fit(X_tr, y_train)\n    joblib.dump(model, os.path.join(MODEL_DIR, f\"{view_name}_full.pkl\"))\n    print(f\" ({time.time()-t0:.1f}s)\")\n    level0_times[view_name] = time.time() - view_t0\n\nX_train_L1 = np.hstack(meta_train_list).astype(np.float32)\nX_test_L1  = np.hstack(meta_test_list).astype(np.float32)\n\nprint(f\"\\nLevel-0 done! X_train_L1: {X_train_L1.shape} (9 models × 9 classes)\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"7792c059-3929-4cda-8990-77392be67e01","cell_type":"code","source":"# ============================================================\n# CELL 9 — PSEUDO-LABELING (OOF-CALIBRATED THRESHOLD)\n# ============================================================\n\nprint(\"\\n\" + \"=\" * 55)\nprint(\"BƯỚC 7: PSEUDO-LABELING (OOF-Calibrated Threshold)\")\nprint(\"=\" * 55)\n\n# === Dùng OOF predictions (có ground truth) để calibrate threshold ===\ndef oof_calibrated_threshold(oof_proba, y_true, base_percentile=95, min_threshold=0.90):\n    \"\"\"\n    Xác định threshold cho từng class dựa trên phân phối confidence\n    của các OOF predictions ĐÚNG (có ground truth).\n    \n    Logic:\n      - Với mỗi class c, lấy tất cả mẫu OOF mà model predict đúng class c\n      - Tính percentile thứ `base_percentile` của confidence khi predict đúng\n      - Class nào model mạnh (confidence cao khi đúng) -> threshold tự cao\n      - Class nào model yếu (confidence thấp khi đúng) -> threshold tự thấp\n    \n    Hoàn toàn data-driven, không có magic numbers.\n    \"\"\"\n    num_classes = oof_proba.shape[1]\n    thresholds = {}\n    \n    for c in range(num_classes):\n        # Mẫu mà model predict đúng class c\n        pred_c = oof_proba.argmax(axis=1) == c\n        true_c = y_true == c\n        correct_mask = pred_c & true_c\n        \n        if correct_mask.sum() == 0:\n            thresholds[c] = 0.99  # fallback nếu không có mẫu đúng\n            continue\n        \n        # Confidence khi predict đúng class c\n        correct_confs = oof_proba[correct_mask, c]\n        \n        # Threshold = percentile của confidence khi predict đúng\n        thresholds[c] = max(min_threshold, np.percentile(correct_confs, base_percentile))\n    \n    return thresholds\n\n# Tính OOF average từ X_train_L1 (9 models × 9 classes -> average across models)\noof_avg = X_train_L1.reshape(len(y_train), N_VIEWS, NUM_CLASSES).mean(axis=1)\n\n# Calibrate threshold từ OOF\nca_thresholds = oof_calibrated_threshold(oof_avg, y_train, base_percentile=90)\n\n# Tính average probability từ 9 full-trained models (cho test set)\nproba_list = []\nfor view_name in VIEW_MODELS.keys():\n    feat_key = VIEW_FEATURE_MAP[view_name]\n    m = joblib.load(os.path.join(MODEL_DIR, f\"{view_name}_full.pkl\"))\n    proba_list.append(m.predict_proba(X_test_views[feat_key]))\n    del m; gc.collect()\n\navg_test_proba = np.mean(proba_list, axis=0)\ndel proba_list; gc.collect()\n\n# Áp dụng threshold per-class lên test set\npred_classes = avg_test_proba.argmax(axis=1)\nmax_conf = avg_test_proba.max(axis=1)\n\nconfident_mask = np.zeros(len(avg_test_proba), dtype=bool)\nfor c in range(NUM_CLASSES):\n    class_mask = (pred_classes == c) & (max_conf >= ca_thresholds[c])\n    confident_mask |= class_mask\n\nconfident_idx = np.where(confident_mask)[0]\npseudo_labels = pred_classes[confident_idx]\n\n# === So sánh Fixed 0.99 vs OOF-Calibrated ===\nfixed_idx = np.where(max_conf >= 0.99)[0]\nfixed_labels = pred_classes[fixed_idx]\n\nprint(f\"\\n  OOF-CALIBRATED THRESHOLDS (base_percentile=90):\")\nprint(f\"  {\"Class\":<8} {\"Train\":>8} {\"OOF Acc\":>10} {\"Threshold\":>10} {\"Fixed\":>8} {\"Calib\":>8}\")\nprint(f\"  {\"─\"*8} {\"─\"*8} {\"─\"*10} {\"─\"*10} {\"─\"*8} {\"─\"*8}\")\nfor c in range(NUM_CLASSES):\n    n_train = np.sum(y_train == c)\n    # OOF accuracy for this class\n    true_c = y_train == c\n    pred_c = oof_avg.argmax(axis=1) == c\n    oof_acc = np.sum(true_c & pred_c) / max(np.sum(true_c), 1)\n    n_fixed = np.sum(fixed_labels == c)\n    n_ca = np.sum(pseudo_labels == c)\n    print(f\"  {c:<8} {n_train:>8} {oof_acc:>10.4f} {ca_thresholds[c]:>10.4f} {n_fixed:>8} {n_ca:>8}\")\n\nprint(f\"\\n  Fixed 0.99         : {len(fixed_idx):,} mau ({100*len(fixed_idx)/len(avg_test_proba):.1f}%)\")\nprint(f\"  OOF-Calibrated     : {len(confident_idx):,} mau ({100*len(confident_idx)/len(avg_test_proba):.1f}%)\")\n\n# Sử dụng kết quả OOF-Calibrated\nX_train_L1_ext_base = np.vstack([X_train_L1, X_test_L1[confident_idx]])\ny_train_ext         = np.concatenate([y_train, pseudo_labels])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"3470630f-dd87-49c3-8816-fe65b05ff28c","cell_type":"code","source":"# ============================================================\n# CELL 10 — LEVEL-1: NESTED CV + GIA TĂNG META-FEATURES \n# ============================================================\n\nprint(\"\\n\" + \"=\" * 55)\nprint(\"BƯỚC 8: LEVEL-1 (META-FEATURE ENGINEERING & TỐI ƯU TRỌNG SỐ)\")\nprint(\"=\" * 55)\n\ndef compute_meta_features(X_L1, num_models=9, num_classes=9):\n    N = X_L1.shape[0]\n    proba_3d = X_L1.reshape(N, num_models, num_classes)\n    \n    feat_mean = np.mean(proba_3d, axis=1)\n    feat_max  = np.max(proba_3d, axis=1)\n    feat_min  = np.min(proba_3d, axis=1)\n    feat_std  = np.std(proba_3d, axis=1)\n    \n    eps = 1e-15\n    proba_clip = np.clip(proba_3d, eps, 1 - eps)\n    entropy_per_model = -np.sum(proba_clip * np.log(proba_clip), axis=2)\n    \n    X_ext = np.hstack([X_L1, feat_mean, feat_max, feat_min, feat_std, entropy_per_model])\n    return X_ext.astype(np.float32)\n\nprint(\"  Đang tính toán Bất đồng (Std) và Vùng hỗn loạn (Entropy)...\", end=\"\", flush=True)\nX_train_L1_smart = compute_meta_features(X_train_L1, num_models=N_VIEWS)\nX_test_L1_smart  = compute_meta_features(X_test_L1,  num_models=N_VIEWS)\n# Tính luôn cho tập chứa Pseudo label\nX_train_L1_ext   = compute_meta_features(X_train_L1_ext_base, num_models=N_VIEWS)\nprint(f\" ({X_train_L1.shape[1]} -> {X_train_L1_smart.shape[1]} đặc trưng)\")\n\noof_lr  = np.zeros((len(y_train), NUM_CLASSES), dtype=np.float32)\noof_xgb = np.zeros((len(y_train), NUM_CLASSES), dtype=np.float32)\n\n\nskf_nested = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=2026)\n\nlevel1_t0 = time.time()\nprint(f\"\\n── Tạo PROPER OOF Level-1 (nested {N_FOLDS}-fold) ──\")\nfor fold, (tr_idx, val_idx) in enumerate(skf_nested.split(X_train_L1_smart, y_train)):\n    # 1. Logistic Regression — thêm class_weight='balanced'\n    scaler_fold_l1 = StandardScaler()\n    X_tr_l1_s = scaler_fold_l1.fit_transform(X_train_L1_smart[tr_idx])\n    X_val_l1_s = scaler_fold_l1.transform(X_train_L1_smart[val_idx])\n    lr_fold = LogisticRegression(\n        C=0.1, solver='lbfgs', max_iter=2000,\n        multi_class='multinomial', class_weight='balanced',\n        random_state=RANDOM_STATE\n    )\n    lr_fold.fit(X_tr_l1_s, y_train[tr_idx])\n    oof_lr[val_idx] = lr_fold.predict_proba(X_val_l1_s)\n\n    # 2. XGB - shared 126 scaled meta-features with LR - add sample_weight\n    xgb_fold = xgb.XGBClassifier(\n        objective='multi:softprob', num_class=NUM_CLASSES,\n        max_depth=3, n_estimators=200, learning_rate=0.05,\n        subsample=0.8, tree_method='hist', device='cuda', random_state=99\n    )\n    sw_l1 = compute_sample_weight(\"balanced\", y_train[tr_idx])\n    xgb_fold.fit(X_tr_l1_s, y_train[tr_idx], sample_weight=sw_l1)\n    oof_xgb[val_idx] = xgb_fold.predict_proba(X_val_l1_s)\n\n\n    \n    print(f\"  Fold {fold+1} xong\")\n\nlevel1_cv_time = time.time() - level1_t0\n\nll_lr_proper  = log_loss(y_train, oof_lr)\nll_xgb_proper = log_loss(y_train, oof_xgb)\nprint(f\"  OOF LR   : {ll_lr_proper:.5f}\")\nprint(f\"  OOF XGB  : {ll_xgb_proper:.5f}\")\n\ndef ensemble_logloss(w):\n    blended = w[0] * oof_lr + w[1] * oof_xgb\n    blended = np.clip(blended, 1e-15, 1 - 1e-15)\n    blended = blended / blended.sum(axis=1, keepdims=True)\n    return log_loss(y_train, blended)\n\ncons = ({'type': 'eq', 'fun': lambda w: 1 - sum(w)})\nbounds = [(0.0, 1.0)] * 2\nresult = minimize(ensemble_logloss, x0=[0.5, 0.5], bounds=bounds, constraints=cons, method='SLSQP')\nw_lr_opt, w_xgb_opt = result.x\nll_opt = result.fun\n\nprint(f\"\\n  Optimal weights: w_LR={w_lr_opt:.4f}, w_XGB={w_xgb_opt:.4f} → Logloss: {ll_opt:.6f}\")\n\n\n# ── FINAL FIT ──\nprint(\"\\n  Fit Final Meta-Models on Set (Train + Pseudo)...\")\nscaler_l1 = StandardScaler()\nX_train_L1_ext_s = scaler_l1.fit_transform(X_train_L1_ext)\nX_test_L1_s      = scaler_l1.transform(X_test_L1_smart)\nmeta_lr = LogisticRegression(\n    C=0.1, solver='lbfgs', max_iter=2000,\n    multi_class='multinomial', class_weight='balanced',\n    random_state=RANDOM_STATE\n)\nmeta_lr.fit(X_train_L1_ext_s, y_train_ext)\n\n\n\nsw_l1_ext = compute_sample_weight(\"balanced\", y_train_ext)\nmeta_xgb = xgb.XGBClassifier(\n    objective='multi:softprob', num_class=NUM_CLASSES,\n    max_depth=3, n_estimators=200, learning_rate=0.05,\n    subsample=0.8, tree_method='hist', device='cuda', random_state=99\n)\nmeta_xgb.fit(X_train_L1_ext_s, y_train_ext, sample_weight=sw_l1_ext)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"4bc08d72","cell_type":"code","source":"# ============================================================\n# CELL 11 — ĐÁNH GIÁ CHI PHÍ TRAINING & INFERENCE\n# ============================================================\n\nprint(\"\\n\" + \"=\" * 55)\nprint(\"BƯỚC 9: ĐÁNH GIÁ CHI PHÍ → Xuất file report\")\nprint(\"=\" * 55)\n\n# ── 1. Model Size ──\nmodel_sizes = {}\ntotal_model_size_mb = 0\nfor view_name in VIEW_MODELS.keys():\n    model_path = os.path.join(MODEL_DIR, f\"{view_name}_full.pkl\")\n    if os.path.exists(model_path):\n        size_mb = os.path.getsize(model_path) / (1024 * 1024)\n        model_sizes[view_name] = size_mb\n        total_model_size_mb += size_mb\n\n# ── 2. Training Time ──\ntotal_l0_time = sum(level0_times.values())\ntotal_pipeline_time = total_l0_time + level1_cv_time\n\n# ── 3. Inference Time ──\nt_inf_start = time.time()\n\nt_l0_inf = time.time()\nfor view_name in VIEW_MODELS.keys():\n    feat_key = VIEW_FEATURE_MAP[view_name]\n    m = joblib.load(os.path.join(MODEL_DIR, f\"{view_name}_full.pkl\"))\n    _ = m.predict_proba(X_test_views[feat_key])\n    del m\nt_l0_inf = time.time() - t_l0_inf\n\nt_l1_inf = time.time()\n_ = meta_lr.predict_proba(X_test_L1_s)\n_ = meta_xgb.predict_proba(X_test_L1_s)\n\nt_l1_inf = time.time() - t_l1_inf\n\nt_inference = time.time() - t_inf_start\nn_test_samples = len(X_test_L1)\n\n# ── 4. Memory Usage ──\nmem_rss_gb = None\nmem_vms_gb = None\ntry:\n    import psutil\n    process = psutil.Process()\n    mem_info = process.memory_info()\n    mem_rss_gb = mem_info.rss / (1024**3)\n    mem_vms_gb = mem_info.vms / (1024**3)\nexcept ImportError:\n    pass\n\n# ══════════════════════════════════════════════════════════════\n# TẠO FILE REPORT\n# ══════════════════════════════════════════════════════════════\nreport_path = os.path.join(WORKING_DIR, 'cost_evaluation_report.md')\nlines = []\n\nlines.append(\"# Cost Evaluation Report — Multi-view Stacking V3\")\nlines.append(\"\")\nlines.append(f\"**Pipeline**: 9 Level-0 models (XGB+LGB) → Meta-Feature Engineering → 2 Level-1 models → SLSQP Ensemble\")\nlines.append(f\"**Dataset**: {len(y_train):,} train / {n_test_samples:,} test — {NUM_CLASSES} classes\")\nlines.append(f\"**Hardware**: Kaggle T4 GPU\")\nlines.append(\"\")\n\n# Model Sizes\nlines.append(\"## 1. Model Sizes (Level-0)\")\nlines.append(\"\")\nlines.append(\"| Model | Size (MB) |\")\nlines.append(\"|---|---:|\")\nfor name, size in model_sizes.items():\n    lines.append(f\"| {name} | {size:.2f} |\")\nlines.append(f\"| **TOTAL** | **{total_model_size_mb:.2f}** |\")\nlines.append(\"\")\n\n# Training Time\nlines.append(\"## 2. Training Time\")\nlines.append(\"\")\nlines.append(\"### Level-0 (9 models x 5-fold CV + full fit)\")\nlines.append(\"\")\nlines.append(\"| View | Time (s) | % of L-0 |\")\nlines.append(\"|---|---:|---:|\")\nfor name, t in level0_times.items():\n    lines.append(f\"| {name} | {t:.1f} | {100*t/total_l0_time:.1f}% |\")\nlines.append(f\"| **TOTAL Level-0** | **{total_l0_time:.1f}** | **{total_l0_time/60:.1f} min** |\")\nlines.append(\"\")\n\nlines.append(\"### Level-1 (nested 5-fold CV + weight optimization)\")\nlines.append(\"\")\nlines.append(f\"| Phase | Time (s) |\")\nlines.append(f\"|---|---:|\")\nlines.append(f\"| Level-1 CV + Optimize | {level1_cv_time:.1f} |\")\nlines.append(\"\")\n\nlines.append(\"### Total Pipeline\")\nlines.append(\"\")\nlines.append(\"| Phase | Time (s) | % |\")\nlines.append(\"|---|---:|---:|\")\nlines.append(f\"| Level-0 | {total_l0_time:.1f} | {100*total_l0_time/total_pipeline_time:.1f}% |\")\nlines.append(f\"| Level-1 | {level1_cv_time:.1f} | {100*level1_cv_time/total_pipeline_time:.1f}% |\")\nlines.append(f\"| **TOTAL** | **{total_pipeline_time:.1f}** | **{total_pipeline_time/60:.1f} min** |\")\nlines.append(\"\")\n\n# Inference Time\nlines.append(\"## 3. Inference Time\")\nlines.append(\"\")\nlines.append(f\"| Phase | Time (s) |\")\nlines.append(f\"|---|---:|\")\nlines.append(f\"| Level-0 (9 models) | {t_l0_inf:.2f} |\")\nlines.append(f\"| Level-1 (2 meta-models) | {t_l1_inf:.2f} |\")\nlines.append(f\"| **Total ({n_test_samples:,} samples)** | **{t_inference:.2f}** |\")\nlines.append(f\"| **Per sample** | **{1000*t_inference/n_test_samples:.3f} ms** |\")\nlines.append(\"\")\n\n# Memory\nlines.append(\"## 4. Memory Usage\")\nlines.append(\"\")\nif mem_rss_gb is not None:\n    lines.append(f\"| Metric | Value |\")\n    lines.append(f\"|---|---:|\")\n    lines.append(f\"| RSS (Resident) | {mem_rss_gb:.2f} GB |\")\n    lines.append(f\"| VMS (Virtual) | {mem_vms_gb:.2f} GB |\")\nelse:\n    lines.append(\"*psutil not available — memory report skipped*\")\nlines.append(\"\")\n\n# Feature Dimensions\nlines.append(\"## 5. Feature Dimensions\")\nlines.append(\"\")\nlines.append(\"| View | Features | Model Type |\")\nlines.append(\"|---|---:|---|\")\nview_info = [\n    (\"tab\", 107, \"XGB\"), (\"byteng\", 3000, \"LGB\"), (\"opng\", 2000, \"XGB\"),\n    (\"pixel\", 1024, \"XGB\"), (\"api\", 173, \"XGB\"), (\"sections\", 27, \"XGB\"),\n    (\"strings\", 2000, \"LGB\"), (\"all (XGB)\", 8331, \"XGB\"), (\"all (LGB)\", 8331, \"LGB\"),\n]\nfor name, dim, model_type in view_info:\n    lines.append(f\"| {name} | {dim:,} | {model_type} |\")\nlines.append(f\"| **Unique total** | **8,331** | |\")\nlines.append(\"\")\n\n# DL Comparison\nlines.append(\"## 6. So sanh voi Deep Learning\")\nlines.append(\"\")\nlines.append(\"| Metric | Our ML Pipeline | Typical DL (CNN/LSTM) |\")\nlines.append(\"|---|---|---|\")\nlines.append(f\"| Training Time | {total_pipeline_time/60:.0f} min | 3-6 hours |\")\nlines.append(f\"| GPU Requirement | Optional (accelerates) | Mandatory |\")\nlines.append(f\"| Total Model Size | {total_model_size_mb:.0f} MB | 200-500 MB |\")\nlines.append(f\"| Inference (per sample) | {1000*t_inference/n_test_samples:.2f} ms | 10-50 ms |\")\nlines.append(f\"| Framework | sklearn + XGB/LGB | PyTorch/TensorFlow |\")\nlines.append(f\"| Interpretability | High (SHAP ready) | Low (black box) |\")\nlines.append(f\"| Hyperparameter Tuning | Moderate | Extensive |\")\nlines.append(\"\")\n\n# Write file\nreport_content = '\\n'.join(lines)\nwith open(report_path, 'w', encoding='utf-8') as f:\n    f.write(report_content)\n\nprint(f\"\\n  Report saved to: {report_path}\")\nprint(f\"  Model size total: {total_model_size_mb:.2f} MB\")\nprint(f\"  Training time:    {total_pipeline_time/60:.1f} min\")\nprint(f\"  Inference:        {1000*t_inference/n_test_samples:.3f} ms/sample\")\nif mem_rss_gb:\n    print(f\"  Memory RSS:       {mem_rss_gb:.2f} GB\")","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","papermill":{"duration":3193.770032,"end_time":"2026-04-22T12:53:11.188510+00:00","exception":false,"start_time":"2026-04-22T11:59:57.418478+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"2bbc7996-3b7f-446b-a1cd-fed21b4f1376","cell_type":"code","source":"# ============================================================\n# CELL 12 — TẠO SUBMISSION\n# ============================================================\n\nprint(\"\\n\" + \"=\" * 55)\nprint(\"BƯỚC 10: TẠO SUBMISSIONS\")\nprint(\"=\" * 55)\n\nproba_lr_test  = meta_lr.predict_proba(X_test_L1_s)\nproba_xgb_test = meta_xgb.predict_proba(X_test_L1_s)\nproba_ens_opt = w_lr_opt * proba_lr_test + w_xgb_opt * proba_xgb_test\nproba_ens_opt = np.clip(proba_ens_opt, 1e-15, 1 - 1e-15)\nproba_ens_opt = proba_ens_opt / proba_ens_opt.sum(axis=1, keepdims=True)\n\ndf_sub = pd.read_csv(SUBMISSION_CSV)\nhas_pred_cols = any(c.startswith(\"Prediction\") for c in df_sub.columns)\n\n# ── Direct Level-0 average ───────────────────────────────────\nproba_direct_avg = X_test_L1.reshape(X_test_L1.shape[0], -1, NUM_CLASSES).mean(axis=1)\n\nsubmissions = {\n    \"ensemble_v2_smart_opt\": (proba_ens_opt,    f\"OPTIMIZED LR={w_lr_opt:.2f} XGB={w_xgb_opt:.2f}\"),\n    \"lr_only\":               (proba_lr_test,    f\"LR only (OOF: {ll_lr_proper:.5f})\"),\n    \"xgb_only\":          (proba_xgb_test,   f\"XGB Only (OOF: {ll_xgb_proper:.5f})\"),\n    \"direct_avg\":            (proba_direct_avg, \"Direct Level-0 average (bypass Level-1)\"),\n}\n\nfor name, (proba, note) in submissions.items():\n    proba = np.clip(proba, 1e-15, 1 - 1e-15)\n    proba = proba / proba.sum(axis=1, keepdims=True)\n\n    df_out = df_sub[[\"Id\"]].copy()\n    if has_pred_cols:\n        for j in range(NUM_CLASSES):\n            df_out[f\"Prediction{j+1}\"] = proba[:, j]\n    else:\n        df_out[\"Class\"] = proba.argmax(axis=1) + 1\n\n    path = os.path.join(WORKING_DIR, f\"submission_{name}.csv\")\n    df_out.to_csv(path, index=False)\n\n    print(f\"\\n   {path}\")\n    print(f\"    [{note}]\")\n\nprint(\"\\n\" + \"=\" * 55)\nprint(\"HOÀN TẤT PIPELINE\")\nprint(\"=\" * 55)\nprint(f\"   THỨ TỰ ƯU TIÊN NỘP BÀI:\")\nprint(f\"     1. submission_ensemble_v2_smart_opt.csv (Tốt nhất - Trộn 2 Level-1 + Smart Meta-features)\")\nprint(f\"     2. submission_lr_only.csv (Kinh điển)\")\nprint(f\"     3. submission_xgb_only.csv (Kinh điển)\")\nprint(f\"     4. submission_direct_avg.csv (Chỉ để check độ lệch chuẩn)\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}