Sử dụng mô hình TensorFlow Lite để suy luận bằng Bộ công cụ học máy trên iOS

Bạn có thể sử dụng Bộ công cụ học máy để tiến hành suy luận trên thiết bị bằng một Mô hình TensorFlow Lite.

Bộ công cụ học máy chỉ có thể sử dụng các mô hình TensorFlow Lite trên các thiết bị chạy iOS 9 và mới hơn.

Trước khi bắt đầu

  1. Nếu bạn chưa thêm Firebase vào ứng dụng của mình, hãy thực hiện bằng cách làm theo hướng dẫn các bước trong hướng dẫn bắt đầu sử dụng.
  2. Thêm các thư viện Bộ công cụ học máy vào Podfile của bạn:
    pod 'Firebase/MLModelInterpreter', '6.25.0'
    
    Sau khi cài đặt hoặc cập nhật Nhóm của dự án, hãy nhớ mở Xcode bằng cách sử dụng .xcworkspace của nó.
  3. Trong ứng dụng của bạn, hãy nhập Firebase:

    Swift

    import Firebase

    Objective-C

    @import Firebase;
  4. Chuyển đổi mô hình TensorFlow mà bạn muốn sử dụng sang định dạng TensorFlow Lite. Xem TOCO: Trình chuyển đổi tối ưu hoá TensorFlow Lite.

Lưu trữ hoặc nhóm mô hình của bạn

Trước khi có thể sử dụng mô hình TensorFlow Lite để suy luận trong ứng dụng, bạn phải cung cấp mô hình đó cho Bộ công cụ học máy. Bộ công cụ học máy có thể sử dụng TensorFlow Lite các mô hình được lưu trữ từ xa bằng Firebase, đi kèm với tệp nhị phân của ứng dụng hoặc cả hai.

Bằng cách lưu trữ mô hình trên Firebase, bạn có thể cập nhật mô hình đó mà không cần phát hành phiên bản ứng dụng mới và bạn có thể sử dụng Cấu hình từ xa và Thử nghiệm A/B để phân phát linh động các mô hình khác nhau cho các nhóm người dùng khác nhau.

Nếu bạn chỉ chọn cung cấp mô hình bằng cách lưu trữ mô hình đó bằng Firebase, chứ không phải hãy kết hợp ứng dụng đó với ứng dụng, bạn có thể giảm kích thước tải xuống ban đầu của ứng dụng. Mặc dù vậy, hãy lưu ý rằng nếu mô hình không được đóng gói với ứng dụng của bạn, bất kỳ sẽ không có sẵn chức năng liên quan đến mô hình cho đến khi ứng dụng của bạn tải xuống mô hình lần đầu tiên.

Bằng cách kết hợp mô hình với ứng dụng, bạn có thể đảm bảo các tính năng học máy của ứng dụng vẫn hoạt động khi không có mô hình lưu trữ trên Firebase.

Mô hình lưu trữ trên Firebase

Cách lưu trữ mô hình TensorFlow Lite trên Firebase:

  1. Trong mục Bộ công cụ học máy của bảng điều khiển của Firebase, hãy nhấp vào thẻ Tuỳ chỉnh.
  2. Nhấp vào Thêm mô hình tuỳ chỉnh (hoặc Thêm mô hình khác).
  3. Chỉ định tên sẽ được dùng để xác định mô hình trong Firebase dự án, sau đó tải tệp mô hình TensorFlow Lite lên (thường kết thúc bằng .tflite hoặc .lite).

Sau khi thêm mô hình tùy chỉnh vào dự án Firebase, bạn có thể tham khảo trong các ứng dụng của mình bằng tên mà bạn đã chỉ định. Bất cứ lúc nào, bạn cũng có thể tải lên một mô hình TensorFlow Lite mới và ứng dụng của bạn sẽ tải mô hình mới xuống cũng như bắt đầu sử dụng vào lần tiếp theo ứng dụng khởi động lại. Bạn có thể xác định thiết bị các điều kiện cần thiết để ứng dụng của bạn cố gắng cập nhật mô hình (xem bên dưới).

Gộp các mô hình bằng một ứng dụng

