ML.NETドキュメントリーディング#4

#1.概要
ML.NETからTensorFlowで作成されたモデルを使えるようですので、TensorFlowのモデルの作成からME.NETで動かすところまでを試してみました。
実際にやってみて、難しかったことや困ったことなどありましたのでそういったところも書いていきます。


ML.NETの以下のチュートリアルに従い実施しています。
[事前トレーニング済みの TensorFlow モデルから ML.NET 画像分類モデルを生成する](https://docs.microsoft.com/ja-jp/dotnet/machine-learning/tutorials/image-classification)

2020/05/11追記
TensorFlowで作成したモデルをML.NETで読み込み、予測結果を確認したところ間違ったデータが取得されていました。(出力の要素数が10のところ30であった)
6章以降に記載したML.NETのコードは参考程度としてください。

#2.TensorFlowモデルについて
ML.NETのチュートリアルでは学習済みモデルを使用するとありますが、全体を理解するためモデル作成から行いました。
TensorFlowのモデルは以下のTensorFlowのチュートリアルを参考に作成しています。

[はじめてのニューラルネットワーク：分類問題の初歩](https://www.tensorflow.org/tutorials/keras/classification?hl=ja)

注意点として、ML.NETのチュートリアルにある学習モデルはTensorFlow1.Xで作成されていると思われます。
今回作成するモデルはTensorFlow2.1で作成するため、TensorFlow1.Xで作成する学習モデルとファイル構成が違っています。

#3.ワークフロー
今回のワークフローは以下のようになっています。
![model.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263183/5b9db8f6-e129-8b74-70dc-01396cbf8f54.png)
[TensorFlow2.0の新機能より](https://blog.tensorflow.org/2019/01/whats-coming-in-tensorflow-2-0.html)

MNISTデータ(fashion)を読み込み、Kerasライブラリを使いCPUで学習します。
学習済みモデルはSavedModelにシリアライズされ、ML.NET(C#)で使われます。
（KerasライブラリはTensorFlow1.1で統合されたニューラルネットワークのライブラリ）

チュートリアルの命題は衣服を分類することとなっているため、今回学習に使用するデータは衣服の画像になります。
![train_image_list.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263183/3fad5b78-79ac-088e-60e3-b1ff1079d0e0.png)
[はじめてのニューラルネットワーク：分類問題の初歩](https://www.tensorflow.org/tutorials/keras/classification?hl=ja)より

これらは画像毎にタグ付けされており、靴やコートなどの種類に分けれらています。
画像とラベルが対応付けられたデータは公開されておりMNISTデータと呼ばれています。

#4.SavedModelの作成
ワークフローにあるSavedMoedデータをTensorFlow2.1で作成します。
サンプルコードでは実行時に画像が表示されていましたが、SavedModelデータを作成する処理にしたいので
すべてファイルに保存しています。(figフォルダに格納)
また、ML.NETで使用する画像やタグ情報をmodelフォルダのassets\imagesに保存するよう処理を追加しています。
SavedModeについてこちらのサイトをみてください。[SavedModelについてのまとめ](https://qiita.com/t_shimmura/items/1ebd2414310f827ed608)


【動作環境】

+ Windows 10 Home(1903)
+ Anaconda v4.8.3
+ Python v3.6.8
+ TensorFlow v2.1.0
+ Visual Studio Code 1.44.2

【事前準備】
①Anacondaの環境を入れる。[Windows版AnacondaでTensorFlow環境構築](https://qiita.com/exy81/items/48314b968d9fad6170c8)
②VisualStudioCodeを入れる。　[【Visual Studio Codeを導入しよう！】Pythonを動かすまでの手順](https://www.sejuku.net/blog/68542)
③VSCodeのデバッグ環境を整える。　[VSCodeでPythonをデバッグする](https://qiita.com/bigengelt/items/780440a146e6a3bdffd4)

【フォルダ構成】
MLNetForTF
　├MLNetForTF　　　　　　　・・・ ML.NETのソリューション
　├model　　　　　　　　　　・・・ SavedModelデータ
　│　├assets
　│　│　└images　　　　　　・・・ ML.NETで使用する画像とタグ情報
　│　├variables　　　　　　　・・・ 学習途中のデータ(チェックポイントなど)
　│　└saved_model.pb
　└python
　 　├fig　　　　　　　　　　　・・・ 画像確認用
　 　└fashion_mnist_train.py　 　・・・ 処理本体

```python:fashion_mnist_train.py
import tensorflow as tf
from tensorflow import keras

# ヘルパーライブラリのインポート
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
from pathlib import Path

print(tf.__version__)

class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']

def plot_image(i, predictions_array, true_label, img):
    predictions_array, true_label, img = predictions_array[i], true_label[i], img[i]
    plt.grid(False)
    plt.xticks([])
    plt.yticks([])

    plt.imshow(img, cmap=plt.cm.binary)

    predicted_label = np.argmax(predictions_array)
    if predicted_label == true_label:
        color = 'blue'
    else:
        color = 'red'

    plt.xlabel("{} {:2.0f}% ({})".format(class_names[predicted_label],
                                    100*np.max(predictions_array),
                                    class_names[true_label]),
                                    color=color)

def plot_value_array(i, predictions_array, true_label):
    predictions_array, true_label = predictions_array[i], true_label[i]
    plt.grid(False)
    plt.xticks([])
    plt.yticks([])
    thisplot = plt.bar(range(10), predictions_array, color="#777777")
    plt.ylim([0, 1]) 
    predicted_label = np.argmax(predictions_array)

    thisplot[predicted_label].set_color('red')
    thisplot[true_label].set_color('blue')


def main():
    fashion_mnist = keras.datasets.fashion_mnist

    (train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
    
    fig_path = Path("./fig")
    if not Path.exists(fig_path) :
        Path.mkdir(fig_path)

    plt.figure()
    plt.imshow(train_images[0])
    plt.colorbar()
    plt.grid(False)
    plt.savefig(str(fig_path / 'train_image_0.png'))

    # 学習用ラベルからユニークな値を取得 (ラベル値,インデックス)
    train_label_unique, train_label_uqidx = np.unique(train_labels, return_index=True)
    train_images_org = train_images
    test_images_org = test_images
    train_images = train_images / 255.0
    test_images = test_images / 255.0

    plt.figure(figsize=(10,10))
    for i in range(25):
        plt.subplot(5,5,i+1)
        plt.xticks([])
        plt.yticks([])
        plt.grid(False)
        plt.imshow(train_images[i], cmap=plt.cm.binary)
        plt.xlabel(class_names[train_labels[i]])
    plt.savefig(str(fig_path / 'train_image_list.png'))

    model = keras.Sequential([
        keras.layers.Flatten(input_shape=(28, 28)),
        keras.layers.Dense(128, activation='relu'),
        keras.layers.Dense(10, activation='softmax')
    ])

    model.compile(optimizer='adam', 
                loss='sparse_categorical_crossentropy',
                metrics=['accuracy'])

    model.fit(train_images, train_labels, epochs=5)

    test_loss, test_acc = model.evaluate(test_images,  test_labels, verbose=2)

    print('\nTest accuracy:', test_acc)

    predictions = model.predict(test_images)

    num_rows = 5
    num_cols = 3
    num_images = num_rows*num_cols
    plt.figure(figsize=(2*2*num_cols, 2*num_rows))
    for i in range(num_images):
        plt.subplot(num_rows, 2*num_cols, 2*i+1)
        plot_image(i, predictions, test_labels, test_images)
        plt.subplot(num_rows, 2*num_cols, 2*i+2)
        plot_value_array(i, predictions, test_labels)
    plt.savefig(str(fig_path / 'train_image_predict.png'))

    # テストデータから1枚目を取得
    img = test_images[0]
    print(img.shape)

    # 画像を配列に格納する
    img = (np.expand_dims(img,0))
    print(img.shape)
    # 推定する
    predictions_single = model.predict(img)
    print(predictions_single)


    plot_value_array(0, predictions_single, test_labels)
    _ = plt.xticks(range(10), class_names, rotation=45)

    print(np.argmax(predictions_single[0]))

    # https://qiita.com/iwatake2222/items/80fc73ff23d8f51650f5
    save_path = (Path("./") / "../model").absolute()
    if not Path.exists(save_path) :
        Path.mkdir(save_path)
    
    model.save(str(save_path), save_format="tf")
        
    # 画像保存
    # テストデータから15枚保存する
    # tagsファイルも作成
    img_path = save_path / "assets/images"
    if not Path.exists(img_path) :
        Path.mkdir(img_path)
    
    # 学習画像
    tagname = "train_tags.tsv"
    num_images = len(train_label_uqidx)
    num_cols = 5
    num_rows = num_images // num_cols
    plt.figure(figsize=(10, 2*num_rows))
    file = open(str(img_path / tagname), 'w', newline="\n", encoding="SJIS")

    for i, uqidx in enumerate(train_label_uqidx):
        fname = "fashoin_mnist_train_{0}.png".format(uqidx)
        fpath = img_path / fname
        img = train_images_org[uqidx]
        pil_img = Image.fromarray(img)
        pil_img.save(str(fpath))
        label = train_label_unique[i]
        row = fname + "\t" + str(label) + "\n"
        file.write(row)
        
        plt.subplot(num_rows, num_cols, i+1)
        plt.xticks([])
        plt.yticks([])
        plt.grid(False)
        plt.imshow(img, cmap=plt.cm.binary)
        plt.xlabel(class_names[label])

    plt.savefig(str(img_path / 'train_image_list.png'))
    file.close()

    # テスト画像
    tagname = "test_tags.tsv"
    num_rows = 3
    num_cols = 5
    num_images = num_rows*num_cols
    plt.figure(figsize=(10, 6))
    file = open(str(img_path / tagname), 'w', newline="\n", encoding="SJIS")

    for i, img in enumerate(test_images_org[:num_images]):
        fname = "fashoin_mnist_test_{0}.png".format(i)
        fpath = img_path / fname
        pil_img = Image.fromarray(img)
        pil_img.save(str(fpath))
        label = test_labels[i]
        row = fname + "\t" + str(label) + "\n"
        file.write(row)
        
        plt.subplot(num_rows, num_cols, i+1)
        plt.xticks([])
        plt.yticks([])
        plt.grid(False)
        plt.imshow(img, cmap=plt.cm.binary)
        plt.xlabel(class_names[label])

    plt.savefig(str(img_path / 'test_image_list.png'))
    file.close()

    # labelとindexの対応作成
    label_name = "label.tsv"
    with open(str(img_path / label_name), "w", newline="\n", encoding="SJIS") as file :
        for i,val in enumerate(class_names):
            file.write(str(i) + "\t" + val + "\n")

if __name__ == '__main__':
    main()
```
#5 Python実行
VSCodeから実行します。以下の順で実行できます。

①ソースコードを以下のpythonフォルダに保存する。
[任意フォルダ]\MLNetForTF\python\fashion_mnist_train.py
②Anaconda Promptを起動し、activateを実行する。
※デフォルトの仮想環境はbaseですが、新規作成した場合は仮想環境の名前を指定してアクティブにする

```bash:AnacondaPrompt
(base) C:\my_folder\MLNetForTF>activate
```
③VSCodeを起動し、fashion_mnist_train.pyを開く。
④VSCodeのコンパイラをAnacondaのPythonにした状態で実行する。
![VSCode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263183/81a42b34-0021-5a74-6064-fde69b4662a8.png)
↓
![VSCode2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263183/8001dcef-8c2f-7584-7bdb-e22bc275872f.png)

#6. Python実行結果

SavedModelデータを作成する箇所は以下の処理になります。
modelフォルダまでのパスを指定しており、saved_model.pbやvariablesフォルダに格納されるファイルが作成されます。
引数のsave_formatを"tf"にするとSavedMode形式で保存されます。


```python:fashion_mnist_train.py抜粋
    model.save(str(save_path), save_format="tf")
```

作成されたmodelフォルダは以下のようになっています。

```bash:AnacondaPrompt
(base) C:\my_folder\MLNetForTF>tree .\model /F
フォルダー パスの一覧
ボリューム シリアル番号は 7245-9833 です
C:\MY_FOLDER\MLNETFORTF\MODEL
│  saved_model.pb
│
├─assets
│  └─images
│          fashoin_mnist_test_0.png
│          fashoin_mnist_test_1.png
│          fashoin_mnist_test_10.png
│          fashoin_mnist_test_11.png
│          fashoin_mnist_test_12.png
│          fashoin_mnist_test_13.png
│          fashoin_mnist_test_14.png
│          fashoin_mnist_test_2.png
│          fashoin_mnist_test_3.png
│          fashoin_mnist_test_4.png
│          fashoin_mnist_test_5.png
│          fashoin_mnist_test_6.png
│          fashoin_mnist_test_7.png
│          fashoin_mnist_test_8.png
│          fashoin_mnist_test_9.png
│          fashoin_mnist_train_0.png
│          fashoin_mnist_train_1.png
│          fashoin_mnist_train_16.png
│          fashoin_mnist_train_18.png
│          fashoin_mnist_train_19.png
│          fashoin_mnist_train_23.png
│          fashoin_mnist_train_3.png
│          fashoin_mnist_train_5.png
│          fashoin_mnist_train_6.png
│          fashoin_mnist_train_8.png
│          label.tsv
│          test_image_list.png
│          test_tags.tsv
│          train_image_list.png
│          train_tags.tsv
│
└─variables
        variables.data-00000-of-00001
        variables.index
```
TensorFlowの予測結果は以下となっています。
![train_image_predict.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263183/b2ec44c8-b520-9bea-6f56-4ffad8aa2930.png)



次に、ML.NETからSavedModeデータをロードする処理を作成するのですが、オペレーションの名前を指定する必要があるためモデルの入出力を「saved_model_cli」コマンドで調べます。

```bash:AnacondaPrompt
(base) C:\my_folder\MLNetForTF>saved_model_cli show --dir .\model --tag_set serve --signature_def serving_default
The given SavedModel SignatureDef contains the following input(s):
  inputs['flatten_input'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, 28, 28)
      name: serving_default_flatten_input:0
The given SavedModel SignatureDef contains the following output(s):
  outputs['dense_1'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, 10)
      name: StatefulPartitionedCall:0
Method name is: tensorflow/serving/predict
```

モデルのインプット、アウトプットはTensorFlowにより以下の名前で作成されていました。

+ インプット：serving_default_flatten_input
+ アウトプット：StatefulPartitionedCall


#6. ML.NETソリューション作成
(※出力結果がTensorFlowのものと違った結果となっています。手順やコードは参考程度にしてください。)
ML.NETのチュートリアルを参考にコンソールのソリューションを作成します。
プロジェクト名は「MLNetForTF」で以下の構成で作成しました。
「MLNetForTF」プロジェクト配下のmodelフォルダはTensorFlowで作成したmodelフォルダからコピーしてください。


【フォルダ構成】
MLNetForTF
　└MLNetForTF　　　　　　　・・・ ML.NETのソリューション
　 　├aMLNetForTF　　　　　・・・ ML.NETのプロジェクト
　 　│　├model
　 　│　│　├assets
　 　│　│　│　└images　　　　　　・・・ ML.NETで使用する画像とタグ情報
　 　│　│　├variables　　　　　　　・・・ 学習途中のデータ(チェックポイントなど)
　 　│　│　└saved_model.pb
　 　│　├MLNetForTF.csproj
　 　│　└Program.cs　　　　　　　・・・ 処理本体
　 　└aMLNetForTF.sln


```C#:Program.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.ML;
using Microsoft.ML.Data;

namespace MLNetForTF
{
    class Program
    {
        static readonly string _modelPath = Path.Combine(Environment.CurrentDirectory, "model");
        static readonly string _assetsPath = Path.Combine(_modelPath, "assets");
        static readonly string _imagesFolder = Path.Combine(_assetsPath, "images");
        static readonly string _trainTagsTsv = Path.Combine(_imagesFolder, "train_tags.tsv");
        static readonly string _testTagsTsv = Path.Combine(_imagesFolder, "test_tags.tsv");
        static readonly string _predictSingleImage = Path.Combine(_imagesFolder, "fashoin_mnist_test_0.png");

        private struct InceptionSettings
        {
            public const int ImageHeight = 28;
            public const int ImageWidth = 28;
            public const float Mean = 0;
            public const float Scale = (float)(1 / 255.0);
            public const bool ChannelsLast = true;
        }

        static void Main(string[] args)
        {
            var mlContext = new MLContext();
            ITransformer model = GenerateModel(mlContext);
            ClassifySingleImage(mlContext, model);
            Console.ReadKey();
        }
        private static void DisplayResults(IEnumerable<ImagePrediction> imagePredictionData)
        {
            foreach (ImagePrediction prediction in imagePredictionData)
            {
                var max = prediction.Score.Max();
                var index = prediction.Score.AsSpan().IndexOf(max);
                Console.WriteLine($"Image: {Path.GetFileName(prediction.ImagePath)} predicted as: {prediction.Score.Max()} with score: {index}");
            }
        }
        public static IEnumerable<ImageData> ReadFromTsv(string file, string folder)
        {
            return File.ReadAllLines(file)
                .Select(line => line.Split('\t'))
                .Select(line => new ImageData()
                {
                    ImagePath = Path.Combine(folder, line[0])
                });
        }
        public static void ClassifySingleImage(MLContext mlContext, ITransformer model)
        {
            var imageData = new ImageData()
            {
                ImagePath = _predictSingleImage
            };

            // Make prediction function (input = ImageData, output = ImagePrediction)
            var predictor = mlContext.Model.CreatePredictionEngine<ImageData, ImagePrediction>(model);
            var prediction = predictor.Predict(imageData);

            var max = prediction.Score.Max();
            var index = prediction.Score.AsSpan().IndexOf(max);
            Console.WriteLine($"Image: {Path.GetFileName(prediction.ImagePath)} predicted as: {prediction.Score.Max()} with score: {index}");
        }

        public static ITransformer GenerateModel(MLContext mlContext)
        {
            string input = "serving_default_flatten_input";
            string output = "StatefulPartitionedCall";

            Action<ImageDataMem, ImageDataMemOut> mapping = (_input, _output) => {
                _output.Features = new float[InceptionSettings.ImageHeight,InceptionSettings.ImageWidth];

                for (int i = 0; i < InceptionSettings.ImageHeight; i++)
                {
                    for (int j = 0; j < InceptionSettings.ImageWidth; j++)
                    {
                        _output.Features[i, j] = _input.ImageData[i,j,0];
                    }
                }

            };

            IEstimator<ITransformer> pipeline = mlContext.Transforms.LoadImages(outputColumnName: input, imageFolder: _imagesFolder, inputColumnName: nameof(ImageData.ImagePath))
                .Append(mlContext.Transforms.ResizeImages(outputColumnName: input, imageWidth: InceptionSettings.ImageWidth, imageHeight: InceptionSettings.ImageHeight, inputColumnName: input))
                .Append(mlContext.Transforms.ExtractPixels(outputColumnName: input, interleavePixelColors: InceptionSettings.ChannelsLast, offsetImage: InceptionSettings.Mean, scaleImage: InceptionSettings.Scale))
                .Append(mlContext.Transforms.CustomMapping(mapping, contractName:null))
                .Append(mlContext.Model.LoadTensorFlowModel(_modelPath).ScoreTensorFlowModel(output, input))
                .Append(mlContext.Transforms.CopyColumns("Score", output));

            IDataView trainingData = mlContext.Data.LoadFromTextFile<ImageData>(path: _trainTagsTsv, hasHeader: false);
            ITransformer model = pipeline.Fit(trainingData);
            IDataView testData = mlContext.Data.LoadFromTextFile<ImageData>(path: _testTagsTsv, hasHeader: false);
            IDataView predictions = model.Transform(testData);

            // Create an IEnumerable for the predictions for displaying results
            IEnumerable<ImagePrediction> imagePredictionData = mlContext.Data.CreateEnumerable<ImagePrediction>(predictions, true);
            DisplayResults(imagePredictionData);

            return model;
        }
    }
    public class ImageData
    {
        [LoadColumn(0)]
        public string ImagePath;

        [LoadColumn(1)]
        public float Label;
    }


    public class ImageDataMem
    {
        [LoadColumn(0)]
        public string ImagePath;

        [LoadColumn(1)]
        public float Label;

        [VectorType(28, 28, 3)]
        [ColumnName("serving_default_flatten_input")]
        public float[,,] ImageData;
    }

    public class ImageDataMemOut
    {
        [VectorType(28, 28)]
        [ColumnName("StatefulPartitionedCall")]
        public float[,] Features;
    }

    public class ImagePrediction: ImageData
    {
        [VectorType]
        public float[] Score;
    }

}

```

#7.ML.NET実行
実行した結果は以下となっています。
![res.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263183/e47f7164-c1a9-0016-72cf-b661ca3aff39.png)

TensorFlowの出力結果では10個の配列に予測値が入っており、予測値が最も高いインデックスを取得していました。
ML.NETの出力結果では30個の配列が返ってきており、このような結果となりました。

どうしてこの結果になったのかまだ分かっていませんが、試したことを書いておきます。

+ ① 画像データから各ピクセルの値を取得するExtractPixelsはRGBでしか取得できず、グレースケールに変換できなかった。→　(28,28,3)の配列を(28,28)配列にするカスタムマッピング関数を作成した。[参考にしたページ](https://docs.microsoft.com/en-us/dotnet/api/microsoft.ml.tensorflowcatalog.loadtensorflowmodel?view=ml-dotnet)

+ ② TensorFlowの入力データは0-1で正規化されていたのでExtractPixelsの引数にスケール(1/255)を追加した。

+ ③ パイプラインの処理は「inputColumnName」と「outputColumnName」の名前がCreatePredictionEngineの型(変数名)と一致するようにした。(パイプラインの入出力のカラムが合っていないとエラーになる)

+ ④ パイプラインの各処理はテストデータを読み込み、デバッガで確認した。

```C#:中間データ確認
IDataView testData = mlContext.Data.LoadFromTextFile<ImageData>(path: _testTagsTsv, hasHeader: false);
IDataView transformedNewData = model.Transform(testData);
DataViewSchema columns = transformedNewData.Schema;
// Create DataViewCursor
using (DataViewRowCursor cursor = transformedNewData.GetRowCursor(columns))
{
    // Define variables where extracted values will be stored to
    String path = default;
    float label = default;
    VBuffer<float> img = default;

    // Define delegates for extracting values from columns
    ValueGetter<String> pathDelegate = cursor.GetGetter<String>(columns[0]);
    ValueGetter<float> labeDelegate = cursor.GetGetter<float>(columns[1]);
    ValueGetter<VBuffer<float>> imgDelegate = cursor.GetGetter<VBuffer<float>>(columns[4]);
    // Iterate over each row
    while (cursor.MoveNext())
    {
        //Get values from respective columns
        pathDelegate.Invoke(ref path);
        labeDelegate.Invoke(ref label);
        imgDelegate.Invoke(ref img);
    }
}
```



#8.まとめ
TensorFlowのモデルを作成し、ML.NETで実行できるか試してみました。
結果はTensorFlowとML.NETで違う結果となってしまいました。

おそらくML.NETの前処理であるパイプラインの作成が間違っているように思います。
TensorFlowは新しいバージョンで試してみましたが、モデルのインプット、アウトプットの名前をCLIを使って調べることができると分かったのが収穫でした。

最後に、パイプラインの作り方を知っている方がいましたら教えてほしいです；；