0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで学習したCNNモデルをC++ (libtorch) で推論してみた

0
Posted at

はじめに

前回の記事では、
30年前にC言語でバックプロパゲーション法を実装した経験を振り返りながら、
PyTorchでMNISTを学習してみた。

今回はその続きとして、Pythonで学習したモデルをC++ (libtorch) で推論してみる。

「C++で推論すれば速くなるはず」という期待とともに始めたのだが、
結果は少し意外なものになった。


なぜC++で推論するのか

Pythonで学習・推論まで完結できるなら、なぜC++を使うのか。
主な理由は以下の通り。

  • Pythonが使えない環境:組み込みシステム・産業用機器など
  • デプロイの簡潔さ:Pythonランタイム不要で単体バイナリとして動く
  • 既存C++システムへの組み込み:30年前からあるような業務システムなど

学習はPythonで行い、推論だけC++で動かすというのが現実的なパターンだ。


全体の流れ

[Python]                    [C++]
学習 → モデル保存(model.pt) → 読み込み → 推論

Pythonで学習したモデルをTorchScript形式でエクスポートし、
C++のlibtrochで読み込んで推論する。


環境構築

DevContainerを使ってC++環境を用意する。

.devcontainer/devcontainer.json:

{
  "name": "C++ libtorch CPU",
  "image": "mcr.microsoft.com/devcontainers/cpp:ubuntu-24.04",
  "postCreateCommand": "bash .devcontainer/setup.sh",
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-vscode.cpptools",
        "ms-vscode.cmake-tools"
      ]
    }
  }
}

.devcontainer/setup.sh:

#!/bin/bash
# libtroch CPU版をダウンロード・展開する

set -e

LIBTORCH_URL="https://download.pytorch.org/libtorch/cpu/libtorch-cxx11-abi-shared-with-deps-2.3.0%2Bcpu.zip"
DEST="$HOME/libtorch"

if [ ! -d "$DEST" ]; then
    echo "libtrochをダウンロード中..."
    wget -q "$LIBTORCH_URL" -O /tmp/libtorch.zip
    unzip -q /tmp/libtorch.zip -d "$HOME/"
    rm /tmp/libtorch.zip
    echo "libtrochのセットアップ完了: $DEST"
else
    echo "libtrochは既にセットアップ済みです: $DEST"
fi

モデルのエクスポート(Python側)

C++から読み込むために、TorchScript形式でモデルを保存する。

import torch
import torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# モデル定義(前回と同じ)
class SimpleCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(1, 16, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(16, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
        )
        self.fc = nn.Sequential(
            nn.Flatten(),
            nn.Linear(32 * 7 * 7, 128),
            nn.ReLU(),
            nn.Linear(128, 10)
        )

    def forward(self, x):
        x = self.conv(x)
        x = self.fc(x)
        return x

# 学習(前回と同じ)
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])
train_dataset = datasets.MNIST(
    root='./data', train=True, download=True, transform=transform
)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)

model = SimpleCNN()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

for epoch in range(5):
    total_loss = 0
    for images, labels in train_loader:
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    print(f"Epoch {epoch+1}/5: Loss = {total_loss / len(train_loader):.4f}")

# TorchScript形式でエクスポート
model.eval()
dummy_input = torch.randn(1, 1, 28, 28)
traced_model = torch.jit.trace(model, dummy_input)
traced_model.save("model.pt")

# Python推論用にも保存
torch.save(model.state_dict(), "model.pth")
print("エクスポート完了: model.pt / model.pth")

C++ (libtorch) で推論

CMakeLists.txt:

cmake_minimum_required(VERSION 3.18)
project(cnn_inference)

set(CMAKE_CXX_STANDARD 17)

find_package(Torch REQUIRED)

add_executable(inference inference.cpp)
target_link_libraries(inference ${TORCH_LIBRARIES})

inference.cpp:

#include <torch/script.h>
#include <torch/torch.h>
#include <iostream>
#include <chrono>

int main() {
    // モデルの読み込み
    torch::jit::script::Module model;
    try {
        model = torch::jit::load("model.pt");
        model.eval();
        std::cout << "モデルを読み込みました: model.pt" << std::endl;
    } catch (const c10::Error& e) {
        std::cerr << "モデルの読み込みに失敗しました: " << e.what() << std::endl;
        return -1;
    }

    // ダミー入力を準備(MNISTと同じサイズ)
    torch::Tensor input = torch::randn({1, 1, 28, 28});
    std::vector<torch::jit::IValue> inputs;
    inputs.push_back(input);

    // ウォームアップ(初回は遅いので除外)
    model.forward(inputs);
    std::cout << "ウォームアップ完了" << std::endl;

    // 推論時間を計測(100回平均)
    const int N = 100;
    auto start = std::chrono::high_resolution_clock::now();

    torch::Tensor output;
    for (int i = 0; i < N; i++) {
        output = model.forward(inputs).toTensor();
    }

    auto end = std::chrono::high_resolution_clock::now();
    double elapsed_ms =
        std::chrono::duration<double, std::milli>(end - start).count() / N;

    int predicted = output.argmax(1).item<int>();
    std::cout << "予測クラス: " << predicted << std::endl;
    std::cout << "推論時間(" << N << "回平均): " << elapsed_ms << " ms" << std::endl;

    return 0;
}

ビルドと実行:

mkdir build && cd build
cmake .. -DCMAKE_PREFIX_PATH=$HOME/libtorch
make
./inference

実行結果・速度比較

実行環境:Ryzen 5 7530U(CPUのみ)、ウォームアップあり、100回平均

実装 推論時間(100回平均)
Python (PyTorch) 0.20 ms
C++ (libtorch) 0.23 ms

ほぼ同じ、むしろPythonの方がわずかに速い。


なぜPythonと同じ速度なのか

「C++で書けば速くなるはず」という期待は裏切られた。
しかしこれには明確な理由がある。

PyTorchの演算コア(テンソル計算・行列積など)はすでにC++で実装されている。
Pythonはその薄いラッパーに過ぎないため、
推論の実行速度においてPythonのオーバーヘッドはほぼゼロだ。

むしろTorchScriptのJIT処理が微妙なオーバーヘッドになっている可能性すらある。


ではなぜC++で推論するのか

速度のためではなく、動作環境のためだ。

  • Pythonランタイムが使えない組み込み環境
  • 既存のC++システムへの統合
  • 単体バイナリとしてデプロイしたい場合

こういったユースケースでは、C++での推論実装が現実的な選択肢になる。


おわりに・次回予告

今回はC++ (libtorch) での推論を試した。
速度面ではPythonと互角だったが、動作環境の柔軟性という点でC++に優位性がある。

次回はONNX Runtimeを使った推論を試す予定。
libtrochに依存しない分、より軽量なデプロイが期待できる。
果たして速度はどう変わるか。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?