Timee Product Team Blog

タイミー開発者ブログ

RecBoleでサクッとレコメンドアルゴリズムの検証をしてみた

こんにちは、データ統括部データサイエンス(以下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形式で保存

    • 下記のコードをノートブック上で実行すると、ユーザー×アイテムのインタラクション、ユーザーの特徴量、アイテムの特徴量を抽出したデータセット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が学習に用いるデータ形式