2
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?

正式GAされたWindows MLを利用してローカルでONNXを動かす

Posted at

Windows ML が正式GA: 解説とGetting Started

2025年9月、Microsoftは Windows ML (Windows Machine Learning) の一般提供 (GA: General Availability) を正式に発表しました。
長期プレビューを経て正式版となったことで、Windows 11 上でのプロダクション利用が可能となり、開発者は安心してアプリケーションに統合できるようになりました。

本記事では、Windows ML についての話と実際に .NET 8 + VS Code で簡単なコードを実装し実際にonnxを使ってローカルで動かしてみます。

Windows ML のこれまでの経緯

Windows ML は初めて登場したのは2018年頃で、Windows 10 SDK の一部としてプレビュー提供されていました。
主に開発者向けの実験的な位置づけでしたが、最近正式GAされて懐かしいと感じる人も少なからずいると思います(私もその1人ですが)
プレビュー版でリリースされた当時、Microsoft HololensでこのWindows MLが利用可能でした。
HoloLensにはRGBカメラが搭載されており、この映像を機械学習で分析する際にWindows ML経由でオフライン推論を実装していました。
過去のQiita記事も幾つか書いています(ほんとに懐かしい)

そんなWindows MLが最近はれてGAされました。基本的なコンセプトは変わっていないのですが大きく変更されている点としては実行プロバイダー(EP)という概念(後述)が導入されているところです。

Windows ML とは

Windows ML は、ONNX Runtime を基盤とした推論ランタイムで、Windows アプリケーションから簡単に機械学習モデルを利用できる環境を提供します。
特徴として、利用可能なハードウェア (CPU / GPU / NPU) を自動的に検出し、最適な実行プロバイダー (EP) を選択して推論を実行します。

図: Windows ML のアーキテクチャ概要

Windows ML のシステム要件

Windows MLの名の通り、Windows向けの機能になっています。

  • OS: Windows 11 バージョン 24H2 (ビルド 26100) 以降
  • アーキテクチャ: x64 または ARM64
  • ハードウェア: 任意の PC 構成 (CPU、統合/個別 GPU、NPU)

実行プロバイダー(EP)

**実行プロバイダー(EP)**は、機械学習 (ML) 操作のハードウェア固有の最適化を可能にするコンポーネントです。 実行プロバイダーは、さまざまな物理的な計算リソース (CPU、GPU、特殊化されたアクセラレータ) を抽象化し、グラフのパーティション分割、カーネル登録、および演算子の実行のための統合インターフェイスを提供します。

Windows MLに含まれている実行プロバイダは以下の2種類があります。

  • CPU
  • DirectML : 推論を高速化するためにDirectMLを利用するコンポーネント。汎用GPUを利用してベンダー固有の拡張機能を使わずに実行を可能にします(要DirectX 12)

別途初期化することで利用可能になる実行プロバイダーは以下の通りです。

  • NvTensorRtRtxExecutionProvider : Nvidia製GPU
  • OpenVINOExecutionProvider : Intel製CPU,GPU,NPU
  • QNNExecutionProvider : Qualcomm製NPU(Snapdragon(R) X系)
  • VitisAIExecutionProvider : AMD系NPU

より詳細な要件については公式ドキュメント - 利用可能な実行プロバイダーに記載があります。

例えば、私の開発環境はゲーミングPCでCPU組み込みのGPUとRTX3080が使えるのですが、以下のプロバイダが利用可能です。
CPU,GPUの種類の違いとDirectX12経由で使うDML、ハードウェア専用の実行プロバイダが選択できます。
必要に応じて実行プロバイダは初期化の上使用するハードウェアを指定します。

  • EpName: CPUExecutionProvider, Type: CPU, Vendor : Intel
  • EpName: DmlExecutionProvider, Type: GPU, Vendor : Intel Corporation(Intel(R) Iris(R) Xe Graphics用)
  • EpName: DmlExecutionProvider, Type: GPU, Vendor : NVIDIA(NVIDIA GeForce RTX 3080 Laptop GPU用)
  • EpName: OpenVINOExecutionProvider, Type: GPU, Vendor : Intel Corporation(Intel(R) Iris(R) Xe Graphics用)
  • EpName: OpenVINOExecutionProvider, Type: CPU, Vendor : Intel(Intel(R) CPU用)
  • EpName: NvTensorRTRTXExecutionProvider, Type: GPU, Vendor : NVIDIA(NVIDIA GeForce RTX 3080 Laptop GPU用)

