ML.NET による画像分類

# ML.NET 1.4.0 で画像分類が正式サポートに

2018/05/07 に [ML.NET 0.1 がプレビュー リリース](https://qiita.com/hiromasa-masuda/items/67920b4dba478d3f83c9)されてから、すでに 2 年半が経ち、多くの機能強化がされ、現在(2020 年 10 月時点)では、ML.NET 1.5.2 が最新のバージョンとなります。

2019 年 9 月にリリースされた、ML.NET 1.4.0 では、画像分類が正式にサポートされました。
これで、独自の画像を使った、独自の画像分類モデルを開発できるようになりました。

以下から学習済モデルを選択し、転移学習で画像データの学習を行うことができます。

- Inception V3
- ResNet V2 101 - Residual Network V2 101 層
- ResNet V2 50 - Residual Network V2 50 層
- MobileNet V2

ML.NET の画像分類の仕組みとしては、下位では Tensorflow が使われていて、Tensorflow API の .NET Standard ライブラリである [TensorFlow.NET](https://github.com/SciSharp/TensorFlow.NET) をラップした形で、画像分類系の API が実装されています。

今回は、ML.NET を利用して、犬と猫の品種の画像データを学習して、犬猫品種画像分類 AI を作成する手順を紹介したいと思います。

#.NET Core プロジェクトの作成
Visual Studio 2019 を起動し、[新しいプロジェクトの作成] - [コンソール アプリ(.NET Core)] を選択し、[次へ] を選択、[プロジェクト名] に任意のプロジェクト名(ここでは、 "PetClassification001" ) を入力し [作成] を選択します。

#ML.NET のインストール
[ソリューション エクスプローラー] から、プロジェクトを選択し、右クリックメニューから、[NuGet パッケージの管理] を選択します。

[参照] タブの [検索]　テキスト ボックスに "Microsoft.ML" と入力し、[Microsoft.ML] を選択し、[インストール] を選択し、以降画面の指示に従い "Microsoft.ML" をインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/ca03aee6-4c2d-fd2b-fe17-a624d6b755a1.png)

続いて、同様の手順で、"Microsoft.ML.Vision"、"Microsoft.ML.ImageAnalytics"、"SciSharp.TensorFlow.Redist" を検索し、インストールします。
画像分類を行うためには、この "Microsoft.ML.Vision" ライブラリが必要です。
"Microsoft.ML.ImageAnalytics" は、画像の加工等に利用します。
"SciSharp.TensorFlow.Redist" は、TensorFlow.NET のライブラリです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/0f5a485a-c3b6-28f3-b7ac-bf1c7b858f26.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/c35bb124-3477-a3df-8f7e-6613a8067596.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/d2e764ce-1bc2-1c6e-b05b-65ed1688a8a4.png)


# 画像データの準備
データセットは、[オックスフォード大学の Visual Geometry Group](http://www.robots.ox.ac.uk/~vgg/) が公開している The Oxford-IIIT Pet Dataset を使用します。
以下のサイトから、[Downloads] - [1.Dataset] を選択して、images.tar.gz をダウンロードして、任意のフォルダーに展開しておいてください。

- [The Oxford-IIIT Pet Dataset](http://www.robots.ox.ac.uk/~vgg/data/pets/)

7,390 個の画像データ (*.jpg) が展開されます。画像ファイル名にラベルが含まれています。ファイル名が、"品種_number.jpg" という書式になっています。これらのファイルをプロジェクトに追加していきます。

[ソリューション エクスプローラー] から、プロジェクトを選択し、右クリックメニューから、[追加] - [新しいフォルダー] を選択し、フォルダー名を "DataSet" とします。さらに、作成したフォルダーを選択し、右クリックメニューから、[追加] - [新しいフォルダー] を選択し、フォルダー名を "PetImages" とします。
次に、"PetImages" を選択し、[エクスプローラーでフォルダーを開く] を選択し、対象フォルダーをエクスプローラーで開きます。同フォルダーへ、展開した 7,390 個の画像ファイル (*.jpg) を移動または、コピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/c3dde3da-409a-606a-05df-449ac3c609da.png)

次に、画像ファイルのパスのリストをテキストファイルに書き出します。

コマンド プロンプトで、"PetImages" フォルダーへ移動し、以下のコマンドを実行します。パス一覧の出力先ファイル名は、"_petImageFileList.txt" としています。
_petImageFileList.txt には、各画像のフルパスが一覧されます。

```dosbatch:cmd

dir *.jpg /b /s > _petImageFileList.txt

```

[ソリューション エクスプローラー] から、"_petImageFileList.txt" を選択し、[プロパティ] - [出力ディレクトリにコピー] から、"常にコピーする" を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/0c218e91-4b36-33e6-4116-871a53f1da73.png)