Để gói mô hình TensorFlow Lite với ứng dụng của bạn, hãy thêm tệp mô hình (thường là kết thúc bằng .tflite hoặc .lite) vào dự án Xcode của bạn, hãy chú ý chọn Sao chép tài nguyên gói khi bạn thực hiện việc này. Tệp mô hình sẽ được đưa vào và có sẵn cho Bộ công cụ học máy.

Tải mô hình

Để sử dụng mô hình TensorFlow Lite trong ứng dụng của bạn, trước tiên hãy định cấu hình Bộ công cụ học máy bằng các vị trí nơi mô hình của bạn có thể sử dụng: từ xa bằng Firebase, bộ nhớ cục bộ hoặc cả hai. Nếu chỉ định cả mô hình cục bộ lẫn mô hình từ xa, bạn có thể sử dụng mô hình từ xa nếu có và quay lại sử dụng mô hình được lưu trữ cục bộ nếu không có mô hình từ xa.

Định cấu hình mô hình lưu trữ trên Firebase

Nếu bạn lưu trữ mô hình bằng Firebase, hãy tạo đối tượng CustomRemoteModel, chỉ định tên mà bạn đã chỉ định cho mô hình khi xuất bản:

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

Sau đó, hãy bắt đầu tác vụ tải mô hình xuống, xác định các điều kiện mà bạn muốn cho phép tải xuống. Nếu kiểu máy này không có trên thiết bị hoặc nếu là kiểu máy mới hơn phiên bản của mô hình sẵn có, tác vụ sẽ tải xuống không đồng bộ mô hình từ 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];

Nhiều ứng dụng bắt đầu tác vụ tải xuống trong mã khởi chạy của ứng dụng, nhưng bạn có thể làm được vì vậy, tại bất kỳ thời điểm nào trước khi cần sử dụng mô hình này.

Định cấu hình mô hình cục bộ

Nếu bạn đã đóng gói mô hình với ứng dụng của mình, hãy tạo một đối tượng CustomLocalModel, chỉ định tên tệp của mô hình 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];

Tạo phiên dịch từ mô hình của bạn

Sau khi bạn định cấu hình các nguồn mô hình của mình, hãy tạo một ModelInterpreter qua một trong số đó.

Nếu chỉ có mô hình được gói cục bộ, bạn chỉ cần truyền CustomLocalModel đối tượng cho modelInterpreter(localModel:):

Swift

let interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)

Objective-C

FIRModelInterpreter *interpreter =
    [FIRModelInterpreter modelInterpreterForLocalModel:localModel];

Nếu có mô hình được lưu trữ từ xa, bạn sẽ phải kiểm tra xem mô hình đó đã được tải xuống trước khi chạy nó. Bạn có thể kiểm tra trạng thái tải mô hình xuống bằng cách sử dụng phương thức isModelDownloaded(remoteModel:) của trình quản lý mô hình.

Mặc dù bạn chỉ phải xác nhận điều này trước khi chạy phiên dịch, nếu bạn có cả mô hình được lưu trữ từ xa và mô hình được gói cục bộ, ý nghĩa để thực hiện bước kiểm tra này khi tạo thực thể cho ModelInterpreter: tạo một từ mô hình từ xa nếu mô hình đã được tải xuống và từ mô hình mô hình khác.

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

Nếu chỉ có một mô hình được lưu trữ từ xa, bạn nên tắt tính năng liên quan đến mô hình đó chức năng (ví dụ: chuyển sang màu xám hoặc ẩn một phần giao diện người dùng) cho đến khi bạn xác nhận mô hình đã được tải xuống.

Bạn có thể biết trạng thái tải xuống mô hình bằng cách đính kèm đối tượng tiếp nhận dữ liệu vào giá trị mặc định Trung tâm thông báo. Hãy nhớ sử dụng tệp tham chiếu yếu đến self trong trình quan sát vì quá trình tải xuống có thể mất chút thời gian và đối tượng gốc có thể được giải phóng vào thời điểm hoàn tất tải xuống. Ví dụ:

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

Chỉ định dữ liệu đầu vào và đầu ra của mô hình

