Neural Networks API

Android Neural Networks API(NNAPI)は、モバイル端末上で機械学習の集中的な計算処理を実行するために設計された Android C API です。 NNAPI は、高レベルの機械学習フレームワーク(TensorFlow Lite、Caffe2 など)に機能ベースレイヤを提供することを目的としています。 この API は Android 8.1(API レベル 27)以降を実行するすべての端末で利用できます。

NNAPI は、トレーニング済みのモデルやデベロッパーが定義したモデルに Android 端末のデータを適用することで、推論処理をサポートします。 この推論処理には、画像の分類、ユーザー行動の予測、検索クエリに対する最適な回答の選択などが含まれます。

オンデバイスの推論には、次のように多くのメリットがあります。

  • レイテンシ: ネットワーク経由でリクエストを送信して応答を待つ必要がありません。 これはカメラからの連続フレームを処理する動画アプリにおいて、非常に重要な点です。
  • 可用性: ネットワークがつながらない状況でもアプリを実行できます。
  • 速度: ニューラル ネットワーク処理に特化した新しいハードウェアによって、汎用 CPU のみを使う場合に比べて計算速度が格段に速くなります。

  • プライバシー: データは端末外に流出しません。

  • 費用: 端末上ですべての計算が実行される場合は、サーバー ファームが要りません。

デベロッパーが留意すべきトレードオフもあります。

  • システム利用: ニューラル ネットワークの評価時は計算量が多くなるため、電池の消費量が増えます。 特に計算が長時間におよぶアプリなどで、この点が懸念される場合は、電池の状態をモニタリングすることをお勧めします。

  • アプリのサイズ: モデルのサイズに注意してください。 モデルは何メガバイトもの容量を占有する場合があります。 APK に含まれるモデルの容量が大きく、ユーザーに悪影響がおよぶ可能性がある場合は、アプリのインストール後にモデルをダウンロードする、モデルのサイズを削減する、クラウド上で計算を実行するなどの対策を検討してください。 なお、クラウド上でモデルを実行する機能は NNAPI にはありません。

関連サンプル:

  1. Android Neural Networks API サンプル

Neural Networks API ランタイムとは

NNAPI は機械学習のライブラリ、フレームワーク、デベロッパーが別の端末でトレーニングしたモデルを Android 端末にデプロイするために使うツールによって呼び出されることを想定して設計されています。 通常、アプリが直接使用するのは NNAPI ではなく、高レベルの機械学習フレームワークです。 そして、そのフレームワークが NNAPI を使用することで、ハードウェア アクセラレーションによる推論処理をサポート端末上で実行しています。

Android のニューラル ネットワーク ランタイムはアプリの要件や端末上のハードウェア性能に応じて、端末上の利用可能なプロセッサに計算負荷を分散させます。具体的には、専用のニューラル ネットワーク ハードウェア、グラフィックス プロセッシング ユニット(GPU)、デジタル シグナル プロセッサ(DSP)などです。

専用のベンダー ドライバがないデバイスの場合、NNAPI ランタイムは最適化されたコードを利用して CPU 上でリクエストを実行します。

図 1 は NNAPI のハイレベル システム アーキテクチャを示しています。

図 1. Android Neural Networks API のシステム アーキテクチャ

Neural Networks API プログラミング モデル

NNAPI を使用して計算をするには、まず実行する計算を定義する有向グラフを構成する必要があります。 この計算グラフに入力データ(機械学習フレームワークから渡される重みやバイアスなど)が統合されて、NNAPI ランタイム評価のモデルが構成されます。

