田舎で並行処理の夢を見る

試したこと、需要がないかもしれないけど細々とアウトプットしてます

【サンプルコード有り】形態素解析をササッと試すならMecabよりもJanomeが良い感じ

Janomeとは

mocobeta.github.io

Pure Python(通常のpythonのみ)で書かれている日本語の形態素解析のためのパッケージです。自然言語処理で主に行われる形態素解析を気軽に行うことが出来ます。そもそも「形態素解析って何?」という方のために形態素解析結果をお見せしますと以下のようになります。

(B'z 今宵月の見える丘により)

手をつないだら行ってみよう

形態素解析の結果

手 名詞,一般,*,*,*,*,手,テ,テ  
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ  
つない 動詞,自立,*,*,五段・ガ行,連用タ接続,つなぐ,ツナイ,ツナイ  
だら  助動詞,*,*,*,特殊・タ,仮定形,だ,ダラ,ダラ  
行っ  動詞,自立,*,*,五段・カ行促音便,連用タ接続,行く,イッ,イッ  
て 助詞,接続助詞,*,*,*,*,て,テ,テ  
みよ  動詞,非自立,*,*,一段,未然ウ接続,みる,ミヨ,ミヨ  
う 助動詞,*,*,*,不変化型,基本形,う,ウ,ウ  

Janomeではこのような結果が返ってきます。形態素解析の対象とした文章(今回は「手をつないだら行ってみよう 」)を品詞ごとに区切り、それぞれの品詞の情報を教えてくれると言えば良いでしょうか。自分は古典の活用形を覚えるのが苦手だったので、形態素解析結果のそれぞれの品詞がどのような特性を持つのかは全く分かりませんが、重要なのは形態素解析パッケージによって文章を品詞に分解出来る」という点です。

僕自身は元々、Mecabという形態素解析エンジンものを使っていましたが、必要な外部パッケージのインストール、ユーザー辞書の準備がそこそこ面倒で毎度、環境構築するたびに結構苦しめられました。
そしたら何とpipのみでササッとインストールできるJanomeを教えてもらいまして、開発環境の準備がめちゃ楽になりました。

とはいいつつも、Janomeの内部ではMecabipadicを使っているようなので、僕の苦労をJanomeが負担してくれていることですね。
圧倒的感謝...

Janomeをインストールする

Pythonはインストール済みであり、ver3.4以上を想定しています

(すでにJanomeがインストール済みの方はこの章をすっとばして下さい)

仮想環境を作成するためのpythonの標準パッケージである、venvを使用してプロジェクト仮想環境を作りJanomeをインストールする場合は以下を参照下さい。ローカル環境にグローバルにJanomeをインストールはこの項目を飛ばしてください。特に理由がなければローカル環境を可能な限りクリーンに保つために仮想環境を作成したインストールをオススメします。

venvを使って仮想環境を作成してJanomeをインストール

まずは適当な名前でディレクトリを作成します。

$ mkdir janome_sample
$ ls

janome_sample

作成したディレクトリの内部に移動します。

$ cd janome_sample

venvを使って仮想環境を作成しましょう。.venvの部分は任意の名前で問題ありません。

$ python3 -m venv .venv

無事に仮想環境が作成されたかを確認します。

$ ls -a | grep ".venv"

.venv

仮想環境を立ち上げて、pipを使用して、Janomeをインストールします。

$ source .venv/bin/activate (.venv) $ pip install janome

Collecting janome
  Cache entry deserialization failed, entry ignored
  Downloading https://files.pythonhosted.org/packages/79/f0/bd7f90806132d7d9d642d418bdc3e870cfdff5947254ea3cab27480983a7/Janome-0.3.10-py2.py3-none-any.whl (21.5MB)
    100% |████████████████████████████████| 21.5MB 53kB/s
Installing collected packages: janome
Successfully installed janome-0.3.10

無事にインストールされたかを確認します。

$ pip freeze

Janome==0.3.10

venvで作成した仮想環境にpip経由でJanomeがインストールされていることを確認出来ました。

ローカル環境にグローバルにインストール

$ pip install janome

or

$ pip3 install janome

たったこれだけ、もう終わり。Mecabが使えるようになるまでのステップと比べると圧倒的に楽です。
qiita.com

形態素解析をするまで

全体のコード(jupyter notebook)をgitに上げていますのでご参照下さい。
github.com

最も基本的な部分のみを試してみます。

# janomeのTokenizerをインポート
from janome.tokenizer import Tokenizer

t = Tokenizer()
res = t.tokenize("手をつないだら行ってみよう")
print(res)

#result: この時点で結果が思った様に表示されない
# [<janome.tokenizer.Token at 0x112151710>,
# :
# :
#  <janome.tokenizer.Token at 0x112151908>]

forを使えば先ほどの様な解析結果を覗くことが出来ます。

for r in res:
    print(r)

#result:
# 手  名詞,一般,*,*,*,*,手,テ,テ
# を  助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
# つない    動詞,自立,*,*,五段・ガ行,連用タ接続,つなぐ,ツナイ,ツナイ
# だら   助動詞,*,*,*,特殊・タ,仮定形,だ,ダラ,ダラ
# 行っ   動詞,自立,*,*,五段・カ行促音便,連用タ接続,行く,イッ,イッ
# て  助詞,接続助詞,*,*,*,*,て,テ,テ
# みよ   動詞,非自立,*,*,一段,未然ウ接続,みる,ミヨ,ミヨ
# う  助動詞,*,*,*,不変化型,基本形,う,ウ,ウ

特定の品詞に該当する単語だけを抽出する

解析結果の配列(先ほどの例でいうres)に対して.part_of_speechメゾットを呼び出すことで、それぞれの単語の品詞を確認する事が出来ます。

res[0].part_of_speech
# '名詞,一般,*,*'

この.part_of_speechの戻り値('名詞,一般,*,*'という情報)はただの文字列なので,でsplitして、内包表記で上手く処理してあげることで各単語の品詞情報だけを取得する処理を記述する事が出来ます。

[f.part_of_speech.split(",")[0] for f in res]
# ['名詞', '助詞', '動詞', '助動詞', '動詞', '助詞', '動詞', '助動詞']

自然言語処理の前処理で頻出するのが名詞だけ取得したい、名詞形容詞だけ取得したいという作業です。特定の品詞だけを取り出しつつ .surfaceメゾットを使って解析結果から該当する品詞の単語の一覧を取得します。

#1つの品詞だけ
[token.surface for token in res if token.part_of_speech.split(",")[0] == u"名詞"]
# ['手']

#複数の品詞
target = [u"名詞", u"動詞"]
[token.surface for token in res if token.part_of_speech.split(",")[0] in target]
# ['手', 'つない', '行っ', 'みよ']

可能か限り、内包表記を使用して記述しているのは少しでも速度を落とさないようにするためです。この手の自然言語処理での前処理は何万というデータに対して行うことが多いため、極力速度が落ちるような記述(通常のfor文など)は避けています。

良い感じですね。使い勝手がいいように関数化しておきましょう。

from janome.tokenizer import Tokenizer

def parser(value_str, tag=u"名詞"):
    t = Tokenizer()
    res = t.tokenize(value_str)
    if isinstance(tag, list):
        return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
    else:
        return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]
    
