Python, Janomeで日本語の形態素解析、分かち書き(単語分割)

JanomeはPythonの形態素解析エンジン。日本語のテキストを形態素ごとに分割して品詞を判定したり分かち書き(単語に分割)したりすることができます。MeCabなどの外部エンジンは必要なくpipでインストールできます。

mocobeta/janome: Japanese morphological analysis engine written in pure Python Welcome to janome's documentation! (Japanese) — Janome v0.3 documentation (ja)

ここでは以下の内容について説明します。サンプルコードで使っているJanomeのバージョンは0.3.6。

Janomeのインストール JanomeとMeCab 解析結果の精度 形態素解析の速度 使いどころ Janomeで形態素解析 基本的な使い方 Tokenオブジェクトの属性 Janomeで分かち書き(単語ごとに分割) 引数wakati リスト内包表記 単語の出現回数をカウント Analyzerフレームワークの使い方 Analyzerによる単語の出現回数のカウント 品詞指定の注意点

Janomeのインストール

pip(環境によってはpip3)でインストールできます。別途インストールが必要な依存ライブラリはなにもない。非常に手軽。

$ pip instrall janome

JanomeとMeCab

日本語の形態素解析エンジンとして有名なものにMeCabがあります。 MeCabをインストールして、さらにそのPythonバインディングもインストールするとPythonからMeCabを使うことができます。 Janomeの公式ドキュメントのFAQにMeCabとの比較についての回答があります。

解析結果の精度

解析結果の精度は同等。

Q. 解析結果の精度は。 A. 辞書,言語モデルともに MeCab のデフォルトシステム辞書をそのまま使わせていただいているため,バグがなければ,MeCab と同等の解析結果になると思います。 Janome公式ドキュメントFAQ

形態素解析の速度

解析速度はJanomeのほうが10倍程度遅い。

Q. 形態素解析の速度は。 A. 文章の長さによりますが,手元の PC では 1 センテンスあたり数ミリ〜数十ミリ秒でした。mecab-python の10倍程度(長い文章だとそれ以上)遅い,というくらいでしょうか。 Janome公式ドキュメントFAQ

使いどころ

Janomeのインストールの手軽さは魅力的。数百文字程度であれば気になるほど遅くもない。形態素解析がどんなものか試してみたい、という目的であればJanomeでまったく問題ないと思います。 繰り返し長文を解析したり大量のテキストを解析したりする必要がある場合はMeCabの環境を整えたほうがいいと思います。

Janomeで形態素解析

基本的な使い方

TokenizerをインポートしてTokenizerオブジェクトのインスタンスを生成、tokenize()メソッドに対象の文字列を渡します。tokenize()メソッドはjanome.tokenizer.Tokenオブジェクトを要素とするリストを返します。

from janome.tokenizer import Tokenizer

t = Tokenizer()

s = 'すもももももももものうち'

print(type(t.tokenize(s)))
# <class 'list'>

print(type(t.tokenize(s)[0]))
# <class 'janome.tokenizer.Token'>

Tokenオブジェクトをprint()で出力すると以下のように解析結果が表示されます。

for token in t.tokenize(s):
    print(token)
# すもも   名詞,一般,*,*,*,*,すもも,スモモ,スモモ
# も 助詞,係助詞,*,*,*,*,も,モ,モ
# もも    名詞,一般,*,*,*,*,もも,モモ,モモ
# も 助詞,係助詞,*,*,*,*,も,モ,モ
# もも    名詞,一般,*,*,*,*,もも,モモ,モモ
# の 助詞,連体化,*,*,*,*,の,ノ,ノ
# うち    名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ

tokenize()メソッドの引数streamをTrueとするとストリーミングモードとなり、リストではなくジェネレーターを返します。リスト全体を保持する必要がなくTokenを逐次的に処理する場合に使います。

print(type(t.tokenize(s, stream=True)))
# <class 'generator'>

for token in t.tokenize(s, stream=True):
    print(token)
# すもも   名詞,一般,*,*,*,*,すもも,スモモ,スモモ
# も 助詞,係助詞,*,*,*,*,も,モ,モ
# もも    名詞,一般,*,*,*,*,もも,モモ,モモ
# も 助詞,係助詞,*,*,*,*,も,モ,モ
# もも    名詞,一般,*,*,*,*,もも,モモ,モモ
# の 助詞,連体化,*,*,*,*,の,ノ,ノ
# うち    名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ

