機械学習 はじめよう

第3回 ベイジアンフィルタを実装してみよう

この記事を読むのに必要な時間:およそ 7 分

ベイジアンフィルタの実装

ここから本格的にベイジアンフィルタの実装に入っていきます。

その前に,まずは先程のリスト1のコードを利用して入力された文章をわかち書きし,単語の集合を返す関数を作成しnaivebayes.pyとして保存しましょう。こちらも先程のmorphological.pyと同様にutf-8で保存してください。

リスト2 文章の分割をする関数(naivebayes.py)

# -*- coding: utf-8 -*-
import math
import sys
#yahoo!形態素解析
import morphological

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

それでは,これから訓練と推定を行なう NaiveBeyse クラスを実装していきましょう。訓練フェーズでは入力された文章とカテゴリからナイーブベイズに必要な値に変換し,クラス内部に保持します。

具体的には確率値を計算することになるため,カテゴリの登場回数や単語の出現回数などを保持することになります。この訓練フェーズにより,入力された文章のカテゴリを推定することが可能になります。

そして,推定フェーズでは入力された単語が各カテゴリでどの程度出現しているかを訓練フェーズで保存した情報を元に計算します。訓練の結果に対して最も高い確率だったカテゴリが推定結果となります。

ナイーブベイズのアルゴリズム

ここで,ナイーブベイズのアルゴリズムを確認しましょう。ナイーブベイズは確率論に基づいたアルゴリズムになりますので簡単な確率式を使って説明していきます。

ナイーブベイズにおいてのカテゴリの推定とは,ある文章が与えられた時にその文章がどのカテゴリに属するのが「もっともらしいか」を単語の出現確率から求めます。この「もっともらしい」度合いを統計学などでは尤度(ゆうど)と呼びます。

今回は文章(doc)が与えられた時,カテゴリ(cat)に属する確率 P(cat|doc) を求める問題になります。入力された複数のカテゴリの中でもっとも高い値となったカテゴリが最終的に選択されることになります。この条件付き確率を直接計算するのは難しいのですが,⁠ベイズの定理」を使うことにより簡単に計算することができます。

前回と同じように確率の乗法定理を使って展開してみましょう。

P(cat, doc)
= P(cat|doc)P(doc)
= P(doc|cat)P(cat)

第2式と第3式を使うことで,P(cat|doc) は以下の式で求められることが分かります。

P(cat|doc) = P(doc|cat)P(cat) / P(doc)

カテゴリの推定では,具体的な確率の値ではなく与えられた複数のカテゴリの中でどのカテゴリになる確率が一番高い順位であるかという情報が必要になります。分母の P(doc) は文章が生起※1する確率になりますが,こればどのカテゴリでも同じ値になるので今回は分子を比較するだけで事足ります。

では,次に分子の P(doc|cat)P(cat) について考えてみましょう。P(cat) はカテゴリが生起する確率になります。ここでも色々なモデルを考えることができますが,今回は「訓練データとしてカテゴリが与えられた件数/総文書数」で求めることにしましょう。そのため,ナイーブベイズの訓練フェーズではカテゴリの登場回数を保存しておけばよいのです。

次に P(doc|cat) ですが,これはあるカテゴリが与えられた時の文章の生起確率になります。ここで先ほど文章をbag-of-wordsの形にしたことを思い出してください。単語をbag-of-wordsの形にすることで,文脈や単語の出現位置を無視しましたよね。本来は「python」「プログラム」と共起※2しやすいし「機械」「学習⁠⁠」も共起しやすいはずです。 しかし,これを無視して単語の独立性を仮定します。この大胆な仮定により,文章の確率を単語の確率の積として単純化するのがナイーブベイズを「ナイーブ(単純な⁠⁠」と呼ばれる理由です。

文章は分割した単語(word)の集合であるので単語の独立性を仮定すると,以下のように近似することができます。

P(doc|cat) = P(word1|cat) P(word2|cat) ... P(wordn|cat)

P(wordn|cat) の確率は分割した単語があるカテゴリに属する確率(単語の条件付き確率)を求めることで文章の生起確率が分かります。これはあるカテゴリに単語が出現した回数をカテゴリに出現した全単語数で割ることで求められます。

したがって,訓練フェーズでは出現した単語があるカテゴリに分類された回数を保存しておけばいいことが分かります。つまり,ナイーブベイズでは各カテゴリの登場回数と,ある文章があるカテゴリに分類された回数を保存しておけばいいのです。推定を行う時は,以上の保存された情報から P(doc|cat)P(cat) をカテゴリ毎に計算します。

※1
生起とはある物事が発生することをいいます。
※2
共起とは任意の文章,文脈中にある単語が出現した時に,同時に別の単語がその中に現われることをいいます。

訓練フェーズの実装

アルゴリズム全体の概要を把握したところで,訓練フェーズで何をすればいいのか理解できたはずです。訓練フェーズでは train() メソッドを使って,カテゴリの出現回数とあるカテゴリの中に単語が現れた回数を保存します。

まずはコンストラクタと訓練フェーズの実装について見てみましょうリスト3⁠。入力として,文章とカテゴリを受け取ります,入力された文章を分割し変数 wordcount にカテゴリに各単語が分類された回数を,変数 catcount にカテゴリの登場回数を保存しています。また,変数 vocabularies には重複を削除した単語の総数を保存しています。これは後述するスムージングに利用します。

リスト3 訓練フェーズ(naivebayes.py)

class NaiveBayes:
  def __init__(self):
    self.vocabularies = set() # 単語の集合
    self.wordcount = {}       # {category : { words : n, ...}}
    self.catcount = {}        # {category : n}

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

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

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

著者プロフィール

恩田伊織(おんだいおり)

1979年生まれの埼玉県出身。数学と関数型言語,SFとボードゲーム好きなプログラマー。好きな言語はRubyとLISP。現在は航空管制の基盤開発に従事している。

Twitter:http://twitter.com/Iori_o