iMind Developers Blog

iMind開発者ブログ

PythonのJanomeを用いた形態素解析

概要

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, 記事投稿