これで、ビルドしたモジュールのカレントに、画像ファイルのパスが一覧されたテキストファイル "_petImageFileList.txt" がコピーされるようになります。


#データセットの定義

学習データのカラムの定義を行います。

[ソリューション エクスプローラー] から、プロジェクトを選択し、右クリックメニューから、[追加] - [クラス] - [名前] に "PetData" と入力し、[追加] を選択します。

PetData.cs に、以下のコードを記述します。
PetData クラスは、ラベルと画像データの読み込みに、PetDataPrediction は、推論結果の格納に使用します。

```csharp:PetData.cs

using Microsoft.ML.Data;

namespace PetClassification001
{
    public class PetData
    {
        public string Breed { get; set; }
        public string ImageFilePath { get; set; }
    }

    public class PetDataPrediction : PetData
    {
        public string PredictedBreed { get; set; }
        public float[] Score { get; set; }
    }
}
```

# データセットの読み込み

以降から、"Program.cs" にコードを追加していきます。
はじめに、以下のコードで各ライブラリを using しておきます。

```csharp:Program.cs

using System;
using System.IO;
using System.Linq;
using Microsoft.ML;
using Microsoft.ML.Data;
using Microsoft.ML.Transforms;
using Microsoft.ML.Vision;

```

以降のコードを Main メソッドに順に記述していきます。

まずは、品種文字列とファイルのパスを格納した PetData の一覧を作成します。

```csharp:Program.cs

string dataSetFilePath = @".\DataSet\PetImages\_petImageFileList.txt";

// パス一覧の読み込み
var imagefilePaths = File.ReadLines(dataSetFilePath , System.Text.Encoding.Default);

// データセットの作成
var petDataSet = imagefilePaths.Select(path =>
    new PetData()
    {
        // ファイル名に品種名が含まれるので、品種名を切り出して Breed に設定
        Breed = path.Substring(path.LastIndexOf('\\') + 1, path.LastIndexOf('_') - path.LastIndexOf('\\') - 1),
        ImageFilePath = path
    });
```

# データセットの加工

次に、データセットを加工していきます。
Label 列を作成し、品種文字列を数値に変換しています。
また、RawImageBytes 列を作成し、パスから画像をロードしています。

```csharp:Program.cs

// コンテキストの生成
MLContext mlContext = new MLContext(seed: 1);

// データのロード
IDataView petDataView = mlContext.Data.LoadFromEnumerable(petDataSet);
// データセットをシャッフル
IDataView shuffledPetDataView = mlContext.Data.ShuffleRows(petDataView);

// データ前処理
// データセットの加工
IDataView transformedDataView = mlContext.Transforms.Conversion.MapValueToKey(
    // 品種文字列を数値に変換して列名を Label とする
    inputColumnName: nameof(PetData.Breed),
    outputColumnName: "Label",
    keyOrdinality: ValueToKeyMappingEstimator.KeyOrdinality.ByValue)
    .Append(mlContext.Transforms.LoadRawImageBytes(
        // パスから画像をロード
        inputColumnName: nameof(PetData.ImageFilePath),
        imageFolder: null,
        outputColumnName: "RawImageBytes"))
    .Fit(shuffledPetDataView)   //　データセット用に Transformer を生成 
    .Transform(shuffledPetDataView); // Transformer をデータセットに適用

```

次に、加工したデータを学習データ、検証データ、評価データに分割します。
ここでは、学習データ: 80%、検証データ: 18%、評価データ: 2% に分割しています。

```csharp:Program.cs

// データセットを学習データ、検証データ、評価データに分割
// データセットを 7:3 に分割
var trainValidationTestSplit = mlContext.Data.TrainTestSplit(transformedDataView, testFraction: 0.3);
// 検証/評価データセットを 8:2 に分割
var validationTestSplit = mlContext.Data.TrainTestSplit(trainValidationTestSplit.TestSet, testFraction: 0.2);

// データセットの 70% を学習データとする
IDataView trainDataView = trainValidationTestSplit.TrainSet;
// データセットの 24% を検証データとする
IDataView validationDataView = validationTestSplit.TrainSet;
// データセットの 6% を評価データとする
IDataView testDataView = validationTestSplit.TestSet;

```

# 学習の定義と実行

