0
1

More than 1 year has passed since last update.

[実装メモ] OpenBanditPipelineで、自分で用意したログを使う

Last updated at Posted at 2023-02-09

概要

OPEを簡単に実行できるPythonライブラリ、「OpenBanditPipeline」の使い方を1つ検討してたので、メモを残します。

上の記事などで紹介される実装では、基本的にOpenBanditDataset(zozoのオープンデータ)かSyntheticBanditDataset(シミュレーション作成したデータ)を使う方法で実装されています。

しかし、私がやりたいことは、「自分で用意した任意のログに対してバンディットのシミュレーションし、OPEを算出したい」という内容です。
そこで、OpenBanditDatasetもSyntheticBanditDatasetも使わずに、自分の定義したデータを既存ポリシーとして利用する方法を検討しました。

bandit_feedbackを定義する関数

OpenBanditDatasetやSyntheticBanditDatasetでデータを作成すると、「bandit_feedback」という辞書形式のデータが作成されます。このデータを任意に定義することができれば、そのデータセットを使ってその後のフローを実行できると思いました。

任意のログデータをbandit_feedbackに変換するための関数を以下のように定義しました。

from typing import List
import pandas as pd
import numpy as np
from scipy.stats import rankdata
from sklearn.preprocessing import LabelEncoder

def bandit_feedback_definder(
    df:pd.DataFrame,
    action_col:str,
    action_prob_col:str,
    position_col:str,
    reward_col:str,
    context_cols:List[str]=None,
    action_context_cols:List[str]=None,
    n_rounds:int=None,
    n_actions:int=None,
) -> dict:
    if n_rounds == None:
        n_rounds = df.shape[0]
    if n_actions == None:
        n_actions = df[action_col].unique().shape[0]
    if context_cols == None:
        context = np.zeros((df.shape[0], 1))
    if action_context_cols == None:
        action_context = None

    bandit_feedback = {
        "n_rounds":n_rounds,
        "n_actions":n_actions,
        "action":df[[action_col]].apply(LabelEncoder().fit_transform).values.reshape(-1),
        "position":(rankdata(df[position_col].astype(int).values, "dense") - 1).astype(int),
        "pscore":df[action_prob_col].values,
        "reward":df[reward_col].values,
        "context":context,
        "action_context":action_context,

    }
    return bandit_feedback

bandit_feedbackの各キーは以下です。

  • "n_rounds":レコード数
  • "n_actions":アイテム数
  • "action":各レコードにおけるレコメンドされたアイテムを格納した配列
  • "position":各レコードの表示順。0始まりで登録する。
  • "pscore":各レコードでレコメンドされたアイテムの、レコメンド確率を格納した配列
  • "reward":各レコードでレコメンドされたアイテムに対する報酬(クリックされた否かなど)を格納した配列
  • "context":ユーザ属性を格納した配列
  • "action_context":アイテム特徴を格納した配列

引数dfにログデータのデータフレームを指定します。それ以外の引数は、そのデータフレームにおける対象のカラム名を指定します。

contextなしの定義

dfの形式

action position action_prob rewards
contents_A 1 0.35 1
contents_B 2 0.21 0
contents_D 3 0.11 0
contents_A 1 0.41 1
... ... ... ...
bandit_feedback = bandit_feedback_definder(
    df, 
    action_col="action", 
    action_prob_col="action_prob", 
    position_col="position", 
    reward_col="rewards"
)

contextありの定義

dfの形式

action position action_prob rewards user_context1 user_context2 ... action_context1 ...
contents_A 1 0.35 1 0.98 1.34 ... 2.00 ...
contents_B 2 0.21 0 0.32 1.79 ... 1.67 ...
contents_D 3 0.11 0 0.77 2.66 ... 0.10 ...
contents_A 1 0.41 1 0.67 0.01 ... 0.05 ...
... ... ... ... ... ... ... ... ...
bandit_feedback = bandit_feedback_definder(
    df, 
    action_col="action", 
    action_prob_col="action_prob", 
    position_col="position", 
    reward_col="rewards",
    context_cols=["user_context1", "user_context2", ...],
    action_context_cols=["action_context1", "action_context2", ...]
)

action_distの定義

action_distは、新しく適用する方のポリシーの情報です。具体的には、各アイテムがそのラウンド(レコード)においてレコメンドされる確率一覧です。

シミュレーションで求める場合

このライブラリには、ログデータに対して、仮にバンディットアルゴリズムを適用した場合をシミュレーションで求める機能があります。

以下、ε-greedyおよびThompson Samplingをシミュレーションするための設定を関数化しました。

