こんにちは、データ統括部データサイエンス(以下DS)グループ所属の小関 ( @ozeshun )です。 本記事では、タイミーで取り組んでいるレコメンドに使用する アルゴリズム を検証する際に活用した、RecBoleでの実験方法について紹介したいと思います。 ※ Timee Advent Calendar2023 の12月8日分の記事です。 RecBoleとは RecBoleを活用したアルゴリズムの実験手順 0. ディレクトリ構成 1. 学習データの準備とRecBoleで使用するconfig fileの用意 2. 学習データをAtomic file *3 へ変換 3. モデルの学習 4. 学習したモデルの検証 おわりに We’re Hiring! RecBoleとは RecBole とは、レコメンド アルゴリズム を統一されたインターフェースで提供する事を目的としたプロジェクトであり、後述のように アルゴリズム 間の比較を簡単に実現出来ます。2023/12/8現在、91種類の アルゴリズム が実装されており、 Python のライブラリ *1 として公開されています。 実装されている アルゴリズム は、 Model Introduction から確認できます。 今回は、実装されている アルゴリズム の中でもexplicitなフィードバックを予測すること *2 を目的とした、 Context-aware Recommendation の アルゴリズム をRecBoleを使用して検証する一連の流れを紹介したいと思います。 RecBoleを活用した アルゴリズム の実験手順 0. ディレクト リ構成 今回は、以下のような ディレクト リ構成の元、ノートブック上で実験を進める手順を説明します。 ├── notebook.ipynb ├── artifact │ ├── saved # 学習したモデルの保存先 │ │ ├── AFM-%m-%d-%Y_%H-%M-%S.pth │ │ ├── DeepFM-%m-%d-%Y_%H-%M-%S.pth │ │ ├── FM-%m-%d-%Y_%H-%M-%S.pth │ │ ├── NFM-%m-%d-%Y_%H-%M-%S.pth │ └── train_data # 学習用のデータの保存先. pickle fileは、Atomic fileに変換する際のソース. │ ├── interact.pkl │ ├── items.pkl │ ├── users.pkl │ ├── train_dataset # RecBoleが学習で使用する、Atomic fileの保存先 │ ├── train_dataset.inter │ ├── train_dataset.item │ └── train_dataset.user ├── base_dataset.py # データセットのI/Oをコントロールするクラスのベース (詳細はStep.2に記述) ├── config ├── model.hyper # 探索したいハイパーパラメータと探索範囲を記述したファイル └── train.yaml # RecBoleで使用する、学習方法などを記述したconfig file 1. 学習データの準備とRecBoleで使用するconfig fileの用意 学習用データの準備 今回は、Context-awareなモデルを学習することを目的としているので、下記のように、user_id、item_id、explicitなフィードバックを表すカラム、ユーザー・アイテムの特徴量を含む pandas.DataFrame 形式のオリジナルデータを用意します。 user_id item_id target user_feature item_feature 1 1 0 0 -1.0 2000 2 1 2 1 0.5 3000 3 2 1 1 -0.8 4000 4 2 2 0 0.0 5000 RecBole用のconfig fileの用意 このファイルには、データの保存先などの環境の設定、使用するデータに関する情報、学習方法や評価方法の設定を記述します。 # config/train.yaml # 使用するモデル名とデータセット名を指定----------------------------------- model : FM dataset : train_dataset # Environment Settings----------------------------------------------- # https://recbole.io/docs/user_guide/config/environment_settings.html gpu_id : 0 use_gpu : False seed : 2023 state : INFO reproducibility : True data_path : 'artifact/train_data/' checkpoint_dir : 'artifact/saved/' show_progress : True save_dataset : True save_dataloaders : False # Data Settings------------------------------------------------------ # https://recbole.io/docs/user_guide/config/data_settings.html # Atomic File Format field_separator : " \t " seq_separator : "@" # Common Features USER_ID_FIELD : user_id ITEM_ID_FIELD : item_id LABEL_FIELD : target # Selectively Loading load_col : # interaction inter : [ user_id, item_id, target ] # ユーザー特徴量 user : [ user_id, user_feature, ] # アイテム特徴量 item : [ item_id, item_feature, ] # Preprocessing # 標準化する特徴量を指定 normalize_field : [ item_feature, ] # Training Setting--------------------------------------------------- # https://recbole.io/docs/user_guide/config/training_settings.html epochs : 100 train_batch_size : 1024 learner : 'adam' train_neg_sample_args : ~ eval_step : 1 stopping_step : 3 loss_decimal_place : 4 weight_decay : 0 # Evaluation Settings------------------------------------------------ # https://recbole.io/docs/user_guide/config/evaluation_settings.html eval_args : group_by : user split : { 'RS' : [ 0.8 , 0.1 , 0.1 ]} mode : labeled repeatable : True metrics : [ 'LogLoss' , 'AUC' ] topk : 20 valid_metric : LogLoss eval_batch_size : 1024 metric_decimal_place : 4 eval_neg_sample_args : ~ 次のステップでAtomic fileに変換する際のソースとなるように、用意したDataFrameをpkl形式で保存 下記のコードをノートブック上で実行すると、ユーザー×アイテムのインタ ラク ション、ユーザーの特徴量、アイテムの特徴量を抽出したデー タセット が ARTIFACT_PATH 配下に保存されます。 import os import yaml import pandas as pd # データの読み込み (データソースはなんでも良い) train_df = pd.read_csv( '/path/to/train.csv' ) # yaml形式で書かれたRecBoleのcofig fileを読み込む TRAIN_YAML_PATH = 'config/train.yaml' with open (TRAIN_YAML_PATH, 'r' ) as yaml_file: train_config = yaml.safe_load(yaml_file) # cofig fileから各データセットに使用するカラム名を抽出 INTERACTION_COLUMNS = train_config[ 'load_col' ][ 'inter' ] USER_COLUMNS = train_config[ 'load_col' ][ 'user' ] ITEM_COLUMNS = train_config[ 'load_col' ][ 'item' ] TOKEN_COLUMNS = [ 'item_id' , 'user_id' , ] # Atomic fileに変換する際のソースデータとしてpkl形式で保存 ARTIFACT_PATH = 'artifact/train_data/' train_df[INTERACTION_COLUMNS].to_pickle(os.path.join(ARTIFACT_PATH, 'interact.pkl' )) train_df[ITEM_COLUMNS].to_pickle(os.path.join(ARTIFACT_PATH, 'items.pkl' )) train_df[USER_COLUMNS].to_pickle(os.path.join(ARTIFACT_PATH, 'users.pkl' )) 2. 学習データをAtomic file *3 へ変換 変換時に使用する、デー タセット のI/Oをコン トロール するクラスをノートブック上に記述 このクラスには、インプット・アウトプット先や、使用するカラムの情報とそのデータ型を記述しておきます。 # https://github.com/RUCAIBox/RecSysDatasets/blob/master/conversion_tools/src/base_dataset.py をそのままimportして継承 from base_dataset import BaseDataset class TrainDataset (BaseDataset): def __init__ (self, input_path, output_path): super (TrainDataset, self).__init__(input_path, output_path) self.dataset_name = 'train_dataset' # input_path self.inter_file = os.path.join(self.input_path, 'interact.pkl' ) self.item_file = os.path.join(self.input_path, 'items.pkl' ) self.user_file = os.path.join(self.input_path, 'users.pkl' ) self.sep = ',' # output_path output_files = self.get_output_files() self.output_inter_file = output_files[ 0 ] self.output_item_file = output_files[ 1 ] self.output_user_file = output_files[ 2 ] # selected feature fields inter_fields = { i: f '{c}:token' if c in TOKEN_COLUMNS else f '{c}:float' for i, c in enumerate (INTERACTION_COLUMNS) } item_fields = {i: f '{c}:token' if c in TOKEN_COLUMNS else f '{c}:float' for i, c in enumerate (OFFER_COLUMNS)} user_fields = {i: f '{c}:token' if c in TOKEN_COLUMNS else f '{c}:float' for i, c in enumerate (USER_COLUMNS)} self.inter_fields = inter_fields self.item_fields = item_fields self.user_fields = user_fields def load_inter_data (self): return pd.read_pickle(self.inter_file) def load_item_data (self): return pd.read_pickle(self.item_file) def load_user_data (self): return pd.read_pickle(self.user_file) 下記コードをノートブック上で実行して、 ARTIFACT_PATH 配下に格納したデー タセット をAtomic file形式に変換 ARTIFACT_PATH = 'artifact/train_data/' # 前のステップで保存したデータセットの保存先 input_path = ARTIFACT_PATH # Atomic fileの書き出し先 output_path = os.path.join(ARTIFACT_PATH, 'train_dataset' ) i_o_args = [input_path, output_path] # 前のステップで作成したI/Oクラスにinput,outputの情報を渡す datasets = TrainDataset(*i_o_args) # DatasetをAtomic fileに変換 datasets.convert_inter() datasets.convert_item() datasets.convert_user() 3. モデルの学習 モデルを学習する関数をノートブック上に定義 パラメータチューニングの各種設定については、コメントで記述したページに詳しく書いてあります。 from recbole.quick_start import objective_function, run_recbole from recbole.trainer import HyperTuning # (再掲) 事前に準備したRecBoleのcofig fileへのパス TRAIN_YAML_PATH = 'config/train.yaml' # 探索したいハイパーパラメータと探索範囲を記述したファイルへのパス HYPER_PARAMS_PATH = 'config/model.hyper' def train_model (model_name: str , config_file_list: str = TRAIN_YAML_PATH, params_file: str = HYPER_PARAMS_PATH) -> None : # ハイパーパラメータチューニングの条件を設定 # 参考: https://recbole.io/docs/user_guide/usage/parameter_tuning.html hp = HyperTuning( objective_function=objective_function, algo= 'bayes' , early_stop= 3 , max_evals= 15 , params_file=params_file, fixed_config_file_list=config_file_list, ) # チューニングを実行 hp.run() # print best parameters print ( 'best params: ' , hp.best_params) # print best result print ( 'best result: ' ) print (hp.params2result[hp.params2str(hp.best_params)]) # bestなパラメータを取得 parameter_dict = { 'train_neg_sample_args' : None , } | hp.best_params # bestなパラメータでモデルを学習 run_recbole( model=model_name, dataset= 'train_dataset' , config_file_list=config_file_list, config_dict=parameter_dict, ) 探索したいハイパーパラメータとその探索範囲を model.hyper というファイルに記述して用意 # config/model.hyper (使用アルゴリズムがFMの場合) learning_rate choice [0.1, 0.05, 0.01] embedding_size choice [10, 16, 32] 定義した学習用の関数に試したい アルゴリズム 名を指定して、学習を実行すると train.yaml に記述した checkpoint_dir 配下に学習済のモデルが保存されます。 # FM train_model('FM') # NFM train_model('NFM') # AFM train_model('AFM') # DeepFM train_model('DeepFM') 4. 学習したモデルの検証 学習したモデルでテストデータに対する予測値を計算し、その評価指標を算出する関数をノートブック上に定義 import torch from recbole.data.interaction import Interaction from recbole.quick_start import load_data_and_model def eval_model ( model_file_name: str , model_saved_dir: str = train_config[ 'checkpoint_dir' ], target: str = train_config[ 'LABEL_FIELD' ], user_columns: list = USER_COLUMNS, item_columns: list = ITEM_COLUMNS, token_columns: list = TOKEN_COLUMNS, ): # 学習したモデルとテストデータを読み込み _, model, _, _, _, test_data = load_data_and_model(model_file=os.path.join(model_saved_dir, model_file_name)) columns = user_columns + item_columns + [target] # テストデータをモデルが予測出来る形式に変換 interactions = {} test_df = pd.DataFrame([]) for c in columns: test_features = torch.tensor([]) for data in test_data: test_features = torch.cat([test_features, data[ 0 ][c]]) if c in token_columns: test_features = test_features.to(torch.int) interactions[c] = test_features if c in [ 'user_id' ] + [target]: test_df[c] = test_features test_interaction_input = Interaction(interactions) # テストデータに対する予測結果を作成 model.eval() with torch.no_grad(): test_result = model.predict(test_interaction_input.to(model.device)) test_df[ 'pred' ] = test_result # テストデータに対するランキングメトリクス、AUC, Loglossを算出する関数を実行 (今回は実装は割愛) # 現状のRecBoleの仕様だとmode: labeledで学習した場合、ランキングメトリクスを指定できないので、自前で計算する必要があります return test_df 定義した検証用の関数に学習済のモデルのファイル名を入れて実行することで、テストデータに対する予測結果とメトリクスが計算されます。 # FM test_df = eval_model( 'FM-%m-%d-%Y_%H-%M-%S.pth' ) # NFM test_df = eval_model( 'NFM-%m-%d-%Y_%H-%M-%S.pth' ) # AFM test_df = eval_model( 'AFM-%m-%d-%Y_%H-%M-%S.pth' ) # DeepFM test_df = eval_model( 'DeepFM-%m-%d-%Y_%H-%M-%S.pth' ) 作成された test_df にはランキングメトリクスが計算出来るようにuser_id、真値、予測値が書き込まれています。 user_id target pred 1 1 1 0.9 2 1 1 0.7 3 1 0 0.3 4 1 0 0.1 test_df を元にテストデータに対するメトリクスを計算することで、 アルゴリズム 間の比較検証が出来ます。 ROC -AUC Logloss Recall@20 MAP@20 MRR@20 FM 0.865 0.105 0.491 0.489 0.577 NFM 0.843 0.110 0.502 0.470 0.540 AFM 0.865 0.103 0.507 0.487 0.568 DeepFM 0.862 0.101 0.523 0.522 0.598 おわりに 今回は、多様なレコメンド アルゴリズム を検証できるRecBoleを活用した実験の手順について紹介しました。 この記事が、レコメンド アルゴリズム 構築に関わる方々の助けに少しでもなれたら嬉しいです! We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう! *1 : PyPI : https://pypi.org/project/recbole/ *2 : 例: CTR予測など *3 : RecBoleが学習に用いる データ形式