iPX社員によるブログ

iPX社員が"社の動向"から"自身の知見や趣味"、"セミナーなどのおすすめ情報"に至るまで幅広い話題を投下していくブログ。社の雰囲気を感じ取っていただけたら幸いです。

Windows 向けの機械学習アプリケーションを構築する

こんにちわ、 iPX のコクブンです。
機械学習環境を構築する際、学習時は(ワークステーションやゲーミング PC 等の)潤沢な環境で実施出来ますが、推論処理を実行する際には、実際に利用されるエンドユーザ様のご要望により Windows PC での実行が求められることがあります。
もちろん、 Windows 環境でも機械学習の実行環境を構築することは可能*1ですが、お客様に提供するアプリケーションとして考えた場合、よりネイティブな形式での実装が求められることも少なくありません。
そこで、今回は Windows 環境に於けるネイティブなアプリケーション実行形式として WPF アプリケーションを想定し、機械学習の推論処理を行う WPF アプリケーションを構築してみたいと思います。
なお、ここでご紹介した WPF アプリケーションのサンプルソースコードは GitHub 上にもあります
※本コードは自由にご利用いただいて構いませんが、本コードに起因するすべての損害について弊社では一切責任を負いません。

ML.NET の導入

WPF 等の .NET アプリケーションで機械学習を行う方法はいくつもありますが、今回は Microsoft が提供している ML.NET を利用します。
ML.NET は NuGet パッケージとして公開されているため、 Visual StudioCLI よりアプリケーションプロジェクトに容易に追加することが出来ます。

Visual Studio でのプロジェクトへの追加方法

Visual Studio を利用可能な場合は、 NuGet パッケージマネージャを利用してパッケージの参照を追加することが出来ます。
Visual Studio (2019) で .NET アプリケーションプロジェクトを開いた上で、メニューの「ツール」→「 NuGet パッケージマネージャー」→「ソリューションの NuGet パッケージの管理」を選択します。

ソリューションの NuGet パッケージ管理ウィンドウが表示されたら、「参照」タブの検索ボックスに "Microsoft.ML" と入力し、リストに表示された「 Microsoft.ML 」パッケージをプロジェクトに追加します。

.NET Core CLI でのプロジェクトへの追加方法

.NET Core CLI でプロジェクトに NuGet パッケージの参照を追加する際は、 add package コマンドを使用します。
プロジェクトフォルダ上で、

> dotnet add package Microsoft.ML

を実行することで、 ML.NET パッケージが追加されます。

ML.NET を利用した WPF アプリケーションの構築

それでは、実際に簡単な機械学習アプリケーションを構築してみましょう。
※なお、当アプリケーションでは Microsoft.ML 以外に以下の NuGet パッケージも使用しています。

  • Microsoft.ML.ImageAnalytics (画像変換機能)
  • Microsoft.ML.OnnxRuntime ( ONNX サポート)
  • Microsoft.ML.OnnxTransformer ( ONNX モデル読込機能)
機械学習モデルの準備

ML.NET では 1 から学習処理を実装することも可能ですが、別の環境で学習した学習済モデル (ONNX) を使用して推論処理のみを実装することも可能です。
今回は、 ONNX プロジェクトから提供されている MobileNet の学習済モデルを使用して、画像の分類を行うアプリケーションを作成します。
上記ページよりダウンロードした ONNX ファイルをプロジェクトに追加します。

機械学習用データクラスの作成

ML.NET を利用する際、機械学習の入力となるデータを格納するクラスと出力データを格納するクラスを用意する必要があります。
今回は、入力として画像のファイルパス、出力としては各分類ごとの確率値を想定するため、それぞれの情報を格納するクラスを作成します。
また、最終的な推論結果となる分類結果のラベル(とその確率)を格納するクラスも併せて作成しておきます。

  • 入力データクラス (ImageData.cs)
public class ImageData
{
  [Microsoft.ML.Data.LoadColumn(0)]
  public string ImagePath { get; set; }
}

入力パラメータとするプロパティには Microsoft.ML.Data.LoadColumn 属性を指定しておきます。

  • 出力データクラス (MobileNetPrediction.cs)
public class MobileNetPrediction
{
  [Microsoft.ML.Data.ColumnName("mobilnetv20_output_flatten0_reshape0")]
  public float[] Output { get; set; }
}

出力パラメータを格納するプロパティには Microsoft.ML.Data.ColumnName 属性を指定します。
属性に指定する名前は、対象となる機械学習モデルの層名を指定します。

  • 推論結果クラス (ImagePrediction.cs)
public class ImagePrediction
{
  public string Label { get; set; }

  public float Estimate { get; set; }
}
ML.NET コンテキストの初期化

ML.NET のすべての処理は Microsoft.ML.MLContext クラスのインスタンスを通じて管理されます。
ML.NET を利用する際は、最初に MLContext インスタンスを生成しておきます。

var context = new MLContext();

