Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기

iOS에서 ML Kit로 추론을 위해 TensorFlow Lite 모델 사용

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

ML Kit를 사용하여 TensorFlow Lite 모델로 기기 내 추론을 수행할 수 있습니다.

ML Kit는 iOS 9 이상을 실행하는 기기에서만 TensorFlow Lite 모델을 사용할 수 있습니다.

시작하기 전에

  1. 아직 앱에 Firebase를 추가하지 않았다면 시작하기 가이드 의 단계에 따라 추가하세요.
  2. Podfile에 ML Kit 라이브러리 포함:
    pod 'Firebase/MLModelInterpreter', '6.25.0'
    
    프로젝트의 Pod를 설치하거나 업데이트한 후 .xcworkspace 를 사용하여 Xcode 프로젝트를 열어야 합니다.
  3. 앱에서 Firebase를 가져옵니다.

    빠른

    import Firebase

    오브젝티브-C

    @import Firebase;
  4. 사용하려는 TensorFlow 모델을 TensorFlow Lite 형식으로 변환합니다. TOCO: TensorFlow Lite 최적화 변환기 를 참조하십시오.

모델 호스팅 또는 번들링

앱에서 추론을 위해 TensorFlow Lite 모델을 사용하려면 먼저 모델을 ML Kit에서 사용할 수 있도록 설정해야 합니다. ML Kit는 Firebase를 사용하여 원격으로 호스팅되는 TensorFlow Lite 모델, 앱 바이너리와 함께 번들로 제공되는 모델 또는 둘 다를 사용할 수 있습니다.

Firebase에서 모델을 호스팅하면 새 앱 버전을 출시하지 않고도 모델을 업데이트할 수 있으며 원격 구성 및 A/B 테스팅을 사용하여 다양한 모델을 다양한 사용자에게 동적으로 제공할 수 있습니다.

모델을 Firebase와 함께 호스팅하여 제공하기만 하고 앱과 번들로 묶지 않도록 선택한 경우 앱의 초기 다운로드 크기를 줄일 수 있습니다. 그러나 모델이 앱과 함께 번들로 제공되지 않으면 앱에서 모델을 처음 다운로드할 때까지 모델 관련 기능을 사용할 수 없습니다.

모델을 앱과 번들로 묶으면 Firebase 호스팅 모델을 사용할 수 없을 때 앱의 ML 기능이 계속 작동하도록 할 수 있습니다.

Firebase에서 모델 호스팅

Firebase에서 TensorFlow Lite 모델을 호스팅하려면 다음 안내를 따르세요.

  1. Firebase 콘솔ML Kit 섹션에서 맞춤 탭을 클릭합니다.
  2. 사용자 지정 모델 추가 (또는 다른 모델 추가 )를 클릭합니다.
  3. Firebase 프로젝트에서 모델을 식별하는 데 사용할 이름을 지정한 다음 TensorFlow Lite 모델 파일(일반적으로 .tflite 또는 .lite 로 끝남)을 업로드합니다.

Firebase 프로젝트에 맞춤 모델을 추가한 후 지정한 이름을 사용하여 앱에서 모델을 참조할 수 있습니다. 언제든지 새 TensorFlow Lite 모델을 업로드할 수 있으며 앱은 다음에 앱이 다시 시작될 때 새 모델을 다운로드하고 사용을 시작합니다. 앱이 모델 업데이트를 시도하는 데 필요한 장치 조건을 정의할 수 있습니다(아래 참조).

앱으로 모델 번들

TensorFlow Lite 모델을 앱과 번들로 묶으려면 모델 파일(일반적으로 .tflite 또는 .lite 로 끝남)을 Xcode 프로젝트에 추가합니다. 이때 Copy bundle resources 를 선택하도록 주의하십시오. 모델 파일은 앱 번들에 포함되며 ML Kit에서 사용할 수 있습니다.

모델 로드

앱에서 TensorFlow Lite 모델을 사용하려면 먼저 Firebase를 사용하여 원격으로, 로컬 저장소에서 또는 둘 다에서 모델을 사용할 수 있는 위치로 ML Kit를 구성합니다. 로컬 모델과 원격 모델을 모두 지정하는 경우 원격 모델이 있는 경우 원격 모델을 사용할 수 있고 원격 모델을 사용할 수 없는 경우 로컬에 저장된 모델로 대체할 수 있습니다.

Firebase 호스팅 모델 구성

Firebase로 모델을 호스팅한 경우 모델을 게시할 때 할당한 이름을 지정하여 CustomRemoteModel 객체를 생성합니다.

빠른

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

오브젝티브-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
)

오브젝티브-C

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

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

많은 앱이 초기화 코드에서 다운로드 작업을 시작하지만 모델을 사용하기 전에 언제든지 시작할 수 있습니다.

로컬 모델 구성

모델을 앱과 번들로 묶은 경우 TensorFlow Lite 모델의 파일 ​​이름을 지정하여 CustomLocalModel 객체를 생성합니다.

빠른

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

오브젝티브-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)

오브젝티브-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)
}

오브젝티브-C

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

원격으로 호스팅되는 모델만 있는 경우 모델이 다운로드되었음을 확인할 때까지 모델 관련 기능(예: UI의 일부를 회색으로 표시하거나 숨기기)을 비활성화해야 합니다.

기본 알림 센터에 관찰자를 연결하여 모델 다운로드 상태를 얻을 수 있습니다. 다운로드에 시간이 걸릴 수 있고 다운로드가 완료될 때 원래 개체가 해제될 수 있으므로 관찰자 블록에서 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]
    // ...
}

오브젝티브-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 개체를 만들어 앱의 모델 인터프리터를 구성합니다.

예를 들어, 부동 소수점 이미지 분류 모델은 N 224x224 3채널(RGB) 이미지의 배치를 나타내는 N x224x224x3 Float 값 배열을 입력으로 사용하고 출력으로 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)")
}

오브젝티브-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)")
}

오브젝티브-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
    // ...
}

오브젝티브-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]

오브젝티브-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)")
  }
}

오브젝티브-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 형식으로 로컬 저장소에 저장합니다.

이론적으로 이것은 누구나 모델을 복사할 수 있음을 의미합니다. 그러나 실제로 대부분의 모델은 애플리케이션에 따라 다르고 최적화로 인해 난독화되어 경쟁업체가 코드를 분해하고 재사용하는 것과 유사한 위험이 있습니다. 그럼에도 불구하고 앱에서 사용자 지정 모델을 사용하기 전에 이러한 위험을 알고 있어야 합니다.