どのように学習を行うかの定義を行います。ここでは、転移学習を行う学習モデルとして、Residual Network V2 50 層 を指定しています。ImageClassificationTrainer.Architecture　列挙型でその他の学習モデルを指定できます。
また、ここで、エポック、バッチサイズ等のハイパーパラメータも指定できます。

```csharp:Program.cs

// 学習の定義
var trainer = mlContext.MulticlassClassification.Trainers.ImageClassification(
    new ImageClassificationTrainer.Options()
    {
        LabelColumnName = "Label", //ラベル列
        FeatureColumnName = "RawImageBytes", // 特徴列
        Arch = ImageClassificationTrainer.Architecture.ResnetV250, //転移学習モデルの選択
        Epoch = 200,
        BatchSize = 10,
        LearningRate = 0.01f,
        ValidationSet = validationDataView, // 検証データを設定
        MetricsCallback = (metrics) => Console.WriteLine(metrics),
        WorkspacePath = @".\Workspace",
    })
    .Append(mlContext.Transforms.Conversion.MapKeyToValue(
        // 推論結果のラベルを数値から品種文字列に変換
        inputColumnName: "PredictedLabel",
        outputColumnName: "PredictedBreed"));

// 学習の実行
ITransformer trainedModel = trainer.Fit(trainDataView);

// 学習モデルをファイルに保存
string modelFilePath = $@".\model{DateTimeOffset.Now:yyyyMMddHmmss}.zip";
mlContext.Model.Save(trainedModel, trainDataView.Schema, modelFilePath);

```

# 学習モデルの評価

最後に、作成した学習モデルを評価データで性能を評価します。
評価指標および推論結果を HTML で書き出しています。

```csharp:Program.cs

// テストデータで推論を実行
IDataView testDataPredictionsDataView = trainedModel.Transform(testDataView);
// テストデータでの推論結果をもとに評価指標を計算
var metrics = mlContext.MulticlassClassification.Evaluate(testDataPredictionsDataView);

// ラベルと品種文字列のキーバリューを取得
VBuffer<ReadOnlyMemory<char>> keyValues = default;
trainDataView.Schema["Label"].GetKeyValues(ref keyValues);


string testFilePath = $@".\test{DateTimeOffset.Now:yyyyMMddHHmmss}.html";

// HTML で評価結果を書き出し
using (var writer = new StreamWriter(testFilePath))
{
    writer.WriteLine($"<html><head><title>{Path.GetFileName(_modelFilePath)}</title>");
    writer.WriteLine("<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css\" integrity=\"sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2\" crossorigin=\"anonymous\">");
    writer.WriteLine("</head><body>");

    writer.WriteLine($"<h1>Metrics for {Path.GetFileName(_modelFilePath)}</h1>");
    // メトリックの書き出し
    writer.WriteLine("<div><table class=\"table table-striped\">");
    writer.WriteLine($"<tr><td>MicroAccuracy</td><td>{metrics.MicroAccuracy:0.000}</td></tr></tr>");
    writer.WriteLine($"<tr><td>MacroAccuracy</td><td>{metrics.MacroAccuracy:0.000}</td></tr></tr>");
    writer.WriteLine($"<tr><td>Precision</td><td>{metrics.ConfusionMatrix.PerClassPrecision.Average():0.000}</td></tr></tr>");
    writer.WriteLine($"<tr><td>Recall</td><td>{metrics.ConfusionMatrix.PerClassRecall.Average():0.000}</td></tr></tr>");
    writer.WriteLine($"<tr><td>LogLoss</td><td>{metrics.LogLoss:0.000}</td></tr></tr>");
    writer.WriteLine($"<tr><td>LogLossReduction</td><td>{metrics.LogLossReduction:0.000}</td></tr></tr>");
                
    // クラス毎の適合率
    writer.WriteLine("<tr><td>PerClassPrecision</td><td>");
    metrics.ConfusionMatrix.PerClassPrecision
    .Select((p, i) => (Precision: p, Index: i))
    .ToList()
    .ForEach(p =>
        writer.WriteLine($"{keyValues.GetItemOrDefault(p.Index)}: {p.Precision:0.000}<br />"));
    writer.WriteLine("</td></tr>");

    // クラス毎の再現率
    writer.WriteLine("<tr><td>PerClassRecall</td><td>");
    metrics.ConfusionMatrix.PerClassRecall
    .Select((p, i) => (Recall: p, Index: i))
    .ToList()
    .ForEach(p =>
        writer.WriteLine($"{keyValues.GetItemOrDefault(p.Index)}: {p.Recall:0.000}<br />"));
    writer.WriteLine("</td></tr></table></div>");

    // 評価データ毎の分類結果
    writer.WriteLine($"<h1>Predictions</h1>");
    writer.WriteLine($"<div><table class=\"table table-bordered\">");

    foreach (var prediction in predictions)
    {
        writer.WriteLine($"<tr><td>");
        // 画像ファイル名
        writer.WriteLine($"{Path.GetFileName(prediction.ImageFilePath)}<br />");
        // 正解ラベル
        writer.WriteLine($"Actual Value: {prediction.Breed}<br />");
        // 推論結果
        writer.WriteLine($"Predicted Value: {prediction.PredictedBreed}<br />");
        // 画像
        writer.WriteLine($"<img class=\"img-fluid\" src=\"{prediction.ImageFilePath}\" /></td>");                    
        // クラス毎の推論結果
        writer.WriteLine($"<td>");
        prediction.Score.Select((s, i) => (Index: i, Label: keyValues.GetItemOrDefault(i), Score: s))
        .OrderByDescending(c => c.Score)
        .Take(10) // 上位 10 件
        .ToList()
        .ForEach(c =>
        {
            writer.WriteLine($"{c.Label}: {c.Score:P}<br />");
        });
                    
        writer.WriteLine("</td></tr>");
    }

    writer.WriteLine("</table></div>");
    writer.WriteLine("</body></html>");
}

```
# 評価結果の確認

