תחילת העבודה עם LiteRT

במדריך הזה מוסבר איך להריץ מודל LiteRT (קיצור של Lite Runtime) במכשיר כדי ליצור תחזיות על סמך נתוני קלט. הדבר מושג באמצעות מתורגמן LiteRT, שמשתמש בסדר סטטי של גרפים ובמקצה זיכרון מותאם אישית (פחות דינמי) כדי להבטיח טעינה, אתחול וזמן אחזור מינימליים של הביצוע.

הסקת מסקנות ב-LiteRT מתבצעת בדרך כלל לפי השלבים הבאים:

  1. טעינת מודל: טעינת מודל .tflite לזיכרון, שמכיל את גרף הביצוע של המודל.

  2. טרנספורמציה של נתונים: המרת נתוני קלט לפורמט ולמאפיינים הצפויים. בדרך כלל, נתוני הקלט הגולמיים של המודל לא תואמים לפורמט נתוני הקלט שהמודל מצפה לקבל. לדוגמה, יכול להיות שתצטרכו לשנות את הגודל של תמונה או לשנות את הפורמט שלה כדי שהיא תהיה תואמת למודל.

  3. הפעלת הסקה: הפעלת מודל LiteRT כדי ליצור תחזיות. בשלב הזה, משתמשים ב-LiteRT API כדי להריץ את המודל. התהליך כולל כמה שלבים, כמו בניית המפענח והקצאת טנסורים.

  4. פירוש הפלט: מפרשים את טנסורי הפלט בצורה משמעותית שמועילה באפליקציה. לדוגמה, יכול להיות שמודל יחזיר רק רשימה של הסתברויות. אתם צריכים למפות את ההסתברויות לקטגוריות רלוונטיות ולעצב את הפלט.

במדריך הזה מוסבר איך לגשת למפרש LiteRT ולבצע הסקה באמצעות C++‎,‏ Java ו-Python.

פלטפורמות נתמכות

ממשקי ה-API של TensorFlow inference זמינים לרוב הפלטפורמות הנפוצות לנייד ולמערכות מוטמעות, כמו Android,‏ iOS ו-Linux, בשפות תכנות שונות.

ברוב המקרים, עיצוב ה-API משקף העדפה של ביצועים על פני קלות השימוש. ‫LiteRT מיועד להסקת מסקנות מהירה במכשירים קטנים, ולכן ממשקי ה-API לא כוללים עותקים מיותרים, על חשבון הנוחות.

בכל הספריות, ה-API ‏LiteRT מאפשר לטעון מודלים, להזין קלט ולאחזר פלט של הסקה.

פלטפורמת Android

ב-Android, אפשר לבצע הסקה של LiteRT באמצעות ממשקי API של Java או C++. ממשקי ה-API של Java מספקים נוחות וניתן להשתמש בהם ישירות בכיתות הפעילות של Android. ממשקי ה-API של C++‎ מציעים גמישות ומהירות גבוהות יותר, אבל יכול להיות שיהיה צורך לכתוב עטיפות JNI כדי להעביר נתונים בין שכבות Java ו-C++‎.

מידע נוסף זמין בקטעים בנושא C++ ו-Java, או במאמר הפעלה מהירה של Android.

פלטפורמת iOS

ב-iOS, ‏ LiteRT זמין בספריות של iOS ב-Swift וב-Objective-C. אפשר גם להשתמש ב-C API ישירות בקוד Objective-C.

אפשר לעיין בקטעים בנושא Swift,‏ Objective-C ו-C API, או לעיין במדריך להתחלה מהירה ל-iOS.

פלטפורמת Linux

בפלטפורמות Linux, אפשר להריץ מסקנות באמצעות ממשקי LiteRT API שזמינים ב-C++‎.

טעינה והרצה של מודל

