人間だったら考えて

なんでよ?考えて考えてっ 人間だったら考えて

LightGBMでサクッとランク学習やってみる

この記事はランク学習(Learning to Rank) Advent Calendar 2018 - Adventarの3本目の記事です。

この記事は何?

1本目・2本目の記事で、ランク学習の大枠を紹介しました。

szdr.hatenablog.com

szdr.hatenablog.com

この記事では、実際にランク学習を動かしてみようと思います。
ランク学習のツールはいくつかあるのですが、実はみんな大好きLightGBMもランク学習に対応しています。そこで、LightGBMを用いたランク学習を紹介したいと思います。



LightGBMのインストール

以下リンクを参考にしてください。
https://lightgbm.readthedocs.io/en/latest/Installation-Guide.html



LightGBMによるランク学習の設定値

LightGBMのレポジトリにはランク学習の例が含まれています。
LightGBM/examples/lambdarank at master · Microsoft/LightGBM · GitHub

LightGBMを使ったことのある方はご存知だと思いますが、LightGBMは実行時に設定ファイルを渡す or 設定値を引数で指定することができます。

レポジトリの例では既に設定ファイルが用意されているので、設定ファイルの中身を確認してみようと思います。


…色々書いてありますが、重要な行だけ紹介します。

まずは、"objective"(分類・回帰・ランク学習を指定)の項目です。

# application type, support following application
# regression , regression task
# binary , binary classification task
# lambdarank , lambdarank task
# alias: application, app
objective = lambdarank

LightGBMでランク学習を実行する際には、objectiveの項目に"lambdarank"を指定してください。
そのうちランク学習(Learning to Rank) Advent Calendar 2018 - Adventarで紹介するかもしれませんが、LightGBMではLambdaRankというランク学習手法が使われています。*1
LambdaRankについての詳細は以下リンクをご確認ください。
www.microsoft.com


次に、"metric"(評価指標)の項目です。

# eval metrics, support multi metric, delimite by ',' , support following metrics
# l1 
# l2 , default metric for regression
# ndcg , default metric for lambdarank
# auc 
# binary_logloss , default metric for binary
# binary_error
metric = ndcg

これもそのうち紹介しますが、ランク学習ではNDCGという評価指標がよく使われており、LightGBMでもサポートされています。

また、NDCGは検索結果リストの上位何件を評価に用いるかというパラメータを持っており、LightGBMでは以下のように指定します。

# evaluation position for ndcg metric, alias : ndcg_at
ndcg_eval_at = 1,3,5

他の設定値はランク学習に限らないものなので、この記事では紹介を省略します。



LightGBMによるランク学習の学習データ形式

次に、学習データの形式を確認します。
ランク学習ってどうやって学習するの?学習データ・特徴量・損失関数 - 人間だったら考えてで紹介しましたが、ランク学習では、ある「検索キーワード」にそれぞれ複数の文書が紐づく…という学習データを扱います。

LightGBMでは以下の2つの方法のどちらかで、「検索キーワード」→「紐づく文書群(データ)」を扱います。

  • クエリファイルを指定
  • 学習データにクエリID列を持たせる

クエリファイルを指定

レポジトリの例では、クエリファイルを用意して「検索キーワード」→「紐づく文書群(データ)」を扱っています。
訓練データのクエリファイルはrank.train.queryで、テストデータのクエリファイルはrank.test.queryとなっています。

設定値で明示的にクエリファイルの名前を指定していませんが、学習データのファイル名に".query"が付いているファイルを自動的にクエリファイルとして読み込みます。

クエリファイルについての記述

If the name of data file is train.txt, the query file should be named as train.txt.query and placed in the same folder as the data file. In this case, LightGBM will load the query file automatically if it exists.

さて、クエリファイルの中身(rank.train.query)を確認してみます。

1
13
5
8
19
12
18
...

…なんだこれって感じですが、これは"rank.train"という学習データの

  • 最初1行目は、検索キーワードq_1に紐づく文書群
  • 続く13行は、検索キーワードq_2に紐づく文書群
  • 続く5行は、検索キーワードq_3に紐づく文書群

…ということを表しています。

なので、学習データの行数と、クエリファイルの要素の和は一致するはずです。

# 学習データの行数
$ wc -l
3005 rank.train

# クエリファイルの要素の和
$ cat rank.train.query| python -c "import sys; print(sum(int(e) for e in sys.stdin))"
3005

学習データにクエリID列を持たせる

