ML.NETのチュートリアルをやってみた

この記事は、[C# Advent Calendar 2018](https://qiita.com/advent-calendar/2018/c-sharp) の11日目の記事です。

初めてAdvent Calendarに参加します。
ASP.NET関係で何か書こうと思いましたが、記事が書けなかったので、[ML.NETのチュートリアル](https://docs.microsoft.com/ja-jp/dotnet/machine-learning/tutorials/)をやってみました。

## ML.NETとは

ML.NETはMicrosoftが公開しているオープンソースかつクロスプラットフォームな機械学習用のライブラリです。

既存の.NETアプリに機械学習の機能をつけるなど.NETから離れることなく利用できるのが利点です。

チュートリアルには以下の代表的なアルゴリズムについてアプリケーションに統合する方法があります。

- 二項分類
- 回帰
- クラスタリング

## 二項分類をやってみる

### 開発に必要なもの

ML.NETを使ったアプリケーションの開発を行う際に必要なものは以下の通りです。

1. Visual Studio 2017 15.6以降（.NET Core クロスプラットフォーム開発がインストール済）
1. 分析するためのデータ（今回はチュートリアルからダウンロード）

### コンソールアプリケーションを作成

1. 「コンソールアプリ（.NET Core）」のプロジェクトテンプレートを選択し、プロジェクト名として「SentimantAnalysis」を設定

1. プロジェクト直下に「Data」フォルダを作成

1. Nugetから「Microsoft.ML」を検索し、追加する

    現時点の最新版は「0.8.0」です。

### データを準備する

データはGithubの[dotnet/machinelearning](https://github.com/dotnet/machinelearning)からとってきます。

今回、2種類のデータを使います。

1つ目は学習用の教師データ[wikipedia-detox-250-line-data.tsv](https://github.com/dotnet/machinelearning/blob/master/test/data/wikipedia-detox-250-line-data.tsv)
です。

2つ目は実際に評価用するときのデータ[wikipedia-detox-250-line-test.tsv](https://github.com/dotnet/machinelearning/blob/master/test/data/wikipedia-detox-250-line-test.tsv)になります。

それぞれをDataフォルダに置き、「出力ディレクトリにコピー」の値を「新しい場合はコピーする」に変更します。

### プログラムを作成する

以下のようなプログラムを作成

Main.csは以下のようになります。

``` C#
using Microsoft.ML;
using Microsoft.ML.Core.Data;
using Microsoft.ML.Runtime.Api;
using Microsoft.ML.Runtime.Data;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace SentimentAnalysis
{
    class Program
    {
        static readonly string _trainDataPath = Path.Combine(Environment.CurrentDirectory, "Data", "wikipedia-detox-250-line-data.tsv");
        static readonly string _testDataPath = Path.Combine(Environment.CurrentDirectory, "Data", "wikipedia-detox-250-line-test.tsv");
        static readonly string _modelPath = Path.Combine(Environment.CurrentDirectory, "Data", "Model.zip");
        static TextLoader _textLoader;
        static async Task Main(string[] args)
        {
            MLContext mlContext = new MLContext(seed:0);
            _textLoader = mlContext.Data.TextReader(new TextLoader.Arguments()
            {
                Separator = "tab",
                HasHeader = true,
                Column = new[]
                {
                    new TextLoader.Column("Label", DataKind.Bool, 0),
                    new TextLoader.Column("SentimentText", DataKind.Text, 1)
                }
            });
            var model = Train(mlContext, _trainDataPath);
            Evaluate(mlContext, model);
            Predict(mlContext, model);
            PredictWithModelLoadedFromFile(mlContext);
            Console.WriteLine();

            Console.WriteLine("=============== End of process ===============");
            Console.ReadKey();
        }


        public static ITransformer Train(MLContext mlContext, string dataPath)
        {
            IDataView dataView = _textLoader.Read(dataPath);
            var pipeline = mlContext.Transforms.Text.FeaturizeText("SentimentText", "Features").Append(mlContext.BinaryClassification.Trainers.FastTree(numLeaves: 50, numTrees: 50, minDatapointsInLeaves: 20));
            Console.WriteLine("=============== Create and Train the Model ===============");
            var model = pipeline.Fit(dataView);
            Console.WriteLine("=============== End of training ===============");
            Console.WriteLine();

            return model;
        }

        public static void Evaluate(MLContext mlContext, ITransformer model)
        {
            IDataView dataView = _textLoader.Read(_testDataPath);
            Console.WriteLine("=============== Evaluating Model accuracy with Test data===============");
            var predictions = model.Transform(dataView);
            var metrics = mlContext.BinaryClassification.Evaluate(predictions, "Label");
            Console.WriteLine();
            Console.WriteLine("Model quality metrics evaluation");
            Console.WriteLine("--------------------------------");
            Console.WriteLine($"Accuracy: {metrics.Accuracy:P2}");
            Console.WriteLine($"Auc: {metrics.Auc:P2}");
            Console.WriteLine($"F1Score: {metrics.F1Score:P2}");
            Console.WriteLine("=============== End of model evaluation ===============");
            SaveModelAsFile(mlContext, model);
        }

        public static void Predict(MLContext mlContext, ITransformer model)
        {
            var predictionFunction = model.MakePredictionFunction<SentimentData, SentimentPrediction>(mlContext);

            SentimentData sampleStatement = new SentimentData
            {
                SentimentText = "This is a very rude movie"
            };

            var resultprediction = predictionFunction.Predict(sampleStatement);
            Console.WriteLine();
            Console.WriteLine("=============== Prediction Test of model with a single sample and test dataset ===============");

            Console.WriteLine();
            Console.WriteLine($"Sentiment: {sampleStatement.SentimentText} | Prediction: {(Convert.ToBoolean(resultprediction.Prediction) ? "Toxic" : "Not Toxic")} | Probability: {resultprediction.Probability} ");
            Console.WriteLine("=============== End of Predictions ===============");
            Console.WriteLine();


        }

        private static void PredictWithModelLoadedFromFile(MLContext mlContext)
        {
            IEnumerable<SentimentData> sentiments = new[]
            {
                new SentimentData
                {
                    SentimentText = "This is a very rude movie"
                },
                new SentimentData
                {

                    SentimentText = "He is the best, and the article should say that."
                }
            };
            ITransformer loadedModel;
            using (var stream = new FileStream(_modelPath, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                loadedModel = mlContext.Model.Load(stream);
            }
            var sentimentStreamingDataView = mlContext.CreateStreamingDataView(sentiments);
            var predictions = loadedModel.Transform(sentimentStreamingDataView);
            var predictedResults = predictions.AsEnumerable<SentimentPrediction>(mlContext, reuseRowObject: false);
            Console.WriteLine();
            Console.WriteLine("=============== Prediction Test of loaded model with a multiple samples ===============");

            Console.WriteLine();

            var sentimentsAndPredictions = sentiments.Zip(predictedResults, (sentiment, prediction) => (sentiment, prediction));

            foreach (var item in sentimentsAndPredictions)
            {
                Console.WriteLine(
                    $"Sentiment: {item.sentiment.SentimentText} | Prediction: {(Convert.ToBoolean(item.prediction.Prediction) ? "Toxic" : "Not Toxic")} | Probability: {item.prediction.Probability} ");

            }

            Console.WriteLine("=============== End of predictions ===============");
        }

        private static void SaveModelAsFile(MLContext mlContext, ITransformer model)
        {
            using (var fs = new FileStream(_modelPath, FileMode.Create, FileAccess.Write, FileShare.Write))
            {
                mlContext.Model.Save(model, fs);
            }

            Console.WriteLine("The model is saved to {0}", _modelPath);
        }
    }
}

```

ファイルから読み取ったデータを格納するクラス（SentimentData.cs）

``` C#
using Microsoft.ML.Runtime.Api;

namespace SentimentAnalysis
{
    public class SentimentData
    {
        [Column(ordinal: "0", name: "Label")]
        public float Sentiment;
        [Column(ordinal: "1")]
        public string SentimentText;
    }

    public class SentimentPrediction
    {
        [ColumnName("PredictedLabel")]
        public bool Prediction { get; set; }

        [ColumnName("Probability")]
        public float Probability { get; set; }

        [ColumnName("Score")]
        public float Score { get; set; }
    }
}

```

### 実行してみる

出来上がったプログラムを実行すると以下のような結果が返ってきます。

```
=============== Create and Train the Model ===============
=============== End of training ===============

=============== Evaluating Model accuracy with Test data===============

Model quality metrics evaluation
--------------------------------
Accuracy: 94.44%
Auc: 98.77%
F1Score: 94.74%
=============== End of model evaluation ===============
The model is saved to G:\workspace\SentimentAnalysis\SentimentAnalysis\bin\Debug\netcoreapp2.1\Data\Model.zip

=============== Prediction Test of model with a single sample and test dataset ===============

Sentiment: This is a very rude movie | Prediction: Toxic | Probability: 0.5297049
=============== End of Predictions ===============


=============== Prediction Test of loaded model with a multiple samples ===============

Sentiment: This is a very rude movie | Prediction: Toxic | Probability: 0.5297049
Sentiment: He is the best, and the article should say that. | Prediction: Toxic | Probability: 0.9918675
=============== End of predictions ===============

=============== End of process ===============
```

## やってみて

チュートリアルをやってみるとDocsに記載されているのと動作するのとで大きく違いがあった。

ML.NETはまだまだドキュメントが整理されていないところがあるので、試してみるときはGithubのサンプルを確認してみる必要がありました。

## 参考

- [ML.NETコンテンツガイド](https://docs.microsoft.com/ja-jp/dotnet/machine-learning/)
- [チュートリアル: センチメント分析のバイナリ分類のシナリオで ML.NET を使用する](https://docs.microsoft.com/ja-jp/dotnet/machine-learning/tutorials/sentiment-analysis)
- [チュートリアルのサンプルコード](https://github.com/dotnet/samples/tree/master/machine-learning/tutorials/SentimentAnalysis)