はじめに
前回の記事では、
ONNX Runtime (C++) で推論し、0.04msという圧倒的な速度を得た。
ただしC++は環境構築が手間で、コードも冗長になりがちだ。
今回は同じmodel.onnxを使い、C#のONNX Runtime APIで推論してみる。
NuGetで簡単にセットアップできる点も見どころだ。
全体の流れ
[PyTorch環境] [ONNX Runtime C#環境]
学習 → model.onnx保存 → 読み込み → 推論
※PyTorchもPythonも不要!
前回と同じmodel.onnxをそのまま使い回せる。
環境構築
C++と大きく違うのがここだ。
libtrochやONNX RuntimeのC++版は手動でダウンロード・配置が必要だったが、
C#ではNuGetパッケージを.csprojに1行書くだけで済む。
.devcontainer/devcontainer.json:
{
"name": "ONNX Runtime C#",
"image": "mcr.microsoft.com/devcontainers/dotnet:8.0",
"customizations": {
"vscode": {
"extensions": [
"ms-dotnettools.csharp"
]
}
}
}
inference.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- NuGetで一発インストール:C++のような手動セットアップ不要 -->
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.18.0" />
</ItemGroup>
</Project>
setup.shもCMakeLists.txtも不要。これだけで環境が整う。
推論コード
Program.cs:
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using System.Diagnostics;
// モデルの読み込み
using var session = new InferenceSession("model.onnx");
Console.WriteLine("モデルを読み込みました: model.onnx");
// ダミー入力を準備(MNISTと同じサイズ: 1x1x28x28)
var inputShape = new int[] { 1, 1, 28, 28 };
int inputSize = 1 * 1 * 28 * 28;
var random = new Random(42);
var inputData = new float[inputSize];
for (int i = 0; i < inputSize; i++)
{
// 正規分布の近似(Box-Muller法)
double u1 = 1.0 - random.NextDouble();
double u2 = 1.0 - random.NextDouble();
inputData[i] = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2));
}
var tensor = new DenseTensor<float>(inputData, inputShape);
var inputName = session.InputMetadata.Keys.First();
var inputs = new List<NamedOnnxValue>
{
NamedOnnxValue.CreateFromTensor(inputName, tensor)
};
// ウォームアップ(前回と条件を揃える)
session.Run(inputs);
Console.WriteLine("ウォームアップ完了");
// 推論時間を計測(100回平均)
const int N = 100;
var sw = Stopwatch.StartNew();
IDisposableReadOnlyCollection<DisposableNamedOnnxValue>? output = null;
for (int i = 0; i < N; i++)
{
output?.Dispose();
output = session.Run(inputs);
}
sw.Stop();
double elapsedMs = sw.Elapsed.TotalMilliseconds / N;
// 結果の表示
var outputTensor = output!.First().AsTensor<float>();
int predicted = Enumerable.Range(0, 10)
.OrderByDescending(i => outputTensor[0, i])
.First();
Console.WriteLine($"予測クラス: {predicted}");
Console.WriteLine($"推論時間({N}回平均): {elapsedMs:F4} ms");
output?.Dispose();
実行:
# ビルドと実行を一括でやってくれる
dotnet run
cmakeもmakeも不要なのがC#の楽なところだ。
実行結果・5者比較
実行環境:Ryzen 5 7530U(CPUのみ)、ウォームアップあり、100回平均
| 実装 | 推論時間(100回平均) | Pythonとの比較 |
|---|---|---|
| Python (PyTorch) | 0.20 ms | 基準 |
| C++ (libtorch) | 0.23 ms | 1.15倍遅い |
| ONNX Runtime (Python) | 0.13 ms | 1.54倍速い |
| ONNX Runtime (C#) | 0.08 ms | 2.5倍速い |
| ONNX Runtime (C++) | 0.04 ms | 5倍速い |
C#はPythonとC++のちょうど中間に収まった。
C#の立ち位置
速度だけで見るとC++には及ばないが、
C#には別の強みがある。
環境構築が圧倒的に楽
C++版はONNX Runtimeを手動でダウンロード・配置し、
CMakeLists.txtでパスを指定する必要があった。
C#はNuGetの1行だけで完結する。
コードが読みやすい
C++版と比べてコード量が少なく、意図が伝わりやすい。
dotnet run一発で動くのも開発効率が高い。
既存C#システムへの組み込みが容易
業務システムや.NETアプリにCNN推論を組み込む場合、
C#は最も自然な選択肢になる。
まとめ
5回の比較を振り返ると:
| 実装 | 速度 | 環境構築 | 主なユースケース |
|---|---|---|---|
| Python (PyTorch) | 0.20 ms | 簡単 | 学習・開発・検証 |
| C++ (libtorch) | 0.23 ms | やや手間 | Pythonが使えない環境 |
| ONNX Runtime (Python) | 0.13 ms | 簡単 | 軽量デプロイ |
| ONNX Runtime (C#) | 0.08 ms | 簡単 | .NETシステムへの組み込み |
| ONNX Runtime (C++) | 0.04 ms | 手間 | 速度重視の本番環境 |
速度・環境構築のしやすさ・ユースケース、
三つのバランスで選ぶのが正解だと思う。
おわりに・次回予告
次回は最終回として、5つの実装を改めて整理し、
「どのユースケースに何を使うべきか」をまとめていく予定。