はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

spaCyのMatcherについて(spaCyで日本語ホットワード抽出の真似事再び)

はじめに

以前の記事でspaCyで手探りで遊んでみました。

itdepends.hateblo.jp

また、これを受けて、PhraseMatcherを少し試してみましたというのが次の記事です。

itdepends.hateblo.jp

上記2つの上の方の記事では、PhraseMatcherとともに、spaCyのMatcherの名前にふれていましたが、今回、実際にMatcherを試してみたというメモです。

spaCyのMatcher

いろいろできますが、正規表現でのパターンマッチングが文字通り文字の並びに関するものとすれば、spaCyのMatcherは品詞や語彙の関係などのルールをもとに、文中の該当フレーズを検索/抜き出しできます。

例えば、[名詞,助詞,動詞]というルールをMatcherのパターンとして登録して、インプットの文章にマッチングを行うと、(「最近眠い」は抽出対象外となる一方)「桜が咲く」とか「太郎が走る」「太郎が走っ」といったフレーズが抽出できます。

より本格的には、モデルを賢くするために使ったりもするようですが(違うかな?)、少なくとも私のようなちょっと変わり種のgrepとして使いたい人にもお手軽に使える便利な仕組みです。

公式リファレンスなど

チュートリアルAPI仕様書を行き来するのが良いでしょう。

spacy.io

spacy.io

ルールについて

上記で、「ルール」の設定のイメージを示しましたが、実際は以下の条件を組み合わせたルールが設定できます。

Rule-based matching · spaCy Usage Documentation

また、次のサイトで英語の文章についてですが、ルールの試し打ちが可能です。

explosion.ai

話が前後しますが、ルールの具体的な設定は、以下のような、dictのリストで、品詞が「ADJ」で....というトークンの並びのパターンを指定し、これに該当するものが抽出されるというやり方になります。

pattern = [{'POS': 'ADJ', 'OP': '?'},
           {'LEMMA': 'match', 'POS': 'NOUN'},
           {'LEMMA': 'be'}]

品詞がADJのトークンが1つ以上 の後に
見出し語が「match」で品詞(用法)が名詞のもの が続いて
見出し語が 「be」に該当する
フレーズ(Span)を抽出せよ.... の意味

↓

文中の
match is
modern matches are
Wooden matches are
matches are

などが抽出される。

ルールの簡単な事例

ルールは、1つのトークンに複数の条件を当てはめることもできます。

また、「OP」により、繰り返しを表現できます。

この他、次の例ではなく、もっと後のサンプルで例示していますが、そのトークンの品詞情報が「【名詞】または【動詞】で始まるもの」のように、トークンの種別に関する正規表現でのルール設定も可能です。

spaCyのMatcherを利用したPythonプログラムサンプル

import spacy
from spacy.matcher import Matcher
import sys
nlp = spacy.load('ja_core_news_md')

_text = """
鳥が飛ぶ。
桜咲く。
海港から関西国際空港へ移動する。
"""

doc = nlp(_text.replace(" ", "。").replace(" ", "、"))  # replaceしているのは個人的な都合


def event(matcher, doc, i, matches):
    """
    本当はいろいろできる...がここではとりあえずマッチした直後にデバッグ文を出力
    """
    match_id, s, e = matches[i]
    print("DEBUG: ", i,
          nlp.vocab.strings[match_id], doc[s:e:], file=sys.stderr)


# Matcherを初期化して、「ルール」のパターンを登録(add)
mc = Matcher(nlp.vocab)
ptn1 = [{"TAG": {"REGEX": '^名詞'}}, {"DEP": 'case', "OP": "*"}, {"POS": "VERB"}]
mc.add("RULE1", event, ptn1)
ptn2 = [{"POS": "PROPN", "ENT_TYPE": "FAC"}]
mc.add("RULE2", event, ptn2)

# 登録したパターンでドキュメントに対してマッチング
for match_id, s, e in mc(doc):
    print(nlp.vocab.strings[match_id], doc[s:e])

# 答え合わせ
for i in doc:
    print(i.text, i.pos_, i.dep_, i.tag_, i.ent_type_)

↓ 出力例

DEBUG:  0 RULE1 鳥が飛ぶ
DEBUG:  1 RULE1 桜咲く
DEBUG:  2 RULE2 関西国際空港
DEBUG:  3 RULE1 関西国際空港へ移動
RULE1 鳥が飛ぶ
RULE1 桜咲く
RULE2 関西国際空港
RULE1 関西国際空港へ移動

 SPACE  _SP 
鳥 NOUN nsubj 名詞-普通名詞-一般 
が ADP case 助詞-格助詞 
飛ぶ VERB ROOT 動詞-一般 
。 PUNCT punct 補助記号-句点 
桜 NOUN compound 名詞-普通名詞-一般 
咲く VERB ROOT 動詞-一般 
。 PUNCT punct 補助記号-句点 
海港 NOUN obl 名詞-普通名詞-一般 
から ADP case 助詞-格助詞 
関西国際空港 PROPN obl 名詞-固有名詞-一般 FAC
へ ADP case 助詞-格助詞 
移動 VERB ROOT 名詞-普通名詞-サ変可能 
する AUX aux 動詞-非自立可能 
。 PUNCT punct 補助記号-句点 

Matcherでホットワード候補抽出(spaCyを使ってホットワード抽出再び)

なんとなくMatcherが分かった気になったところで応用です。

冒頭に引用した次の記事で、Matcherを使わずに、文書から品詞などを判定しながら、「ホットワード」抽出を試みました。

itdepends.hateblo.jp

ということで、上記で試みたオレオレ名人芸(素人芸?)で得られるものとおおよそ同等の条件に近づくように、Matcherを使ったらどうなるかということで試してみたのがこちら↓です。

spaCyのMatchのお試し

上記の実行結果

f:id:azotar:20200918173434p:plain

(少なくとも自分の中では)以前のものより「多少すっきり実現できた」気がしています。

試してみる前は、自分の腕前の限界もあって、それほど変わらないのではと思いましたが、MatcherでDSL的に大きく処理できる効果があったように思います。

前回は逐次的に処理していた部分を、Matcherで今回「フレーズとみなす」ものの最低限の条件を満たしたSpanの切り取りで最初の課題を飛び越えられるのはなかなか良いですね。

Matcherはあくまで個々のトークンの種別や並びでの抽出なので、依存関係が繋がっているかといったところは、対象外です。

よって、主語と述語として意味をなすように繋がっているか、といったところは、一旦取り出したspanについて追加で行うことになりますが、むしろMatcherができることがはっきりして役割分担もすっきりしたかもしれない。

それにひきかえ、以前のものはそれほど日が経っていないのに、今はもう何をしているかさっぱり分からない....

やってみてよかったです。

以上です。