--> -->

skimemo


Laravel-20181217 のバックアップ(No.1)


_ fieg/bayesをMySQL対応する

これは Laravel開発中に日々学んだこと Advent Calendar 2018 の17日目の記事です。
さすがに1つの記事に書き足すのに限界を感じたので分けました。

_ 概要

  1. ベイジアンフィルタとしてfieg/bayesが良さげ
  2. DB対応していないのでトレーニングを忘れてしまう
  3. ∴DB対応の拡張を入れた

_ 方針

ライブラリ化はしません。あくまで自分のプロジェクト内での拡張です。

_ 方法

  1. テーブルを作る
    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`)
    );

  2. 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から得た結果を配列にすることです。

  3. 評価・学習クラスを拡張する
    これが主眼です。\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側でした方が後々困らないです