形態素解析器比較 Sudachi vs Mecab+Neologd

ブレインパッドさんのpodcast「白金鉱業.FM」の聞いてたらSudachiの開発の話を聞いて興味が出たので触ってみました。
shirokane-kougyou.fm
(「白金鉱業.FM」はデータ分析現場の生の声が聴けるのでなかなか面白いです。)

Sudachiとは

ワークスアプリケーションズ徳島人工知能NLP研究所でオープンソース開発されている形態素解析器です。
www.worksap.co.jp

形態素解析器とは日本語を単語に分かち書きしたり、品詞を特定する機能を有するもので、日本語の自然言語処理では必須です。
同じ様なものにはMecabやJuman++などがあります。
Sudachiの強みは辞書にあるらしく、長年研究してる専門家が辞書のメンテナンスをしているそうです。
また、分かち書きの方法(モード)が複数あり、切る長さを選ぶことが出来ます。

使ってみる

pythonのモジュールはここにあります。
https://github.com/WorksApplications/SudachiPy

まずはインストールします。

pip install sudachipy # 本体のインストール
pip install sudachidict_core #辞書のインストール

インポートしてそれぞれモードで分かち書きしてみる。

from sudachipy import tokenizer
from sudachipy import dictionary

tokenizer_obj = dictionary.Dictionary().create()


モードA

mode = tokenizer.Tokenizer.SplitMode.A
[
    (
        m.surface(), 
        m.dictionary_form(), 
        m.reading_form(),
        m.part_of_speech()
    )
    for m in tokenizer_obj.tokenize("国家公務員", mode)]

出力:

[('国家', '国家', 'コッカ', ['名詞', '普通名詞', '一般', '*', '*', '*']),
 ('公務', '公務', 'コウム', ['名詞', '普通名詞', '一般', '*', '*', '*']),
 ('員', '員', 'イン', ['接尾辞', '名詞的', '一般', '*', '*', '*'])]


モードB

mode = tokenizer.Tokenizer.SplitMode.B
[
    (
        m.surface(), 
        m.dictionary_form(), 
        m.reading_form(),
        m.part_of_speech()
    )
    for m in tokenizer_obj.tokenize("国家公務員", mode)]

出力:

[('国家', '国家', 'コッカ', ['名詞', '普通名詞', '一般', '*', '*', '*']),
 ('公務員', '公務員', 'コウムイン', ['名詞', '普通名詞', '一般', '*', '*', '*'])]


モードC

mode = tokenizer.Tokenizer.SplitMode.C
[
    (
        m.surface(), 
        m.dictionary_form(), 
        m.reading_form(),
        m.part_of_speech()
    )
    for m in tokenizer_obj.tokenize("国家公務員", mode)]

出力:

[('国家公務員', '国家公務員', 'コッカコウムイン', ['名詞', '普通名詞', '一般', '*', '*', '*'])]

モードAは細かく分解するのに対してモードCはあまり分解しない様です。
モジュールとしての使い勝手としてもよさそうです。

また正規化もしてくれます。

tokenizer_obj.tokenize("SUMMER", mode)[0].normalized_form()

出力:

'サマー'

すばらしい。

比較

ニュース記事分類のタスクで性能を比較してみます。
具体的にはSudachiとMecab+Neologdで分かち書きしたものをそれぞれtf-idfでベクトル化してロジスティック回帰で分類してみます。

データセット

データセットにはお馴染み(?)のlivedoor ニュースコーパスを使います。
https://www.rondhuit.com/download.html#ldcc

9種類の記事があり全部で7376記事あります。

使用したモジュール

この辺のモジュールを使いました。

import re
import math
import resource
import numpy as np
from urllib import request 
from pathlib import Path


import MeCab
import neologdn
import gensim
from gensim import corpora
from gensim.corpora import Dictionary


import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.metrics import recall_score, precision_score, accuracy_score
from sklearn.linear_model import LogisticRegression

from sudachipy import tokenizer
from sudachipy import dictionary

トークナイザー

SudachiとMecabそれぞれに対してほぼ同じ処理をするトークナイザーを作りました。

class SudachiTokenizer():
    def __init__(self, mode="C", stopwords=None, include_pos=None):
        
        if mode not in ["A", "B", "C"]:
            raise Exception("invalid mode. 'A' ,'B' or 'C'")
        self.mode = getattr(tokenizer.Tokenizer.SplitMode, mode)
        print(self.mode )
        
        if stopwords is None:
            self.stopwords = []
        else:
            self.stopwords = stopwords
        if include_pos is None:
            self.include_pos = ["名詞", "動詞", "形容詞"]
        else:
            self.include_pos = include_pos
    
    def parser(self, text):
        return tokenizer_obj.tokenize(text, self.mode)
    
    
    def tokenize(self, text, pos=False):
        res = []
        for m in self.parser(text):
            p = m.part_of_speech()
            base = m.normalized_form() #.dictionary_form()
            #print(base, ": ", p)
            if p[0] in self.include_pos and base not in self.stopwords and p[1] != "数詞":
                if pos:
                    res.append((base, p[0]))
                else:
                    res.append(base)
        return res
class MeCabTokenizer:
    def __init__(self, dic_dir=None, stopwords=None, include_pos=None):
        tagger_cmd = "-Ochasen"
        if dic_dir:
            tagger_cmd += " -d {}".format(dic_dir)
        mecab = MeCab.Tagger(tagger_cmd)
        self.parser = mecab.parse
        if stopwords is None:
            self.stopwords = []
        else:
            self.stopwords = stopwords
        if include_pos is None:
            self.include_pos = ["名詞", "動詞", "形容詞"]
        else:
            self.include_pos = include_pos

    def tokenize(self, text, pos=False):
        l = [line.split("\t") for line in self.parser(text).split("\n")]
        res = []
        for w in l:
            if len(w) >=4: # check nomal words (e.g. not EOS)
                p = w[3]
                group_pos = p.split("-")[0]
                base = w[2]
                if group_pos in self.include_pos and base not in self.stopwords and "数" not in p:
                    if pos:
                        res.append((base, p))
                    else:
                        res.append(base)
        return res

