(FUN Advent Calendar 2018) 入門Android Neural Networks API
この記事は、FUN Advent Calendar 2018 7日目の記事です。
前置き
今年もアドベントカレンダーの時期がやってきましたね。@n_atmarkです。
今年は、卒業研究で扱っているAndroid Neural Networks API(以下NNAPI)について書こうと思います。
入門 Android Neural Networks API
Android Neural Networks APIとは
Android8.1 (APIレベル27)から利用可能になったオンデバイス上で機械学習推論を行うためのAPIで、NNAPIを経由することで機械学習推論を行う際にハードウェア高速化を行うことが可能になります。
NNAPIに入門する前に
NNAPIの抽象化
NNAPIは4つの主な抽象化を使用しています
モデル: 数学的操作のグラフ
- コンパイル: 上記モデルを下位レベルのコードにコンパイル
- メモリ: メモリマップされたファイル、メモリバッファをさす
- 実行: 入力セットにNNAPIモデルを提供し、結果を収集するためのインスタンス
NNAPIのモデル抽象化
さらに、NNAPIのモデルはオペランドとオペレーションという2つの抽象化がされています
- オペランド (グラフの定義に使用されるデータオブジェクト)
- オペレーション
NNAPIがサポートする操作(オペレーション)
NNAPIがサポートする操作として以下のようなものがあります。
- 数学演算
- ANEURALNETWORKS_ADD
- ANEURALNETWORKS_MUL
- ANEURALNETWORKS_FLOOR
- 配列演算
- ANEURALNETWORKS_CONCATENATION
- ANEURALNETWORKS_DEPTH_TO_SPACE
- ANEURALNETWORKS_DEQUANTIZE
- ANEURALNETWOKRS_RESHAPE
- ANEURALNETWORKS_SPACE_TO_DEPTH
- イメージ操作
- ANEURALNETWORKS_RESIZE_BILINEAR
- 検索操作
- ANEURALNETWORKS_HASHTABLE_LOOKUP
- ANEURALNETWORKS_EMBEDDING_LOOKUP
- 正規化操作
- ANEURALNETWORKS_L2_NORMALIZATION
- ANEURALNETWORKS_LOCAL_RESPONSE_NORMALIZATION
- 畳み込み演算
- ANEURALNETWORKS_CONV_2D
- ANEURALNETWORKS_DEPTHWISE_CONV_2D
- プーリング操作
- ANEURALNETWORKS_AVERAGE_POOL_2D
- ANEURALNETWORKS_L2_POOL_2D
- ANEURALNETWORKS_MAX_POOL_2D
- アクティベーション操作
- ANEURALNETWORKS_LOGISTIC
- ANEURALNETWORKS_RELU
- ANEURALNETWORKS_RELU1
- ANUERALNETWORKS_RELU6
- ANEURALNETOWORKS_SOFTMAX
- ANEURALNETWORKS_TANH
- その他の操作
- ANEURALNETWORKS_FULLY_CONNECTED
- ANEURALNETWORKS_LSH_PROJECTION
- ANEURALNETWORKS_LSTM
- ANEURALNETWORKS_RNN
- ANEURALNETWORKS_SVDF
NNAPI Flow
NNAPIが推論を行うまでのフローは以下のようになっています
NNAPIに入門する
今回、NNAPIに入門するためにAndroidNDKのnn_sampleを例に説明します。 github.com
2つの入力を受け取って、1つの出力を返すサンプルになっています。
モデル構造は加算と乗算だけのシンプルなものになっています。
トレーニングデータへのアクセス
NNAPIが提供している ANeuralNetworksMemory_createFromFd
関数を用いて、ファイルディスクリプタを渡すことで、訓練データをファイルから取得し、メモリバッファを作成します。
// 訓練されたデータを含むファイルからメモリバッファを作成 ANeuralNetworksMemory * memoryModel = NULL; int fd = open(“training_data”, O_RDONLY); ANeuralNetworksMemory_createFromFd(size + offset, PROT_READ, fd, 0, &memoryModel);
オペランドを追加する
NNAPIが提供している ANeuralNetworksModel_addOperand
関数を用いて、モデルにオペランドを追加します
// 空のモデルを定義する ANeuralNetworksModel * model; ANeuralNetworksModel_create(&model); // [200]のテンソルを定義 ANeuralNetworksOperandType float32TensorType{ .type = ANEURALNETWORKS_TENSOR_FLOAT32, .dimensionCount = 1, .dimensions = dimensions, .scale = 0.0f, .zeroPoint = 0, }; // アクティベーション関数用のオペランドを定義 ANeuralNetworksOperandType scalarInt32Type{ .type = ANEURALNETWORKS_INT32, .dimensionCount = 0, .dimensions = nullptr, .scale = 0.0f, .zeroPoint = 0, }; ANeuralNetworksModel_addOperand(model, &scalarInt32Type); // for NONE activation function ANeuralNetworksModel_addOperand(model, &float32TensorType); // tensor0 ANeuralNetworksModel_addOperand(model, &float32TensorType); // tensor1 ANeuralNetworksModel_addOperand(model, &float32TensorType); // tensor2 ANeuralNetworksModel_addOperand(model, &float32TensorType); // tensor3 ANeuralNetworksModel_addOperand(model, &float32TensorType); // intermediateOutput0 ANeuralNetworksModel_addOperand(model, &float32TensorType); // intermediateOutput1 ANeuralNetworksModel_addOperand(model, &float32TensorType); // multiplierOutput // addOperandした順番に、オペランドにindexがついてる uint32_t fusedActivationFuncNone = 0; uint32_t tensor0 = 1; uint32_t tensor1 = 2; uint32_t tensor2 = 3; uint32_t tensor3 = 4; uint32_t intermediateOutput0 = 5; uint32_t intermediateOutput1 = 6; uint32_t multiplierOutput = 7;
学習済みデータから取得した、重みやバイアスなどの定数値をオペランドの値に設定する
ANeuralNetworksModel_setOperandValue
関数、ANeuralNetworksModel_setOperandValueFromMemory
関数を利用して、以下のようにtensor0、tensor2に学習済みデータから取得した定数値を指定します。
int32_t fusedActivationCodeValue = ANEURALNETWORKS_FUSED_NONE; ANeuralNetworksModel_setOperandValue(model, fusedActivationFuncNone, &fusedActivationCodeValue, sizeof(fusedActivationCodeValue)); // 非活性化関数FUSED_NONEを設定 ANeuralNetworksModel_setOperandValueFromMemory(model, tensor0, memoryModel, offset, tensorSize * sizeof(float)); // tensor0に学習済みデータから取得した定数値を指定 ANeuralNetworksModel_setOperandValueFromMemory(model, tensor2, memoryModel, offset + tensorSize * sizeof(float), tensorSize * sizeof(float)); // tensor2に学習済みデータから取得した定数値を指定
オペレーションを追加する
ANeuralNetworksModel_addOperation
関数を用いてオペレーションを追加します
std::vector<uint32_t> add1InputOperands = { tensor0, tensor1, fusedActivationFuncNone }; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, add1InputOperands.size(), add1InputOperands.data(), 1, &intermediateOutput0); // tensor0とtensor1を加算 std::vector<uint32_t> add2InputOperands = { tensor2, tensor3, fusedActivationFuncNone }; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, add2InputOperands.size(), add2InputOperands.data(), 1, &intermediateOutput1); // tensor2とtensor3を加算 std::vector<uint32_t> mulInputOperands = { intermediateOutput0, intermediateOutput1, fusedActivationFuncNone }; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_MUL, mulInputOperands.size(), mulInputOperands.data(), 1, &multiplierOutput); // intermediateOutput0とintermediateOutput1を乗算
入力と出力として扱うオペランドを指定する
ANeuralNetworksModel_identifyInputsAndOutputs
関数を用いて、入力と出力として扱うオペランドを指定します
// 入力と出力として扱うオペランドを指定する std::vector<uint32_t> modelInputOperands = { tensor1, tensor3, }; ANeuralNetworksModel_identifyInputsAndOutputs(model, modelInputOperands.size(), modelInputOperands.data(), 1, &multiplierOutput); ANeuralNetworksModel_finish(model);
コンパイラのインスタンスを作成
ANeuralNetworksCompilation_create
関数を用いて、上で作成したモデルからコンパイラのインスタンスを作成します
ANeuralNetworksCompilation *compilation; ANeuralNetworksCompilation_create(model, &compilation); ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER); ANeuralNetworksCompilation_finish(compilation);
ここで、ANeuralNetworksCompilation_setPreference
関数を用いることでコンパイラのオプションを指定することができます。指定できるものは以下のようなものがあります。
- ANEURALNETWORKS_PREFER_LOW_POWER
- バッテリー使用率を最小限に抑えて実行(頻繁に実行される場合に適している)
- ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER
- より多くの電力消費が発生したとしても、できるだけ早く回答を返す
- ANEURALNETWORKS_PREFER_SUSTAINED_SPEED
- 連続するフレームのスループットを最大にする(カメラからの連続フレームを処理する場合など)
モデル実行インスタンスを作成する
上で作成したコンパイラのインスタンスからANeuralNetworksExecution_create
関数を用いて実行インスタンスを作成します。
ANeuralNetworksExecution *execution; ANeuralNetworksExecution_create(compilation, &execution)
入力値を指定する
ANeuralNetworksExecution_setInput
関数を用いて、入力を設定します
std::vector<float> inputTensor1; std::fill(inputTensor1.data(), inputTensor1.data() + tensorSize, inputValue1); //tensor1の形に合わせて[200]分をinputValue1で埋める ANeuralNetworksExecution_setInput(execution, 0, nullptr, inputTensor1.data(), tensorSize * sizeof(float)); //ANeuralNetworksModel_identifyInputsAndOutputsで指定した入力ユニットのindex ANeuralNetworksExecution_setInputFromMemory(execution, 1, nullptr, memoryInput2, 0, tensorSize * sizeof(float)); //memoryからも指定可能
出力先を指定する
ANeuralNetworksExecution_setOutput
関数を用いて出力先を指定します
ANeuralNetworksExecution_setOutputFromMemory(execution, 0, nullptr, memoryOutput, 0, tensorSize* sizeof(float)); //ANeuralNetworksModel_identifyInputsAndOutputsで指定した出力ユニットのindex // memoryを使用しない場合 float32 output[200]; ANeuralNetworksExecution_setOutput(execution, 0, nullptr, output, tensorSize* sizeof(float));
モデルを実行する
ANeuralNetworksExecution_startCompute
関数を用いることで、モデルを実行できます
ANeuralNetworksEvent *event = nullptr; ANeuralNetworksExecution_startCompute(execution, &event); // 推論の実行 // 実行の完了を待機 ANeuralNetworksEvent_wait(event); ANeuralNetworksEvent_free(event); ANeuralNetworksExecution_free(execution)
かけ足でしたが、ここまでの操作によってNNAPIを持ちいて機械学習推論を実行することができるようになりました。
まとめ
今回NNAPIについて紹介しましたが、実際は自分でNNAPIを直接操作することは殆どなく、モバイル向け機械学習フレームワークのML KitやTensorFlow Liteを用いる例が多くあまり触れられない部分だと思います。
おそらくこの記事を読んでNNAPIをさわるぞ!という人も少ないかと思いますが、もし興味があれば記事中でも紹介した googlesamples/android-ndk をみてみたり、さらにNNAPIの内部実装 android/frameworks/ml/nn/runtime/NeuralNetworks.cppをのぞいてみると面白いかもしれません。
明日は@akashi_akatsuki明石君の、「Prologでコード進行をコーディングする」です。