ElasticsearchのSignigicant Terms AggregationのCustom Scoreによる特徴語抽出


概要

株式会社RevCommにて音声認識と全文検索を担当するk_ishiというものです。
この記事は 2020 年の RevComm アドベントカレンダー 21 日目の記事です。 20 日目は @rhoboro さんの「たった1行から始めるPythonのAST(抽象構文木)入門」でした。

ElasticsearchにはSignificant Terms Aggregationという、テキストフィールドに含まれる特徴語を抽出する機能が搭載されています。Significant Terms Aggregationは、デフォルトではJLH Scoreという指標で特徴語の抽出を行うのですが、どうもPainlessスクリプトを利用して記述するCustom Scoreによる特徴語抽出も可能となっているようです。このCustom Score機能のことがうっすらと気になっていたのですが、どうもネット上に情報が少ないような・・・と思ったので、本稿ではCustom Score機能について調査してまとめてみたいと思います。

特徴語とは

特徴語とは、ある属性をもつテキストに偏って出現する単語のことで、Twitterのトレンドのキーワードのようなものをイメージしていただくとわかりやすいと思います。
例えば、2020年12月のテキストには「クリスマス」や「アドベントカレンダー」などの単語が偏って出現するので、これらの単語は2020年12月のテキストの特徴語であると言えます。
ElasticsearchのSignificant Terms Aggregationでは、デフォルトでJLH Scoreというスコアの高い単語を特徴語として抽出します。
JLH Scoreは、下記の通り、指定範囲のドキュメントと全体のドキュメントでの、単語の出現割合を比較してスコアを算出するものです。

JLH = 絶対割合変化 × 相対割合変化
絶対割合変化 = 指定範囲のドキュメントでの出現割合 - 全体のドキュメントでの出現割合
相対割合変化 = 指定範囲のドキュメントでの出現割合 ÷ 全体のドキュメントでの出現割合

基本的には、全体であまり出現しなかった単語が、指定範囲で多めに出現していれば、その単語のスコアが高くなり、特徴語として抽出されます。

利用ソフトウェアなど

ソフトウェア バージョン URL
Elasticsearch 7.10.1 https://www.elastic.co/jp/downloads/elasticsearch
Python Elasticsearch Client 7.10.1 https://elasticsearch-py.readthedocs.io/en/7.10.0/
Citation-Network v1 https://cn.aminer.org/citation

データセットの準備

まずは、特徴語抽出の対象とするデータセットをElasticsearchに入れて準備します。
今回はデータセットとして、 https://cn.aminer.org/citation のCitation-Network v1を使います。
まず、Elasticsearchをダウンロードし、ローカルで立ち上げてください。
そして、下記のコマンドでindexを作ります。
Citation-Network v1は国際会議論文のタイトル、著者、国際会議名、開催年、アブストラクト、参考文献の情報が入っているデータセットです。
今回はアブストラクトから特徴語を抽出したいので、 abstractfielddatatrue にしています。
(Significant Terms Aggregationによる特徴語抽出は、fielddata:trueのフィールドでしか使えません)


curl -H "Content-Type: application/json" -XPUT 'http://localhost:9200/paper' -d '{
    "mappings": {
        "properties": {  
            "title": {
                "type": "text"
            },
            "authors": {
                "type": "text"
            },
            "venue": {
                "type": "text"
            },
            "abstract": {
                "type": "text",
                "fielddata": true
            },
            "year": {
                "type": "text"
            },
            "references": {
                "type": "text"
            }           
        }
    }
}'

次に、Citation-Network v1のデータをElasticsearchにインデクシングします。
今回は、下記のスクリプトで1万件だけインデクシングしました。
https://gist.github.com/ken57/6a299e67c90fac8a027a600daeeda352

JLH Scoreによる特徴語抽出

Significant Terms AggregationのJLH Scoreによる特徴語抽出

ElasticsearchのSignificant Terms Aggregationでは、JLHスコアを利用して特徴語抽出ができます。
下記のクエリにて、JLH Scoreにより、2003年論文のabstractの特徴語が抽出できます。
yearのところを"2004" に変えれば、2004年論文のabstractの特徴語が抽出できます。


curl -H 'Content-Type: application/json' localhost:9200/_search -d '{
    "size": 0,
    "query": {
        "term": {
            "year": "2003"
        }
    },
    "aggs": {
        "significant_terms_result": {
            "significant_terms": {
                "size": 5,
                "min_doc_count": 3,
                "field": "abstract"
            }
        }
    }
}'

