iOS에 Edge 배포 가이드

빌드 대상

이 가이드에서는 AutoML Vision Edge에서 내보낸 커스텀 TensorFlow Lite 모델을 다운로드합니다. 그런 다음 모델을 사용하여 꽃 이미지를 식별하는 미리 제작된 iOS 앱을 실행합니다.

최종 제품 모바일 스크린샷
이미지 출처: 펠리페 베난시오, '어머니의 정원에서'(CC BY 2.0, 앱에 표시된 이미지)

목표

이 입문 내용에서는 코드를 사용해 다음을 수행하는 전반적인 과정을 둘러봅니다.

  • iOS 앱에서 TFLite 인터프리터를 사용하여 선행 학습된 모델을 실행합니다.

시작하기 전에

TensorFlow 설치

가이드를 시작하기 전에 몇 가지 소프트웨어를 설치해야 합니다.

Python이 설치되어 있으면 다음 명령어를 실행하여 이 소프트웨어를 다운로드합니다.

pip install --upgrade  "tensorflow==1.7.*"
pip install PILLOW

Git 저장소 클론

명령줄에서 다음 명령어를 사용하여 Git 저장소를 클론합니다.

git clone https://github.com/googlecodelabs/tensorflow-for-poets-2

저장소 로컬 클론의 디렉터리로 이동합니다(tensorflow-for-poets-2 디렉터리). 이 디렉토리에서 다음 코드 샘플을 모두 실행합니다.

cd tensorflow-for-poets-2

iOS 앱 설정

iOS 앱 데모에는 다음과 같은 추가 도구가 필요합니다.

  1. Xcode
  2. Xcode 명령줄 도구
  3. CocoaPods

Xcode 다운로드

다음 링크를 사용하여 머신에 Xcode를 다운로드합니다.

Xcode 명령줄 도구 설치

다음 명령어를 실행하여 Xcode 명령줄 도구를 설치합니다.

xcode-select --install

CocoaPods 설치

CocoaPods는 기본적으로 macOS에 설치된 Ruby를 사용합니다.

CocoaPods를 설치하려면 다음 명령어를 실행합니다.

sudo gem install cocoapods
Install TFLite Cocoapod

이 Codelab의 나머지 부분은 macOS에서 직접 실행해야 하므로 지금 docker를 종료합니다(Ctrl-D 키를 눌러 docker 종료).

다음 명령어를 사용하여 TensorFlow Lite를 설치하고 CocoaPods를 사용하여 .xcworkspace 파일을 만듭니다.

pod install --project-directory=ios/tflite/

Xcode로 프로젝트를 엽니다. 명령줄 또는 UI를 통해 프로젝트를 열 수 있습니다.

명령줄을 통해 프로젝트를 열려면 다음 명령어를 실행합니다.

open ios/tflite/tflite_photos_example.xcworkspace

UI를 통해 프로젝트를 열려면 Xcode를 실행하고 "다른 프로젝트 열기" 버튼을 선택합니다.

Xcode UI

프로젝트를 연 후 .xcworkspace 파일(.xcproject 파일이 아님)로 이동합니다.

원래의 앱 실행

이 앱은 iOS 시뮬레이터에서 영상 인식 모델을 실행하는 간단한 예시입니다. 시뮬레이터가 카메라 입력을 지원하지 않기 때문에 앱은 사진 라이브러리에서 읽어옵니다.

커스텀 모델을 삽입하기 전에 1,000개의 ImageNet 카테고리에 대해 학습된 기본 "mobilenet"을 사용하는 앱의 기준 버전을 테스트합니다.

시뮬레이터에서 앱을 실행하려면 Xcode 창의 오른쪽 상단에 있는 재생 버튼 xcode 재생 아이콘을 선택합니다.

"다음 사진" 버튼을 사용해 기기에서 다음 사진으로 이동합니다.

사진을 시뮬레이터 창으로 드래그 앤 드롭하여 사진을 기기의 사진 라이브러리에 추가할 수 있습니다.

결과에는 다음 이미지와 유사한 주석이 표시됩니다.

앱 실행 테스트 스크린샷

맞춤설정된 앱 실행

원래의 앱 설정은 표준 MobileNet을 사용하여 이미지를 1,000개의 ImageNet 클래스 중 하나로 분류합니다.

커스텀 이미지 카테고리와 함께 다시 학습된 모델을 사용하도록 앱을 수정합니다.

모델 파일을 프로젝트에 추가

데모 프로젝트는 android/tflite/app/src/main/assets/ 디렉터리에서 graph.litelabels.txt 파일을 검색하도록 구성되었습니다.

이 두 파일을 원하는 버전으로 바꾸려면 다음 명령어를 실행합니다.