関連技術: Foundry Local

Microsoft が提供するローカル推論基盤として Foundry Local があります。
両者とも ONNX を基盤としていますが用途が少し異なります。Windows ML は Windows アプリケーション向けに提供されています。ローカル推論を自身のアプリケーションに組込むことを想定した機能として提供されています。
一方、Foundry Localはその名にも含まれているAzure AI Foundryに関連のあるモデル基盤として機能します。Azure AI Foundryがもつカタログ情報から任意のonnxランタイムをローカルで実行します。Foundry Localサービスとして起動し、OpenAI API準拠のインターフェースを持ちます。このため、従来クラウドを利用した生成AIをFoundry Localに切替えることでローカルで実行可能に手軽に変更できます。

Windows ML、Foundry Localともに生成AIで処理する情報を保護することができる点がもっとも特徴的だと言えます。

Getting Started - 簡単に試してみる

実際にWindows MLを利用する為の準備や実装について紹介します。実際に試す場合は何らかのonnx形式のモデルを用意する必要があります。処理時間を考慮しなければGPU,NPU非搭載のPCでも検証可能です。以下のサンプルは実行プロバイダを初期化していないのでCPU優先で実行されます。

開発環境

  • OS: Windows 11 (22H2以降推奨)
  • エディタ: Visual Studio Code + C# Dev Kit
  • ランタイム: .NET 8
  • NuGet: Microsoft.ML.OnnxRuntime
  • モデル: ONNX形式 (ONNX Model Zooなどから入手可能)

セットアップ

# .NET 8 SDK をインストール
winget install Microsoft.DotNet.SDK.8

# プロジェクト作成
dotnet new console -n WindowsMLSample
cd WindowsMLSample

# SDK を追加
dotnet add package Microsoft.WindowsAppSDK

実装例(デバイス自動制御)

以下のサンプルは処理の流れを示したものです。
実装はシンプルで、用意したOnnxを登録し、入力データを準備しセッションを開始するだけになります。推論が完了後結果を処理します。

using System;
using System.Collections.Generic;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using Microsoft.Windows.AI.MachineLearning;

class Program
{
    static void Main()
    {
                
        //実行プロバイダの初期化。現在提供されているカタログを取得し、実行プロバイダのダウンロードや登録を実施
        //このプロジェクトの最初の実行時のみ必要
        var catalog = ExecutionProviderCatalog.GetDefault();
        await catalog.EnsureAndRegisterCertifiedAsync();

        //使用するonnxモデルのロード
        using var session = new InferenceSession("model.onnx");

        var inputTensor = new DenseTensor<float>(new[] {1, 3, 224, 224});
        var inputs = new List<NamedOnnxValue>
        {
            NamedOnnxValue.CreateFromTensor("input", inputTensor)
        };

        // 推論の実施
        using var results = session.Run(inputs);

        foreach (var result in results)
        {
            Console.WriteLine($"{result.Name}: {string.Join(", ", result.AsEnumerable<float>())}");
        }
    }
}

実行プロバイダーを指定する

Windows MLでは実行するハードウェアの構成によってどの演算装置で推論実行するかは自動判別可能ですが、開発者側で調整することも可能です。
SessionOptions を利用すると、開発者が利用する EP の優先度を制御できます。かなり柔軟な設定が可能で、GPU優先、CPU優先、使いたいハードウェア順で指定可能です。

最初に実行プロバイダの初期化を行います。この処理には実行プロバイダのダウンロードが含まれるため初回に実行が必要です。初期化を行うと利用できる実行プロバイダがダウンロード&登録されます。ネットワーク環境などによっては時間がかかる場合があります。

