השתמש בדגם TensorFlow Lite להסקת מסקנות עם ערכת ML ב-iOS

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

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

לפני שאתה מתחיל

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

    מָהִיר

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

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

מארח דגמים ב-Firebase

כדי לארח את דגם TensorFlow Lite ב-Firebase:

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

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

צרור דגמים עם אפליקציה

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

טען את הדגם

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

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

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

מָהִיר

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:

מָהִיר

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:

מָהִיר

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:) :

מָהִיר

let interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)

Objective-C

FIRModelInterpreter *interpreter =
    [FIRModelInterpreter modelInterpreterForLocalModel:localModel];

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

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

מָהִיר

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

מָהִיר

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 עם המספר והממדים ("צורה") של המערכים שהמודל שלך משתמש בו.

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

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

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

מָהִיר

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

מָהִיר

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:) מתורגמן המודל שלך.

מָהִיר

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:) של האובייקט שמוחזר. לדוגמה:

מָהִיר

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

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

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

מָהִיר

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 מאחסנת אותם בפורמט פרוטובוף סידורי סטנדרטי באחסון מקומי.

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