cp tf_files/optimized_graph.lite ios/tflite/data/graph.lite
cp tf_files/retrained_labels.txt ios/tflite/data/labels.txt

앱 실행

시뮬레이터에서 앱을 다시 실행하려면 Xcode 창의 오른쪽 상단에 있는 재생 버튼 xcode 재생 아이콘을 선택합니다.

수정 사항을 테스트하려면 flower_photos/ 디렉터리의 이미지 파일을 추가하고 예측을 가져옵니다.

결과는 다음과 유사하게 나타납니다.

최종 제품 모바일 스크린샷
이미지 출처: 펠리페 베난시오, '어머니의 정원에서'(CC BY 2.0, 앱에 표시된 이미지)

기본 이미지는 꽃이 아닙니다.

실제로 모델을 사용해 보려면 이전에 다운로드한 학습 데이터 이미지 일부를 추가하거나 Google 검색에서 일부 이미지를 다운로드하여 예측에 사용하세요.

작동 방식

앱을 실행했으므로 이제 TensorFlow Lite 전용 코드를 살펴 보겠습니다.

TensorFlowLite Pod

이 앱은 사전 컴파일된 TFLite CocoaPod를 사용합니다. Podfile은 프로젝트에 CocoaPod를 포함합니다.

Podfile

platform :ios, '8.0'
inhibit_all_warnings!

target 'tflite_photos_example'
       pod 'TensorFlowLite'

TFLite에 인터페이싱하는 코드는 모두 CameraExampleViewController.mm 파일에 포함됩니다.

설정

첫 번째로 살펴볼 관심분야 블록(필요한 가져오기 이후)은 viewDidLoad 메서드입니다.

CameraExampleViewController.mm

#include "tensorflow/contrib/lite/kernels/register.h"
#include "tensorflow/contrib/lite/model.h"
#include "tensorflow/contrib/lite/string_util.h"
#include "tensorflow/contrib/lite/tools/mutable_op_resolver.h"

...

