콘텐츠로 이동

C/C++ API를 사용한 이미지 분류

이 튜토리얼은 RBLN SDK C/C++ API를 사용하여 파이토치 ResNet50 모델을 배포하는 방법을 소개합니다. 모델 컴파일은 RBLN SDK Python API를 통해 수행할 수 있으며, 결과로 나온 *.rbln 파일은 RBLN SDK C/C++ API를 사용하여 추론을 실행 할 수 있습니다.

이는 Python API의 간편한 모델 준비와 C/C++의 빠른 추론 성능을 결합한 접근 방식입니다. 튜토리얼에서 사용된 전체 코드는 RBLN Model Zoo에서 확인할 수 있습니다.

이 튜토리얼은 두 부분으로 나누어져 있습니다:

  1. Python API로 PyTorch ResNet50을 컴파일하고, 컴파일된 모델을 저장하는 방법
  2. 컴파일된 모델을 C/C++ 런타임 기반 추론 환경에서 배포하는 방법

전제 조건

시작하기 전에 시스템에 다음 패키지가 설치되어 있는지 확인하세요:

1단계. 컴파일 방법

RBLN Python API는 RBLN SDK 내에서 컴파일과 추론을 모두 처리할 수 있는 포괄적인 기능을 제공하지만, RBLN SDK C/C++ API는 추론 작업에만 특별히 최적화되어 있습니다.

이 튜토리얼에서는 모델 컴파일은 RBLN Python API를 사용하고, 추론 작업에는 RBLN C/C++ API를 사용합니다.

모델 준비

먼저 TorchVision 라이브러리에서 ResNet50 모델을 가져올 수 있습니다.

1
2
3
4
5
6
7
from torchvision import models
import rebel
import torch

model_name = "resnet50"
weights = models.get_model_weights(model_name).DEFAULT
model = getattr(models, model_name)(weights=weights).eval()

모델 컴파일

torch 모델 torch.nn.Module이 준비되면, rebel.compile_from_torch() 메서드를 사용하여 간단히 모델을 컴파일할 수 있습니다.

# 모델 컴파일
compiled_model = rebel.compile_from_torch(model, [("x", [1, 3, 224, 224], "float32")], npu="RBLN-CA02")

NPU가 호스트 머신에 장착되어 있다면, rebel.compile_from_torch() 함수에서 npu 인자를 생략할 수 있습니다. 이 경우 함수가 자동으로 장착되어 있는 NPU를 감지하여 NPU 종류에 맞게 컴파일을 진행합니다. 만약 NPU가 호스트 머신에 장착되어 있지 않다면, 오류를 방지하기 위해 npu 인자를 사용하여 대상 NPU를 지정해야 합니다.

현재 지원되는 NPU 이름은 RBLN-CA02, RBLN-CA12 두 가지입니다. 대상 NPU의 이름을 모르는 경우, NPU가 설치된 호스트 머신의 명령어 창에서 rbln-stat 명령을 실행하여 확인할 수 있습니다.

컴파일된 모델 저장

컴파일된 모델을 로컬 저장소에 저장하려면, compiled_model.save() 메서드를 사용할 수 있습니다. 이 함수를 사용하면 배포를 위해 컴파일된 모델을 저장할 수 있습니다:

# 로컬 저장소에 컴파일된 모델을 저장
compiled_model.save(f"{model_name}.rbln")   # model_name = resnet50

컴파일 진행

위의 컴파일 코드는 compile.py에 포함되어 있습니다. 모델을 컴파일하고 *.rbln 파일을 생성하려면 다음 명령으로 compile.py를 실행하세요:

python compile.py --model-name=resnet50

이 과정이 성공적으로 완료되면 로컬 저장소에서 resnet50.rbln을 찾을 수 있습니다. 이 파일은 컴파일된 ResNet50 모델을 포함하고 있으며,RBLN SDK C/C++ API를 사용하여 배포할 준비가 된 상태입니다.

2단계. RBLN SDK C/C++ API를 사용한 배포 방법

이제 RBLN SDK C/C++ API를 사용하여 컴파일된 모델을 로드하고, 추론을 실행하고, 출력 결과를 확인할 수 있습니다.

CMake 빌드 스크립트 준비

