{"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":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# OTTO GNNRec with CUDF Covisitation Matrix\n\nPipeline sử dụng CUDF (RAPIDS GPU) để build covisitation matrices + GNNRec 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":"train_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')\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\nif inter_df['ts'].max() > 1e12:\n    inter_df = inter_df.with_columns((pl.col('ts') / 1000).cast(pl.Int32).alias('ts'))\nelse:\n    inter_df = inter_df.with_columns(pl.col('ts').cast(pl.Int32).alias('ts'))\n\ninter_df = inter_df.rename({'session': 'session:token', 'aid': 'aid:token', 'ts': 'ts:float'})\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 GNNRec 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\nimport numpy as np\nfrom logging import getLogger\nfrom recbole.config import Config\nfrom recbole.data import create_dataset, data_preparation\nfrom recbole.model.sequential_recommender import SRGNN\nfrom recbole.trainer import Trainer\nfrom recbole.utils import init_seed, init_logger\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 (Giữ nguyên)\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 (Giữ nguyên)\nnp.str = np.str_\nnp.unicode = np.str_\nnp.unicode_ = np.str_\nnp.long = np.int64\n\n# 3. Cấu hình recbole_config cho SR-GNN\nrecbole_config = {\n    'data_path': '.',\n    'USER_ID_FIELD': 'session',\n    'ITEM_ID_FIELD': 'aid',\n    'TIME_FIELD': 'ts',\n    'user_inter_num_interval': \"[2,Inf)\",  # SR-GNN cần tối thiểu 2 item để tạo đồ thị có cạnh\n    'item_inter_num_interval': \"[0,Inf)\",\n    'load_col': {'inter': ['session', 'aid', 'ts']},\n    'train_neg_sample_args': None,\n    'epochs': 5,                         # Tăng epoch để GNN đủ thời gian hội tụ\n    'stopping_step': 3,\n    'eval_batch_size': 512,\n    'train_batch_size': 512,\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 SR-GNN (GNN based) ======\n    'embedding_size': 64,    # Kích thước vector nhúng cho item\n    'step': 2,               # Số bước truyền tin (GNN layers)\n    'loss_type': 'CE',       # Cross Entropy loss\n}\n\nconfig = Config(model='SRGNN', dataset='recbox_data', config_dict=recbole_config)\ninit_seed(config['seed'], config['reproducibility'])\ninit_logger(config)\nlogger = getLogger()\n\n# Xóa các handler cũ nếu có để tránh lặp log\nfor handler in logger.handlers[:]:\n    logger.removeHandler(handler)\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 = SRGNN(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\nfrom recbole.data.interaction import Interaction\n\n# ==============================================================================\n# BƯỚC 1: LẤY DỰ ĐOÁN TỪ SR-GNN (ĐÃ SỬA LỖI AID_LIST)\n# ==============================================================================\n\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    test_df = test_df.sort_values(['session', 'ts'])\n    test_session_AIDs = test_df.groupby('session')['aid'].apply(list).to_dict()\n    del test_df\n    gc.collect()\n\ndef get_gnn_predictions(model, dataset, sessions_dict, topk=20, batch_size=128): # Giảm batch xuống 128 cho an toàn\n    model.eval()\n    device = model.device\n    all_preds = {}\n    \n    session_ids = list(sessions_dict.keys())\n    # Lấy đúng độ dài chuỗi từ config\n    max_item_len = config['MAX_ITEM_LIST_LENGTH'] if 'MAX_ITEM_LIST_LENGTH' in config else 20\n    \n    vocab = dataset.field2token_id[dataset.iid_field]\n    model_vocab_size = model.item_embedding.weight.shape[0]\n    \n    print(f\"   Running SR-GNN Inference on {len(session_ids)} sessions...\")\n    \n    for start_idx in tqdm(range(0, len(session_ids), batch_size)):\n        end_idx = min(start_idx + batch_size, len(session_ids))\n        batch_sessions = session_ids[start_idx:end_idx]\n        \n        batch_ids = []\n        lengths = []\n        for sess in batch_sessions:\n            seq = [str(aid) for aid in sessions_dict[sess][-max_item_len:]]\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        # Padding\n        max_len_batch = max(lengths)\n        padded = torch.zeros((len(batch_sessions), max_len_batch), dtype=torch.long, device=device)\n        for i, ids in enumerate(batch_ids):\n            l = len(ids)\n            padded[i, :l] = torch.tensor(ids, 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                # --- SỬA LỖI CHÍNH Ở ĐÂY ---\n                # Dùng model.ITEM_SEQ thay vì string cứng. \n                # RecBole sẽ tự điền 'aid_list' hoặc 'item_id_list' tùy config.\n                inter = Interaction({\n                    model.ITEM_SEQ: padded,\n                    model.ITEM_SEQ_LEN: len_tensor\n                })\n                \n                scores = model.full_sort_predict(inter)\n                scores[:, 0] = -1e9 # Mask padding\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            for i, row in enumerate(batch_tokens):\n                all_preds[batch_sessions[i]] = [int(x) for x in row if str(x).isdigit()]\n                \n        except Exception as e:\n            if start_idx == 0: print(f\"Detailed Error at first batch: {e}\")\n            for sess in batch_sessions: all_preds[sess] = []\n\n    return all_preds\n\n# Thực thi Inference\ngnn_preds_dict = get_gnn_predictions(model, dataset, test_session_AIDs, topk=20, batch_size=128)\n\n# Xóa model để giải phóng VRAM cho bước Matrix\nprint(\"   -> Clearing VRAM...\")\ntry: del model, dataset, train_data, valid_data, trainer\nexcept: pass\ngc.collect()\ntorch.cuda.empty_cache()\n\n# ==============================================================================\n# BƯỚC 2: TRUY VẤN MATRIX TỐI ƯU (CHUNKING ĐỂ TRÁNH OOM)\n# ==============================================================================\ndef get_covisit_candidates_chunked(session_last_items, matrix_path, top_n=20, chunk_size=50000):\n    print(f\"   Loading matrix: {matrix_path.split('/')[-1]}\")\n    matrix = cudf.read_parquet(matrix_path, columns=['aid_x', 'aid_y', 'wgt'])\n    \n    all_recs = {}\n    sids = list(session_last_items.keys())\n    aids = list(session_last_items.values())\n    \n    # Chia nhỏ session để merge, tránh tràn RAM\n    for i in range(0, len(sids), chunk_size):\n        batch_sids = sids[i:i+chunk_size]\n        batch_aids = aids[i:i+chunk_size]\n        \n        df_batch = cudf.DataFrame({'session': batch_sids, 'aid_x': batch_aids})\n        recs = df_batch.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 ngay lập tức để giải phóng GPU memory\n        batch_dict = recs[['session', 'aid_y']].to_pandas().groupby('session')['aid_y'].apply(list).to_dict()\n        all_recs.update(batch_dict)\n        \n        del df_batch, recs\n    \n    del matrix\n    gc.collect()\n    torch.cuda.empty_cache()\n    return all_recs\n\n# ==============================================================================\n# BƯỚC 3: HYBRID MERGE & SUBMISSION\n# ==============================================================================\nprint(\"\\n2. [Hybrid] Merging Results...\")\n\nlast_item_dict = {sess: aids[-1] for sess, aids in test_session_AIDs.items() if len(aids) > 0}\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\nfinal_submission_data = []\n\nfor st in ['clicks', 'carts', 'orders']:\n    print(f\"\\n--- Processing {st.upper()} ---\")\n    covisit_recs = get_covisit_candidates_chunked(last_item_dict, matrix_files[st], top_n=20)\n    \n    for sess_id in tqdm(list(test_session_AIDs.keys())):\n        history = test_session_AIDs[sess_id][::-1]\n        gnn_rec = gnn_preds_dict.get(sess_id, [])\n        matrix_rec = covisit_recs.get(sess_id, [])\n        \n        # Logic ưu tiên tối ưu\n        if st == 'clicks':\n            combined = history + gnn_rec + matrix_rec\n        else:\n            combined = history + matrix_rec + gnn_rec\n            \n        unique_recs = list(dict.fromkeys(combined))[:20]\n        \n        final_submission_data.append({\n            'session_type': f\"{sess_id}_{st}\",\n            'labels': \" \".join(map(str, unique_recs))\n        })\n        \n    del covisit_recs\n    gc.collect()\n\nprint(\"\\n3. Saving final submission...\")\nsubmission = pd.DataFrame(final_submission_data)\nsubmission.to_csv('submission_gnn_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}]}