実行プロバイダをダウンロードしカタログに登録
var catalog = ExecutionProviderCatalog.GetDefault();
await catalog.EnsureAndRegisterCertifiedAsync();

オフライン環境等ダウンロードができない場合は既存(ダウンロード済み/標準で搭載しているプロバイダ)の実行プロバイダのみで構成することも可能です。

実行プロバイダをダウンロードせずにカタログに登録
var catalog = ExecutionProviderCatalog.GetDefault();
await catalog.RegisterCertifiedAsync();

場合によっては、オンラインの時はダウンロード含めて実施したいといった話があるかもしれません。この場合はダウンロードせずにすべての実行プロバイダのリストを取得することが可能です。このリストから利用できない実行プロバイダを検索することで、ユーザとの対話によってダウンロードの実施を選択するといった実装も可能です。
以下は、未ダウンロードのものは、ダウンロードして登録する例になります。

未ダウンロードのみダウンロードしてカタログに登録

var catalog = ExecutionProviderCatalog.GetDefault();

var providers = catalog.FindAllProviders();

if (providers is null || providers.Length == 0) {
    throw new InvalidOperationException("No execution providers found in catalog.");
}

foreach (var provider in providers) {
    Console.WriteLine($"Provider: {provider.Name}");
    try {
        var readyState = provider.ReadyState;
        Console.WriteLine($"  Ready state: {readyState}");

        // Only call EnsureReadyAsync if we allow downloads or if the provider is already ready
        if (readyState != ExecutionProviderReadyState.NotPresent)
        {
            await provider.EnsureReadyAsync();
        }

        provider.TryRegister();
    }
    catch (Exception ex) {
        throw new InvalidOperationException($"Failed to initialize provider {provider.Name}", ex);
    }
}

どういった実行プロバイダやプロセッサが対象になってるか見たい場合は以下の実装で確認可能です。

デバッグ用の一覧表示例
EnvironmentCreationOptions envOptions = new() {
    logId = "Demo",
    logLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR
};

OrtEnv ortEnv = OrtEnv.CreateInstanceWithOptions(ref envOptions);

foreach (var device in ortEnv.GetEpDevices()) {
    
    Console.WriteLine("-------------------------------");
    Console.WriteLine($"EpName: {device.EpName}\nVendor : {device.HardwareDevice.Vendor}\nType: {device.HardwareDevice.Type}");
    Console.WriteLine($" DeviceInfo:");
                device.HardwareDevice.Metadata.Entries.Keys.ToList().ForEach(e => Console.WriteLine($"  {e}: {device.HardwareDevice.Metadata.Entries[e]}"));
}

実際に使う場合はSessionOptionsに実行したいプロバイダに対応したプロセッサ順でプロバイダを登録し、セッション開始時に渡します。以下はその例です。

GPU → CPU の順で優先
// GPU → CPU の順で優先
var sessionOptions = new SessionOptions();
options.AppendExecutionProvider_DML();
options.AppendExecutionProvider_CPU();

using var session = new InferenceSession(modelPath, sessionOptions);
...

実行プロバイダをプロセッサ順で追加する場合は以下のようにortEnv.GetEpDevices()で一覧の中から取得します。

この際、追加する実行プロバイダを混ぜることはできません。例えば、DirectMLとベンダ向けの実行プロバイダを組合わせると実行時に以下のような例外発生します。
Unhandled exception. Microsoft.ML.OnnxRuntime.OnnxRuntimeException: [ErrorCode:InvalidArgument] All OrtEpDevice values in ep_devices must have the same execution provider.

NVIDIAの実行プロバイダを利用
// NVIDIAのGPUを利用
EnvironmentCreationOptions envOptions = new() {
    logId = "Demo",
    logLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR
};
var sessionOptions = new SessionOptions();
OrtEnv ortEnv = OrtEnv.CreateInstanceWithOptions(ref envOptions);