Tokenオブジェクトの属性

print()で一括出力するだけでなく、Tokenオブジェクトの属性からそれぞれの情報を取得できます。

janome.tokenizer.Token — Janome API reference v0.3

以下のTokenオブジェクトを例とします。

token = t.tokenize('走れ')[0]

print(type(token))
# <class 'janome.tokenizer.Token'>

print(token)
# 走れ    動詞,自立,*,*,五段・ラ行,命令e,走る,ハシレ,ハシレ


surface(表層形)
print(token.surface)
# 走れ

文字列の中で使われているそのままの形。

part_of_speech(品詞)
print(token.part_of_speech)
# 動詞,自立,*,*

品詞,品詞細分類1,品詞細分類2,品詞細分類3という文字列。細分類が定義されていないと*となります。 split()メソッドでカンマで分割したリストを取得できます。品詞だけ取得したい場合はそのリストの最初の要素を取り出せばいい。

print(token.part_of_speech.split(','))
# ['動詞', '自立', '*', '*']

print(token.part_of_speech.split(',')[0])
# 動詞

infl_type(活用型)

print(token.infl_type)
# 五段・ラ行

活用がない名詞などでは*。 infl_form(活用形)

print(token.infl_form)
# 命令e

活用がない名詞などでは*。 base_form(基本形、見出し語)

print(token.base_form)
# 走る

活用されていない基本形(原形)。辞書の見出し語に相当します。 reading(読み)

print(token.reading)
# ハシレ

phonetic(発音)

print(token.phonetic)
# ハシレ

Janomeで分かち書き(単語ごとに分割)

英語は単語ごとにスペースで区切られているので分割するのが簡単だが、日本語は難しい。 Janomeを使うと日本語のテキストを分かち書き(単語ごとに分割)することができます。なお、厳密には形態素と単語は異なるが、ここでは深追いしない。 以下の文字列を例とします。

s = '走れと言われたので走ると言った'

for token in t.tokenize(s):
    print(token)
# 走れ    動詞,自立,*,*,五段・ラ行,命令e,走る,ハシレ,ハシレ
# と 助詞,格助詞,引用,*,*,*,と,ト,ト
# 言わ    動詞,自立,*,*,五段・ワ行促音便,未然形,言う,イワ,イワ
# れ 動詞,接尾,*,*,一段,連用形,れる,レ,レ
# た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
# ので    助詞,接続助詞,*,*,*,*,ので,ノデ,ノデ
# 走る    動詞,自立,*,*,五段・ラ行,基本形,走る,ハシル,ハシル
# と 助詞,格助詞,引用,*,*,*,と,ト,ト
# 言っ    動詞,自立,*,*,五段・ワ行促音便,連用タ接続,言う,イッ,イッ
# た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ

引数wakati

tokenize()メソッドの引数wakatiをTrueとするとTokenオブジェクトのリストではなく表層形の文字列strのリストを返します。

print(t.tokenize(s, wakati=True))
# ['走れ', 'と', '言わ', 'れ', 'た', 'ので', '走る', 'と', '言っ', 'た']

Tokenizerオブジェクトのコンストラクタで引数wakati=Trueとすることもできます。このTokenizerオブジェクトを使うと常に分かち書きモードで処理するようになります。

t_wakati = Tokenizer(wakati=True)

print(t_wakati.tokenize(s))
# ['走れ', 'と', '言わ', 'れ', 'た', 'ので', '走る', 'と', '言っ', 'た']

リスト内包表記

リスト内包表記を使うとTokenオブジェクトから所望の属性を取り出してリスト化できます。 リスト内包表記については

元の文字列をそのまま分かち書きしたい場合はsurface属性を使います。引数wakatiをTrueとした場合の結果と同じ、

print([token.surface for token in t.tokenize(s)])
# ['走れ', 'と', '言わ', 'れ', 'た', 'ので', '走る', 'と', '言っ', 'た']

base_formやpart_of_speechで基本形や品詞のリストを取得することもできます。

print([token.base_form for token in t.tokenize(s)])
# ['走る', 'と', '言う', 'れる', 'た', 'ので', '走る', 'と', '言う', 'た']

print([token.part_of_speech.split(',')[0] for token in t.tokenize(s)])
# ['動詞', '助詞', '動詞', '動詞', '助動詞', '助詞', '動詞', '助詞', '動詞', '助動詞']