parser("手をつないだら行ってみよう")
#result: ['手']

この関数はこのままでは、呼び出されるたびにTokenizerインスタンスを作ってしまい、パフォーマンスが悪そうなので一度だけTokenizerインスタンスを作れば良いように、クラス内の関数にしておきます。

class JanomeParser:
    def __init__(self):
        self.t = Tokenizer()
        
    def parser(self, value_str, tag=u"名詞"):
        res = self.t.tokenize(value_str)
        if isinstance(tag, list):
            return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
        else:
            return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]
        
j = JanomeParser()
j.parser("手をつないだら行ってみよう")
#result: ['手']

これでクラス化も完了しました。

ユーザー辞書の作成と読み込み

Mecab.csv形式で作成したファイルを.dic形式にcompileする必要がないため、pandasを使って簡単にユーザー辞書を作成することが出来ます。以下のコードは単語のリスト([]string)からJanome指定の.csv形式のファイルを作成する関数になります。

コピペで使えます~

# pandasがインストールされていない場合はインストールしてください
# > pip install pandas
import pandas pd

def create_user_dic(words, dic_name):
    """
      TASK: 指定された単語群からJanome指定のユーザー辞書.csvファイルを作成する
      words: []string -> 辞書に登録したい単語群
      dic_name: string -> __.csvに該当するファイル名
      return void
    """
    df = words_to_df(words)
    df = to_janome_csv_style(df)
    save_df_to_csv(df, dic_name)

