Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

GiNZA+Elasticsearchで係り受け検索の第一歩

急に冷え込んできてお布団が恋しい季節になってきました。
こんにちは。@Ssk1029Takashiです。
この記事は自然言語処理 Advent Calendarの6日目の記事になります。
qiita.com

全文検索システムは単語検索であることが多いですが、単語検索だけだと困ることもあります
症例検索を例にとって見てみましょう。
検索エンジンに以下の2つの文章が登録されているとします。
「ずっと胃がキリキリと痛い。ただ、熱は無く平熱のままだ。」
「昨日からとても頭が痛い。おまけに胃がむかむかする。」
この時、「胃が痛い」と検索したとき、通常の単語検索の場合だと両方ともヒットしてしまいますが、下の文章は意味としては異なる文章のためゴミになります。

この記事では、GiNZAとElasticsearchを使って意味的に正しい上の文章だけを拾ってくる仕組みを簡単に実現してみようと思います。

どうやって解決するか

概要は以下の図のようになります。

f:id:acro-engineer:20191203000159p:plain

1. 投入時には文章を係り受け解析をして、主語・述語のペアを抜き出します。
 例えば、「昨日から胃がキリキリと痛い。ただし、熱はない」という文章からは、「主語:胃、述語:痛い」「主語:熱、述語:ない」という2つのペアを抽出します。
 また、今回は修飾語や目的語などの係り受け関係は考慮せず、主語述語のみとして、修飾語は次回の課題とします。

2. 検索時には入力として、文章を想定します。
 入力された文章を係り受け解析して、投入時と同じく主語・述語のペアにして、同じペアを持つ文書を検索するようにします。
 また、同時に主語・述語以外の単語でも絞るために、単語検索も同時に実行します。

GiNZAとは

詳細に入る前にGiNZAについて簡単に説明します。
megagonlabs.github.io

GiNZAはリクルートさんと国立国語研究所さんが共同で開発した自然言語処理用のライブラリです。
欧米でよく使用されている自然言語処理ライブラリであるspaCyを日本語に対応させたものになります。

特長は以下の3点だと思います。
1. 形態素解析・依存構造解析・固有表現抽出・埋め込みベクトル等一つのライブラリで様々なタスクに対応している。
2. pip install一行でインストールが可能。
3. 高精度なモデルがプリセットとして提供されている。

簡単に言うなら、 一行でインストールできる高精度・高機能な日本語処理ライブラリです。

個人的には形態素解析にSudachiが使用されており、表記揺れなどにもデフォルトで対応できるのがとてもありがたいです。

係り受け解析

それでは、実際に文書から主語・述語を抽出していきます。
主語・述語を抽出する手順は以下のようになります。
1. 日本語用モデルをロードする
2. 入力された文章を解析する。(この段階で形態素解析・依存構造解析を行う)
3. 解析結果から主語・述語のペアを抜き出す。

準備

まずは、GiNZAを使う準備からです。
以下のコマンドでインストールします。

pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"

モデルのロード・日本語文章の解析

まず試しに、モデルのロード・解析だけをやってみましょう。

import spacy

nlp = spacy.load('ja_ginza')
doc = nlp('昨日から胃がキリキリと痛い。')

for sent in doc.sents:
    for token in sent:
        print(token.i, token.orth_, token.lemma_, token.pos_, token.tag_, token.dep_, token.head.i)
    print('EOS')

出力は以下のようになります。

0 昨日 昨日 NOUN 名詞-普通名詞-副詞可能 nmod 6
1 から から ADP 助詞-格助詞 case 0
2 胃 胃 NOUN 名詞-普通名詞-一般 nsubj 6
3 が が ADP 助詞-格助詞 case 2
4 キリキリ きりきり NOUN 名詞-普通名詞-一般 nmod 6
5 と と ADP 助詞-格助詞 case 4
6 痛い 痛い ADJ 形容詞-一般 ROOT 6
7 。 。 PUNCT 補助記号-句点 punct 6

表示している項目は、左から単語・見出し語・品詞タグ・品詞情報・依存関係ラベル・係り先の単語インデックスとなっています。
これらの項目をトークンとごとに出力しています。
この中にある、NOUN・ADPや、nmod・nsubjという文字列はUniversal Dependencyというプロジェクトで定義されているラベルになります。
係り受け関係を可視化すると次のようになります。
f:id:acro-engineer:20191203013929p:plain

Universal Dependencyとは

軽くUniversal Dependency(以下UD)について説明しておきたいと思います。
UDとは、構文構造を多言語間で統一しようという世界的な活動のことを差します。
その中で、NOUN(名詞)やVERB(動詞)などの品詞ラベルや、 nsubj(主語述語関係)・dobj(目的語関係)などの係り受け関係を定義しています。
GiNZAはデフォルトではUDで定義されたラベルで依存構造解析を行います。
日本語でのUDについては以下の論文で詳しく解説されています。図を眺めるだけでもイメージを掴めるためぜひ読んでみてください。
https://www.anlp.jp/proceedings/annual_meeting/2015/pdf_dir/E3-4.pdf