이 튜토리얼은 이미지 전/후처리를 위해 OpenCV를 사용하고, 명령줄 인터페이스(CLI)에서 사용자 매개변수를 파싱하기 위해 argparse를 사용합니다.

다음 CMake 스크립트는 외부 패키지에 대한 의존성과 이들을 예제 애플리케이션 코드와 연결하는 방법을 설명합니다.

# 외부 패키지에 대한 의존성 정의
include(FetchContent)
include(cmake/opencv.cmake)
include(cmake/argparse.cmake)

# 실행 파일의 이름 정의
add_executable(image_classification main.cc)

# 패키지 의존성에 대한 링크 정보 업데이트: OpenCV
find_package(OpenCV CONFIG REQUIRED)
target_link_libraries(image_classification ${OpenCV_LIBS})

# 의존성에 대한 링크 정보 업데이트: RBLN
find_package(rbln CONFIG REQUIRED)
target_link_libraries(image_classification rbln::rbln_runtime)

# 의존성 포함 업데이트: argparse
target_include_directories(image_classification PRIVATE ${argparse_INCLUDE_DIRS})

입력 준비

사전 훈련된 ResNet50 모델에 필요한 전처리된 이미지를 입력 데이터로 준비해야 합니다. 여기서는 OpenCV가 제공하는 다양한 비전 API를 사용하여 입력 이미지에 대한 전처리를 수행합니다.

  std::string input_path = "${SAMPLE_PATH}/tabby.jpg";

  // 이미지 전처리
  cv::Mat input_image;
  try {
    input_image = cv::imread(input_path);
  }
  catch (const cv::Exception &err) {
    std::cerr << err.what() << std::endl;
    std::exit(1);
  }
  cv::Mat image;
  cv::cvtColor(input_image, image, cv::COLOR_BGR2RGB);

  // 종횡비를 유지하며 크기 조정
  float scale = image.rows < image.cols ? 256. / image.rows : 256. / image.cols;
  cv::resize(image, image, cv::Size(), scale, scale, cv::INTER_LINEAR);

  // 이미지 중앙 위치로 크롭
  image = image(cv::Rect((image.cols - 224) / 2, (image.rows - 224) / 2, 224, 224));

  // 이미지 정규화
  image.convertTo(image, CV_32F);
  cv::Vec3f mean(123.68, 116.28, 103.53);
  cv::Vec3f std(58.395, 57.120, 57.385);
  for (unsigned i = 0; i < image.rows; i++) {
    for (unsigned j = 0; j < image.cols; j++) {
      cv::subtract(image.at<cv::Vec3f>(i, j), mean, image.at<cv::Vec3f>(i, j));
      cv::divide(image.at<cv::Vec3f>(i, j), std, image.at<cv::Vec3f>(i, j));
    }
  }

  // CV 행렬(텐서)로 이미지 변환
  cv::Mat blob = cv::dnn::blobFromImage(image);

추론 실행

RBLN SDK C/C++ API는 동기와 비동기 추론 방식을 지원합니다. 간략화된 API들에 대한 설명은 아래를 참조하세요.

RBLN API rbln_create_model은 저장된 모델의 경로를 입력 인자로 전달하여 컴파일된 모델을 로드하는 데 사용됩니다.

또한, rbln_create_runtime을 사용하여 RBLNModel, 모듈 이름, 장치 ID로부터 동기 런타임을 생성할 수 있습니다. 비동기 동작을 위해서는 동기식 런타임에 사용한 것과 동일한 인자들을 rbln_create_async_runtime에 전달하여 비동기 런타임을 생성할 수 있습니다.

런타임에 입력 이미지를 할당하기 위해 rbln_set_input을 사용합니다. 이 API는 RBLNRuntime, 입력 버퍼의 인덱스, 전처리된 버퍼의 주소를 인자로 받습니다. 해당 API는 동기식 동작에서만 사용 할 수 있습니다.

모든 입력이 업데이트되면, RBLNRuntime을 인자로 rbln_run을 호출하여 동기 추론을 수행할 수 있습니다. 비동기 동작의 경우 rbln_async_run에 입력 버퍼와 출력 버퍼를 전달함으로써 비동기 추론 수행을 할 수 있습니다.

