(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を経由することで機械学習推論を行う際にハードウェア高速化を行うことが可能になります。 f:id:n_atmark:20181124002626p:plain

NNAPIに入門する前に

NNAPIの抽象化

  • NNAPIは4つの主な抽象化を使用しています

  • モデル: 数学的操作のグラフ

  • コンパイル: 上記モデルを下位レベルのコードにコンパイル
  • メモリ: メモリマップされたファイル、メモリバッファをさす
    • アプリケーションは通常、モデルを定義するのに必要なすべてのテンソルを含む1つの共有メモリバッファを作成
    • メモリバッファを使用して、実行インスタンスの入力と出力を格納できる
  • 実行: 入力セットに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が推論を行うまでのフローは以下のようになっています f:id:n_atmark:20181124003539p:plain

NNAPIに入門する

今回、NNAPIに入門するためにAndroidNDKのnn_sampleを例に説明します。 github.com

2つの入力を受け取って、1つの出力を返すサンプルになっています。 f:id:n_atmark:20181124010135p:plain

モデル構造は加算と乗算だけのシンプルなものになっています。 f:id:n_atmark:20181124011443p:plain

レーニングデータへのアクセス

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でコード進行をコーディングする」です。