MenohとC#とONNXファイルでの推論

#1. 概要
PFNよりIntel-MKLベースのONNXモデル推論ライブラリが提供されています。
本体はC言語で実装されており、それに各種言語のラッパークラスが幅広く用意されています。

WindowsMLと機能が被りますが、WindowsMLはバグ抱えているし、
最近登場してきた(C#好きな私的には)大本命のMicrosoft製[ONNX Runtime](https://github.com/Microsoft/onnxruntime)もバージョンが0.1.5(2018/12/13時点)で当分は正式リリースされなさそうだしということで、Menohを活用して「実用的な推論器の実装」を勉強してみました。

#2. 目標
C# + .NetFramework4.5.1でVGG19による推論を実施するところを目標にします。
まんまexampleのコードなので困難なポイントはそこまでないと思います。
ただ、モデルの準備の話やコードの解説があまり見かけないので、その辺も含めて解説していきます。
あと私だけかもしれませんが、本家Githubのサンプルが何故か走らなかったので、同じ状況に陥った人たちへのフォローにもなれば幸いです。

#3. 手順
##3-1. プロジェクトの作成とMenohSharpの入手
フォームアプリも良いですが手軽さ重視で、今回はコンソールアプリとして作成していきます。
作成したらまず初めにプロジェクトのプロパティを開いて、
ビルド＞プラットフォームターゲットを「x64」にします。32bitでは走りません。
あと、アンセーフコードの実行を許可しておきます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/248663/d81d2bb5-c694-735e-6029-5d6694757a28.png)



次にnugetで[MenohSharp](https://github.com/pfnet-research/menoh-sharp)を入手します。
（プロジェクトをダウンロードしてビルド＆関連ライブラリを自分で集めるでも可能です）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/248663/4c0f81f2-df28-f9ea-8c30-8f47beb3ed79.png)