List<OrtEpDevice> list = new List<OrtEpDevice>();
foreach (var device in ortEnv.GetEpDevices()) {
    if (device.EpVendor == "NVIDIA") {
        list.Add(device);
    }
}
var epOptions = new Dictionary<string,string>();
sessionOptions.AppendExecutionProvider(OrtEnv,sessionOptions.list.AsReadOnly<OrtEpDevice>(),epOptions);
using var session = new InferenceSession(modelPath, sessionOptions);
...

実践サンプル:画像分類モデル(SqueezeNet / MobileNet)

折角なので、何か実際にonnxで推論してみましょう。
Windows ML を活用すると、軽量な画像分類モデル(例:SqueezeNet、MobileNet)を簡単に実行できます。
ここでは、1枚の画像を読み込んで分類し、上位5件の結果を表示する実戦的な例を紹介します。

開発環境

  • OS: Windows 11
  • エディタ: Visual Studio Code
  • ランタイム: .NET 8
  • NuGet: Microsoft.ML.OnnxRuntime
  • モデル例: SqueezeNet ONNX Model

ファイル構成

/assets
 ├─ model.onnx        # 例: SqueezeNet モデル
 ├─ image.jpg         # 分類したい画像
 └─ labels.txt        # クラス名(任意)[synset.txt](https://github.com/onnx/models/blob/main/validated/vision/classification/synset.txt)の中身をコピー

セットアップ

最初に空のプロジェクトを作成します。今回はLTSの.NET 8を使用しました。

# .NET 8 SDK をインストール
winget install Microsoft.DotNet.SDK.8

# プロジェクト作成
dotnet new console -n WindowsMLSample1
cd WindowsMLSample1

# SDK を追加
dotnet add package Microsoft.WindowsAppSDK
dotnet add package System.Numerics.Tensors

セットアップが完了したら、最初にWindowsMLSample.csprojを開いてPropertyGroupタグ内にいくつか追加を行います。

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0-windows10.0.22621.0</TargetFramework> <!-- 変更 -->
    <ImplicitUsings>enable</ImplicitUsings> <!-- 追加 -->
    <WindowsPackageType>None</WindowsPackageType>
    <Nullable>enable</Nullable>
  </PropertyGroup>

コード(Top-5分類表示)

using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Graphics.Imaging;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using Microsoft.Windows.AI.MachineLearning;


class Program
{
    const int W = 224, H = 224;
    const int TOPK = 5;

    static void Main()
    {
        InitializeExecutionProvider().Wait();

        var sessionOptions = new SessionOptions();

        // GPUのみ
        // DMLが先かIPが先かはこのロジックでは制御していない
        EnvironmentCreationOptions envOptions = new() {
            logId = "Demo",
            logLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR
        };
        OrtEnv ortEnv = OrtEnv.Instance();

        List<OrtEpDevice> list = new List<OrtEpDevice>();
        foreach (var device in ortEnv.GetEpDevices()) {
            if (device.EpVendor == "NVIDIA") {
                list.Add(device);
            }
        }
        var epOptions = new Dictionary<string,string>();
        sessionOptions.AppendExecutionProvider(ortEnv,list.AsReadOnly(),epOptions);


        string modelPath = Path.Combine("assets", "model.onnx");
        string imgPath = Path.Combine("assets", "image.jpg");
        string labelsPath = Path.Combine("assets", "labels.txt");

        var input = ConvertToTensorAsync(imgPath, W, H).Result;


        using var session = new InferenceSession(modelPath, sessionOptions);
        string inName = session.InputMetadata.Keys.First();
        string outName = session.OutputMetadata.Keys.First();

        using var results = session.Run(new[] { NamedOnnxValue.CreateFromTensor(inName, input) });
        var output = results.First(v => v.Name == outName).AsEnumerable<float>().ToArray();

        var probs = Softmax(output);
        var topk = probs.Select((p, i) => (i, p))
                        .OrderByDescending(t => t.p)
                        .Take(TOPK);

        string[] labels = File.Exists(labelsPath) ? File.ReadAllLines(labelsPath) : Array.Empty<string>();
        foreach (var (i, p) in topk)
        {
            string name = (labels.Length > i) ? labels[i] : $"class_{i}";
            Console.WriteLine($"{name}: {p:0.0000}");
        }
    }

