X-CUBE-AIを使った推論プログラムの作り方をわかりやすく解説

X-CUBE-AIを使った推論プログラムの作り方がわからない!という方のために、実際にSTM32マイコン上で推論させるまでの方法をわかりやすく解説します。

 

公式のX-CUBE-AIのGettingStarted動画がありますが、実は学習済みモデルの検証しか行っていません。また、STM32CubeIDE(CubeMX)を使っているのに初期化コードの自動生成もver.3.4.0では空っぽです。

現状、X-CUBE-AIを使って推論プログラムの作り方がわからないという人が多いのではないでしょうか。

本記事では、そういった方々の課題を解決します。

 

ゴールの確認

まずはゴールを確認しましょう。今回はSTM32マイコン上でXORを1秒間隔で推論し、その結果をprintf(UART)に出力するプログラムを作成します。

このプログラム作成を通してX-CUBE-AIを使った推論プログラムの開発方法を学びましょう

 

前提条件

  • STM32Cube.IDE ver.1.00
  • NUCLEO-F756ZG

 

XORの学習済みモデルを作る

XORの学習済みモデルはKerasで作りました。ソースコードは下記を参考にしてください。

 

import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Activation
from keras.optimizers import SGD

np.random.seed(123)

# 入出力
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
Y = np.array([[0], [1], [1], [0]])

# モデル
model = Sequential()
model.add(Dense(input_dim=2, units=2))
model.add(Activation('sigmoid'))
model.add(Dense(units=1))
model.add(Activation('sigmoid'))
model.compile(loss='binary_crossentropy', optimizer=SGD(lr=0.1))

# 学習
model.fit(X, Y, epochs=4000, batch_size=4)

# 学習結果の出力
prob = model.predict_proba(X, batch_size=4)
print(prob)

# 学習済みモデルをHDFファイルに保存
model.save('xor.h5')

 

これで学習済みモデルが『xor.h5』に保存されます。

ちなみに推論結果は下記のようになりました。上から[0, 0]、[0, 1]、[1, 0]、[1, 1]を入力とした時の推論結果です。もう少し学習させた方が良い気がしますが、本質的ではないので今回はこのまま進めます。

 

[[0.08178978]
 [0.8161957 ]
 [0.81678647]
 [0.243449 ]]

 

 

プロジェクトの作成

続いてプロジェクトを作成していきます。プロジェクトの作成方法は公式動画とほぼ同じなので、そちらを参考になさってください。

ここでは動画と異なる点だけ説明していきます。

 

公式動画はこちら

 

X-CUBE-AIの設定

2か所設定します。

 

  • Artificial Intelligence X-CUBE-AIを有効化
  • 学習済みモデル『xor.h5』を追加

 

X-CUBE-AIの設定

 

本来であればArticficial Intelligence Applicationにもチェックを入れて、ApplicationTemplateを使いたいところなのですが、ApplicationTemplateは現在意味を持ちません。詳しくは下記の記事をご覧になってください。

X-CUBE-AI/Applicationの3つのアプリケーションの違いをわかりやすく解説

 

インクルードパスを設定

下記のインクルードパスが自動的に設定されないことがあるので、確認してください。入ってなければ追加しておきましょう。

 

../Drivers/CMSIS/DSP/Include
../Middlewares/ST/AI/AI/data
../Middlewares/ST/AI/AI/include

 

インクルードパス

 

推論プログラムの実装

準備が整いましたので、プログラムを書いていきましょう。

ここでは使い方のイメージを掴むことを重視して、エラー処理や美しい構造などは気にしないで進めていきます。これらは後ほどやります。

 

推論プログラムの書き方は大きく分けて5ステップになります。順に説明していきますね。

 

プログラム手順

 

(1)各種変数の宣言

まずは推論プログラムが使用する変数を宣言します。

 

/* USER CODE BEGIN PV */
static ai_handle xor = AI_HANDLE_NULL;

AI_ALIGNED(4)
static ai_u8 activations[AI_XOR_DATA_ACTIVATIONS_SIZE];

static ai_buffer ai_input[AI_XOR_IN_NUM] = { AI_XOR_IN_1 };
static ai_buffer ai_output[AI_XOR_OUT_NUM] = { AI_XOR_OUT_1 };

