{"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":[{"sourceId":38760,"databundleVersionId":4493939,"sourceType":"competition"},{"sourceId":4461402,"sourceType":"datasetVersion","datasetId":2611514},{"sourceId":4474043,"sourceType":"datasetVersion","datasetId":2601572}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# OTTO BERT4Rec with CUDF Covisitation Matrix\n\nPipeline sử dụng CUDF (RAPIDS GPU) để build covisitation matrices + BERT4Rec cho predictions.\n\n**Data: hgy1 và hgy2**","metadata":{}},{"cell_type":"code","source":"!pip install pandas numpy matplotlib seaborn polars pyarrow tqdm h5py pydantic -q\n!pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 -q\n!pip install recbole -q\n!pip install protobuf==3.20.0 -q","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport sys\nimport gc\nimport glob\nimport random\nfrom collections import defaultdict, Counter\nfrom multiprocessing import Pool\n\nimport numpy as np\nimport pandas as pd\nimport polars as pl\n\nimport cudf\nimport numba\n\nfrom tqdm.auto import tqdm\nimport pyarrow.parquet as pq","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 1. Configuration & Parameters","metadata":{}},{"cell_type":"code","source":"\nTYPE_LABELS = {\n    \"clicks\": 0, \n    \"carts\": 1, \n    \"orders\": 2\n}\n\n# Reverse mapping\nID_TO_TYPE = {v: k for k, v in TYPE_LABELS.items()}\n\n# Matrix computation config\nDISK_PIECES = 4  # Number of partitions for memory management\nTOP_N_COVISIT = 20  # Top N items to keep per source item\n\n# RecBole config\nMAX_ITEM = 20","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 2. Matrix Utility Functions","metadata":{}},{"cell_type":"code","source":"def df_parallelize_run(func, t_split):\n    \"\"\"\n    Pool multiprocessing for speedup.\n    \"\"\"\n    num_cores = np.min([20, len(t_split)])\n    pool = Pool(num_cores)\n    df = pool.map(func, t_split)\n    pool.close()\n    pool.join()\n    return df\n\n\ndef matrix_to_candids_dict(matrix):\n    \"\"\"\n    Phiên bản AN TOÀN: Kiểm tra bảng rỗng trước khi xử lý.\n    \"\"\"\n    # 1. Check an toàn: Nếu matrix rỗng hoặc None\n    if matrix is None or len(matrix) == 0:\n        print(\"⚠️ Cảnh báo: Matrix rỗng! Không tìm thấy co-visitation nào.\")\n        return {}\n\n    # 2. Check an toàn: Nếu là DataFrame nhưng thiếu cột\n    if isinstance(matrix, pd.DataFrame):\n        if 'aid_x' not in matrix.columns:\n            print(\"⚠️ Cảnh báo: Matrix thiếu cột 'aid_x'.\")\n            return {}\n        matrix = cudf.from_pandas(matrix)\n        is_pandas = True\n    else:\n        is_pandas = False\n    \n    # Code cũ giữ nguyên\n    matrix = matrix.sort_values([\"aid_x\", \"wgt\"], ascending=[True, False])\n    candids = matrix[[\"aid_x\", \"aid_y\"]].groupby(\"aid_x\").agg(list)\n    \n    candids_pd = candids.to_pandas()\n    candids_pd[\"aid_y\"] = candids_pd[\"aid_y\"].apply(\n        lambda x: x.tolist() if hasattr(x, 'tolist') else (list(x) if isinstance(x, (list, tuple)) else x)\n    )\n    candids_dict = candids_pd.to_dict()[\"aid_y\"]\n    \n    if not is_pandas:\n        del matrix, candids\n        clear_gpu_memory()\n\n    return candids_dict","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 3. Covisitation Matrix Computation (CUDF)","metadata":{}},{"cell_type":"code","source":"def read_file_to_cache(f):\n    \"\"\"\n    Reads a parquet file to cache for later use.\n    Data already has type as int (0, 1, 2), no mapping needed.\n    \"\"\"\n    df = pd.read_parquet(f)\n    \n    # Convert timestamp from ms to seconds if needed\n    # Check if ts is in milliseconds (very large number)\n    if df.ts.max() > 1e12:\n        df.ts = (df.ts / 1000).astype(\"int32\")\n    else:\n        df.ts = df.ts.astype(\"int32\")\n    df = df[df[\"type\"].notna()]\n    df[\"type\"] = df[\"type\"].astype(\"int8\")\n\n    return df\n\n\ndef read_file(f, data_cache):\n    \"\"\"\n    Converts cached pandas DataFrame to cudf DataFrame.\n    \"\"\"\n    return cudf.DataFrame(data_cache[f])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def compute_covisitation_matrix_optimized(\n    files,\n    weighting=\"\",\n    type_weight={},\n    considered_types=[0, 1, 2],\n    n=20,\n    save_folder=\"\",\n    suffix=\"\",\n    chunk_size=1,  # Process 1 file at a time to minimize memory\n    max_session_length=20,  # Limit to 20 events per session to reduce pairs\n):\n    \"\"\"\n    Memory-optimized covisitation matrix computation.\n    \n    Key differences from original:\n    - No pre-caching: loads files directly to GPU\n    - Smaller chunk size: 5 files instead of 10\n    - More aggressive memory cleanup\n    \n    Args:\n        files (list): List of parquet filenames to process.\n        weighting (str): \"temporal\", \"type\", or \"\" (none).\n        type_weight (dict): Event type weights for weighting=\"type\".\n        considered_types (list): Event types to include [0, 1, 2].\n        n (int): Top N items to keep per source item.\n        save_folder (str): Folder to save the matrix.\n        suffix (str): Save suffix.\n        chunk_size (int): Number of files to process at once.\n        max_session_length (int): Max events per session to consider.\n    \n    Returns:\n        pandas DataFrame: Covisitation matrix.\n    \"\"\"\n    DISK_PIECES = 4\n    SIZE = 1.86e6 / DISK_PIECES\n\n    # Process files in smaller chunks\n    chunks = [files[x: x + chunk_size] for x in range(0, len(files), chunk_size)]\n\n    matrices = []\n    for part in range(DISK_PIECES):\n        print(f\"\\n{'='*50}\")\n        print(f\"Processing partition {part + 1}/{DISK_PIECES}\")\n        \n        matrix = None\n        \n        for idx, chunk in enumerate(tqdm(chunks, desc=f\"Part {part}\")):\n            try:\n                # LAZY LOAD - read directly to GPU, no caching\n                df = cudf.concat(\n                    [read_file_lazy(file) for file in chunk], ignore_index=True\n                )\n\n                # Filter by considered event types\n                if considered_types != [0, 1, 2]:\n                    df = df.loc[df[\"type\"].isin(considered_types)]\n\n                # Sort by session and timestamp (descending for recency)\n                df = df.sort_values([\"session\", \"ts\"], ascending=[True, False])\n\n                # LIMIT SESSION LENGTH - keep only last N events per session\n                df = df.reset_index(drop=True)\n                df[\"n\"] = df.groupby(\"session\").cumcount()\n                df = df.loc[df.n < max_session_length].drop(\"n\", axis=1)\n\n                # PARTITION EARLY - filter source items before self-join to save memory\n                df = df.loc[(df.aid >= part * SIZE) & (df.aid < (part + 1) * SIZE)]\n                \n                if len(df) == 0:\n                    del df\n                    clear_gpu_memory()\n                    continue\n\n                # CREATE PAIRS - self-join on session\n                # Rename columns before merge to avoid _x, _y suffix issues\n                df_left = df.rename(columns={\"aid\": \"aid_x\", \"ts\": \"ts_x\", \"type\": \"type_x\"})\n                df_right = df.rename(columns={\"aid\": \"aid_y\", \"ts\": \"ts_y\", \"type\": \"type_y\"})\n                \n                del df\n                clear_gpu_memory()\n                \n                df = df_left.merge(df_right, on=\"session\")\n                \n                del df_left, df_right\n                clear_gpu_memory()\n                \n                # Filter pairs:\n                # - Within 24 hours of each other\n                # - Different items (no self-loops)\n                df = df.loc[\n                    ((df.ts_x - df.ts_y).abs() < 24 * 60 * 60) & (df.aid_x != df.aid_y)\n                ]\n\n                # ASSIGN WEIGHTS - drop duplicates first\n                df = df[[\"session\", \"aid_x\", \"aid_y\", \"ts_x\", \"type_y\"]].drop_duplicates(\n                    [\"session\", \"aid_x\", \"aid_y\"]\n                )\n\n                if weighting == \"temporal\":\n                    df.drop(\"type_y\", axis=1, inplace=True)\n                    df[\"wgt\"] = 1 + 3 * (df.ts_x - 1659304800) / (1662328791 - 1659304800)\n                elif weighting == \"type\":\n                    df.drop(\"ts_x\", axis=1, inplace=True)\n                    df[\"wgt\"] = df.type_y.map(type_weight)\n                else:\n                    df.drop([\"type_y\", \"ts_x\"], axis=1, inplace=True)\n                    df[\"wgt\"] = 1\n\n                # Aggregate weights\n                df = df[[\"aid_x\", \"aid_y\", \"wgt\"]]\n                df.wgt = df.wgt.astype(\"float32\")\n                df = df.groupby([\"aid_x\", \"aid_y\"]).wgt.sum()\n\n                # COMBINE CHUNKS within partition\n                if matrix is None:\n                    matrix = df\n                else:\n                    matrix = matrix.add(df, fill_value=0)\n\n                del df\n                clear_gpu_memory()\n                \n            except Exception as e:\n                print(f\"Error processing chunk {idx}: {e}\")\n                clear_gpu_memory()\n                continue\n\n        if matrix is None or len(matrix) == 0:\n            print(f\"Warning: No data for partition {part}\")\n            continue\n            \n        # FINALIZE PARTITION\n        matrix = matrix.reset_index()\n        matrix = matrix.sort_values([\"aid_x\", \"wgt\"], ascending=[True, False])\n\n        # SAVE TOP N per source item\n        matrix = matrix.reset_index(drop=True)\n        matrix[\"n\"] = matrix.groupby(\"aid_x\").aid_y.cumcount()\n\n        if n:\n            matrix = matrix.loc[matrix.n < n].drop(\"n\", axis=1)\n\n        # Giữ trên GPU (cudf) thay vì convert sang pandas ngay\n        # Chỉ convert khi cần concat hoặc lưu file\n        matrices.append(matrix)\n        \n        # Không xóa matrix ngay, để giữ trên GPU\n        clear_gpu_memory()\n\n    if not matrices:\n        print(\"Warning: No matrices generated!\")\n        return pd.DataFrame()\n\n    # COMBINE ALL PARTITIONS trên GPU (nhanh hơn nhiều)\n    # Tất cả matrices đều là cudf DataFrame\n    print(f\"Combining {len(matrices)} partitions on GPU...\")\n    result = cudf.concat(matrices, ignore_index=True)\n    del matrices\n    clear_gpu_memory()\n    \n    # Chỉ convert sang pandas khi cần lưu file hoặc return\n    # Nếu save_folder được chỉ định, giữ trên GPU cho đến khi save\n\n    # SAVE FINAL MATRIX\n    if save_folder:\n        if weighting == \"type\":\n            weighting_str = weighting + \"\".join(map(str, list(type_weight.values())))\n        else:\n            weighting_str = weighting\n        \n        save_path = os.path.join(\n            save_folder,\n            f'matrix_{\"\".join(map(str, considered_types))}_{weighting_str}_{n}_{suffix}.pqt',\n        )\n        print(f\"\\nSaving matrix to {save_path}\")\n        # Cudf có thể lưu parquet trực tiếp, nhanh hơn\n        result.to_parquet(save_path)\n\n    # Convert sang pandas chỉ khi return (để tương thích với code cũ)\n    # Nhưng có thể giữ cudf nếu code sau đó hỗ trợ\n    result_pd = result.to_pandas()\n    del result\n    clear_gpu_memory()\n    \n    return result_pd\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_matrices_for_otto_optimized(\n    train_files,\n    save_folder=\"matrices\",\n    suffix=\"train\",\n    n=20,\n    chunk_size=1,  # Process 1 file at a time for safer memory\n):\n    \"\"\"\n    Memory-optimized version: Build all three covisitation matrices.\n    \n    Key difference: No pre-caching, uses lazy loading.\n    \n    Args:\n        train_files (list): List of training parquet files.\n        save_folder (str): Folder to save matrices.\n        suffix (str): Suffix for saved files.\n        n (int): Top N items to keep per source.\n        chunk_size (int): Files per chunk (lower = less memory).\n    \n    Returns:\n        tuple: (click_matrix, cart_matrix, order_matrix)\n    \"\"\"\n    os.makedirs(save_folder, exist_ok=True)\n    \n    print(\"=\"*60)\n    print(\"Building Click-to-Click Matrix...\")\n    print(\"=\"*60)\n    click_matrix = compute_covisitation_matrix_optimized(\n        files=train_files,\n        weighting=\"\",\n        considered_types=[0],  # clicks only\n        n=n,\n        save_folder=save_folder,\n        suffix=f\"clicks_{suffix}\",\n        chunk_size=chunk_size,\n    )\n    \n    # Cleanup between matrices\n    clear_gpu_memory()\n    \n    print(\"\\n\" + \"=\"*60)\n    print(\"Building Click/Cart-to-Cart Matrix...\")\n    print(\"=\"*60)\n    cart_matrix = compute_covisitation_matrix_optimized(\n        files=train_files,\n        weighting=\"type\",\n        type_weight={0: 1, 1: 6, 2: 3},\n        considered_types=[0, 1],  # clicks and carts\n        n=n,\n        save_folder=save_folder,\n        suffix=f\"carts_{suffix}\",\n        chunk_size=chunk_size,\n    )\n    \n    clear_gpu_memory()\n    \n    print(\"\\n\" + \"=\"*60)\n    print(\"Building Cart/Order-to-Order Matrix...\")\n    print(\"=\"*60)\n    order_matrix = compute_covisitation_matrix_optimized(\n        files=train_files,\n        weighting=\"type\",\n        type_weight={1: 6, 2: 3},\n        considered_types=[1, 2],  # carts and orders\n        n=n,\n        save_folder=save_folder,\n        suffix=f\"orders_{suffix}\",\n        chunk_size=chunk_size,\n    )\n    \n    print(\"\\n\" + \"=\"*60)\n    print(\"All matrices built successfully!\")\n    \n    return click_matrix, cart_matrix, order_matrix\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 4. Build Covisitation Matrices","metadata":{}},{"cell_type":"code","source":"# ====== LOAD DATA TO CACHE ======\n# Sử dụng hgy1 và hgy2 (single parquet files)\nTRAIN_FILES = [\n    '/kaggle/input/otto-train-and-test-data-for-local-validation/train.parquet',\n]\nSAVE_FOLDER = \"matrices\"\n\nprint(\"Loading data to cache...\")\nfiles = TRAIN_FILES\nprint(f\"Found {len(files)} parquet files\")\n\n# Cache all files in memory\n# data_cache = {}\n# for f in tqdm(files, desc=\"Caching files\"):\n#     data_cache[f] = read_file_to_cache(f)\n\n# print(f\"Cached {len(data_cache)} files\")\n# gc.collect()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def read_file_lazy(f):\n    \"\"\"\n    Reads a parquet file directly to cudf DataFrame (no caching).\n    \n    Args:\n        f (str): File path to load.\n    \n    Returns:\n        cudf DataFrame: GPU-accelerated DataFrame.\n    \"\"\"\n    df = cudf.read_parquet(f)\n    df[\"ts\"] = (df[\"ts\"] / 1000).astype(\"int32\")\n    # Handle both string and numeric type columns\n    if df[\"type\"].dtype == \"object\":\n        df[\"type\"] = df[\"type\"].map(TYPE_LABELS).astype(\"int8\")\n    else:\n        df[\"type\"] = df[\"type\"].astype(\"int8\")\n    return df\n\ndef clear_gpu_memory():\n    \"\"\"Aggressively clear GPU memory.\"\"\"\n    numba.cuda.current_context().deallocations.clear()\n    gc.collect()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# Không cần pre-cache data_cache nữa!\n# Build matrices trực tiếp với lazy loading\nclick_matrix, cart_matrix, order_matrix = build_matrices_for_otto_optimized(\n    train_files=TRAIN_FILES,\n    save_folder=SAVE_FOLDER,\n    suffix=\"train\",\n    n=TOP_N_COVISIT,\n    chunk_size=1,  # Xử lý 1 file mỗi lần\n)\n\ngc.collect()\nprint(\"Done!\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====== CONVERT TO CANDIDATE DICTIONARIES (CPU) ======\nimport gc\n\nprint(\"Converting matrices to Dictionaries...\")\nprint(\"(Using GPU for sorting, then moving to CPU RAM to save VRAM for Model)\")\n\n# 1. Chuyển đổi Matrix Clicks\n# Hàm matrix_to_candids_dict (đã sửa ở bước trước) sẽ dùng GPU để sort, sau đó trả về Dict CPU\nprint(\"Processing Click Matrix...\")\ncovisit_click2click = matrix_to_candids_dict(click_matrix)\nclear_gpu_memory() # Dọn dẹp ngay lập tức\n\n# 2. Chuyển đổi Matrix Carts\nprint(\"Processing Cart Matrix...\")\ncovisit_to_cart = matrix_to_candids_dict(cart_matrix)\nclear_gpu_memory()\n\n# 3. Chuyển đổi Matrix Orders\nprint(\"Processing Order Matrix...\")\ncovisit_to_order = matrix_to_candids_dict(order_matrix)\nclear_gpu_memory()\n\n# 4. In thông tin kiểm tra\nprint(f\"Click candidates: {len(covisit_click2click):,} source items\")\nprint(f\"Cart candidates: {len(covisit_to_cart):,} source items\")\nprint(f\"Order candidates: {len(covisit_to_order):,} source items\")\n\n# 5. QUAN TRỌNG: Xóa sạch các biến DataFrame gốc để giải phóng RAM/VRAM hoàn toàn\nprint(\"Cleaning up raw matrices...\")\ntry:\n    del click_matrix\n    del cart_matrix\n    del order_matrix\n    \n    # Xóa các biến cudf cũ nếu lỡ có chạy trước đó\n    if 'covisit_click2click_cudf' in globals(): del covisit_click2click_cudf\n    if 'covisit_to_cart_cudf' in globals(): del covisit_to_cart_cudf\n    if 'covisit_to_order_cudf' in globals(): del covisit_to_order_cudf\nexcept Exception as e:\n    pass\n\n# Gọi Garbage Collector để chắc chắn RAM được giải phóng\ngc.collect()\nclear_gpu_memory()\n\nprint(\"✓ Memory Cleaned. GPU is ready for BERT4Rec Model.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 5. Create RecBole Dataset","metadata":{}},{"cell_type":"code","source":"# Đọc dữ liệu test của OTTO từ hgy1 và hgy2\ntrain_df = pl.read_parquet('/kaggle/input/otto-train-and-test-data-for-local-validation/test.parquet')\ntest_df = pl.read_parquet('/kaggle/input/otto-full-optimized-memory-footprint/test.parquet')\n\ninter_df = pl.concat([train_df, test_df])\n\n# Sắp xếp theo session, aid và timestamp\ninter_df = inter_df.sort(['session', 'aid', 'ts'])\n\n# RecBole format\ninter_df = inter_df.with_columns((pl.col('ts') * 1e9).alias('ts'))\ninter_df = inter_df.rename({'session': 'session:token', 'aid': 'aid:token', 'ts': 'ts:float'})\n\nprint(inter_df.columns)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"directory = \"recbox_data\"\nif not os.path.exists(directory):\n    os.makedirs(directory)\n    print(f\"Created: {directory}\")\nelse:\n    print(f\"Exists: {directory}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pandas_df = inter_df[['session:token', 'aid:token', 'ts:float']].to_pandas()\n\npandas_df.to_csv(\n    'recbox_data/recbox_data.inter',\n    sep='\\t',\n    index=False\n)\n\ndel inter_df, pandas_df, train_df, test_df\ngc.collect()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 6. Train BERT4Rec Model","metadata":{}},{"cell_type":"code","source":"!pip install recbole[all]\n!pip install kmeans-pytorch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:10:35.223650Z","iopub.execute_input":"2026-01-10T05:10:35.223880Z","iopub.status.idle":"2026-01-10T05:10:46.504413Z","shell.execute_reply.started":"2026-01-10T05:10:35.223857Z","shell.execute_reply":"2026-01-10T05:10:46.503593Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import logging\nfrom logging import getLogger\nfrom recbole.config import Config\nfrom recbole.data import create_dataset, data_preparation\nfrom recbole.model.sequential_recommender import BERT4Rec\nfrom recbole.trainer import Trainer\nfrom recbole.utils import init_seed, init_logger\n\nfrom recbole.utils.case_study import full_sort_topk","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:11:08.746745Z","iopub.execute_input":"2026-01-10T05:11:08.747571Z","iopub.status.idle":"2026-01-10T05:11:39.102204Z","shell.execute_reply.started":"2026-01-10T05:11:08.747532Z","shell.execute_reply":"2026-01-10T05:11:39.101517Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nnp.float = np.float64\nnp.float_ = np.float64\nnp.int = np.int64\nnp.int_ = np.int64\nnp.bool = np.bool_\nnp.bool_ = np.bool_\nnp.complex = np.complex128\nnp.complex_ = np.complex128\nnp.object = np.object_\nnp.object_ = np.object_\nnp.str = np.str_\nnp.unicode = np.str_\nnp.unicode_ = np.str_\nnp.long = np.int64\n\nrecbole_config = {\n    'data_path': '.',\n    'USER_ID_FIELD': 'session',\n    'ITEM_ID_FIELD': 'aid',\n    'TIME_FIELD': 'ts',\n    'user_inter_num_interval': \"[5,Inf)\",\n    'item_inter_num_interval': \"[5,Inf)\",\n    'load_col': {'inter': ['session', 'aid', 'ts']},\n    'train_neg_sample_args': None,\n    \n    'epochs': 5,                     \n    'stopping_step': 5,    \n    'learning_rate': 0.0005,         \n\n    'train_batch_size': 512,          \n    'eval_batch_size': 512, \n    'transform': 'mask_itemseq',     \n    'mask_ratio': 0.15,\n\n    'MAX_ITEM_LIST_LENGTH': 20,\n    'eval_args': {\n        'split': {'RS': [9, 1, 0]},\n        'group_by': 'user',\n        'order': 'TO',\n        'mode': 'full',\n    },\n\n    'hidden_size': 128,              \n    'inner_size': 512,               \n    \n    'n_layers': 2,                  \n    'n_heads': 4,                    \n    \n    'hidden_dropout_prob': 0.2,      \n    'attn_dropout_prob': 0.2,        \n    \n    'layer_norm_eps': 1e-12,\n    'initializer_range': 0.02,\n    'loss_type': 'CE'\n}\n\nfrom recbole.config import Config\nfrom recbole.utils import init_seed, init_logger\nfrom logging import getLogger\nimport logging\n\nconfig = Config(model='BERT4Rec', dataset='recbox_data', config_dict=recbole_config)\ninit_seed(config['seed'], config['reproducibility'])\ninit_logger(config)\nlogger = getLogger()\n\nconsole_handler = logging.StreamHandler()\nconsole_handler.setLevel(logging.INFO)\nlogger.addHandler(console_handler)\nlogger.info(config)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:12:01.144274Z","iopub.execute_input":"2026-01-10T05:12:01.145034Z","iopub.status.idle":"2026-01-10T05:12:01.750213Z","shell.execute_reply.started":"2026-01-10T05:12:01.145000Z","shell.execute_reply":"2026-01-10T05:12:01.749113Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dataset = create_dataset(config)\nlogger.info(dataset)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:12:09.904409Z","iopub.execute_input":"2026-01-10T05:12:09.905001Z","iopub.status.idle":"2026-01-10T05:12:09.935423Z","shell.execute_reply.started":"2026-01-10T05:12:09.904970Z","shell.execute_reply":"2026-01-10T05:12:09.934448Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_data, valid_data, test_data = data_preparation(config, dataset)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = BERT4Rec(config, train_data.dataset).to(config['device'])\nlogger.info(model)\n\ntrainer = Trainer(config, model)\nbest_valid_score, best_valid_result = trainer.fit(train_data, valid_data)","metadata":{"scrolled":true,"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = BERT4Rec(config, train_data.dataset).to(config['device'])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nos.environ['CUDA_LAUNCH_BLOCKING'] = \"1\"\nimport torch","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"del trainer, train_data, valid_data, test_data\ngc.collect()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cudf\nimport pandas as pd\nimport gc\nimport torch\nfrom tqdm.auto import tqdm\nimport glob\nfrom recbole.data.interaction import Interaction\n\nif 'test_session_AIDs' not in globals():\n    test_df = pd.read_parquet('/kaggle/input/otto-full-optimized-memory-footprint/test.parquet')\n    test_df = test_df.sort_values(['session', 'ts']) # SORT QUAN TRỌNG\n    test_session_AIDs = test_df.groupby('session')['aid'].apply(list)\n    del test_df\n    gc.collect()\n\ndef get_bert_predictions(model, dataset, sessions_dict, topk=20, batch_size=256):\n    model.eval()\n    device = model.device\n    all_preds = []\n    \n    vocab = dataset.field2token_id[dataset.iid_field]\n    model_vocab_size = model.item_embedding.weight.shape[0]\n    \n    session_ids = list(sessions_dict.keys())\n    sequences = [[str(aid) for aid in sessions_dict[sess][-20:]] for sess in session_ids]\n    \n    print(f\"   Running BERT4Rec Inference on {len(session_ids)} sessions...\")\n    \n    for start_idx in tqdm(range(0, len(sequences), batch_size)):\n        end_idx = min(start_idx + batch_size, len(sequences))\n        batch_seqs = sequences[start_idx:end_idx]\n        \n        batch_ids = []\n        lengths = []\n        for seq in batch_seqs:\n            ids = [vocab[t] for t in seq if t in vocab and vocab[t] < model_vocab_size]\n            if not ids: ids = [0] \n            batch_ids.append(ids)\n            lengths.append(len(ids))\n            \n        max_len = 20 \n        padded = torch.zeros((len(batch_seqs), max_len), dtype=torch.long, device=device)\n        for i, ids in enumerate(batch_ids):\n            l = min(len(ids), max_len)\n            padded[i, :l] = torch.tensor(ids[-l:], dtype=torch.long, device=device)\n            \n        len_tensor = torch.tensor(lengths, dtype=torch.long, device=device)\n        \n        try:\n            with torch.no_grad():\n                inter = Interaction({\n                    model.ITEM_SEQ: padded,\n                    model.ITEM_SEQ_LEN: len_tensor\n                })\n                scores = model.full_sort_predict(inter)\n                scores[:, 0] = -1e9 # Mask padding token (item 0)\n                \n                _, topk_indices = torch.topk(scores, min(topk, scores.shape[1]), dim=1)\n                \n            batch_tokens = dataset.id2token(dataset.iid_field, topk_indices.cpu().numpy())\n            \n            for row in batch_tokens:\n                all_preds.append([int(x) for x in row if str(x).isdigit()])\n                \n        except Exception as e:\n            print(f\"Error batch {start_idx}: {e}\")\n            all_preds.extend([[] for _ in range(len(batch_seqs))])\n\n    return all_preds\n\n\nbert_preds = get_bert_predictions(model, dataset, test_session_AIDs, topk=20, batch_size=256)\n\nprint(\"   -> Clearing VRAM...\")\ntry: del model\nexcept: pass\n\ntry: del dataset\nexcept: pass\n\ntry: del train_data, valid_data\nexcept: pass\n\nif 'trainer' in globals(): \n    try: del trainer\n    except: pass\n\ngc.collect()\ntorch.cuda.empty_cache()\n\ndef get_covisit_candidates_gpu(session_last_items_dict, matrix_path, top_n=20):\n    \"\"\"\n    Query ma trận bằng Item cuối cùng thực sự (Last Item)\n    \"\"\"\n    print(f\"   Loading matrix from {matrix_path}...\")\n    # Load pqt lên GPU\n    matrix = cudf.read_parquet(matrix_path)\n    matrix = matrix[['aid_x', 'aid_y', 'wgt']]\n    \n    print(\"   Preparing query...\")\n    df_query = cudf.DataFrame({\n        'session': list(session_last_items_dict.keys()),\n        'aid_x': list(session_last_items_dict.values())\n    })\n    \n    print(\"   Merging & Ranking...\")\n    recs = df_query.merge(matrix, on='aid_x', how='inner')\n    recs = recs.sort_values(['session', 'wgt'], ascending=[True, False])\n    recs = recs.groupby('session').head(top_n)\n    \n    # Đưa về CPU dict\n    recs_pd = recs[['session', 'aid_y']].to_pandas()\n    del matrix, df_query, recs\n    gc.collect()\n    \n    return recs_pd.groupby('session')['aid_y'].apply(list).to_dict()\n\n\nlast_item_dict = {sess: aids[-1] for sess, aids in test_session_AIDs.items() if len(aids) > 0}\n\nbase_candidates_dict = {}\nsession_ids_list = list(test_session_AIDs.keys())\n\nfor i, sess_id in enumerate(session_ids_list):\n    history = test_session_AIDs[sess_id]\n    model_rec = bert_preds[i] \n    \n    uniq = list(dict.fromkeys(history[::-1] + model_rec))\n    base_candidates_dict[sess_id] = uniq[:20]\n\nfinal_submission_data = []\n\nmatrix_files = {\n    'clicks': glob.glob(\"matrices/*clicks*.pqt\")[0],\n    'carts':  glob.glob(\"matrices/*carts*.pqt\")[0],\n    'orders': glob.glob(\"matrices/*orders*.pqt\")[0]\n}\n\nfor st in ['clicks', 'carts', 'orders']:\n    print(f\"\\n--- Processing {st.upper()} ---\")\n    \n    covisit_recs = get_covisit_candidates_gpu(last_item_dict, matrix_files[st], top_n=20)\n    \n    print(f\"   Finalizing {st}...\")\n    for sess_id in tqdm(session_ids_list):\n        recs = base_candidates_dict[sess_id].copy()\n        \n        if sess_id in covisit_recs:\n            for item in covisit_recs[sess_id]:\n                if item not in recs:\n                    recs.append(item)\n                if len(recs) >= 20: break\n        \n        final_submission_data.append({\n            'session_type': f\"{sess_id}_{st}\",\n            'labels': \" \".join(map(str, recs[:20]))\n        })\n        \n    del covisit_recs\n    gc.collect()\n\n\nsubmission = pd.DataFrame(final_submission_data)\nsubmission.to_csv('submission_bert_hybrid_optimized.csv', index=False)\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-01-02T11:39:55.92Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}