PyTorch는 Python 프로그래밍 언어에서 사용하기 적합한 딥러닝 라이브러리입니다. Python은 동적성과 신속한 이터레이션이 필요한 상황에서 많이들 선호되고 있죠. 하지만 실시간 처리 등 짧은 지연시간 혹은 응답시간을 요구하는 업무에는 실행시간이 다소 긴 Python을 사용하기엔 한계가 있습니다.
이로 인해 상용 환경에서는 많은 사람들이 Python 대신 C++를 개발언어로 채택하고 있습니다. 저도 실제 임베디드 관련 업무를 수행할 때는, C++를 사용하고 있죠. C++는 실제로 iOS를 개발할 때, Swift와 TorchScript를 연동시키는 브릿지로써의 역할을 하기도 합니다.
그래서 이번 챕터에서는 C++ 환경에서 PyTorch 기반의 학습된 딥러닝 모델을 읽고 실행할 수 있는 연습을 진행해보도록 하겠습니다.
Chap01에서 설명했듯이 TorchScript는 Python이 아닌 다양한 언어 및 플랫폼 환경에서도 딥러닝 모델을 사용할 수 있는 PyTorch 모델의 한 표현방식입니다. C++에서 PyTorch 기반의 딥러닝 모델을 실행하기 위해선, 딥러닝 모델을 TorchScript 모델로 변환하는 것입니다.
이번 실습에서는 torchvision
에서 지원하는 이미지 분류 딥러닝 모델인 resnet18
을 TorchScript로 변환해보겠습니다.
앞서 설명한 것과 같이 트레이싱을 통한 TorchScript 변환은 딥러닝 모델의 인스턴스와 임의의 입력값을 torch.jit.trace
함수에 입력해야합니다. 그러면 torch.jit.trace
함수는 torch.jit.ScriptModule
객체를 생성하게 되죠. (실습 2.1)을 보겠습니다.
import torch
import torchvision
model = torchvision.models.resnet18() # --- (1)
sample_input = torch.rand(1, 3, 224, 224) # --- (2)
traced_script_module = torch.jit.trace(model, sample_input) # --- (3)
(1)
resnet18
의 모델 인스턴스인model
을 생성합니다.
(2)sample_input
는 트레이싱에 사용할 임의의 입력값입니다.
(3) 모델 인스턴스model
과 임의의 입력값sample_input
을torch.jit.trace
에 입력해torch.jit.ScriptModule
객체를 생성합니다.
모델을 트레이싱을 통해 ScriptModule
로 변환했다면, C++에서도 이 객체를 읽어올 수 있도록 모델을 저장하도록 하겠습니다. 모델을 TorchScript 형태로 저장하기 위해선 (실습 2.2)와 같이 .save
함수를 호출해야합니다.
traced_script_module.save("traced_script_model.pt") # --- (1)
(1) 을 실행하면, [Output] 과 같이 작업 디렉토리에
traced_script_model.pt
파일이 생성될 것입니다. 이로써 이제 Python의 세계에서 벗어나 C++ 환경에서 작업할 준비를 마쳤습니다.
[Output]
지금까지 torch.jit.trace
함수를 통해 resnet18
모델을 TorchScript의 객체인 ScriptModule
로 변환해 저장하였습니다. 이제부터 본격적으로 ScriptModule
을 C++에서 로드해보겠습니다.
C++에서 ScriptModule
을 로드하기 위해서는, 반드시 LibTorch
라는 PyTorch C++ API가 설치되어 있어야 합니다.
LibTorch 란?
LibTorch는 C++에서도 PyTorch의 기능을 사용할 수 있게 해주는 라이브러리입니다. 이를 통해 사용자는 PyTorch의 강력한 기능을 Python과 C++ 모두에서 활용할 수 있게 되며, 이는 특히 성능이 중요한 어플리케이션과 임베디드 시스템, 또는 Python이 사용되기 어려운 환경에서 PyTorch 모델을 배포하는 데 유용합니다.
LibTorch 사용 사례
LibTorch는 TorchScript 모델을 불러오고 실행하는 기능을 제공해, 고성능을 요구하는 C++ 기반 어플리케이션에서 PyTorch 모델을 최적화하고 더 빠른 인퍼런스와 효율적인 메모리 사용을 달성할 수 있게 해줍니다. 이러한 특성으로 인해 임베디드 시스템과 모바일 디바이스에서의 모델 배포, 서버 백엔드와 고성능 어플리케이션 등에 주로 사용되죠.
C++ 어플리케이션을 만들기 전, LibTorch
를 먼저 설치하도록 하겠습니다.
https://pytorch.org/get-started/locally/ 로 이동하여 아래와 같이 선택 후,
(저는 현재 Ubuntu 22.04
및 Cuda 12.1
버전을 사용 중이라 아래와 같이 선택했습니다. 환경에 맞게 적절한 선택을 해주세요)
다운로드하고자 하는 경로를 정해주세요.
(저는 /evo/libs
라는 경로에 만들어 추가해보도록 하겠습니다.)
경로로 이동하신 후 wget
명령어로 LibTorch
를 다운로드 해보겠습니다.
Run this Command에서 Download here의 url을 복사해 터미널에서 실행해주세요.
wget [Run This Command에 표기된 URL]
저는 아래와 같은 명령어로 LibTorch
를 설치해보겠습니다.
wget https://download.pytorch.org/libtorch/cu121/libtorch-cxx11-abi-shared-with-deps-2.2.2%2Bcu121.zip
[설치 완료 화면]
다운받은 zip파일의 압축을 풀도록 하겠습니다.
unzip [다운받은 zip 파일 이름]
[압축풀기 후 생성된 libtorch
]
이제 libTorch 설치도 완료했으니, 빌드를 위한 준비를 해보도록 하겠습니다.
현재 프로젝트의 작업 경로에서 다음과 같이 CMakeLists.txt
를 작성해야합니다.
CMakeList.txt ?
CMake를 사용하여 C++ 프로젝트를 구성하고 빌드하는 방법을 정의하는 텍스트 파일입니다.
# CMakeLists.txt
cmake_minimum_required(VERSION 3.0 FATAL_ERROR) # --- (1)
project(Chap02) # --- (2)
find_package(Torch REQUIRED) # --- (3)
add_executable(main main.cpp) # --- (4)
target_link_libraries(main "${TORCH_LIBRARIES}") # --- (5)
set_property(TARGET main PROPERTY CXX_STANDARD 14) # --- (6)
(1) CMake 최소 버전 요구 사항 설정입니다. CMake 버전이 3.0 버전 이상 필요함을 나타내며, 요구사항을 충족하지 못할 경우,
FATAL_ERROR
로 처리해 구성 과정을 중단하라는 의미입니다.
(2) 프로젝트 이름을 설정합니다. 여기서는 프로젝트 이름을 Chap02로 설정합니다.
(3)find_package
명령은 CMake에게 LibTorch를 찾도록 지시합니다. 'REQUIRED' 키워드는 LibTorch가 프로젝트에 필수적임을 나타내며, 이 패키지를 찾을 수 없으면 오류를 발생시킵니다.
(4)add_executable
명령은main.cpp
파일에서 정의된 소스 코드를 사용하여main
이라는 이름의 실행 파일을 생성하도록 지시합니다. 이는 빌드 대상이 됩니다.
(5)main
실행 파일을 빌드할 때, LibTorch 라이브러리를 링크하도록 합니다.${TORCH_LIBRARIES}
변수는find_package(Torch REQUIRED)
에 의해 설정되며, LibTorch 관련 라이브러리의 위치를 포함합니다.
(6)main
타겟에 대해 사용할 C++ 표준을 C++14로 설정합니다. LibTorch가 요구하는 최소 C++ 표준 버전을 충족하거나, 프로젝트의 특정 요구 사항에 맞추기 위한 설정입니다.
이제 C++ 환경에서 ScriptModule
을 읽고 실행하는 간단한 C++ 어플리케이션을 만들어보도록 하겠습니다.
먼저 libTorch
가 C++
에서 정상 로드되는지 확인해보도록 하겠습니다.
#include <torch/script.h>
#include <iostream>
#include <memory>
using namespace std;
int main(int argc, const char* argv[]){ // --- (1)
if (argc != 2) {
cerr << "usage: example-app <path-to-exported-script-module>\n"; // --- (2)
}
torch::jit::script::Module module; // --- (3)
try {
module = torch::jit::load(argv[1]); // --- (4)
} catch (const c10::Error& e){
cerr<<"error loading the module \n"; // --- (5)
return -1;
}
cout << "ok \n"; // --- (6)
}
(1) 프로그램의 진입점입니다.
argc
는 커맨드라인에서 입력된 인자의 수를,argv
는 인자의 값을 갖는 배열을 나타냅니다. 이 실습에서는 모델의 경로를 커맨드라인 인자로 받습니다.
(2) 프로그램이 올바른 인자(모델 파일 경로)와 함께 실행되었는지 확인합니다. 인자의 수(argc
)가 2가 아니라면(프로그램 이름 포함), 사용법을 안내하는 메시지를 출력하고 프로그램을 종료합니다.
(3) TorchScript 모듈을 로드하기 위한 객체를 선언합니다. 이 객체는 나중에 저장된 모델을 담게 됩니다.
(4) 저장된 TorchScript 모델을 불러오려고 시도합니다.argv[1]
은 커맨드라인에서 전달된 모델의 파일 경로입니다.
(5) 모델 로드 과정에서 오류가 발생하면, 오류 메시지를 출력하고 프로그램을-1
과 함께 종료합니다. 이는 실패를 나타냅니다.
(6) 모델이 성공적으로 로드되면, 콘솔에 "ok" 메시지를 출력합니다.
이제 어플리케이션을 빌드해보도록 하겠습니다. 현재 디렉토리 구조가 아래와 같다고 가정하겠습니다.
아래 명령어들을 사용해 Chap02/
폴더 안에서 어플리케이션을 빌드할 수 있습니다.
mkdir build # --- (1)
cd build # --- (2)
cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch .. # --- (3)
cmake --build . --config Release # --- (4)
(1) 현재 프로젝트 디렉토리 안에
build
디렉토리를 생성합니다. CMake를 사용할 때, 소스 디렉토리와는 별도로 빌드 디렉토리를 생성하여 빌드 파일들을 관리하는 것이 일반적입니다. 이를 통해 소스 디렉토리를 깔끔하게 유지할 수 있으며, 여러 빌드 구성(Debug, Release)을 쉽게 관리할 수 있게 해줍니다.
(2) 방금 생성된build
디렉토리로 작업 디렉토리를 변경합니다. 이제 모든 CMake 명령어와 빌드 과정은 이 디렉토리 내에서 실행됩니다.
(3) CMake 구성을 시작합니다.-DCMAKE_PREFIX_PATH=/path/to/libtorch
는 CMake에게 LibTorch가 설치된 경로를 알려줍니다. 이 경로는 CMake가 LibTorch를 찾고, 프로젝트 필드에 필요한 설정을 할 때 사용됩니다. 마지막에 있는..
는 CMakeLists.txt 파일이 상위 디렉토리에 위치함을 나타냅니다.
(/path/to/libtorch
는 LibTorch가 설치된 경로로 저는./evo/libs/libtorch
를 사용하겠습니다.)
(4)cmake --build .
명령어는 현재 디렉토리(.)에 구성된 빌드를 시작합니다.--config Release
옵션은 Release 모드로 프로젝트를 빌드하라는 것을 의미합니다. Release 모드는 최적화가 적용된 상태로 빌드되어, 개발 및 테스트가 아닌 실제 배포에 적합한 실행 파일을 생성합니다.
(3) 에서 정상적으로 CMake 구성이 될 경우,
(4) 에서 정상적으로 빌드될 경우,
이제 실행을 한번 시켜보도록 하겠습니다. 터미널에서 다음 명령어를 실행해주세요.
./main ../traced_script_model.pt
정상적으로 LibTorch
를 로딩한다면, 아래와 같이 ok
메시지가 출력됩니다.
[Output]
이제 Library가 정상적으로 include되는 것을 확인했으니, [2-2]에서 생성한 ScriptModule을 C++에서 로드해보겠습니다.
main.cpp 파일의
cout << "ok \n";
위에 아래 코드를 추가해주세요.
std::vector<torch::jit::IValue> inputs;
inputs.push_back(torch::ones({1, 3, 224, 224})); // --- (1)
at::Tensor output = module.forward(inputs).toTensor(); // --- (2)
std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/5) << '\n';
(1) 입력 텐서 생성
크기가{1, 3, 224, 224}
인 텐서를 생성하고 이를 입력 벡터에 추가합니다. 여기서torch::ones
함수는 모든 요소가 1인 텐서를 생성합니다. 이 텐서는 예를 들어, 이미지 분류 작업에서 사용될 수 있는 단일 이미지(3채널 RGB, 224x224 픽셀)를 나타낼 수 있습니다.
(2) 준비된 입력으로 모델의forward
함수를 호출하고, 결과를 텐서로 변환하여output
에 저장합니다. 이때module
은 이미 로드되어 있고 초기화된PyTorch
모델을 가리킵니다.
준비되었다면, 다시 한번 프로그램을 실행해볼까요?
다시 한번 C++ 애플리케이션을 빌드해주고,
cmake --build . --config Release
이제 다시 한번 빌드된 프로그램을 실행해주세요. 실행 방법은 이전과 동일합니다.
./main ../traced_script_model.pt
출력 결과를 보면 임의의 입력 텐서 inputs에 대한 결과물이 출력되는 것을 확인할 수 있습니다.
[Output]
이번 챕터에서는 TorchScript로 생성된 딥러닝 모델을 C++에 로드하는 실습을 진행해보았습니다. 처음에는 MacOS에서 진행을 했지만 보안 이슈로 결국에는 Ubuntu22.04에서 진행을 했네요ㅜ
다음 챕터에서는 드디어 생성된 TorchScript를 iOS에 올려보도록 하겠습니다!
다음 챕터에서 뵙겠습니다! ㅎ