--> -->
#title(fieg/bayesをMySQL対応する) * fieg/bayesをMySQL対応する [#u73162a8] これは ''Laravel開発中に日々学んだこと Advent Calendar 2018'' の17日目の記事です。~ さすがに1つの記事に書き足すのに限界を感じたので分けました。~ ** 概要 [#ncc0e5c8] + ベイジアンフィルタとして[[fieg/bayes:https://github.com/fieg/bayes]]が良さげ + DB対応していないのでトレーニングを忘れてしまう + ∴DB対応の拡張を入れた ** 方針 [#rd366dd7] ライブラリ化はしません。あくまで自分のプロジェクト内での拡張です。 ** 方法 [#v50a214b] + テーブルを作る~ DBにテーブルを作ります。~ コメントはプロジェクト内容に合わせて適当に・・・。~ CREATE TABLE IF NOT EXISTS `bayes` ( `label` varchar(64) NOT NULL COMMENT '解答パターン', `token` varchar(256) NOT NULL COMMENT '出現単語', `count` int(11) NOT NULL DEFAULT '1' COMMENT '回数', PRIMARY KEY (`label`,`token`) ); ~ + DBへのアクセス処理を作る~ こんな感じ。 #code(php){{ <?php namespace App\Http\Bayes; use DB; use Exception; class DbBayes { /** * 全データを取得する * @return array */ public function getBayes() { return $this->empty2Null(DB::table('bayes')->get()); } /** * データを更新する * @param int $label * @param string $token * @param int $count * @return bool 0=更新なし, 1=更新成功, 2=挿入, -1=挿入失敗 */ public function updateBayes($label, $token, $count) { $oldCount = DB::table('bayes')->where('label',$label)->where('token',$token)->value('count'); if($oldCount==null) { // 新規 try { DB::table('bayes')->insert(['label'=>$label, 'token'=>$token, 'count'=>$count]); $result = 2; } catch( Exception $e ){ $result = -1; logger()->error(__METHOD__ . ":" . $e->getMessage()); } } elseif( $oldCount!=$count ) { // 既存 DB::table('bayes')->where('label',$label)->where('token',$token)->update(['count'=>$count]); $result = 1; } else { // 変更無し $result = 0; } return $result; } /** * DBからの取得結果が無かったらNULLを返す * @param array|object|int $result DBからのSELECT内容 * @return array 中身があればそのまま、無ければnull */ private function empty2Null($result){ if( count($result) == 0 ){ // $resultが配列/Objectじゃない場合はcount()==1となる $this->exist = false; return []; } else { $this->exist = true; return $this->stdClass2Array($result); } } /** * SQL結果のobjectをarrayに変換する * @param array|object $result SQL実行結果 * @return array 返還後結果 */ private function stdClass2Array($result) { if( is_object($result) ) { if (get_class($result) == 'stdClass') { // 1階層の場合(->first()とか) return (array)$result; } else { // 2階層の場合(->get()とか) return array_map(function ($value) { return (array)$value; }, ($result->toArray())); } } else { // 直値などの場合 return $result; } } } }} このクラスで使用されている&inlinecode{empty2Null()};はDBからの返り値を整流(?)するものですが、あまり良くない書き方(countableじゃないのにcountしていたり)しているので参考にしない方が良いです。~ このクラスで使用されている&inlinecode{empty2Null()};はDBからの返り値を整流(?)するものですが、あまり良くない書き方(countableじゃないのにcountしていたり)をしているので参考にしない方が良いです。~ 目的はDBから得た結果を配列にすることです。~ ~ + 評価・学習クラスを拡張する~ これが主眼です。&inlinecode{\Fieg\Bayes\Classifier};を継承して、拡張します。 #code(php){{ <?php namespace App\Http\Bayes; use Fieg\Bayes\TokenizerInterface; class Classifier extends \Fieg\Bayes\Classifier { const DOCS_TOKEN = "__docs__"; /** * Classifier constructor. * クラス変数にDBの内容を読み込み * @param TokenizerInterface $tokenizer */ public function __construct(TokenizerInterface $tokenizer) { parent::__construct($tokenizer); // DBからデータを取得 $dbBayes = new DbBayes(); $data = $dbBayes->getBayes(); $this->tokens = []; $this->labels = []; foreach( $data as $row){ if( $row['token']==self::DOCS_TOKEN ){ $this->docs[$row['label']] = $row['count']; } else { $this->data[$row['label']][$row['token']] = $row['count']; $this->tokens[$row['token']] = (isset($this->tokens[$row['token']]) ? $this->tokens[$row['token']] : 0) + 1; $this->labels[$row['label']] = (isset($this->labels[$row['label']]) ? $this->labels[$row['label']] : 0) + 1; } } } /** * Classifier destructor. * クラス変数の中身をDBに書き戻す */ public function __destruct() { // 差分があったらupdateする $dbBayes = new DbBayes(); $dbdata = $dbBayes->getBayes(); foreach( $this->data as $label => $tokens ){ foreach($tokens as $token => $count){ if( $this->getDbData($dbdata, $label, $token) != $count ){ $dbBayes->updateBayes($label, $token, $count); } } } foreach( $this->docs as $label => $count ){ if( $this->getDbData($dbdata, $label, self::DOCS_TOKEN) != $count ) { $dbBayes->updateBayes($label, self::DOCS_TOKEN, $count); } } } /** * 配列の中からキーに一致するデータを探す * @param array $dbdata heystack * @param string $label needle1 * @param string $token needle2 * @return int 見つかったデータのcount、-1=無かった */ private function getDbData($dbdata, $label, $token) { $count = -1; foreach($dbdata as $value){ if( $value['label']==$label and $value['token']==$token ){ $count = $value['count']; break; } } return $count; } } }} このクラスを使用する時に、コンストラクタでDBから値を取得し、クラス変数に格納します。(元のライブラリのクラス変数が全てprotectedで定義してあるので可能になっています。ライブラリを作る時のお手本(常識?)ですね)~ そしてデストラクタでクラス変数の値をDBに書き戻します。この際、数が膨大になるので、変数レベルで差分を確認してからSQLを発行しています。((DBはスケールしづらいですがWEBサーバーのスケールは容易なので、こういう前処理はなるべくPHP側でした方が後々困らないです))~ ~ あとは、&inlinecode{\Fieg\Bayes\Classifier};の代わりに&inlinecode{App\Http\Bayes\Classifier};を使えばOKです。~ ~ おわり