_ fieg/bayesをMySQL対応する
これは Laravel開発中に日々学んだこと Advent Calendar 2018 の17日目の記事です。
さすがに1つの記事に書き足すのに限界を感じたので分けました。
_ 概要
- ベイジアンフィルタとしてfieg/bayesが良さげ
- DB対応していないのでトレーニングを忘れてしまう
- ∴DB対応の拡張を入れた
_ 方針
ライブラリ化はしません。あくまで自分のプロジェクト内での拡張です。
_ 方法
- テーブルを作る
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へのアクセス処理を作る
こんな感じ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
| <?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;
}
}
}
|
このクラスで使用されているempty2Null()はDBからの返り値を整流(?)するものですが、あまり良くない書き方(countableじゃないのにcountしていたり)しているので参考にしない方が良いです。
目的はDBから得た結果を配列にすることです。
- 評価・学習クラスを拡張する
これが主眼です。\Fieg\Bayes\Classifierを継承して、拡張します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| <?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を発行しています。*1
あとは、\Fieg\Bayes\Classifierの代わりにApp\Http\Bayes\Classifierを使えばOKです。
おわり
DBはスケールしづらいですがWEBサーバーのスケールは容易なので、こういう前処理はなるべくPHP側でした方が後々困らないです