Pythonでナイーブベイズによるテキスト分類を実装した。
この記事はまだ編集途中です。はてなブログって非公開機能ないんですか!!!
自分がそうだったように、いきなりナイーブベイズによるテキスト分類器と言われてもピンと来ない人の方が多いと思う。例を挙げながら、テキスト分類とは何かと、そのやり方について書いてみる。自分でどこが分かってないのかはっきりするだろう。いつか誰かの助けになれば尚良い。
テキスト分類って
テキスト分類器は、読んで字の如く「テキスト」を「分類」するプログラムのことだ。もう少し分かりやすく言うとテキスト分類は、「複数のテキストを決められたカテゴリに仕分ける」ということを意味する。
例えば、メールをスパムメールとそれ以外に自動分類したり、ニュースの記事を国際カテゴリとかスポーツカテゴリとかテクノロジーカテゴリに分類したりする。ここでは、メール、ニュース記事がテキストに該当する。
テキストの数が少なければ、人手で仕分けることもできるけど、大量のテキストをいちいち仕分けるのは大変だ。それをプログラムで自動的にやっちゃう、ってのがテキスト分類器である。
テキスト分類を実現するには
今回は「機械学習」を使ってテキスト分類を実現する。機械学習というのは、簡単に言うと、人間が物事を学習するのと同じことを機械にもやらせようということ。いくつかのサンプルからデータを学習して、データから規則や基準を抽出する。ぼやっとしていて分からないと思うから、ニュース記事を例にどうするのかを説明する。
サンプルデータ
まず、サンプルとなるデータを用意する。サンプルデータは、カテゴリ情報とテキスト本文のペアからなる。
サンプルの例。
カテゴリ:国際 本文:マレーシア航空機撃墜で、ウクライナ危機は、上空を飛行する民間国際便に まで戦火が及ぶ異常事態となった。 カテゴリ:スポーツ 本文:サッカーのドイツ代表主将として、ワールドカップブラジル大会で24年ぶ り4度目の優勝に貢献したフィリップ・ラームが代表引退を決めた。 カテゴリ:テクノロジー 本文:ソニーとフェリカネットワークスは2014年7月17日、FeliCaの最新技術や商 品・サービスについての動向を発表した。
例えば、上の例では国際、スポーツ、テクノロジーがカテゴリ情報にあたる。ごちゃごちゃして見づらくなったから本文は1文目しか書いていないけど、実際のデータでは、本文は記事全文を使う。それから記事の数も、ここでは1カテゴリあたり1記事しか用意していないけど、もっとたくさん用意する。
学習
サンプルが準備できたら、次は学習をする。人間がこの記事を読んだとき、「ウクライナ」「ロシア」という単語が出てきたら国際ニュースの記事だろうと推測できると思う。同じように「サッカー」「ワールドカップ」があればスポーツだろうし、「ソニー」「技術」があればテクノロジーの可能性が高いと判断できる。
機械学習では、コンピュータに同じことをさせる。まず、単語に分割して、それぞれの単語がそれぞれのカテゴリでどれくらい出てくるのかを計算する。
カテゴリごとの単語の出現回数
国際カテゴリ ウクライナ: 10回 国際: 15回 ブラジル: 3回... スポーツカテゴリ サッカー: 20回 ワールドカップ: 20回 ブラジル: 15回... テクノロジーカテゴリ 技術: 15回 ソニー: 4回 ICカード: 3回...
ここでは、それっぽくなるように適当な数字を用意した。こんな感じで全ての単語について出現回数を計算する。今は、テキストの特徴をよく表しているであろう名詞だけを抽出した。これで、機械に学習させたことになる。
分類
次は、この学習データを使って、新しく追加されたテキストがどのカテゴリに分類されるかを当てる。次のテキストを分類することにする。こちらも、学習時と同じように名詞だけを考える。
今年度のワールドカップはブラジルで開催される。 -> 今年度、ワールドカップ、ブラジル、開催
分類をするために、それぞれの単語があるカテゴリに出現する確率を計算する。この計算にはさっき学習したデータを使う。全てのカテゴリの総単語数が100語だと仮定すると「ワールドカップ」がスポーツカテゴリに含まれる確率は 20/100=0.2 となる。「ブラジル」という単語の場合は、国際カテゴリに含まれる確率は 3/100=0.03 で、スポーツカテゴリに含まれる確率は 15/100=0.15 となる。この確率を P(単語|カテゴリ) という形で表すことにする。
同時に、カテゴリの確率も計算しておく。テキストが1000個あって、その内の300個がスポーツカテゴリの記事なら、単語を見る前のテキストがスポーツカテゴリである確率は 300/1000=0.3 となる。これを P(カテゴリ) という形で表す。
それぞれの確率が計算できたら、テキスト全体の確率、つまり、テキスト(=今年度のワールドカップはブラジルで開催される)がスポーツカテゴリである確率 P(テキスト|スポーツ) を次の式で計算することができる。
P(テキスト|スポーツ) = P(スポーツ) * P(今年度|スポーツ) * P(ワールドカップ|スポーツ) * P(ブラジル|スポーツ) * P(開催|スポーツ)
つまり、あるテキストがあるカテゴリである確率は、カテゴリ自体の確率と、各単語がカテゴリに含まれる確率の積になる。同じようにして、このテキストが国際カテゴリ、テクノロジーカテゴリである確率も計算することができる。
分類するためには、テキストが入力されたら全てのカテゴリについて P(テキスト|カテゴリ) を計算する。そしてテキストは、それぞれの確率を比較して最も確率の高かったカテゴリに分類される。P(テキスト|国際)=0.4、P(テキスト|スポーツ)=0.5、P(テキスト|テクノロジー)=0.1 という結果が得られたら、最も確率の高いスポーツカテゴリに分類されるというわけだ。
ナイーブベイズって
ナイーブベイズ要素は、テキストの確率を計算するところにある。テキスト全体の確率は、テキストに含まれる単語の確率の積で計算できると説明した。しかし、実際には単語は周辺の単語と依存関係があるため、もう少し確率は複雑になる。例えば、テキスト中の「ワールドカップ」の確率は、「今年度」が現れたあとに「ワールドカップ」が現れる確率を計算する必要がある。
ナイーブベイズでは、単語の出現に独立性を仮定する。つまり、前後の単語との依存関係を無視して、「ワールドカップ」の確率は「ワールドカップ」という単語だけを考えた時の確率とする。そうすることで、計算は単純になり、先述のように単語ごとの確率の積でテキストの確率を計算できるようになる。
実装
何はともあれ、とにかく実装してみる。
必要な要素は、だいたいこんな感じか。
- 単語の出現頻度を数える
- カテゴリcatの確率P(cat)を計算する
- 単語wがカテゴリcatに含まれる確率P(w|cat)を計算する
- テキストdocがカテゴリcatに含まれる確率P(doc|cat)を計算する
- 確率が最大になるカテゴリを選択する
実際のプログラムはこちら。
# -*- coding: utf-8 -*- import math import os import sys from collections import defaultdict def pos_filter(word, infos): """品詞フィルタ""" info = infos.split(',') if info[0] in ('形容詞','名詞','動詞') and info[1] not in ('接尾','非自立'): return True return False def getwords(doc): """bag-of-wordsを取得""" os.system('echo "%s" | mecab > tmp.txt' % doc) words = [] for line in open('tmp.txt'): if line == 'EOS\n': continue line = line.strip('\n').split('\t') if pos_filter(line[0], line[1]): words.append(line[0]) return words class NaiveBayes: def __init__(self): self.vocabularies = set() self.wordcount = defaultdict(lambda : defaultdict(int)) self.catcount = defaultdict(int) def train(self, doc, cat): """ナイーブベイズ分類器の訓練""" words = getwords(doc) for word in words: self.wordcount[cat][word] += 1 self.vocabularies.add(word) self.catcount[cat] += 1 def p_cat(self, cat): """カテゴリの事前確率 P(cat) を求める""" return float(self.catcount[cat]) / sum(self.catcount.values()) def p_word_cat(self, w, cat): """単語の条件付確率 P(word|cat) を求める""" prob = (self.wordcount[cat][w] + 1.0) / \ (sum(self.wordcount[cat].values()) + len(self.vocabularies) * 1.0) return prob def p_doc_cat(self, doc, cat): """文書が与えられたときのカテゴリの事後確率の対数 log P(doc|cat) を求める""" score = math.log(self.p_cat(cat)) for w in getwords(doc): score += math.log(self.p_word_cat(w, cat)) return score def classifier(self, doc): """事後確率 log P(cat|doc) が最も大きいカテゴリを返す""" best = None max = -sys.maxint for cat in self.catcount.keys(): prob = self.p_doc_cat(doc, cat) if prob > max: max = prob best = cat return best if __name__ == '__main__': nb = NaiveBayes() nb.train('''Python(パイソン)は,オランダ人のグイド・ヴァンロッサムが作ったオープンソ\ ースのプログラミング言語。オブジェクト指向スクリプト言語の一種であり,Perlとともに欧米で\ 広く普及している。イギリスのテレビ局 BBC が製作したコメディ番組『空飛ぶモンティパイソン』\ にちなんで名付けられた。Python は英語で爬虫類のニシキヘビの意味で,Python言語のマスコッ\ トやアイコンとして使われることがある。Pythonは汎用の高水準言語である。プログラマの生産性\ とコードの信頼性を重視して設計されており,核となるシンタックスおよびセマンティクスは必要\ 最小限に抑えられている反面,利便性の高い大規模な標準ライブラリを備えている。Unicode に\ よる文字列操作をサポートしており,日本語処理も標準で可能である。多くのプラットフォームを\ サポートしており(動作するプラットフォーム),また,豊富なドキュメント,豊富なライブラリ\ があることから,産業界でも利用が増えつつある。''', 'python') nb.train('''Ruby(ルビー)は,まつもとゆきひろ(通称Matz)により開発されたオブジェクト\ 指向スクリプト言語であり,従来 Perlなどのスクリプト言語が用いられてきた領域でのオブジェ\ クト指向プログラミングを実現する。Rubyは当初1993年2月24日に生まれ, 1995年12月にfj上\ で発表された。名称のRubyは,プログラミング言語Perlが6月の誕生石であるPearl(真珠)と同\ じ発音をすることから,まつもとの同僚の誕生石(7月)のルビーを取って名付けられた。''', 'ruby') nb.train('''豊富な機械学習(きかいがくしゅう,Machine learning)とは,人工知能におけ\ る研究課題の一つで,人間が自然に行っている学習能力と同様の機能をコンピュータで実現させる\ ための技術・手法のことである。ある程度の数のサンプルデータ集合を対象に解析を行い,そのデ\ ータから有用な規則,ルール,知識表現,判断基準などを抽出する。データ集合を解析するため,\ 統計学との関連も非常に深い。機械学習は検索エンジン,医療診断,スパムメールの検出,金融市\ 場の予測,DNA配列の分類,音声認識や文字認識などのパターン認識,ゲーム戦略,ロボット,な\ ど幅広い分野で用いられている。応用分野の特性に応じて学習手法も適切に選択する必要があり,\ 様々な手法が提案されている。それらの手法は, Machine Learning や IEEE Transactions\ on Pattern Analysis and Machine Intelligence などの学術雑誌などで発表されること\ が多い。''', '機械学習') #Python doc = 'グイド・ヴァンロッサム氏によって開発されました' print '{}\n推定カテゴリ:{}\n'.format(''.join(doc), nb.classifier(doc)) doc = '豊富なドキュメントや豊富なライブラリがあります' print '{}\n推定カテゴリ:{}\n'.format(''.join(doc), nb.classifier(doc)) # Ruby doc = '純粋なオブジェクト指向言語です' print '{}\n推定カテゴリ:{}\n'.format(''.join(doc), nb.classifier(doc)) doc = 'Rubyはまつもとゆきひろ氏(通称Matz)により開発されました' print '{}\n推定カテゴリ:{}\n'.format(''.join(doc), nb.classifier(doc)) doc = '「機械学習 はじめよう」が始まりました' print '{}\n推定カテゴリ:{}\n'.format(''.join(doc), nb.classifier(doc)) doc = '検索エンジンや画像認識に利用されています' print '{}\n推定カテゴリ:{}\n'.format(''.join(doc), nb.classifier(doc))