Recruit Data Blog

こんにちは。人材領域でレコメンドシステムの機能開発をしている羽鳥です。 今日はレコメンドの要素技術でもよく使われているLightGBMでランキング学習を行うときのTipsと、LightGBMのパラメータに関してのちょっとした知識について紹介したいと思います。

ランキング学習とは

レコメンドの問題設定として、あるユーザーにアイテムを配信する場合を考えます。この際の代表的な手法として、過去の配信ログを学習し、各ユーザーに対して最もコンバージョン(CV)確率が高いアイテムを配信するようなものがあります。

この時に使用される手法はいくつかありますが、データの前処理にあまり手間がかからないLightGBMなどの勾配ブースティング木のモデルがよく使われているようです。

LightGBMでこの手のタスクを解くとなったときに、一番よくあるパターンはCVしたユーザとアイテムのペアを正例、CVしなかったユーザーとアイテムのペアを負例として学習するようなものです。この時の目的関数としてはloglossがよく用いられます。これはloglossが二値分類の目的関数としてデファクトスタンダードであり、理論的な解釈が容易であることや、モデルの予測値が確率として解釈できるために、その後のモニタリングから示唆が得やすくなるなどの理由があるためです。

loglossで学習したモデルが十分機能することもあるのですが、実務的には以下のような問題点が見つかることも多々あります。

  • サイト上で行動が活発なユーザーに対してはどのようなアイテムであっても予測確率が高く出やすい
  • 各ユーザーに対してのアイテムの並び順を最適化しているわけではないので、loglossが改善していても、AUCやprecision@kなどの並び順に関する指標が改善しないことがある

そこで登場するのがランキング学習です。ランキング学習には様々な派生手法がありますが、そのうちの一つがMicrosoftの論文で提案されている LambdaRankという手法です。この手法ではあるクエリに対してのアイテムの並び順を教師ラベルとし、モデルが与える予測値との差分を用いて目的関数を構成しています。 並び順という本質的に連続的でない変化をするものを例えば対数関数などの滑らかな関数に押し込めることによって、一般的な機械学習の手法のような最適化タスクと同様に扱うことができるようになります。

こうしてloglossなどの既存の目的関数では難しかった、各ユーザーに対しての「アイテムの並び順」を最適化することができるようになっています。 この手法は元々は情報検索を念頭に置いて提案された手法ですが、ユーザーをクエリ、配信されるアイテムをドキュメントとして捉えればレコメンドタスクに応用することができます。この学習がうまくいけば、loglossの問題であった特定のユーザーに対してスコアが高く出てしまう問題であったり、並び順が最適化されない問題などを解決することが期待されています。

しかもこちらのランキング学習はLightGBMの目的関数として既に実装されているため、学習の際の引数を指定するだけでかなり簡単に使うことができます。加えて有志による日本語のチュートリアルも数多く存在しており 1、実装に迷うことはあまりなさそうです。

ただし、「LambdaRankの目的変数をどのように設定すべきか」に関しては、少し注意する必要があります。

ランキング学習の目的変数について

今回の記事では、LambdaRankの目的変数の周辺を調査してみようと思います。 まず基本的なところですが、LambdaRankの目的変数はどのように設定するべきなのでしょうか。よくある2値分類のタスクでは正例を1、負例を0とすることが多いと思いますが、LambdaRankもそれで良いのでしょうか。 また、まさしくランキングを学習する時には1位, 2位, 3位, 着外といったラベルをそのまま1, 2, 3, 4…と設定して良いのでしょうか。などと思いながらぼんやりとLightGBMのソースコードを眺めていたら、まさに該当部分の説明がありました。

label should be int type, and larger number represents the higher relevance (e.g. 0:bad, 1:fair, 2:good, 3:perfect)

https://github.com/microsoft/LightGBM/blob/master/docs/Parameters.rst

ここからわかるように、LambdaRankの目的変数は良い評価のものほど数値が大きい想定で実装されています。これは2値分類の場合の自然な拡張になっており、とても納得感のあるものだと思います。

これで実装方針とチューニングの方針がかなり明確になってきました。

レコメンドのタスクの場合

  • クリックなどのCV情報を教師とする場合は2値分類の場合と同じく、CVしたアイテムを1、それ以外を0とすれば良い
  • 配信したアイテムに対して、ユーザーからのレーティング(例として3段階)が得られる場合は、最も良いものを2, 真ん中を1, 最も悪いものを0とすれば良い
  • 配信したアイテムのうち、特にユーザーが気に入ったもののみのランキング(例としてtop3)が得られる場合は, 最も良いものを3, 真ん中を2, 最も悪いものを1, それ以外のものを0とすれば良い

このような方針が思い浮かびます。

また、label_gainというオプションを用いることで、順位ごとのgainも調整することができます。これはデフォルトでは0,1,3,7,15,31,63,...,2^30-1と設定されており、ランキングrに対してのgainは2^r-1と表現することができます。

