note.nkmk.me

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

Date: 2018-06-30 / tags: Python, 文字列操作
このエントリーをはてなブックマークに追加

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

ここでは以下の内容について説明する。サンプルコードで使っている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()メソッドの引数streamTrueとするとストリーミングモードとなり、リストではなくジェネレーターを返す。リスト全体を保持する必要がなくTokenを逐次的に処理する場合に使う。

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

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

Tokenオブジェクトの属性

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

以下の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()メソッドの引数wakatiTrueとするとTokenオブジェクトのリストではなく表層形の文字列strのリストを返す。

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

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

t_wakati = Tokenizer(wakati=True)

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

リスト内包表記

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

リスト内包表記については以下の記事を参照。

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

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

base_formpart_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リファレンスを参照。

以下の文字列を例として基本的な使い方を説明する。analyzercharfilter, 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.Countermost_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を立てておいたのでそのうち対応してもらえるかもしれない。

スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事