日本語の tokenize, lemmatize, romanize の速度比較

# 1. はじめに
日本語の tokenize ライブラリの速度を比較している記事は沢山ありますが、「日本語の文章を単語のレンマごとにローマ字化したい」というニッチな要望に応えるものはなかったため、比較してみます。

# 2. 所要時間の測り方
処理を1000回行い、その平均所要時間を算出します。

```python
from functools import wraps
from time import perf_counter
from statistics import mean

def timeit(func):
    @wraps(func)
    def _wrapper(*args, **kwargs):
        es = []
        for _ in range(1000):
            t = perf_counter()
            result = func(*args, **kwargs)
            e = perf_counter() - t
            es.append(e)
        return result, mean(es)
    return _wrapper
```

# 3. tokenize => lemmatize

## 3-1. 使用するライブラリ
下記の4種類のライブラリを比較します。
```bash
fugashi==1.2.1
ginza==5.1.2
Janome==0.4.2
SudachiPy==0.6.7
```

### 3-1-1. fugashi
https://github.com/polm/fugashi

```python
from fugashi import Tagger

tagger_fugashi = Tagger('-Owakati')

@timeit
def lemmatize_by_fugashi(sent: str) -> list[str]:
    return [word.feature.lemma for word in tagger_fugashi(sent)]
```

### 3-1-2. GiNZA
https://github.com/megagonlabs/ginza

`GiNZA` は tokenize 周りに `SudachiPy` を使用しています。

```python
import spacy

nlp = spacy.load('ja_ginza', enable=[''])

@timeit
def lemmatize_by_ginza(sent: str) -> list[str]:
    return [token.lemma_ for token in nlp(sent)]
```

### 3-1-3. Janome
https://github.com/mocobeta/janome

```python
from janome.tokenizer import Tokenizer

t = Tokenizer()

@timeit
def lemmatize_by_janome(sent: str) -> list[str]:
    return [token.base_form for token in t.tokenize(sent)]
```

### 3-1-4. mecab-python3
https://github.com/SamuraiT/mecab-python3

```python
import MeCab

tagger_mecab = MeCab.Tagger()

@timeit
def lemmatize_by_mecab(sent: str) -> list[str]:
    """数詞の場合IndexErrorが起こるので、Noneを返すように修正"""
    return [
        None if len(row.split(',')) < 8 else row.split(',')[7]
        for row in tagger_mecab.parse(sent).split('\n')[:-2]
    ]
```

### 3-1-5. SudachiPy
https://github.com/WorksApplications/SudachiPy

```python
from typing import Literal
from sudachipy import tokenizer
from sudachipy import dictionary

tokenizer_obj = dictionary.Dictionary().create()

@timeit
def lemmatize_by_sudachipy(sent: str, mode: Literal['a', 'b', 'c']) -> list[str]:
    match mode:
        case 'a':
            split_mode = tokenizer.Tokenizer.SplitMode.A
        case 'b':
            split_mode = tokenizer.Tokenizer.SplitMode.B
        case 'c':
            split_mode = tokenizer.Tokenizer.SplitMode.C
    return [m.dictionary_form() for m in tokenizer_obj.tokenize(sent, split_mode)]
```

## 3-2. 検証結果

### 3-2-1. 文章①
`日越外交関係樹立50周年記念ハイレベル経済セミナー`

| ライブラリ | 出力 | 速度 |
| :-- | :-- | :-- |
| fugashi | 日越 / 外交 / 関係 / 樹立 / None / 周年 / 記念 / ハイ-high / レベル-level / 経済 / セミナー-seminar | 0.00010 |
| GiNZA | 日越 / 外交 / 関係 / 樹立 / 50 / 周年 / 記念 / ハイレベル / 経済 / セミナー | 0.00030 |
| Janome | 日 / 越 / 外交 / 関係 / 樹立 / 50 / 周年 / 記念 / ハイレベル / 経済 / セミナー | 0.00104 |
| mecab-python3 | 日越 / 外交 / 関係 / 樹立 / None / None / 記念 / ハイ-high / レベル-level / 経済 / セミナー-seminar | 0.00006 |
| SudachiPy (A) | 日越 / 外交 / 関係 / 樹立 / 50 / 周年 / 記念 / ハイ / レベル / 経済 / セミナー | 0.00003 |
| SudachiPy (B) | 日越 / 外交 / 関係 / 樹立 / 50 / 周年 / 記念 / ハイ / レベル / 経済 / セミナー | 0.00003 |
| SudachiPy (C) | 日越 / 外交 / 関係 / 樹立 / 50 / 周年 / 記念 / ハイレベル / 経済 / セミナー | 0.00003 |

