以前も記事にしたが、ウェブ上のユーザーの行動ログを使って推薦システムを開発している自分のようなMLエンジニアにとって、ランキング学習におけるポジションバイアスの除去は重要なテーマである。サービスのログは通常様々なバイアスに塗れており、特にリストの上位に表示されたアイテムほどクリックが集まりやすくなってしまうポジションバイアスは非常に厄介だ。アカデミアではこの手のテーマはだいぶ研究が進んでいるものの、これまでは論文や書籍で手法が紹介されるだけで、手軽にパッと使えるライブラリは存在しなかった。
しかしどうやら最近になって XGBoost や LightGBM という多くの人が使う強力なGBDTライブラリにポジションバイアスを除去する機能が実装されたらしく、これが使い物になるのであれば実務で利用するハードルがグッと下がると思い、実験して性能を検証してみた。
ここではランキング学習の評価では定番である MSLR-WEB30K を加工してポジションバイアスのあるクリックデータセットを作成した。具体的なポジションの付与方法や、クリックラベルの生成方法は以前書いた記事と同様なのでそちらを参照してほしい。
このデータセットでは、ポジションと(バイアスのない真の)relevance との交絡度合いをパラメータ $w$ によって調整している。$w=0$ であれば、ポジションの値は relevance とは関係なく決まり、$w=1$ であれば relevance によって完全に決まる(最も relevance の高いアイテムを一番上に配置する)ようになっている。そしてサービスのログとして得られるクリックは、(ユーザーにそのアイテムが表示される確率)×(ユーザーがその表示されたアイテムを気に入る確率) によって発生する。
我々が本当にモデリングしたいのは、後者の「ユーザーがその表示されたアイテムを気に入る確率」であるが、クリックログからはそれを単独で切り出すことができないというのがこの問題の難しいところである。
XGBoostでは version 2.0.0 からポジションバイアスに対処できる手法である Unbiased LambdaMART が実装された。このアルゴリズム自体は以下の ByteDance の論文で提案されており、傾向スコアベースの手法になっている。論文ではこれまで提案されてきた pointwise 損失に対する IPW 法 を pairwise 損失に拡張し、また傾向スコアを前もって求めるのではなく木の学習と同時に推定する手法を提案している。そして実際のサービス上でABテストを実施して有効性を確かめたということまで書かれている。1
実際のコード例は XGBoostのドキュメント に一応あるが非常にわかりづらい。サンプルコード内でデータセットを用意するのに使われている sort_ltr_samples
という関数の実装を読みに行くと、各クエリグループの中でポジションの昇順にソートしているようである。なので例えば pandas でデータセットを作成する際には以下のような感じでソートする必要がある。
df_train = df_train.sort_values(by=["query", "position"])
X_train = df_train[features]
y_train = df_train['click']
group_train = df_train['group']
モデル学習時には、lambdarank_unbiased
で unbiased LambdaMART を使うよう指定する。また lambdarank_bias_norm
で傾向スコア部分に対する正則化度合いを調整できる。
import xgboost as xgb
model = xgb.XGBRanker(
lambdarank_unbiased=True,
lambdarank_bias_norm=1,
)
model.fit(
X_train,
y_train,
qid=group_train,
eval_set=[
(X_train, y_train),
(X_val, y_val),
],
eval_qid=[group_train, group_val],
)
LightGBM でも最近ポジションバイアスに対処する機能が実装されている。IPW を採用していた XGBoost とは異なり、LightGBM では一般化加法モデルベースの手法を採用している。これは学習時には $s(x, pos) = f(x) + g(pos)$ という和の形でデータをフィットし ($x$ はポジションを含まないモデルの特徴量)、ポジションの効果を $g$ に押し付けた上で推論時には $f$ だけを利用してスコアリングするという手法である。例えば以下の論文などではニューラルネットワークモデルでこのアイディアを実装して有効性を示している。
この手法については以前も記事にしたのでそちらも参照してほしい。
こちらもドキュメントがわかりづらいが、要はデータセットを作る際にポジションの値を array として渡せばそれでバイアス除去がオンになるようである。
import lightgbm as lgb
ds_train = lgb.Dataset(
X_train,
label=y_train,
group=group_train,
position=position_train,
)
model = lgb.train(model_params, ds_train,...)
また、lambdarank_position_bias_regularization
というパラメータでポジションバイアス因子(上の $g$)の強さを調整できる。
上で MSLR-WEB30K から作成したデータセットに対して、普通の XGBoost・LightGBM とそのポジションバイアス除去版の性能を比較した。評価指標は NDCG@5 で、学習はバイアスのあるクリックラベルについて行い、評価時は真に予測したい unbiased な relevance ラベルについて評価した。
$w$ | XGBoost | Debiased XGBoost | LightGBM | Debiased LightGBM |
---|---|---|---|---|
0 | 0.2683 | 0.2881 | 0.2748 | 0.3115 |
0.2 | 0.2964 | 0.2783 | 0.3047 | 0.3212 |
0.6 | 0.3057 | 0.2963 | 0.3179 | 0.3362 |
0.8 | 0.3124 | 0.2782 | 0.2997 | 0.3414 |
1 | 0.2927 | 0.2905 | 0.3025 | 0.3414 |
これを見ると、一貫して LightGBM のポジション除去機能が高い性能を示していることがわかった。
コードは以下。
今回の結果はあくまで人工的にポジションを生成したデータセットにおけるもので、現実のデータではポジションバイアスがもっと複雑な入り方をしている場合もあるので注意が必要ではあるだろう。それでも LightGBM に position を記録した array を渡すだけで手軽に利用できるのはとてもありがたく、これまでよりも気軽に実装してABテスト等にかけることができそうだ。
ByteDance なので TikTok かと思ったが、Toutiao というニュースプラットフォームでのABテストらしい。