כדי לטעון ולהפעיל מודל LiteRT, צריך לבצע את השלבים הבאים:

  1. טעינת המודל לזיכרון.
  2. בניית Interpreter על סמך מודל קיים.
  3. הגדרת ערכי טנסור קלט.
  4. הסקת מסקנות.
  5. פלט של ערכי טנסור.

‫Android‏ (Java)

ה-API של Java להפעלת מסקנות באמצעות LiteRT מיועד בעיקר לשימוש ב-Android, ולכן הוא זמין כתלות בספריית Android:‏ com.google.ai.edge.litert.

ב-Java, משתמשים במחלקה Interpreter כדי לטעון מודל ולהסיק מסקנות לגבי המודל. במקרים רבים, יכול להיות שזה יהיה ה-API היחיד שתצטרכו.

אפשר לאתחל Interpreter באמצעות קובץ FlatBuffers‏ (.tflite):

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 מקבלת שלושה ארגומנטים:

  • Inputs : מיפוי של קלטים משם הקלט בחתימה לאובייקט קלט.

  • Outputs : map for output mapping from output name in signature to output data.

  • שם החתימה (אופציונלי): שם החתימה (אפשר להשאיר את השדה ריק אם למודל יש חתימה אחת).

דרך נוספת להריץ הסקת מסקנות כשאין למודל חתימות מוגדרות. פשוט מתקשרים אל Interpreter.run(). לדוגמה:

try (Interpreter interpreter = new Interpreter(file_of_a_tensorflowlite_model)) {
  interpreter.run(input, output);
}

השיטה run() מקבלת רק קלט אחד ומחזירה רק פלט אחד. לכן, אם למודל יש כמה קלטים או כמה פלטים, צריך להשתמש במקום זאת ב:

interpreter.runForMultipleInputsOutputs(inputs, map_of_indices_to_outputs);

במקרה הזה, כל ערך ב-inputs מתאים לטנסור קלט, ו-map_of_indices_to_outputs ממפה את האינדקסים של טנסורי הפלט לנתוני הפלט המתאימים.

בשני המקרים, האינדקסים של הטנסור צריכים להתאים לערכים שנתתם לכלי להמרת LiteRT כשבניתם את המודל. חשוב לדעת שסדר הטנסורים ב-input צריך להיות זהה לסדר שצוין ב-LiteRT Converter.

בנוסף, המחלקה Interpreter מספקת פונקציות נוחות לקבלת האינדקס של כל קלט או פלט של מודל באמצעות שם פעולה:

public int getInputIndex(String opName);
public int getOutputIndex(String opName);

אם opName היא לא פעולה תקינה במודל, המערכת תציג את השגיאה IllegalArgumentException.

חשוב לדעת ש-Interpreter הוא הבעלים של המשאבים. כדי למנוע דליפת זיכרון, צריך לשחרר את המשאבים אחרי השימוש בהם. לשם כך:

interpreter.close();

דוגמה לפרויקט עם Java מופיעה באפליקציה לדוגמה לזיהוי אובייקטים ב-Android.

סוגי נתונים נתמכים

כדי להשתמש ב-LiteRT, סוגי הנתונים של טנסורי הקלט והפלט צריכים להיות אחד מסוגי ה-primitive הבאים:

  • float
  • int
  • long
  • byte

יש גם תמיכה בסוגים String, אבל הם מקודדים באופן שונה מהסוגים הפרימיטיביים. בפרט, הצורה של טנסור מחרוזת מכתיבה את המספר והסידור של המחרוזות בטנסור, כאשר כל רכיב הוא מחרוזת באורך משתנה. במובן הזה, אי אפשר לחשב את הגודל (בבייט) של הטנסור רק לפי הצורה והסוג, ולכן אי אפשר לספק מחרוזות כארגומנט שטוח יחיד ByteBuffer.

אם משתמשים בסוגי נתונים אחרים, כולל סוגים מובנים כמו Integer ו-Float, תופעל שגיאת IllegalArgumentException.