def words_to_df(words):
    """
      TASK: 単語リストをpandas.DataFrame形式に変換する
      wrods: []string -> 対象の単語群
      return pandas.DataFrame
    """
    to_df_list = [[w] for w in words]
    return pd.DataFrame(words, columns=["単語"])

def to_janome_csv_style(df):
    """
      TASK: 読み込まれたdfをjanomeが指定するユーザー辞書.csv形式に変換する
      df: pandas.DataFrame -> 単語リストから生成されたdf
      return pandas.DataFrame
    """
    return df.assign(
    a=df.pipe(lambda df: -1),
    b=df.pipe(lambda df: -1),
    c=df.pipe(lambda df: 1000),
    d=df.pipe(lambda df: "名詞"),
    e=df.pipe(lambda df: "一般"),
    f=df.pipe(lambda df: "*"),
    g=df.pipe(lambda df: "*"),
    h=df.pipe(lambda df: "*"),
    i=df.pipe(lambda df: "*"),
    j=df.pipe(lambda df: df["単語"]),
    k=df.pipe(lambda df: "*"),
    l=df.pipe(lambda df: "*"),
)

def save_df_to_csv(df, file_name):
    """
      TASK: dfを.csv形式で保存する
      df: pandas.DataFrame -> to_janome_csv_style()の戻り値のdf
      file_name: ファイルの保存名(拡張子は含まない)
      return void
    """
    df.to_csv(f"{file_name}.csv", header=False, index=False, encoding="cp932")

使い方は簡単でcreate_user_dicに辞書として登録したいワードとファイル保存名(.csvを除いた)を渡すだけになります。

words = ["果てない思い", "ウルトラソウル"]
create_user_dic(words, "B'z_dic")

関数を実行したカレントディレクトリに.csv形式のファイルが生成されているかと思います。

$ ls

"B'z_dic.csv"

これでユーザー辞書の作成は完了です。

ユーザー辞書の読み込み

ユーザー辞書が読み込まれていない状態だと以下のような結果になります。

# 名詞だけを取得
parser("果てない思い")
# ['思い']

parser("ウルトラソウル")
# ['ウルトラ', 'ソウル']

せっかくユーザー辞書を作成したので読み込むようにしましょう。めちゃくちゃ簡単です。Tokenizerインスタンスの生成時に、ユーザー辞書.csvのパスを引数に指定するだけで読み込まれます。

# 作成したユーザー辞書へのパス
USER_DIC_PATH = "B'z_dic.csv"

# 簡略化のため定数値からユーザー辞書を指定
def parser(value_str, tag=u"名詞"):
    t = Tokenizer(USER_DIC_PATH, udic_enc="cp932")
    res = t.tokenize(value_str)
    if isinstance(tag, list):
        return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
    else:
        return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]

実行結果

parser("果てない思い")
# ['果てない思い']
parser("ウルトラソウル")
# ['ウルトラソウル']

ユーザー辞書を読み込むように更新して、単語が分割されないようになりました。無事にユーザー辞書の読み込みに成功しているということになります。コード量が多くなってしまうため、ブログの方には貼りませんが、jupyter notebook内にてインスタンス生成時に、ユーザー辞書のパスを指定するものも作成しましたので、よければ覗いて見てください。

github.com

まとめ

Janomeはいかがでしたか。インストールも簡単で使い方もシンプル。特に使用するまでに外部のパッケージをあれこれいれる必要がなく、「形態素解析」の実行にまで非常にスムーズに辿り着くことが出来ます。速度に関してはMecabには劣ってしまうのですが、形態素解析を少し試した時にはJanomeが非常に良いのではないでしょうか。

こちらの記事では紹介していないJanomeの様々な便利機能がまだまだあり、こちらで非常に丁寧に紹介されていますので、ぜひご覧ください。私も非常に勉強させて頂きました。

note.nkmk.me

おまけ: 関数とクラス内関数で実行速度を比較

1万回の実行結果の平均値を比べてみました。コードはjupyter notebookの一番下のところにあります。割と量が多くなってしまったのでリンクを再度、貼っておきます。

実行形式 試行回数 平均値
関数 10,000 0.08182s
クラス内関数 10,000 0.08891s

速度に関しては若干、関数形式の方が勝る結果もありましたが、ほとんど誤差といっても良いかと思います。実際は速度以外にもメモリ使用率の問題などもあるので、一概にこの結果だけからどちらが良いかとは言えませんが、速度上では関数形式の方が速いということが分かりました。

参考文献