SudachiPyのPosMatcher

:::note info
この記事のすべてのコードは[このGoogle Colab](https://colab.research.google.com/drive/17XGak0ZV3rC8XHDZS9ao1PP88LsMwu4x?usp=sharing)から試すことができます。
:::

# SudachiPyの品詞

形態素解析後、形態素の品詞によって異なる処理をしたいということがよくあります。SudachiPyでは品詞情報は`Morpheme`の`part_of_speech()`と`part_of_speech_id()`のメソッドでアクセスできます。

```python
import sudachipy
dic = sudachipy.Dictionary()
tok = dic.create()
morphs = tok.tokenize("品詞によって違う処理をする必要があります")
for m in morphs:
  print(m.surface(), m.part_of_speech(), sep="\t")
```
```
品詞	('名詞', '普通名詞', '一般', '*', '*', '*')
に	('助詞', '格助詞', '*', '*', '*', '*')
よっ	('動詞', '一般', '*', '*', '五段-ラ行', '連用形-促音便')
て	('助詞', '接続助詞', '*', '*', '*', '*')
違う	('動詞', '一般', '*', '*', '五段-ワア行', '連体形-一般')
処理	('名詞', '普通名詞', 'サ変可能', '*', '*', '*')
を	('助詞', '格助詞', '*', '*', '*', '*')
する	('動詞', '非自立可能', '*', '*', 'サ行変格', '連体形-一般')
必要	('名詞', '普通名詞', '形状詞可能', '*', '*', '*')
が	('助詞', '格助詞', '*', '*', '*', '*')
あり	('動詞', '非自立可能', '*', '*', '五段-ラ行', '連用形-一般')
ます	('助動詞', '*', '*', '*', '助動詞-マス', '終止形-一般')
```

一番簡単なやりかたは直接品詞Tupleの要素の文字列で条件を作ることです。

```python
for m in morphs:
  print(m.surface(), m.part_of_speech()[0] == "名詞", sep="\t")
```
```
品詞	True
に		False
よっ	False
て		False
違う	False
処理	True
を		False
する	False
必要	True
が		False
あり	False
ます	False
```

# PosMatcher

SudachiPy 0.6.1以降では、品詞が定義された集合に含まれているかというチェックを簡単に行えるようにPosMatcherのAPIが追加されました。PosMatcherはDictionaryの`pos_matcher`の関数を呼び出して作ります。引数の型によって2つの使い方があります。

## 関数から生成
一つ目の作り方は、pos_matcherの引数としてCallable（例えば、lambda関数）を渡すことです。品詞タグに対して真偽を返すような関数を渡すと、同様の動作をするPosMatcherが作られます。

```python
m1 = dic.pos_matcher(lambda x: x[0] == "名詞")
for m in morphs:
  print(m.surface(), m1(m), sep="\t") # PosMatcherを関数として使います
```
```
品詞	True
に		False
よっ	False
て		False
違う	False
処理	True
を		False
する	False
必要	True
が		False
あり	False
ます	False
```

## 部分品詞リストから生成
二つ目の作り方は、引数として部分品詞のリストを渡すことです。部分品詞とは、品詞Tupleの一部を省略可能な書き方です。省略したい要素をNoneにすると、ワイルドカードとしてマッチします。
```python
("名詞",) # 品詞の一層目が"名詞"のすべての品詞。Tupleのシンタックスとして、最後のカンマは必要。
(None, None, "サ変可能") # 品詞の2層目が"サ変可能"。
() # すべての品詞にマッチする。
```

部分品詞のPosMatcherの使い方の例は以下です。

```python
m2 = dic.pos_matcher([("名詞",)])
for m in morphs:
  print(m.surface(), m2(m), sep="\t")
```
```
品詞	True
に		False
よっ	False
て		False
違う	False
処理	True
を		False
する	False
必要	True
が		False
あり	False
ます	False
```

# 集合の処理

一度作ったPosMatcherのオブジェクトを集合の処理で違うPosMatcherに変えることもできます。基本的な4つの処理を実装しています。`Dictionary.pos_matcher`で同じPosMatcherを作ることも可能ですが、場合によって集合の処理での作り方のほうがわかりやすいケースもあります。この機能はSudachiPy 0.6.3以降です。

## 合併 (和集合)

```python
nouns = dic.pos_matcher(lambda x: x[0] == "名詞")
verbs = dic.pos_matcher(lambda x: x[0] == "動詞")
verbs_or_nouns = nouns | verbs
for m in morphs:
  print(m.surface(), verbs_or_nouns(m), sep="\t")
```
```
品詞	True
に		False
よっ	True
て		False
違う	True
処理	True
を		False
する	True
必要	True
が		False
あり	True
ます	False
```

## 交叉 (積集合)

```python
conj_form = dic.pos_matcher(lambda x: x[5] == "連用形-一般")
verbs_in_conj_form = verbs & conj_form
for m in morphs:
  print(m.surface(), verbs_in_conj_form(m), sep="\t")
```
```
品詞	False
に		False
よっ	False
て		False
違う	False
処理	False
を		False
する	False
必要	False
が		False
あり	True
ます	False
```

## 差・相対補 (差集合)

```python
suru_possible = dic.pos_matcher(lambda x: x[2] == "サ変可能")
nouns_suru_not_possible = nouns - suru_possible
for m in morphs:
  print(m.surface(), nouns_suru_not_possible(m), sep="\t")
```
```
品詞	True
に		False
よっ	False
て		False
違う	False
処理	False
を		False
する	False
必要	True
が		False
あり	False
ます	False
```

## 補・絶対補 (補集合)

```python
not_nouns = ~nouns
for m in morphs:
  print(m.surface(), not_nouns(m), sep="\t")
```
```
品詞	False
に		True
よっ	True
て		True
違う	True
処理	False
を		True
する	True
必要	False
が		True
あり	True
ます	True
```

# PosMatcherの品詞

PosMatcherに入っている品詞とその数を参照することもできます。Pythonの普通の`len`関数で辞書の品詞セット中のマッチした品詞の数を調べることができます。

```python
print(len(verbs_in_conj_form))
print(list(verbs_in_conj_form))
```
```
108
[('動詞', '一般', '*', '*', '五段-ラ行', '連用形-一般'), ('動詞', '一般', '*', '*', '下一段-マ行', '連用形-一般'), ...]
```

:::note info
PosMatcherの`iter`は必ず品詞をID順で返します。すべての品詞にマッチするPosMatcherは現在のすべての品詞をIDで参照可能な順番で返します。`map((y, x) for (x, y) in enumerate(matcher))`で品詞→品詞IDの`map`を作ることができます。
:::

# 速度

`PosMatcher`のもう一つの目的は品詞分岐の高速化です。ロジックは[Rustで実装されて](https://github.com/WorksApplications/sudachi.rs/blob/develop/python/src/pos_matcher.rs)、品詞IDで高速に品詞のチェックをします。速度の比較は以下の3つのケースで行いました。[コードはColabに載っています](https://colab.research.google.com/drive/17XGak0ZV3rC8XHDZS9ao1PP88LsMwu4x#scrollTo=iAs-BD9aqaTL&line=2&uniqifier=1)。

```python
# Baseline: 品詞情報をつかわない
count = 0
for m in morphs:    
    count += 1

# Case 1: 品詞情報をそのまま使う
count = 0
for m in morphs:
  pos = m.part_of_speech()
  if pos[0] == "名詞" or pos[0] == "動詞":
    count += 1

# Case 2: PosMatcherを使う
count = 0
for m in morphs:  
  if matcher(m):
    count += 1
```

Baselineは2.5マイクロ秒程度、Case1は5.6マイクロ秒、Case2は4.1マイクロ秒でした。Baselineにくらべると、Case1は3.1マイクロ秒のオーバーヘッドがあり、Case2は1.6マイクロ秒のオーバーヘッドがあります。この簡単なテストではPosMatcherのオーバーヘッドは2分の1です。条件が複雑になればなるほど、この差が広がります。

# 終わりに

PosMatcherはSudachiPyでMorphemeをフィルタリングするための簡単なAPIです。直接品詞を参照するよりおすすめです。コードの簡略化と処理速度の向上のメリットがあります。

よいSudachi Lifeを。