Windows PCかつオフライン環境でお気軽に画像認識する ML.NET 1.0 対応版

# TL;DR
Windows, つよいGPUがない, インターネットに画像をアップできない, アドミン権限がないなどの環境でも動作する、転移学習を利用した画像認識アプリを .NET Framework (Windows Forms) で作成しました。

アプリケーションをダウンロードし、分類したい名前を付けたフォルダに数枚ずつ画像を用意するだけで短時間で簡単に学習ができます。Pythonなどをインストールする必要もありません。色々なものを分類して最近の画像認識の精度を体感してみてください。

アプリケーションは、Microsoft がオープンソースで開発を進めている[ML.NET](https://dotnet.microsoft.com/apps/machinelearning-ai/ml-dotnet) (ML.NET単体では Linux, Macなどでも動きます) を利用しています。実装されている TensorFlow API を利用して、学習済みの Inception モデルを読み込み、画像特徴を抽出し、[Stochastic Dual Coordinate Ascent(SDCA)](https://docs.microsoft.com/en-us/dotnet/api/microsoft.ml.trainers.sdcamulticlasstrainer?view=ml-dotnet) を利用した多クラス分類器用いて転移学習をします。

* [ソースコード](https://github.com/ksasao/ImageClassification)
* [アプリ (Windows 10/x64/4GB以上のメモリ/Core i 程度のCPU/.NET Framework 4.6.1以降で動作)](https://github.com/ksasao/ImageClassification/raw/master/app/ImageClassificationApp1.0.0.zip)

# アプリケーションの利用方法
1. [アプリ](https://github.com/ksasao/ImageClassification/raw/master/app/ImageClassificationApp1.0.0.zip)をダウンロードして展開し、```ImageClassificationApp1.0.0\ImageClassificationApp.exe``` を起動します。
2. 画像を学習するには、[学習する] のボタンをクリックし、開いたダイアログから画像を分類済みのフォルダ(下図の場合は SampleImages フォルダ)を選択してOKをクリックします。
![image](https://qiita-image-store.s3.amazonaws.com/0/43450/d317c92a-45cd-c137-25ff-6c6caba1104d.png)
3. 10秒から1分程度で学習が完了します。![image](https://qiita-image-store.s3.amazonaws.com/0/43450/9e4ce96e-9c01-a2c6-ce50-033bae766087.png)下図のように表示されたら画像を Drag & Drop すると認識結果と確信度(0～1, 1が最も高い)が表示されます。 ![image](https://qiita-image-store.s3.amazonaws.com/0/43450/37f64033-b9c2-4755-d8d0-75cd5c3d076a.png)
4. あらかじめ学習したモデルを利用することも可能です。先ほど画像を学習したフォルダに```imageClassifier.zip```というファイルが生成されていますので、それを読み込んでみてください。学習済みモデルのファイル名は適宜変更可能です。また学習に利用した画像も不要です。![image](https://qiita-image-store.s3.amazonaws.com/0/43450/8e367dc8-3322-b02f-97e5-6e777f793f46.png)

# .NET 開発者向けの解説

## ML.NET とは
[ML.NET](https://dotnet.microsoft.com/apps/machinelearning-ai/ml-dotnet) は Microsoft がオープンソースで進めている .NET 開発者向け機械学習フレームワークです([GitHub](https://github.com/dotnet/machinelearning))。クロスプラットフォームで開発されており、Windows, Linux,  macOS で動作します。Windows Hello, Bing Ads PowerPoint デザインアイディアなどでも使われています。
類似のものとして、[Windows Machine Learning](https://docs.microsoft.com/en-us/windows/ai/) がありますが、こちらは Windows 10 以降の OS に特化したものとなります。API等に互換性はありません。

## .NET Core 版 ML.NET を Windows Forms から利用する
ML.NET はクロスプラットフォームを実現するため .NET Core で作成されています。.NET Core で作成した DLL は、.NET Framework で作成しているアプリケーションからは直接参照することはできませんが、.NET Standard ライブラリを作成し、その中で .NET Core を利用するコードを記述すれば、.NET Framework から参照できるようになります。~~今回は ML.NET 0.7.0 (.NET Core 2.0) を利用しているため、.NET Standard 2.0 / .NET Framework 4.6.1 以降が必要です。また、依存しているネイティブライブラリが x64 を対象としているため 64bit環境でのみ動作します。
なお、 2018年12月時点の最新版である ML.NET 0.8.0 は、.NET Core 2.1 ベースとなっており、.NET Framework から利用するためには、.NET Standard 2.1 が必要となりますが [未リリース](https://github.com/dotnet/standard/milestone/3)です。~~2019年5月時点の ML.NET 1.0 では .NET Core 2.1 ベースになっていますが、この記事の範囲内では .NET Core 2.0 でコンパイルしても動作するため、.NET Standard 2.0 でビルドしています。ビルドには .NET Framework 4.6.1 以降が必要です。また、依存しているネイティブライブラリが x64 を対象としているため 64bit環境でのみ動作します。

1. .NET Standard 2.0 のクラスライブラリのプロジェクトを作成し、nuget を利用して、必要なライブラリ(今回は、Microsoft.ML 1.0.0, Microsoft.ML.ImageAnalytics 1.0.0, Microsoft.ML.TensorFlow 0.12.0)を追加する
![image](https://qiita-image-store.s3.amazonaws.com/0/43450/ab9d826a-4d6b-ca79-a9ed-0d1383649e81.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43450/02181a77-661e-3fe3-b425-6f7b89df8158.png)
2. 1.のプロジェクトを .NET Framework のプロジェクトから参照する
3. .NET Framework のプロジェクトの .csproj を開き、```<RestoreProjectStyle>PackageReference</RestoreProjectStyle>``` を最初の ```<PropertyGroup>``` 内の先頭に追加する
![image](https://qiita-image-store.s3.amazonaws.com/0/43450/5cb3c7e9-173e-1612-357e-e067c6812165.png)
4. x64でビルドする

下記のようなメッセージが出てデバッグ実行に失敗する場合は、10秒くらい待ってもう一度実行してください。

> The OutputPath property is not set for project 'CoreReferenceForm.csproj'.  Please check to make sure that you have specified a valid combination of Configuration and Platform for this project.  Configuration='Debug'  Platform='x64'.  This error may also appear if some other project is trying to follow a project-to-project reference to this project, this project has been unloaded or is not included in the solution, and the referencing project does not build using the same or an equivalent Configuration or Platform.	CoreReferenceForm			

### .NET Framework 版 ML.NET
Windows プラットフォーム限定となりますが、ML.NET の .NET Framework 版も提供されています。.NET Framework プロジェクトから、上記と同様に利用することが可能です。

### 参考
* [.NET Standardなライブラリプロジェクトを作成して参照する](https://www.buildinsider.net/language/dotnetcore/05)
* [Referencing .NET Standard Assemblies from both .NET Core and .NET Framework](https://www.hanselman.com/blog/ReferencingNETStandardAssembliesFromBothNETCoreAndNETFramework.aspx)

## 実装について
ML.NETのサンプルにある [Image Classification](https://github.com/dotnet/machinelearning-samples/tree/master/samples/csharp/getting-started/DeepLearning_TensorFlowEstimator) をベースにしています。以下は説明のため部分的に抜粋しています。詳しくは [ソースコード](https://github.com/ksasao/ImageClassification)を参照してください。

### 学習
ML.NET では下記のようにパイプラインを構成していきます。

```csharp
var pipeline = mlContext.Transforms.Conversion.MapValueToKey(outputColumnName: LabelTokey, inputColumnName: "Label")
                .Append(mlContext.Transforms.LoadImages(outputColumnName: "input", imageFolder: imagesFolder, inputColumnName: nameof(ImageNetData.ImagePath)))
                .Append(mlContext.Transforms.ResizeImages(outputColumnName: "input", imageWidth: ImageNetSettings.imageWidth, imageHeight: ImageNetSettings.imageHeight, inputColumnName: "input"))
                .Append(mlContext.Transforms.ExtractPixels(outputColumnName: "input", interleavePixelColors: ImageNetSettings.channelsLast, offsetImage: ImageNetSettings.mean))
                .Append(mlContext.Model.LoadTensorFlowModel(featurizerModelLocation).
                     ScoreTensorFlowModel(outputColumnNames: new[] { "softmax2_pre_activation" }, inputColumnNames: new[] { "input" }, addBatchDimensionInput: true))
                .Append(mlContext.MulticlassClassification.Trainers.LbfgsMaximumEntropy(labelColumnName: LabelTokey, featureColumnName: "softmax2_pre_activation"))
                .Append(mlContext.Transforms.Conversion.MapKeyToValue(PredictedLabelValue, "PredictedLabel"))
                .AppendCacheCheckpoint(mlContext);
```
上記のコードでは

* 画像ファイルとその画像のラベル(クラス名; 日本語 (UTF-8) が利用可能)が記載された.tsvファイル読み込み
* 画像のリサイズ
* Tensorflowモデルの読み込みと特徴抽出に利用する層(softmax2_pre_activation)の指定
* [Stochastic Dual Coordinate Ascent(SDCA)](https://docs.microsoft.com/en-us/dotnet/api/microsoft.ml.trainers.sdcamulticlasstrainer?view=ml-dotnet) を利用した画像特徴の多クラス分類

といったような各処理をつないでいます。パイプラインの構成は、画像に限らず、自然言語処理や数値(CSVファイル)データの学習でも同様です。詳しくは、ML.NETの[チュートリアル](https://dotnet.microsoft.com/learn/machinelearning-ai/ml-dotnet-get-started-tutorial)や[サンプルコード](https://github.com/dotnet/machinelearning-samples)を参照してください。

パイプラインができたら、そこに学習データを渡して学習を行います。

```csharp
var data = mlContext.Data.LoadFromTextFile<ImageNetData>(path: dataLocation, hasHeader: false);
var model = pipeline.Fit(data);
 ```
 
つづいて、できたモデルを評価します。

```csharp
var classificationContext = mlContext.MulticlassClassification;
ConsoleWriteHeader("Classification metrics");
var metrics = classificationContext.Evaluate(trainData, labelColumnName: LabelTokey, predictedLabelColumnName: "PredictedLabel");
Console.WriteLine($"LogLoss is: {metrics.LogLoss}");
Console.WriteLine($"PerClassLogLoss is: {String.Join(" , ", metrics.PerClassLogLoss.Select(c => c.ToString()))}");
```
 
 モデルの保存も簡単です。

```csharp
mlContext.Model.Save(model, trainData.Schema, outputModelLocation);
```

### 推論
推論はモデルを読み込んで、画像ファイルを渡すだけです。作成したモデルにはラベル(クラス名)も含まれています。

```csharp
// モデル読み込み
loadedModel = mlContext.Model.Load(modelLocation, out var modelInputSchema);
var predictor = loadedModel.MakePredictionFunction<ImageNetData, ImageNetPrediction>(env);

// 画像読み込み
var predictor = mlContext.Model.CreatePredictionEngine<ImageNetData, ImageNetPrediction>(loadedModel);
var testData = ImageNetData.ReadImage(filename, "---");

ImageNetPrediction data = predictor.Predict(testData);
return data.PredictedLabelValue + " " + data.Score.Max();
```