2003年JLHスコアにより抽出された論文の特徴語は下記の通りでした。

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 748,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "aggregations": {
    "significant_terms_result": {
      "doc_count": 748,
      "bg_count": 10000,
      "buckets": [
        {
          "key": "preplogic",
          "doc_count": 10,
          "score": 0.16536074809116647,
          "bg_count": 10
        },
        {
          "key": "2003",
          "doc_count": 27,
          "score": 0.15273559065358508,
          "bg_count": 69
        },
        {
          "key": "cd",
          "doc_count": 26,
          "score": 0.1201397428198309,
          "bg_count": 78
        },
        {
          "key": "pass",
          "doc_count": 12,
          "score": 0.10651474979880143,
          "bg_count": 21
        },
        {
          "key": "joggers",
          "doc_count": 8,
          "score": 0.10369184134519144,
          "bg_count": 10
        }
      ]
    }
  }
}

2004年JLHスコアにより抽出された論文の特徴語は下記の通りでした。

{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 802,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "aggregations": {
    "significant_terms_result": {
      "doc_count": 802,
      "bg_count": 10000,
      "buckets": [
        {
          "key": "geometric",
          "doc_count": 14,
          "score": 0.07488439032397091,
          "bg_count": 33
        },
        {
          "key": "cad",
          "doc_count": 6,
          "score": 0.06248095472043083,
          "bg_count": 8
        },
        {
          "key": "mesh",
          "doc_count": 9,
          "score": 0.041249743471744586,
          "bg_count": 24
        },
        {
          "key": "boundary",
          "doc_count": 10,
          "score": 0.037683323667341985,
          "bg_count": 31
        },
        {
          "key": "subdivision",
          "doc_count": 4,
          "score": 0.03647158081521052,
          "bg_count": 6
        }
      ]
    }
  }
}

2003年のトップの preplogic というのはちょっとよくわからないですね。
調べてみたところ、ネットワーク関係の会社名(?)のようでした。
2004年のトップは、 geometric とのことで、幾何学とかが流行ってたんでしょうか・・・

手計算によるJLHスコア算出

たいして難しい計算でもないので、手計算で geometric のスコアを出してみます。

2004年のgeometricの件数

curl -H 'Content-Type: application/json' localhost:9200/_count -d '{
    "query": {
        "bool": {
            "must": [
                {
                    "term": {
                        "year": "2004"
                    }
                },
                {
                    "match": {
                        "abstract": "geometric"
                    }
                }
            ]
        }
    }
}'

結果は14件

2004年のドキュメント件数

curl -H 'Content-Type: application/json' localhost:9200/_count -d '{
    "query": {
        "bool": {
            "must": [
                {
                    "term": {
                        "year": "2004"
                    }
                }
            ]
        }
    }
}'

結果は802件

ドキュメント全体でのgeometricの件数

curl -H 'Content-Type: application/json' localhost:9200/_count -d '{
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "abstract": "geometric"
                    }
                }
            ]
        }
    }
}'

結果は33件

全体のドキュメント件数は10,000件です。

geometric のJLH Scoreは
JLH Score(geometric) = (14/802 - 33/10000) × (14/802/(33/10000)) = 0.07488439
となり、Significant Terms Aggregationの結果と一致します。

Custom Scoreによる特徴語抽出

ElasticsearchのSignificant Terms Aggregationでは、JLHスコアの他にも Mutual Information、Chi square、Google normalized distance、Percentage を利用して特徴語抽出ができますが、ここではPlainless Scriptを利用した Custom Scoreでの特徴語抽出を試してみます。
まずは、Painless ScriptでJLH Scoreを追実装してみます。
Custom Scoreでは下記のパラメータが利用できます。

  • params._subset_size: 指定範囲のドキュメント数
  • params._superset_size: 全体でのドキュメント数
  • params._subset_freq: 指定範囲での単語の出現数
  • params._superset_freq: 全体での単語の出現数
long sbs = params._subset_size.longValue();
long sps = params._superset_size.longValue();
long sbf = params._subset_freq.longValue();
long spf = params._superset_freq.longValue();
if (sbs > 0 && sps > 0 && sbf > 0 && spf > 0) {
  double sbr = sbf / (double) sbs;
  double spr = spf / (double) sps;
  return (sbr - spr) * (sbr / spr);
} else {
  return 0.0;
}

これを一行にまとめてjlh-scoreというIDでElasticsearchに保存します・・・。保存すると、Elasticsearch内でスクリプトがコンパイルされます。(複数行のまま保存する方法ないんだろうか)

