ゼッタノート

ゼッタが勉強したことをまとめたノート代わりのブログ。たまに日記になるかも。

勉強したこと:NaiveBayes編

 勉強したこと記事、第2回はNaiveBayesです。scikit-learnのチートシートに従うと、テキストデータの分類をする際にLinear SVCで上手くいかなかった場合に用いることになるやつです。

 今回は以下の記事を中心に勉強していきました。
ナイーブベイズで自然言語処理(クラス分類)【Pythonとscikit-learnで機械学習:第7回】

※注意!この記事は機械学習入門者が独自に調べ独自に解釈した内容により構成されているため、誤った記述が含まれている可能性があります。

NaiveBayesって?

 日本語ではそのまま単純ベイズとか言ったりします。主に使われる場面は迷惑メールフィルタです。前回のLinear SVCでは各データをグラフにプロットし、マージンが最大になるように直線を引くことで分類を行いましたが、今回は確率によって分類します。
 ベイズ、と名前に入っているだけあって、中心となる式は、以下に示す確率を習った際に出てきたであろうベイズの定理を利用しています。

P(B|A) = \frac{P(A|B)P(B)}{P(A)}\\

Bはクラス、Aはデータ(上の参考URLを例にすると、Bは記事のカテゴリ、Aは文書内に含まれる単語出現頻度のベクトル)です。未知の文書を分類する際はベイズの定理による計算結果、すなわち事後確率が最も大きいクラスに分類します。

詳しく

 ここでは文書データを分類するという仮定の元説明していきます。まず、Bをある文書カテゴリcat_x、Aを文書内に含まれる単語の出現頻度ベクトル\boldsymbol{doc}とし、ベイズの定理を書き換えます。

P(cat_x|\boldsymbol{doc}) = \frac{P(\boldsymbol{doc}|cat_x)P(cat_x)}{P(\boldsymbol{doc})}\\

 未知のデータを分類する際は上記の式を計算すればいいのですが、P(\boldsymbol{doc}|cat_x), P(cat_x), P(\boldsymbol{doc})の値が分からないのでこれらを算出する必要があります。そして、それら3つの値(本当は2つの値)を算出する過程が今回の「学習」ということになります。

 まず、なぜ2つの値のみを算出すればいいのか?今回必要のない値はP(\boldsymbol{doc})です。例えば未知の文書を2つのカテゴリcat_1, cat_2に分類する場合、どのように比較するかと言うと未知文書の単語出現頻度ベクトル\boldsymbol{doc}に対して

 \frac{P(\boldsymbol{doc}|cat_1)P(cat_1)}{P(\boldsymbol{doc})}

 \frac{P(\boldsymbol{doc}|cat_2)P(cat_2)}{P(\boldsymbol{doc})}\\

のうち、値の高いほうに分類します。ここで、P(\boldsymbol{doc})は共通していますよね?なのでどちらが大きいかを比較する場合は分子だけに着目すればいいことになります。式的に比を取ると分母が消える、と考えてもよいでしょう。なのでP(\boldsymbol{doc})は計算する必要がありません。

 P(\boldsymbol{doc})を計算する必要がなくなったので、単純ベイズで用いる式は以下のようになります。

P(\boldsymbol{doc}|cat_x)P(cat_x)\\

 また、これを変形すると

P(\boldsymbol{doc}|cat_x)P(cat_x) = \frac{P(\boldsymbol{doc}, cat_x)}{P(cat_x)}P(cat_x) = P(\boldsymbol{doc}, cat_x)\\

となります。ここでP(\boldsymbol{doc})がどういうものかについて説明します。インデックスは出現した単語を指し、要素はその記事中の出現単語数になります。例えばある記事の中にAppleが3回、代理が1回、Microsoftが2回出てきたとし、それぞれインデックスが0, 1, 2だとすると\boldsymbol{doc} = (3, 1, 2)となります。
 さて、それぞれの単語出現頻度をword_1, word_2, word_3, ..., word_nとすると、

