株式会社スマレジの開発部でスマレジのサーバサイドを作っています

チケットの見える化にチャレンジしてみる(2-2: 準備編 Janomeを使った形態素解析)

こんばんは!株式会社スマレジ 、開発部のmasaです。

前回、今回は数学のお話になりそうと言っていましたが、理論をする前に少し実装のイメージを記載した方が、あとで自分が読み返すときにわかりやすいかと思い直したので、今回は形態素解析の具体例として、Janomeを使ってredmine チケットの単語の頻度抽出をやってみます。

Janomeの概要については前回のブログをご覧ください。

masa2019.hatenablog.com

まずはソースコード

実装をまず上げて、そのあとで説明を加えていきます。

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

import json
import requests

char_filters = [UnicodeNormalizeCharFilter(), RegexReplaceCharFilter('<("[^"]*"|\'[^\']*\'|[^\'">])*>', u'')]
tokenizer = Tokenizer()
token_filters = [CompoundNounFilter(), POSKeepFilter(['名詞']), LowerCaseFilter(), TokenCountFilter(sorted=True)]
headers = {'X-Redmine-API-Key': "<キーの値>"}

params = {'project_id': 7}

response = requests.get("<Redmine APIのURL>", headers=headers, params=params)
text = response.text
resJson = json.loads(text)
issues = resJson["issues"]
sentence = ""
for issue in issues:
    sentence += issue["description"]

analyzeInfo = Analyzer(char_filters=char_filters, tokenizer=tokenizer, token_filters=token_filters)
surfaceList = []

for result in analyzeInfo.analyze(sentence):
    print(str(result))

ブロックワードについて

形態素解析を使って品詞の抜き出しをする際、頻繁に使われるのが名詞です。これはいわゆる「固有名詞」がその文章を特徴づけるデータとなることが多いためです。ところが、日本語の形態素解析の場合、半角の記号などは言語体系として存在しないため、名詞に分類されてしまうことが多いです。(特にhtmlを解析するときはhtmlタグが軒並み名刺扱いされることも)

こういった意図しないワードのヒットを避けるための概念として存在するのが「ブロックワード」、つまり単語のブラックリストになります。

上記の「文字」に関するフィルタリングはソースコード上の

char_filters = [UnicodeNormalizeCharFilter(), RegexReplaceCharFilter('<("[^"]*"|\'[^\']*\'|[^\'">])*>', u'')]

ここが該当します。UnicodeNormalizeCharFilter()で文字コードを指定して、RegexReplaceCharFilter('<("[^"]"|\'[^\']\'|[^\'">])*>', u'')でhtml形式のフィルタをかけています。

また、例えば弊社であれば「スマレジ 」という言葉は日常的に使われている言葉であり、Redmineチケットや車内の文書でこの言葉が頻繁に使われているからと言って、それが特別な意味を持つこともなく、逆に分析の上でノイズになってしまいます。

また、ブロックワードの概念とは少しズレますが、分析の目的に応じて、品詞を指定して抽出することは重要です。

まず、助詞・感嘆詞などは文書上極めて頻繁に使われるため、さっきの「スマレジ 」の例と同じくノイズになってしまうため、計量テキスト解析をする際は除外することが多いです。

こういった、文字に関するフィルタリングは下記の部分が該当します。また、この部分で頻度を計測する指定もしています。

token_filters = [CompoundNounFilter(), POSKeepFilter(['名詞']), LowerCaseFilter(), TokenCountFilter(sorted=True)]

ここでそれぞれのフィルタの意味についてはAPIリファレンスを見てください。

mocobeta.github.io

[補足]ブロックワードは分析の目的によってチューニングする

「動詞・形容詞・形容動詞」などの述部で使用される品詞は文書上の意味を断定する役割を持つため、分析の目的によって抽出対象にするかどうかを判断する必要があります。例えば「花が綺麗だ」と「花が醜い」では同じ「花」という名詞が主語に来ていても、意味が真逆になります。「花」に関する話題の量を計測したいなら、同じ重みでいいですが、「花」に向けられた意味を含めた計量をしたい場合、これらは別々に計量しなければいけません。(当たり前のことなんですが、何度も実施していると分析麻痺になることもあるので、意識は常に持っておくのが良いと思います。)

これは、個人的な意見になりますが、業務で実施するのであればまずは名詞だけに限定して、分析対象を広く取って、 固有名詞の分布状況の把握から行うのが良いと思います。最も手軽ですし、計算負荷も軽量であることが多いためです。 名刺のみの分析結果から導き出される結論は、人間の認識からずれることも少なく、新たな発見にはつながりづらいですが、 プログラムのバグを潰すためのある種の教師データにもなり得ますし、本当にズレがあったのならそれはチケット状のやり取りと人間の認識が大きくずれていることを示すため、重大な事象を示している可能性もあるためです。

Redmine APIをコールする

これは過去のブログでも扱ったことがあるので、該当コードの部分だけ示します。

過去のブログは↓

masa2019.hatenablog.com

該当するソースはここ。今回はproject_idが7のチケットを全て取ってくるように指定しています。 その上で、渡ってきたテキスト情報をjsonにデコードして辞書型で取り込んで、issueのみ取り出しています。 そのあとで、チケットの本文部分を全てつなげてとってきています。

headers = {'X-Redmine-API-Key': "<キーの値>"}

params = {'project_id': 7}

response = requests.get("<Redmine APIのURL>", headers=headers, params=params)
text = response.text
resJson = json.loads(text)
issues = resJson["issues"]

sentence = ""
for issue in issues:
    sentence += issue["description"]

ブロックワードを適応させ、分析を実行する

ここも対応するソースコードだけ。詳しく知りたい人は上記のAPIリファレンスを見てみて下さい。

analyzeInfo = Analyzer(char_filters=char_filters, tokenizer=tokenizer, token_filters=token_filters)

for result in analyzeInfo.analyze(sentence):
    print(str(result))