מסקנות מ-TensorFlow Lite

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

בדף הזה נסביר איך לגשת לכלי התרגום של TensorFlow Lite ולבצע הסקת מסקנות באמצעות C++ , Java ו-Python, וגם קישורים למשאבים אחרים לכל פלטפורמה נתמכת.

מושגים חשובים

בדרך כלל, תהליך ההסקה מ-TensorFlow Lite פועל לפי השלבים הבאים:

  1. טעינת מודל

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

  2. טרנספורמציה של נתונים

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

  3. הסקת מסקנות פועלת

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

  4. פירוש הפלט

    כשמקבלים את התוצאות מההסקה של המודל, צריך לפרש את Tensor באופן משמעותי שמועיל לאפליקציה.

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

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

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

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

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

פלטפורמת Android

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

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

מחולל קוד wrapper ל-Android ב-TensorFlow Lite

במודל TensorFlow Lite עם מטא-נתונים, המפתחים יכולים להשתמש במחולל קודי wrapper של TensorFlow Lite כדי ליצור קוד wrapper לפלטפורמה ספציפית. קוד ה-wrapper מבטל את הצורך לתקשר ישירות עם ByteBuffer ב-Android. במקום זאת, מפתחים יכולים להשתמש במודל TensorFlow Lite עם אובייקטים מוקלדים כמו Bitmap ו-Rect. למידע נוסף, ראו מחולל קודי wrapper של Android TensorFlow Lite.

פלטפורמת iOS

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

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

פלטפורמת Linux

בפלטפורמות Linux (כולל Raspberry Pi), אפשר להריץ הסקת מסקנות באמצעות ממשקי TensorFlow Lite API שזמינים ב-C++ וב-Python, כפי שמוצג בקטעים הבאים.

הרצת מודל

הפעלת מודל TensorFlow Lite כרוכה בכמה שלבים פשוטים:

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

בקטעים הבאים מתואר איך לבצע את השלבים האלה בכל שפה.

טעינה והפעלה של מודל ב-Java

פלטפורמה: Android

Java API להרצת הסקת מסקנות באמצעות TensorFlow Lite מיועד בעיקר לשימוש ב-Android, ולכן הוא זמין כתלות של ספריית Android : org.tensorflow:tensorflow-lite.

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

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

public Interpreter(@NotNull File modelFile);

או באמצעות MappedByteBuffer:

public Interpreter(@NotNull MappedByteBuffer mappedByteBuffer);

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

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

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

  • 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 ממפה את האינדקסים של גופי הפלט לנתוני הפלט התואמים.

בשני המקרים, האינדקסים של Tensor צריכים להתאים לערכים שנתתם ל-TensorFlow Lite Converter כשיצרתם את המודל. חשוב לזכור שהסדר של רכיבי ה-tensor ב-input חייב להתאים לסדר שהוגדר ל-TensorFlow Lite Converter.

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

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

אם opName היא לא פעולה חוקית במודל, היא תגרום ל-IllegalArgumentException.

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

interpreter.close();

תוכלו להיעזר בדוגמה לסיווג תמונות ב-Android כדי לראות פרויקט לדוגמה ב-Java.

סוגי נתונים נתמכים (ב-Java)

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

  • float
  • int
  • long
  • byte

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

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

קלט

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

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

פלט

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

טעינה והפעלה של מודל ב-Swift

פלטפורמה: iOS

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...
}

טעינה והפעלה של מודל ב-Objective-C

פלטפורמה: iOS

ה-Objective-C API זמין ב-TensorFlowLiteObjC Pod של Cocoapods.

קודם כול צריך לייבא את המודול TensorFlowLite.

@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++

פלטפורמות: Android, iOS ו-Linux

ב-C++, המודל מאוחסן במחלקה FlatBufferModel. הוא כולל מודל TensorFlow Lite, ואפשר לפתח אותו בכמה דרכים שונות, בהתאם למיקום שבו מאוחסן המודל:

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() מיד אחרי שינוי הגודל של ה-tensor.

השימוש הפשוט ביותר ב-TensorFlow Lite עם 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 desired.
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

פלטפורמה: Linux

ב-Python API להפעלת הסקת מסקנות צריך בדרך כלל רק tf.lite.Interpreter כדי לטעון מודל ולהריץ מסקנה.

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

הדוגמה הזו מומלצת אם אתם ממירים מ-SavedModel עם SignatureDef מוגדר. זמין החל מ-TensorFlow 2.5

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 TFLite model in TFLite Interpreter
interpreter = tf.lite.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 TFLite model and allocate tensors.
interpreter = tf.lite.Interpreter(model_path="converted_model.tflite")
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 שהומר מראש, אפשר לשלב את הקוד עם TensorFlow Lite Converter Python API. כך אפשר להמיר את מודל Keras לפורמט TensorFlow Lite ולהריץ את ההסקה:

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 TF Lite format
converter = tf.lite.TFLiteConverter.from_keras_model(tf.keras.models.Model(inputs=[img], outputs=[out]))
tflite_model = converter.convert()

# Load the TFLite model and allocate tensors.
interpreter = tf.lite.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 במודלים של TFLite.

בדוגמאות הבאות אפשר לראות איך לשנות את הגודל של צורת הקלט לפני הרצת הסקת מסקנות בשפות שונות. כל הדוגמאות מבוססות על ההנחה שצורת הקלט מוגדרת כ-[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 TFLite model in TFLite Interpreter
interpreter = tf.lite.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()

פעולות נתמכות

ב-TensorFlow Lite יש תמיכה בקבוצת משנה של פעולות ב-TensorFlow עם מגבלות מסוימות. לרשימה המלאה של הפעולות והמגבלות, עיינו בדף של TF Lite Ops.