ML Kit를 통해 TensorFlow Lite 모델을 사용하여 기기별 추론을 수행할 수 있습니다.
ML Kit는 iOS 9 이상을 실행하는 기기에서만 TensorFlow Lite 모델을 사용할 수 있습니다.
시작하기 전에
- 앱에 Firebase를 아직 추가하지 않았다면 시작 가이드의 단계에 따라 추가합니다.
- Podfile에 ML Kit 라이브러리를 포함합니다.
프로젝트의 포드를 설치하거나 업데이트한 후pod 'Firebase/MLModelInterpreter', '6.25.0'
.xcworkspace
를 사용하여 Xcode 프로젝트를 열어야 합니다. - 앱에서 Firebase를 가져옵니다.
Swift
import Firebase
Objective-C
@import Firebase;
- 사용하려는 TensorFlow 모델을 TensorFlow Lite 형식으로 변환합니다. TOCO: TensorFlow Lite 최적화 변환기를 참조하세요.
모델 호스팅 또는 번들로 묶기
앱에서 TensorFlow Lite 모델을 사용하여 추론하려면 ML Kit에서 모델을 사용할 수 있도록 설정해야 합니다. ML Kit는 Firebase를 사용하여 원격으로 호스팅되는 TensorFlow Lite 모델이나 앱 바이너리에 번들로 묶은 TensorFlow Lite 모델 혹은 두 모델을 모두 사용할 수 있습니다.
Firebase에서 모델을 호스팅하면 앱 버전을 새롭게 출시하지 않고 모델을 업데이트할 수 있고 원격 구성과 A/B 테스팅을 사용하여 다양한 사용자 집합에 각기 다른 모델을 동적으로 제공할 수 있습니다.
모델을 앱과 번들로 묶지 않고 Firebase에서 모델을 호스팅하여 제공하는 방법만 선택한 경우 앱의 초기 다운로드 크기를 줄일 수 있습니다. 다만 모델을 앱과 번들로 묶지 않을 경우 앱이 처음으로 모델을 다운로드하기 전까지 모든 모델 관련 기능을 사용할 수 없다는 점에 유의하세요.
모델을 앱과 번들로 묶으면 Firebase 호스팅 모델을 사용할 수 없는 경우에도 앱의 ML 기능이 계속 작동하도록 할 수 있습니다.
Firebase에서 모델 호스팅
Firebase에서 TensorFlow Lite 모델을 호스팅하는 방법은 다음과 같습니다.
- Firebase Console의 ML Kit 섹션에서 커스텀 탭을 클릭합니다.
- 커스텀 모델 추가 또는 다른 모델 추가를 클릭합니다.
- Firebase 프로젝트에서 모델을 식별하는 데 사용할 이름을 지정한 다음 일반적으로
.tflite
또는.lite
로 끝나는 TensorFlow Lite 모델 파일을 업로드합니다.
Firebase 프로젝트에 커스텀 모델을 추가한 후 지정한 이름을 사용하여 앱에서 모델을 참조할 수 있습니다. 언제든지 새 TensorFlow Lite 모델을 업로드할 수 있으며 다음에 앱이 다시 시작될 때 앱에서 새 모델을 다운로드한 후 사용하기 시작합니다. 앱이 모델 업데이트를 시도하는 데 필요한 기기 조건을 정의할 수 있습니다. 아래를 참조하세요.
모델을 앱과 번들로 묶기
TensorFlow Lite 모델을 앱과 번들로 묶으려면 일반적으로 .tflite
또는 .lite
로 끝나는 모델 파일을 Xcode 프로젝트에 추가합니다. 이때 Copy bundle resources(번들 리소스 복사)를 선택해야 합니다. 모델 파일이 앱 번들에 포함되며 ML Kit에서 사용할 수 있습니다.
모델 로드
앱에서 TensorFlow Lite 모델을 사용하려면 먼저 모델을 사용할 수 있는 위치(Firebase를 사용하는 원격 위치, 로컬 스토리지 또는 둘 다)로 ML Kit를 구성합니다. 로컬 모델과 원격 모델을 둘 다 지정한 경우 원격 모델을 사용할 수 있으면 원격 모델을 사용하고 그렇지 않으면 로컬에 저장된 모델을 대신 사용합니다.
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];
대부분의 앱은 초기화 코드로 다운로드 작업을 시작하지만 모델 사용이 필요한 시점 이전에는 언제든지 다운로드할 수 있습니다.
로컬 모델 구성
모델을 앱과 번들로 묶은 경우에는 TensorFlow Lite 모델의 파일 이름을 지정하여 CustomLocalModel
객체를 만듭니다.
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];
}
원격 호스팅 모델만 있다면 모델 다운로드 여부가 확인될 때까지 모델 관련 기능 사용을 중지해야 합니다(예: UI 비활성화 또는 숨김).
기본 알림 센터에 관찰자를 연결하여 모델 다운로드 상태를 가져올 수 있습니다. 다운로드하는 데 시간이 걸릴 수 있고 다운로드가 완료되면 원래 객체가 해제될 수 있으므로 관찰자 블록의 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
객체를 만들어 앱의 모델 인터프리터를 구성합니다.
예를 들어 부동 소수점 이미지 분류 모델은 N개의 224x224 3채널(RGB) 이미지 배치를 나타내는 Nx224x224x3 Float
값 배열을 입력으로 사용하여 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 형식으로 모델을 저장합니다.
즉 누구나 모델을 복사할 수 있어야 한다는 말입니다. 하지만 실제로는 대부분의 모델이 애플리케이션별로 너무나 다르며 최적화를 통해 난독화되므로 위험도는 경쟁업체가 내 코드를 분해해서 재사용하는 것과 비슷한 수준입니다. 그러나 앱에서 커스텀 모델을 사용하기 전에 이러한 위험성을 알고 있어야 합니다.