機械学習初心者がナイーブベイズに手を出してみる (2) - 実装編 

## このページ

前回、 [機械学習初心者がナイーブベイズに手を出してみる (1) - 理論編](http://qiita.com/RINDO/items/fe4c32eddc3ed880433b) でナイーブベイズの理論 (数式)に関しての説明をしました。

このページでは、 Pythonを使って簡単な実装をしてみようと思います。 例によって参考にさせていただいたサイトを模倣して作っていますのでそれの解説 & 気になったとこの説明、ということになります。 ちなみにPythonも全く書いたことがないので、暖かい目で見守ってください..。

サンプルコードは一番下に載せておきます。

## 若干復習

ナイーブベイズの式は、以下のように表せました。

```math
P(cat|doc) = \log P(cat) + \prod_{i=0}^k \log P(word_k|cat)
```

プログラムにするとわからないもので...。先に必要なものを整理しておきます。

#### < P(cat) >

* 全文章数
* カテゴリのカウント `{'cat1' : 1, 'cat2' : 1, ...}`

#### < P(word|cat) >

* ユニークな全単語
* カテゴリに出てきた単語のカウント `{'cat1' : {'word1': 1, ...}, ...}`
 
#### < その他 > 

* ユニークなカテゴリ (分類のときに使いたい)

## 実装！

#### 方針

```python
nb = NaiveBayes()
nb.train(data)
```

上記のように使いたいので、Classとして設計します。 上記で必要と言ったものをinitで宣言してしまうと、 `__init__` までのコードは..

```python
class NaiveBayes:

    def __init__(self):
        self.vocabularies    = set() # 重複なしのカテゴリ
        self.categories      = set() # 重複なしの語彙
        self.category_count  = {}    # カテゴリの出現回数, category_count[cat]
        self.word_count      = {}    # カテゴリ毎の単語出現回数, word_count[cat]
	    ...
```

* `set()` は重複なしの配列です。 vocabularies, categoriesに使います。
* `{}` は辞書です。カテゴリの数, カテゴリ毎の単語の数のカウントに使います。

#### 学習まで

```python
def train(self, data):
    self.category_count = defaultdict(lambda: 0)

    for d in data:
        category = d[0]
        self.categories.add(category) # カテゴリの追加
        self.category_count[category] += 1

        for word in d[1:]: # 語彙の追加
            self.vocabularies.add(word)

    for category in self.categories:
        self.word_count[category]  = defaultdict(lambda: 0)

    for d in data:
        category = d[0]
        for word in d[1:]:
            self.word_count[category][word] += 1
```

これは下準備です。もう...あれです。

```python
from collections import defaultdict

hash = {}
hash = defaultdict(lambda: 0)
```

`defaultdict` を使うことで、値がなかったときの初期値を指定することができます。ラプラススムージングの件もあり悩んだのですが、計算するところで `+1` することにしました。

#### P(word|cat)

```math
\prod_{i=0}^k \log P(word_k|cat)
```

の各項を計算するために、 `word_probability` という関数を定義します。 この求め方は

```math
P(word_k|cat) = \frac{カテゴリ(cat)内での単語(word)の出現回数 + 1}{カテゴリ(cat)に出てきた単語の総数 + 総単語数}
```

でしたね。 ここで `ラプラススムージングを適用`しています。 

```python
def word_probability(self, word, category):
        '''単語が与えられた時のカテゴリである確率, P(word|cat)'''
        # ラプラススムージング を適用
        word_count       = self.word_count[category][word] + 1
        vocabulary_count = sum(self.word_count[category].values()) + len(self.vocabularies)
        return float(word_count) / float(vocabulary_count)
```

#### 文章に対してのスコアを計算する

`P(cat|doc)`を計算します。

```math
P(cat|doc) = \log P(cat) + \prod_{i=0}^k \log P(word_k|cat)
```

文章が与えられたときに上の関数を使って、各単語の確率の総積をとらないといけないのでスコアを計算する関数を作ります。  
ただし、 `同じ底を持つlogの掛け算は足し算になる` ので総積ではなく、総和になりますので注意してください。  
また、`logの底が10` なので計算するとマイナスの値になってしまいます。個人的に嫌だったので最後に `x (-1)` しています。どちらでもいいと思います。

まずは `score` に `P(cat)` を入れて、その後にループで `P(word|cat)` を足していってるイメージです。

```python
def score(self, words, category):
        '''文書(単語)が与えられたときのカテゴリである確率'''
        documents_count = sum(self.category_count.values())
        score = math.log(float(self.category_count[category]) / documents_count)

        for word in words:
            score += math.log(self.word_probability(word, category))

        # logの底が10なのでマイナスになるから+にしちゃう
        return score * (-1)
```

#### 分類する

さて、いよいよ分類です。 実際には、学習させた後はこの関数を呼ぶだけになると思います。

```python
def classify(self, words):
        '''P(cat|doc)が最も大きなカテゴリを返す'''
        best  = None
        value = 0

        for category in self.categories:
            v = self.score(words, category)
            if v > value:
                best  = category
                value = v
        return best
```

構造は単純で、学習段階で渡された全てのカテゴリに対してスコアを計算して一番高くなったものを返しています。

## 完全なコード

```python
#coding:utf-8

from collections import defaultdict
import math

class NaiveBayes:

    def __init__(self):
        self.vocabularies    = set() # 重複なしのカテゴリ
        self.categories      = set() # 重複なしの語彙
        self.category_count  = {}    # カテゴリの出現回数, category_count[cat]
        self.word_count      = {}    # カテゴリ毎の単語出現回数, word_count[cat]


    def train(self, data):
        self.category_count = defaultdict(lambda: 0)

        for d in data:
            category = d[0]
            self.categories.add(category) # カテゴリの追加
            self.category_count[category] += 1

            for word in d[1:]: # 語彙の追加
                self.vocabularies.add(word)

        for category in self.categories:
            self.word_count[category]  = defaultdict(lambda: 0)

        for d in data:
            category = d[0]
            for word in d[1:]:
                self.word_count[category][word] += 1


    def word_probability(self, word, category):
        '''単語が与えられた時のカテゴリである確率, P(word|cat)'''
        # ラプラススムージング を適用
        word_count       = self.word_count[category][word] + 1
        vocabulary_count = sum(self.word_count[category].values()) + len(self.vocabularies)
        return float(word_count) / float(vocabulary_count)


    def score(self, words, category):
        '''文書(単語)が与えられたときのカテゴリである確率'''
        documents_count = sum(self.category_count.values())
        score = math.log(float(self.category_count[category]) / documents_count)

        for word in words:
            score += math.log(self.word_probability(word, category))

        # logの底が10なのでマイナスになるから+にしちゃう
        return score * (-1)


    def classify(self, words):
        '''P(cat|doc)が最も大きなカテゴリを返す'''
        best  = None
        value = 0

        for category in self.categories:
            v = self.score(words, category)
            if v > value:
                best  = category
                value = v
        return best


if __name__ == "__main__":
    data = [["yes", "Chinese", "Beijing", "Chinese"],
            ["yes", "Chinese", "Chinese", "Shanghai"],
            ["yes", "Chinese", "Macao"],
            ["no", "Tokyo", "Japan", "Chinese"]]

    # ナイーブベイズ分類器を訓練
    nb = NaiveBayes()
    nb.train(data)

    print "P(Chinese|yes) = ", nb.word_probability("Chinese", "yes")
    print "P(Tokyo|yes) = ", nb.word_probability("Tokyo", "yes")
    print "P(Japan|yes) = ", nb.word_probability("Japan", "yes")
    print "P(Chinese|no) = ", nb.word_probability("Chinese", "no")
    print "P(Tokyo|no) = ", nb.word_probability("Tokyo", "no")
    print "P(Japan|no) = ", nb.word_probability("Japan", "no")
    #
    # # テストデータのカテゴリを予測
    test = ["Chinese", "Chinese", "Chinese", "Tokyo", "Japan"]
    print "log P(yes|test) =", nb.score(test, "yes")
    print "log P(no|test) =", nb.score(test, "no")
    print nb.classify(test)
```

## 参考

以下のページを大変参考にさせていただきました。ありがとうございました。

* [ナイーブベイズを用いたテキスト分類](http://aidiary.hatenablog.com/entry/20100613/1276389337)
* [[WIP]単純ベイズ分類器がまったく単純じゃないので入門](https://hogehuga.com/post-563/#3)