書き出した HTML(<プロジェクト フォルダー>\bin\Debug\netcoreapp3.1 以下に出力) を見ていきます。
今回の学習モデルの各指標は、以下の様になりました。
評価データの範囲では、かなり性能の高い学習モデルが出来ているようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/8a498144-29c4-372f-5398-44d814e259c5.png)
かなり高い確率で、アビシニアン(Abyssinian)という品種を特定出来ています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/4ec66be4-ed58-87c7-e44f-8909c99c67be.png)
アビシニアン(Abyssinian)とベンガル(Bengal)を正しく分類できています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/7fb981a7-5890-7582-54bf-b4c232fbc20d.png)
ブリティッシュショートヘア(British_Shorthair)をロシアンブルー(Russian_Blue)と間違えています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/14d0d78d-9ce8-27d6-65aa-2c766afc86bd.png)
逆に、ロシアンブルー(Russian_Blue)をブリティッシュショートヘア(British_Shorthair)と間違えているパターンもあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/f9801b03-deac-d6c7-6e13-ea5f6985e93a.png)

# まとめ

ML.NET で画像分類がサポートされました。.NET プログラミングの知識で、カスタムの画像分類を実現するカスタムの機械学習モデルを開発できるようになりました。さらに、転移学習により画像分類の学習モデル開発できるため、学習データも少量で済み、学習に要する時間も少なくて済みます。
これまで、.NET でアプリケーションを開発してきたエンジニアにとっては、既存のアプリケーションに独自の機械学習モデルを組み込み、機械学習ならではの機能を拡張することができるようになったと考えます。

# 参考サイト
- [ML.NET 1.0 による回帰分析](https://qiita.com/hiromasa-masuda/items/c57e9009389f7f1dbe12)
- [Microsoft.ML.Vision Namespace](https://docs.microsoft.com/en-us/dotnet/api/microsoft.ml.vision)
- [ImageClassificationTrainer Class](https://docs.microsoft.com/en-us/dotnet/api/microsoft.ml.vision.imageclassificationtrainer)
- [ConversionsExtensionsCatalog.MapValueToKey Method](https://docs.microsoft.com/en-us/dotnet/api/microsoft.ml.conversionsextensionscatalog.mapvaluetokey)
- [Announcing ML.NET 1.4 general availability (Machine Learning for .NET)](https://devblogs.microsoft.com/dotnet/announcing-ml-net-1-4-global-availability-machine-learning-for-net/)
- [チュートリアル: 事前トレーニング済みの TensorFlow モデルから ML.NET 画像分類モデルを生成する
](https://docs.microsoft.com/ja-jp/dotnet/machine-learning/tutorials/image-classification)
- [Tutorial: Automated visual inspection using transfer learning with the ML.NET Image Classification API](https://docs.microsoft.com/en-us/dotnet/machine-learning/tutorials/image-classification-api-transfer-learning)
- [ML.NET 1.4.0](https://github.com/dotnet/machinelearning/releases/tag/v1.4.0)
- [ResNet (Residual Network) の実装](https://www.bigdata-navi.com/aidrops/2611/)