【2023年版】python環境で利用できる日本語形態素解析ライブラリを比較してみた

# 概要

趣味や業務で**python環境**で自然言語処理のコードを書くことが多いが、
形態素解析（分かち書き）をしてくれるライブラリが多くてどれが最適か分からない
**とりあえず速度と分かち書きの性能を比較してみよう！！！**

※2023/11/17修正 遅延評価で実装されているライブラリを適切に評価できていなかったため検証方法を変更、それにともない結論を変更しています

# 結論

python上での形態素解析ライブラリは、
**Vibratoが高速に実行可能** で、
Mecabの結果とvibratoの結果は参照する辞書が同じ場合大差がなかった。

そのため、速度を気にする処理を実装する場合はvibratoの利用を検討するべきと判断した。

~~python上での形態素解析ライブラリは、~~
~~**JanomeとVibratoが高速に実行可能** で、~~
~~その2つの結果は参照する辞書が同じ場合大差がなかった。~~

~~個人的にはJanomeの関数の設計がシンプルであり、~~
~~慎重な単語抽出を見据えた辞書選択が必要な場合を除き、~~
~~**Janomeで十分な結果が得られる**と判断した。~~

# 検証内容

## 検証環境

Google Colaboratory

## 比較に選んだライブラリ

### 検証したもの

#### 最小コスト法