context-freeなBanditアルゴリズム向けのシミュレーション関数を定義

from obp.policy import EpsilonGreedy
from obp.policy import BernoulliTS

def context_free_simulation(
    bandit_feedback:dict,
    how:str="e", 
    epsilon:float=0.2,
    alpha:int=1,
    beta:int=1,
    seed:int=1234
):

    if how == "e":
        evaluation_policy = EpsilonGreedy(
            n_actions=bandit_feedback["n_actions"],
            len_list = int(max(bandit_feedback["position"]) + 1), 
            epsilon = 0.2,
            random_state=seed
        )
    elif how == "t":
        evaluation_policy = BernoulliTS(
            n_actions=bandit_feedback["n_actions"],
            len_list = int(max(bandit_feedback["position"]) + 1), 
            alpha=alpha,
            beta=beta,
            random_state=seed
        )
    else:
        raise ValueError("'how' must be 'e' as epsilon-greedy or 't' as Thompson-Sampling.")
    
    return evaluation_policy

evaluation_policyで定義されている各値は以下です。

  • len_list:1レコメンドにおける表示数。
  • epsilon:ε-greedyにおける探索率
  • alpha:Thompson Samplingにおける初期値
  • beta:Thompson Samplingにおける初期値

ε-greedyをシミュレーションする場合

rom obp.simulator import run_bandit_simulation

action_dist = run_bandit_simulation(
    bandit_feedback=bandit_feedback, 
    policy=evaluation_policy
)

action_distには、(レコード数(ラウンド数), アイテム数, 表示枠数)というshapeのnp.arrayが格納されます。
以下は、アイテム数5、表示枠数3の場合の例です。

action_dist = np.array([ [ [0, 0, 1],  
                [1, 0, 0],
                [0, 1, 0],
                [0, 0, 0],
                [0, 0, 0], ] 」第0レコード
              [ [0, 0, 0], 
                [0, 0, 0],
                [1, 0, 0],
                [0, 1, 0],
                [0, 0, 1],  ] 」第1レコード
              ...
          ] )

第0レコードに対するシミュレーション結果は、「表示枠2にアイテム2、表示枠0にアイテム0、表示枠1にアイテム1」をレコメンドするとなっています。

Off-Policy Estimatesの算出

IPS

どのデータを正解にするか(bandit_feedback)と、何で評価するか(ope_estimators)を指定します。

from obp.ope import OffPolicyEvaluation, InverseProbabilityWeighting as IPW


ope = OffPolicyEvaluation(
    bandit_feedback=bandit_feedback, 
    ope_estimators=[IPW()]
)

評価値算出

  • π(a|X) = action_dist
  • π_b(a|X) = bandit_feedback["action_prob"](ライブラリ内部でこれを計算)
    という定義で計算されます。
estimated_policy_value = ope.estimate_policy_values(action_dist=action_dist)
estimated_policy_value

 -> {'ipw': 0.11583844057885634}

IPS & DR

DRのために、未観測データに対するrewardを予測するモデルを作成し、その予測結果(estimated_rewards)を求めます。
以下は、モデルにロジスティック回帰を利用する場合で実装しています。

from obp.ope import RegressionModel

regression_model = RegressionModel(
    n_actions = bandit_feedback["n_actions"],
    len_list = int(max(bandit_feedback["position"]) + 1), 
    base_model = LogisticRegression(C=100, random_state=12345)
)

estimated_rewards = regression_model.fit_predict(
    context = bandit_feedback["context"],
    action = bandit_feedback["action"],
    reward = bandit_feedback["reward"],
    position = bandit_feedback["positison"],
    random_state = 12345
)

その後同様に、どのデータを正解にするか(bandit_feedback)と、何で評価するか(ope_estimators)を指定します。

from obp.ope import OffPolicyEvaluation
from obp.ope import InverseProbabilityWeighting as IPW
from obp.ope import DoublyRobust as DR

ope = OffPolicyEvaluation(
    bandit_feedback=bandit_feedback, 
    ope_estimators=[IPW(), DR()]
)

評価値算出

  • π(a|X) = action_dist
  • π_b(a|X) = bandit_feedback["action_prob"](ライブラリ内部でこれを計算)
  • f(X, π(a|X)) = estimated_rewards
    という定義で計算されます。
estimated_policy_value = ope.estimate_policy_values(
    action_dist = action_dist,
    estimated_rewards_by_reg_model = estimated_rewards
)
estimated_policy_value

 -> {'ipw': 0.11583844057885634, 'DR':0.134872019445}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1