概要
株式会社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は国際会議論文のタイトル、著者、国際会議名、開催年、アブストラクト、参考文献の情報が入っているデータセットです。
今回はアブストラクトから特徴語を抽出したいので、 abstract
の fielddata
を true
にしています。
(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さんの記事になります。お楽しみに!