ナード戦隊データマン

機械学習と自然言語処理についてのブログ

ElasticsearchのLTRプラグインの使い方の実例

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

利用するプラグイン

以下のプラグインを利用します: 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/