HoloLensのInsider Preview版1809(17763)でWindows MLを使うTips

# Windows MLについて
Windows MLはONNXを呼ばれる学習済みモデルを利用し、ローカルマシン上で機械学習の推論が実行できるAPIです。
学習済みモデルから生成したONNXがあれば誰でもオフラインでの機械学習の恩恵を受けることができます。
このAPIはOSバージョン1803（春のアップデート）から導入されています。
このような状況の中HoloLensの日本語版でOSバージョン1809（ビルド番号17663)を使いながらWindows MLでONNX（Tiny Yolo V2）を取扱う実験を行いました。その結果現時点では色々と扱いが難しいところが多く、その情報をTipsとして共有します。
実際にTiny Yolo V2を導入したアプリの構築については別記事で紹介します。
HoloLensで実験しましたが、一般のWindows OS全般同じだと思います。

## Windows MLのOSバージョンによる違い
Windows MLですが、OSバージョン1803(以下1803)とOSバージョン1809(以下1809)では大きな違いあります。この結果ほぼ別物じゃないの？という状況が垣間見えます。違いを表にすると以下の通りです。

|項目|1803|1809|
|:-:|:--:|:--:|
|Windows MLの名前空間|Windows.AI.MachineLearning<font color="red">.Preview</font>|Windows.AI.MachineLearning|
|ONNXへの入出力型|.NET標準のクラス<br/>(floatやstring,List等)|.NET標準のクラス＋専用クラス<ul><li>ImageFeatureValue </li><li>TensorBoolean</li><li>TensorDouble</li><li>TensorFloat</li><li>TensorFloat16Bit</li><li>TensorInt16Bit</li><li>TensorInt32Bit</li><li>TensorInt64Bit</li><li>TensorInt8Bit</li><li>TensorString</li><li>TensorUInt16Bit</li><li>TensorUInt32Bit</li><li>TensorUInt64Bit</li><li>TensorUInt8Bit</li></ul>|
|ONNXのバージョン|ONNX 1.0～|ONNX 1.2～|

上記のように対応するONNXバージョンも変わりAPIのインターフェースを変更されているのでかなり変わっています。
ただ、基本的な実装方法は大きく変わっていません。