curl -XPUT -H 'Content-Type: application/json' localhost:9200/_scripts/jlh-score -d '{
    "script": {
    "lang": "painless",
    "source": "long sbs = params._subset_size.longValue();long sps = params._superset_size.longValue();long sbf = params._subset_freq.longValue();long spf = params._superset_freq.longValue();if ( sbs > 0 && sps > 0 && sbf > 0 && spf > 0) {  double sbr = sbf / (double) sbs;  double spr = spf / (double) sps;  return  (sbr - spr) * (sbr / spr);} else {  return 0.0;}"
  }
}'

下記で登録したスクリプトを利用して特徴語抽出ができます。

curl -H 'Content-Type: application/json' localhost:9200/_search -d '{
    "size": 0,
    "query": {
        "term": {
            "year": "2004"
        }
    },
    "aggs": {
        "significant_terms_result": {
            "significant_terms": {
                "size": 5,
                "field": "abstract",
                "script_heuristic": {
                    "script": {
                        "stored": "jlh-score"
                    }
                }
            }
        }
    }
}'

JLHスコアで特徴語抽出したときと同じ結果がレスポンスされます。で・・・オリジナルのスコアを考えたいのですが・・・良い案も思いつかないので、今回は適当に下記としてみます・・・。

# 変数αは外から与える
# Custom Score = pow(指定範囲のドキュメントの出現数, α) × 絶対割合変化 × 相対割合変化

long sbs = params._subset_size.longValue();
long sps = params._superset_size.longValue();
long sbf = params._subset_freq.longValue();
long spf = params._superset_freq.longValue();
if (sbs > 0 && sps > 0 && sbf > 0 && spf > 0) {
  double sbr = sbf / (double) sbs;
  double spr = spf / (double) sps;
  return Math.pow(sbf, params.alpha) * (sbr - spr) * (sbr / spr);
} else {
  return 0.0;
}

ID: custom-scoreで保存します。

curl -XPUT -H 'Content-Type: application/json' localhost:9200/_scripts/custom-score -d '{
    "script": {
    "lang": "painless",
    "source": "long sbs = params._subset_size.longValue();long sps = params._superset_size.longValue();long sbf = params._subset_freq.longValue();long spf = params._superset_freq.longValue();if ( sbs > 0 && sps > 0 && sbf > 0 && spf > 0) {  double sbr = sbf / (double) sbs;  double spr = spf / (double) sps;  return  Math.pow(sbf, params.alpha) * (sbr - spr) * (sbr / spr);} else {  return 0.0;}"
  }
}'

alphaに2を指定しつつ、custom-scoreを利用した特徴語を実行するときの例です。
下記のalphaの値を大きくすると、指定範囲での頻度が大きい単語が上位に上がりやすくなります。

curl -H 'Content-Type: application/json' localhost:9200/_search -d '{
    "size": 0,
    "query": {
        "term": {
            "year": "2004"
        }
    },
    "aggs": {
        "significant_terms_result": {
            "significant_terms": {
                "size": 5,
                "field": "abstract",
                "script_heuristic": {
                    "script": {
                        "params": {
                            "alpha": 2
                        },
                        "stored": "custom-score"
                    }
                }
            }
        }
    }
}'
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 802,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "aggregations": {
    "significant_terms_result": {
      "doc_count": 802,
      "bg_count": 10000,
      "buckets": [
        {
          "key": "you",
          "doc_count": 42,
          "score": 15.12774174289961,
          "bg_count": 450
        },
        {
          "key": "geometric",
          "doc_count": 14,
          "score": 14.6773405034983,
          "bg_count": 33
        },
        {
          "key": "guide",
          "doc_count": 29,
          "score": 8.038082622472649,
          "bg_count": 286
        },
        {
          "key": "3d",
          "doc_count": 13,
          "score": 5.050820197613765,
          "bg_count": 57
        },
        {
          "key": "boundary",
          "doc_count": 10,
          "score": 3.768332366734198,
          "bg_count": 31
        }
      ]
    }
  }
}

まとめ

以上がElasticsearchのSignigicant Terms AggregationのCustom Scoreによる特徴語抽出の例でした。
スクリプトの外部からパラメータを与えたりもできるようなので、結構色々なことができそうな感じもしますね。
予め用意している指標で満足のいく結果が得られないときは、Custom Scoreを利用してみるのもありかも知れません。

明日は、@mpayu2さんの記事になります。お楽しみに!