コンテンツにスキップ

C++でのMNISTによる数字認識

このサンプルは、Model ZooのMNISTモデルを使用しています: https://github.com/onnx/models/tree/main/validated/vision/classification/mnist

スクリーンショット

コンパイル済みのOnnxruntime.dll / lib(dllのビルド方法へのリンク) Windows Visual Studioコンパイラ(cl.exe)

このディレクトリで ‘build.bat’ を実行してcl.exeを呼び出し、MNIST.exeを生成します。 その後、MNIST.exeを実行します。

左側のボックスで、左マウスボタン(またはタッチ)を使用して数字を描画します。マウスボタンを離すと、モデルが実行され、モデルの出力が表示されます。複数の描画ストロークを必要とする数字を描画する場合、各ストロークの終わりにモデルが実行され、おそらく誤った予測が表示されることに注意してください(しかし、それを見るのは面白く、「モデルを実行」ボタンを押す必要がなくなります)。

画像をクリアするには、どこでも右マウスボタンをクリックします。

ランタイムを初期化するために、単一のOrt::Envがグローバルに作成されます。

Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "test"};

[ソース]

MNIST構造体は、Onnx Runtimeとのすべての対話を抽象化し、テンソルを作成し、モデルを実行します。

WWinMainはWindowsのエントリポイントであり、メインウィンドウを作成します。

WndProcはウィンドウのウィンドウプロシージャであり、マウス入力を処理し、グラフィックを描画します。

MNISTの入力は{1,1,28,28}形状のfloatテンソルであり、基本的には28x28の浮動小数点グレースケール画像です(0.0 = 背景、1.0 = 前景)。

このサンプルでは、画像をピクセルあたり32ビットのWindows DIBセクションに格納します。これは、Windowsで描画したり画面に描画したりするのが簡単だからです。DIBはここで作成されます:

{
BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
bmi.bmiHeader.biWidth = MNIST::width_;
bmi.bmiHeader.biHeight = -MNIST::height_;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biCompression = BI_RGB;
void* bits;
dib_ = CreateDIBSection(nullptr, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
}

[ソース]

DIBデータを変換してモデルの入力テンソルに書き込む関数:

void ConvertDibToMnist() {
DIBInfo info{dib_};
const DWORD* input = reinterpret_cast<const DWORD*>(info.Bits());
float* output = mnist_.input_image_.data();
std::fill(mnist_.input_image_.begin(), mnist_.input_image_.end(), 0.f);
for (unsigned y = 0; y < MNIST::height_; y++) {
for (unsigned x = 0; x < MNIST::width_; x++) {
output[x] += input[x] == 0 ? 1.0f : 0.0f;
}
input = reinterpret_cast<const DWORD*>(reinterpret_cast<const BYTE*>(input) + info.Pitch());
}
output += MNIST::width_;
}

[ソース]

MNISTの出力は、各数字の尤度重みを保持する単純な{1,10} floatテンソルです。値が最も高い数字が、モデルの最良の推測です。

MNIST構造体は、これを行うためにstd::max_elementを使用し、それをresult_に格納します:

result_ = std::distance(results_.begin(), std::max_element(results_.begin(), results_.end()));

[ソース]

さらに面白くするために、ウィンドウペイントハンドラーは確率をグラフ化し、ここで重みを表示します:

// 勝者をハイライト
RECT rc{graphs_left, mnist_.result_ * 16, graphs_left + graph_width + 128, (mnist_.result_ + 1) * 16};
FillRect(hdc, &rc, brush_winner_);
// すべてのエントリについて、確率とグラフを描画します
SetBkMode(hdc, TRANSPARENT);
wchar_t value[80];
for (unsigned i = 0; i < 10; i++) {
int y = 16 * i;
float result = mnist_.results_[i];
auto length = wsprintf(value, L"%2d: %d.%02d", i, int(result), abs(int(result * 100) % 100));
TextOut(hdc, graphs_left + graph_width + 5, y, value, length);
Rectangle(hdc, graphs_zero, y + 1, graphs_zero + result * graph_width / range, y + 14);
}
// ゼロ線を描画
MoveToEx(hdc, graphs_zero, 0, nullptr);
LineTo(hdc, graphs_zero, 16 * 10);

[ソース]

  1. 作成:Ort::Sessionは、MNIST構造体内でここで作成されます:

    Ort::Session session_{env, ORT_TSTR("model.onnx"), Ort::SessionOptions{nullptr}};

    [ソース]

  2. 入力と出力の設定:入力と出力のテンソルはここで作成されます:

    MNIST() {
    auto allocator_info = Ort::AllocatorInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
    input_tensor_ = Ort::Value::CreateTensor<float>(allocator_info, input_image_.data(), input_image_.size(), input_shape_.data(), input_shape_.size());
    output_tensor_ = Ort::Value::CreateTensor<float>(allocator_info, results_.data(), results_.size(), output_shape_.data(), output_shape_.size());
    }

    [ソース]

    この使用法では、Ortにバッファを割り当てさせる代わりに、データのメモリ位置を提供しています。この場合、バッファは小さく、MNIST構造体の固定メンバーにすることができるため、この方が簡単です。

  3. 実行:セッションの実行はRun()メソッドで行われます:

    int Run() {
    const char* input_names[] = {"Input3"};
    const char* output_names[] = {"Plus214_Output_0"};
    session_.Run(Ort::RunOptions{nullptr}, input_names, &input_tensor_, 1, output_names, &output_tensor_, 1);
    result_ = std::distance(results_.begin(), std::max_element(results_.begin(), results_.end()));
    return result_;
    }

    [ソース]