読者です 読者をやめる 読者になる 読者になる

PGは電子羊の夢を見るのか?

データとパターンのあいだ

Python3 + Mecabでベイジアンフィルタを実装してみた。

技評さんの過去の機械学習関連の連載の中に「ベイジアンフィルタを実装してみよう」というものがあります。
ちょっとした理由からベイジアンフィルタの実装をしなければいけなくなったので、勉強を兼ねて上記のリンクのものを形態素解析エンジン「MeCab」を使いPython3で書きなおしてみた。(元記事では形態素解析にはYahoo!APIを使用し、Pythonも2.X系で実装されている。)

まず、そもそも「ベイジアンフィルタとは何か?」みたいな話はここでは詳しくはしないけれど、ざっくりと言うと「主観によって変動するベイズ確率を用いたフィルタ」のこと。

早速実装。
環境は以下のとおり。

上記サイトに習って、文章を形態素解析する部分は単独のクラスとして実装。
今回はMeCabを使っているので、「mecabutil.py」としています。

# coding: utf-8
import MeCab

mode = '-Ochasen'
word_classes = ['名詞', '動詞', '形容詞']


def getwords(sentence):
    tagger = MeCab.Tagger(mode)
    tagger.parse('')
    node = tagger.parseToNode(sentence)
    words = []
    while node:
        if node.feature.split(',')[0] in word_classes:
            words.append(node.surface)
        node = node.next

    return words

上2行はおまじないとインポート。

今回は「特定の品詞の単語のみを抽出したい!」と思ったので、単語ごとに品詞を拾ってこれるchasenを採用。 あと、word_classesという配列にほしい品詞を入れておきます。

getwords関数について。 まずはMeCab.Taggerに先ほどのmodeを渡してtaggerを作ります。 その次の行はmecab-python3のバグ対応のため。(参考: python3 + mecabでnode.surfaceが取得できないバグへの対応 - Qiita

tagger.parseToNode()に文章を渡すことで、含まれる単語とその品詞の情報を持ったnodeの一覧を返してくれます。 あとは、これをループで回して含まれるのnodeを一つ一つ確認していく。

node.featureは「名詞,一般,,,,,*」という形式の文字列を持っているので、コンマで分割した時の先頭を見れば品詞が取ってこれます。 これがword_classesに含まれる品詞だった場合には出力となるwordsの中に格納。

最後にnodeを次のnodeへと進めます。 最終的にすべてのnodeを見終わると、node=nullになってwhileループを抜けます。

以上が今回の単語抽出部です。

次に、ベイジアンフィルタ部分。 これは上記リンクとほぼ同じです。(一部バグ修正やリファクタリングあり。)

# coding: utf-8

import math
import sys
import mecabutil


def getwords(doc):
    words = [s.lower() for s in mecabutil.getwords(doc)]
    return tuple(w for w in words)


class NaiveBayes:
    def __init__(self):
        self.vocablaries = set()
        self.wordcount = {}
        self.catcount = {}

    def wordcountup(self, word, cat):
        self.wordcount.setdefault(cat, {})
        self.wordcount[cat].setdefault(word, 0)
        self.wordcount[cat][word] += 1
        self.vocablaries.add(word)

    def catcountup(self, cat):
        self.catcount.setdefault(cat, 0)
        self.catcount[cat] += 1

    def train(self, doc, cat):
        for w in getwords(doc):
            self.wordcountup(w, cat)
        self.catcountup(cat)

    def priorprob(self, cat):
        return float(self.catcount[cat]) / sum(self.catcount.values())

    def incategory(self, word, cat):
        if word in self.wordcount[cat]:
            return float(self.wordcount[cat][word])
        return 0.0

    def wordprob(self, word, cat):
        return (self.incategory(word, cat) + 1.0) / (sum(self.wordcount[cat].values()) + len(self.vocablaries) * 1.0)

    def score(self, word, cat):
        score = math.log(self.priorprob(cat))
        for w in word:
            score += math.log(self.wordprob(w, cat))
        return score

    def classifier(self, doc):
        best = None
        max = -sys.maxsize
        word = getwords(doc)

        for cat in self.catcount.keys():
            prob = self.score(word, cat)
            if prob > max:
                max = prob
                best = cat
        return best

Python2 -> Python3に変更したけれど、大きな修正はなかったので簡単に移植できました。 こちらの実装内容についてはベイジアンフィルタに関する理解も必要になるので、上記リンクに任せます。 しばらくはデータを突っ込んでみて遊んでみようと思います。

実装リンク

github.com

参考リンク

d.hatena.ne.jp