Apple 플랫폼에서 커스텀 TensorFlow Lite 모델 사용

앱에서 커스텀 TensorFlow Lite 모델을 사용하는 경우 Firebase ML을 사용하여 모델을 배포할 수 있습니다. Firebase로 모델을 배포하면 앱의 초기 다운로드 크기를 줄일 수 있으며 앱의 새 버전을 출시하지 않고도 앱의 ML 모델을 업데이트할 수 있습니다. 또한 원격 구성 및 A/B 테스팅을 사용하면 다양한 사용자 집합에 서로 다른 모델을 동적으로 제공할 수 있습니다.

기본 요건

  • MLModelDownloader 라이브러리는 Swift에만 사용할 수 있습니다.
  • TensorFlow Lite는 iOS 9 이상을 사용하는 기기에서만 실행됩니다.

TensorFlow Lite 모델

TensorFlow Lite 모델은 휴대기기에서 실행되도록 최적화된 ML 모델입니다. TensorFlow Lite 모델을 가져오려면 다음 안내를 따르세요.

시작하기 전에

현재 TensorFlowLite가 Swift Package Manager를 통한 설치를 지원하지 않기 때문에 TensorFlow Lite를 Firebase와 함께 사용하려면 CocoaPods를 사용해야 합니다. 설치 방법MLModelDownloader에 대한 안내는 CocoaPods 설치 가이드를 참고하세요.

CocoaPods를 설치한 후 Firebase와 TensorFlowLite를 가져와 사용하세요.

Swift

import FirebaseMLModelDownloader
import TensorFlowLite

1. 모델 배포

Firebase Console 또는 Firebase Admin Python 및 Node.js SDK를 사용하여 커스텀 TensorFlow 모델을 배포합니다. 커스텀 모델 배포 및 관리를 참조하세요.

Firebase 프로젝트에 커스텀 모델을 추가한 후 지정한 이름을 사용하여 앱에서 모델을 참조할 수 있습니다. 언제든지 새 TensorFlow Lite 모델을 배포하고 getModel()을 호출하여 새 모델을 사용자 기기에 다운로드할 수 있습니다(아래 참조).

2. 기기에 모델 다운로드 및 TensorFlow Lite 인터프리터 초기화

앱에서 TensorFlow Lite 모델을 사용하려면 우선 Firebase ML SDK를 사용하여 모델의 최신 버전을 기기에 다운로드합니다.

모델 다운로드를 시작하려면 모델을 업로드할 때 할당한 이름, 항상 최신 모델을 다운로드할지 여부, 다운로드 허용 조건을 지정하여 모델 다운로더의 getModel() 메서드를 호출합니다.

다음 세 가지 다운로드 동작 중에서 선택할 수 있습니다.

다운로드 유형 설명
localModel 기기에서 로컬 모델을 가져옵니다. 사용할 수 있는 로컬 모델이 없으면 latestModel처럼 작동합니다. 모델 업데이트를 확인하지 않아도 된다면 이 다운로드 유형을 사용하세요. 예를 들어 원격 구성을 사용하여 모델 이름을 검색하고 항상 새 이름으로 모델을 업로드합니다(권장).
localModelUpdateInBackground 기기에서 로컬 모델을 가져와 백그라운드에서 모델 업데이트를 시작합니다. 사용할 수 있는 로컬 모델이 없으면 latestModel처럼 작동합니다.
latestModel 최신 모델을 가져옵니다. 로컬 모델이 최신 버전이면 로컬 모델을 반환합니다. 그렇지 않은 경우 최신 모델을 다운로드합니다. 이 동작은 최신 버전이 다운로드될 때까지 차단됩니다(권장하지 않음). 최신 버전이 명시적으로 필요한 경우에만 이 동작을 사용하세요.

모델 다운로드 여부가 확인될 때까지 모델 관련 기능 사용을 중지해야 합니다(예: UI 비활성화 또는 숨김).

Swift

let conditions = ModelDownloadConditions(allowsCellularAccess: false)
ModelDownloader.modelDownloader()
    .getModel(name: "your_model",
              downloadType: .localModelUpdateInBackground,
              conditions: conditions) { result in
        switch (result) {
        case .success(let customModel):
            do {
                // Download complete. Depending on your app, you could enable the ML
                // feature, or switch from the local model to the remote model, etc.

                // The CustomModel object contains the local path of the model file,
                // which you can use to instantiate a TensorFlow Lite interpreter.
                let interpreter = try Interpreter(modelPath: customModel.path)
            } catch {
                // Error. Bad model file?
            }
        case .failure(let error):
            // Download was unsuccessful. Don't enable ML features.
            print(error)
        }
}

