--> -->

skimemo


Laravel-20181217 の変更点


#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です。~
~
おわり