TensorFlow C API를 이용해 C/C++에서 TensorFlow 모델 사용하기

· by 박승재

TensorFlow for C를 이용하면 C와 C++에서 TensorFlow 모델을 불러와 사용할 수 있습니다.

TensorFlow는 C++로 작성되어 있기 때문에 TensorFlow Core API를 직접 불러와 사용할 수도 있지만, 예고 없이 API가 변경될 수 있기 때문에 TensorFlow C API 사용을 권장하고 있습니다.

Python

TensorFlow C++ 코드를 작성하기에 앞서, 코드를 테스트할 모델을 만듭니다.

from tensorflow.keras import Input, Sequential
from tensorflow.keras.layers import Dense

x = [[0, 0], [0, 1], [1, 0], [1, 1]]
y = [[0], [1], [1], [0]]

model = Sequential([
    Input(shape=(2,), name='input', dtype='int32'),
    Dense(16, activation='relu'),
    Dense(1, activation='sigmoid', name='output')
])
model.summary()

model.compile(optimizer='adam', loss='mse', metrics=['acc'])

history = model.fit(x, y, epochs=500, verbose=2)

model.save('data/saved_model')  # save as tf saved model

model.predict(x)  # test
Epoch 1/500
1/1 - 0s - loss: 0.2559 - acc: 0.7500
Epoch 2/500
1/1 - 0s - loss: 0.2555 - acc: 0.5000
Epoch 3/500
1/1 - 0s - loss: 0.2550 - acc: 0.5000
...
Epoch 498/500
1/1 - 0s - loss: 0.0823 - acc: 1.0000
Epoch 499/500
1/1 - 0s - loss: 0.0820 - acc: 1.0000
Epoch 500/500
1/1 - 0s - loss: 0.0817 - acc: 1.0000

array([[0.3111288 ],
       [0.72106665],
       [0.724661  ],
       [0.27483132]], dtype=float32)

Model

간단하게 XOR 연산에 대해 출력값을 예측하는 모델을 생성했습니다.

C++

이제 C++ 코드로 돌아가 C API 헤더를 추가합니다.

라이브러리(libtensorflow) 설치에 대해서는 TensorFlow for C를 참고하세요.

#include <tensorflow/c/c_api.h>

아래 코드를 통해 libtensorflow가 정상적으로 설치되고 링크되었는지 확인할 수 있습니다.

#include <iostream>

int main() {
    std::cout << "TensorFlow Version: " << TF_Version() << std::endl;
    return 0;
}

TensorFlow에서 모델은 TF_LoadSessionFromSavedModel 함수로 불러올 수 있습니다. 먼저 TensorFlow의 Session을 불러오는 함수의 인자에 필요한 값을 정의하고 초기화합니다.

auto *run_options = TF_NewBufferFromString("", 0);
auto *session_options = TF_NewSessionOptions();
auto *graph = TF_NewGraph();
auto *status = TF_NewStatus();
std::array<char const *, 1> tags{ "serve" };

이때 "serve"는 사용할 모델의 태그 이름으로, SavedModel CLI를 이용해 찾아올 수 있습니다. 태그의 기본값"serve"로, 위 코드와 같이 모델을 저장했다면 태그 이름으로 "serve"를 넣어 모델을 불러올 수 있을 것입니다.

SavedModel CLI 항목에 SavedModel의 정보를 가져오는 방법이 자세히 나와 있습니다.

$ saved_model_cli show --dir data/saved_model/ --tag_set serve --signature_def serving_default
...
The given SavedModel SignatureDef contains the following input(s):
  inputs['input'] tensor_info:
      dtype: DT_INT32
      shape: (-1, 2)
      name: serving_default_input:0
The given SavedModel SignatureDef contains the following output(s):
  outputs['output'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, 1)
      name: StatefulPartitionedCall:0
Method name is: tensorflow/serving/predict
auto* session = TF_LoadSessionFromSavedModel(session_options, run_options,
                                             "data/saved_model", tags.data(), tags.size(),
                                             graph, nullptr, status);
if (TF_GetCode(status) != TF_OK) {
    std::cout << TF_Message(status) << '\n';
}

Tensorflow의 SavedModel을 data/saved_model 디렉토리에 넣어줘야 합니다.

모델이 정상적으로 불러와 져 session에 정상적으로 값이 할당되면, TF_GetCode(status)TF_OK가 됩니다.

