search
LoginSignup
22
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

JustSystems Advent Calendar 2017 Day 23

posted at

updated at

Elasticsearchのランキング学習を試した

この記事は、Justsystems Advent Calendar 2017の23日目の記事です。
今回はElasticsearchでランキング学習をやってみます。

ランキング学習について

ランキング学習は機械学習のモデルを用いて検索ランキングを最適化することをさします。
検索結果のランキングはtf-idfやBM25、ページランクなどを使うことが多いと思いますが、ランキング学習により検索サーバーのクリックログなどに基づき順位の最適化を行うことができます。
その際、処理速度などの問題から順位の最適化はトップN件のリランクという形で行われることが多いです (下図参照)。

250px-MLR-search-engine-example.png

Elasticsearchはこのようなランキング学習を行うことができるプラグインが開発されていますが、まだ詳細を確認できていなかったのでこの機会に試してみました。誰かの参考になれば幸いです。

この記事では次のことをします。

  • The Movie Databaseの映画情報を用いてインデックスを作成する
  • 映画と検索ユーザーの言語/嗜好ジャンルが一致する場合にクリックされやすくなる検索シミュレーションを行い、検索ログを生成する
  • 検索ログを用いてランキング学習し、言語とジャンルが一致する映画が上位にリランキングされるようになることを確かめる

Elasticsearchとプラグインの導入

適当なディレクトリにElasticsearchとプラグインをダウンロード、インストールし、起動します。

cd /tmp
curl -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.6.4.zip
unzip elasticsearch-5.6.4.zip && cd elasticsearch-5.6.4
curl -O http://es-learn-to-rank.labs.o19s.com/ltr-1.0.0-RC2-es5.6.4.zip
./bin/elasticsearch-plugin install -b file:///tmp/elasticsearch-5.6.4/ltr-1.0.0-RC2-es5.6.4.zip
./bin/elasticsearch &

データのインデックス作成

The Movie Databaseの映画情報ダウンロードし、インデックスを作成します。

curl -O http://es-learn-to-rank.labs.o19s.com/tmdb.json

マッピング定義

curl -X PUT "http://localhost:9200/tmdb" '{
  "mappings": {
    "movie": {
      "properties": {
        "title": {
          "type": "text"
        },
        "overview": {
          "type": "text"
        },
        "genres": {
          "type": "keyword"
        },
        "original_language": {
          "type": "keyword"
        }
      }
    }
  }
}'

データをbulkリクエスト形式に整形

import json

with open("tmdb.json") as infile, open("tmdb-bulk.txt", "w") as outfile:
  action = '{ "index": {} }'
  fields = ["title", "overview", "genres", "original_language"]
  contents = json.loads(infile.read())
  for record in contents.values():
    movie = {field: record[field] for field in fields if field in record}
    outfile.write("%s\n%s\n" % (action, json.dumps(movie)))

データの投入

curl -XPOST -H "Content-Type:application/json" "localhost:9200/tmdb/movie/_bulk" --data-binary @tmdb-bulk.txt

教師データの生成

映画と検索ユーザーの言語/嗜好ジャンルが一致する場合にクリックされやすくなる検索シミュレーションを行い、検索ログを生成します。

検索に用いる単語リストのダンロード

curl -O https://raw.githubusercontent.com/dwyl/english-words/master/words.txt

シミュレーションの実行

import math
import random
import elasticsearch

client = elasticsearch.Elasticsearch("localhost")

queries = [word.rstrip() for word in open("words.txt")]
languages = ["en", "ja"]
genres = ['Animation', 'TV Movie', 'Mystery', 'War', 'Adventure', 'Science Fiction',
          'Horror', 'Foreign', 'Music', 'Thriller', 'Family', 'Comedy', 'History',
          'Western', 'Romance', 'Action', 'Drama', 'Fantasy', 'Documentary', 'Crime']

simulation_result = list()
random.shuffle(queries)
for query_id in range(100000):
    # 検索クエリ、ユーザーの嗜好ジャンル、言語をランダムに選択
    query = queries[query_id]
    user_language = languages[random.randint(0, len(languages) - 1)]
    user_genre = genres[random.randint(0, len(genres) - 1)]

    # 検索の実行
    body = {"query": {"multi_match": {"query": query, "fields": ["title", "overview"]}}}
    result = client.search(index="tmdb", doc_type="movie", body=body)
    hits = result.get("hits").get("hits")
    if len(hits) == 0:
        continue

    # ヒットした結果をクリックするかどうかシミュレートする
    hit_group = list()
    for hit in hits:
        source = hit["_source"]
        movie_language = source["original_language"]
        movie_genres = [genre["name"] for genre in source.get("genres", [])]

        match_language = user_language == movie_language
        match_genre = user_genre in movie_genres

        logit = 0
        if match_language:
        # 言語がマッチした場合クリックされやすい
            logit += 1
        if match_genre:
        # ジャンルがマッチした場合クリックされやすい
            logit += 1

        p = 1 - 1 / (1 + math.exp(logit))
        if p > random.random():
            clicked = 1
        else:
            clicked = 0
        hit_group.append((clicked, query_id, int(match_language), int(match_genre)))
    simulation_result.append(hit_group)

# xgboostの教師/テストデータとして出力
def write_result(result, outfile, groupfile):
    for hit_group in result:
        for clicked, query_id, match_language, match_genre in hit_group:
            outfile.write("%s %s 1:%s 2:%s\n" % (clicked, query_id, match_language, match_genre))
        groupfile.write("%s\n" % len(hit_group))