NNAPI には 4 つの主な抽象概念があります。

  • モデル: 算術演算とトレーニング プロセスで学習した定数値の計算のグラフです。 これらの演算はニューラル ネットワークごとに固有です。 たとえば 2 次元の畳み込み、ロジスティック(シグモイド)活性化関数、正規化線形(ReLU)活性化関数などが含まれます。 モデルの生成は同期的な処理ですが、いったん正常に作成できると、さまざまなスレッドやコンパイルで再利用できます。 NNAPI では、モデルは ANeuralNetworksModel インスタンスとして表されます。
  • コンパイル: NNAPI モデルを低レベルのコードにコンパイルするための設定を表します。 コンパイルの生成は同期的な処理ですが、いったん正常に作成できると、さまざまなスレッドや実行で再利用できます。 NNAPI では、各コンパイルが ANeuralNetworksCompilation インスタンスとして表されます。

  • メモリ: 共有メモリ、メモリマップ ファイル、および同様のメモリバッファを表します。 メモリバッファを使うと、NNAPI ランタイムはデータをより効率的にドライバへ転送できます。 一般的にアプリは、モデルの定義に必要なテンソルをすべて含む共有メモリバッファを 1 つ作成します。 メモリバッファを使用して、実行インスタンスの入出力データを保存することもできます。 NNAPI では、各メモリバッファは ANeuralNetworksMemory インスタンスとして表されます。

  • 実行: NNAPI モデルを入力データのセットに適用して結果を収集するインターフェースです。 実行は非同期処理で、 1 件の実行において複数のスレッドが待機できます。 実行が完了すると、すべてのスレッドが解放されます。 NNAPI では、各実行が ANeuralNetworksExecution インスタンスとして表されます。

図 2 は基本的なプログラミング フローを示しています。

図 2. Android Neural Networks API のプログラミング フロー

このセクションの残りのパートでは、NNAPI モデルを設定して、モデルの計算とコンパイルを行い、コンパイルしたモデルを実行する手順を説明します。

トレーニング データへのアクセスを可能にする

通常、トレーニング済みの重みやバイアスのデータはファイルに保存してあります。 このデータに NNAPI ランタイムが効率よくアクセスできるようにするには、ANeuralNetworksMemory_createFromFd() 関数を呼び出し、開かれたデータファイルのファイル ディスクリプタを渡して、ANeuralNetworksMemory インスタンスを作成します。

メモリ保護フラグと、ファイル内の共有メモリ領域の開始点となるオフセットを指定することもできます。

// Create a memory buffer from the file that contains the trained data.
ANeuralNetworksMemory* mem1 = NULL;
int fd = open("training_data", O_RDONLY);
ANeuralNetworksMemory_createFromFd(file_size, PROT_READ, fd, 0, &mem1);

この例では、すべての重みに対して 1 つの ANeuralNetworksMemory インスタンスのみを使用していますが、複数のファイルに対して複数の ANeuralNetworksMemory インスタンスを使用することもできます。

モデル

NNAPI の計算において、モデルは基本構成要素です。 各モデルは 1 つ以上のオペランド演算で定義されます。

オペランド

オペランドはグラフの定義に使われるデータ オブジェクトです。 オペランドの中には、モデルへの入出力データ、ある演算から別の演算に渡されるデータを含む中間ノード、それらの演算に渡される定数が含まれます。

NNAPI モデルに追加できるオペランドのタイプは、「スカラー」と「テンソル」の 2 種類です。

スカラーは単一の数値を表します。 NNAPI では 32 ビット浮動小数点数、32 ビット整数、符号なし 32 ビット整数の型のスカラー値をサポートしています。

NNAPI を用いる演算の多くには、テンソルが含まれます。 テンソルとは N 次元の配列です。 NNAPI では 32 ビット整数、32 ビット浮動小数点数、8 ビットの量子化された値をサポートしています。

たとえば以下の図は、2 つの演算(加算のあとに乗算)からなるモデルを表しています。 このモデルは入力テンソルを取り、出力テンソルを算出します。

図 3. NNAPI モデルのオペランドの例

上記のモデルには 7 つのオペランドがあります。 これらのオペランドは、モデルに追加される順番を示すインデックスによって暗黙に特定されます。 最初に追加されるオペランドには 0 のインデックス、 2 番目のオペランドには 1 のインデックス、となります。

オペランドを追加する順序に意味はありません。 たとえば、モデルの出力オペランドが最初に追加したものにもなり得ます。 重要なのは、オペランドを参照するときに適切なインデックスを使用することです。

オペランドにはタイプがあり、 モデルに追加されるときに指定されます。 1 つのオペランドをモデルの入力と出力の両方に使用することはできません。

オペランドの詳しい使用方法については、オペランドの詳細のトピックをご覧ください。

演算

演算は実行する計算を指定するもので、 次の 3 つの要素で構成されます。

  • 演算タイプ(加算、乗算、畳み込みなど)
  • 演算の入力に使用するオペランドのインデックス リスト
  • 演算の出力に使用するオペランドのインデックス リスト

これらのリスト内の順番には意味があります。想定される入出力データの各演算については、NNAPI API リファレンスをご覧ください。

