概要
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}