このガイドでは、LiteRT(Lite Runtime の略)モデルをデバイス上で実行し、入力データに基づいて予測を行うプロセスについて説明します。これは、静的グラフの順序付けとカスタム(動的性の低い)メモリ アロケータを使用して、読み込み、初期化、実行のレイテンシを最小限に抑える LiteRT インタープリタによって実現されます。
LiteRT 推論は通常、次の手順で行われます。
モデルの読み込み: モデルの実行グラフを含む
.tfliteモデルをメモリに読み込みます。データの変換: 入力データを想定される形式とディメンションに変換します。モデルの未加工の入力データは、通常、モデルが想定する入力データ形式と一致しません。たとえば、モデルと互換性を持たせるために、画像のサイズを変更したり、画像形式を変更したりする必要がある場合があります。
推論の実行: LiteRT モデルを実行して予測を行います。このステップでは、LiteRT API を使用してモデルを実行します。これには、インタープリタのビルドやテンソルの割り当てなどの手順が含まれます。
出力の解釈: アプリケーションで役立つ意味のある方法で出力テンソルを解釈します。たとえば、モデルが確率のリストのみを返す場合があります。確率を関連するカテゴリにマッピングして出力をフォーマットするのはユーザーの責任です。
このガイドでは、LiteRT インタープリタにアクセスし、C++、Java、Python を使用して推論を実行する方法について説明します。
対応プラットフォーム
TensorFlow 推論 API は、Android、iOS、Linux などの最も一般的なモバイル プラットフォームと組み込みプラットフォーム向けに、複数のプログラミング言語で提供されています。
ほとんどの場合、API 設計では使いやすさよりもパフォーマンスが優先されます。LiteRT は小型デバイスでの高速推論用に設計されているため、API は利便性を犠牲にして不要なコピーを回避します。
すべてのライブラリで、LiteRT API を使用してモデルの読み込み、入力のフィード、推論出力の取得を行うことができます。
Android プラットフォーム
Android では、Java API または C++ API を使用して LiteRT 推論を実行できます。Java API は利便性が高く、Android アクティビティ クラス内で直接使用できます。C++ API は柔軟性と速度に優れていますが、Java レイヤと C++ レイヤ間でデータを移動するために JNI ラッパーの作成が必要になる場合があります。
詳細については、C++ と Java のセクションをご覧ください。または、Android クイック スタートに従ってください。
iOS プラットフォーム
iOS では、LiteRT は Swift と Objective-C の iOS ライブラリで利用できます。Objective-C コードで C API を直接使用することもできます。
Swift、Objective-C、C API の各セクションを参照するか、iOS クイックスタートに沿って操作してください。
Linux プラットフォーム
Linux プラットフォームでは、C++ で使用可能な LiteRT API を使用して推論を実行できます。
モデルを読み込んで実行する
LiteRT モデルを読み込んで実行する手順は次のとおりです。
- モデルをメモリに読み込む。
- 既存のモデルに基づいて
Interpreterを構築する。 - 入力テンソル値を設定します。
- 推論を呼び出す。
- テンソル値の出力。
Android(Java)
LiteRT で推論を実行するための Java API は、主に Android での使用を想定して設計されているため、Android ライブラリの依存関係(com.google.ai.edge.litert)として提供されています。
Java では、Interpreter クラスを使用してモデルを読み込み、モデル推論を駆動します。多くの場合、この API だけで十分です。
FlatBuffers(.tflite)ファイルを使用して Interpreter を初期化できます。
public Interpreter(@NotNull File modelFile);
または MappedByteBuffer を使用する場合:
public Interpreter(@NotNull MappedByteBuffer mappedByteBuffer);
どちらの場合も、有効な LiteRT モデルを指定する必要があります。そうしないと、API は IllegalArgumentException をスローします。MappedByteBuffer を使用して Interpreter を初期化する場合、Interpreter のライフタイム全体で変更されないようにする必要があります。
モデルで推論を実行する推奨の方法はシグネチャを使用することです。これは Tensorflow 2.5 以降で変換されたモデルで使用できます。
try (Interpreter interpreter = new Interpreter(file_of_tensorflowlite_model)) {
Map<String, Object> inputs = new HashMap<>();
inputs.put("input_1", input1);
inputs.put("input_2", input2);
Map<String, Object> outputs = new HashMap<>();
outputs.put("output_1", output1);
interpreter.runSignature(inputs, outputs, "mySignature");
}
runSignature メソッドは次の 3 つの引数を取ります。
入力 : シグネチャの入力名から入力オブジェクトへの入力のマッピング。
Outputs : シグネチャの出力名から出力データへの出力マッピングのマップ。
シグネチャ名(省略可): シグネチャ名(モデルに単一のシグネチャがある場合は空のままにできます)。
モデルに定義されたシグネチャがない場合に推論を実行する別の方法。Interpreter.run() を呼び出します。次に例を示します。
try (Interpreter interpreter = new Interpreter(file_of_a_tensorflowlite_model)) {
interpreter.run(input, output);
}
run() メソッドは 1 つの入力のみを受け取り、1 つの出力のみを返します。モデルに複数の入力または複数の出力がある場合は、代わりに次のコードを使用します。
interpreter.runForMultipleInputsOutputs(inputs, map_of_indices_to_outputs);
この場合、inputs の各エントリは入力テンソルに対応し、map_of_indices_to_outputs は出力テンソルのインデックスを対応する出力データにマッピングします。
どちらの場合も、テンソル インデックスは、モデルの作成時に LiteRT Converter に指定した値に対応している必要があります。input のテンソルの順序は、LiteRT Converter に指定された順序と一致している必要があります。
Interpreter クラスには、オペレーション名を使用してモデルの入力または出力のインデックスを取得するための便利な関数も用意されています。
public int getInputIndex(String opName);
public int getOutputIndex(String opName);
opName がモデル内の有効なオペレーションでない場合、IllegalArgumentException がスローされます。
また、Interpreter はリソースを所有していることに注意してください。メモリリークを回避するには、使用後に次の方法でリソースを解放する必要があります。
interpreter.close();
Java を使用したサンプル プロジェクトについては、Android オブジェクト検出のサンプルアプリをご覧ください。
サポートされるデータタイプ
LiteRT を使用するには、入力テンソルと出力テンソルのデータ型が次のプリミティブ型のいずれかである必要があります。
floatintlongbyte
String 型もサポートされていますが、プリミティブ型とは異なる方法でエンコードされます。特に、文字列 Tensor の形状は、Tensor 内の文字列の数と配置を決定します。各要素自体が可変長の文字列になります。この意味で、Tensor の(バイト)サイズは形状と型だけから計算することはできません。したがって、文字列を単一のフラットな ByteBuffer 引数として指定することはできません。
Integer や Float などのボックス型を含む他のデータ型が使用されている場合は、IllegalArgumentException がスローされます。
入力
各入力は、サポートされているプリミティブ型の配列または多次元配列、または適切なサイズの未加工の ByteBuffer である必要があります。入力が配列または多次元配列の場合、関連する入力テンソルは推論時に配列のディメンションに暗黙的にサイズ変更されます。入力が ByteBuffer の場合、呼び出し元は推論を実行する前に、関連付けられた入力テンソルを手動でサイズ変更(Interpreter.resizeInput() を使用)する必要があります。
ByteBuffer を使用する場合は、直接バイトバッファを使用することをおすすめします。これにより、Interpreter で不要なコピーを回避できます。ByteBuffer が直接バイトバッファの場合、その順序は ByteOrder.nativeOrder() でなければなりません。モデル推論に使用された後は、モデル推論が完了するまで変更されません。
出力
各出力は、サポートされているプリミティブ型の配列または多次元配列、または適切なサイズの ByteBuffer である必要があります。一部のモデルでは、出力が動的で、出力テンソルの形状が入力によって異なる場合があります。既存の Java 推論 API でこの処理を簡単に行う方法はありませんが、計画されている拡張機能により可能になります。
iOS - Swift
Swift API は、Cocoapods の TensorFlowLiteSwift Pod で使用できます。
まず、TensorFlowLite モジュールをインポートする必要があります。
import TensorFlowLite
// Getting model path
guard
let modelPath = Bundle.main.path(forResource: "model", ofType: "tflite")
else {
// Error handling...
}
do {
// Initialize an interpreter with the model.
let interpreter = try Interpreter(modelPath: modelPath)
// Allocate memory for the model's input `Tensor`s.
try interpreter.allocateTensors()
let inputData: Data // Should be initialized
// input data preparation...
// Copy the input data to the input `Tensor`.
try self.interpreter.copy(inputData, toInputAt: 0)
// Run inference by invoking the `Interpreter`.
try self.interpreter.invoke()
// Get the output `Tensor`
let outputTensor = try self.interpreter.output(at: 0)
// Copy output to `Data` to process the inference results.
let outputSize = outputTensor.shape.dimensions.reduce(1, {x, y in x * y})
let outputData =
UnsafeMutableBufferPointer<Float32>.allocate(capacity: outputSize)
outputTensor.data.copyBytes(to: outputData)
if (error != nil) { /* Error handling... */ }
} catch error {
// Error handling...
}
iOS(Objective-C)
Objective-C API は、Cocoapods の LiteRTObjC Pod で使用できます。
まず、TensorFlowLiteObjC モジュールをインポートする必要があります。
@import TensorFlowLite;
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"model"
ofType:@"tflite"];
NSError *error;
// Initialize an interpreter with the model.
TFLInterpreter *interpreter = [[TFLInterpreter alloc] initWithModelPath:modelPath
error:&error];
if (error != nil) { /* Error handling... */ }
// Allocate memory for the model's input `TFLTensor`s.
[interpreter allocateTensorsWithError:&error];
if (error != nil) { /* Error handling... */ }
NSMutableData *inputData; // Should be initialized
// input data preparation...
// Get the input `TFLTensor`
TFLTensor *inputTensor = [interpreter inputTensorAtIndex:0 error:&error];
if (error != nil) { /* Error handling... */ }
// Copy the input data to the input `TFLTensor`.
[inputTensor copyData:inputData error:&error];
if (error != nil) { /* Error handling... */ }
// Run inference by invoking the `TFLInterpreter`.
[interpreter invokeWithError:&error];
if (error != nil) { /* Error handling... */ }
// Get the output `TFLTensor`
TFLTensor *outputTensor = [interpreter outputTensorAtIndex:0 error:&error];
if (error != nil) { /* Error handling... */ }
// Copy output to `NSData` to process the inference results.
NSData *outputData = [outputTensor dataWithError:&error];
if (error != nil) { /* Error handling... */ }
Objective-C コードの C API
Objective-C API はデリゲートをサポートしていません。Objective-C コードでデリゲートを使用するには、基盤となる C API を直接呼び出す必要があります。
#include "tensorflow/lite/c/c_api.h"
TfLiteModel* model = TfLiteModelCreateFromFile([modelPath UTF8String]);
TfLiteInterpreterOptions* options = TfLiteInterpreterOptionsCreate();
// Create the interpreter.
TfLiteInterpreter* interpreter = TfLiteInterpreterCreate(model, options);
// Allocate tensors and populate the input tensor data.
TfLiteInterpreterAllocateTensors(interpreter);
TfLiteTensor* input_tensor =
TfLiteInterpreterGetInputTensor(interpreter, 0);
TfLiteTensorCopyFromBuffer(input_tensor, input.data(),
input.size() * sizeof(float));
// Execute inference.
TfLiteInterpreterInvoke(interpreter);
// Extract the output tensor data.
const TfLiteTensor* output_tensor =
TfLiteInterpreterGetOutputTensor(interpreter, 0);
TfLiteTensorCopyToBuffer(output_tensor, output.data(),
output.size() * sizeof(float));
// Dispose of the model and interpreter objects.
TfLiteInterpreterDelete(interpreter);
TfLiteInterpreterOptionsDelete(options);
TfLiteModelDelete(model);
C++
LiteRT で推論を実行するための C++ API は、Android、iOS、Linux プラットフォームと互換性があります。iOS の C++ API は、bazel を使用している場合にのみ使用できます。
C++ では、モデルは FlatBufferModel クラスに保存されます。LiteRT モデルをカプセル化します。モデルの保存場所に応じて、いくつかの方法でビルドできます。
class FlatBufferModel {
// Build a model based on a file. Return a nullptr in case of failure.
static std::unique_ptr<FlatBufferModel> BuildFromFile(
const char* filename,
ErrorReporter* error_reporter);
// Build a model based on a pre-loaded flatbuffer. The caller retains
// ownership of the buffer and should keep it alive until the returned object
// is destroyed. Return a nullptr in case of failure.
static std::unique_ptr<FlatBufferModel> BuildFromBuffer(
const char* buffer,
size_t buffer_size,
ErrorReporter* error_reporter);
};
モデルが FlatBufferModel オブジェクトになったので、Interpreter を使用して実行できます。1 つの FlatBufferModel を複数の Interpreter で同時に使用できます。
次のコード スニペットは、Interpreter API の重要な部分を示しています。次の点にご注意ください。
- テンソルは、文字列の比較(および文字列ライブラリへの固定依存関係)を回避するために、整数で表されます。
- インタープリタには、同時実行スレッドからアクセスできません。
- 入力テンソルと出力テンソルのメモリ割り当ては、テンソルのサイズ変更の直後に
AllocateTensors()を呼び出すことでトリガーする必要があります。
C++ での LiteRT の最も簡単な使用方法は次のとおりです。
// Load the model
std::unique_ptr<tflite::FlatBufferModel> model =
tflite::FlatBufferModel::BuildFromFile(filename);
// Build the interpreter
tflite::ops::builtin::BuiltinOpResolver resolver;
std::unique_ptr<tflite::Interpreter> interpreter;
tflite::InterpreterBuilder(*model, resolver)(&interpreter);
// Resize input tensors, if needed.
interpreter->AllocateTensors();
float* input = interpreter->typed_input_tensor<float>(0);
// Fill `input`.
interpreter->Invoke();
float* output = interpreter->typed_output_tensor<float>(0);
その他のコード例については、minimal.cc と label_image.cc をご覧ください。
Python
推論を実行するための Python API は、Interpreter を使用してモデルを読み込み、推論を実行します。
LiteRT パッケージをインストールします。
$ python3 -m pip install ai-edge-litert
LiteRT インタープリタをインポートする
from ai_edge_litert.interpreter import Interpreter
Interpreter = Interpreter(model_path=args.model.file)
次の例は、Python インタープリタを使用して FlatBuffers(.tflite)ファイルを読み込み、ランダムな入力データで推論を実行する方法を示しています。
この例は、定義済みの SignatureDef を使用して SavedModel から変換する場合におすすめします。
class TestModel(tf.Module):
def __init__(self):
super(TestModel, self).__init__()
@tf.function(input_signature=[tf.TensorSpec(shape=[1, 10], dtype=tf.float32)])
def add(self, x):
'''
Simple method that accepts single input 'x' and returns 'x' + 4.
'''
# Name the output 'result' for convenience.
return {'result' : x + 4}
SAVED_MODEL_PATH = 'content/saved_models/test_variable'
TFLITE_FILE_PATH = 'content/test_variable.tflite'
# Save the model
module = TestModel()
# You can omit the signatures argument and a default signature name will be
# created with name 'serving_default'.
tf.saved_model.save(
module, SAVED_MODEL_PATH,
signatures={'my_signature':module.add.get_concrete_function()})
# Convert the model using TFLiteConverter
converter = tf.lite.TFLiteConverter.from_saved_model(SAVED_MODEL_PATH)
tflite_model = converter.convert()
with open(TFLITE_FILE_PATH, 'wb') as f:
f.write(tflite_model)
# Load the LiteRT model in LiteRT Interpreter
from ai_edge_litert.interpreter import Interpreter
interpreter = Interpreter(TFLITE_FILE_PATH)
# There is only 1 signature defined in the model,
# so it will return it by default.
# If there are multiple signatures then we can pass the name.
my_signature = interpreter.get_signature_runner()
# my_signature is callable with input as arguments.
output = my_signature(x=tf.constant([1.0], shape=(1,10), dtype=tf.float32))
# 'output' is dictionary with all outputs from the inference.
# In this case we have single output 'result'.
print(output['result'])
モデルに SignatureDefs が定義されていない場合の別の例。
import numpy as np
import tensorflow as tf
# Load the LiteRT model and allocate tensors.
from ai_edge_litert.interpreter import Interpreter
interpreter = Interpreter(TFLITE_FILE_PATH)
interpreter.allocate_tensors()
# Get input and output tensors.
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
# Test the model on random input data.
input_shape = input_details[0]['shape']
input_data = np.array(np.random.random_sample(input_shape), dtype=np.float32)
interpreter.set_tensor(input_details[0]['index'], input_data)
interpreter.invoke()
# The function `get_tensor()` returns a copy of the tensor data.
# Use `tensor()` in order to get a pointer to the tensor.
output_data = interpreter.get_tensor(output_details[0]['index'])
print(output_data)
モデルを事前に変換された .tflite ファイルとして読み込む代わりに、コードを LiteRT コンパイラと組み合わせることで、Keras モデルを LiteRT 形式に変換してから推論を実行できます。
import numpy as np
import tensorflow as tf
img = tf.keras.Input(shape=(64, 64, 3), name="img")
const = tf.constant([1., 2., 3.]) + tf.constant([1., 4., 4.])
val = img + const
out = tf.identity(val, name="out")
# Convert to LiteRT format
converter = tf.lite.TFLiteConverter.from_keras_model(tf.keras.models.Model(inputs=[img], outputs=[out]))
tflite_model = converter.convert()
# Load the LiteRT model and allocate tensors.
from ai_edge_litert.interpreter import Interpreter
interpreter = Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()
# Continue to get tensors and so forth, as shown above...
その他の Python サンプルコードについては、label_image.py をご覧ください。
動的シェイプ モデルで推論を実行する
動的入力形状でモデルを実行する場合は、推論を実行する前に入力形状のサイズを変更します。それ以外の場合、Tensorflow モデルの None シェイプは、LiteRT モデルの 1 のプレースホルダに置き換えられます。
次の例は、さまざまな言語で推論を実行する前に入力形状のサイズを変更する方法を示しています。すべての例では、入力シェイプが [1/None, 10] として定義され、[3, 10] にサイズ変更されることを前提としています。
C++ の例
// Resize input tensors before allocate tensors
interpreter->ResizeInputTensor(/*tensor_index=*/0, std::vector<int>{3,10});
interpreter->AllocateTensors();
Python の例:
# Load the LiteRT model in LiteRT Interpreter
from ai_edge_litert.interpreter import Interpreter
interpreter = Interpreter(model_path=TFLITE_FILE_PATH)
# Resize input shape for dynamic shape model and allocate tensor
interpreter.resize_tensor_input(interpreter.get_input_details()[0]['index'], [3, 10])
interpreter.allocate_tensors()
# Get input and output tensors.
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()