P(\boldsymbol{doc}, cat_x) = P(word_1, word_2, word_3, ..., word_n, cat_x)\\
= P(word_1|word_2, word_3, ..., word_n, cat_x)P(word_2, ..., word_n, cat_x)\\
= ... \\
=P(word_1|word_2, ..., word_n, cat_x)P(word_2|word_3, ..., wordn, cat_x)...\\
P(word_{n-1}|word_n, cat_x)P(word_n|cat_x)P(cat_x)\\

見づらくなっちゃいましたが、最後の行は全部かけてます。乗法定理P(A,B)=P(A|B)P(B)を繰り返し用いることでこのような変形を行っています。さて、ここで一つ乱暴ともいえる仮定を行います。それはそれぞれの単語は独立に生起するというものです。本来「機械」と「学習」はセットで出てきやすいですし、「響け」と「ユーフォニアム」もセットで出てきやすいです。ですがこの仮定では「機械」と「学習」という単語間の関連性を無視し、それぞれが独立のものとして現れた、と考えます。このような状況を条件付き独立と言います。そうすると、式は以下のようになります。

P(word_1|cat_x)P(word_2|cat_x)...P(word_n|cat_x)P(cat_x)\\
=P(cat_x)\prod_{i=1}^n P(word_i|cat_x)\\

大変簡単な式になりましたね。これが単純ベイズのありがたみであり、この仮定が「単純(Naive)」と言われる所以です。

 で、先ほどの式に現れたP(cat_x)が分かりませんので、これも計算する必要があります。これは全文書中に、そのカテゴリが占める割合を算出すれば良いです。式で表すと

\frac{カテゴリcatの文書数}{全文書数}\\

となります。

 ここで一つ問題があります。未知の文書を分類する際、学習した文書の中には現れなかった単語が含まれる可能性は十二分に考えられます。その場合、どのカテゴリに対してもP(未知の単語|cat_x) = 0となり、掛け算をしているために事後確率も0になってしまいます。どんなに機械学習に分類されるような単語が含まれていたとしても、猫の判別についての記事だったためにどのカテゴリにも属さないと判定される...これでは全く使えませんよね。これが迷惑メールフィルタだとすると実害がより実感しやすいでしょうか。この問題はゼロ頻度問題と言われています。
 今回用いたscikit-learnのnaive_bayes.MultinomialNB()では加算スムージングと言われる手法でこの問題を解決しているようです。加算スムージングについては以下のサイトを参考にしました。
言語モデルにおける未知語の扱いとスムージング
 以下、自分なりのまとめ

 未知語が出てきた際、その確率が0にならないようにしなければなりません。そこで、ある一定の値を足してあげることで解決を図ります。加算スムージングの式は以下です。

P(word_x) = \frac{n(word_x)+\alpha}{C+\alpha V}\\

このように、ある一定の値\alphaを足してあげることによって、未知語が出てきたとしても、ほんの少しの値を持つことになり、事後確率が0になるのを防ぎます。

コードを書く

 理論についてまとめたので、今度は参考サイトを見ながらコードを書く上で勉強したことを。

記事を分かち書きする

 記事を分かち書きする際、名詞、動詞、形容詞のみを抽出していました。確かに助詞を持ってこられてもカテゴリ分類には役立ちそうにありませんね。どんなカテゴリにも「が」は含まれがちですし、邪魔になりそうです。また、活用の元の形を利用する(遊びました、遊ぶなどを分かち書きした際、動詞については両方 遊ぶ で集約)ことによって、効率化しています。

Bag-of-Words

 ニュース記事をベクトルにしました。ここは結構ややこしかったのでまとめます。

 まず全記事に出てきた単語をまとめます。word_dicが出てきた単語をまとめている辞書で、例えば 機械 という単語が最初に登録された場合、word_dic = {"MAX":1, "機械":0}という風になります。MAXは登録されている単語数、各単語の値は後に単語の出現回数を数えるためのcntというリストのインデックスになります。分かち書きした結果を保存したファイルを読み込みながら、全単語を登録した辞書を作成します。
 次に、ジャンルごとにファイルを読み込み、単語の出現回数を数えます。ここでは各ジャンルごとに100ファイルずつ利用していました。1ジャンルごとにcount_file_freq()を呼び出し、カウントしていきます。先ほどの例を用いると、機械という単語はインデックス0にあたるので、機械という単語が出てくるたびにcnt[0]に1加算されていきます。カウントし終えたら、リストXに単語のカウント結果cntを、リストYにカテゴリ名cat_idxを登録します。カテゴリ名と言っていますが、実際は0から始まる数字です。最終的に
