機械学習 はじめよう

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

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

推定フェーズの実装

続けて,推定フェーズの実装を見ていきましょう。最終的には推定したカテゴリを返すclassifier()メソッドを見ることになるのですが,それに必要な P(cat) や P(wordn|cat) を算出するためのメソッドを確認していきます。

まずは,P(cat) の生起確率を求めていきますリスト4)⁠先ほど説明したように,cat の生起確率 P(cat) は「訓練データとしてカテゴリが与えられた件数/総文書数」で算出することができます。priorprob()は P(cat) を求めるメソッドです。

リスト4 cat の生起確率 P(cat) を求める(naivebayes.py)

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

次に,cat に word が含まれる条件付き確率 P(word|cat) を求めますリスト5)⁠ここでは,先ほどのカテゴリの中に単語が現れた回数とカテゴリに出現した単語の総数で算出することができます。

しかし,ここで1つ大きな問題があります。このままでは訓練データに出現しなかった特徴 word_i が現れたときに出現確率 P(word_i|cat) が0になってしまいます。この問題をゼロ頻度問題やスパースネスの問題と呼びます。

普通に考えて,単語の出現頻度が0だからといって世の中にその単語がカテゴリに属する確率が0にならないことは分かりますよね。この時,ゼロ頻度問題を回避するために,訓練データから得られる単純な推定値を用いるのではなく,何らかの形で補正する必要があります。この補正操作のことをスムージング(smoothing)と呼びます。

今回のように一様な値を加えるスムージングを加算スムージングと呼びます。加算スムージングでは単語の出現回数に一定数の値を加え,出現回数0の単語をなくす方法です。なかでも,今回のように1を加えるものをラプラス法,実数に一般化したものをリッドストーン法と呼びます。加算スムージングは単純であり簡単に実現できますが,一般的に精度が悪いことが知られています。精度の高いスムージングの方法としては線形補間法やヘルドアウト補間法などがあります。

リスト5 単語の条件付き確率を求める(naivebayes.py)⁠

  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):
    # P(word|cat) が生起する確率を求める
    prob = \
      (self.incategory(word, cat) + 1.0) / \
          (sum(self.wordcount[cat].values()) + \
           len(self.vocabularies) * 1.0)
 
   return prob

しかし,ゼロ頻度問題を解決してもまだ1つ問題があります。確率値は1以下の非常に小さな値であり,しかも文章の中にはたくさんの単語が含まれています。そのため P(word|cat) の積を求めていくと,とても小さな値になってしまい,アンダーフローを起こす可能性があるのです。

それに対応するために対数をとり,積の計算を和の計算に変換します。ここで最終的に求めたいものは確率の値そのものではなく,事後確率の大小関係なので対数をとってもその関係は変化しないので問題はありません。リスト6の score() メソッドでスコア(=確率値の対数)を算出しています。

リスト6 確率値の対数の算出

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

カテゴリの推定

あとは,そのカテゴリに属する確率の中からもっとも高い確率のものを決定すればいいでしょうリスト7)⁠以上でベイジアンフィルタの実装は完了します。

リスト7 確率値の対数の比較(naivebayes.py)

  def classifier(self, doc):
    best = None # 最適なカテゴリ
    max = -sys.maxint
    word = getwords(doc)
    
    # カテゴリ毎に確率の対数を求める
    for cat in self.catcount.keys():
      prob = self.score(word, cat)
      if prob < max:
        max = prob
        best = cat

    return best

実行してみよう

それでは,実際に動かして確認してみましょう。実行のためにnaivebayes.pyに以下のコードを追加しましょうリスト8)⁠

今回,訓練データとして入力する文章はWikipediaにあるそれぞれの概要の文章を抜粋しました。この文章をそれぞれの項目「Python」⁠Ruby」⁠機械学習」のカテゴリとして分類したあとにカテゴリの推定を行います。

リスト8 実行スクリプト(naivebayes.py)

