Elasticsearchには、あらかじめ設定されたSynonym(シノニム/同義語/類義語)で検索を行える機能があります。
今回は、このSynonymの設定を自動的に生成した手順を紹介します。
目次
- 環境
- 概要
- Wikipediaのダンプからテキストの抽出
- テキストの分かち書き
- Word2Vecの学習済みモデル作成
- Synonymファイルの作成
- Elasticsearchへの適応
- 所感
- 参考ドキュメント・記事
環境
- Python 3.8
- WikiExtractor (https://github.com/attardi/wikiextractor)
- SudachiPy (https://github.com/WorksApplications/SudachiPy)
- gensim (https://radimrehurek.com/gensim/)
- Elasticsearch 7.6.2
- Kibana 7.6.2
概要
Elasticsearch のアナライザをカスタマイズするで、Synonymの設定を簡単に解説しましたが、この設定を何もないところから手作業で作成するのは大変そうです。
そこで、テンプレートになるものや、自動で生成できる方法がないかを模索していました。
今回紹介する方法は、一定のノイズは出るものの、プログラムによって作成できるため、何もないところから手作業で作るよりも初回の導入コストを軽減できます。
Word2Vec
Synonymを抽出するためにWord2Vecを使用しました。
Word2VecについてはWeb上の情報も豊富でさまざまなエントリがあります。
簡単には名前の通り、単語をベクトルで表現する手法です。
ベクトル化を行って、あるベクトル(単語)が別のベクトルとどれだけ同じ方向を向いているかを計算することで、単語同士の類似度合いがわかります。
方向は、2つのベクトルの成す角のコサインで求めることができ、これをコサイン類似度といいます。
Wikipediaデータベースのダンプ
抽出元になる日本語のコーパスには、日本語Wikipediaのダンプを使用しました。
Wikipediaは不定期にデータベースのダンプの生成と提供が行われています。
詳細はこちらをご覧ください。
https://ja.wikipedia.org/wiki/Wikipedia:%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89
Wikipediaのダンプからテキストの抽出
以下のページから、最新の日本語Wikipediaのダンプを取得します。 https://dumps.wikimedia.org/jawiki/latest/
たくさんリンクがありますが、今回はすべての記事が含まれる、次のデータを使用しました。
jawiki-latest-pages-articles-multistream.xml.bz2
記事作成時点では、圧縮されている状態で3GB以上のファイルになっていました。
圧縮されているXMLファイルから、記事のテキストを抽出したいので、WikiExtractorという、Wikipediaデータベースのダンプからテキストを抽出できるPythonスクリプトを使用します。
WikiExtractor
は直接スクリプトを呼び出して使用できます。
python WikiExtractor.py jawiki-latest-pages-articles-multistream.xml.bz2
コマンドを実行すると、text
というディレクトリが作成されて、次のような構造でファイルが出力されていきます。
text/ ├─AA/ │ ├─wiki_00 │ ├─wiki_01 │ : │ └─wiki_99 ├─AB/ ├─AC/ : ├─/AZ ├─/BA : :
各ファイルの内容は、いくつかのWikipediaページのURL、タイトル、記事、となっていることが確認できます。
オプションで何も指定しない場合は、wiki_**
ファイルは1MB単位で区切られるようです。
テキストの分かち書き
記事の文章そのままでは単語のベクトル化が行えないので、前処理として、記事の文章を分かち書き(単語と単語の間へ半角スペースを挿入)して、1つのファイルにまとめます。
単語単位の分解を行う形態素解析器として、今回はSudachiPyを使用しました。
リポジトリのREADMEにある通り、インストールは以下のコマンドで行なえます。
pip install sudachipy sudachidict_core
以下のようなPythonスクリプトで、WikiExtractor
で出力されたtext
ディレクトリ内を走査して、wiki_**
のテキストを分かち書きしつつ、jawiki-wakachigaki.txt
というファイルへ出力します。
step01_wakachi.py
# -*- coding: utf-8 -*- import os import codecs from sudachipy import tokenizer from sudachipy import dictionary tokenizer_obj = dictionary.Dictionary().create() split_mode = tokenizer.Tokenizer.SplitMode.C def convert(dst, src_filename): """ src_filename のファイルを読み込み、半角空白で分かち書きしたものを dst へ書き込む. """ print('Convert:[%s]' % src_filename) f = codecs.open(src_filename, 'r', 'utf-8') lines = f.readlines() f.close() for line in lines: if line.strip() == '': dst.write('\n') else: token_array = [m.surface() for m in tokenizer_obj.tokenize(line, split_mode)] cnv_line = ' '.join(token_array) + '\n' dst.write(cnv_line) print('Complete:[%s]' % src_filename) def main(): out = codecs.open('jawiki-wakachigaki.txt', 'w', 'utf-8') # `text`ディレクトリ内のファイルを走査 for root, _, files in os.walk('text'): for file in files: file_path = os.path.join(root, file) convert(out, file_path) out.close() if __name__ == "__main__": main()
Word2Vecの学習済みモデル作成
前処理ができたので、Word2Vecで単語のベクトル化を行った学習済みモデルを作成します。
言語処理でよく使われるアルゴリズムを含むPythonのライブラリgensimを使用します。
SudachiPyと同様、pipでインストールできます。
pip install gensim
Word2Vecの学習済みモデルを作成するスクリプトは以下の通りです。
モデル作成を行っている、word2vec.Word2Vec
のパラメーターの詳細についてはAPIリファレンスをご確認ください。
また、logging
モジュールのbasicConfig
関数で、ログのフォーマットとレベルを設定することで、モデル作成の進捗が確認できます。
step02_model.py
# -*- coding: utf-8 -*- from gensim.models import word2vec from logging import basicConfig, INFO def main(): sentences = word2vec.Text8Corpus('jawiki-wakachigaki.txt') model = word2vec.Word2Vec(sentences, size=100, min_count=20, window=15) model.save("jawiki.model") if __name__ == "__main__": # 出力するログのフォーマットとレベルを設定 basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=INFO) main()
Synonymファイルの作成
モデルの作成ができたら、モデルを基にSynonymファイルの生成を行います。
step03_synonym.py
# -*- coding: utf-8 -*- import codecs from gensim.models.word2vec import Word2vec from sudachipy import tokenizer from sudachipy import dictionary def main(): t_obj = dictionary.Dictionary().create() mode = tokenizer.Tokenizer.SplitMode.C # 同義語(類義語)とみなす閾値(最大1.0) SIMILARTY_THRESHOLD = 0.95 model = Word2Vec.load("jawiki.model") wv = model.wv # Word2VecKeyedVectors keys = wv.vocab.keys() out = codecs.open('synonym.txt', 'w', 'utf-8') for key in keys: # 1文字以下の単語はスキップ if not (len(key) > 1): continue # key の品詞を取得 tokens = t_obj.tokenize(key, mode) pos = tokens[0].part_of_speech() # 名詞以外はスキップ if pos[0] != '名詞': continue # 数詞はスキップ if pos[1] == '数詞': continue results = wv.most_similar(positive=[key]) # 閾値以上の単語を synonym_array へ追加 synonym_array = [] for result in results: if result[1] >= SIMILARTY_THRESHOLD: synonym_array.append(result[0]) # 1つ以上類似判定された単語があれば書き込む if synonym_array != []: syn = ', '.join(synonym_array) out.write(key + ', ' + syn + '\n') print('key:[%s], synonym[%s]' % (key, syn)) out.close() if __name__ == "__main__": main() |
コサイン類似度の閾値を 0.95 に設定して この値以上の単語のみ抽出します。 モデルファイル jawiki.model から、学習できた 単語を keys として取り出しています。 1文字だけのもの、名詞以外のもの、 また、名詞の中でも数詞は除外します。 most_similar で類似の単語を取得します。 most_similar 関数には類似した単語の 上位いくつまでを返却するかを指定できる topn というデフォルト引数があり、 デフォルト値は 10になっています。 result は単語とコサイン類似度のタプルです。 1つ以上、類似と判定できた単語があれば ファイルへ書き込みを行います。 |
結果のsynonym.txt
を確認してみると、コサイン類似度で近しい言葉を集めているので、厳密には同義語とは言えないものの、似た文脈で使われそうな言葉が収集できていました。
中には対義語も混じっています。
世紀, 世紀末 いくつ, 幾つ 前述, 先述 ヶ所, カ所, ヵ所 起き, 起こっ みなし, 見なし イギリス英語, アメリカ英語 アメリカ英語, イギリス英語 すべて, 全て みなさ, 見なさ, 見做さ 西部, 東部 東部, 西部, 北部 南部, 北部 カ国, か国, ヶ国, ヵ国 か国, カ国, ヶ国 国連, 国際連合 減少, 増加 増加, 減少 全て, すべて 目途, 目処 佐藤, 鈴木 見なし, みなし ...
初めは厳密に同義語に絞り込む必要がある、と思い込んでいました。
ただ、この結果を見たときに、近しい文脈で使われる言葉が集められたのであれば用途によっては検索にヒットしやすくなるのではないかと考えるようになりました。
Elasticsearchへの適応
『Elasticsearch のアナライザをカスタマイズする』で紹介したSynonymの設定と同様に、Elastecsearchへ適応します。
ただ、まったく同じアナライザーの設定のままではIllegal Argument Exception
が発生する可能性が高いです。
Exceptionの原因はさまざまなのですが、たとえば
%, パーセント
というsynonymは、前処理にあたる、Character filters
によって、%
が除去されてしまっている、という旨のレスポンスが返却されます。
他にも
東京芸術大学, 東京藝術大学
がエラーになることを確認しました。
東京芸術大学
がSydachiPy
のモードtokenizer.Tokenizer.SplitMode.C
では、ひとつの形態素として扱われていたものが、kuromoji_tokenizer
では複数の形態素に分解されてしまうためエラーとなってしまっているようです。
これらを解決するには、たとえばフィルターの設定を見直したり、ElasticsearchのSudachiプラグインを導入して形態素解析器器のモードを統一するなどの方法があります。
またそれでもエラーが起きる場合、Elasticsearchは問題が起きているsynonymファイルの行数を丁寧にレスポンスしてくれるので、エラーが起きている箇所を確認し、ひとつずつ修正していく必要があります。
所感
膨大な同義語を手作業で収集する事は避けたいと考えて、今回の調査・検証を行いました。
最終的にはやはり人の手が必要でメンテナンスして育てていく必要があるということを痛感したのですが、それでもスクラッチで設定するよりもコストが減らせているのではないかと思います。
Wikipedia以外のコーパスでも同じことが行えそうですので、商品検索、医療、エンターテインメント等、サービスの目的に応じてコーパスや形態素解析器を選択することで検索の精度が上げられるのではないでしょうか。