대부분의 앱은 초기화 코드로 다운로드 작업을 시작하지만 모델 사용이 필요한 시점 이전에는 언제든지 다운로드할 수 있습니다.

3. 입력 데이터에 대한 추론 수행

모델의 입력 및 출력 모양 가져오기

TensorFlow Lite 모델 인터프리터는 하나 이상의 다차원 배열을 입력으로 받아 출력합니다. 이러한 배열은 byte, int, long, float 값 중 하나를 포함합니다. 모델에 데이터를 전달하거나 결과를 사용하려면 먼저 모델에서 사용하는 배열의 수와 차원('모양')을 알아야 합니다.

모델을 직접 빌드했거나 모델의 입력 및 출력 형식이 문서화된 경우 이 정보를 이미 알고 있을 수 있습니다. 모델의 입출력 모양과 데이터 유형을 모르는 경우 TensorFlow Lite 인터프리터를 사용하여 모델을 검사할 수 있습니다. 예를 들면 다음과 같습니다.

Python

import tensorflow as tf

interpreter = tf.lite.Interpreter(model_path="your_model.tflite")
interpreter.allocate_tensors()

# Print input shape and type
inputs = interpreter.get_input_details()
print('{} input(s):'.format(len(inputs)))
for i in range(0, len(inputs)):
    print('{} {}'.format(inputs[i]['shape'], inputs[i]['dtype']))

# Print output shape and type
outputs = interpreter.get_output_details()
print('\n{} output(s):'.format(len(outputs)))
for i in range(0, len(outputs)):
    print('{} {}'.format(outputs[i]['shape'], outputs[i]['dtype']))

출력 예시:

1 input(s):
[  1 224 224   3] <class 'numpy.float32'>

1 output(s):
[1 1000] <class 'numpy.float32'>

인터프리터 실행

모델의 입력과 출력 형식을 확인했으면 입력 데이터를 가져와 모델에 적합한 입력 모양을 가져오는 데 필요한 데이터 변환을 수행합니다.

예를 들어 모델에서 이미지를 처리하고 모델의 입력 크기가 [1, 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 }

var inputData = Data()
for row in 0 ..&lt; 224 {
  for col in 0 ..&lt; 224 {
    let offset = 4 * (row * context.width + col)
    // (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(&amp;bytes, &amp;normalizedRed, elementSize)
    inputData.append(&amp;bytes, count: elementSize)
    memcpy(&amp;bytes, &amp;normalizedGreen, elementSize)
    inputData.append(&amp;bytes, count: elementSize)
    memcpy(&ammp;bytes, &amp;normalizedBlue, elementSize)
    inputData.append(&amp;bytes, count: elementSize)
  }
}

그런 다음 입력 NSData를 인터프리터에 복사하고 실행합니다.

Swift

try interpreter.allocateTensors()
try interpreter.copy(inputData, toInputAt: 0)
try interpreter.invoke()

인터프리터의 output(at:) 메서드를 호출하여 모델의 출력을 가져올 수 있습니다. 출력을 사용하는 방법은 사용 중인 모델에 따라 다릅니다.

예를 들어 분류를 수행하면 결과의 색인을 색인이 나타내는 라벨에 매핑할 수 있습니다.

Swift

let output = try interpreter.output(at: 0)
let probabilities =
        UnsafeMutableBufferPointer<Float32>.allocate(capacity: 1000)
output.data.copyBytes(to: probabilities)

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 labels.indices {
    print("\(labels[i]): \(probabilities[i])")
}

부록: 모델 보안

TensorFlow Lite 모델을 어떤 방식으로 Firebase ML에 제공하든 Firebase ML은 로컬 스토리지에 직렬화된 표준 프로토콜 버퍼(protobuf) 형식으로 모델을 저장합니다.

즉, 이론적으로는 누구나 모델을 복사할 수 있습니다. 하지만 실제로는 대부분의 모델이 애플리케이션별로 너무나 다르며 최적화를 통해 난독화되므로 위험도는 경쟁업체가 내 코드를 분해해서 재사용하는 것과 비슷한 수준입니다. 그러나 앱에서 커스텀 모델을 사용하기 전에 이러한 위험성을 알고 있어야 합니다.