שימוש במודל TensorFlow Lite להסקת מסקנות בעזרת ML Kit ב-iOS

אפשר להשתמש ב-ML Kit כדי לבצע הסקת מסקנות במכשיר באמצעות מודל TensorFlow Lite.

אפשר להשתמש במודלים של TensorFlow Lite ב-ML Kit רק במכשירים עם מערכת iOS מגרסה 9 ואילך.

לפני שמתחילים

  1. אם עדיין לא הוספתם את Firebase לאפליקציה, תוכלו לפעול לפי השלבים שמפורטים במדריך לתחילת העבודה.
  2. כוללים את ספריות ML Kit ב-Podfile:
    pod 'Firebase/MLModelInterpreter', '6.25.0'
    
    אחרי שמתקינים או מעדכנים את ה-Pods של הפרויקט, חשוב לפתוח את הפרויקט ב-Xcode באמצעות .xcworkspace שלו.
  3. באפליקציה, מייבאים את Firebase:

    Swift

    import Firebase

    Objective-C

    @import Firebase;
  4. ממירים את מודל TensorFlow שבו רוצים להשתמש לפורמט TensorFlow Lite. למידע נוסף, ראו TOCO: TensorFlow Lite Optimizing Converter.

אירוח או אריזה של המודל

כדי להשתמש במודל TensorFlow Lite להסקה באפליקציה, צריך להפוך את המודל לזמין ל-ML Kit. ב-ML Kit אפשר להשתמש במודלים של TensorFlow Lite שמתארחים מרחוק באמצעות Firebase, במודלים שמצורפים לקובץ הבינארי של האפליקציה או בשניהם.

אירוח מודל ב-Firebase מאפשר לכם לעדכן את המודל בלי לפרסם גרסת אפליקציה חדשה, ולהשתמש ב-Remote Config וב-A/B Testing כדי להציג דינמית מודלים שונים לקבוצות שונות של משתמשים.

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

כשמצרפים את המודל לאפליקציה, אפשר לוודא שפיצ'רים של למידת מכונה באפליקציה ימשיכו לפעול גם כשהמודל שמתארח ב-Firebase לא זמין.

אירוח מודלים ב-Firebase

כדי לארח את מודל TensorFlow Lite ב-Firebase:

  1. בקטע ML Kit במסוף Firebase, לוחצים על הכרטיסייה Custom.
  2. לוחצים על Add custom model (הוספת מודל מותאם אישית) או על Add another model (הוספת מודל נוסף).
  3. מציינים שם שישמש לזיהוי המודל בפרויקט Firebase, ואז מעלים את קובץ המודל של TensorFlow Lite (בדרך כלל מסתיים ב-.tflite או ב-.lite).

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

איך מחברים מודלים לאפליקציה

כדי לארוז את מודל TensorFlow Lite עם האפליקציה, מוסיפים את קובץ המודל (בדרך כלל עם הסיומת .tflite או .lite) לפרויקט Xcode, תוך הקפדה על בחירה באפשרות Copy bundle resources. קובץ המודל ייכלל בחבילת האפליקציה ויהיה זמין ל-ML Kit.

טעינת המודל

כדי להשתמש במודל TensorFlow Lite באפליקציה, קודם צריך להגדיר את ML Kit עם המיקומים שבהם המודל זמין: מרחוק באמצעות Firebase, באחסון המקומי או בשניהם. אם מציינים מודל מקומי וגם מודל מרוחק, אפשר להשתמש במודל המרוחק אם הוא זמין, ולהשתמש במודל שנשמר באופן מקומי אם המודל המרוחק לא זמין.

הגדרת מודל שמתארח ב-Firebase

אם האירוח של המודל בוצע ב-Firebase, יוצרים אובייקט CustomRemoteModel ומציינים את השם שהקציתם למודל כשפרסמתם אותו:

Swift

let remoteModel = CustomRemoteModel(
  name: "your_remote_model"  // The name you assigned in the Firebase console.
)

Objective-C

// Initialize using the name you assigned in the Firebase console.
FIRCustomRemoteModel *remoteModel =
    [[FIRCustomRemoteModel alloc] initWithName:@"your_remote_model"];

לאחר מכן, מפעילים את המשימה של הורדת המודל ומציינים את התנאים שבהם רוצים לאפשר הורדה. אם המודל לא נמצא במכשיר, או אם יש גרסה חדשה יותר של המודל, המשימה תוריד את המודל מ-Firebase באופן אסינכררוני:

Swift

