こんにちは、データ統括部データサイエンス(以下DS)グループ所属の小関 (@ozeshun)です。
本記事では、タイミーで取り組んでいるレコメンドに使用するアルゴリズムを検証する際に活用した、RecBoleでの実験方法について紹介したいと思います。
※Timee Advent Calendar2023の12月8日分の記事です。
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形式で保存
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 へ変換
# 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!
タイミーのデータ統括部では、ともに働くメンバーを募集しています!!
現在募集中のポジションはこちらです!
「話を聞きたい」と思われた方は、是非一度カジュアル面談でお話ししましょう!