※サンプルアプリケーションは一画面構成のアプリケーションのため MainViewModel のコンストラクタでインスタンス生成を行っていますが、一般的な複数画面構成のアプリケーションでは、 App.xaml.cs 等でインスタンスを生成し、アプリケーション全体として 1 つのインスタンスで管理することが望ましいと思われます。

パイプラインの構築

MobileNet を使用した機械学習を ML.NET に適用するため、 ML.NET のパイプラインを構築します。
今回のパイプラインは以下の流れになっています。

  1. 画像ファイルを読み込む (LoadImages)
  2. 読み込んだ画像ファイルを MobileNet の入力サイズに合わせてリサイズする (ResizeImages)
  3. リサイズした画像ファイルを MobileNet の処理サイズに合わせてクリップする (ResizeImages)
  4. クリップした画像ファイルを色要素毎に平準化する (ExtractPixels)
  5. 色要素毎に平準化した結果を結合する (Concatenate)
  6. MobileNet ONNX モデルを適用する (ApplyOnnxModel)
  7. MobileNet の出力を最終的な推論結果に変換する (CustomMapping)
var pipeline =
    context.Transforms.LoadImages(
        outputColumnName: "image",
        imageFolder: string.Empty,
        inputColumnName: nameof(ImageData.ImagePath))
  .Append(
    context.Transforms.ResizeImages(
        outputColumnName: "resizedimage",
        imageWidth: 256,
        imageHeight: 256,
        resizing: Microsoft.ML.Transforms.Images.ImageResizingEstimator.ResizingKind.Fill,
        inputColumnName: "image"))
  .Append(
    context.Transforms.ResizeImages(
        outputColumnName: "croppedimage",
        imageWidth: 224,
        imageHeight: 224,
        resizing: Microsoft.ML.Transforms.Images.ImageResizingEstimator.ResizingKind.IsoCrop,
        cropAnchor: Microsoft.ML.Transforms.Images.ImageResizingEstimator.Anchor.Center,
        inputColumnName: "resizedimage"))
  .Append(
    context.Transforms.ExtractPixels(
        outputColumnName: "red",
        colorsToExtract: Microsoft.ML.Transforms.Images.ImagePixelExtractingEstimator.ColorBits.Red,
        offsetImage: 0.485f * 255,
        scaleImage: 1 / (0.229f * 255),
        inputColumnName: "croppedimage"))
  .Append(
    context.Transforms.ExtractPixels(
        outputColumnName: "green",
        colorsToExtract: Microsoft.ML.Transforms.Images.ImagePixelExtractingEstimator.ColorBits.Green,
        offsetImage: 0.456f * 255,
        scaleImage: 1 / (0.224f * 255),
        inputColumnName: "croppedimage"))
  .Append(
    context.Transforms.ExtractPixels(
        outputColumnName: "blue",
        colorsToExtract: Microsoft.ML.Transforms.Images.ImagePixelExtractingEstimator.ColorBits.Blue,
        offsetImage: 0.406f * 255,
        scaleImage: 1 / (0.225f * 255),
        inputColumnName: "croppedimage"))
  .Append(
    context.Transforms.Concatenate(
        outputColumnName: "data",
        "red", "green", "blue"))
  .Append(
    context.Transforms.ApplyOnnxModel(
        modelFile: "mobilenetv2-1.0.onnx",
        outputColumnNames: new[] { "mobilnetv20_output_flatten0_reshape0" },
        inputColumnNames: new[] { "data" }))
  .Append(
    context.Transforms.CustomMapping<MobileNetPrediction, ImagePrediction>(
        (networkResult, prediction) =>
        {
          prediction.Estimate = networkResult.Output.Max();
          var index = networkResult.Output.ToList().IndexOf(prediction.Estimate);
          prediction.Label = labels[index];
        }, "Prediction"));

※ CustomMapping の中で使用している labels 変数(リスト)には、予め MobileNet の分類ラベルを文字列リストで格納してください。

推論用モデルの構築

作成したパイプラインの Fit メソッドを呼び出すことで学習済モデルの構築を行うことが出来ます。
通常、学習を実施する際は Fit メソッドに学習用データを渡しますが、推論のみで利用する場合は Fit メソッドにダミーの(空の)データを渡します。

var model = pipeline.Fit(context.Data.LoadFromEnumerable(new List<ImageData>()));
推論の実行

実際の推論処理は、先ほど作成した推論用モデルの Transform メソッドで行います。
Transform メソッドの結果は IDataView 型で返ってくるため、実際の推論結果値を取得する際は IDataView.GetColumn メソッドを使用して取得します。

var data = context.Data.LoadFromEnumerable(new[] { new ImageData { ImagePath = "20200214171302.jpg" } });
var predict = model.Transform(data);
var label = predict.GetColumn<string>(nameof(ImagePrediction.Label)).First();
var estimate = predict.GetColumn<float>(nameof(ImagePrediction.Estimate)).First();

なお、今回最終的に完成した WPF アプリケーションの実行結果は以下の様な感じになりました。

*1:機械学習ライブラリである TensorFlowPyTorch 等(弊社でも多く利用しています)は Windows 環境向けにも提供されています。