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

デジタル・デザイン・ラボラトリーな日々

アラフィフプログラマーが数学と物理を基礎からやり直す

確率を理解してみる-ベイジアンフィルタを実装

はじめに

前回、「ベイズの定理」について説明しました。 yaju3d.hatenablog.jp

今回、ベイズの定理を利用したベイジアンフィルタの中で最もシンプルなナイーブベイズ(Naive Bayes)を実装してみます。

ベイジアンフィルタ

ベイジアンフィルタは、迷惑メールフィルタの仕組みとして広く知られている機械学習処理のアルゴリズムで、膨大な言葉の組合せで表現される自然言語の文章の分類に、その真価が発揮されます。
今回はベイジアンフィルタの中でもナイーブベイズ(Naive Bayes)というアルゴリズムを使用します。ナイーブは「ばからしいほど単純」という意味があるため「単純ベイズ分類器」とも呼びます。しかし、単純とはいえ馬鹿にできない分類性能を持ちます。

ナイーブベイズの問題点

あるクラスの学習データに存在しない単語を含む文書は決してそのクラスに分類されない(ゼロ頻度問題)があります。

実装環境

下記の記事を使用しますので、言語はPythonとなります。その記事では「Yahoo!デベロッパーズネットワークの日本語形態素解析」を使用しているのですが、その部分は形態素解析の定番である「MeCab」ならぬ「Janome」を使ってみました。 gihyo.jp

Pythonは、Docker上のTensorFlowで構築した環境を使用しています。
参照:WindowsユーザーがTensorFlowをインストールしてみた(Docker版) - デジタル・デザイン・ラボラトリーな日々

環境

Janomeについて

Janome (蛇の目) は, Pure Python で書かれた, 辞書内包の形態素解析器です. 依存ライブラリなしで簡単にインストールでき, アプリケーションに組み込みやすいシンプルな API を備える形態素解析ライブラリを目指しています
http://mocobeta.github.io/janome/

最初は形態素解析の定番である「MeCab」を使う予定だったのですが、インストールの参考サイトで提示されている「https://mecab.googlecode.com/files/mecab-python-0.996.tar.gz」が既にgooglecode自体の消滅により存在しない状態となっているのと、Docker上のJupyterを使用しているので、pipインストールで簡単に済ませたいと思っている中で下記サイトを見つけました。 ailaby.com

Janomeのインストール

Jupyter NotebookのTerminalにて、下記コマンドでインストールします。

# pip install janome
 ︙
Successfully installed janome-0.2.8

実装(morphological.py)

第3回 ベイジアンフィルタを実装してみよう:機械学習 はじめよう|gihyo.jp … 技術評論社
リスト1 形態素解析を使って,わかち書きをする(morphological.py)

Mecabに変更したサイト(Python の「Yahoo!デベロッパーズネットワークの日本語形態素解析で分かち書きする」スクリプトを MeCab を使ってやってみるメモ - 牌語備忘録 -pygo)を参考にJanomeに変更しました。

from janome.tokenizer import Tokenizer

def split(doc, word_class=["形容詞", "形容動詞", "感動詞", "副詞", "連体詞", "名詞", "動詞"]):
    t = Tokenizer()
    tokens = t.tokenize(doc)
    word_list = []
    for token in tokens:
        word_list.append(token.surface)
    return [word for word in word_list]


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

結果

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

実装と説明

Jupyter Notebookを使用しているため、実装は分けて書いていきます。
基本的な説明は、「第3回 ベイジアンフィルタを実装してみよう - ナイーブベイズのアルゴリズム」に書かれているのですが、自分なりに少し補足していきます。

今回は文章(doc)が与えられた時、カテゴリ(cat)に属する確率 P(cat|doc) を求める問題になります。

ベイズの定理