- [MeCab](https://taku910.github.io/mecab/)
  - 形態素解析といえば！という手法としてチョイス
- [Janome](https://mocobeta.github.io/janome/)
  - 後述するVibrato等を公表しているLegal Oneの中の人が公開しているモデル
  - コードの記述が平易
- [Sudachi](https://github.com/WorksApplications/Sudachi)
  - [ワークス徳島人工知能NLP研究所](https://nlp.worksap.co.jp/)が開発
  - [GiNZA](https://megagonlabs.github.io/ginza/)の形態素解析にも組み込まれている

#### 点予測法

- [Vibrato](https://github.com/daac-tools/vibrato)

### 検証しなかったもの

- [Juman++](https://nlp.ist.i.kyoto-u.ac.jp/?JUMAN%2B%2B)
  - 2年前から更新が止まっているため
- [SentencePiece](https://github.com/google/sentencepiece)
  - 学習が必要であり日本語の分かち書きモデルの利用に手間が必要なため
- [KyTea](http://www.phontron.com/kytea/index-ja.html)
  - 点予測を用いたのライブラリ。Vibratoで事足りた感があったので未検証。
- [Kuromoji](https://www.atilika.com/ja/kuromoji/)
  - Javaネイティブでpython用のラッピングが存在しないため
- [Jagger](http://www.tkl.iis.u-tokyo.ac.jp/~ynaga/jagger/index.ja.html)
  - 最小コスト法や点予測法と異なり最長一致法を用いて他手法より高速な点が特徴
  - 現状C++で実装されており今後のpythonラッピングに期待

## 分かち書き速度の検証

### 検証の用いるテキストデータの取得

今回は青空文庫で公開されている、夏目漱石著『こころ』のテキストデータを利用。
以下がテキストデータを取得し、1文ずつリストに分割したコード。
検証には以下のコードで生成した `sentences` を入力に用いた。

```python
import urllib.request
from bs4 import BeautifulSoup
import re
import time

def get_txt_from_aozorabunko(url):
  html = urllib.request.urlopen(url=url)
  soup = BeautifulSoup(html, "html.parser")
  sentences = soup.find("div","main_text")
  sentences = sentences.get_text().replace("\r", "").replace("\n", "").replace("\u3000", "")
  sentences = re.sub("（.*?）", "", sentences)
  return sentences

url = "https://www.aozora.gr.jp/cards/000148/files/773_14560.html" # 青空文庫のURL指定、これは「こころ」
sentences = get_txt_from_aozorabunko(url)
sentences = sentences.split("。")
```

### 実行時間の計測

ライブラリとモデルを読み込んでから、全ての文章に対して形態素解析を実行し終わるまでの時間をそれぞれ計測した。Colab環境で実行しているため、実行時間が毎回ある程度異なっていた。

#### 結果

**Vibrato、次点でMecabが他2つのライブラリに比べて高速**であることが分かった。

### 10回繰り返し合計時間

| ライブラリ | 実行時間（秒） |
|:-:|:-:|
|  MeCab | 2.85  |
| Janome  | 161.30  |
|  Sudachi | 17.00 |
| Vibrato  | 2.49  |

### 100回繰り返し合計時間

| ライブラリ | 実行時間（秒） |
|:-:|:-:|
|  MeCab | 20.22  |
| Janome  | 非常に時間がかかるため省略  |
|  Sudachi | 62.40 |
| Vibrato  | 11.32  |

#### MeCab

```python
import MeCab

# 形態素解析モデルの構築
t = MeCab.Tagger()

# 形態素解析の実行
start = time.time()
for idx in range(10):
  tokenized = []
  for sentence in sentences:
    tokens = t.parse(sentence).split(" ")[:-1]
    tokenized.extend(tokens)
print(time.time() - start)
```
#### Janome

```python
from janome.tokenizer import Tokenizer

# 形態素解析モデルの構築
t = Tokenizer()

# 形態素解析の実行
start = time.time()
for idx in range(10):
  tokenized = []
  for sentence in sentences:
    tokens = t.tokenize(sentence, wakati=True)
    tokens = [str(token) for token in tokens]
    tokenized.extend(tokens)
print(time.time() - start)
```
#### Sudachi

```python
from sudachipy import tokenizer
from sudachipy import dictionary

# 形態素解析モデルの構築
tokenizer_obj = dictionary.Dictionary().create()
mode = tokenizer.Tokenizer.SplitMode.C

# 形態素解析の実行
start = time.time()
for idx in range(10):
  for sentence in sentences:
    tokens = tokenizer_obj.tokenize(sentence, mode)
    tokens = [token.surface() for token in tokens]
    tokenized.extend(tokens)
print(time.time() - start)
```
#### Vibrato

```python
# 分類モデルのダウンロード
!wget https://github.com/daac-tools/vibrato/releases/download/v0.5.0/ipadic-mecab-2_7_0.tar.xz
!tar xf ipadic-mecab-2_7_0.tar.xz

import vibrato
import zstandard

# 分類モデルの読み込み
dctx = zstandard.ZstdDecompressor()
with open('ipadic-mecab-2_7_0/system.dic.zst', 'rb') as fp:
     with dctx.stream_reader(fp) as dict_reader:
        tokenizer = vibrato.Vibrato(dict_reader.read())

# 形態素解析の実行
start = time.time()
for idx in range(10):
  for sentence in sentences:
    tokens = tokenizer.tokenize(sentence)
    tokens = [token.surface() for token in tokens]
    tokenized.extend(tokens)
print(time.time() - start)
```

### 形態素解析の結果の比較

性能比較は実行が高速であったMecabとVibratoで実施した。結果として、「とうきょう」といった文字において分かち書きの内容の差異が確認できた。

入力テキスト
`東京スカイツリーへのお越しは、東武スカイツリーライン「とうきょうスカイツリー駅」が便利です`

#### Mecab

```python
import MeCab
t = MeCab.Tagger()
tokens = t.parse(text)
print(tokens)
```
```
東京	トーキョー	トウキョウ	トウキョウ	名詞-固有名詞-地名-一般			0
スカイ	スカイ	スカイ	スカイ-sky	名詞-普通名詞-一般			2
ツリー	ツリー	ツリー	ツリー-tree	名詞-普通名詞-一般			2
へ	エ	ヘ	へ	助詞-格助詞			
の	ノ	ノ	の	助詞-格助詞			
お	オ	オ	御	接頭辞			
越し	コシ	コス	越す	動詞-一般	五段-サ行	連用形-一般	0
は	ワ	ハ	は	助詞-係助詞			
、			、	補助記号-読点			
東武	トーブ	トウブ	トウブ	名詞-固有名詞-地名-一般			1
スカイ	スカイ	スカイ	スカイ-sky	名詞-普通名詞-一般			2
ツリー	ツリー	ツリー	ツリー-tree	名詞-普通名詞-一般			2
ライン	ライン	ライン	ライン-line	名詞-普通名詞-一般			1
「			「	補助記号-括弧開			
とうきょう	トーキョー	トウキョウ	トウキョウ	名詞-固有名詞-地名-一般			0
スカイ	スカイ	スカイ	スカイ-sky	名詞-普通名詞-一般			2
ツリー	ツリー	ツリー	ツリー-tree	名詞-普通名詞-一般			2
駅	エキ	エキ	駅	名詞-普通名詞-一般			1
」			」	補助記号-括弧閉			
が	ガ	ガ	が	助詞-格助詞			
便利	ベンリ	ベンリ	便利	名詞-普通名詞-形状詞可能			1
です	デス	デス	です	助動詞	助動詞-デス	終止形-一般	
。			。	補助記号-句点			
EOS
```

#### Vibrato
```python
import vibrato
import zstandard
dctx = zstandard.ZstdDecompressor()
with open('ipadic-mecab-2_7_0/system.dic.zst', 'rb') as fp:
     with dctx.stream_reader(fp) as dict_reader:
        tokenizer = vibrato.Vibrato(dict_reader.read())
tokens = tokenizer.tokenize(text)
for token in tokens:
  print(token.surface(), token.feature())
```
```
東京 名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー
スカイ 名詞,一般,*,*,*,*,スカイ,スカイ,スカイ
ツリー 名詞,一般,*,*,*,*,ツリー,ツリー,ツリー
へ 助詞,格助詞,一般,*,*,*,へ,ヘ,エ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
お越し 名詞,一般,*,*,*,*,お越し,オコシ,オコシ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
、 記号,読点,*,*,*,*,、,、,、
東武 名詞,固有名詞,組織,*,*,*,東武,トウブ,トーブ
スカイツリーライン 名詞,一般,*,*,*,*,*
「 記号,括弧開,*,*,*,*,「,「,「
とう 副詞,助詞類接続,*,*,*,*,とう,トウ,トウ
きょう 名詞,副詞可能,*,*,*,*,きょう,キョウ,キョー
スカイ 名詞,一般,*,*,*,*,スカイ,スカイ,スカイ
ツリー 名詞,一般,*,*,*,*,ツリー,ツリー,ツリー
駅 名詞,接尾,地域,*,*,*,駅,エキ,エキ
」 記号,括弧閉,*,*,*,*,」,」,」
が 助詞,格助詞,一般,*,*,*,が,ガ,ガ
便利 名詞,形容動詞語幹,*,*,*,*,便利,ベンリ,ベンリ
です 助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。 記号,句点,*,*,*,*,。,。,。
```

# 所感

※2023/11/17追記
しっかり調べられていない点が露見した形になり、当初投稿していた内容から大幅に記事の内容を変更しました（コメントでのご指摘ありがとうございました！）
pythonのyieldを使った遅延評価の実装の考慮が抜けていたため、分かち書きが実施されていない状態で評価をしてしまいました。
その上で改めて、速度向上を狙って実装されたVibratoが実際にも高速に動作することが分かったことは良かったです。改めて、プログラムの教養を学び直したいと思った今日この頃です。

~~正直Janomeの実行速度が速いことに驚いた。おそらくJanomeのみがピュアpythonで記述されており**Pythonから実行する環境において**は十分な性能を発揮できるのではと考察している。~~
~~Neologdなどの新語を積極的に取り入れている辞書を活用するためには他のライブラリを利用することも検討されるが、そこまで精密な結果を求めない場合においてはコーディングも容易で実行も高速なJanomeは十分な価値を発揮すると考える。~~
そこまでしっかり調べられていない部分もあるので訂正コメント等もお待ちしています！

# 参考
[^1]:https://qiita.com/hi-asano/items/aaf406db875f1c81530e
- [2019年末版 形態素解析器の比較](https://qiita.com/hi-asano/items/aaf406db875f1c81530e)
- [形態素解析器の違いで分かち書きに違いが出るのか実験してみた](https://phoneappli.hatenablog.com/entry/2022/09/01/181223)
- [形態素解析（自然言語処理の餅屋）](https://www.jnlp.org/nlp/%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90)
- [Sentencepiece : ニューラル言語処理向けトークナイザ](https://qiita.com/taku910/items/7e52f1e58d0ea6e7859c)
- [yieldのメリットを解説する](https://qiita.com/Tadahiro_Yamamura/items/26a925d9a670f01bf497)