마지막으로, rbln_get_output을 사용하여 추론 결과가 포함된 출력 버퍼를 검색할 수 있습니다. 이 API는 RBLNRuntime과 출력 인덱스를 인자로 받습니다. 비동기식 동작의 경우, rbln_run을 호출할 때 입력 버퍼와 출력 버퍼를 전달했으므로, 해당 출력 버퍼들을 직접 참조해야 합니다.

각 추론 모드에서 필요한 API 사용법에 대해서는 다음 두 가지 예제를 참고해 주세요:

  • 동기 실행

      std::string model_path = "${SAMPLE_PATH}/resnet50.rbln";
    
      RBLNModel *mod = rbln_create_model(model_path.c_str());
      RBLNRuntime *rt = rbln_create_runtime(mod, "default", 0);
    
      // 입력 데이터 설정
      rbln_set_input(rt, 0, blob.data);
    
      // 동기 추론 실행
      rbln_run(rt);
    
      // 출력 결과 가져오기
      float *logits = static_cast<float *>(rbln_get_output(rt, 0));
    

  • 비동기 실행

      std::string model_path = "${SAMPLE_PATH}/resnet50.rbln";
    
      RBLNModel *mod = rbln_create_model(model_path.c_str());
      RBLNRuntime *rt = rbln_create_async_runtime(mod, "default", 0);
    
      // 출력 버퍼 할당
      auto buf_size = rbln_get_layout_nbytes(rbln_get_output_layout(rt, 0));
      std::vector<float> logits(buf_size/sizeof(float));
    
      // 비동기 추론 실행
      int rid = rbln_async_run(rt, blob.data, logits.data());
    
      // 추론 완료 대기
      rbln_async_wait(rt, rid, 1000);
    

후처리

출력 logits는 크기가 (1, 1000)인 float32 데이터 배열로, 각 요소는 ImageNet 데이터셋의 해당 카테고리의 점수를 나타냅니다. 이 logits에서 top-1 인덱스를 도출하고, 이 top-1 인덱스를 사용하여 미리 정의된 Top1 클래스에서 해당 카테고리를 검색할 수 있습니다.

  // 후처리
  size_t max_idx = 0;
  float max_val = std::numeric_limits<float>::min();
  for (size_t i = 0; i < 1000; i++) {
    if (logits[i] > max_val) {
      max_val = logits[i];
      max_idx = i;
    }
  }

  // 분류된 출력 출력
  std::cout << "예측된 카테고리: " << IMAGENET_CATEGORIES[max_idx] << std::endl;

리소스 해제

1
2
3
4
5
  // 런타임 해제
  rbln_destroy_runtime(rt);

  // 모델 해제
  rbln_destroy_model(mod);

CMake를 사용하여 빌드하는 방법

위의 API 예제들에 대한 전체 코드는 RBLN Model Zoo C++ 예제에 포함되어 있습니다. 다음 명령어를 사용하여 코드를 쉽게 컴파일하고 실행 가능한 바이너리를 생성할 수 있습니다:

${SAMPLE_PATH}는 예제 애플리케이션의 경로를 나타냅니다. (예: rbln-model-zoo/cpp/image_classification)

1
2
3
4
mkdir ${SAMPLE_PATH}/build
cd ${SAMPLE_PATH}/build
cmake ..
make

Note

앞서 언급했듯이, 예제 애플리케이션은 이미지 처리 작업을 위해 OpenCV API를 사용합니다. 이를 위해, CMake 빌드 시스템이 OpenCV를 소스에서 직접 가져와 설치합니다. 이 과정은 시스템 사양과 인터넷 연결 속도에 따라 5분 이상 소요될 수 있습니다.

실행 파일 실행 방법

위의 모든 단계를 완료했다면, cmake 디렉토리 아래에서 동기식과 비동기식 추론을 위한 image_classificationimage_classification_async라는 이름의 실행 가능한 바이너리들을 찾을 수 있습니다.

  • 동기 실행

    ${SAMPLE_PATH}/build/image_classification -i ${SAMPLE_PATH}/tabby.jpg  -m ${SAMPLE_PATH}/resnet50.rbln
    

  • 비동기 실행

    ${SAMPLE_PATH}/build/image_classification_async -i ${SAMPLE_PATH}/tabby.jpg  -m ${SAMPLE_PATH}/resnet50.rbln
    

출력 결과는 다음과 같습니다:

Predicted category: tabby