{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":38760,"databundleVersionId":4493939,"sourceType":"competition"},{"sourceId":4461402,"sourceType":"datasetVersion","datasetId":2611514},{"sourceId":4474043,"sourceType":"datasetVersion","datasetId":2601572},{"sourceId":14221854,"sourceType":"datasetVersion","datasetId":9072213},{"sourceId":14222068,"sourceType":"datasetVersion","datasetId":9072372},{"sourceId":14450720,"sourceType":"datasetVersion","datasetId":9230194}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install recbole","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:29:26.177568Z","iopub.execute_input":"2026-01-10T05:29:26.178706Z","iopub.status.idle":"2026-01-10T05:29:32.280400Z","shell.execute_reply.started":"2026-01-10T05:29:26.178663Z","shell.execute_reply":"2026-01-10T05:29:32.279030Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Đường dẫn của mô hình đã huấn luyện trong input\n!cp /kaggle/input/sasrec-new/SASRec.pth /kaggle/working/","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:29:32.282167Z","iopub.execute_input":"2026-01-10T05:29:32.282543Z","iopub.status.idle":"2026-01-10T05:29:36.503683Z","shell.execute_reply.started":"2026-01-10T05:29:32.282487Z","shell.execute_reply":"2026-01-10T05:29:36.502044Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tqdm\nimport polars as pl\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nimport random\nimport os \nimport h5py\nimport sys\nimport gc\n\nfrom matplotlib import pyplot as plt\nimport pyarrow.parquet as pq","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:29:36.506031Z","iopub.execute_input":"2026-01-10T05:29:36.506443Z","iopub.status.idle":"2026-01-10T05:29:36.514377Z","shell.execute_reply.started":"2026-01-10T05:29:36.506402Z","shell.execute_reply":"2026-01-10T05:29:36.513310Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = pl.read_parquet('/kaggle/input/otto-train-and-test-data-for-local-validation/test.parquet')\ntest = pl.read_parquet('/kaggle/input/otto-full-optimized-memory-footprint/test.parquet')\n\ndf = pl.concat([train, test])\n\ndf = df.sort(['session', 'ts'])\ndf = df.with_columns((pl.col('ts') * 1e9).alias('ts'))\ndf = df.rename({'session': 'session:token', 'aid': 'aid:token', 'ts': 'ts:float'})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:29:38.632921Z","iopub.execute_input":"2026-01-10T05:29:38.633339Z","iopub.status.idle":"2026-01-10T05:29:40.717814Z","shell.execute_reply.started":"2026-01-10T05:29:38.633305Z","shell.execute_reply":"2026-01-10T05:29:40.716743Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!mkdir -p /kaggle/working/recbox_data\ndf[['session:token', 'aid:token', 'ts:float']].write_csv('/kaggle/working/recbox_data/recbox_data.inter', separator='\\t')\n\ndel df, train, test\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:29:41.807452Z","iopub.execute_input":"2026-01-10T05:29:41.807842Z","iopub.status.idle":"2026-01-10T05:29:49.994784Z","shell.execute_reply.started":"2026-01-10T05:29:41.807811Z","shell.execute_reply":"2026-01-10T05:29:49.993428Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import logging\nfrom logging import getLogger\nimport typing\nfrom typing_extensions import Literal\ntyping.Literal = Literal\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,"execution":{"iopub.status.busy":"2026-01-10T05:29:49.996708Z","iopub.execute_input":"2026-01-10T05:29:49.997186Z","iopub.status.idle":"2026-01-10T05:29:50.005220Z","shell.execute_reply.started":"2026-01-10T05:29:49.997152Z","shell.execute_reply":"2026-01-10T05:29:50.004219Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"MAX_ITEM = 30  \n\nparameter_dict = {\n    # === Data ===\n    'data_path': '/kaggle/working/',\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\n    # === Tham số huấn luyện ===\n    'epochs': 20,                    \n    'stopping_step': 5,\n    'train_batch_size': 512,         \n    'eval_batch_size': 1024,\n    'train_neg_sample_args': None,\n    'learning_rate': 5e-4,\n\n    # === Sequence handling ===\n    'MAX_ITEM_LIST_LENGTH': MAX_ITEM,\n    'MAX_SEQ_LENGTH': MAX_ITEM,       \n    'hidden_size': 128,               # embedding dimension\n    'num_heads': 4,                   # số head trong self-attention\n    'num_layers': 2,                  # số layer Transformer\n    'hidden_dropout_prob': 0.2,       # dropout cho feed-forward\n    'attn_dropout_prob': 0.2,         # dropout cho attention\n\n    # === Evaluation Metrics ===\n    'metrics': ['Recall', 'MRR', 'NDCG', 'Hit', 'Precision'],\n    'topk': [20],                \n    'valid_metric': 'Recall@20', \n\n    # === Evaluation ===\n    'eval_args': {\n        'split': {'LS': 'valid_and_test'},\n        'group_by': 'user',\n        'order': 'TO',\n        'mode': 'full'\n    },\n\n    # === Hàm mất mát ===\n    'loss_type': 'CE',              \n\n    # === Lưu mô hình ===\n    'checkpoint_dir': '/kaggle/working/',   \n    'save_best': True,                       \n}\n\n# === Khởi tạo config ===\nconfig = Config(model='SASRec', dataset='recbox_data', config_dict=parameter_dict)\n\n# === Khởi tạo random seed và logger ===\ninit_seed(config['seed'], config['reproducibility'])\ninit_logger(config)\nlogger = getLogger()\n\n# === Tạo handler để log ra màn hình ===\nc_handler = logging.StreamHandler()\nc_handler.setLevel(logging.INFO)\nlogger.addHandler(c_handler)\n\n# === In config để kiểm tra ===\nlogger.info(config)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:30:47.205855Z","iopub.execute_input":"2026-01-10T05:30:47.206614Z","iopub.status.idle":"2026-01-10T05:30:47.612389Z","shell.execute_reply.started":"2026-01-10T05:30:47.206585Z","shell.execute_reply":"2026-01-10T05:30:47.611138Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = SASRec(config, train_data.dataset).to(config['device'])\nlogger.info(model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:38:15.734381Z","iopub.execute_input":"2026-01-10T05:38:15.734962Z","iopub.status.idle":"2026-01-10T05:38:16.811956Z","shell.execute_reply.started":"2026-01-10T05:38:15.734930Z","shell.execute_reply":"2026-01-10T05:38:16.810964Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\n\n#Đường dẫn của mô hình đã huấn luyện trong input\ncheckpoint_path = '/kaggle/input/sasrec-new/SASRec.pth'\n\n#Tải lại trọng số đã huấn luyện\nckpt = torch.load(checkpoint_path, map_location=config['device'], weights_only=False)\nmodel.load_state_dict(ckpt['state_dict'])\nmodel.eval()\n\nlogger.info(\"Model weights loaded successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:38:16.814414Z","iopub.execute_input":"2026-01-10T05:38:16.814769Z","iopub.status.idle":"2026-01-10T05:38:17.961302Z","shell.execute_reply.started":"2026-01-10T05:38:16.814748Z","shell.execute_reply":"2026-01-10T05:38:17.960480Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==============================================================================\n# ĐỊNH NGHĨA HÀM INFERENCE (TRẢ VỀ CẢ SCORE)\n# ==============================================================================\ndef pred_user_to_item_batch_with_score(item_sequences: List[List[str]], topk: int = 20, batch_size: int = 2048):\n    \"\"\"\n    Hàm dự đoán trả về cả Item ID và Score (Logit) để dùng cho Rerank\n    \"\"\"\n    model.eval()\n    vocab = dataset.field2token_id[dataset.iid_field]\n    \n    all_items = []\n    all_scores = []\n    \n    # Tự động lấy độ dài tối đa của model\n    try:\n        max_model_len = model.position_embedding.weight.shape[0]\n    except AttributeError:\n        max_model_len = 50 \n    \n    # Loop qua từng batch\n    for start_idx in tqdm(range(0, len(item_sequences), batch_size), desc=\"Predicting Batches\"):\n        end_idx = start_idx + batch_size\n        batch_seqs = item_sequences[start_idx : end_idx]\n        \n        # Xử lý Sequence (Cắt ngắn & Padding)\n        cleaned_sequences = []\n        for seq in batch_seqs:\n            # Lọc item có trong vocab\n            valid_items = [x for x in seq if x in vocab]\n            # Cắt nếu quá dài (giữ phần đuôi)\n            if len(valid_items) > max_model_len:\n                valid_items = valid_items[-max_model_len:]\n            cleaned_sequences.append(valid_items)\n\n        lengths = [len(seq) for seq in cleaned_sequences]\n        max_len = max(lengths) if lengths else 0\n        \n        if max_len == 0:\n            all_items.extend([[] for _ in batch_seqs])\n            all_scores.extend([[] for _ in batch_seqs])\n            continue\n\n        token_ids = []\n        for seq in cleaned_sequences:\n            if len(seq) == 0:\n                token_ids.append([0] * max_len)\n            else:\n                tokens = dataset.token2id(dataset.iid_field, seq)\n                if isinstance(tokens, np.ndarray): tokens = tokens.tolist()\n                token_ids.append(tokens + [0]*(max_len - len(seq)))\n\n        token_tensor = torch.tensor(token_ids, dtype=torch.long, device=model.device)\n        safe_lengths = [l if l > 0 else 1 for l in lengths]\n        length_tensor = torch.tensor(safe_lengths, dtype=torch.long, device=model.device)\n\n        seq_field = f\"{dataset.iid_field}_list\"\n        input_interaction = Interaction({\n            dataset.iid_field: token_tensor, \n            seq_field: token_tensor,        \n            'item_length': length_tensor\n        })\n\n        # Dự đoán và lấy điểm\n        with torch.no_grad():\n            scores = model.full_sort_predict(input_interaction)\n            scores[:, 0] = -np.inf # Mask padding token\n            \n            # Lấy cả Score và Item Index\n            batch_scores, topk_iids = torch.topk(scores, topk)\n        \n        # Chuyển về CPU list\n        topk_iids_list = topk_iids.cpu().tolist()\n        batch_scores_list = batch_scores.cpu().tolist()\n        \n        # Dọn dẹp GPU\n        del token_tensor, length_tensor, input_interaction, scores, topk_iids, batch_scores\n        \n        # Decode (Index -> Item ID)\n        batch_results_items = []\n        batch_results_scores = []\n        \n        for idx, (l, original_len) in enumerate(zip(topk_iids_list, lengths)):\n            if original_len == 0:\n                batch_results_items.append([])\n                batch_results_scores.append([])\n                continue\n            try:\n                decoded = dataset.id2token(dataset.iid_field, l)\n                # Item\n                if isinstance(decoded, np.ndarray): batch_results_items.append(decoded.tolist())\n                else: batch_results_items.append(list(decoded))\n                # Score\n                batch_results_scores.append(batch_scores_list[idx])\n            except:\n                batch_results_items.append([])\n                batch_results_scores.append([])\n        \n        all_items.extend(batch_results_items)\n        all_scores.extend(batch_results_scores)\n\n    return all_items, all_scores\n\n# ==============================================================================\n# CHUẨN BỊ DỮ LIỆU & CHẠY INFERENCE\n# ==============================================================================\nprint(\"Loading Test Data...\")\ntest = pl.read_parquet('/kaggle/input/otto-full-optimized-memory-footprint/test.parquet')\n\n# Gom nhóm session thành list\nprint(\"Grouping sessions...\")\nsession_df = (\n    test.group_by(\"session\")\n        .agg([pl.col(\"aid\").alias(\"aid\")])\n        .sort(\"session\")\n)\ndel test; gc.collect()\n\n# Chuyển sang List Python\nsession_ids = session_df[\"session\"].to_list()\nsession_aids = session_df[\"aid\"].to_list()\ndel session_df; gc.collect()\n\n# Chuyển item sang string \nprint(\"Converting to string sequences...\")\nall_sessions_str = [[str(x) for x in aids] for aids in session_aids]\n\n# Chạy mô hình\nprint(f\"Running Inference for {len(all_sessions_str)} sessions...\")\npred_items, pred_scores = pred_user_to_item_batch_with_score(\n    all_sessions_str, \n    topk=100,             \n    batch_size=2048      \n)\n\n# Dọn dẹp input\ndel all_sessions_str\ngc.collect()\n\n# ==============================================================================\n# LƯU KẾT QUẢ (POLARS)\n# ==============================================================================\nprint(\"Creating Output DataFrame...\")\ndf_sasrec = pl.DataFrame({\n    \"session\": session_ids,\n    \"aid\": pred_items,\n    \"sasrec_score\": pred_scores\n})\n\n# Trải phẳng list thành các dòng\nprint(\"Exploding list...\")\ndf_sasrec = df_sasrec.explode([\"aid\", \"sasrec_score\"])\n\n# Loại bỏ dòng null (nếu có session rỗng)\ndf_sasrec = df_sasrec.drop_nulls()\n\n# Ép kiểu và Tạo Rank\nprint(\"Formatting & Ranking...\")\ndf_sasrec = df_sasrec.with_columns([\n    pl.col(\"session\").cast(pl.Int32),\n    pl.col(\"aid\").cast(pl.Int32),\n    pl.col(\"sasrec_score\").cast(pl.Float32),\n    \n    # Tạo cột Rank (1 -> 100)\n    pl.col(\"sasrec_score\")\n      .rank(method=\"ordinal\", descending=True)\n      .over(\"session\")\n      .alias(\"sasrec_rank\")\n      .cast(pl.Int32)\n])\n\n# Lưu file\noutput_path = '/kaggle/working/sasrec_predictions.parquet'\nprint(f\"Saving to {output_path}...\")\ndf_sasrec.write_parquet(output_path)\n\nprint(f\"DONE! File saved with shape: {df_sasrec.shape}\")\nprint(df_sasrec.head())\n\ndel df_sasrec, pred_items, pred_scores, session_ids\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-11T01:48:15.201606Z","iopub.execute_input":"2026-01-11T01:48:15.202637Z","iopub.status.idle":"2026-01-11T01:48:15.349255Z","shell.execute_reply.started":"2026-01-11T01:48:15.202606Z","shell.execute_reply":"2026-01-11T01:48:15.347723Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null}]}