演算を追加する前に、演算で使用または生成するオペランドをモデルに追加する必要があります。

演算の追加順には意味がありません。 NNAPI は、オペランドの計算グラフと演算の実行順序を決める演算によって確定される依存関係に基づいています。

以下の表は、NNAPI がサポートする演算をまとめたものです。

カテゴリ 演算
要素ごとの算術演算
配列演算
画像演算
検索演算
正規化演算
畳み込み演算
プーリング演算
活性化演算
その他の演算

既知の問題点: ANEURALNETWORKS_TENSOR_QUANT8_ASYMM テンソルを ANEURALNETWORKS_PAD 演算(Android 9、API レベル 28 以降で利用可能)に渡すときに、NNAPI からの出力が、TensorFlow Lite などの高レベルの機械学習フレームワークからの出力と一致しない場合があります。 問題が解決するまでは、その代わりに ANEURALNETWORKS_TENSOR_FLOAT32 のみを渡す必要があります。

モデルの作成

モデルの作成手順は次のとおりです。

  1. ANeuralNetworksModel_create() 関数を呼び出して、空のモデルを定義します。

    以下の例では、図 3にある 2 つの演算を含むモデルを作成します。

    ANeuralNetworksModel* model = NULL;
    ANeuralNetworksModel_create(&model);
    
  2. ANeuralNetworks_addOperand() を呼び出して、オペランドをモデルに追加します。 データ型は、ANeuralNetworksOperandType データ構造を使用して定義します。

    // In our example, all our tensors are matrices of dimension [3][4].
    ANeuralNetworksOperandType tensor3x4Type;
    tensor3x4Type.type = ANEURALNETWORKS_TENSOR_FLOAT32;
    tensor3x4Type.scale = 0.f;    // These fields are useful for quantized tensors.
    tensor3x4Type.zeroPoint = 0;  // These fields are useful for quantized tensors.
    tensor3x4Type.dimensionCount = 2;
    uint32_t dims[2] = {3, 4};
    tensor3x4Type.dimensions = dims;
    
    // We also specify operands that are activation function specifiers.
    ANeuralNetworksOperandType activationType;
    activationType.type = ANEURALNETWORKS_INT32;
    activationType.scale = 0.f;
    activationType.zeroPoint = 0;
    activationType.dimensionCount = 0;
    activationType.dimensions = NULL;
    
    // Now we add the seven operands, in the same order defined in the diagram.
    ANeuralNetworksModel_addOperand(model, &tensor3x4Type);  // operand 0
    ANeuralNetworksModel_addOperand(model, &tensor3x4Type);  // operand 1
    ANeuralNetworksModel_addOperand(model, &activationType); // operand 2
    ANeuralNetworksModel_addOperand(model, &tensor3x4Type);  // operand 3
    ANeuralNetworksModel_addOperand(model, &tensor3x4Type);  // operand 4
    ANeuralNetworksModel_addOperand(model, &activationType); // operand 5
    ANeuralNetworksModel_addOperand(model, &tensor3x4Type);  // operand 6
    
  3. トレーニング プロセスでアプリが取得した重みやバイアスなどの定数を含むオペランドには、ANeuralNetworksModel_setOperandValue() 関数と ANeuralNetworksModel_setOperandValueFromMemory() 関数を使用します。

    以下の例では、先ほどメモリバッファを作成したトレーニング データのファイルにある定数値を設定しています。

    // In our example, operands 1 and 3 are constant tensors whose value was
    // established during the training process.
    const int sizeOfTensor = 3 * 4 * 4;    // The formula for size calculation is dim0 * dim1 * elementSize.
    ANeuralNetworksModel_setOperandValueFromMemory(model, 1, mem1, 0, sizeOfTensor);
    ANeuralNetworksModel_setOperandValueFromMemory(model, 3, mem1, sizeOfTensor, sizeOfTensor);
    
    // We set the values of the activation operands, in our example operands 2 and 5.
    int32_t noneValue = ANEURALNETWORKS_FUSED_NONE;
    ANeuralNetworksModel_setOperandValue(model, 2, &noneValue, sizeof(noneValue));
    ANeuralNetworksModel_setOperandValue(model, 5, &noneValue, sizeof(noneValue));
    
  4. 計算したい有向グラフ内の各演算について、ANeuralNetworksModel_addOperation() 関数を呼び出して演算をモデルに追加します。

    この呼び出しの際に、アプリは次のパラメータを指定する必要があります。

    • 演算タイプ
    • 入力値の総数
    • 入力オペランドのインデックス配列
    • 出力値の総数
    • 出力オペランドのインデックス配列

    同一の演算で、1 つのオペランドを入力と出力の両方に使用することはできません。

    // We have two operations in our example.
    // The first consumes operands 1, 0, 2, and produces operand 4.
    uint32_t addInputIndexes[3] = {1, 0, 2};
    uint32_t addOutputIndexes[1] = {4};
    ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, 3, addInputIndexes, 1, addOutputIndexes);
    
    // The second consumes operands 3, 4, 5, and produces operand 6.
    uint32_t multInputIndexes[3] = {3, 4, 5};
    uint32_t multOutputIndexes[1] = {6};
    ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_MUL, 3, multInputIndexes, 1, multOutputIndexes);
    
  5. ANeuralNetworksModel_identifyInputsAndOutputs() 関数を呼び出して、モデルで入力および出力データとして扱うオペランドを指定します。 この関数では、上記の手順 4 で指定した入力および出力オペランドのサブセットをモデルで使用するよう設定できます。

    // Our model has one input (0) and one output (6).
    uint32_t modelInputIndexes[1] = {0};
    uint32_t modelOutputIndexes[1] = {6};
    ANeuralNetworksModel_identifyInputsAndOutputs(model, 1, modelInputIndexes, 1 modelOutputIndexes);
    
  6. 任意で ANeuralNetworksModel_relaxComputationFloat32toFloat16() を呼び出し、IEEE 754 16 ビット浮動小数点形式と同程度の範囲と精度で ANEURALNETWORKS_TENSOR_FLOAT32 を計算するかどうかを指定します。

  7. ANeuralNetworksModel_finish() を呼び出して、モデルの定義を完了します。 エラーがなければ、この関数は結果コード ANEURALNETWORKS_NO_ERROR を返します。

    ANeuralNetworksModel_finish(model);
    