if __name__ == "__main__":
  nb = NaiveBayes()

  nb.train(u'''Python(パイソン)は,オランダ人のグイド・ヴァンロッサムが作ったオープンソースのプログラミング言語。
オブジェクト指向スクリプト言語の一種であり,Perlとともに欧米で広く普及している。イギリスのテレビ局 BBC が製作したコメディ番組『空飛ぶモンティパイソン』にちなんで名付けられた。
Python は英語で爬虫類のニシキヘビの意味で,Python言語のマスコットやアイコンとして使われることがある。Pythonは汎用の高水準言語である。プログラマの生産性とコードの信頼性を重視して設計されており,核となるシンタックスおよびセマンティクスは必要最小限に抑えられている反面,利便性の高い大規模な標準ライブラリを備えている。
Unicode による文字列操作をサポートしており,日本語処理も標準で可能である。多くのプラットフォームをサポートしており(動作するプラットフォーム),また,豊富なドキュメント,豊富なライブラリがあることから,産業界でも利用が増えつつある。''', 'Python')

  nb.train(u'''Ruby(ルビー)は,まつもとゆきひろ(通称Matz)により開発されたオブジェクト指向スクリプト言語であり,従来 Perlなどのスクリプト言語が用いられてきた領域でのオブジェクト指向プログラミングを実現する。Rubyは当初1993年2月24日に生まれ, 1995年12月にfj上で発表された。名称のRubyは,プログラミング言語Perlが6月の誕生石であるPearl(真珠)と同じ発音をすることから,まつもとの同僚の誕生石(7月)のルビーを取って名付けられた。''', 'Ruby')

  nb.train(u'''豊富な機械学習(きかいがくしゅう,Machine learning)とは,人工知能における研究課題の一つで,人間が自然に行っている学習能力と同様の機能をコンピュータで実現させるための技術・手法のことである。ある程度の数のサンプルデータ集合を対象に解析を行い,そのデータから有用な規則,ルール,知識表現,判断基準などを抽出する。データ集合を解析するため,統計学との関連も非常に深い。
機械学習は検索エンジン,医療診断,スパムメールの検出,金融市場の予測,DNA配列の分類,音声認識や文字認識などのパターン認識,ゲーム戦略,ロボット,など幅広い分野で用いられている。応用分野の特性に応じて学習手法も適切に選択する必要があり,様々な手法が提案されている。それらの手法は, Machine Learning や IEEE Transactions on Pattern Analysis and Machine Intelligence などの学術雑誌などで発表されることが多い。''', u'機械学習')

#Python
words = u'ヴァンロッサム氏によって開発されました.'
print u'%s => 推定カテゴリ: %s' % (words ,nb.classifier(words))

words = u'豊富なドキュメントや豊富なライブラリがあります.'
print u'%s => 推定カテゴリ: %s' % (words ,nb.classifier(words))

#Ruby
words = u'純粋なオブジェクト指向言語です.'
print u'%s => 推定カテゴリ: %s' % (words ,nb.classifier(words))

words = u'Rubyはまつもとゆきひろ氏(通称Matz)により開発されました.'
print u'%s => 推定カテゴリ: %s' % (words ,nb.classifier(words))

#機械学習
words = u'「機械学習 はじめよう」が始まりました.'
print u'%s => 推定カテゴリ: %s' % (words ,nb.classifier(words))

words = u'検索エンジンや画像認識に利用されています.'
print u'%s => 推定カテゴリ: %s' % (words , nb.classifier(words))

以下の結果のように,期待していたカテゴリに分類されましたリスト9)⁠

リスト9 実行結果

% python naivebayes.py
ヴァンロッサム氏によって開発されました. => 推定カテゴリ: Python
豊富なドキュメントや豊富なライブラリがあります. => 推定カテゴリ: Python
純粋なオブジェクト指向言語です. => 推定カテゴリ: Ruby
Rubyはまつもとゆきひろ氏(通称Matz)により開発されました. => 推定カテゴリ: Ruby
「機械学習 はじめよう」が始まりました. => 推定カテゴリ: 機械学習
検索エンジンや画像認識に利用されています. => 推定カテゴリ: 機械学習

「オブジェクト指向言語」のように複数のカテゴリに登場している単語でも,それ以外の単語の出現によって正しく分類されているのが分かると思います。

今回は訓練データとして各カテゴリに対して1件ずつしか用意していませんが,冒頭で説明したスパム判定ではだいたい4,000件のデータを使ってトレーニングしているようです(訓練データが増えることによって,より正確な分類ができるようになるので興味のある方はご自身で試してみてください)⁠

今回実装したベイジアンフィルタはとても簡単なものです。ベイジアンフィルタは色々な言語のオープンソースプロジェクトでも開発されているので見比べてみるのも面白いかもしれません。

次回のお知らせ

次回の理論編では正規分布を説明します。

今回のサンプルファイル

今回取り上げたサンプルプログラムを一式ダウンロードできます。

著者プロフィール

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

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

Twitter:http://twitter.com/Iori_o