- (void)viewDidLoad {
  [super viewDidLoad];
  labelLayers = [[NSMutableArray alloc] init];

  NSString* graph_path = FilePathForResourceName(model_file_name, model_file_type);
  model = tflite::FlatBufferModel::BuildFromFile([graph_path UTF8String]);
  if (!model) {
    LOG(FATAL) << "Failed to mmap model " << graph_path;
  }
  LOG(INFO) << "Loaded model " << graph_path;
  model->error_reporter();
  LOG(INFO) << "resolved reporter";

  ...

메서드의 전반부에서 핵심 줄은 model = tflite::FlatBufferModel::BuildFromFile([graph_path UTF8String]); 줄입니다. 이 코드는 그래프 파일에서 FlatBufferModel을 만듭니다.

FlatBuffer는 메모리를 맵핑할 수 있는 데이터 구조입니다. TFLite는 시스템이 모델에서 사용되는 메모리를 더 잘 관리할 수 있게 해주는 주요 기능입니다. 필요에 따라 시스템은 모델의 일부를 메모리 내외로 투명하게 교체할 수 있습니다.

메서드의 후반부는 모델의 인터프리터를 빌드하여 이전에 로드한 그래프 데이터 구조에 작업 구현을 연결합니다.

CameraExampleViewController.mm

- (void)viewDidLoad {
  ...

  tflite::ops::builtin::BuiltinOpResolver resolver;
  LoadLabels(labels_file_name, labels_file_type, &labels);

  tflite::InterpreterBuilder(*model, resolver)(&interpreter);
  if (!interpreter) {
    LOG(FATAL) << "Failed to construct interpreter";
  }
  if (interpreter->AllocateTensors() != kTfLiteOk) {
    LOG(FATAL) << "Failed to allocate tensors!";
  }

  [self attachPreviewLayer];
}

Python의 TensorFlow에 익숙하다면 tf.Session() 빌드와 거의 동일합니다.

모델 실행

UpdatePhoto 메서드는 다음 사진 가져오기, 미리보기 창을 업데이트, 사진에서 모델 실행하기에 대한 모든 세부정보를 처리합니다.

CameraExampleViewController.mm

- (void)UpdatePhoto{
  PHAsset* asset;
  if (photos==nil || photos_index >= photos.count){
    [self updatePhotosLibrary];
    photos_index=0;
  }
  if (photos.count){
    asset = photos[photos_index];
    photos_index += 1;
    input_image = [self convertImageFromAsset:asset
                                   targetSize:CGSizeMake(wanted_input_width, wanted_input_height)
                                         mode:PHImageContentModeAspectFill];
    display_image = [self convertImageFromAsset:asset
                                     targetSize:CGSizeMake(asset.pixelWidth,asset.pixelHeight)
                                           mode:PHImageContentModeAspectFit];
    [self DrawImage];
  }

  if (input_image != nil){
    image_data image = [self CGImageToPixels:input_image.CGImage];
    [self inputImageToModel:image];
    [self runModel];
  }
}

마지막 3줄이 주목하는 부분입니다.

CGImageToPixels 메서드는 iOS 사진 라이브러리에서 반환한 CGImage를 너비, 높이, 채널, 픽셀 데이터가 포함된 간단한 구조로 변환합니다.

CameraExampleViewController.h

typedef struct {
  int width;
  int height;
  int channels;
  std::vector<uint8_t> data;
} image_data;

inputImageToModel 메서드는 이미지를 인터프리터 메모리에 삽입하는 작업을 처리합니다. 여기에는 이미지 크기 조절, 모델에 맞게 픽셀 값 조정 작업이 포함됩니다.

CameraExampleViewController.mm

- (void)inputImageToModel:(image_data)image{
  float* out = interpreter->typed_input_tensor<float>(0);

  const float input_mean = 127.5f;
  const float input_std = 127.5f;
  assert(image.channels >= wanted_input_channels);
  uint8_t* in = image.data.data();

  for (int y = 0; y < wanted_input_height; ++y) {
    const int in_y = (y * image.height) / wanted_input_height;
    uint8_t* in_row = in + (in_y * image.width * image.channels);
    float* out_row = out + (y * wanted_input_width * wanted_input_channels);
    for (int x = 0; x < wanted_input_width; ++x) {
      const int in_x = (x * image.width) / wanted_input_width;
      uint8_t* in_pixel = in_row + (in_x * image.channels);
      float* out_pixel = out_row + (x * wanted_input_channels);
      for (int c = 0; c < wanted_input_channels; ++c) {
        out_pixel[c] = (in_pixel[c] - input_mean) / input_std;
      }
    }
  }
}

모델에 입력이 하나 뿐이므로 float* out = interpreter->typed_input_tensor<float>(0); 명령줄은 인터프리터에 입력 0의 메모리에 대한 포인터를 요청합니다. 나머지 메서드는 포인터 연산 및 픽셀 확장을 처리하여 데이터를 입력 배열에 복사합니다.

마지막으로 runModel 메서드는 모델을 실행합니다.

CameraExampleViewController.mm

- (void)runModel {
  double startTimestamp = [[NSDate new] timeIntervalSince1970];
  if (interpreter->Invoke() != kTfLiteOk) {
    LOG(FATAL) << "Failed to invoke!";
  }
  double endTimestamp = [[NSDate new] timeIntervalSince1970];
  total_latency += (endTimestamp - startTimestamp);
  total_count += 1;
  NSLog(@"Time: %.4lf, avg: %.4lf, count: %d", endTimestamp - startTimestamp,
        total_latency / total_count,  total_count);

  ...

}

다음 runModel은 결과를 다시 읽어옵니다. 이를 위해 인터프리터에 출력 배열 데이터에 대한 포인터를 요청합니다. 출력은 간단한 float 배열입니다. GetTopN 메서드는 우선순위 큐를 사용하여 상위 5개 결과의 추출을 처리합니다.

CameraExampleViewController.mm

- (void)runModel {
  ...

  const int output_size = (int)labels.size();
  const int kNumResults = 5;
  const float kThreshold = 0.1f;

  std::vector<std::pair<float, int>> top_results;

  float* output = interpreter->typed_output_tensor<float>(0);
  GetTopN(output, output_size, kNumResults, kThreshold, &top_results);

  ...
}

다음 몇 줄은 상위 5개 (probability, class_id) 쌍을 (probability, label) 쌍으로 간단하게 변환한 다음 해당 결과를 비동기식으로 setPredictionValues 메서드로 전달하여 화면 보고서를 업데이트합니다.

CameraExampleViewController.mm

- (void)runModel {
  ...

  std::vector<std::pair<float, std::string>> newValues;
  for (const auto& result : top_results) {
    std::pair<float, std::string> item;
    item.first = result.first;
    item.second = labels[result.second];

    newValues.push_back(item);
  }
  dispatch_async(dispatch_get_main_queue(), ^(void) {
    [self setPredictionValues:newValues];
  });
}

다음 단계

이제 Edge 모델을 사용하여 iOS 꽃 분류 앱의 둘러보기를 완료했습니다. 이미지 분류 앱을 수정하고 샘플 주석을 가져오기 전에 학습된 Tensorflow Lite 모델을 사용하여 이미지 분류 앱을 테스트했습니다. 그런 다음 TensorFlow Lite 관련 코드를 검사하여 기본 기능을 이해했습니다.

다음은 계속해서 TensorFlow 모델 및 AutoML Vision Edge를 배우는 데 도움이 되는 리소스입니다.