クエリファイルを別に用意するのではなく、学習データの列にクエリIDを持たせることもできます。私はこちらの方が好きです。

クエリIDは整数*2で与え、"group_column"という設定値に「何行目の列がクエリIDか」or「headerのどの項目がクエリID列に対応するか」を指定します。
→LightGBMのドキュメントのgroup_columnについての記述

注意として、クエリIDを与えたとしても、同じクエリIDに紐づく文書は学習データ内で連続していないといけません。バラバラだと異なるクエリIDに紐づくものと見なされてしまいます。*3



LightGBMの実行

設定値・学習データの形式を確認したところで、LightGBMを実行してみようと思います。

$ lightgbm config=train.conf
[LightGBM] [Info] Finished loading parameters
[LightGBM] [Info] Loading query boundaries...
[LightGBM] [Info] Loading query boundaries...
[LightGBM] [Info] Finished loading data in 0.105936 seconds
[LightGBM] [Info] Total Bins 6177
[LightGBM] [Info] Number of data: 3005, number of used features: 211
[LightGBM] [Info] Finished initializing training
[LightGBM] [Info] Started training...
[LightGBM] [Info] Iteration:1, training ndcg@1 : 0.703104
[LightGBM] [Info] Iteration:1, training ndcg@3 : 0.707849
[LightGBM] [Info] Iteration:1, training ndcg@5 : 0.735594
[LightGBM] [Info] Iteration:1, valid_1 ndcg@1 : 0.528762
[LightGBM] [Info] Iteration:1, valid_1 ndcg@3 : 0.583405
[LightGBM] [Info] Iteration:1, valid_1 ndcg@5 : 0.635418
[LightGBM] [Info] 0.013787 seconds elapsed, finished iteration 1
[LightGBM] [Info] Iteration:2, training ndcg@1 : 0.786022
[LightGBM] [Info] Iteration:2, training ndcg@3 : 0.77919
[LightGBM] [Info] Iteration:2, training ndcg@5 : 0.789961
[LightGBM] [Info] Iteration:2, valid_1 ndcg@1 : 0.58019
[LightGBM] [Info] Iteration:2, valid_1 ndcg@3 : 0.606662
[LightGBM] [Info] Iteration:2, valid_1 ndcg@5 : 0.646633
[LightGBM] [Info] 0.024056 seconds elapsed, finished iteration 2

...

[LightGBM] [Info] Iteration:100, training ndcg@1 : 0.986164
[LightGBM] [Info] Iteration:100, training ndcg@3 : 0.989809
[LightGBM] [Info] Iteration:100, training ndcg@5 : 0.983497
[LightGBM] [Info] Iteration:100, valid_1 ndcg@1 : 0.575619
[LightGBM] [Info] Iteration:100, valid_1 ndcg@3 : 0.608138
[LightGBM] [Info] Iteration:100, valid_1 ndcg@5 : 0.646253
[LightGBM] [Info] 1.245260 seconds elapsed, finished iteration 100
[LightGBM] [Info] Finished training

学習できました。お手軽ですね。

同じディレクトリに"LightGBM_model.txt"というモデルファイルが出来上がっているはずです。これを使ってテストデータに対する予測を実行してみます。

$ lightgbm config=predict.conf
[LightGBM] [Info] Finished loading parameters
[LightGBM] [Info] Finished initializing prediction, total used 100 iterations
[LightGBM] [Info] Finished prediction

予測できました。お手軽ですね。

同じディレクトリに"LightGBM_predict_result.txt"という予測結果ファイルが出来上がっているはずです。中身を確認してみます。

0.39914484932165278
0.14700876442599842
-0.095115700541625242
-0.042717963288159994
...

これは各文書に対するランキングモデルのスコアを表しており、スコアが大きいほど高い順位にある文書だということを表しています。



まとめ

この記事では、LightGBMを使ったランク学習を紹介しました。設定値がちょっとだけクセがあるものの、非常にお手軽にランク学習を回すことができたと思います。

次の記事では、上の実行例でも出てきましたが、NDCGをはじめとするランク学習における精度評価指標について紹介していきたいと思います。

*1:LightGBMでは勾配ブースティング木+LambdaRankなので、LambdaMARTと呼ぶのが正しいと思うのですが…細かい話ですね。

*2:たぶん0より大きい整数じゃないといけないと思います

*3:クエリIDを整数以外でも扱えて欲しいし、学習データ内でバラバラになっていても良い感じに読み込んで欲しい…contributeチャンス!