let downloadConditions = ModelDownloadConditions(
  allowsCellularAccess: true,
  allowsBackgroundDownloading: true
)

let downloadProgress = ModelManager.modelManager().download(
  remoteModel,
  conditions: downloadConditions
)

Objective-C

FIRModelDownloadConditions *downloadConditions =
    [[FIRModelDownloadConditions alloc] initWithAllowsCellularAccess:YES
                                         allowsBackgroundDownloading:YES];

NSProgress *downloadProgress =
    [[FIRModelManager modelManager] downloadRemoteModel:remoteModel
                                             conditions:downloadConditions];

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

הגדרת מודל מקומי

אם המודלים נכללים בחבילה של האפליקציה, יוצרים אובייקט CustomLocalModel ומציינים את שם הקובץ של מודל TensorFlow Lite:

Swift

guard let modelPath = Bundle.main.path(
  forResource: "your_model",
  ofType: "tflite",
  inDirectory: "your_model_directory"
) else { /* Handle error. */ }
let localModel = CustomLocalModel(modelPath: modelPath)

Objective-C

NSString *modelPath = [NSBundle.mainBundle pathForResource:@"your_model"
                                                    ofType:@"tflite"
                                               inDirectory:@"your_model_directory"];
FIRCustomLocalModel *localModel =
    [[FIRCustomLocalModel alloc] initWithModelPath:modelPath];

יצירת מתורגמן מהמודל

אחרי שמגדירים את מקורות המודלים, יוצרים אובייקט ModelInterpreter מאחד מהם.

אם יש לכם רק מודל בחבילה מקומית, פשוט מעבירים את האובייקט CustomLocalModel אל modelInterpreter(localModel:):

Swift

let interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)

Objective-C

FIRModelInterpreter *interpreter =
    [FIRModelInterpreter modelInterpreterForLocalModel:localModel];

אם יש לכם מודל שמתארח מרחוק, תצטרכו לוודא שהוא הורדה לפני שתפעילו אותו. אפשר לבדוק את סטטוס המשימה של הורדת המודל באמצעות השיטה isModelDownloaded(remoteModel:) של מנהל המודל.

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

Swift

var interpreter: ModelInterpreter
if ModelManager.modelManager().isModelDownloaded(remoteModel) {
  interpreter = ModelInterpreter.modelInterpreter(remoteModel: remoteModel)
} else {
  interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)
}

Objective-C

FIRModelInterpreter *interpreter;
if ([[FIRModelManager modelManager] isModelDownloaded:remoteModel]) {
  interpreter = [FIRModelInterpreter modelInterpreterForRemoteModel:remoteModel];
} else {
  interpreter = [FIRModelInterpreter modelInterpreterForLocalModel:localModel];
}

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

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

Swift

NotificationCenter.default.addObserver(
    forName: .firebaseMLModelDownloadDidSucceed,
    object: nil,
    queue: nil
) { [weak self] notification in
    guard let strongSelf = self,
        let userInfo = notification.userInfo,
        let model = userInfo[ModelDownloadUserInfoKey.remoteModel.rawValue]
            as? RemoteModel,
        model.name == "your_remote_model"
        else { return }
    // The model was downloaded and is available on the device
}

NotificationCenter.default.addObserver(
    forName: .firebaseMLModelDownloadDidFail,
    object: nil,
    queue: nil
) { [weak self] notification in
    guard let strongSelf = self,
        let userInfo = notification.userInfo,
        let model = userInfo[ModelDownloadUserInfoKey.remoteModel.rawValue]
            as? RemoteModel
        else { return }
    let error = userInfo[ModelDownloadUserInfoKey.error.rawValue]
    // ...
}

Objective-C

__weak typeof(self) weakSelf = self;

[NSNotificationCenter.defaultCenter
    addObserverForName:FIRModelDownloadDidSucceedNotification
                object:nil
                 queue:nil
            usingBlock:^(NSNotification *_Nonnull note) {
              if (weakSelf == nil | note.userInfo == nil) {
                return;
              }
              __strong typeof(self) strongSelf = weakSelf;

              FIRRemoteModel *model = note.userInfo[FIRModelDownloadUserInfoKeyRemoteModel];
              if ([model.name isEqualToString:@"your_remote_model"]) {
                // The model was downloaded and is available on the device
              }
            }];