本文に合わせてみると、P(X)がP(doc)、P(Y)がP(cat)となります。
P(Y|X) = \displaystyle{\frac{P(Y)P(X|Y)}{P(X)}}P(cat|doc) = \displaystyle{\frac{P(cat)P(doc|cat)}{P(doc)}}

  • P(X) : X が起きる確率
  • P(Y) : Y が起きる確率(事前確率)
  • P(X|Y) : Y の後でX が起きる確率(条件付き確率、尤度)
  • P(Y|X) : X の後でY が起きる確率(条件付き確率、事後確率)

文章は分割した単語(word)の集合であるので単語の独立性を仮定すると,以下のように近似することができます。
P(doc|cat) = \displaystyle{P(word1|cat) P(word2|cat)  \cdots P(wordn|cat)}

対数は何のため

ソースリストの中で対数を使用していますので、何のためか疑問を持ちました。
そもそも対数と何かなんですが、底の乗数を返します。10を底としたなら1000だと10の3乗なので3となります。
math.log10なら底が10となりますが今回はmath.logで底の指定がないため、底は自然対数のネイピア数(2.71828182…)となります。
jbpress.ismedia.jp

logを取らないと値が0.000….01のような小数となり、小さすぎてアンダーフローを起こす可能性があります。よって対数をとってかけ算を足し算化します。事後確率の大小関係は対数をとっても変化しないので問題ありません。

math.log(0.0000000000001)    # -29.933606208922594
math.log(0.0000000000000001) # -36.841361487904734

参照:対数関連
aidiary.hatenablog.com
qiita.com

ソースリスト

import math
import sys

from janome.tokenizer import Tokenizer

def split(doc, word_class=["形容詞", "形容動詞", "感動詞", "副詞", "連体詞", "名詞", "動詞"]):
    t = Tokenizer()
    tokens = t.tokenize(doc)
    word_list = []
    for token in tokens:
        word_list.append(token.surface)
    return [word for word in word_list]

def getwords(doc):
    words = [s.lower() for s in split(doc)]
    return tuple(w for w in words)
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)

    # 推定フェーズ:分類
    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

    # 推定フェーズ:スコア計算
    def score(self, word, cat):
        score = math.log(self.priorprob(cat))
        for w in word:
            score += math.log(self.wordprob(w, cat))
        return score

    # 推定フェーズ:catの生起確率 P(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

    # 推定フェーズ:条件付き確率 P(word|cat)(補正つき)
    def wordprob(self, word, cat):
        prob = \
            (self.incategory(word, cat) + 1.0) / \
                  (sum(self.wordcount[cat].values()) + \
                   len(self.vocabularies) * 1.0)
        return prob

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

考察

結果を見るとカテゴライズされているのですが、最初の文章が期待通り「Python」に分類されず「Ruby」と分類されてしまっています。 原因については、下記サイト(Rubyに翻訳)で調査されていて、Mecab側にあるとのことです。
antimon2.hatenablog.jp

問題点は、いくつか出てきたのですが大きなのは以下の2つ。

  • 人名「グイド・ヴァンロッサム」が、この塊で一般名詞として認識されている。 特に「ヴァンロッサム」が単語として認識されていないのでカウントに引っかからない。
  • 日本語的には助動詞の「れ(る)」「られ(る)」が、動詞(接尾)として認識されている。 動詞は検索対象なので、これが含まれる量がカテゴリ推定に大きく左右されてしまっている。

学習

連載記事の最後の方にもきちんと書いてあります。

訓練データが増えることによって,より正確な分類ができるようになるので興味のある方はご自身で試してみてください

なので訓練データを追加します。

nb.train(u'''ヴァンロッサム氏によって開発されました。''', 'Python')

結果

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

最後に

ベイズの定理の理解からベイジアンフィルタを実装というところまでやりました。
実装といってもほとんど写経に終わってますし、ベイズの定理が少し分かったからといって一から実装するまでの知識は無いわけで、参考記事があって助かっています。今回、形態素解析ってのも初めてでしたし、いろいろと勉強になりましたね。