また、これらのトークナイザーに通す前には共通の関数でノーマライズしてます。

kaomoji_reg = r'[\[|\(][^あ-ん\u30A1-\u30F4\u2E80-\u2FDF\u3005-\u3007\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\U00020000-\U0002EBEF]+?[\]|\)]'
m = re.compile(kaomoji_reg)
def normalize(text):
    text = str(text)
    text = text.replace("\n", " ")
    text = re.sub(r"http(s)?:\/{2}[\d\w-]+(\.[\d\w-]+)*(?:(?:\/[^\s/]*))*", " ", text)
    text = re.sub(r"\S*@\S*\s?" ," ", text)
    text = text.lower()
    text = re.sub(kaomoji_reg, " ", text)
    text = re.sub(r'\d+', '', text)
    text = neologdn.normalize(text)
    return text

ストップワードも用意します。

sw_filename = "stopwords.txt"
if not Path(sw_filename).exists():
    res = request.urlopen("http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt").read().decode("utf-8")
    with open(sw_filename, "w") as f:
        f.write(res)
else:
    with open(sw_filename) as f:
        res = f.read()
stopwords = [line.strip() for line in res.split("\n")]
print(len(stopwords)) #331
print(stopwords[:3]) # ['あそこ', 'あたり', 'あちら']

トークナイザー使用例

include_pos = ["名詞", "動詞", "形容詞"]
mecab_tokenizer = MeCabTokenizer(dic_dir="/usr/local/lib/mecab/dic/mecab-ipadic-neologd", stopwords=stopwords, include_pos=include_pos)
sudachi_tokenizer = SudachiTokenizer(mode="B", stopwords=stopwords, include_pos=include_pos+["形状詞"])

mecab_tokenizer.tokenize(normalize(text), pos=False)
# 出力 ['コード', '命令', '冗長', '算出', 'プロパティ', '利用', 'する', 'バージョン', '比較', 'する', 'みる']

sudachi_tokenizer.tokenize(normalize(text), pos=False)
# 出力 ['コード', '命令', '冗長', '算出', 'プロパティー', '利用', '為る', 'バージョン', '比較', '為る', '見る']

「する」と「為る」、「みる」と「見る」の違いはsudachiのnormalized_form()から来ています。

辞書の統計的フィルター

今回は分類のタスクなので、全ての記事の中で1回しか出てない単語や、全体の9割より多く出現してる単語を除去しました。

dictinonary_mecab = Dictionary(sentences_mecab)
dictinonary_mecab.filter_extremes(no_below=2, no_above=0.9)
dictinonary_mecab.compactify()
corpus_mecab = [dictinonary_mecab.doc2bow(w) for w in sentences_mecab]

dictinonary_sudachi = Dictionary(sentences_sudachi)
dictinonary_sudachi.filter_extremes(no_below=2, no_above=0.9)
dictinonary_sudachi.compactify()
corpus_sudachi = [dictinonary_sudachi.doc2bow(w) for w in sentences_sudachi]

ベクトル化

tf-idfでベクトル化します。つまり一つの記事は語彙数次元のスパースなベクトルになります。
モジュールにはgensimを使いました。

分類器

分類器にはロジスティック回帰を使いました。
今回はweightの設定もハイパラのチューニングもせずにscikit-learnのデフォルト値で学習

結果

Sudachi(モードA)

f:id:tdualdir:20200713160434p:plain
sudachi_A

Sudachi(モードB)

f:id:tdualdir:20200713161020p:plain
sudachi_B

Sudachi(モードC)

f:id:tdualdir:20200713160721p:plain
sudachi_C

Mecab+Neologd

f:id:tdualdir:20200713154222p:plain
mecab

正解率
Sudachi(モードA) : 0.936
Sudachi(モードB) : 0.934
Sudachi(モードC) : 0.935
Mecab+Neologd: 0.943

Mecab+Neologdが一番良いです。

速度について

気になったのが速度です。
訓練データとして5532個のニュース記事をトークナイズした結果です。
f:id:tdualdir:20200713141431p:plain
Mecabが30秒かからずに終わっていますが、Sudachiは7分30秒ほどかかっています。

その他

品詞の付与について

品詞の特定はMecabとほとんど変わらないのですが、ちょいちょい違う場合もある様です。
f:id:tdualdir:20200713142212p:plain

なので今回は"名詞", "動詞", "形容詞"のみを抽出する予定でしたが、Sudachiの場合は"形状詞"も抽出しました。

終わりに

今回のタスクにおいてはMecab+Neologdの方が良かったです。
podcastでも少し言ってましたが、今は検索タスクを優先して改善してる様なので今回の使い方はまだフォーカスしてないのかもしれません。
この先10年はメンテしていく予定らしいので、ビジネスに使うとかシステムに組み込むという話になった時は一つ選択肢には上がると思います。
(組み込んだ後のNeologdの更新って皆んなどうしてるんだろう🤔)

また今回のコードはここにあります。こうした方が良いとかアドバイスください!
github.com


↓今すぐフォローすべきキラキラ アカウント


↓今すぐ登録すべきキラキラAIサービス
www.matrixflow.net


じゃあの。