static ai_float in_data[2];
static ai_float out_data[1];

/* USER CODE END PV */

 

各種変数の宣言では、X-CUBE-AIがモデルをC言語に変換した際に、一緒に生成されるマクロや関数を使います。それらの名前は『AI_モデル名_…』の形式になっていて、ここではモデル名は『XOR』ですね。

書き方は決まり切っているため、おまじないと思ってもらっても問題ありません。

一応説明すると次のようになっています。

ai_handleは学習済みモデルのハンドル(参照)です。生成の時に代入するのでここではNULLを指定しています。

activationsは活性化関数で使用するメモリ領域です。AI_ALIGNED(4)はマニュアルのサンプルコードに説明なしに書いてあったのですが、ALIGNEDなのでデータを確保するサイズを4バイト固定にして欲しいということなのでしょう。

ai_bufferは入力/出力データのデータ構造とデータへの参照を持ちます。

ai_floatはX-CUBE-AIアプリケーションが使うデータ型で、中身はfloatです。この配列が実際の入力/出力データとして使われます。現在は他のデータ型は使用不可になっています。

 

(2)モデルの生成

宣言した各種変数を使ってモデルを生成します。モデルの生成はcreate()関数を使います。

 

ai_xor_create(&xor, AI_XOR_DATA_CONFIG);

 

これだけです。簡単ですね。返り値にエラーが入っていることがるので、エラー処理をした方が良いのですが、前述した通り後回しにしておきましょう。

 

(3)モデルの初期化

続いてモデルの初期化を行います。初期化はinit()関数を使います。

 

const ai_network_params xor_params = {
        AI_XOR_DATA_WEIGHTS(ai_xor_data_weights_get()),
        AI_XOR_DATA_ACTIVATIONS(activations)
};
ai_xor_init(xor, &xor_params);

 

初期化の際には、ニューラルネットワークのパラメータ(ai_network_params)を渡す必要があるため、init()の直前で作って渡しています。

 

(4)入出力データの初期化

入出力データ(ai_buffer)も初期化が必要です。

 

ai_input[0].n_batches = 1;
ai_input[0].data = AI_HANDLE_PTR(&in_data);
ai_output[0].n_batches = 1;
ai_output[0].data = AI_HANDLE_PTR(&out_data);

 

ai_inputとai_outputが配列になっていますね。これは将来拡張のためで、複数の入力/出力を扱えるようにするためのものだそうです。現状は1セットなので[0]にしておけば大丈夫です。

n_batchesは、バッチ数です。今回は1ですね。

dataは、入力/出力データへの参照を入れます。

 

(5)推論の実行

ついに推論を実行します。推論の実行はrun()関数を使います。

 

run()の前に、入力データに値を入れておきます。今回は実行の度に入力が[0, 0]、[0, 1]、[1, 0]、[1, 1]と変わるようにしました。

 

switch (index) {
case 0:
	in_data[0] = 0.0f;
	in_data[1] = 0.0f;
	break;
case 1:
	in_data[0] = 0.0f;
	in_data[1] = 1.0f;
	break;
case 2:
	in_data[0] = 1.0f;
	in_data[1] = 0.0f;
	break;
default:
	in_data[0] = 1.0f;
	in_data[1] = 1.0f;
	break;
}
index++;
if (index > 3) {
	index = 0;
}

 

そしてrun()の呼び出しです。実行の度にprintfしています。

 

ai_xor_run(xor, &ai_input[0], &ai_output[0]);
printf("input[%1.3f, %1.3f] -> output[%1.3f]\r\n", in_data[0], in_data[1], out_data[0]);

 

これを実行してみましょう。

 

XORアプリのターミナル出力

 

Kerasの実行結果と同じ結果が出力されました!これで一安心ですね。

 

リファクタリング

とりあえず動きましたが、正直あまり美しいコードとは言えませんね。このままだと使い辛いのでリファクタリングしましょうか。ついでに簡単なエラー処理も入れておきましょう。

 

皆さんもおそらくXORをカプセル化したいと考えるはずです。なのでXorNNというクラスにXORの処理をうまくまとめましょう。NNはNeural Networkの略で付けました。

 

上記の処理手順を眺めて、どうまとめるか考えてみます。

 

