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

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

ML Kit יכול להשתמש במודלים של TensorFlow Lite רק במכשירים עם iOS 9 חדש יותר.

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

  1. אם עדיין לא הוספתם את Firebase לאפליקציה, צריך לבצע את הפעולות הבאות במדריך לתחילת העבודה.
  2. כוללים את ספריות ML Kit ב-Podfile:
    pod 'Firebase/MLModelInterpreter', '6.25.0'
    
    אחרי שמתקינים או מעדכנים את קבוצות ה-Pod של הפרויקט, חשוב לפתוח את ה-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 לצרף אותו לאפליקציה שלך, אפשר להקטין את גודל ההורדה הראשוני של האפליקציה. עם זאת, חשוב לזכור שאם המודל לא נכלל בחבילה עם האפליקציה שלכם, שקשורה למודלים לא יהיו זמינים עד שהאפליקציה תוריד את בפעם הראשונה.

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

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

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

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

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

קיבוץ מודלים עם אפליקציה

כדי לצרף את מודל TensorFlow Lite לאפליקציה, צריך להוסיף את קובץ המודל (בדרך כלל שמסתיים בספרות .tflite או .lite) לפרויקט Xcode שלך, תוך הקפדה על הבחירה לשם כך, מעתיקים את משאבי החבילה. קובץ המודל ייכלל App Bundle וזמין ל-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 במספר והמאפיינים ("הצורה") של המערכים של המודל.

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

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.

לדוגמה, מודל לסיווג תמונות בנקודות צפות עשוי לקבל כקלט מערך של ערכים Float בגודל Nx224x224x3, שמייצגים קבוצה של 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] ערכי נקודה צפה (floating-point), יכול להיות שיהיה צורך לשנות את קנה המידה את ערכי הצבעים של התמונה בטווח של נקודה צפה (floating-point) כמו בדוגמה הבאה:

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
  // ...
}];

אפשר לקבל את הפלט על ידי קריאה ל-method של 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) באחסון המקומי.

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