Cara Inference Machine Learning Menggunakan ONNX C++
ONNX Runtime adalah framework open-source untuk menjalankan model cross-platform. Jadi biasanya untuk deploy sudah tidak pakai format seperti .h5 dari Tensorflow atau .pt dari pytorch karena semuanya sudah di-standarisasi menggunakan onnx.
Instalasi
Sebelumnya pastikan sudah install onnxruntime dan OpenCV untuk C++. disarankan kalian develop di Docker biar lebih aman.
Install Onnxruntime
untuk install onnxruntime untuk C++ kalian bisa download di https://github.com/microsoft/onnxruntime/releases. Setelah itu bisa kalian extract ke directory /usr kalian.
tar -zxvf onnxruntime-linux-x64-gpu-1.19.2.tgz -C /opt
cp -r /opt/onnxruntime-linux-x64-gpu-1.19.2/include /usr/local
cp -r /opt/onnxruntime-linux-x64-gpu-1.19.2/lib /usr/local
Install Opencv C++
Untuk install OpenCV clone dari repo OpenCV lalu build menggunakan CMake.
git clone https://github.com/opencv/opencv.git /opt/opencv
git clone https://github.com/opencv/opencv_contrib.git /opt/opencv_contrib
cd /opt/opencv && \
mkdir build && \
cd build && \
cmake -D OPENCV_EXTRA_MODULES_PATH=/opt/opencv_contrib/modules \
.. && \
make -j$(nproc) && \
make install
Pastikan kalian juga sudah install compiler untuk C++ dan build tools (CMake) nya juga. Jadi untuk Dockerfile nya akan jadi seperti ini:
FROM ubuntu:20.04
# compiler, cmake, git
RUN apt update -y \
&& apt upgrade -y && \
apt install g++ -y && \ # compiler C++
apt install cmake -y && \ # build tools untuk C++
apt install git -y # optional
# Onnxruntime
copy onnxruntime-linux-x64-gpu-1.19.2.tgz .
RUN tar -zxvf onnxruntime-linux-x64-gpu-1.19.2.tgz -C /opt
RUN cp -r /opt/onnxruntime-linux-x64-gpu-1.19.2/include /usr/local && \
cp -r /opt/onnxruntime-linux-x64-gpu-1.19.2/lib /usr/local
# Opencv
RUN git clone https://github.com/opencv/opencv.git /opt/opencv
RUN git clone https://github.com/opencv/opencv_contrib.git /opt/opencv_contrib
RUN cd /opt/opencv && \
mkdir build && \
cd build && \
cmake -D OPENCV_EXTRA_MODULES_PATH=/opt/opencv_contrib/modules .. && \
make -j$(nproc) && \
make install
onnxruntime C++ Flow
secara general flow di atas adalah step by step inferensi onnx di C++. Dimulai dari load model, buat tensor untuk input/output, prediksi (inference) dan postprocessing. Jauh lebih kompleks daripada versi python karena kalian harus buat buffer untuk input dan outputnya sebelum inference.
Model yang dipakai adalah mobilenet dengan 1000 class dari onnx zoo
Code
include header dan library yang diperlukan seperti openCV, onnxruntime, dan dependencies lain.
#include <onnxruntime_cxx_api.h>
#include <iostream>
#include <memory>
#include <opencv2/opencv.hpp>
#include <vector>
#include <numeric>
#include <cmath>
#include <fstream>
Lalu tulis konfigurasi di CMakeLists.txt untuk manage library. Isitilahnya adalah ’linking library’.
cmake_minimum_required(VERSION 3.12)
project(onnxruntime_cpp)
set(ONNXRUNTIME_ROOTDIR "/usr/local") # tempat instalasi onnxruntime
include_directories("${ONNXRUNTIME_ROOTDIR}/include"
"${ONNXRUNTIME_ROOTDIR}/include/onnxruntime"
"${ONNXRUNTIME_ROOTDIR}/include/onnxruntime/core/session"
)
link_directories("${ONNXRUNTIME_ROOTDIR}/lib")
find_package(OpenCV)
include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(onnxruntime_cpp main.cpp)
target_link_libraries(onnxruntime_cpp onnxruntime)
target_link_libraries(onnxruntime_cpp ${OpenCV_LIBS})
Untuk sekarang kita akan tulis semua code nya dalam satu file main cpp. Kalau mau dibuat lebih modular dan OOP style seperti memisahkan masing-masing function dalam file cpp tersendiri, maka file CMakeLists.txt akan lebih kompleks.
Jadi untuk struktur projek akan seperti ini:
Load ONNX Model
Untuk load model setidaknya ada 3 parameter
- Ort::env = untuk mencatat log, kalau mau lebih lengkap kalian bisa ganti dengaan ORT_LOGGING_LEVEL_INFO
- Ort::SessionOptions = buat konfigurasi bagaimana model dijalankan, seperti penggunaan device e.g. CUDA, TensorRT, OpenVINO, dll.
- Ort::Session = untuk load model, diisi dengan parameter di atas seperti Env, sessionOptions, dan pastinya path model onnx kalian.
Ort::Env mEnv(ORT_LOGGING_LEVEL_WARNING);
Ort::SessionOptions sessionOptions;
std::string modelPath = "/onnx/mobilenetv2-12.onnx";
Ort::Session mSession(mEnv, modelPath.c_str(), sessionOptions);
Get Input Size & Input Name
Input size nanti akan dipakai untuk membuat buffer untuk menyimpan vector gambar yang akan diinferensi. Jadi kalau kita print variabel inputSize hasilnya adalah ukuran dari input nya. Contoh di bawah adalah [1, 3, 244, 244] yang artinya [batch_size, channel, height, width]. Model Onnx yang dipakai berati dari pytorch, kalau dari tensorflow formatnya jadi [1, 244, 244, 3]
// Input Size
size_t numInputNodes = mSession.GetInputCount();
Ort::TypeInfo inputTypeInfo = mSession.GetInputTypeInfo(0);
auto inputTensorInfo = inputTypeInfo.GetTensorTypeAndShapeInfo();
std::vector<int64_t> inputSize = inputTensorInfo.GetShape();
// for (int64_t value : inputSize) {
// std::cout << value << " ";
// }
// output = 1 3 224 224
Setelah dapat nama input/outputnya di netron kalian bisa langsung masukan namanya seperti di bawah, jadinya code nya bisa lebih ringkas :)
std::vector<const char*> inputNames = { "input" }
tapi kalau mau dapet inputNames by code, kalian juga bisa tulis seperti ini:
// Input Name
Ort::AllocatorWithDefaultOptions allocator;
Ort::AllocatedStringPtr inputName = mSession.GetInputNameAllocated(0, allocator);
std::vector<const char*> inputNames = { inputName.get() };
Get Output Size & Output Name
Sama seperti input size & name, fungsinya nanti buat bikin buffer untuk nyimpen hasil prediksi. Karena model yang dipakai adalah model klasifikasi, jadi size nya adalah [1, 1000] atau [batch_size, jumlah_class]
// Output Size
size_t numOutputNodes = mSession.GetOutputCount();
Ort::TypeInfo outputTypeInfo = mSession.GetOutputTypeInfo(0);
Ort::ConstTensorTypeAndShapeInfo outputTensorInfo = outputTypeInfo.GetTensorTypeAndShapeInfo();
std::vector<int64_t> outputSize = outputTensorInfo.GetShape();
// for (int64_t value : outputSize) {
// std::cout << value << " ";
// }
// output = 1 1000
// Output Name
Ort::AllocatedStringPtr outputName = mSession.GetOutputNameAllocated(0, allocator);
std::vector<const char*> outputNames = { outputName.get() };
// std::vector<const char*> inputNames = { "output" };
Image Processing
Untuk image processing sepertinya lebih enak pakai OpenCV. Read image nya, convert ke RGB (default openCV adalah BGR), normalisasi, dan resize. Hasilnya nanti disimpen ke variabel preprocessedImage
cv::Mat imageArr = cv::imread("/workspaces/ONNX-cpp/sample2.jpg");
if (imageArr.empty()) {
std::cerr << "Failed to load image" << std::endl;
return -1;
}
cv::Mat RgbImage;
cv::cvtColor(imageArr, RgbImage, cv::COLOR_BGR2RGB);
cv::Mat preprocessedImage;
cv::dnn::blobFromImage(RgbImage, preprocessedImage, 1 / 255.0, cv::Size(224, 224), cv::Scalar(0, 0, 0), false, false);
Input Tensor
Hasil gambar yang sudah diproses lalu dimasukan ke dalam vector dengan tipe data tensor dari onnxruntime. Informasi seperti inputSize yang sudah disebutkan di atas akan dibutuhkan ketika membuat inputTensors. inputTensors ini yang akan jadi input untuk inferensi nanti.
std::vector<float> inputTensorValues(preprocessedImage.begin<float>(), preprocessedImage.end<float>());
Ort::MemoryInfo memoryInfo = Ort::MemoryInfo::CreateCpu(OrtAllocatorType::OrtArenaAllocator, OrtMemType::OrtMemTypeDefault);
std::vector<Ort::Value> inputTensors;
inputTensors.push_back(
Ort::Value::CreateTensor<float>(
memoryInfo,
inputTensorValues.data(),
inputTensorSize,
inputSize.data(),
inputSize.size()
)
);
Output Tensor
Sama seperti inputTensors, outputTensors juga akan dijadikan input saat inferensi dijalankan untuk menyimpan hasil prediksi. Ukuran ouputTensors berati adalah [1, 1000] yang nilainya nol semua.
// Create output tensor buffer
size_t outputTensorSize = outputSize;
std::vector<float> outputTensorValues(outputTensorSize);
// Create output tensors of ORT::Value
std::vector<Ort::Value> outputTensors;
outputTensors.push_back(
Ort::Value::CreateTensor<float>(
memoryInfo,
outputTensorValues.data(),
outputTensorSize,
mOutputDims.data(),
mOutputDims.size()
)
);
Run Inference
Setelah semuanya ready baru bisa kita jalankan inferensi nya.
mSession.Run(
Ort::RunOptions{nullptr},
inputNames.data(),
inputTensors.data(),
1,
outputNames.data(),
outputTensors.data(),
1
);
Maksud angka 1 di situ adalah jumlah input dan outputnya. Karena model klasifikasi kita hanya punya input dan output masing-masing satu maka diisi satu. Kalau misal kita punya multi-head model yang punya output 3, maka bisa ditulis:
mSession.Run(
Ort::RunOptions{nullptr},
inputNames.data(),
inputTensors.data(),
1,
outputNames.data(),
outputTensors.data(),
3
);
Postprocessing
Karena modelnya adalah multiclass classification, jadi perlu implemen softmax. Untuk function softmax bisa kalian buat di luar main( )
std::vector<float> softmax(const std::vector<float>& logits) {
std::vector<float> expVals(logits.size());
float maxLogit = *std::max_element(logits.begin(), logits.end());
// Compute exponentials of logits
float sumExp = 0.0f;
for (size_t i = 0; i < logits.size(); ++i) {
expVals[i] = std::exp(logits[i] - maxLogit); // Numerical stability improvement
sumExp += expVals[i];
}
// Normalize to get probabilities
for (size_t i = 0; i < expVals.size(); ++i) {
expVals[i] /= sumExp;
}
return expVals;
}
Setelah itu kalian ambil probability terbesar, argmax, lalu print hasilnya.
std::vector<std::string> labels = {"label1", "label2", "etc..."};
std::vector<float> probabilities = softmax(outputTensorValues);
// Find the class with the highest probability
auto maxIt = std::max_element(probabilities.begin(), probabilities.end());
int predictedClass = std::distance(probabilities.begin(), maxIt);
float maxProb = *maxIt;
// Output the results
std::cout << "Predicted class: " << labels[predictedClass] << std::endl;
std::cout << "Probability: " << maxProb << std::endl;
Build
masih di direktori yang sama dengan main.cpp dan CMakeLists.txt; ketik di terminal
mkdir build
cd build
cmake ../
make
Setelah itu kalian akan dapat executable file dengan nama ‘onnxruntime_cpp’. Nama executable file bisa diatur di CMakeLists.txt. Setelah itu kalian bisa run executable filenya
chmod +x onnxruntime_cpp
./onnxruntime_cpp
lalu outputnya sebagai berikut:
Ketika kalian buat perubahan pada codenya, kalian perlu untuk compile ulang menggunakan ‘make’. Kecuali kalau kalian modif lagi file CMakeLists.txt nya berati harus ulang lagi dari awal.
repo: haidhiSkuy/easy-cpp-onnx