with open("movie.train", "w") as train, open("movie.train.group", "w") as train_group, \
        open("movie.test", "w") as test, open("movie.test.group", "w") as test_group, \
        open("movie.vali", "w") as vali, open("movie.vali.group", "w") as vali_group:
    result_length = len(simulation_result)
    random.shuffle(simulation_result)
    write_result(simulation_result[:int(0.8 * result_length)], train, train_group)
    write_result(simulation_result[int(0.8 * result_length):int(0.9 * result_length)], test, test_group)
    write_result(simulation_result[int(0.9 * result_length):], vali, vali_group)

特徴の定義

データの特徴量をelasticsearchに登録していきます。
まずfeature storeという特徴の保存場所を作成します。

curl -XPUT "localhost:9200/_ltr"

次に特徴を定義します。

  • ユーザーの言語とヒットした映画の言語が一致しているかどうか
  • ユーザーの嗜好ジャンルとヒットした映画のジャンルが一致しているかどうか
curl -XPOST "localhost:9200/_ltr/_featureset/movie_features" -d '{
  "featureset": {
    "name": "movie_features",
    "features": [
      {
        "name": "language_match",
        "params": [
          "user_lang"
        ],
        "template_language": "mustache",
        "template": {
          "match": {
            "original_language": "{{user_lang}}"
          }
        }
      },
      {
        "name": "genres_match",
        "params": [
          "user_genres"
        ],
        "template_language": "mustache",
        "template": {
          "match": {
            "genres": "{{user_genres}}"
          }
        }
      }
    ]
  }
}'

特徴の定義はElasticsearchのクエリと対応しており、クエリで表現できるものが利用できます。
{{}}で囲われたものはmustacheテンプレートで、検索実行時に指定します。
また、Elasticsearch標準のクエリ以外にもプラグインが提供しているクエリを使うことで、tfやdfなどよく利用される特徴も定義することができます。詳細はドキュメントを参照ください。

学習の実行、モデルのアップロード

シミュレーションの条件だと線形モデルでも十分すぎますが、xgboostのペアワイズロスを用いて学習します。設定の詳細についてはxgboostのドキュメントを参照ください。

xgboostの設定ファイル

objective="rank:pairwise"

# Tree Booster Parameters
# step size shrinkage
eta = 0.1 
# minimum loss reduction required to make a further partition
gamma = 1.0 
# minimum sum of instance weight(hessian) needed in a child
min_child_weight = 0.1
# maximum depth of a tree
max_depth = 3

# Task parameters
# the number of round to do boosting
num_round = 2
# 0 means do not save any model except the final round model
save_period = 0 
# The path of training data
data = "movie.train" 
# The path of validation data, used to monitor training process, here [test] sets name of the validation set
eval[test] = "movie.vali" 
# The path of test data 
test:data = "movie.test"   

xgboostの実行

../../xgboost movie.conf
echo -e "0 na q\n1 language_match q\n2 genres_match q" > featmap.txt
../../xgboost movie.conf task=dump model_in=0004.model name_dump=dump.json dump_format=json fmap=featmap.txt

プラグインが線形モデル、ブースティング木の予測モデルをサポートしているので、簡単にElasticsearchに取り込むことができます。

xgboostのダンプからモデルの定義ファイル出力

import json

with open("dump.json") as infile, open("model_payload.json", "w") as outfile:
    model_payload = {
        "model": {
            "name": "movie_model",
            "model": {
                "type": "model/xgboost+json",
                "definition": infile.read()
            }
        }
    }
    outfile.write(json.dumps(model_payload))

モデルのアップロード

curl -XPOST -H "Content-Type:application/json" "localhost:9200/_ltr/_featureset/movie_features/_createmodel" -d @model_payload.json

学習したモデルで検索する

準備が整ったので、学習モデルを用いて検索をしてみましょう。

  • 言語 日本語、ジャンル SFでstarを検索
curl -XPOST "localhost:9200/tmdb/_search" -d '{
  "query": {
    "multi_match": {
      "query": "star",
      "fields": ["title", "overview"]
    }
  },
  "rescore": {
    "window_size": 1000,
    "query": {
      "rescore_query": {
        "sltr": {
          "params": {
            "user_lang": "ja",
            "user_genres": "Science Fiction"
          },
          "model": "movie_model"
        }
      },
      "query_weight" : 0.0,
      "rescore_query_weight" : 1.0
    }
  }
}'

top3

タイトル 言語 ジャンル
Gantz: Perfect Answer ja  Action, Mystery, Science Fiction
Fullmetal Alchemist: The Sacred Star of Milos ja Adventure, Fantasy, Animation, Action, Science Fiction
Fist of the North Star ja Action, Animation, Drama, Science Fiction, Thriller
  • ランキング学習を利用せずにstarを検索
curl -XPOST "localhost:9200/tmdb/_search" -d '{
  "query": {
    "multi_match": {
      "query": "star",
      "fields": ["title", "overview"]
    }
  }
}'

top3

タイトル 言語 ジャンル
Star! en  Romance, Drama, Music
Third Star en Drama
Afghan Star en Documentary, Music

ランキング学習により、言語や嗜好ジャンルがあったものが上位にリランクできています。

まとめ

Elasticsearchでランキング学習ができるプラグインを試してみました。
xgboostで学習したモデルを簡単に利用できる点が非常に便利だと感じました。
ただ特徴の定義ごとにクエリが必要なため、特徴の数が多い場合だと大変そうだなーという印象です。
性能も見ようと思っていましたが時間が無くなってしまったのでまた今度にします。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
22
Help us understand the problem. What are the problem?