リスト内包表記でifを使うと特定の品詞のみをリストアップしたりできます。

print([token.surface for token in t.tokenize(s)
       if token.part_of_speech.startswith('動詞')])
# ['走れ', '言わ', 'れ', '走る', '言っ']

print([token.surface for token in t.tokenize(s)
       if not token.part_of_speech.startswith('動詞')])
# ['と', 'た', 'ので', 'と', 'た']

print([token.surface for token in t.tokenize(s)
       if token.part_of_speech.startswith('動詞,自立')])
# ['走れ', '言わ', '走る', '言っ']

print([token.surface for token in t.tokenize(s)
       if token.part_of_speech.split(',')[0] in ['動詞', '助動詞']])
# ['走れ', '言わ', 'れ', 'た', '走る', '言っ', 'た']

単語の出現回数をカウント

分かち書きしたリストがあれば、それぞれの単語の出現回数をカウントするのも簡単。 Python標準ライブラリcollectionsのCounterクラスを使います。Counterの詳しい使い方は

なお、JanomeのAnalyzerフレームワークを使ってカウントすることもできます。後述します。 単語のリストをコンストラクタCounter()に渡すとCounterオブジェクトが得られます。

from janome.tokenizer import Tokenizer
import collections

t = Tokenizer()

s = '人民の人民による人民のための政治'

for token in t.tokenize(s):
    print(token)
# 人民    名詞,一般,*,*,*,*,人民,ジンミン,ジンミン
# の 助詞,連体化,*,*,*,*,の,ノ,ノ
# 人民    名詞,一般,*,*,*,*,人民,ジンミン,ジンミン
# による   助詞,格助詞,連語,*,*,*,による,ニヨル,ニヨル
# 人民    名詞,一般,*,*,*,*,人民,ジンミン,ジンミン
# の 助詞,連体化,*,*,*,*,の,ノ,ノ
# ため    名詞,非自立,副詞可能,*,*,*,ため,タメ,タメ
# の 助詞,連体化,*,*,*,*,の,ノ,ノ
# 政治    名詞,一般,*,*,*,*,政治,セイジ,セイジ

c = collections.Counter(t.tokenize(s, wakati=True))

print(type(c))
# <class 'collections.Counter'>

print(c)
# Counter({'人民': 3, 'の': 3, 'による': 1, 'ため': 1, '政治': 1})

単語を指定するとその出現回数を取得できます。存在しない単語は0。

print(c['人民'])
# 3

print(c['国民'])
# 0

Counterオブジェクトのmost_common()メソッドは、(単語, 出現回数)のタプルが出現回数の多いほうから順に並んだリストを返します。

mc = c.most_common()
print(mc)
# [('人民', 3), ('の', 3), ('による', 1), ('ため', 1), ('政治', 1)]

先頭の要素から最頻出単語とその出現回数を取得したり、出現回数順の単語のタプルを取得したりできます。

print(mc[0][0])
# 人民

print(mc[0][1])
# 3

words, counts = zip(*c.most_common())

print(words)
# ('人民', 'の', 'による', 'ため', '政治')

print(counts)
# (3, 3, 1, 1, 1)

引数wakatiを使った分かち書きだと表層形のカウントになります。動詞などを基本形でカウントしたい場合は上述のリスト内包表記を使います。

s = '走れと言われたので走ると言った'

print(collections.Counter(t.tokenize(s, wakati=True)))
# Counter({'と': 2, 'た': 2, '走れ': 1, '言わ': 1, 'れ': 1, 'ので': 1, '走る': 1, '言っ': 1})

print(collections.Counter(token.base_form for token in t.tokenize(s)))
# Counter({'走る': 2, 'と': 2, '言う': 2, 'た': 2, 'れる': 1, 'ので': 1})

なお、ここではリスト内包表記のジェネレーター版([]ではなく()で囲む)を使っています。()内ではジェネレーター内包表記の()を省略できます。

print(type(token.base_form for token in t.tokenize(s)))
# <class 'generator'>

特定の品詞のみをカウントしたり、品詞ごとの個数をカウントしたりすることもできます。

print(collections.Counter(token.base_form for token in t.tokenize(s)
                          if token.part_of_speech.startswith('動詞,自立')))