### 参考：ONNXのバージョン違いを見分ける方法
ONNXのバージョンを見分ける方法として[Netron](https://github.com/lutzroeder/Netron)というツールを紹介します。
このツールはONNXやcoreMLのモデル構造をGUIで確認できるツールです。このツールを使うと次のことがわかります。

* ONNXの入出力パラメータの型
* ONNXが使用するライブラリのバージョン

ONNXのバージョンは2つ目のライブラリバージョンから判別できます。
![image](https://qiita-image-store.s3.amazonaws.com/0/152548/d42ccf8e-0451-1d86-cf0c-7fd6955cdc11.png)
ONNX1.2～はライブラリとして「ai.onnx v7」を使っているので、Netronでモデルのパラメータを表示しimportsパラメータにこのバージョンのものが使われているかを見るとONNX1.2～かは判別できます。

## 1803でのWindows ML APIの開発
1803での開発はネットで検索すれば色々と情報が出てきますのでそれほど困らないかと思います。ただし、いくつかのはまりどころはあったのでそのTipsを記載します。
### Visual Studioのターゲットは1803にする。
Visual Studio2017のUWPプロジェクトのターゲットを1803に変更します。Windows MLは1803から導入されています。このためターゲットの設定が正しい事を確認してください。ターゲットに1803が表示されない場合、Windows SDKがインストールされていない可能性があります。この場合はWindows SDKをインストールしてください。

![image](https://qiita-image-store.s3.amazonaws.com/0/152548/20557e12-1534-955d-4021-8ea0029a4088.png)

### 出力パラメータが配列やコレクションの場合
配列やコレクションの場合は、自動生成されたラッパークラスに少し追加実装が必要な場合があります。
Windows ML APIはWindows SDK(17743)を入れておくとVisual Studioにonnxファイルをインポートするとラッパークラスが生成されます。
1803の場合、TinyYoloV2のONNXを利用すると実装部分が以下のようになっています。

```csharp:TinyYoloV1_0.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Media;
using Windows.Storage;
using Windows.AI.MachineLearning.Preview;

// TinyYoloV1_0
namespace App1
{
    public sealed class TinyYoloV1_0ModelInput
    {
        public VideoFrame image { get; set; }
    }

    public sealed class TinyYoloV1_0ModelOutput
    {
        public IList<float> grid { get; set; }
        public TinyYoloV1_0ModelOutput()
        {
            this.grid = new List<float>();
        }
    }

    public sealed class TinyYoloV1_0Model
    {
        private LearningModelPreview learningModel;
        public static async Task<TinyYoloV1_0Model> CreateTinyYoloV1_0Model(StorageFile file)
        {
            LearningModelPreview learningModel = await LearningModelPreview.LoadModelFromStorageFileAsync(file);
            TinyYoloV1_0Model model = new TinyYoloV1_0Model();
            model.learningModel = learningModel;
            return model;
        }
        public async Task<TinyYoloV1_0ModelOutput> EvaluateAsync(TinyYoloV1_0ModelInput input) {
            TinyYoloV1_0ModelOutput output = new TinyYoloV1_0ModelOutput();
            LearningModelBindingPreview binding = new LearningModelBindingPreview(learningModel);
            binding.Bind("image", input.image);
            binding.Bind("grid", output.grid);
            LearningModelEvaluationResultPreview evalResult = await learningModel.EvaluateAsync(binding, string.Empty);
            return output;
        }
    }
}
```

この場合、出力パラメータのgridプロパティが`IList<float>`になっているのですが、このまま処理を実行するとエラーが発生します。要素が0のためで、ONNXが必要とする要素数をあらかじめ確保しておかないといけません。
TinyYoloV2の場合ONNXからの出力パラメータは`float32[none,125,13,13]`となっています。
![image](https://qiita-image-store.s3.amazonaws.com/0/152548/55f0c85f-aede-a3bd-7d86-a1074c3cae79.png)

よって以下のように125 X 13 X 13 = 21,125の要素で初期化しておく必要があります。

```csharp
public sealed class TinyYoloV1_0ModelOutput
{
    public IList<float> grid { get; set; }
    public TinyYoloV1_0ModelOutput()
    {
        this.grid = new List<float>(new float[125*13*13]);
    }
}
```

## 1809でのWindows ML APIの開発
基本的な開発は1803と変わりません。ただし、一部ラッパークラスの実装が変わったりと色々変更さている点があります。

### Visual Studioのターゲットは1809にする。
Visual Studio2017のUWPプロジェクトのターゲットを1809に変更します。新しい方のWindows MLは1809から導入されています。このためターゲットの設定が正しい事を確認してください。ターゲットに1809が表示されない場合、Windows SDKがインストールされていない可能性があります。この場合はWindows SDKをインストールしてください。

![image](https://qiita-image-store.s3.amazonaws.com/0/152548/2cddb133-4c1c-adb7-11d4-2ecc09183a43.png)

### 入出力パラメータが変更
1809ではONNXの入出力パラメータが変更されています。例えば1803でも扱っていたTinyYoloV2を使ったラッパークラスは以下のように出力します。　

```csharp:TinyYoloV1_2.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Media;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.AI.MachineLearning;
namespace App1
{
    
    public sealed class TinyYoloV1_2Input
    {
        public TensorFloat image; // shape(-1,3,416,416)
    }
    
    public sealed class TinyYoloV1_2Output
    {
        public TensorFloat grid; // shape(-1,125,13,13)
    }
    
    public sealed class TinyYoloV1_2Model
    {
        private LearningModel model;
        private LearningModelSession session;
        private LearningModelBinding binding;
        public static async Task<TinyYoloV1_2Model> CreateFromStreamAsync(IRandomAccessStreamReference stream)
        {
            TinyYoloV1_2Model learningModel = new TinyYoloV1_2Model();
            learningModel.model = await LearningModel.LoadFromStreamAsync(stream);
            learningModel.session = new LearningModelSession(learningModel.model);
            learningModel.binding = new LearningModelBinding(learningModel.session);
            return learningModel;
        }
        public async Task<TinyYoloV1_2Output> EvaluateAsync(TinyYoloV1_2Input input)
        {
            binding.Bind("image", input.image);
            var result = await session.EvaluateAsync(binding, "0");
            var output = new TinyYoloV1_2Output();
            output.grid = result.Outputs["grid"] as TensorFloat;
            return output;
        }
    }
}
```

1803の時と比べるとかなりシンプルな形になってます。1803では入出力パラメータ両方のバインドが必要でした。1809になってからは入力パラメータだけバインドすればいいようになったため、初期化の事を意識する必要がなくなりました。
また、パラメータについてはTensorFloatとという型に置き換わっています。TensorFloatは主に以下のパラメータを持つクラスです。

* Shape
* データ

ShapeについてはTensorFlow等で利用されているShapeと同じものです。次元数を定義します。データが実は結構厄介です。TensorFloatの場合、データはfloatの1次元配列で格納されています。例えばTinyYoloV2についてはShapeが[None,125,13,13]となっています。float配列にはこの次元数に従ったデータが格納されています。よって自分でフェッチをすることになります。ただ、1803の時も`IList<float>`だったのでその点では大きく変わりません。

### 画像を扱う場合は実は・・・
それぞれのラッパークラスを見た時に入出力にかかわる部分は1803から変更されています。その中でも厄介なものが入力に画像を扱うモデルです。TinyYoloV2は入力にRGBの416x416の画像データを扱います。これらの入力にかかわるラッパークラスは以下の通りです。

```csharp:1803の入力用クラス
public sealed class TinyYoloV1_0ModelInput
{
    public VideoFrame image { get; set; }
}
```

```csharp:1809の入力用クラス
public sealed class TinyYoloV1_2Input
{
    public TensorFloat image; // shape(-1,3,416,416)
}
```
1803ではVideoFrameだったものが1809ではTensorFloatになっています。TinyYoloV2では入力画像は以下の通りに加工します。

1. RGB成分のみにする。
2. 値はfloatの0～1の範囲で正規化する。
3. データを3X416X416の並べ替える。

このため1809では読込んだ画像から上記の加工を行ったfloat配列をTensorFloatで投入する必要があります。
なのですが、どうも1809からはWindows ML APIは画像入力は`ImageFeatureValue`クラスを使用することが正しそうです。また、1803はVideoFrameなのですが1809の場合もVideoFrameは正しく動きます。
ただ、TensorFloatの形式に合わせてデータを入力すると、推論は実行されていても結果が正しくなかったです。
このことからWindows ML APIは画像を入力とする場合は`ImageFeatureValue`または`VideoFrame`を前提にした制御が入ってると考えられます。よって推論を正しく処理するためには1809の入力用クラスは画像を扱う場合だけ`ImageFeatureValue`を使うようにした方がうまくいくと思います。

### ONNXファイルの定義次第で変わるようです。
**現状試している中で分かっていることとしてCustom VisionのONNXを使うと入力パラメータの型は`ImageFeatureValue`になります。**パラメータをNetronで調べた時のものです。

![image](https://qiita-image-store.s3.amazonaws.com/0/152548/dddc6263-6623-57e2-ae10-3e3fd45b62b3.png)

denotationというパラメータが付与されているのがわかるかと思います。この設定で`Image(Rgb8)`とあるONNXファイルを使った場合は入力パラメータの型は`ImageFeatureValue`になります。

## Custom Vision を利用したAPI開発
Custom Visionはちょうど1803のリリースに合わせてONNX形式でのエクスポートをサポートするようになりました。これによりWindows MLでカスタムした画像分類等の機能を利用できるという非常に楽しいことができるようになっています。
Custom Visionは1809のリリースを考慮してONNX 1.0とONNX 1.2での出力が選べるようになっています。これによると1809相当の仕様変更が入っているのはビルド番号で17738以降となっているようです。
![image](https://qiita-image-store.s3.amazonaws.com/0/152548/e0c05baa-5655-a0d0-0ae0-8b7511834af8.png)


しかし、1809については実は上手く動作しません。この投稿書いてる時点では次の問題を抱えている事がわかっています。

* Custom Visionの出力パラメータの内lossパラメータは取得できない。

モデルによる推論は正常に動作しているため、classLabelプロパティは取得可能です。
lossパラメータはONNXでは`sequence<map<string,float>>`となっています。このラッパークラスは以下のようになります。sequence部分がILisクラスで出力されていることがわかるかと思います。

```csharp:1809のCustomVision用出力パラメータクラス
public sealed class CustomVisionModelOutput
{
    public TensorString classLabel; // shape(-1,1)
    public IList<Dictionary<string,float>> loss;
}
```

ちなみに1803の場合は以下の通りです。1803の場合は'Dictionary'クラスのみで生成します。

```csharp:1803のCustomVision用出力パラメータクラス
public sealed class CustomVisionModelOutput
{
    public IList<string> classLabel { get; set; }
    public IDictionary<string, float> loss { get; set; }
    public CustomVisionModelOutput()
    {
        this.classLabel = new List<string>();
        this.loss = new Dictionary<string, float>()
        {
            { "AngelPie", float.NaN },
            { "ChocoPie", float.NaN },
        };
    }
```

この違いの理由ですが、これはONNXのバージョンによる仕様変更が影響しています。Custom Visionの学習モデルではlossパラメータの最終段にzipmapオペレーターを使用します。このzipmapオペレータの出力仕様がONNX1.2から変更されています。その結果、Custom Visionのlossパラメータの型も変わっているのですがWindows MLでは現在この型をうまく処理できないためエラーにならないのですが値を取得できません。

この件に関係するかは不明ですが現在のWindows ML には既知のバグがあるようです。

## まとめ
今後リリースされる1809でWindows MLを扱う場合は以下に注意する。

* 1803で生成したラッパークラスは1809用に修正するか再度自動生成する
* 使用できるONNXは1.2以降
* 使用するONNXのパラメータが画像の場合、ラッパークラスの型をVideoFrameまたはImageFeatureValueに変更する。
* 推論がうまくいっているにも関わらず結果が返らない場合は入出力パラメータの型をチェックする
* どうしても無理な場合はバグかもしれないので様子を見る。