[janome 開発日誌] 速くなってワードカウント機能が追加された janome 0.3.5 をリリースしました

janome 0.3.5 をリリースしました。変更点は以下のとおりです。

Improvements

① tokenize() が速くなりました

解析速度の向上を自分で試みていて限界を感じてきたため,【Help wanted】tokenize() を速くしたい というチケットを上げてPRを募っていました。募ってみたところ nakagami さんからおもむろに凄いPRが届き,(それを参考に自分でも他の箇所を修正し,)解析が速くなりました…!

複数の箇所が改善されていて,PR のポイントをまとめると以下でした。

  • オブジェクトのプロパティへの無駄なアクセスを削減する
  • struct.unpack をやめて packされたバイナリデータを自前でデコードする(unpackする型と要素数が決まっているので,自前で実装したほうが速くなるようです)
  • functools.lru_cache デコレータ (Python 3.2 で追加) で,関数呼び出しをキャッシュ(メモ化)

手元で試してみた感じ,この中で一番効いたのは関数呼び出しのキャッシュだったので,Python 3.2 以上で速度改善の効果が高いと思います。実は @lru_cacheの存在を知らず,今まで自前のメモ化も入れていたのですが,functools を使ったほうが効率的にキャッシュされるようでした(自前のキャッシュは古い Python バージョンのために残しています)。標準ライブラリは知っておかないとだめですね orz

どのくらい速くなったか,手元の PC でtokenize() 実行時間(Tokenizerの初期化にかかる時間は含みません)を比較した数値です。

※環境は,Core i5–2300 CPU @ 2.80GHz, fedora25, python 3.6.2

+-----------------|--------|-----------|-----------|---------+
|入力テキスト |文字数 |v0.3.4 |v0.3.5 |短縮率(*1)|
+-----------------|--------|-----------|-----------|---------+
|檸檬(*2) | 5,517| 0.979 sec| 0.686 sec| 29% |
|坊っちゃん(*2) | 104,217| 15.407 sec| 10.367 sec| 32% |
|吾輩は猫である(*2) | 374,076| 54.435 sec| 37.800 sec| 30% |
+-----------------|--------|-----------|-----------|---------+
(*1) 計算式は (v0.3.4の解析速度 - v0.3.5の解析速度)/v0.3.4の解析速度 です。
(*2) 青空文庫のテキストファイル全文をUTF8に変換したものです。

30% 程度,速くなっています。前述のとおり,一番効いているのがLRUキャッシュなので,とても短いドキュメント(ツイート程度)だとこれほど改善しないかもしれません。

② 単語出現頻度を数える TokenFilter を追加しました

0.3.4 で追加した Analyzer への機能追加です。

形態素解析そのものから離れるのですが,よくあるワードカウントを手軽に実行できる TokenCountFilter というビルトインの Filter クラスを追加しました。

たとえば以下のように POSKeepFilter と組み合わせて,品詞を絞って単語の出現回数をカウントできます。

>>> from janome.tokenizer import Tokenizer
>>> from janome.analyzer import Analyzer
>>> from janome.tokenfilter import *
>>> text = u'すもももももももものうち'
>>> token_filters = [POSKeepFilter('名詞'), TokenCountFilter()]
>>> a = Analyzer(token_filters=token_filters)
>>> for k, v in a.analyze(text):
... print('%s: %d' % (k, v))
...
もも: 2
すもも: 1
うち: 1

出現回数の多い順に返されるので,こんな感じで出現回数の多いトップ N ワードを出力する,といったこともできます。

>>> from janome.tokenizer import Tokenizer
>>> from janome.analyzer import Analyzer
>>> from janome.tokenfilter import *
>>> text = open('bocchan_utf8.txt').read()
>>> token_filters = [POSKeepFilter('名詞'), TokenCountFilter()]
>>> a = Analyzer(token_filters=token_filters)
>>> top_words = list(a.analyze(text))[:10]
>>> top_words
[('おれ', 472), ('の', 383), ('事', 291), ('ん', 255), ('もの', 222), ('人', 213), ('君', 184), ('よう', 178), ('赤', 178), ('一', 172)]

ワードカウントはよく実装すると思うので,何かと便利かな…と思います。 😃

Other

細かな仕様変更ですが,未知語の base_formsurface_form と同じ文字列を入れるようにしました。

  • v0.3.4 で未知語(”Python”)が入っている場合に base_form を抜き出した場合
>>> for token in t.tokenize('Pythonのすすめ'):
... print(token.base_form)
...
*

すすめ
  • v0.3.5 で未知語(”Python”)が入っている場合に base_form を抜き出した場合
>>> for token in t.tokenize('Pythonのすすめ'):
... print(token.base_form)
...
Python

すすめ

MeCab との互換性をとる必要がある場合などは,backward compatibility のためのオプション baseform_unk=False を指定してください(デフォルトは True)。

>>> for token in t.tokenize('Pythonのすすめ', baseform_unk=False):
... print(token.base_form)
...
*

すすめ

以上です。

(もっと落ち着いて出していくつもりだったのですが,着手してみるとやりたいことが五月雨に出てきたので,結果的に)しばらくリリースラッシュが続いてしまいました。少し本体の開発はお休みして,他の便利なライブラリとつなぐプラグインやバインディングの開発をしていこうかなと思います。#builderscon のこちらの発表で話題になっていた Pure Python な全文検索ライブラリ Whoosh に組み込んでみても面白そうかなと :)

janome へのリクエスト/PR は随時受け付けています!便利な組み込みの CharFilter, TokenFilter を増やしたいかなぁ…。|д゚)チラッ

Like what you read? Give moco(beta)’s backup 2nd a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.