作成したモデルは何度でもコンパイルでき、コンパイルしたものは何度でも実行できます。

コンパイル

コンパイルの手順では、モデルを実行するプロセッサを決定し、対応するドライバで実行の準備をします。 この手順には、モデルを実行するプロセッサに固有のマシンコードの生成が含まれる場合があります。

モデルをコンパイルする手順は次のとおりです。

  1. ANeuralNetworksCompilation_create() 関数を呼び出して、コンパイル インスタンスを新規作成します。

    // Compile the model.
    ANeuralNetworksCompilation* compilation;
    ANeuralNetworksCompilation_create(model, &compilation);
    
  2. 電池消費と実行スピードの優先度をランタイムで決める方法を任意で制御できます。 そのためには ANeuralNetworksCompilation_setPreference() を呼び出します。

    // Ask to optimize for low power consumption.
    ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
    

    指定できる有効なプリファレンスは次のとおりです。

  3. ANeuralNetworksCompilation_finish() を呼び出して、コンパイルの定義を完了します。 エラーがなければ、この関数は結果コード ANEURALNETWORKS_NO_ERROR を返します。

    ANeuralNetworksCompilation_finish(compilation);
    

実行

実行の手順では、モデルを入力データのセットに適用し、計算結果を 1 つ以上のユーザー バッファまたはアプリが割り当てたメモリ領域に保存します。

