ナイーブベイズ分類器のBayesモジュールを使う
ナイーブベイズ分類器は、次のようなことができます。
- スパムメールの判定
- ニュース記事やブログ記事のカテゴリー判定
ごく簡単にいうと、学習に必要なのはカテゴリーに関連する単語をたくさん登録するだけです。カテゴリーのわかっている文章を単語に分解して登録します。判定するときには、カテゴリーに関わる単語の出現率で判定されます。
もちろん、もっと正しい理解をしたほうがいいですが、bayesモジュールを使うならこの程度のイメージを持っておくだけで使えて、なかなか有益な結果を得られます。詳しく知りたい方は末尾のリンク先を参照してください。1
使い方(イメージ)
// 学習classifier.learn('カテゴリーAに関する長文・・・・・','カテゴリーA')classifier.learn('カテゴリーBに関する長文・・・・・','カテゴリーB')classifier.learn('カテゴリーCに関する長文・・・・・','カテゴリーC')// 判定constcategory=classifier.categorize('カテゴリーを判定したい文章')
準備(予備知識)
分かち書き
先に示したように、bayesモジュールのlearnメソッドを使ってカテゴリーのわかっている文章を学習させます。英語であれば自動で単語に分割して登録されるのですが、日本語の場合は単語への分割がうまくいきません。そこで、bayesが日本語の文章の単語への分割=分かち書きができるようにその機能を持ったメソッドを渡してあげます。分かち書きの機能を提供するtiny-segmenterモジュールを使って次のようにします。(イメージを掴むために、こちらのサイトでtiny-segmenterの動作を見ておくとよいです。)
constsegmenter=newTinySegmenter()varclassifier=bayes({tokenizer:function(text){returnsegmenter.segment(text);}});
async/await
また、最初の使い方イメージでは省略しましたが、bayesのlearnメソッドとcategorizeメソッドはasyncで提供されているため、簡単に使用するにはawaitをつけて呼び出す必要があります。awaitはaysncメソッド内でしか使えません。そのため下記のサンプルコードではasyncが使われています。
データ準備
学習に使う文章はWikipediaからもってきましょう。以下のURLでアクセスするとXMLデータが得られます。2
https://ja.wikipedia.org/wiki/特別:データ書き出し/キーワード
得られるデータにはちょっと無駄な情報が多いですが簡単に済ますために今回はこれをこのまま使いましょう。ブラウザで以下の3つのURLにアクセスして、それぞれyoritomo.txt、takauji.txt、ieyasu.txtとして保存してください。
https://ja.wikipedia.org/wiki/特別:データ書き出し/源頼朝
https://ja.wikipedia.org/wiki/特別:データ書き出し/足利尊氏
https://ja.wikipedia.org/wiki/特別:データ書き出し/徳川家康
これで準備は完了です。
インストール
npm install bayes
npm install tiny-segmenter
デモ・コード
実行結果は次の通りです。
$ node bayes-demo.js
判定=[源頼朝] -- 日本で最初に幕府を開いた人物で、妻は尼将軍としても有名な北条政子である。
判定=[源頼朝] -- 後鳥羽天皇によって征夷大将軍に任ぜられた。
判定=[源頼朝] -- 奥州を平定した。
判定=[足利尊氏] -- 室町幕府を開いた。
判定=[足利尊氏] -- 鎌倉幕府の滅亡後、鎮守府将軍・左兵衛督に任ぜられた。
判定=[足利尊氏] -- 歌人としても知られる。
判定=[徳川家康] -- 幼少時代を人質として過ごした。
判定=[徳川家康] -- 室町幕府最後の将軍足利義昭が信長包囲網を企てたとき、協力要請を受けたがこれを無視した。
これがデモ・コードです。ぐだぐだ説明する必要もないと思います。シンプル。
varbayes=require('bayes');constTinySegmenter=require('tiny-segmenter')constfs=require('fs')// 分かち書きの機能を使うためconstsegmenter=newTinySegmenter()// 学習用文章の読み込みvartxt_yoritomo=fs.readFileSync('yoritomo.txt','utf-8')vartxt_takauji=fs.readFileSync('takauji.txt','utf-8')vartxt_ieyasu=fs.readFileSync('ieyasu.txt','utf-8')// 分かち書き機能の設定varclassifier=bayes({tokenizer:function(text){returnsegmenter.segment(text);}});asyncfunctiondemo(){// 学習awaitclassifier.learn(txt_yoritomo,'源頼朝');awaitclassifier.learn(txt_takauji,'足利尊氏');awaitclassifier.learn(txt_ieyasu,'徳川家康');// 判定して結果を表示asyncfunctioncategorize(text){// 判定varr=awaitclassifier.categorize(text);console.log("判定=["+r+"] -- "+text);}// 文章のカテゴリーを判定する(分類する)categorize('日本で最初に幕府を開いた人物で、妻は尼将軍としても有名な北条政子である。');categorize('後鳥羽天皇によって征夷大将軍に任ぜられた。');categorize('奥州を平定した。');categorize('室町幕府を開いた。');categorize('鎌倉幕府の滅亡後、鎮守府将軍・左兵衛督に任ぜられた。');categorize('歌人としても知られる。');categorize('幼少時代を人質として過ごした。');categorize('室町幕府最後の将軍足利義昭が信長包囲網を企てたとき、協力要請を受けたがこれを無視した。');}demo()
bayesモジュールのコードを読む(わずか271行!)
bayesモジュールのソースコードを見てみると、わずか271行しかない比較的簡単な内容となっている。読んで理解するにはさすがに少しナイーブベイズ分類器について理解を深めておいたほうがよい。ナイーブベイズ分類器の良い解説記事はたくさんあるので探して読んでください。1
leanメソッドが学習部分です。
/**
* textがどのcategoryに対応しているか学習することで、ナイーブベイズ分類器を訓練する
*
* @param {String} text
* @param {Promise<String>} class
*/Naivebayes.prototype.learn=asyncfunction(text,category){varself=this//はじめてのカテゴリの場合は、カテゴリのデータ構造を初期化するself.initializeCategory(category)//カテゴリにマップされたドキュメント数をカウントするself.docCount[category]++//学習したドキュメントの総数をカウントself.totalDocuments++//テキストを単語に分割して配列にするvartokens=awaitself.tokenizer(text)
learnメソッドの後半では、各単語の出現回数をカウントしています。カウントしているのは、カテゴリ中の単語出現回数(wordFreqencyCount)と、カテゴリーの総単語数(wordCount)です。
//テキスト内の各トークンの頻度カウントを取得します。//get a frequency count for each token in the textvarfrequencyTable=self.frequencyTable(tokens)/*
このカテゴリの語彙数と単語数を更新します。
Update our vocabulary and our word frequency count for this category
*/Object.keys(frequencyTable).forEach(function(token){//この単語がない場合は、私たちの語彙に追加します。//add this word to our vocabulary if not already existingif(!self.vocabulary[token]){self.vocabulary[token]=trueself.vocabularySize++}varfrequencyInText=frequencyTable[token]//このカテゴリのこの単語の頻度情報を更新する//update the frequency information for this word in this categoryif(!self.wordFrequencyCount[category][token])self.wordFrequencyCount[category][token]=frequencyInTextelseself.wordFrequencyCount[category][token]+=frequencyInText//このカテゴリにマップされたすべての単語のカウントを更新します。//update the count of all words we have seen mapped to this categoryself.wordCount[category]+=frequencyInText})returnself}
categorizeメソッドは与えられたテキストのカテゴリーを判定します。すべてのカテゴリーごとに可能性を調べて、最も高い可能性のカテゴリーを選択します。可能性の算出は、テキスト中の各単語について、各単語の確率を加算するという方法です。非常にシンプルですね。
/**
* テキストがどのカテゴリに属するかを決定する
*
* @param {String} text
* @return {Promise<string>} category
*/Naivebayes.prototype.categorize=asyncfunction(text){varself=this,maxProbability=-Infinity,chosenCategory=nullvartokens=awaitself.tokenizer(text)varfrequencyTable=self.frequencyTable(tokens)//カテゴリを反復処理して、最も確率の高いカテゴリを求めるObject.keys(self.categories).forEach(function(category){// このカテゴリの全体的な確率を計算することから始める// => 学習したすべての文書のうち、このカテゴリのものはどれくらいあったかvarcategoryProbability=self.docCount[category]/self.totalDocuments//アンダーフロー対策に対数(log)を取るvarlogProbability=Math.log(categoryProbability)//テキスト中の各単語 `w` について P( w | c ) を決定するObject.keys(frequencyTable).forEach(function(token){varfrequencyInText=frequencyTable[token]vartokenProbability=self.tokenProbability(token,category)// console.log('token: %s category: `%s` tokenProbability: %d', token, category, tokenProbability)//この単語のP( w | c )の対数(log)を求めるlogProbability+=frequencyInText*Math.log(tokenProbability)})if(logProbability>maxProbability){maxProbability=logProbabilitychosenCategory=category}})returnchosenCategory}
以上