概要
Janomeは導入が手軽なピュアPython形態素解析ライブラリ。わざわざmecabやjuman++を導入する手順踏むほどでもないような軽い解析処理を行うシーンで使うと便利。
個人的にはPySpark上でわかち書きをする時に利用しています。
バージョン情報
- Python 3.6.5
- Janome==0.3.6
導入
$ pip install janome
以上。とても楽。
Janomeについて
公式サイトは下記。ここさえ読んでおけば利用で困ることはあまりなさそう。
http://mocobeta.github.io/janome/
要点としては
- ピュアPythonで書かれている
- 解析速度はmecabの10倍くらいかかる
- 形態素解析結果はmecabと概ね同じになる
- Elasticsearch/SolrにあるようなAnalyzer/Filterも用意されている
- Kuromojiライクなユーザー辞書も利用可能
スピード以外については不足を感じることは少ないのではないかと思われる。
実際に動かしてみる
まずはわかち書きをから。tokenizeにwakati=Trueを指定して渡すとわかち書きの結果が取れる。
from janome.tokenizer import Tokenizer tokenizer = Tokenizer() [token for token in tokenizer.tokenize('今日は休みます', wakati=True)] #=> ['今日', 'は', '休み', 'ます']
Tokenizerを初期化する際に辞書を読み込むので少し時間がかかる。Tokenizerは再利用可能なので初期化時だけ発生するコスト。
%time tokenizer = Tokenizer() # => CPU times: user 715 ms, sys: 92.3 ms, total: 807 ms # => Wall time: 806 ms
上記コードではwakati=Trueを設定したが、設定しなければ読み、原型、品詞等の情報が取れる。
下記は表層語、発音、品詞、基本形を表示した例。
tokenizer = Tokenizer() for token in tokenizer.tokenize('今日は休みます'): print(token.surface, token.reading, token.part_of_speech, token.base_form)
実行結果
今日 キョウ 名詞,副詞可能,*,* 今日 は ハ 助詞,係助詞,*,* は 休み ヤスミ 動詞,自立,*,* 休む ます マス 助動詞,*,*,* ます
フィルタも噛ませてみる
Janomeには各種フィルタが用意されている。例えばUnicode正規化を事前に実行したい場合はUnicodeNormalizeCharFilter、英字をLowerCaseに揃えたい場合はLowerCaseFilterを設定する。
下記は全角半角入り混じった「Englishmanがニューヨークへ行った」という文字列にUnicodeNormalizeCharFilterとLowerCaseFilterをかけた例。
from janome.tokenizer import Tokenizer from janome.analyzer import Analyzer from janome import tokenfilter, charfilter char_filters = [charfilter.UnicodeNormalizeCharFilter()] token_filters = [tokenfilter.LowerCaseFilter()] tokenizer = Tokenizer() analyzer = Analyzer(char_filters, tokenizer, token_filters) for token in analyzer.analyze('Englishmanがニューヨークへ行った'): print(token.surface)
実行結果。
englishman が ニューヨーク へ 行っ た
フィルタは下記などが用意されている。
package | class | 概要 |
---|---|---|
charfilter | RegexReplaceCharFilter(pattern, repl | 正規表現で文字を置き換える |
charfilter | UnicodeNormalizeCharFilter | Unicode正規化(デフォルトはNFKC) |
tokenfilter | CompoundNounFilter | 連続する名詞をひとまとめにする |
tokenfilter | ExtractAttributeFilter | 指定した属性を返す |
tokenfilter | LowerCaseFilter | lower case |
tokenfilter | POSKeepFilter | 指定品詞以外を除去する |
tokenfilter | POSStopFilter | 指定品詞を除去する |
tokenfilter | TokenCountFilter | 指定された属性でTokenをカウントする |
tokenfilter | UpperCaseFilter | upper case |
以降、いくつかのFilterについて実演して動きを見てみる。
CompoundNounFilter
CompoundNounFilterは連続する名詞を接続する。例えば「東京特許許可局」はFilterを使わなければ「東京」「特許」「許可」「局」に分割されるが、CompoundNounFilterを使うと一語にまとめられる。
token_filters = [tokenfilter.CompoundNounFilter()] tokenizer = Tokenizer() analyzer = Analyzer([], tokenizer, token_filters) for token in analyzer.analyze('東京特許許可局'): print(token.surface, token.part_of_speech)
実行結果
東京特許許可局 名詞,複合,*,*
品詞は名詞,複合になる。
POSKeepFilter / POSStopFilter
POSKeepFilterは指定品詞のみを、POSStopFilterは指定品詞以外を返す。
POSKeepFilterで名詞と動詞のみを抽出してみる。
token_filters = [tokenfilter.POSKeepFilter(['名詞', '動詞'])] tokenizer = Tokenizer() analyzer = Analyzer([], tokenizer, token_filters) for token in analyzer.analyze('時よ止まれ、お前は美しい'): print(token.surface)
実行結果
時 止まれ お前
続いてPOSStopFilter。名詞と動詞以外を抽出してみる。
token_filters = [tokenfilter.POSStopFilter(['名詞', '動詞'])] tokenizer = Tokenizer() analyzer = Analyzer([], tokenizer, token_filters) for token in analyzer.analyze('時よ止まれ、お前は美しい'): print(token.surface)
実行結果
よ 、 は 美しい
TokenCountFilter
TokenCountFilterは実行結果を単語とカウントのtupleで返す。
token_filters = [tokenfilter.TokenCountFilter()] tokenizer = Tokenizer() analyzer = Analyzer([], tokenizer, token_filters) for word, count in analyzer.analyze('働けど働けど働けど我が納期楽にならず'): print(word, count)
実行結果
働け 3 ど 3 我が 1 納期 1 楽 1 に 1 なら 1 ず 1
デフォルトはsurfaceでカウントされるが、下記のようにattに引数を指定することでbase_formでカウントしたりpart_of_speechやreadingでカウントすることもできる。
token_filters = [tokenfilter.TokenCountFilter(att='base_form')] tokenizer = Tokenizer() analyzer = Analyzer([], tokenizer, token_filters) for word, count in analyzer.analyze('そこにありここにある'): print(word, count)
実行結果
そこ 1 に 2 ある 2 ここ 1
token_filters = [tokenfilter.TokenCountFilter(att='part_of_speech')] tokenizer = Tokenizer() analyzer = Analyzer([], tokenizer, token_filters) for word, count in analyzer.analyze('働けど働けど働けど我が納期楽にならず'): print(word, count)
実行結果
動詞,自立,*,* 4 助詞,接続助詞,*,* 2 動詞,非自立,*,* 1 連体詞,*,*,* 1 名詞,一般,*,* 1 名詞,接尾,一般,* 1 助詞,格助詞,一般,* 1 助動詞,*,*,* 1
ExtractAttributeFilter
ExtractAttributeFilterは指定した属性のみを結果として返す。例えばbase_formだけが欲しいのであれば下記のように記述できる。
token_filters = [tokenfilter.ExtractAttributeFilter(att='base_form')] tokenizer = Tokenizer() analyzer = Analyzer([], tokenizer, token_filters) for base_form in analyzer.analyze('時よ止まれ、お前は美しい'): print(base_form)
実行結果
時 よ 止まれる 、 お前 は 美しい
mecab、kuromoji、juman++との実行速度比較
実際にどの程度の実行速度になるのか。試しに生のMecab、mecab-python, Kuromoji、Janome、Juman++の4つで実行速度を比較してみる。
解析対象ファイルはWikipediaのabstractのところだけをテキストファイルに変換したもの。ファイルサイズ876KB、EOS合わせてだいたい18万5千語。辞書はMecab、Kuromoji、JanomeはIPA辞書相当のものを選択。Juman++はデフォルトの辞書を使用。Pythonのコードではprintしてリダイレクトしてファイルに書き込む。
対象ファイルのサイズ詳細
$ wc abstract.txt 5763 14163 896389 abstract.txt
mecabの実行
$ time mecab < abstract.txt > result.txt real 0m0.062s
mecab-python
import MeCab tagger = MeCab.Tagger('-Ochasen') tagger.parse('') with open('abstract.txt') as f: for line in f: node = tagger.parseToNode(line) while node: print(node.surface, node.feature) node = node.next
kuromojiの実行コード
Tokenizer tokenizer = new Tokenizer(); String line; try ( BufferedReader br = new BufferedReader(new FileReader("abstract.txt")); BufferedWriter bw = new BufferedWriter(new FileWriter("result.txt"))) { while((line = br.readLine()) != null) for(Token token : tokenizer.tokenize(line)) bw.write(token.getSurface() + " " + token.getReading() + " " + token.getBaseForm() + " " + token.getPartOfSpeechLevel1() + " " + token.getPartOfSpeechLevel2() + " " + token.getPartOfSpeechLevel3()); }
Janomeの実行コード
from janome.tokenizer import Tokenizer tokenizer = Tokenizer() with open('abstract.txt') as f: for line in f: for token in tokenizer.tokenize(line): print(token.surface, token.reading, token.base_form, token.part_of_speech)
jumanppの実行
$ time jumanpp < abstract.txt > result.txt real 4m27.272s
下記が計測結果。JVMやPythonの生起コストは含まない。辞書の生起コストは含む。
library | time(msec) |
---|---|
mecab 0.996 | 195 |
mecab-python3 0.996.1 | 737 |
kuromoji 0.9.0 | 1,440 |
janome 0.3.6 | 16,683 |
juman++ 1.02 | 473,830 |
コード的に若干不平等なところはあるが実行時間のオーダーとしてはここから大きく変わることはないと思われる。
それなりに大きなサイズのテキストでも十数秒で結果が返るので、余程巨大テキストを処理する場合やリアルタイムで速度を求められるようなシチュエーションでなければJanomeでも十分に利用可能な速度だと言えそう。
改定履歴
Author: watanabe, Date: 2019-1-14, 記事投稿