Janomeで形態素解析してみる

Janome


前回に引き続きPythonネタ。

最近始めた業務でちょぃちょぃ話題になるのが「テキストマイニング」やら「自然言語解析」。
話には聞いて事あるし、理屈はわかっているつもりだけど、
実際にはどうやるんだろ?と思い、試してみた。
元々興味もあったしね。

やろうとしたこと

大量の問い合わせ記録から、どう言ったジャンルの問い合わせが多いのか傾向分析したかった。
使用したツールはJanomeとword2Vec。
結果的に傾向とかはよくわからなかったんだけど、
とりあえず、Word2Vecで文章をベクター化するとこまではできた。
そこまでの手順を書き起こしておく。

日本語を単語ごとに分割してみる

日本語で言語解析を始める前についてまわるのが
「形態素解析」と言う単語分割。
英語と違い、日本語は単語が分かれていないんで、
分析する前に文章を単語ごとに分割しないといけない。

形態素解析にはいくつか有名なツールがあるんだけど、今回は「Janome」を使用。
「Mecab」も使ってみたけどだいたい同じ感じ。
ただ、環境構築に若干クセがあるようで、職場のPCではうまく動かなかった。
Janomeはその点お手軽で、安定して動作しているようなので採用。

環境構築とサンプルコード

まずは、Janomeをインストール。

>pip instrall janome

次にサンプルコードで簡単な形態素解析をしてみる。
[code language=”python”]
from janome.tokenizer import Tokenizer
t = Tokenizer()
for token in t.tokenize(u’すもももももももものうち’):
print(token)
[/code]

すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も    助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
も    助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
の    助詞,連体化,*,*,*,*,の,ノ,ノ
うち  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ

形態素解析するならこれだけ。
かなり簡単。
ただ、実際に解析するにはもう少し手を加える必要がある。

ユーザー辞書を読み込ませる

例えば先ほどのサンプルコードで、こんな文章を解析させると・・・
[code language=”python”]
from janome.tokenizer import Tokenizer
t = Tokenizer()
for token in t.tokenize(u’今日は国会議事堂に行った。’):
print(token)
[/code]

今日	名詞,副詞可能,*,*,*,*,今日,キョウ,キョー
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
国会	名詞,一般,*,*,*,*,国会,コッカイ,コッカイ
議事堂	名詞,一般,*,*,*,*,議事堂,ギジドウ,ギジドー
に	助詞,格助詞,一般,*,*,*,に,ニ,ニ
行っ	動詞,自立,*,*,五段・カ行促音便,連用タ接続,行く,イッ,イッ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
。	記号,句点,*,*,*,*,。,。,。

「国会議事堂」が「国会」と「議事堂」に分かれてしまっている。
そのため、「国会議事堂」がひとつの単語だと言うことを教えてやる必要がある。
それを教えるためのファイルが「ユーザー辞書」である。

JanomeはMecabと同じフォーマットのユーザー辞書を読み込ませることが可能。
ただ、Mecabのフォーマットは項目も多くて作るのが大変。
そんなものぐさな私のために、「簡易辞書」と言うフォーマットも用意されている。
以下のような「<表層形>,<品詞>,<読み>」のフォーマットでcsvファイルを用意しておく。

user_simpledic.csv
[code language=”python” gutter=”false”]
国会議事堂,名詞,コッカイギジドウ
[/code]
Tokenizerの初期化時に上記のファイルを読み込ませて解析すると・・・
[code language=”python”]
from janome.tokenizer import Tokenizer
t = Tokenizer("userdict.csv", udic_type="simpledic", udic_enc="utf8")
for token in t.tokenize(u’今日は国会議事堂に行った。’):
print(token)
[/code]