コンパイル済みのモデルを実行する手順は次のとおりです。

  1. ANeuralNetworksExecution_create() 関数を呼び出して、実行インスタンスを新規作成します。

    // Run the compiled model against a set of inputs.
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
    
  2. 計算の入力値をアプリで読み込む箇所を指定します。 アプリで入力値をユーザー バッファまたは割り当てられたメモリ領域から読み出すには、それぞれ ANeuralNetworksExecution_setInput() または ANeuralNetworksExecution_setInputFromMemory() を呼び出します。

    // Set the single input to our sample model. Since it is small, we won’t use a memory buffer.
    float32 myInput[3][4] = { ..the data.. };
    ANeuralNetworksExecution_setInput(run1, 0, NULL, myInput, sizeof(myInput));
    
  3. アプリが出力値を書き込む箇所を指定します。 アプリで出力値をユーザー バッファまたは割り当てられたメモリ領域に書き込むには、それぞれ ANeuralNetworksExecution_setOutput() または ANeuralNetworksExecution_setOutputFromMemory() を呼び出します。

    // Set the output.
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
    
  4. ANeuralNetworksExecution_startCompute() 関数を呼び出して、実行を開始するタイミングを決めます。 エラーがなければ、この関数は結果コード ANEURALNETWORKS_NO_ERROR を返します。

    // Starts the work. The work proceeds asynchronously.
    ANeuralNetworksEvent* run1_end = NULL;
    ANeuralNetworksExecution_startCompute(run1, &run1_end);
    
  5. ANeuralNetworksEvent_wait() 関数を呼び出して、実行が完了するのを待ちます。 正常に実行が完了すると、この関数は結果コード ANEURALNETWORKS_NO_ERROR を返します。 実行を開始したスレッドとは別のスレッドで終了を待つことができます。

    // For our example, we have no other work to do and will just wait for the completion.
    ANeuralNetworksEvent_wait(run1_end);
    ANeuralNetworksEvent_free(run1_end);
    ANeuralNetworksExecution_free(run1);
    
  6. ANeuralNetworksExecution インスタンスを新規作成する際に使用したのと同じコンパイル インスタンスを用いて、任意で別の入力データセットをコンパイル済みのモデルに適用できます。

    // Apply the compiled model to a different set of inputs.
    ANeuralNetworksExecution* run2;
    ANeuralNetworksExecution_create(compilation, &run2);
    ANeuralNetworksExecution_setInput(run2, ...);
    ANeuralNetworksExecution_setOutput(run2, ...);
    ANeuralNetworksEvent* run2_end = NULL;
    ANeuralNetworksExecution_startCompute(run2, &run2_end);
    ANeuralNetworksEvent_wait(run2_end);
    ANeuralNetworksEvent_free(run2_end);
    ANeuralNetworksExecution_free(run2);
    

クリーンアップ

クリーンアップの手順では、計算に使用した内部リソースを解放します。

// Cleanup
ANeuralNetworksCompilation_free(compilation);
ANeuralNetworksModel_free(model);
ANeuralNetworksMemory_free(mem1);

オペランドの詳細

以下のセクションでは、オペランドの使用に関する高度なトピックを扱います。

量子化テンソル

量子化テンソルは、浮動小数点数値の N 次元配列を簡潔に表現する方法です。

NNAPI は 8 ビットの非対称な量子化テンソルをサポートします。 これらのテンソルにおいて、各セルの値は 8 ビットの整数で表現されます。 テンソルと関連づけられているのがスケールと零点値で、 これらは 8 ビットの整数を浮動小数点数値に変換して表現するために使用されます。

式は次のとおりです。

(cellValue - zeroPoint) * scale

ここで、zeroPoint 値は 32 ビット整数で、スケールは 32 ビット浮動小数点数値です。

32 ビット浮動小数点数値のテンソルに比べ、8 ビットの量子化テンソルには 2 つのメリットがあります。

  • トレーニング済みの重みのサイズが 32 ビットテンソルの 4 分の 1 なので、アプリのサイズが小さくなります。

  • 一般的に計算の実行速度が上がります。 これは、メモリから取得する必要があるデータ量が減り、整数値の計算中に DSP などのプロセッサ効率が上がるためです。

浮動小数点数モデルを量子化モデルに変換することは可能ですが、量子化モデルを直接トレーニングしたほうが良い結果が得られることがこれまでの経験から分かっています。 実際にニューラル ネットワークは、各値の粒度の粗さを補うように学習します。 各量子化テンソルのスケールと zeroPoint の値はトレーニング プロセスで決定されます。

NNAPI では、ANeuralNetworksOperandType データ構造のタイプ フィールドを ANEURALNETWORKS_TENSOR_QUANT8_ASYMM に設定することで、量子化テンソルを定義します。 そのデータ構造内でテンソルのスケールと zeroPoint の値も指定できます。

任意のオペランド

ANEURALNETWORKS_LSH_PROJECTION など、少数の演算では任意のオペランドを取ります。 モデル内で任意のオペランドが省略されていることを示すには、ANeuralNetworksModel_setOperandValue() 関数を呼び出して、バッファに NULL を、長さに 0 を指定します。

オペランドが存在するかどうかの決定が実行のたびに異なる場合は、ANeuralNetworksExecution_setInput() または ANeuralNetworksExecution_setOutput() 関数を使用し、バッファに NULL を、長さに 0 を指定して、そのオペランドが省略されていることを示します。