--> -->
#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していたり)をしているので参考にしない方が良いです。~
目的は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です。~
~
おわり