[NSNotificationCenter.defaultCenter
    addObserverForName:FIRModelDownloadDidFailNotification
                object:nil
                 queue:nil
            usingBlock:^(NSNotification *_Nonnull note) {
              if (weakSelf == nil | note.userInfo == nil) {
                return;
              }
              __strong typeof(self) strongSelf = weakSelf;

              NSError *error = note.userInfo[FIRModelDownloadUserInfoKeyError];
            }];

ציון הקלט והפלט של המודל

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

מודל TensorFlow Lite מקבל מערך אחד או יותר בממדים מרובים כקלט ומפיק מערך אחד או יותר בממדים מרובים כפלט. המערכי האלה מכילים את הערכים byte,‏ int,‏ long או float. צריך להגדיר ב-ML Kit את המספר והמאפיינים ('הצורה') של המערכים שבהם המודלים משתמשים.

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

import tensorflow as tf

interpreter = tf.lite.Interpreter(model_path="my_model.tflite")
interpreter.allocate_tensors()

# Print input shape and type
print(interpreter.get_input_details()[0]['shape'])  # Example: [1 224 224 3]
print(interpreter.get_input_details()[0]['dtype'])  # Example: <class 'numpy.float32'>

# Print output shape and type
print(interpreter.get_output_details()[0]['shape'])  # Example: [1 1000]
print(interpreter.get_output_details()[0]['dtype'])  # Example: <class 'numpy.float32'>

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

לדוגמה, מודל לסיווג תמונות בנקודות צפות עשוי לקבל כקלט מערך Nx224x224x3 של ערכים של Float, שמייצגים קבוצה של N תמונות בגודל 224x224 עם שלושה ערוצים (RGB), וליצור כפלט רשימה של 1,000 ערכים של Float, שכל אחד מהם מייצג את ההסתברות שהתמונה שייכת לאחת מ-1,000 הקטגוריות שהמודל צופה.

במודל כזה, צריך להגדיר את הקלט והפלט של מפרש המודל, כפי שמתואר בהמשך:

Swift

let ioOptions = ModelInputOutputOptions()
do {
    try ioOptions.setInputFormat(index: 0, type: .float32, dimensions: [1, 224, 224, 3])
    try ioOptions.setOutputFormat(index: 0, type: .float32, dimensions: [1, 1000])
} catch let error as NSError {
    print("Failed to set input or output format with error: \(error.localizedDescription)")
}

Objective-C

FIRModelInputOutputOptions *ioOptions = [[FIRModelInputOutputOptions alloc] init];
NSError *error;
[ioOptions setInputFormatForIndex:0
                             type:FIRModelElementTypeFloat32
                       dimensions:@[@1, @224, @224, @3]
                            error:&error];
if (error != nil) { return; }
[ioOptions setOutputFormatForIndex:0
                              type:FIRModelElementTypeFloat32
                        dimensions:@[@1, @1000]
                             error:&error];
if (error != nil) { return; }

ביצוע היסק על נתוני הקלט

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

לדוגמה, אם המודל מעבד תמונות, ויש לו מאפייני קלט של [BATCH_SIZE, 224, 224, 3] ערכים של נקודה צפה, יכול להיות שתצטרכו לשנות את קנה המידה של ערכי הצבע של התמונה לטווח של נקודה צפה, כמו בדוגמה הבאה:

Swift

let image: CGImage = // Your input image
guard let context = CGContext(
  data: nil,
  width: image.width, height: image.height,
  bitsPerComponent: 8, bytesPerRow: image.width * 4,
  space: CGColorSpaceCreateDeviceRGB(),
  bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue
) else {
  return false
}

context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
guard let imageData = context.data else { return false }

let inputs = ModelInputs()
var inputData = Data()
do {
  for row in 0 ..< 224 {
    for col in 0 ..< 224 {
      let offset = 4 * (col * context.width + row)
      // (Ignore offset 0, the unused alpha channel)
      let red = imageData.load(fromByteOffset: offset+1, as: UInt8.self)
      let green = imageData.load(fromByteOffset: offset+2, as: UInt8.self)
      let blue = imageData.load(fromByteOffset: offset+3, as: UInt8.self)

      // Normalize channel values to [0.0, 1.0]. This requirement varies
      // by model. For example, some models might require values to be
      // normalized to the range [-1.0, 1.0] instead, and others might
      // require fixed-point values or the original bytes.
      var normalizedRed = Float32(red) / 255.0
      var normalizedGreen = Float32(green) / 255.0
      var normalizedBlue = Float32(blue) / 255.0

      // Append normalized values to Data object in RGB order.
      let elementSize = MemoryLayout.size(ofValue: normalizedRed)
      var bytes = [UInt8](repeating: 0, count: elementSize)
      memcpy(&bytes, &normalizedRed, elementSize)
      inputData.append(&bytes, count: elementSize)
      memcpy(&bytes, &normalizedGreen, elementSize)
      inputData.append(&bytes, count: elementSize)
      memcpy(&ammp;bytes, &normalizedBlue, elementSize)
      inputData.append(&bytes, count: elementSize)
    }
  }
  try inputs.addInput(inputData)
} catch let error {
  print("Failed to add input: \(error)")
}