이제 모델의 graph에서 Tensorflow의 Operation(연산)을 찾아야 합니다. Operation 이름도 SavedModel CLI를 통해 찾을 수 있습니다. 위 파이썬 코드로 생성한 모델의 입력과 출력 Operation의 기본값은 각각 "serving_default_input", "StatefulPartitionedCall"입니다.

auto *input_op = TF_GraphOperationByName(graph, "serving_default_input");
if (input_op == nullptr) {
    std::cout << "Failed to find graph operation\n";
}

auto *output_op = TF_GraphOperationByName(graph, "StatefulPartitionedCall");
if (output_op == nullptr) {
    std::cout << "Failed to find graph operation\n";
}

Tensorflow에서는 입력과 출력 Operation이 여러 개일 수도 있으므로 이를 배열 형태로 받습니다. 따라서 우리도 저장할 값이 1개뿐이더라도, 사용하기 편리하게 이를 배열 형태로 저장해둡시다.

std::array<TF_Output, 1> input_ops = { TF_Output{ input_op, 0 } };
std::array<TF_Output, 1> output_ops = { TF_Output{ output_op, 0 } };

TF_Output 오타 아닙니다. TF_SessionRun 함수의 인자를 확인해보세요.

이제 학습된 모델에 값을 넣어 출력을 예측해봅시다.

std::array<int, 2> x{ 1, 0 };
std::vector<std::array<int, 2>> inputs{ x };

std::array<int64_t, 2> const dims{ static_cast<int64_t>(inputs.size()), static_cast<int64_t>(x.size()) };
void *data = (void *) inputs.data();
std::size_t const ndata = inputs.size() * x.size() * TF_DataTypeSize(TF_INT32);

auto const deallocator = [](void *, std::size_t, void *) {}; // unused deallocator because of RAII

auto *input_tensor = TF_NewTensor(TF_INT32, dims.data(), dims.size(), data, ndata, deallocator, nullptr);
std::array<TF_Tensor *, 1> input_values{ input_tensor };

std::array<TF_Tensor *, 1> output_values{};

[](void *, size_t, void *) {}는 Deallocator로 저희는 C++의 RAII에 의존해 메모리를 관리하기 때문에 빈 함수로 정의했습니다.

만약 입력이 실수(float)인 경우에는 TF_INT32TF_FLOAT로 교체합니다.

std::array<int> x는 모델에 들어갈 실제 입력값 입니다. 이것을 std::vector로 감싸는 이유는, 주어진 모델이 여러 개의 입력을 한 번(Batch)에 계산할 수 있기 때문입니다.

input_tensorstd::array로 감싸서 넣게 되는데, 이는 앞서 input_ops가 입력 Operation이 여러 개일 수도 있기 때문에 std::array로 감싸 넣은 이유와 같습니다.

입력 값을 Tensor로 변환하면 이를 모델의 입력 Operation에 넣어 출력값을 계산합니다.

TF_SessionRun(session,
              run_options,
              input_ops.data(), input_values.data(), input_ops.size(),
              output_ops.data(), output_values.data(), output_ops.size(),
              nullptr, 0,
              nullptr,
              status);
if (TF_GetCode(status) != TF_OK) {
    std::cout << TF_Message(status) << '\n';
}

TF_SessionRun는 위에서 만들어둔 input_opsinput_values을 넣어, output_ops까지 계산된 결과를 output_values에 저장합니다.

이제 output_values를 Tensor로 변환해 결과값을 출력해봅시다.

auto *output_tensor = static_cast<std::array<float, 1> *>(TF_TensorData(output_values[0]));
std::vector<std::array<float, 1>> outputs{ output_tensor, output_tensor + inputs.size() };

std::cout << "output: " << outputs[0][0] << '\n'; // output: 0.907109

결과값을 다 썼으면, 사용한 메모리를 회수합니다.

TF_DeleteTensor(input_values[0]);
TF_DeleteTensor(output_values[0]);

모델 사용이 끝나면 마찬가지로 사용한 메모리를 회수하고 리소스를 정리합니다.

TF_DeleteBuffer(run_options);
TF_DeleteSessionOptions(session_options);
TF_DeleteSession(session, status);
TF_DeleteGraph(graph);
TF_DeleteStatus(status);

input_ops[0].opergraph에 저장된 Operaion의 포인터이기 때문에 별도로 delete할 필요는 없이, TF_DeleteGraph를 통해 같이 해제됩니다.

참고 1: Deploying Tensorflow as C/C++ executable

참고 2: Example TensorFlow C API