##3-2. モデルの準備
今回は[Model Zoo](https://github.com/onnx/models)からVGG19を持ってきます。
他のモデルも使えるとは思いますが、Menohが対応していない演算が組み込まれたモデルは走らないので注意です。
もちろん、自分で作ったモデルでもOKです。
最近はSonyのNeualNetworkConsoleがONNX出力に対応したので誰でもコーディング無しで簡単に作れてしまいます。

##3-3. モデルの仕様を確認する
少し前までONNXモデルの中を確認するのが面倒でしたが、現在は[Netron](https://github.com/lutzroeder/netron)という便利なツールがあるのでそれを利用します。

まずReleaseページからWindows用のバイナリを入手してインストールして、onnxファイルをNetronで開きます。
（関連付けされていればダブルクリック、そうでない場合はドラッグ＆ドロップで起動）

以下のような表示が出ると思います。ここでモデルのプロパティを確認します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/248663/b6667836-7c05-d038-6cc8-1eb193505d82.png)

ここで必要な情報は入出力の情報です。
したがって、
>Inputs
>id: data_0
>type: float32[1,3,224,224]

>Outputs
>id: prob_1
>type: float32[1,1000]

この部分の情報を控えておきます。
MenohSharpではポインタを扱うことになるので、**型もしっかり押さえておきます**。

##3-3. コーディング
###1) 画像を読み込んで縮小する

この部分は特別なものは無いので本家の[サンプルコード](https://github.com/pfnet-research/menoh-sharp/blob/master/MenohSharpExample/Program.cs)を見てもらうのが良いです。
サンプルコードでは以下の3つのことを実施しています。

 - ファイルから読み込み
 - 短辺長さで切り抜きからのリサイズ
 - 画像のテンソルを変換[1,224,224,3]=>[1,3,224,224]してさらに1次元配列化

###2) モデルをロード、画像を渡して推論する

こんな感じのコードで推論できます。
所々で先ほどNetronにて確認した情報を入力していきます。

```C#:Programs.cs
//ONNXモデルデータのロード
var model_data = MenohSharp.ModelData.MakeModelDataFromONNX("model.onnx");

//VPT構築の事前設定をする
var vpt_builder = new MenohSharp.VariableProfileTableBuilder();
//モデルプロパティで確認した入力データ仕様を設定
vpt_builder.AddInputProfile("data_0", MenohSharp.DType.Float, new[] { 1, 3, 224, 224});
//モデルプロパティで確認した出力ノード名を設定（次元数は自動算出）
vpt_builder.AddOutputName("prob_1");
//モデルデータを渡してVPTを構築
var vpt = vpt_builder.BuildVariableProfileTable(model_data);
//計算された出力テンソルの次元を取得
//(大抵は既知なのでわざわざ取得してなくて良いと思いますが、一つのテクニックとして）
var outputDims = vpt.GetVariableProfile("prob_1").Dims;
//VPTを使ってmodel_builderを作成
var model_builder = new MenohSharp.ModelBuilder(vpt);

//(unsafe)  ポインタを扱うため、ガーベージコレクタによるアドレス移動を抑制
//image_dataは1次元配列(float[]型)
fixed (float* p = image_data)
{
    //モデルに入力画像データのポインタを渡す
    model_builder.AttachExternalBuffer(inputNode, (IntPtr)p);

    //計算用のモデルを構築
    var model = model_builder.BuildModel(model_data, "mkldnn");

    //出力バッファのポインタを取得
    float* softmax_output_buff = (float*)(model.GetVariable(outputNode).BufferHandle);

    //推論を実施
    model.Run();

    //出力を取得
    //出力データはポインタだけ与えられているので、
    //事前に確認している変数型を頼りにデータへアクセスする
    var top_k = 5;
    var top_k_indices = ExtractTopKIndexList(
        softmax_output_buff, softmax_output_buff + outputDims[1], top_k);  //出力から上位5つを取得
    var categories = LoadCategoryList("CategoryList.txt");   //カテゴリのリスト(適当に準備する)
    Console.WriteLine("top " + top_k + "  categories are");
    foreach (var ki in top_k_indices)
    {
        Console.WriteLine(ki + " " + (*(softmax_output_buff + ki)).ToString() + 
                     "  categories are" + categories[ki]);  //カテゴリを適当に表示する
    }
}

//モデルデータの破棄
model_data.Dispose();

//-------------------------------------------------
static unsafe int[] ExtractTopKIndexList(float* first, float* last, int k)
{
    var q = new List<Tuple<float, int>>();
    for (var i = 0; first != last; first++, i++)
    {
        q.Add(Tuple.Create(*first, i));
    }
    return q.OrderByDescending(_ => _.Item1).Take(k).Select(_ => _.Item2).ToArray();
}

static unsafe string[] LoadCategoryList(string synset_words_path)
{
    return System.IO.File.ReadAllLines(synset_words_path);
}
```

##3-4. 実行
ビルドして実行すればターミナルに結果が並びます。

#4. まとめ
かなり簡単に推論を実行することができました。動作も早いです。
様々な言語から利用できることもあり、もっと広がっても良い気がします。
VGGに対応していたら実用上は多くのタスクに対応できると思いますし。

現状はIntel CPUにしか対応していませんが、
今後のバージョンアップでどんどん使いやすくなる気がします。
シングルボードコンピュータではLattePandaあたりが使いやすそうですかね。

~~個人的にはラズパイで使いたいので次はARM CPUに対応してくれると嬉しいです。~~
(2019.5.12追記)
ARMで動作するbranchがあるとの[アドバイス](https://twitter.com/Vengineer/status/1127438967178010625)をいただきました。
https://github.com/pfnet-research/menoh/tree/armnn/menoh/armnn
これを求めてました。今度トライしてみようと思います。




暇があったら次は、Intel CPUもARM CPUもnVIDIA GPUにも対応しているMicrosoft製[ONNX Runtime](https://github.com/Microsoft/onnxruntime)あたりでも確認してみようと思います。
(2019.5.12追記)
ONNX Runtimeにトライしてみましたが、ラズパイ向けにビルド(Dockerコンテナを使ったクロスコンパイル)を行おうとしても上手く通らないので当分保留です。。。