### 3-2-2. 文章②
`両国関係は様々な分野で引き続き協力を促進するための大きな余地がある。`

| ライブラリ | 出力 | 速度 |
| :-- | :-- | :-- |
| fugashi | 両国 / 関係 / は / 様々 / だ / 分野 / で / 引き続く / 協力 / を / 促進 / 為る / 為 / の / 大きな / 余地 / が / 有る / 。 | 0.00015 |
| GiNZA | 両国 / 関係 / は / 様々 / だ / 分野 / で / 引き続く / 協力 / を / 促進 / する / ため / の / 大きな / 余地 / が / ある / 。 | 0.00050 |
| Janome | 両国 / 関係 / は / 様々 / だ / 分野 / で / 引き続き / 協力 / を / 促進 / する / ため / の / 大きな / 余地 / が / ある / 。 | 0.00132 |
| mecab-python3 | 両国 / 関係 / は / 様々 / だ / 分野 / で / 引き続く / 協力 / を / 促進 / 為る / 為 / の / 大きな / 余地 / が / 有る / 。 | 0.00011 |
| SudachiPy (A) | 両国 / 関係 / は / 様々 / だ / 分野 / で / 引く / 続く / 協力 / を / 促進 / する / ため / の / 大きな / 余地 / が / ある / 。 | 0.00004 |
| SudachiPy (B) | 両国 / 関係 / は / 様々 / だ / 分野 / で / 引き続く / 協力 / を / 促進 / する / ため / の / 大きな / 余地 / が / ある / 。 | 0.00004 |
| SudachiPy (C) | 両国 / 関係 / は / 様々 / だ / 分野 / で / 引き続く / 協力 / を / 促進 / する / ため / の / 大きな / 余地 / が / ある / 。 | 0.00004 |

### 3-2-3. 文章③
`コミュニケーション不足により信頼関係を築くことができない。`

| ライブラリ | 出力 | 速度 |
| :-- | :-- | :-- |
| fugashi | コミュニケーション-communication / 不足 / に / 因る / 信頼 / 関係 / を / 築く / 事 / が / 出来る / ない / 。 | 0.00012 |
| GiNZA | コミュニケーション / 不足 / に / よる / 信頼関係 / を / 築く / こと / が / できる / ない / 。 | 0.00040 |
| Janome | コミュニケーション / 不足 / により / 信頼 / 関係 / を / 築く / こと / が / できる / ない / 。 | 0.00106 |
| mecab-python3 | コミュニケーション-communication / 不足 / に / 因る / 信頼 / 関係 / を / 築く / 事 / が / 出来る / ない / 。 | 0.00009 |
| SudachiPy (A) | コミュニケーション / 不足 / に / よる / 信頼 / 関係 / を / 築く / こと / が / できる / ない / 。 | 0.00004 |
| SudachiPy (B) | コミュニケーション / 不足 / に / よる / 信頼 / 関係 / を / 築く / こと / が / できる / ない / 。 | 0.00004 |
| SudachiPy (C) | コミュニケーション / 不足 / に / よる / 信頼関係 / を / 築く / こと / が / できる / ない / 。 | 0.00004 |

`mecab-python3` は tokenize そのものはとても速いのですが、出力に手を加えないといけないので、予想していたより遅くなりました。
出力は `GiNZA` と `SudachiPy (C)` が単語を大きく取りがちという傾向がありました。

# 4. romanize

## 4-1. 使用するライブラリ
下記の2種類のライブラリを比較します。
```bash
cutlet==0.1.19
pykakasi==2.2.1
```

どちらのライブラリもローマ字化の方法を複数実装していますが、今回は**訓令式**を指定した結果を載せます。
また、どちらも内部で tokenize してから romanize しますが、今回は指定した単語を丸々 romanize したいので、無理やりくっつけます。

### 4-1-1. Pykakasi
https://github.com/miurahr/pykakasi

```python
import pykakasi

kks = pykakasi.kakasi()

@timeit
def romanize_by_pykakasi(lemma: str) -> str:
    return ''.join([item['kunrei'] for item in kks.convert(lemma)]).lower()
```

### 4-1-2. cutlet
https://github.com/polm/cutlet

`cutlet` は tokenize に `fugashi` を使用しています。

```python
import cutlet

katsu = cutlet.Cutlet(system='kunrei')
katsu.use_foreign_spelling = False

@timeit
def romanize_by_cutlet(lemma: str) -> str:
    return katsu.romaji(lemma).replace(' ', '').lower()
```

## 4-2. 検証結果