今日		名詞,副詞可能,*,*,*,*,今日,キョウ,キョー
は		助詞,係助詞,*,*,*,*,は,ハ,ワ
国会議事堂	名詞,*,*,*,*,*,国会議事堂,コッカイギジドウ,コッカイギジドウ
に		助詞,格助詞,一般,*,*,*,に,ニ,ニ
行っ		動詞,自立,*,*,五段・カ行促音便,連用タ接続,行く,イッ,イッ
た		助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
。		記号,句点,*,*,*,*,。,。,。

「国会議事堂」が単語として認識された。

前処理と後処理のためのAnalyzer

単純に文章を形態素解析した結果を分析しても、
様々な問題があり、うまくいかないケースがある。
具体的な例は後述するが、
これらを解消するためのAnalyzerという機能がJanomeには用意されている。
[code language=”python”]
char_filters = []
tokenizer = Tokenizer()
token_filters = []
analyzer = Analyzer(char_filters, tokenizer, token_filters)
for token in analyzer.analyze(text):
print(token)
[/code]
char_filtersに前処理、token_filtersに後処理をリストにして登録することで、
analyze時にセットで実行してくれる。
では、具体的にどんな前処理を行うか?

Unicode正規化を行うUnicodeNormalizeCharFilter

前処理として文字列の比較をしやすいようにUnicode正規化をする。
[code language=”python”]
char_filters.append(UnicodeNormalizeCharFilter())
[/code]
正規化形式は「form」オプションで指定する。デフォルトは「NFKC」。

表記ゆれを抑えるため、RegexReplaceCharFilterで置換する

人によって表記の仕方が違ったり、言い回しがちがうと、
分析がうまくいかないケースがある。
例えば、同じ「ファックス」について記載しているのに
・ファックス
・ファクス
・ファクシミリ
・fax
・Fax
・FAX
など、書き方が違う。
このまま出現頻度をカウントするとそれぞれ別にカウントされてしまう。
そこで単語を置換し、表記ゆれを抑える。
[code language=”python”]
char_filters.append(RegexReplaceCharFilter("ファックス", "fax"))
[/code]
実際にはCSVファイルにして読み込ませることで、簡単に追加できるようにしておく。

他にも電話番号にマスクかけたりするのも
RegexReplaceCharFilterでできる。
こんな感じ。
[code language=”python”]
char_filters.append(RegexReplaceCharFilter("\d{3}[-\(]\d{4}[-\)]\d{4}", "000-0000-0000"))
[/code]

品詞を絞り込むPOSStopFilter

解析の仕方によっては、余計な品詞を取り除きたい場合がある。
何について文章かを掴むだけなら、名詞と動詞を抜き出せばいいし、
ネガティブ/ポジティブ判定をしたいなら助動詞も必要になる。
なんでそうなるかについてはこちらのサイトがわかりやすかった。
自然言語処理の必要最低限の知識だけ簡単にまとめる(1)/前処理としての形態素解析+pythonサンプル

