静かなる名辞

pythonとプログラミングのこと

2019/03/22:TechAcademyがteratailの質問・回答を盗用していた件
2019/03/26:TechAcademy盗用事件 公式発表と深まる疑念


mecab-pythonで品詞を見るときはfeature.splitしない方が速い

はじめに

 mecab-pythonで形態素解析を行って何らかの処理をするとき、特定の品詞だけ取り出したいということがよくあります。

 そういう目的で書かれたコードとして、よくこんなものを見たりすると思います。

import MeCab

tagger = MeCab.Tagger()
tagger.parse("")
node = tagger.parseToNode("MeCabのテストをする")
lst = []
while node:
    if "名詞" == node.feature.split(",")[0]:  # ここが問題
        lst.append(node.surface)
    node = node.next
print(lst)  # => ['MeCab', 'テスト']

 それはそれで良いんですが、改めて冷静に考えてみると「str.splitはそこそこ負荷の高い処理なので、全形態素に対してやるのは問題にならないか?」という懸念が生じてきます。

 ということで、他の方法がないのか考えて速度を比較してみようと思います。

やり方の検討

 featureはIPA辞書を使っていれば、

蟻	名詞,一般,*,*,*,*,蟻,アリ,アリ

 のように出てきます(タブのあとの部分がfeatureです)。

 よって、これでもできます。

    if "名詞," in node.feature:

 この方法だと「名詞」という単語が出てくると若干問題があるのですが、実用上はほとんど問題ないでしょうし、回避策もその気になれば色々あるでしょう。

 これと同じことは正規表現でもできます。正規表現エンジンでやった方が速いかもという淡い期待を抱いて、そちらも試すことにします。

実験

 まず実験用データとして、wikipediaの「形態素解析」の記事の冒頭部分をコピペして、data.txtとして実行時カレントディレクトリに保存しておきました。文字数は445文字になりました。

形態素解析 - Wikipedia

 その上で以下のコードを書いて実行しました。

import re
import timeit

import MeCab

with open("data.txt") as f:
    text = f.read()

tagger = MeCab.Tagger("")
tagger.parse("")
def f1():
    """分割して条件式を作成
    """
    lst = []
    node = tagger.parseToNode(text).next
    while node.next:
        feature = node.feature.split(",")
        if feature[0] == "名詞":
            lst.append(node.surface)
        node = node.next
    return lst

def f2():
    """カンマ区切りのfeatureに対してinで直接調べる
    """
    lst = []
    node = tagger.parseToNode(text).next
    while node.next:
        if "名詞," in node.feature:
            lst.append(node.surface)
        node = node.next
    return lst

def f3():
    """正規表現を使う
    """
    r = re.compile(r"名詞,")
    lst = []
    node = tagger.parseToNode(text).next
    while node.next:
        if r.match(node.feature):
            lst.append(node.surface)
        node = node.next
    return lst

# 確認
print(f1() == f2() == f3())

# 計測
print(timeit.timeit(f1, number=1000))
print(timeit.timeit(f2, number=1000))
print(timeit.timeit(f3, number=1000))

 結果は以下のようになりました。

True
1.841627002999303
1.6039621180025279
1.7953744690021267

 案の定、inが一番速いです。正規表現はsplitするよりは速いものの、inには負けるようです。

解説

 str.splitを行うと、

  • splitの結果生じた各strオブジェクトの生成
  • それを格納するlistオブジェクトの生成

 という処理が行われ、新規にメモリ領域を確保するタイプの処理が走るため*1、とても遅くなります。ついでにいうと、参照カウンタも余計に走ります。

 inの操作は値比較だけでできます。また、処理系の組み込み型の比較演算などは、多くの場合は極めて高度な最適化が施されていて、比較的に高速に実行できます。

まとめ

 まあでも、これで期待される改善幅は1割とかそんなものなので、柔軟性と可読性を犠牲にしてまでやるかというと微妙かもしれません。

 むしろ安直に書きたいときに使えます。

*1:処理系内部でバッファ領域はあると思いますが