はじめに
前回の記事では、
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に依存しない分、より軽量なデプロイが期待できる。
果たして速度はどう変わるか。