במדריך הזה נסביר על תהליך ההפעלה של מודל LiteRT (ראשי תיבות של Lite Runtime) במכשיר כדי לבצע חיזויים על סמך נתוני קלט. כדי לעשות זאת, אנחנו משתמשים במפרש LiteRT שמשתמש בסדר גרפי סטטי ובמקצב זיכרון מותאם אישית (פחות דינמי) כדי להבטיח זמן אחזור קצר ככל האפשר לטעינה, להפעלה ולהפעלת הקוד.
בדרך כלל, ההסקה של LiteRT מתבצעת לפי השלבים הבאים:
טעינה של מודל: טעינת המודל
.tflite
לזיכרון, שמכיל את תרשים הביצועים של המודל.טרנספורמציה של נתונים: טרנספורמציה של נתוני קלט לפורמט הצפוי מאפיינים. בדרך כלל, נתוני הקלט הגולמיים של המודל לא תואמים לפורמט של נתוני הקלט שהמודל מצפה להם. לדוגמה, יכול להיות שתצטרכו לשנות את הגודל של תמונה או את הפורמט שלה כדי שהיא תהיה תואמת למודל.
הרצת היסק: הפעלת מודל LiteRT כדי ליצור תחזיות. הזה כולל שימוש ב-LiteRT API כדי להפעיל את המודל. התהליך כולל כמה שלבים, כמו בניית המתורגם והקצאת טנסורים.
פירוש פלט: פירוש של רכיבי הפלט שימושי באפליקציה שלכם. לדוגמה, מודל עשוי להחזיר רק רשימה של סבירות. באחריותכם למפות את ההסתברויות לנתונים רלוונטיים לקטגוריות ולעיצוב הפלט.
במדריך הזה נסביר איך לגשת למפרש 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++ מציעים יותר גמישות ומהירות, אבל ייתכן שהם ידרשו מכם לכתוב wrappers של JNI כדי להעביר נתונים בין שכבות Java ו-C++.
מידע נוסף זמין בקטעים C++ ו-Java, או במדריך למתחילים ב-Android.
פלטפורמת iOS
ב-iOS, LiteRT זמין במדינות הבאות Swift וגם Objective-C ספריות של iOS. אפשר גם להשתמש ב-C API ישירות בקוד Objective-C.
מידע נוסף זמין ב-Swift, ב-Objective-C וב-C API או לפעול לפי ההוראות במדריך למתחילים של iOS.
פלטפורמת Linux
בפלטפורמות Linux, ניתן להריץ מסקנות באמצעות ממשקי API של LiteRT שזמינים C++.
טעינה והפעלה של מודל
כדי לטעון ולהריץ מודל LiteRT, מבצעים את השלבים הבאים:
- טעינת המודל לזיכרון.
- פיתוח
Interpreter
על סמך מודל קיים. - הגדרת ערכי הטנסור של הקלט.
- הפעלת מסקנות.
- הפלט של ערכי טינסור.
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
מתייחסת לשלושה ארגומנטים:
קלט : מיפוי של קלט מהשם שהוזן בחתימה לקלט לאובייקט.
פלט : מיפוי של מיפוי פלט משם הפלט בחתימה לפלט .
Signature Name (אופציונלי): שם החתימה (אפשר להשאיר את השדה ריק אם למודל יש חתימה אחת).
דרך נוספת להריץ מסקנות כשאין למודל חתימות מוגדרות.
פשוט קוראים ל-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
תואמת ל-tensor של קלט, ו-map_of_indices_to_outputs
ממפה אינדקסים של tensors של פלט לנתוני הפלט התואמים.
בשני המקרים, אינדקסי הטנזורים צריכים להתאים לערכים שסיפקתם ל-LiteRT Converter כשיצרתם את המודל. שימו לב
שהסדר של רכיבי הטנזור ב-input
חייב להיות תואם לסדר שניתן ל-LiteRT.
משתמש שביצע המרה.
בכיתה Interpreter
יש גם פונקציות נוחות לקבלת המדד של כל קלט או פלט של מודל באמצעות שם פעולה:
public int getInputIndex(String opName);
public int getOutputIndex(String opName);
אם הפעולה opName
לא חוקית במודל, מתקבלת הודעת השגיאה IllegalArgumentException
.
חשוב לזכור שInterpreter
הוא הבעלים של המשאבים. כדי למנוע דליפת זיכרון,
יש לשחרר משאבים אחרי השימוש על ידי:
interpreter.close();
לפרויקט לדוגמה עם Java, ראו דוגמה לזיהוי אובייקטים של Android app.
סוגי נתונים נתמכים
כדי להשתמש ב-LiteRT, סוגי הנתונים של הטנסורים של הקלט והפלט צריכים להיות אחד מהסוגים הבאים:
float
int
long
byte
יש תמיכה גם בסוגים של String
, אבל הם מקודדים באופן שונה
מסוגים פרימיטיביים. באופן ספציפי, הצורה של טינסור מחרוזת קובעת את המספר והסידור של המחרוזות בטינסור, כאשר כל רכיב הוא מחרוזת באורך משתנה. במובן הזה, אי אפשר לחשב את הגודל (בייט) של הטנזור מהצורה ומהסוג בלבד, ולכן אי אפשר לספק מחרוזות כארגומנטים ByteBuffer
שטוח יחיד.
אם משתמשים בסוגי נתונים אחרים, כולל סוגי נתונים בקופסה כמו Integer
ו-Float
, תושלם זריקה של IllegalArgumentException
.
קלט
כל קלט צריך להיות מערך או מערך רב-מימדי של הערכים הנתמכים
סוגים פרימיטיביים, או ByteBuffer
גולמי בגודל המתאים. אם הקלט הוא מערך או מערך רב-ממדי, בזמן ההסקה המערכת תשנה באופן משתמע את הגודל של טינסור הקלט המשויך כך שיתאים למאפיינים של המערך. אם הקלט הוא
ByteBuffer, מבצע הקריאה החוזרת צריך קודם לשנות את הגודל של הקלט המשויך באופן ידני
את Tensor (דרך Interpreter.resizeInput()
) לפני הרצת ההסקה.
כשמשתמשים ב-ByteBuffer
, עדיף להשתמש במאגרי בייטים ישירים, כי כך אפשר למנוע ב-Interpreter
עותקים מיותרים. אם ByteBuffer
הוא בייט ישיר
מאגר הנתונים הזמני, הסדר שלו חייב להיות ByteOrder.nativeOrder()
. אחרי שהיא משמשת להסקת מודל, היא לא יכולה להשתנות עד שההסקה של המודל מסתיימת.
פלט
כל פלט צריך להיות מערך או מערך רב-מימדי של סוגי הפרימטיבים הנתמכים, או ByteBuffer בגודל המתאים. חשוב לזכור שלמודלים מסוימים יש פלטים דינמיים, שבהם הצורה של הטנסורים של הפלט יכולה להשתנות בהתאם לקלט. אין דרך ישירה לטפל בעניין הזה באמצעות API בהסקת מסקנות Java, אבל תוספים מתוכננים יאפשרו זאת.
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)
יעד ג'
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 לא תומך בנציגים. כדי להשתמש ב-delegates עם קוד 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++
ה-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 Compiler כדי להמיר את מודל 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()