(1)の各種変数の宣言は単にメンバ変数と考えることができそうです。これはXorNN.cの中に隠ぺい化しましょう。

(2)~(4)は結局やってることは初期化ですよね。これはXorNN_init()メソッドを作ってそこでまとめてしまいましょう。

残った(5)の推論の実行は、まとめるものも特にありませんが、1つメソッドを用意してその中で単にrun()を呼び出すようにしましょう。名前はXorNN_execute()にします。run()だと永続的に実行されるニュアンスが含まれ違和感を感じるので、より単一の実行のイメージがあるexecute()にしました。

後、入力/出力データは利用プログラム側が直接配列を触りたいので、ゲッターを用意してあげましょうか。

 

まとめると下記の画像のようなクラスになります。init()とexecute()の返り値がbool型ですが、これは処理が成功した時のみtrueを返すようにするためです。

 

XorNNクラス図

 

これをXorNN.hにすると次のようになります。

 

#include <stdio.h>
#include <stdbool.h>
#include "app_x-cube-ai.h"
#include "xor.h"

extern bool XorNN_init();

extern bool XorNN_execute();

extern ai_float *XorNN_getInputs();

extern ai_float *XorNN_GetOutputs();

 

特に説明はいりませんね。ではこの実装のXorNN.cを見てみましょう。

 

#include <XorNN.h>

#define BATCH_SIZE					1

// xor handle
static ai_handle xor = AI_HANDLE_NULL;

// activations buffer
AI_ALIGNED(4)
static ai_u8 activations[AI_XOR_DATA_ACTIVATIONS_SIZE];

// input/output buffer
static ai_buffer ai_input[AI_XOR_IN_NUM] = { AI_XOR_IN_1 };
static ai_buffer ai_output[AI_XOR_OUT_NUM] = { AI_XOR_OUT_1 };

// input/output data
static ai_float in_data[2];
static ai_float out_data[1];

bool XorNN_init() {
    int ret = true;

    // create
    ai_error xor_error = ai_xor_create(&xor, AI_XOR_DATA_CONFIG);
    if (xor_error.type != AI_ERROR_NONE) {
        printf("XOR creating failure! type=%d,code=%d\r\n", xor_error.type, xor_error.code);
        ret = false;
    }

    // init
    if (ret) {
        const ai_network_params xor_params = {
                AI_XOR_DATA_WEIGHTS(ai_xor_data_weights_get()),
                AI_XOR_DATA_ACTIVATIONS(activations)
        };
        if (!ai_xor_init(xor, &xor_params)) {
            xor_error = ai_xor_get_error(xor);
            printf("XOR creating failure! type=%d,code=%d\r\n", xor_error.type, xor_error.code);
            ret = false;
        }
    }

    // setup input and output
    if (ret) {
        ai_input[0].n_batches = BATCH_SIZE;
        ai_input[0].data = AI_HANDLE_PTR(&in_data);
        ai_output[0].n_batches = BATCH_SIZE;
        ai_output[0].data = AI_HANDLE_PTR(&out_data);
    }

    return ret;
}

bool XorNN_execute() {
    bool ret = true;

    int batched = ai_xor_run(xor, &ai_input[0], &ai_output[0]);
    if (batched != BATCH_SIZE) {
        ai_error xor_error = ai_xor_get_error(xor);
        printf("XOR running failure! type=%d,code=%d\r\n", xor_error.type, xor_error.code);
        ret = false;
    }

    return ret;
}

ai_float *XorNN_getInputs() {
    return in_data;
}

ai_float *XorNN_GetOutputs() {
    return out_data;
}

 

簡単なエラー処理として、都度printfするようにしました。それ以外のところは特に説明は不要と思います。

このXorNNを使うmain.cは次のようになります。

 

bool isXORInitialized = XorNN_init();
  :
ai_float *input = XorNN_getInputs();
  :
  if (isXORInitialized) {
  	bool isSuccessful = XorNN_execute();
  	if (isSuccessful) {
  		printf("input[%1.3f, %1.3f] -> output[%1.3f]\r\n", input[0], input[1], XorNN_GetOutputs()[0]);
  	} else {
  		// do error
  	}
  }

 

だいぶスッキリした感じがしますね!

解説は以上になります。他の学習モデルでも同じ手順で作れますので、ぜひ参考になさってください。お疲れ様でした!