PowerCMS Xプラグインで機械学習(ベイジアンフィルタ)を実装する(1)の続編です。前回の記事で今後の予定に挙げていた学習データのCSVインポートと削除処理を実装します。インポート/エクスポート自体はモデル設定から有効にできますが、一つのデータ群(ベイズ文章)を解析して複数のモデルにデータを保存する場合はどうでしょうか。こういったケースはコールバック処理で実現できます。
インポート対応
プラグインのconfig.jsonにベイズ文章モデルへインポートした際に実行されるpost_importコールバックを追記します。
"callbacks": {
"naivebayes_post_import_bayes_term_block": {
"bayes_term_block": {
"post_import": {
"component": "NaiveBayes",
"priority": 1,
"method": "post_import_bayes_term_block"
}
}
}
}
コールバックは種類が多く、利用するコールバックによって引数が変わるため、慣れるまではプラグインのスケルトンの作成 (PluginStarterプラグイン)を使ってコールバックプラグインを作成して参考にすることをおすすめします。
インポート用CSVの作成について
インポート用CSVは、オブジェクト一覧画面のリストアクションよりエクスポートしたCSVを雛形として利用すると良いでしょう。ID列が存在していると既存オブジェクトの上書きで、ID列を削除すると新規作成になります。今回はインポートによるデータ投入を目的にしていますので、ID列を削除して雛形として利用します。
インポート処理
具体的な学習処理まで載せると長くなるためコードは一部抜粋になりますが、以下コードのようにインポートのコールバックで学習させてオブジェクトのカラムを学習済みに更新しています。
/**
* ベイズ文章インポートのコールバック
*
* @param object $cb コールバックオブジェクト
* @param Prototype $app アプリケーション
* @param object $obj 保存後のオブジェクト
* @param object $original 保存前のオブジェクト
*
* @return bool
*/
public function post_import_bayes_term_block(&$cb, $app, &$obj, $original) {
// インポートした文章が未学習ならテキストからベイズ単語/カテゴリに登録
if (!$obj->is_trained) {
// 文章を学習させて学習済みに更新
$this->train($app, $obj);
$obj->is_trained(1);
$obj->save();
}
return true;
}
/**
* ベイズ文章から学習
*
* @param Prototype $app アプリケーション
* @param object $termBlockObject ベイズ文章オブジェクト
*/
public function train($app, $termBlockObject) {
// 文章を単語ベクトルに変換
$vec = $this->text2vec($termBlockObject->texts);
// 文章に含まれる単語数を取得
$termCount = 0;
foreach ($vec as $word => $count) {
$termCount += $count;
}
if (!$termCount) {
return;
}
// ベイズ文章のリレーションから不要なオブジェクトを削除
$this->removeObjectFromTermBlock($app, $termBlockObject);
// カテゴリを保存
$categoryId = $this->saveCategory($app, $termBlockObject, $termCount);
// ベイズ単語とベイズ単語カテゴリを保存
$order = 1;
foreach ($vec as $word => $count) {
$this->saveTerm($app, $word, $termBlockObject->id, $categoryId, $count, $order);
$order++;
}
}
学習データの削除対応
ここまで書いてきたように、一つのベイズ文章から複数のモデル(ベイズ単語やカテゴリ)にデータを保存しています。つまり、一つのオブジェクトを削除したら関連するオブジェクトを削除しないと、DBに不要なレコード(どこにも使用されていないデータ)が溜まってしまいます。ここでもコールバックで不要なオブジェクトを検索して削除することで対応します。
4モデルへ相互にデータを保存しているため、コールバックは4モデルに設定します。
"callbacks": {
"naivebayes_pre_delete_bayes_term_block": {
"bayes_term_block": {
"pre_delete": {
"component": "NaiveBayes",
"priority": 1,
"method": "pre_delete_bayes_term_block"
}
}
},
"naivebayes_pre_delete_bayes_term": {
"bayes_term": {
"pre_delete": {
"component": "NaiveBayes",
"priority": 1,
"method": "pre_delete_bayes_term"
}
}
},
"naivebayes_pre_delete_bayes_category": {
"bayes_category": {
"pre_delete": {
"component": "NaiveBayes",
"priority": 1,
"method": "pre_delete_bayes_category"
}
}
},
"naivebayes_pre_delete_bayes_term_category": {
"bayes_term_category": {
"pre_delete": {
"component": "NaiveBayes",
"priority": 1,
"method": "pre_delete_bayes_term_category"
}
}
}
}
不要なオブジェクトを検索する前準備
ベイズ文章とベイズ単語・カテゴリの関連をmt_relationテーブルに保存します。mt_relationテーブルとはPowerCMS X標準でも記事とカテゴリやアセットなどの関連付けの保存に利用されているテーブルです。文章学習時にリレーションに関連付けを保存しておくことで、それぞれのオブジェクト削除時に検索できるようにします。
リレーションの保存方法
mt_relationテーブルはクラスPrototypeのset_relations関数か、クラスPADOのget_by_key関数で取得(更新の場合)or作成どちらでも構いません。まとめて保存する場合はset_relations関数を利用すると簡単です。
// set_relations関数でまとめて保存
// 第2引数でリレーション先ID(to_id)の配列を渡す
$categoryIds = [1, 2, 3, 4];
$args = [
'name' => 'bayes_category',
'from_obj' => 'bayes_term_block',
'from_id' => $termBlockObject->id,
'to_obj' => 'bayes_category'
];
$app->set_relations($args, $categoryIds);
// get_by_key関数で取得or作成して保存
// set_relations関数と違ってorderを自分で設定する必要がある
$termIds = [1, 2, 3, 4];
$order = 1;
foreach ($termIds as $termId) {
$relation = $app->db->model('relation')->get_by_key([
'name' => 'bayes_category',
'from_obj' => 'bayes_term_block',
'from_id' => $termBlockObject->id,
'to_obj' => 'bayes_term',
'to_id' => $termId,
'order' => $order
]);
$relation->save();
$order++;
}
set_relations関数でも内部的にget_by_key関数が呼ばれていますが、BULK INSERTしているため大量のレコードを保存したらパフォーマンスにも違いが出るかもしれません。
削除処理
以下はベイズ文章削除時の処理を一部抜粋しています。削除処理の詳細は割愛しますが、オブジェクトの編集画面(パラメータ__mode=view)と一覧画面(__mode=list)で$objで取得できるオブジェクトに違いがある点に注意が必要です。
具体的には一覧から削除した時のコールバックで$obj->idなどごく一部のカラム値は取得できますが、$obj->category_nameなどそれ以外のカラムが取得できません。IDは取得できるので、クラスPADOのload関数でオブジェクトをロードすることで一覧・編集画面どちらから削除しても同じ値を取得可能です。
/**
* ベイズ文章オブジェクト削除前のコールバック
*
* @param object $cb コールバックオブジェクト
* @param Prototype $app アプリケーション
* @param object $obj 削除後のオブジェクト
* @param object $original 削除前のオブジェクト
*
* @return bool
*/
public function pre_delete_bayes_term_block(&$cb, $app, &$obj, $original) {
$workspaceId = (int) $app->param('workspace_id');
// 一覧から削除すると$objで一部カラムしか取得できないため、DBからベイズ文章オブジェクトをロード
$termBlock = $app->db->model('bayes_term_block')->load([
'id' => $obj->id,
'workspace_id' => $workspaceId
]);
$obj = $termBlock[0] ?? null;
// ベイズ文章のリレーションから不要なオブジェクトを削除
$this->removeObjectFromTermBlock($app, $obj);
return true;
}
まとめ
一つのデータ群から複数のテーブルに分けてデータを保存すると削除処理が複雑になります。ただ、学習データを用いた演算処理速度や、管理上分かれているメリットもあるため設計の重要性を再認識しました。今後機能拡張していく過程でテーブルを見直すことがあるかもしれません。気づきがあればまたシェアしたいと思います。
今後の予定
- 非同期でデータ分類の取得(RESTful APIのエンドポイント追加など)
- 複数のフィルタ対応(親子カテゴリの追加など)