ここから、例えば3位までのランキングを学習する場合は1位のgainは3, 2位のgainは1, 3位のgainは0となることがわかります。 もし、1位のgainを大きくしたい場合はlabel_gain="0,1,5"などと設定することで目的関数に対する順位の影響を操作することができるようになっています。 ちなみにこのパラメータは与え方が他のパラメータとは異なっており、カンマ区切りの整数の列を文字列として与えることになっています。この時に空白などを挟むとエラーになるので注意が必要です。

LightGBMのソースコードを見てみる

最後に目的変数のドキュメントの周辺をもう少しだけ深掘りしてみようと思います。 先ほども紹介したように、LightGBMの各種パラメータについて書いてある部分はParameters.rstになるわけですが、このファイルを呼び出している箇所はparameter_generator.pyというpythonファイルになります。

このpythonファイルはconfig.hというC++のヘッダーファイルからLightGBMのオプションを定義したドキュメントなどを作成するためのものです。そしてこのファイルはpythonとRとC++が複雑に絡み合うLightGBMのソースコードの中で、数少ない部分的に実行できるモジュールになっています。

というわけで早速デバッガーを仕込んで実行してみましょう。

git clone https://github.com/microsoft/LightGBM.git
cd LightGBM
python -m pdb helpers/parameter_generator.py

適当なところにブレークポイントを仕込んで中を見てみます。

(Pdb) b 386
Breakpoint 1 at ~/LightGBM/helpers/parameter_generator.py:386
(Pdb) c
> ~/LightGBM/helpers/parameter_generator.py(386)<module>()
-> sections, descriptions = gen_parameter_code(config_hpp, config_out_cpp)
(Pdb) n
> ~/LightGBM/helpers/parameter_generator.py(387)<module>()
-> gen_parameter_description(sections, descriptions, params_rst)

このgen_parameter_codeがこのpythonファイルの肝となっている部分です。config.hで定義されたパラメータ情報を整形し、sectionsdescriptionsという二つの変数を返すとともに、config_auto.cppを自動生成しています。

ここで sections の中身を見てみます。

(Pdb) pp type(sections), len(sections), sections[0], sections[1]
(<class 'list'>, 10, ('Core Parameters', 1), ('Learning Control Parameters', 1))

このようにsectionsは長さが10のListで、その中にはLightGBMが持つパラメータの大分類が入っていることがわかりました。Core ParametersやらLearning Control ParametersやらはLightGBMの公式ドキュメントなどでおなじみだと思います。

次にdescriptionsを見てみると、こちらも長さ10のListであることがわかります。

(Pdb) pp type(descriptions), len(descriptions)
(<class 'list'>, 10))

descriptionssectionsに対応するパラメータの詳細が記述されています。 例えば、sections[1]には Learning Control Parametersが格納されていましたが、descriptions[1]にはこれに対応するmax_depthbagging_fractionなどの具体のパラメータの仕様が入っています。 試しに4番目の中身を見てみると、慣れ親しんだmax_depthの詳細が入っていることがわかります。

(Pdb) pp descriptions[1][3]
{'default': ['-1'],
 'desc': [('l1',
           'limit the max depth for tree model. This is used to deal with '
           'over-fitting when ``#data`` is small. Tree still grows leaf-wise'),
          ('l1', '``<= 0`` means no limit')],
 'inner_type': ['int'],
 'name': ['max_depth']}

このようにして得られたパラメータに関する情報をgen_parameter_description関数の中でparams_rstで定義されているパスに書き込んで全体の処理は終わりになります。

こうしてみてみるとLightGBMのドキュメントは大元となるconfigファイルを特定のフォーマットに従って記入し、そこからドキュメントを自動的に生成していることがわかります。試しに手元でParameters.rst.. start params list\n\nから\n\n.. end params listまでと、config_auto.cppの全てを削除したのちにparameter_generator.pyを実行してみると、Parameters.rstconfig_auto.cppの内容が再度挿入されることがわかると思います。

ちなみに、もともとの発端となったランキング学習の目的変数についてもconfig.hの中に記載があることが見て取れます。

終わりに

本記事ではランキング学習の目的変数について詳細を確認するとともに、LightGBMのパラメータに関するドキュメントがどのように生成されているかをコードを実行しつつ検証してみました。

一緒に働きませんか?

弊社では新卒・中途ともに様々な職種のエンジニアを募集しています。ご興味ある方は是非以下の採用ページをご覧ください。


  1. 例えば今回の記事を執筆するにあたってはLightGBMでランキング学習や、LightGBMでサクッとランク学習やってみるなどを参考にさせていただきました。 ↩︎

羽鳥冬星

人材領域のレコメンドシステムの改善を担当

羽鳥冬星

kaggleとスマブラSPが好きです