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

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

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

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

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

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

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

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

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

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

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

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

פלטפורמת Android

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

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

פלטפורמת iOS

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

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

פלטפורמת Linux

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

טעינה והפעלה של מודל

טעינה והפעלה של מודל LiteRT כרוכות בשלבים הבאים:

  1. טעינת המודל לזיכרון.
  2. פיתוח Interpreter על סמך מודל קיים.
  3. הגדרה של ערכי גבולות גזרה לקלט.
  4. הסקת מסקנות.
  5. פלט של ערכי t e n s o r f l o w.

Android (Java)

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

ב-Java, משתמשים במחלקה Interpreter כדי לטעון מודל ומודל Drive מסיקה. במקרים רבים זה ממשק ה-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 מתייחסת לשלושה ארגומנטים:

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

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

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

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

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

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

אם opName היא לא פעולה תקינה במודל, IllegalArgumentException.

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

interpreter.close();

לפרויקט לדוגמה עם Java, ראו דוגמה לזיהוי אובייקטים של Android app.

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

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

  • float
  • int
  • long
  • byte

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

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

קלט

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

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

פלט

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

iOS (Swift)

סוויפט API זמין ב-Pod TensorFlowLiteSwift מ-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)

יעד ג' API זמין ב-Pod LiteRTObjC מ-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 לא תומך בנציגים. כדי להשתמש במשתמשים עם הרשאות גישה לקוד מטרה ג', עליכם לקרוא ישירות ל-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++‎

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

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

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

ב-API של Python להרצת ההסקה נעשה שימוש 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) ומריצים מסקנות עם נתוני קלט אקראי:

הדוגמה הזו מומלצת אם אתם מבצעים המרה מ-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()