理由はさておき、品詞を絞り込むには
POSStopFilterにリスト形式で出力したくない品詞を指定する。
[code language=”python”]
token_filters.append(RegexReplaceCharFilter(POSStopFilter(["記号","助詞","接頭詞"]))
[/code]

ストップワードを除外するため、Filterを自作する

分析に邪魔な単語を解析結果から除外するためFilterを自作してみる。
RegexReplaceCharFilterと同様に、正規表現と置換後の文字列をパラメータに分割したtokenを置換する。
置換した結果が空文字になった場合は返却しないようにすることで、
ストップワードを除外している。
[code language=”python”]
class RegexReplaceTokenFilter(TokenFilter):
__fromword = None
__toword = None
def __init__(self, fromword, toword):
self.__fromword = fromword
self.__toword = toword

def apply(self, tokens):
for token in tokens:
token.surface = re.sub(self.__fromword, self.__toword, token.surface)
if token.surface == "":
continue
yield token
[/code]
ストップワードに「hoge」を追加する場合
[code language=”python”]
token_filters.append(RegexReplaceTokenFilter("hoge", "))
[/code]

その他のFilter

その他のFilterについては以下のサイトが参考になる。
本家からもリンクされてた。
け日記:Python janomeのanalyzerが便利

上記を踏まえ、クラス化する

実際に解析する時は、データをファイルから読み込んで、1行ずつ形態素解析する感じになる。
使いやすいようにクラス化しておきたいところ。
[code language=”python”]
import re
from janome.tokenizer import Tokenizer
from janome.analyzer import Analyzer
from janome.charfilter import *
from janome.tokenfilter import *

# ストップワード除去用TokenFilter
class RegexReplaceTokenFilter(TokenFilter):
__fromword = None
__toword = None
def __init__(self, fromword, toword):
self.__fromword = fromword
self.__toword = toword

def apply(self, tokens):
for token in tokens:
token.surface = re.sub(self.__fromword, self.__toword, token.surface)
if token.surface == "":
continue
yield token

# 形態素解析
class WordSplit:

# ファイル定義
__synonym_list = "/data/synonym_list.csv"
__stopwords_list = "/data/stopword_list.txt"
__userdict = "/data/userdict.csv"

# メンバ
__token_filters = []
__char_filters = []
__tokenizer = None
__Analyzer = None

def __init__(self):
self.c = self

#前処理Filter初期化
#unicodeの正規化
self.__char_filters.append(UnicodeNormalizeCharFilter())
#英字の小文字化
self.__char_filters.append(LowerCharFilter())

#シノニムリスト読み込み
with open(self.__synonym_list, mode="r", encoding="utf-8") as f:
for row in f:

# コメントアウト除外
row = re.sub(r"^#.*&lt;pre wp-pre-tag-15=""&gt;&lt;/pre&gt;quot;, "", row).rstrip()

if not re.match(r"^\s*&lt;pre wp-pre-tag-15=""&gt;&lt;/pre&gt;quot;, row):
# 分割
synonym = row.split(",")
if len(synonym) == 2:
from_word = re.sub(r"^\"|\"&lt;pre wp-pre-tag-15=""&gt;&lt;/pre&gt;quot;, "", synonym[0])
to_word = re.sub(r"^\"|\"&lt;pre wp-pre-tag-15=""&gt;&lt;/pre&gt;quot;, "", synonym[1])
#前処理Filterに追加
self.__char_filters.append(RegexReplaceCharFilter(from_word, to_word))

#後処理用Filter初期化
# 品詞絞込み
self.__token_filters.append(POSStopFilter(["記号","助詞","接頭詞"]))
#小文字化
self.__token_filters.append(LowerCaseFilter())

#ストップワードリスト読み込み
self.__stopwords = []
with open(self.__stopwords_list, mode="r", encoding="utf-8") as f:
for stopword in f:

# コメントアウト除外
stopword = re.sub(r"^#.*&lt;pre wp-pre-tag-15=""&gt;&lt;/pre&gt;quot;, "", stopword).rstrip()

if not re.match(r"^\s*&lt;pre wp-pre-tag-15=""&gt;&lt;/pre&gt;quot;, stopword):
#後処理用Filterに追加
self.__token_filters.append((RegexReplaceTokenFilter(stopword, "")))

#Tokenizer初期化
self.__tokenizer = Tokenizer(self.__userdict, udic_type="simpledic", udic_enc="utf-8", mmap=True)

#Analyzer初期化
self.__Analyzer = Analyzer(self.__char_filters, self.__tokenizer, self.__token_filters)

# 実解析処理
def tokenize(self, target):
return self.__Analyzer.analyze(target)
[/code]
呼び出し元はこんな感じかな?
[code language=”python”]
def main():
ws = WordSplit()
with open(input_file, mode="r", encoding="utf-8") as f:
# input_file読み込み
for row in f:
# 文字分かち
for word in ws.tokenize(row):
# 分かち結果を出力
print(word.surface)
[/code]
これで解析の下準備ができた感じ。
次回は実際に解析してみよう。

参考サイト:Janome v0.3 documentation (ja)

コメント

タイトルとURLをコピーしました