X[{1つ目の記事のカウント結果}, {2つ目の記事のカウント結果}, ..., {n個目の記事のカウント結果}]
Y[(1つ目の記事のカテゴリ名), (2つ目の記事のカテゴリ名), ..., (n個目の記事のカテゴリ名)]
となります。要は、XとYの同じインデックス番号どうしで対応が取れている形になります。これでベクトル化は終了です。

実際に分類する

 ここでは1点だけ。naive_bayes.MultinomialNB()の引数、alphaは先ほど述べた加算スムージングにおける\alphaの値です。

まとめ

線形SVMとは異なり、単純ベイズは単語の出現頻度を基にした、確率によって分類を行うアルゴリズムである。様々な工夫によって計算を単純化しているため、単純と言われる。その工夫とは、比較する際は分子のみに着目すればよかったり、条件付き独立性を仮定することである。未出現の単語については加算平滑化スムージングという手法によって事後確率が0にならないようにしなければならない。

所感

調べてみたけど解決しなかったことについていろいろ考えるこのコーナー。今回は2つ。

scikit-learnのチートシートで、なぜLinear SVCが上手くいかなかったらNaive Bayesなのか?

個人的に、確率的手法かそうでないかっていうのがミソになっているんじゃないのかなと。SVMは空間上で境界線(面)を引くのが難しい場合でも境界によって分類をしなければなりません。なので無理やり境界を引いたところで、あやふやなまま分類をする必要がある...一方単純ベイズは確率を求め、高いほうを採用するという手法なので、より柔軟である...?自分で言ってても本当かよ、という感じです。あとは単純に計算コストですかね。やっぱり楽な手法で上手くいくならそれに越したことはないので、先にコストのかからないSVMを試して、それでだめなら単純ベイズ...みたいな。

MultinomialNB()の引数、fit_priorについて

わからなかったので本編では触れませんでした。リファレンスによるとクラス事前確率を学ぶかどうかを指定する変数のようですが...参考サイトには学習データの偏りがあった場合に、それを考慮するか、とも書いてました。といってもどう考慮するのかが分かりません。クラスの事前確率っていうぐらいですから、例えばスパムフィルタを作る際の学習データが、ハムメール:5件、スパムメール:1件で構成されていた場合、未知のメールを分類する際に、単語の出現頻度による事後確率を計算した後、ハムメールのほうが来やすいよね!ってことで何かしらの値を掛けたりするんでしょうか。確かにこれなら出現頻度を考慮しているような...?fit_priorの引数はデフォルトではTrueで考慮するようになっているようですが、Falseにすると一様分布になるようですし、全体的な頻度を考慮したうえで分類してくれるってことなのかな...?
 と、ここまで書いてブログの見直しをしていたら、今ここで書いたことはP(cat_x)のことなのでは...と気づいてしまいました。となるとこの引数は実際にP(cat_x)を計算するか、はたまた全ての記事が一様に現れるとしてP(cat_x) = 定数とし、事後確率を計算するか、ということなんでしょうか。全てのカテゴリの記事を均等に用意したのであれば、引数をFalseにすることで計算量を少なくすることができる...?

サンプルコードを書いていて思ったこと

 最終的にXとYについて膨大な量の辞書型を生成してしまうわけですが、これって

{cat0:出現頻度, cat1:出現頻度...}

という辞書型を生成するほうが良いのでは?と思ったりしました。


 と、いうことで第2回は終わりです。コードを追いかけつつ説明していった前回とは違い、今回は単純ベイズの理論をどーんと説明した後、コードを追ってみました。今後はこの形式になるんじゃないかと思います。後、最後に未解決問題を書いてみたりもしました。ここを書いているうちに何か閃かないかなという希望を持ちながらこのコーナーを作りました。もしわかる方がいれば教えていただければ...と。またソースが確認できず不確定なことや、見出しの名の通り、思ったこともそこに書いていきたいと思います。

 ここまで読んでいただき、ありがとうございました。

P.S. コードインテリセンスの選択をTabキーにするとQ.O.Lが上がった。