{"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":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# OTTO SASRec with CUDF Covisitation Matrix\n\nPipeline sử dụng CUDF (RAPIDS GPU) để build covisitation matrices + SASRec cho predictions.\n\n**Data: hgy1 và hgy2**","metadata":{}},{"cell_type":"code","source":"# Install dependencies\n!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 THƯ VIỆN ======\nimport 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\n# RAPIDS cudf for GPU acceleration\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":"# ====== OTTO PARAMETERS ======\n\n# Event type mapping\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    Converts a matrix to a dict of candidates sorted by weight.\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    try:\n        candids = candids.to_pandas()\n    except AttributeError:\n        pass\n\n    candids[\"aid_y\"] = candids[\"aid_y\"].apply(lambda x: x.tolist() if hasattr(x, 'tolist') else x)\n    candids_dict = candids.to_dict()[\"aid_y\"]\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        # Convert to pandas and store\n        matrices.append(matrix.to_pandas())\n        \n        del matrix\n        clear_gpu_memory()\n\n    if not matrices:\n        print(\"Warning: No matrices generated!\")\n        return pd.DataFrame()\n\n    # COMBINE ALL PARTITIONS\n    result = pd.concat(matrices, ignore_index=True)\n    del matrices\n    gc.collect()\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        result.to_parquet(save_path)\n\n    clear_gpu_memory()\n    \n    return result\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: gộp 2 file test.parquet làm train data (giống bert4rec.ipynb)\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":"# 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 DICTS ======\nprint(\"Converting matrices to candidate dictionaries...\")\n\ncovisit_click2click = matrix_to_candids_dict(click_matrix)\ncovisit_to_cart = matrix_to_candids_dict(cart_matrix)\ncovisit_to_order = matrix_to_candids_dict(order_matrix)\n\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# Free matrix memory\ndel click_matrix, cart_matrix, order_matrix\ngc.collect()","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 SASRec Model","metadata":{}},{"cell_type":"code","source":"!pip install recbole -q\n!pip install kmeans-pytorch -q","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import logging\nimport torch\nfrom logging import getLogger\nfrom recbole.config import Config\nfrom recbole.data import create_dataset, data_preparation\nfrom recbole.model.sequential_recommender import SASRec\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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Vá lỗi các kiểu dữ liệu số và logic\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_\n\n# 2. Vá lỗi các kiểu dữ liệu chuỗi và số nguyên dài (Sửa lỗi np.unicode_)\nnp.str = np.str_\nnp.unicode = np.str_     # np.unicode_ đã bị xóa, dùng np.str_ thay thế\nnp.unicode_ = np.str_\nnp.long = np.int64       # np.long thường tương ứng với 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    'epochs': 20,\n    'stopping_step': 3,\n    'eval_batch_size': 512,  # Giảm để tiết kiệm memory cho Kaggle free\n    'train_batch_size': 512,  # Giảm để tiết kiệm memory cho Kaggle free\n    'MAX_ITEM_LIST_LENGTH': MAX_ITEM,\n    'eval_args': {\n        'split': {'RS': [9, 1, 0]},\n        'group_by': 'user',\n        'order': 'TO',\n        'mode': 'full',\n    },\n    # ====== CÁC THAM SỐ ĐẶC TRƯNG CHO SASRec (Tối ưu cho Kaggle Free GPU) ======\n    'hidden_size': 64,              # Giảm từ mặc định để tiết kiệm memory\n    'inner_size': 256,              # Kích thước feed-forward layer\n    'n_layers': 2,                  # Số layers Transformer (giảm để nhanh hơn)\n    'n_heads': 2,                   # Số attention heads (giảm để tiết kiệm memory)\n    'dropout_prob': 0.2,           # Dropout probability (SASRec dùng chung một dropout)\n    'layer_norm_eps': 1e-12,        # Epsilon cho layer normalization\n    'initializer_range': 0.02,      # Range cho weight initialization\n}\n\nconfig = Config(model='SASRec', 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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dataset = create_dataset(config)\nlogger.info(dataset)","metadata":{"trusted":true},"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 = SASRec(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":"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\n\n# ==============================================================================\n# BƯỚC 1: LẤY DỰ ĐOÁN TỪ SASRec (Batch Inference)\n# ==============================================================================\n\n# 1.1 Chuẩn bị dữ liệu\nprint(\"1. Preparing Test Data...\")\nif 'test_session_AIDs' not in globals():\n    test_df = pd.read_parquet('/kaggle/input/otto-full-optimized-memory-footprint/test.parquet')\n    # Sort quan trọng để lấy đúng last item\n    test_df = test_df.sort_values(['session', 'ts'])\n    test_session_AIDs = test_df.groupby('session')['aid'].apply(list)\n    test_session_types = test_df.groupby('session')['type'].apply(list)\n    del test_df\n    gc.collect()\n\n# 1.2 Hàm chạy SASRec Batch\ndef get_sasrec_predictions(model, dataset, sessions_dict, topk=20, batch_size=512):\n    model.eval()\n    device = model.device\n    all_preds = []\n    \n    # Chuyển dict thành list các sequences\n    # Lưu ý: RecBole cần input là string token\n    session_ids = list(sessions_dict.keys())\n    # Chỉ lấy MAX_ITEM cuối cùng để đưa vào model\n    MAX_ITEM = 20 # Đảm bảo biến này khớp với config model\n    sequences = [[str(aid) for aid in sessions_dict[sess][-MAX_ITEM:]] for sess in session_ids]\n    \n    vocab = dataset.field2token_id[dataset.iid_field]\n    model_vocab_size = model.item_embedding.weight.shape[0]\n    \n    print(f\"   Running SASRec 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        # Prepare Batch Tensor\n        batch_ids = []\n        lengths = []\n        for seq in batch_seqs:\n            # Filter valid tokens\n            ids = [vocab[t] for t in seq if t in vocab and vocab[t] < model_vocab_size]\n            if not ids: ids = [0] # Handle empty\n            batch_ids.append(ids)\n            lengths.append(len(ids))\n            \n        # Padding\n        max_len = 20 # Fixed max len\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        # Predict\n        try:\n            with torch.no_grad():\n                # Tạo interaction giả lập\n                from recbole.data.interaction import Interaction\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 pad\n                _, topk_indices = torch.topk(scores, min(topk, scores.shape[1]), dim=1)\n                \n            # Decode\n            batch_tokens = dataset.id2token(dataset.iid_field, topk_indices.cpu().numpy())\n            \n            # Convert to int list\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# 1.3 Thực thi\nsasrec_preds = get_sasrec_predictions(model, dataset, test_session_AIDs, topk=20, batch_size=512)\n\n# --- SỬA LỖI NAME ERROR (XÓA AN TOÀN) ---\nprint(\"   -> Clearing VRAM...\")\ntry: del model\nexcept: pass\n\ntry: del dataset\nexcept: pass\n\ntry: del train_data\nexcept: pass\n\ntry: del valid_data\nexcept: pass\n\nif 'trainer' in globals():\n    try: del trainer\n    except: pass\n\ngc.collect()\ntorch.cuda.empty_cache()\n# ----------------------------------------\n\n# ==============================================================================\n# BƯỚC 2: HÀM QUERY MATRIX TRÊN GPU (Fixed Logic & Performance)\n# ==============================================================================\ndef get_covisit_candidates_gpu(session_last_items_dict, matrix_path, top_n=20):\n    print(f\"   Loading matrix from {matrix_path}...\")\n    matrix = cudf.read_parquet(matrix_path)\n    matrix = matrix[['aid_x', 'aid_y', 'wgt']]\n    \n    print(\"   Preparing query...\")\n    # Tạo dataframe chứa item cuối cùng của mỗi session\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    # Merge 1 lần duy nhất cho 1.6 triệu user (Cực nhanh)\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    # Convert về 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# ==============================================================================\n# BƯỚC 3: HYBRID MERGE & SUBMISSION\n# ==============================================================================\nprint(\"\\n2. [Hybrid] Merging Results...\")\n\n# 3.1 Tạo Base Candidates (History + SASRec)\n# Lấy item cuối cùng thực sự để query matrix\nlast_item_dict = {sess: aids[-1] for sess, aids in test_session_AIDs.items() if len(aids) > 0}\nbase_candidates_dict = {}\nsession_ids_list = list(test_session_AIDs.keys())\n\n# Ghép History và SASRec\nfor i, sess_id in enumerate(session_ids_list):\n    history = test_session_AIDs[sess_id]\n    model_rec = sasrec_preds[i]\n    # Logic: History (đảo ngược) -> SASRec\n    uniq = list(dict.fromkeys(history[::-1] + model_rec))\n    base_candidates_dict[sess_id] = uniq[:20]\n\nfinal_submission_data = []\n\n# Tìm file matrix\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\n# 3.2 Loop qua từng loại và fill\nfor st in ['clicks', 'carts', 'orders']:\n    print(f\"\\n--- Processing {st.upper()} ---\")\n    \n    # Lấy gợi ý từ Matrix (GPU)\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        # Fill thêm từ Matrix nếu chưa đủ 20\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# 3.3 Lưu file\nprint(\"\\n3. Saving submission...\")\nsubmission = pd.DataFrame(final_submission_data)\nsubmission.to_csv('submission_sasrec_hybrid.csv', index=False)\nprint(f\"✓ DONE! Rows: {len(submission)}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}