קלט

כל קלט צריך להיות מערך או מערך רב-ממדי של טיפוסי הנתונים הפרימיטיביים הנתמכים, או ByteBuffer גולמי בגודל המתאים. אם הקלט הוא מערך או מערך רב-ממדי, גודל טנסור הקלט המשויך ישתנה באופן מרומז לגודל המערך בזמן ההסקה. אם הקלט הוא ByteBuffer, המתקשר צריך לשנות את הגודל של טנסור הקלט המשויך באופן ידני (באמצעות Interpreter.resizeInput()) לפני הפעלת ההסקה.

כשמשתמשים ב-ByteBuffer, עדיף להשתמש במאגרי בייטים ישירים, כי כך Interpreter יכול להימנע מעותקים מיותרים. אם ByteBuffer הוא מאגר בייטים ישיר, הסדר שלו חייב להיות ByteOrder.nativeOrder(). אחרי שמשתמשים בו להסקת מסקנות של מודל, הוא צריך להישאר ללא שינוי עד שהסקת המסקנות של המודל מסתיימת.

פלט

כל פלט צריך להיות מערך או מערך רב-ממדי של הסוגים הפרימיטיביים הנתמכים, או ByteBuffer בגודל המתאים. חשוב לזכור שלחלק מהמודלים יש פלטים דינמיים, שבהם הצורה של טנסורים של פלט יכולה להשתנות בהתאם לקלט. אין דרך פשוטה לטפל בזה באמצעות Java inference API הקיים, אבל תוספים מתוכננים יאפשרו זאת.

‫iOS (Swift)

Swift API זמין ב-TensorFlowLiteSwift Pod מ-Cocoapods.

קודם כול, צריך לייבא את המודול 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 זמין ב-LiteRTObjC Pod מ-Cocoapods.

קודם כול, צריך לייבא את המודול 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... */ }

‫C API בקוד Objective-C

‫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++‎

‫C++ API להפעלת הסקה באמצעות LiteRT תואם לפלטפורמות Android,‏ iOS ו-Linux. ‫C++ API ב-iOS זמין רק כשמשתמשים ב-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. יותר מInterpreter יכולים להשתמש בו-זמנית בFlatBufferModel אחד.

קטע הקוד הבא מציג את החלקים החשובים של Interpreter API. חשוב לדעת:

  • טנסורים מיוצגים על ידי מספרים שלמים, כדי להימנע מהשוואות של מחרוזות (ומתלות קבועה בספריות של מחרוזות).
  • אסור לגשת למתורגמן משרשורים מקבילים.
  • צריך להפעיל הקצאת זיכרון לטנסורים של קלט ופלט על ידי קריאה ל-AllocateTensors() מיד אחרי שינוי הגודל של הטנסורים.

השימוש הכי פשוט ב-LiteRT עם C++‎ נראה כך:

// 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 Interpreter

from ai_edge_litert.interpreter import Interpreter
Interpreter = Interpreter(model_path=args.model.file)

בדוגמה הבאה אפשר לראות איך להשתמש ברכיב התרגום של Python כדי לטעון קובץ FlatBuffers ‏ (.tflite) ולהריץ הסקה עם נתוני קלט אקראיים:

הדוגמה הזו מומלצת אם אתם מבצעים המרה מ-SavedModel עם SignatureDef מוגדר.

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.

הרצת הסקה עם מודל של צורה דינמית

אם רוצים להפעיל מודל עם צורת קלט דינמית, צריך לשנות את הגודל של צורת הקלט לפני שמריצים את ההסקה. אחרת, הצורה None במודלים של TensorFlow תוחלף ב-placeholder של 1 במודלים של LiteRT.

בדוגמאות הבאות אפשר לראות איך משנים את הגודל של צורת הקלט לפני שמריצים הסקה בשפות שונות. כל הדוגמאות מניחות שצורת הקלט מוגדרת כ-[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()