Edited at

検索エンジンにおけるLTRの訓練方法

以前、LTRについての概略を書きましたが、技術的な詳細をあまり書かなかったので、今回はElasticsearchのLTRプラグインを用いた訓練の仕方を書きます。


利用するプラグイン

以下のプラグインを利用します:

https://github.com/o19s/elasticsearch-learning-to-rank


パイプライン

検索エンジンにおけるLTRの運用の仕方は、以下の流れで行うことができます。


  1. クローリング。

  2. クローリングしたドキュメントのインデクシング。

  3. インデクシングしたドキュメントの特徴量を求め、格納する。

  4. 訓練データ(クエリ、ドキュメント)を用意する。

  5. 特徴量を検索システムのクエリに置き換え、訓練する。

  6. テストする。

ここでは、特にインデクシング時とその後にどのようにして特徴量を求めるのか、そしてそれをどうやって訓練するのかについて見ていきます。


インデクシング

インデクシングは以下のようなスクリプトを使うことができます。

https://github.com/sugiyamath/ltr_experiments/blob/master/indexing/index_fast.py

コンテンツ、タイトル、URLなどはトークナイズしてインデクシングする設定を行ってください。

https://qiita.com/mikika/items/1e83c97865c3148b3a7b

この際、高速に求まる特徴量を同時に格納してしまいます。例えば、url内のスラッシュの数のカウントなどが該当します。


高速に求まらない特徴量

高速に求まらない特徴量は、あとからUpdateします。以下のスクリプトが例です:

https://github.com/sugiyamath/ltr_experiments/tree/master/scripts

例えば、pagerankなどを求めたcsvをupdateによってelasticsearchへ格納するには、

#!/usr/bin/python3


from elasticsearch import Elasticsearch, helpers
from tqdm import tqdm
import pandas as pd

if __name__ == "__main__":

es = Elasticsearch('192.168.88.85:9200')
df = pd.read_csv('/home/shun/work/data/nodes_and_pagerank.csv',
header=None, names=["id", "pr"])
df = pd.merge(df, pd.read_csv('/home/shun/work/data/nodes_and_QoL.csv',
header=None, names=["id", "qol"]))
df = pd.merge(df, pd.read_csv('/home/shun/work/data/nodes_and_QoC.csv',
header=None, names=["id", "qoc"]))
df = pd.merge(df, pd.read_csv('/home/shun/work/data/nodes_and_degree.txt',
header=None, names=["id", "out", "in"]))
df = pd.merge(df, pd.read_csv('/home/shun/work/data/nodes_and_urlf.csv'))

df.to_csv("/home/shun/work/data/test.csv", index=False)
df = df.sort_values(by="id")

actions = [{
'_op_type': 'update',
'_index': 'webpage2',
'_type': 'page',
'_id': d['id'],
'doc': {
"pagerank": float(d['pr']),
"QoC": float(d['qoc']),
"QoL": float(d['qol']),
"num_outlink": int(d['out']),
"num_inlink": int(d['in']),
"len_url": int(d['len_url']),
"num_slash_in_url": int(d['num_slash_in_url']),
"qmark_in_url": bool(d['qmark_in_url'])
}
} for i, d in tqdm(df.iterrows())]

chunk_size = 10000

proceeded = 0

for i in tqdm(range(1+int(len(actions)/chunk_size))):
try:
helpers.bulk(
es, actions[i*chunk_size:(i+1)*chunk_size],
raise_on_exception=False)
proceeded += 1
except Exception as e:
print(e)
print("proceeded actions:", proceeded*chunk_size)

のようなスクリプトを実行します。つまり、


  1. 特徴量を求めるステップ。

  2. 特徴量をアップロードするステップ。

に分かれます。特に、PageRankなどのリンクベース特徴量は、それ自体が求めるのに膨大な計算を必要とするので、このような分離をしておくことになります。


訓練データの用意

RankLibを使うので、以下のような訓練データを定義します:

https://github.com/sugiyamath/ltr_experiments/blob/master/LTR_o19s/sample_judgments.txt

# grade (0-4)   queryid docId   title

#
# Add your keyword strings below, the feature script will
# Use them to populate your query templates
#
# qid:1: 医療
# qid:2: 統合失調症
# qid:3: 風邪
# qid:4: 骨折
# qid:5: サッカー
# qid:6: 野球
# qid:7: 筋トレ
# qid:8: まとめ
# qid:9: 美味しい食べ物
# qid:10: 理論
# qid:11: 頭痛
# qid:12: 筋肉痛
# qid:13: 風邪薬
# qid:14: スポーツ
# qid:15: 健康
# qid:16: 画像
# qid:17: 政治
# qid:18: 動画
# qid:19: コーラ
# qid:20: 美味しいコーヒーの淹れ方
# qid:21: 速報
# qid:22: 花粉症
# qid:23: wikipedia
#
# https://sourceforge.net/p/lemur/wiki/RankLib%20File%20Format/
#
#
4 qid:1 # 1 qlife
4 qid:1 # 21134 qlife pro news 1
4 qid:1 # 21146 qlife pro news 2
4 qid:1 # 21138 qlife pro
3 qid:1 # 21136 qlife pro paper translate
2 qid:1 # 182492 kaigo job navi
1 qid:1 # 182495 kaigo job navi: shikaku
1 qid:1 # 1973 tisato chuo clinic
0 qid:1 # 170897 qlife kuchikomi
0 qid:1 # 10819 tiken fumin
0 qid:1 # 9410 qlife otoiawase
0 qid:1 # 183085 mens skin care
0 qid:1 # 8188 qlife kuchikomi
0 qid:1 # 78 qlife kuchikomi policy
.
.
.

これは、スコアの値と、クエリ・ドキュメントペアを対応させたものです。この訓練データは次のステップで特徴量へ変換されます。


特徴量の定義

特徴量は、jsonファイルで定義します。このjsonは、その特徴量をElasticsearch上で取得するクエリです。

以下のリンクでは、定義されたjsonリストの例をアップロードしておきました:

https://github.com/sugiyamath/ltr_experiments/tree/master/LTR_o19s

特徴量には2つあり、LTRプラグインの機能によって、クエリやドキュメントのテキストから取得するものと、Elasticsearchのドキュメントと一緒に保存した値の2つがあります。

訓練には、上記リンク内においてあるtrain.pyを実行します。訓練による精度も出力されます。


参考

[0] https://github.com/o19s/elasticsearch-learning-to-rank

[1] https://misreading.chat/2019/01/21/episode-46-an-introduction-to-neural-information-retrieval/