    public static async Task<DenseTensor<float>> ConvertToTensorAsync(string imgPath, int newWidth, int newHeight)
    {

        if (!File.Exists(imgPath))
            throw new FileNotFoundException("assets/image.jpg を用意してください。");
        using var fs = File.OpenRead(imgPath);
        using var ras = fs.AsRandomAccessStream();

        var decoder = await BitmapDecoder.CreateAsync(ras);

        var transform = new BitmapTransform
        {
            ScaledWidth = (uint)newWidth,
            ScaledHeight = (uint)newHeight,
            InterpolationMode = BitmapInterpolationMode.Fant
        };

        var pixelProvider = await decoder.GetPixelDataAsync(
            BitmapPixelFormat.Bgra8,
            BitmapAlphaMode.Ignore,
            transform,
            ExifOrientationMode.IgnoreExifOrientation,
            ColorManagementMode.DoNotColorManage);

        var pixels = pixelProvider.DetachPixelData();

        // Convert to tensor (NCHW, normalized to [0,1])
        var input = new DenseTensor<float>(new[] { 1, 3, H, W });
        for (int y = 0; y < H; y++)
            for (int x = 0; x < W; x++)
            {
                int idx = (y * W + x) * 4;
                input[0, 0, y, x] = pixels[idx + 2] / 255f; // R
                input[0, 1, y, x] = pixels[idx + 1] / 255f; // G
                input[0, 2, y, x] = pixels[idx + 0] / 255f; // B
            }

        return input;
    }

    public static async Task InitializeExecutionProvider()
    {
        Console.WriteLine("Getting available providers...");
        var catalog = ExecutionProviderCatalog.GetDefault();
        var providers = catalog.FindAllProviders();
        if (providers is null || providers.Length == 0)
        {
            throw new InvalidOperationException("No execution providers found in catalog.");
        }
        foreach (var provider in providers)
        {
            Console.WriteLine($"Provider: {provider.Name}");
            try
            {
                var readyState = provider.ReadyState;
                Console.WriteLine($"  Ready state: {readyState}");
                if (readyState != ExecutionProviderReadyState.NotPresent)
                {
                    await provider.EnsureReadyAsync();
                }
                provider.TryRegister();
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException($"Failed to initialize provider {provider.Name}", ex);
            }
        }
    }

    static float[] Softmax(float[] v)
    {
        float max = v.Max();
        double sum = 0;
        var e = new double[v.Length];
        for (int i = 0; i < v.Length; i++) { e[i] = Math.Exp(v[i] - max); sum += e[i]; }
        var r = new float[v.Length];
        for (int i = 0; i < v.Length; i++) r[i] = (float)(e[i] / sum);
        return r;
    }
}

実行

上記の構成で実装が終わったら、何か画像(猫や車など)を用意して実行すると以下のように分類結果が返ってきます。今回は猫の写真を使ったので猫の分類結果が返ってくるのが分かります。

PS D:\hoge\WindowsMLSample> dotnet run
Getting available providers...
Provider: OpenVINOExecutionProvider
  Ready state: NotReady
Provider: NvTensorRTRTXExecutionProvider
  Ready state: NotReady
n02124075 Egyptian cat: 0.7301
n02123045 tabby, tabby cat: 0.1834
n02123159 tiger cat: 0.0584
n03958227 plastic bag: 0.0108
n02123394 Persian cat: 0.0056

まとめ

今回は GAされたWindows ML について紹介しました。Windows 11 上で正式に利用可能な推論ランタイムでONNX Runtime + 実行プロバイダー (NPU / GPU / CPU) による自動選択の機能を独自のアプリに組み込んで利用することができるようになります。実行プロバイダーの調整は不要ですが必要に応じて手動でも設定できます。
「特定業務で機械学習の推論や分析をオフラインで行うアプリを提供する」場合に特に有効な機能だともいます。一方でアプリケーションに組込んで利用する形式にはなるためモデルを頻繁に入れ替えるような場合等、要件によっては注意が必要です。

参考資料

2
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
2
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?