Objective-C

CGImageRef image = // Your input image
long imageWidth = CGImageGetWidth(image);
long imageHeight = CGImageGetHeight(image);
CGContextRef context = CGBitmapContextCreate(nil,
                                             imageWidth, imageHeight,
                                             8,
                                             imageWidth * 4,
                                             CGColorSpaceCreateDeviceRGB(),
                                             kCGImageAlphaNoneSkipFirst);
CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), image);
UInt8 *imageData = CGBitmapContextGetData(context);

FIRModelInputs *inputs = [[FIRModelInputs alloc] init];
NSMutableData *inputData = [[NSMutableData alloc] initWithCapacity:0];

for (int row = 0; row < 224; row++) {
  for (int col = 0; col < 224; col++) {
    long offset = 4 * (col * imageWidth + row);
    // Normalize channel values to [0.0, 1.0]. This requirement varies
    // by model. For example, some models might require values to be
    // normalized to the range [-1.0, 1.0] instead, and others might
    // require fixed-point values or the original bytes.
    // (Ignore offset 0, the unused alpha channel)
    Float32 red = imageData[offset+1] / 255.0f;
    Float32 green = imageData[offset+2] / 255.0f;
    Float32 blue = imageData[offset+3] / 255.0f;

    [inputData appendBytes:&red length:sizeof(red)];
    [inputData appendBytes:&green length:sizeof(green)];
    [inputData appendBytes:&blue length:sizeof(blue)];
  }
}

[inputs addInput:inputData error:&error];
if (error != nil) { return nil; }

אחרי שמכינים את הקלט של המודל (ואחרי שמאשרים שהמודל זמין), מעבירים את הקלט ואת אפשרויות הקלט/הפלט לשיטה run(inputs:options:completion:) של מתרגם המודל.

Swift

interpreter.run(inputs: inputs, options: ioOptions) { outputs, error in
    guard error == nil, let outputs = outputs else { return }
    // Process outputs
    // ...
}

Objective-C

[interpreter runWithInputs:inputs
                   options:ioOptions
                completion:^(FIRModelOutputs * _Nullable outputs,
                             NSError * _Nullable error) {
  if (error != nil || outputs == nil) {
    return;
  }
  // Process outputs
  // ...
}];

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

Swift

// Get first and only output of inference with a batch size of 1
let output = try? outputs.output(index: 0) as? [[NSNumber]]
let probabilities = output??[0]

Objective-C

// Get first and only output of inference with a batch size of 1
NSError *outputError;
NSArray *probabilites = [outputs outputAtIndex:0 error:&outputError][0];

אופן השימוש בפלט תלוי במודל שבו אתם משתמשים.

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

Swift

guard let labelPath = Bundle.main.path(forResource: "retrained_labels", ofType: "txt") else { return }
let fileContents = try? String(contentsOfFile: labelPath)
guard let labels = fileContents?.components(separatedBy: "\n") else { return }

for i in 0 ..< labels.count {
  if let probability = probabilities?[i] {
    print("\(labels[i]): \(probability)")
  }
}

Objective-C

NSError *labelReadError = nil;
NSString *labelPath = [NSBundle.mainBundle pathForResource:@"retrained_labels"
                                                    ofType:@"txt"];
NSString *fileContents = [NSString stringWithContentsOfFile:labelPath
                                                   encoding:NSUTF8StringEncoding
                                                      error:&labelReadError];
if (labelReadError != nil || fileContents == NULL) { return; }
NSArray<NSString *> *labels = [fileContents componentsSeparatedByString:@"\n"];
for (int i = 0; i < labels.count; i++) {
    NSString *label = labels[i];
    NSNumber *probability = probabilites[i];
    NSLog(@"%@: %f", label, probability.floatValue);
}

נספח: אבטחת מודל

לא משנה איך תהפכו את המודלים של TensorFlow Lite לזמינים ל-ML Kit, הם יישמרו ב-ML Kit בפורמט ה-protobuf הרגיל בסדרה (serialized) באחסון המקומי.

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