Tiếp theo, hãy định cấu hình định dạng đầu vào và đầu ra của trình phiên dịch mô hình.

Mô hình TensorFlow Lite lấy dữ liệu đầu vào và tạo ra một hoặc nhiều đầu ra mảng đa chiều. Các mảng này chứa byte, Giá trị int, long hoặc float. Bạn phải định cấu hình Bộ công cụ học máy bằng số lượng và kích thước ("hình dạng") của các mảng mà bạn mô hình sử dụng.

Nếu bạn không biết hình dạng và kiểu dữ liệu của đầu vào và đầu ra của mô hình, bạn có thể sử dụng trình thông dịch TensorFlow Lite Python để kiểm tra mô hình của mình. Để ví dụ:

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'>

Sau khi xác định định dạng đầu vào và đầu ra của mô hình, hãy định cấu hình trình phiên dịch mô hình của ứng dụng bằng cách tạo một Đối tượng ModelInputOutputOptions.

Ví dụ: mô hình phân loại hình ảnh dấu phẩy động có thể lấy dữ liệu đầu vào là NMảng x224x224x3 của Float, đại diện cho một loạt N hình ảnh ba kênh (RGB) 224x224 và xuất ra danh sách 1000 giá trị Float, mỗi giá trị biểu thị xác suất mà hình ảnh là một thành phần của một trong 1.000 danh mục mà mô hình dự đoán.

Đối với mô hình như vậy, bạn sẽ định cấu hình đầu vào và đầu ra của trình phiên dịch mô hình như minh hoạ dưới đây:

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

Tiến hành suy luận về dữ liệu đầu vào

Cuối cùng, để tiến hành suy luận bằng mô hình này, hãy lấy dữ liệu đầu vào, thực hiện mọi các chuyển đổi trên dữ liệu có thể cần thiết cho mô hình của bạn và tạo một Đối tượng Data chứa dữ liệu.

Ví dụ: nếu mô hình của bạn xử lý hình ảnh và mô hình có phương diện đầu vào [BATCH_SIZE, 224, 224, 3] giá trị dấu phẩy động, nên bạn có thể phải mở rộng quy mô các giá trị màu của hình ảnh thành một dải dấu phẩy động như trong ví dụ sau:

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

Sau khi bạn chuẩn bị đầu vào cho mô hình (và sau khi bạn xác nhận mô hình đã sẵn có), truyền các tuỳ chọn đầu vào và đầu vào/đầu ra cho run(inputs:options:completion:) của trình phiên dịch mẫu của bạn .

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

Bạn có thể nhận kết quả bằng cách gọi phương thức output(index:) của đối tượng bị trả về. Ví dụ:

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

Cách bạn sử dụng dữ liệu đầu ra phụ thuộc vào mô hình bạn đang sử dụng.

Ví dụ: nếu bạn đang thực hiện phân loại, trong bước tiếp theo, bạn có thể ánh xạ các chỉ mục của kết quả với nhãn mà chúng đại diện. Giả sử bạn có một tệp văn bản có các chuỗi nhãn cho từng danh mục của mô hình; bạn có thể lập bản đồ chuỗi nhãn với xác suất đầu ra bằng cách thực hiện một số thao tác như sau:

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);
}

Phụ lục: Bảo mật mô hình

Bất kể bạn áp dụng mô hình TensorFlow Lite bằng cách nào ML Kit, ML Kit lưu trữ chúng ở định dạng protobuf được chuyển đổi tuần tự tiêu chuẩn trong lưu trữ cục bộ.

Về mặt lý thuyết, điều này có nghĩa là bất kỳ ai cũng có thể sao chép mô hình của bạn. Tuy nhiên, trong thực tế, hầu hết các mô hình đều dành riêng cho ứng dụng và bị làm rối mã nguồn rủi ro tương tự như các biện pháp tối ưu hoá của đối thủ cạnh tranh bị loại bỏ và việc sử dụng lại mã. Tuy nhiên, bạn nên lưu ý rủi ro này trước khi sử dụng một mô hình tuỳ chỉnh trong ứng dụng của bạn.