主語述語のペアを抜き出す

それでは、主語述語のペアを抜き出していきます。
上で述べたように、nsubjというラベルが主語述語の係り受けなので、nsubjのラベルを持っているトークンと係り受け先のトークンをペアとします。
ただし、今報告されている問題として、依存関係のラベルがnsubjのものがiobjになってしまう課題があるので、ここではiobjも対象にします。

実装は以下のようになります。

def parse_document(sentence, nlp):
    doc = nlp(sentence)
    tokens = []

    ## 参照しやすいようにトークンのリストを作る
    for sent in doc.sents:
        for token in sent:
            tokens.append(token)

    ## 主語述語ペアのリスト
    subject_list = []

    for token in tokens:
        ## 依存関係ラベルがnsubj or iobjであれば「<見出し語>:<係り先の見出し語>」をリストに追加する。
        if token.dep_ in  ["nsubj", "iobj"]:
            subject_list.append(f"{token.lemma_}:{tokens[token.head.i].lemma_}")
    
    return subject_list

試しに「昨日から胃がキリキリと痛い。ただし、熱はない」という文章を引数にすると以下のようになります。

nlp = spacy.load('ja_ginza')
print(parse_document("昨日から胃がキリキリと痛い。ただ、熱は無い。", nlp))

出力

['胃:痛い', '熱:無い']

主語述語のペアが取れていますね。

Elasticsearchでの準備

次はElasticsearchへの投入と検索の部分を作っていきます。
バージョンとプラグインは以下のようになっています。
バージョン:7.4
プラグイン:analysis-sudachi

残り必要なことは以下の3つです。
①Mappingを決めて、データを投入する
②クエリを決める
③入力された検索文章からクエリに変換する

Mapping

Mappingについては、元文章は単語検索の対象になるためtext型で、主語述語のペアは完全一致のためkeyword型の配列で持つようにします。
また、形態素解析を使った単語検索のためにはtext型のフィールドにはanalyzerを設定しておきます。

クエリ

次に検索クエリを作成します。
例えば「胃が痛い」という入力のときには、以下のようなクエリになります。

{
  "query": {
    "bool": {
      "must": [
        {
          "query_string": {
            "default_field": "content",
            "query": "胃が痛い",
            "analyzer": "sudachi_analyzer"
          }
        },
        {
          "terms": {
            "subjects": [
              "胃:痛い"
            ]
          }
        }
      ]
    }
  }
}

クエリとしては、単語検索と主語述語の組み合わせをAND条件で取得するようにしています。

試してみる

それではサンプルデータを入れて検索してみましょう。

今回はサンプルのデータとして以下の2件をElasticseachに登録しておきます。(サンプルなので若干意図的な文章にしていますが)
「ずっと胃がキリキリと痛い。ただ、熱は無く平熱のままだ。」
「昨日からとても頭が痛い。おまけに胃がむかむかする。」

例として「胃が痛い」という文章で単純な単語検索と今回作成した検索を比べてみましょう。

単語検索の場合
クエリはquery_stringクエリを使用して、contentフィールドをします。

GET content/_search
{
  "query": {
    "query_string": {
      "default_field": "content",
      "query": "胃が痛い",
      "analyzer": "sudachi_analyzer"
    }
  }
}

結果としては、2件ともヒットします。
ただ、2文目のほうは検索意図からは外れているので、結果としてほしいものではありません。

次に今回作成した検索の結果を見てみましょう。
結果

{
    "hits": [
        {
            "_id": "DDzRwW4BGThQze84g2Hm",
            "_index": "content",
            "_score": 1.3560746,
            "_source": {
                "content": "ずっと胃がキリキリと痛い。ただ、熱は無く平熱のままだ。",
                "subjects": [
                    "胃:痛い",
                    "熱:侭"
                ]
            },
            "_type": "_doc"
        }
    ],
    "max_score": 1.3560746,
    "total": {
        "relation": "eq",
        "value": 1
    }
}

今回は一つ目の文書、つまり結果としてほしい結果のみがヒットしています。
このように、GiNZAの依存構造解析と検索エンジンと組み合わせることで、より意味的に欲しいものを検索することができます。

まとめ

GiNZAとElasticsearchを使って、簡易的にですが、係り受け検索を実現してみました。
GiNZAを使うことで、少ないコードで簡単に実現することができるので本当に便利だと思います。
今回は主語述語の関係のみに注目しましたが、他にも形容詞や否定語など考慮することは多くあるので次回への課題としようと思います。
皆さんもぜひGiNZAで日本語処理ライフを。

明日はg-kさんの記事になります。
それではまた。


Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Kaggle Masterと働きたい尖ったエンジニアWanted! - Acroquest Technology株式会社のエンジニアの求人 - Wantedlywww.wantedly.com