| 単語 | Pykakasi | cutlet | Pykakasi速度 | cutlet速度 |
| :-- | :-- | :-- | :-- | :-- |
| ある | aru | aru | 0.000008 | 0.000047 |
| が | ga | ga | 0.000006 | 0.000038 |
| こと | koto | koto | 0.000009 | 0.000022 |
| する | suru | suru | 0.000005 | 0.000021 |
| ため | tame | tame | 0.000004 | 0.000011 |
| だ | da | da | 0.000003 | 0.000011 |
| で | de | de | 0.000003 | 0.000010 |
| できる | dekiru | dekiru | 0.000006 | 0.000014 |
| ない | nai | nai | 0.000004 | 0.000013 |
| に | ni | ni | 0.000003 | 0.000010 |
| により | niyori | niyori | 0.000006 | 0.000021 |
| の | no | no | 0.000003 | 0.000010 |
| は | <font color="red">ha</font> | <font color="red">wa</font> | 0.000003 | 0.000010 |
| よる | yoru | yoru | 0.000004 | 0.000012 |
| を | <font color="red">wo</font> | <font color="red">o</font> | 0.000003 | 0.000009 |
| コミュニケーション | komyunikeesyon | komyunikeesyon | 0.000024 | 0.000028 |
| セミナー | seminaa | seminaa | 0.000009 | 0.000020 |
| ハイ | hai | hai | 0.000007 | 0.000013 |
| ハイレベル | haireberu | haireberu | 0.000013 | 0.000028 |
| レベル | reberu | reberu | 0.000009 | 0.000016 |
| 不足 | <font color="red">fusoku</font> | <font color="red">busoku</font> | 0.000005 | 0.000013 |
| 両国 | <font color="red">ryougoku</font> | <font color="red">ryoukoku</font> | 0.000005 | 0.000016 |
| 事 | koto | koto | 0.000005 | 0.000012 |
| 余地 | yoti | yoti | 0.000005 | 0.000012 |
| 促進 | sokusin | sokusin | 0.000005 | 0.000014 |
| 信頼 | sinrai | sinrai | 0.000005 | 0.000016 |
| 信頼関係 | sinraikankei | sinraikankei | 0.000005 | 0.000031 |
| 出来る | dekiru | dekiru | 0.000009 | 0.000024 |
| 分野 | <font color="red">bunya</font> | <font color="red">bun'ya</font> | 0.000008 | 0.000017 |
| 協力 | kyouryoku | kyouryoku | 0.000007 | 0.000022 |
| 周年 | syuunen | syuunen | 0.000005 | 0.000018 |
| 因る | yoru | yoru | 0.000005 | 0.000012 |
| 外交 | gaikou | gaikou | 0.000005 | 0.000014 |
| 大きな | ookina | ookina | 0.000007 | 0.000015 |
| 引き続き | hikituzuki | hikituzuki | 0.000005 | 0.000017 |
| 引き続く | hikituzuku | hikituzuku | 0.000005 | 0.000017 |
| 引く | hiku | hiku | 0.000005 | 0.000012 |
| 日 | <font color="red">niti</font> | <font color="red">hi</font> | 0.000006 | 0.000012 |
| 日越 | nitietu | nitietu | 0.000005 | 0.000017 |
| 有る | aru | aru | 0.000004 | 0.000012 |
| 様々 | samazama | samazama | 0.000005 | 0.000020 |
| 樹立 | <font color="red">juritu</font> | <font color="red">zyuritu</font> | 0.000005 | 0.000013 |
| 為 | tame | tame | 0.000005 | 0.000011 |
| 為る | <font color="red">suru</font> | <font color="red">naru</font> | 0.000005 | 0.000012 |
| 築く | kizuku | kizuku | 0.000006 | 0.000014 |
| 経済 | keizai | keizai | 0.000005 | 0.000015 |
| 続く | tuzuku | tuzuku | 0.000006 | 0.000014 |
| 記念 | kinen | kinen | 0.000006 | 0.000013 |
| 越 | <font color="red">etu</font> | <font color="red">kosi</font> | 0.000005 | 0.000012 |
| 関係 | kankei | kankei | 0.000005 | 0.000014 |

赤字でつけた差分を見ると `Pykakasi` の方が欲しい結果が出ている感じがします。
速度も `Pykakasi` の方が2-3倍速いようです。

# 5. 終わりに
今回に要望に対しては `SudachiPy + Pykakasi` が有効だということが分かりました。

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

kks = pykakasi.kakasi()
tokenizer_obj = dictionary.Dictionary().create()
mode = tokenizer.Tokenizer.SplitMode.C

def romanize_each_lemma(sent: str) -> list[str]:
    return [
        ''.join([item['kunrei'] for item in kks.convert(m.dictionary_form())]).lower()
        for m in tokenizer_obj.tokenize(sent, mode)
        if m.dictionary_form().isalpha()
    ]
```