はじめに
2019年6月にMarkLogic10がリリースされ、GPU対応およびCNTKが実装されました。今回はこの新機能を使って多層パーセプトロンによる訓練と推論を実装してみようと思います。
例題は定番のアイリス分類とします。言語はXQueryです。
ソースコードは以下に置きました。
https://github.com/t2hk/marklogic-mlp/
MarkLogicのCNTK
MarkLogicはCNTKを実装しています。APIはオリジナルのCNTKに類似しているものの、XQueryという言語やMarkLogic自体の特性の影響からかCNTKオリジナルとは異なる部分も多々あります。
それでも、XQueryで機械学習のアルゴリズムを実装してモデル構築でき、データベース上のデータを直接学習・推論に利用できるのは便利です。
学習データとテストデータをMarkLogicにロードする
アイリスのデータセットを学習用とテスト用にわけます。今回は学習用に120件、テスト用に30件としました。
データ形式はCSVで、以下のようなデータになります(一部抜粋)。
sepal_length | sepal_width | petal_length | petal_width | species |
---|---|---|---|---|
5.1 | 3.5 | 1.4 | 0.2 | setosa |
4.9 | 3.0 | 1.4 | 0.2 | setosa |
4.7 | 3.2 | 1.3 | 0.2 | setosa |
7.0 | 3.2 | 4.7 | 1.4 | versicolor |
6.4 | 3.2 | 4.5 | 1.5 | versicolor |
6.9 | 3.1 | 4.9 | 1.5 | versicolor |
用意したCSVファイルをMarkLogic用のデータロードツール「MLCP」を使用してXML変換してロードします。
学習データは"/iris/train/"にロードし、テストデータは"/iris/test/"にロードしています。
MLCPのオプションは以下のような感じです。学習データのロード用の設定例です。
import \
-mode local \
-host [MarkLogicサーバ] \
-port [XDBCポート番号] \
-username [ユーザ名] -password [パスワード] \
-input_file_type delimited_text \
-input_file_path [学習データのフルパス] \
-delimited_root_name iris \
-document_type xml \
-output_uri_replace "[学習データのフルパス], '/iris/train/iris'" \
-generate_uri true \
-output_uri_suffix .xml
学習用のXQuery
ソースコードは以下になります。
https://github.com/t2hk/marklogic-mlp/blob/master/ML10_MLP_Train.xqy
MLPの入力層と隠れ層を関数化
隠れ層のレイヤー数を任意とするため関数化して再帰呼び出し可能としました。
今回はアイリス分類なので、入力層の入力数は4となります(がく片の長さと幅、花びらの長さと幅)。
隠れ層は全結合のDenseを使用しています。
(:
マルチパーセプトロンの入力層と隠れ層を構築する。出力層は含まない。
input_variable : 入力層の入力数
num_hidden_layers : 隠れ層のレイヤー数
dense_map : アクティベーションのアルゴリズムや隠れ層の出力数などのパラメータを保持するmap
:)
declare function local:mlp($input_variable as cntk:variable, $num_hidden_layers as xs:integer, $dense_map as map:map){
if($num_hidden_layers = 0) then cntk:dense-layer($input_variable, $dense_map)
else cntk:dense-layer(local:mlp($input_variable, ($num_hidden_layers - 1), $dense_map), $dense_map)
};
パラメータ
MLPの各種パラメータを変数定義しています。隠れ層の数や出力数は可変です。
let $input_dims := 4 (:入力数:)
let $num_classes := 3 (:分類数:)
let $num_hidden_layers := 2 (:隠れ層の数:)
let $hidden_layers_dim := 10 (:隠れ層の出力数:)
let $hidden_activation := "relu"
let $minibatch_size := 100 (:ミニバッチの実行回数:)
学習データの作成
ロードした学習データを1x4の行列として読み込みます。
let $input_variable := cntk:input-variable(cntk:shape(( 1, $input_dims)), "float")
let $train_data :=
for $x in xdmp:directory("/iris/train/", "infinity")
return ($x/iris/sepal_length/text(), $x/iris/sepal_width/text(), $x/iris/petal_length/text(), $x/iris/petal_width/text())
let $input_value := cntk:batch(cntk:shape((1, $input_dims)), json:to-array($train_data))
正解ラベルの作成
同様に正解ラベルを作成します。正解ラベルは分類名そのものではなく、One-Hot表現としています。
Pythonなどなら楽なのですが、XQueryでOne-Hot表現を作り出すのは泥臭く力業になります。
私が良い方法を知らないだけだと思いますが・・・
let $label_variable := cntk:input-variable(cntk:shape(( 1, $num_classes)), "float")
let $labels :=
for $x in xdmp:directory("/iris/train/", "infinity")
let $_label := $x/iris/species/text()
return if ($_label = "setosa") then (1,0,0)
else if ($_label = "virginica") then (0,1,0)
else (0,0,1)
let $label_value := cntk:batch(cntk:shape((1,$num_classes)), json:to-array(($labels)))
MLPを組み立てる
関数定義したMLP関数を使用し、出力層も含めて構築します。
アクティベーションや出力数などはmapにまとめて設定します。
(:入力層と隠れ層を組み立てる:)
let $_model_map := map:map()
let $_ := map:put($_model_map, "activation", $hidden_activation)
let $_ := map:put($_model_map, "output-shape", cntk:shape((1, $hidden_layers_dim)))
let $_model := local:mlp($input_variable, ($num_hidden_layers), $_model_map)
(:出力層を組み立てる:)
let $_output_map := map:map()
let $_ := map:put($_output_map, "activation", $hidden_activation)
let $_ := map:put($_output_map, "output-shape", cntk:shape((1, $num_classes)))
let $model := cntk:dense-layer($_model, $_output_map)
訓練器を組み立てる
評価関数などを設定し、訓練器を組み立てます。
(:予測結果と正解の交差エントロピー誤差を求める:)
let $loss := cntk:cross-entropy-with-softmax(
$model,
$label_variable,
cntk:axis(-1))
(:予測結果の評価:)
let $err := cntk:classification-error($model, $label_variable, 1, cntk:axis(-1))
(:訓練器の構築:)
let $parameter:=cntk:function-parameters($model)
let $learner :=
cntk:sgd-learner(($parameter),
cntk:learning-rate-schedule-from-constant(0.1))
let $trainer := cntk:trainer($model, ($learner), $loss, $err)
ミニバッチの実行
学習データと正解ラベルを使ってミニバッチを実行します。
(:学習データと正解データからミニバッチ用データを組み立てる:)
let $input_pair := json:to-array(($input_variable, $input_value))
let $labels_pair := json:to-array(($label_variable, $label_value))
let $minibatch := json:to-array(($input_pair, $labels_pair))
(:ミニバッチ実行:)
let $train_result :=
for $i in (1 to $minibatch_size)
let $__ := cntk:train-minibatch($trainer, $minibatch, fn:false())
let $loss := cntk:previous-minibatch-loss-average($trainer)
let $evaluation := (1.0 - cntk:previous-minibatch-evaluation-average($trainer))
return json:to-array(($loss, $evaluation))
モデルの保存
学習したモデルをMarkLogicに保存して学習を終了します。
let $model_doc := cntk:function-save($model)
let $__ := xdmp:document-insert("/model/iris_model", $model_doc)
損失と正解率
学習した結果の損失と正解率を出力すると、以下のようになります。
[1.04729131062826, 0.516666666666667]
[0.685380744934082, 0.666666666666667]
[0.591424878438314, 0.808333333333333]
[0.532787577311198, 0.95]
[0.495430056254069, 0.841666666666667]
[0.482377560933431, 0.883333333333333]
[0.554083633422852, 0.666666666666667]
[0.767242622375488, 0.658333333333333]
[0.967457008361816, 0.666666666666667]
[0.577702267964681, 0.666666666666667]
[0.488694985707601, 0.925]
[0.439605490366618, 0.958333333333333]
・・・
推論用のXQuery
続いて学習したモデルを使用した推論です。
モデルの読み込み
学習したモデルを読み込み、入出力層のパラメータを設定します。
let $input_dims := 4
let $num_classes := 3
let $model := cntk:function(fn:doc("/model/iris_model")/binary())
let $input_variable := cntk:function-arguments($model)
let $output_variable := cntk:function-output($model)
推論対象のデータの読み込み
推論するデータを読み込み、入力データと正解ラベルを組み立てます。正解ラベルはOneHot表現とします。
let $test_data_docs :=
for $doc in xdmp:directory("/iris/test/", "infinity")
return $doc
(:入力データの組み立て:)
let $test_data :=
for $x in $test_data_docs
let $_test_data := ($x/iris/sepal_length/text(), $x/iris/sepal_width/text(), $x/iris/petal_length/text(), $x/iris/petal_width/text())
return $_test_data
(:正解ラベルの組み立て:)
let $test_labels :=
for $x in $test_data_docs
let $_label := $x/iris/species/text()
let $_one_hot_label :=
if ($_label = "setosa") then ("setosa", 1,0,0)
else if ($_label = "virginica") then ("virginica", 0,1,0)
else if ($_label = "versicolor") then ("versicolor", 0,0,1)
else ("unknown", 0,0,0)
return json:to-array($_one_hot_label)
推論実行
推論を実行します。
let $test_value := cntk:batch(cntk:shape((1, $input_dims)), json:to-array($test_data))
let $test_pair := json:to-array(($input_variable, $test_value))
let $output_value := cntk:evaluate(
$model,
$test_pair,
($output_variable))
let $result_array := cntk:value-to-array($output_variable, $output_value)
推論した結果は以下のようになります。OneHotで表した各分類の確率が出力されます。
確率が一番高い分類が予測結果となります。
[0.1775239, 0.4641904, 0.3582857],
[0.1355018, 0.4985289, 0.3659692],
[0.1276607, 0.5236636, 0.3486757],
[0.1344947, 0.5015103, 0.363995],
・・・
おしまい
XQueryで機械学習というもちょっと変化球な感がありますが、DB上でGPUを使用した機械学習を実現できるのはなかなか便利だと思います。
今回はXQueryを使用しましたが、MarkLogicはサーバサイドでJavascriptも実行でき、CNTKのJavascript実装も搭載されています。Javascriptに慣れている方は、こちらを利用した方が良いと思います。