# Counter({'走る': 2, '言う': 2})

print(collections.Counter(token.part_of_speech.split(',')[0] for token in t.tokenize(s)))
# Counter({'動詞': 5, '助詞': 3, '助動詞': 2})

Analyzerフレームワークの使い方

バージョン0.3.4からAnalyzerフレームワークが追加されました。形態素解析の前処理・後処理ができます。 バージョン0.3.6時点で以下のフィルターが用意されています。

CharFilter(対象文字列の正規化などの前処理) UnicodeNormalizeCharFilter() Unicodeをunicodedata.normalize()で正規化 引数に'NFC', 'NFKC', 'NFD', 'NFKD'を指定可能 デフォルトは'NFKC'で、全角→半角などの変換が行われる RegexReplaceCharFilter() 正規表現で置換 TokenFilter(トークンのフィルタリングなどの後処理) CompoundNounFilter() 連続する名詞の複合名詞化 ExtractAttributeFilter() 抽出する属性(‘surface’や‘part_of_speech’など)を指定 LowerCaseFilter() / UpperCaseFilter() アルファベットを小文字 / 大文字に変換 POSKeepFilter() / POSStopFilter() 結果に含む / 結果から除外する品詞をリストで指定 TokenCountFilter() 出現回数をカウント バージョン0.3.5で追加

詳細は公式のAPIリファレンスを参照。

janome package — Janome API reference v0.3

以下の文字列を例として基本的な使い方を説明します。analyzerとcharfilter, tokenfilterをそれぞれインポートします。

from janome.tokenizer import Tokenizer
from janome.analyzer import Analyzer
from janome.charfilter import *
from janome.tokenfilter import *

t = Tokenizer()

s = '<div>PythonとPYTHONとパイソンとパイソン</div>'

for token in t.tokenize(s):
    print(token)
# < 名詞,サ変接続,*,*,*,*,<,*,*
# div   名詞,一般,*,*,*,*,div,*,*
# > 名詞,サ変接続,*,*,*,*,>,*,*
# Python    名詞,一般,*,*,*,*,Python,*,*
# と 助詞,並立助詞,*,*,*,*,と,ト,ト
# PYTHON    名詞,固有名詞,組織,*,*,*,PYTHON,*,*
# と 助詞,並立助詞,*,*,*,*,と,ト,ト
# パイソン  名詞,一般,*,*,*,*,パイソン,*,*
# と 助詞,並立助詞,*,*,*,*,と,ト,ト
# パイソン 名詞,一般,*,*,*,*,パイソン,*,*
# </    名詞,サ変接続,*,*,*,*,</,*,*
# div   名詞,一般,*,*,*,*,div,*,*
# > 名詞,サ変接続,*,*,*,*,>,*,*

CharFilterおよびTokenFilterをリストで指定してAnalyzerオブジェクトを生成し、analyze()メソッドに対象の文字列を渡します。リストの順番でフィルターが適用されるので気をつけてください。 全角を半角に変換し、正規表現によりHTMLタグを消去(空文字列で置換)、さらに名詞のみを抽出してアルファベットを小文字化、'surface'属性のみを抽出しています。

char_filters = [UnicodeNormalizeCharFilter(),
                RegexReplaceCharFilter('<.*?>', '')]

token_filters = [POSKeepFilter(['名詞']),
                 LowerCaseFilter(),
                 ExtractAttributeFilter('surface')]

a = Analyzer(char_filters=char_filters, token_filters=token_filters)

for token in a.analyze(s):
    print(token)
# python
# python
# パイソン
# パイソン

CompoundNounFilter()による複合名詞化の例を示します。

s = '自然言語処理による日本国憲法の形態素解析'

for token in t.tokenize(s):
    print(token)
# 自然    名詞,形容動詞語幹,*,*,*,*,自然,シゼン,シゼン
# 言語    名詞,一般,*,*,*,*,言語,ゲンゴ,ゲンゴ
# 処理    名詞,サ変接続,*,*,*,*,処理,ショリ,ショリ
# による   助詞,格助詞,連語,*,*,*,による,ニヨル,ニヨル
# 日本国   名詞,固有名詞,地域,国,*,*,日本国,ニッポンコク,ニッポンコク
# 憲法    名詞,一般,*,*,*,*,憲法,ケンポウ,ケンポー
# の 助詞,連体化,*,*,*,*,の,ノ,ノ
# 形態素   名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ
# 解析    名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ

a = Analyzer(token_filters=[CompoundNounFilter()])

for token in a.analyze(s):
    print(token)
# 自然言語処理    名詞,複合,*,*,*,*,自然言語処理,シゼンゲンゴショリ,シゼンゲンゴショリ
# による   助詞,格助詞,連語,*,*,*,による,ニヨル,ニヨル
# 日本国憲法 名詞,複合,*,*,*,*,日本国憲法,ニッポンコクケンポウ,ニッポンコクケンポー
# の 助詞,連体化,*,*,*,*,の,ノ,ノ
# 形態素解析 名詞,複合,*,*,*,*,形態素解析,ケイタイソカイセキ,ケイタイソカイセキ

Analyzerによる単語の出現回数のカウント

バージョン0.3.5で追加されたTokenCountFilter()を使うと単語の出現回数をカウントできます。 POSKeepFilter()など他のフィルターと組み合わせることが可能(ただしTokenCountFilter()は末尾に置く)。 (単語, 出現回数)のタプルをジェネレーターで返します。

s = '人民の人民による人民のための政治'

a = Analyzer(token_filters=[POSKeepFilter(['名詞']), TokenCountFilter()])

g_count = a.analyze(s)
print(type(g_count))
# <class 'generator'>

for i in g_count:
    print(i)
# ('人民', 3)
# ('ため', 1)
# ('政治', 1)

リスト化したい場合はlist()を使います。上述のcollections.Counterのmost_common()メソッドの返り値と同じ。

l_count = list(a.analyze(s))
print(type(l_count))
# <class 'list'>

print(l_count)
# [('人民', 3), ('ため', 1), ('政治', 1)]

dict()で辞書(dict型オブジェクト)に変換することもできます。

d_count = dict(a.analyze(s))
print(type(d_count))
# <class 'dict'>

print(d_count)
# {'人民': 3, 'ため': 1, '政治': 1}

辞書のキーを指定して単語の出現回数を取得できます。get()メソッドを使うと存在しない単語に対してもエラーにならない。

print(d_count['人民'])
# 3

# print(d_count['国民'])
# KeyError: '国民'

print(d_count.get('国民', 0))
# 0

TokenCountFilter()の引数attrでカウントする属性を指定できます。動詞の基本形をカウントしたい場合に便利です。

s = '走れと言われたので走ると言った'

a = Analyzer(token_filters=[TokenCountFilter()])

print(list(a.analyze(s)))
# [('走れ', 1), ('と', 2), ('言わ', 1), ('れ', 1), ('た', 2), ('ので', 1), ('走る', 1), ('言っ', 1)]

a = Analyzer(token_filters=[TokenCountFilter(att='base_form')])

print(list(a.analyze(s)))
# [('走る', 2), ('と', 2), ('言う', 2), ('れる', 1), ('た', 2), ('ので', 1)]

'part_of_speech'は細分類を含んだ文字列となります。品詞だけでカウントしたい場合は上述の内包表記を用いた方法を使います。

a = Analyzer(token_filters=[TokenCountFilter(att='part_of_speech')])

print(list(a.analyze(s)))
# [('動詞,自立,*,*', 4), ('助詞,格助詞,引用,*', 2), ('動詞,接尾,*,*', 1), ('助動詞,*,*,*', 2), ('助詞,接続助詞,*,*', 1)]

品詞指定の注意点

POSKeepFilter(), POSStopFilter()で品詞を限定したり除外する場合、ひとつだけ指定する場合もリストを使います。 公式ドキュメントでは文字列をそのまま指定している記述もあるが、想定外の結果となります。

s = '吾輩は猫である'

a = Analyzer(token_filters=[POSKeepFilter('助動詞')])

for token in a.analyze(s):
    print(token)
# は 助詞,係助詞,*,*,*,*,は,ハ,ワ
# で 助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
# ある    助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル

a = Analyzer(token_filters=[POSKeepFilter(['助動詞'])])

for token in a.analyze(s):
    print(token)
# で 助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
# ある    助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル

GitHubにissueを立てておいたのでそのうち対応してもらえるかもしれない。

Expected argument type for POSStopFilter() and POSKeepFilter() is unclear · Issue #52 · mocobeta/janome

Last Updated: 6/26/2019, 10:34:03 PM