commit 76072df047fca2168cf8ea05054a2276632d309c
Author: Roman Kalivoda <kalivoda@students.zcu.cz>
Date:   Thu Jun 10 19:01:36 2021 +0200

    Re #8597 Implementation of AbstractClassificationPredictor, SdcaMEClassifier

diff --git a/Server/ServerApp/Predictor/AbstractClassificationPredictor.cs b/Server/ServerApp/Predictor/AbstractClassificationPredictor.cs
new file mode 100644
index 0000000..7a46c49
--- /dev/null
+++ b/Server/ServerApp/Predictor/AbstractClassificationPredictor.cs
@@ -0,0 +1,80 @@
+﻿//
+// Author: Roman Kalivoda
+//
+
+using System;
+using System.Collections.Generic;
+using log4net;
+using Microsoft.ML;
+
+namespace ServerApp.Predictor
+{
+    abstract class AbstractClassificationPredictor : IPredictor
+    {
+        private static readonly ILog _log = LogManager.GetLogger(typeof(AbstractClassificationPredictor));
+
+        /// <summary>
+        /// Context of the ML.NET framework.
+        /// </summary>
+        protected MLContext _mlContext;
+
+        /// <summary>
+        /// Model instance
+        /// </summary>
+        protected ITransformer _trainedModel;
+
+        protected PredictionEngine<ModelInput, ModelOutput> _predictionEngine;
+
+        protected IDataView _trainingDataView;
+
+        public void Evaluate(IEnumerable<ModelInput> modelInputs)
+        {
+            var testDataView = this._mlContext.Data.LoadFromEnumerable(modelInputs);
+            var data = _trainedModel.Transform(testDataView);
+            var testMetrics = _mlContext.MulticlassClassification.Evaluate(data);
+
+            Console.WriteLine($"*************************************************************************************************************");
+            Console.WriteLine($"*       Metrics for Multi-class Classification model - Test Data     ");
+            Console.WriteLine($"*------------------------------------------------------------------------------------------------------------");
+            Console.WriteLine($"*       MicroAccuracy:    {testMetrics.MicroAccuracy:0.###}");
+            Console.WriteLine($"*       MacroAccuracy:    {testMetrics.MacroAccuracy:0.###}");
+            Console.WriteLine($"*       LogLoss:          {testMetrics.LogLoss:#.###}");
+            Console.WriteLine($"*       LogLossReduction: {testMetrics.LogLossReduction:#.###}");
+            Console.WriteLine($"*       Confusion Matrix: {testMetrics.ConfusionMatrix.GetFormattedConfusionTable()}");
+            Console.WriteLine($"*************************************************************************************************************");
+        }
+
+        public abstract void Fit(IEnumerable<ModelInput> trainInput);
+
+        public void Load(string filename)
+        {
+            DataViewSchema modelSchema;
+            this._trainedModel = _mlContext.Model.Load(filename, out modelSchema);
+            // TODO check if the loaded model has valid input and output schema
+            this._predictionEngine = _mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(this._trainedModel);
+        }
+
+        public string Predict(ModelInput input)
+        {
+            _log.Debug($"Predicting for input: {input}");
+            return this._predictionEngine.Predict(input).PredictedLabel;
+        }
+
+        public void Save(string filename)
+        {
+            if (this._trainingDataView is null)
+            {
+                throw new NullReferenceException("DataView is not set.");
+            }
+            if (this._trainedModel is null)
+            {
+                throw new NullReferenceException("Trained model instance does not exist. This predictor has not been trained yet.");
+            }
+            if (filename is null)
+            {
+                throw new ArgumentNullException(nameof(filename));
+            }
+            this._mlContext.Model.Save(this._trainedModel, this._trainingDataView.Schema, filename);
+        }
+    }
+}
diff --git a/Server/ServerApp/Predictor/IPredictor.cs b/Server/ServerApp/Predictor/IPredictor.cs
index 13bd67e..f752cf1 100644
--- a/Server/ServerApp/Predictor/IPredictor.cs
+++ b/Server/ServerApp/Predictor/IPredictor.cs
@@ -42,5 +42,12 @@ namespace ServerApp.Predictor
         /// </summary>
         /// <param name="filename">Path of the file.</param>
         public void Save(string filename);
+
+        /// <summary>
+        /// Loads an IPredictor model from file
+        /// </summary>
+        /// <param name="filename">Path to the model file.</param>
+        /// <returns>A prediction model.</returns>
+        public void Load(string filename);
     }
 }
diff --git a/Server/ServerApp/Predictor/NaiveBayesClassifier.cs b/Server/ServerApp/Predictor/NaiveBayesClassifier.cs
index a63294e..c8b6610 100644
--- a/Server/ServerApp/Predictor/NaiveBayesClassifier.cs
+++ b/Server/ServerApp/Predictor/NaiveBayesClassifier.cs
@@ -13,30 +13,16 @@ namespace ServerApp.Predictor
     /// <summary>
     /// Implementation of the naive Bayes classifier in ML.NET.
     /// </summary>
-    class NaiveBayesClassifier : IPredictor
+    class NaiveBayesClassifier : AbstractClassificationPredictor
     {
         private static readonly ILog _log = LogManager.GetLogger(typeof(NaiveBayesClassifier));
 
-        /// <summary>
-        /// Context of the ML.NET framework.
-        /// </summary>
-        private MLContext _mlContext;
-
-        /// <summary>
-        /// Model instance
-        /// </summary>
-        private ITransformer _trainedModel;
-
-        private PredictionEngine<ModelInput, ModelOutput> _predictionEngine;
-
-        IDataView _trainingDataView;
-
         /// <summary>
         /// Instantiates new <c>MLContext</c>.
         /// </summary>
         public NaiveBayesClassifier()
         {
-            _mlContext = new MLContext();
+            this._mlContext = new MLContext();
         }
 
         public NaiveBayesClassifier(string filename) : this()
@@ -47,24 +33,7 @@ namespace ServerApp.Predictor
             this._predictionEngine = _mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(this._trainedModel);
         }
 
-        public void Save(string filename)
-        {
-            if (this._trainingDataView is null)
-            {
-                throw new NullReferenceException("DataView is not set.");
-            }
-            if( this._trainedModel is null)
-            {
-                throw new NullReferenceException("Trained model instance does not exist. This predictor has not been trained yet.");
-            }
-            if(filename is null)
-            {
-                throw new ArgumentNullException(nameof(filename));
-            }
-            this._mlContext.Model.Save(this._trainedModel, this._trainingDataView.Schema, filename);
-        }
-
-        public void Fit(IEnumerable<ModelInput> trainInput)
+        public override void Fit(IEnumerable<ModelInput> trainInput)
         {
             this._trainingDataView = _mlContext.Data.LoadFromEnumerable(trainInput);
             var pipeline = _mlContext.Transforms.Conversion.MapValueToKey(nameof(ModelInput.Label))
@@ -82,29 +51,6 @@ namespace ServerApp.Predictor
             _log.Info($"Selected the model #{cvResults.OrderByDescending(fold => fold.Metrics.MicroAccuracy).Select(fold => fold.Fold).First()} as the best.");
             this._predictionEngine = _mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(this._trainedModel);
 
-        }
-
-        public string Predict(ModelInput input)
-        {
-            _log.Debug($"Predicting for input: {input}");
-            return this._predictionEngine.Predict(input).PredictedLabel;
-        }
-
-        public void Evaluate(IEnumerable<ModelInput> modelInputs)
-        {
-            var testDataView = this._mlContext.Data.LoadFromEnumerable(modelInputs);
-            var data = _trainedModel.Transform(testDataView);
-            var testMetrics = _mlContext.MulticlassClassification.Evaluate(data);
-
-            Console.WriteLine($"*************************************************************************************************************");
-            Console.WriteLine($"*       Metrics for Multi-class Classification model - Test Data     ");
-            Console.WriteLine($"*------------------------------------------------------------------------------------------------------------");
-            Console.WriteLine($"*       MicroAccuracy:    {testMetrics.MicroAccuracy:0.###}");
-            Console.WriteLine($"*       MacroAccuracy:    {testMetrics.MacroAccuracy:0.###}");
-            Console.WriteLine($"*       LogLoss:          {testMetrics.LogLoss:#.###}");
-            Console.WriteLine($"*       LogLossReduction: {testMetrics.LogLossReduction:#.###}");
-            Console.WriteLine($"*       Confusion Matrix: {testMetrics.ConfusionMatrix.GetFormattedConfusionTable()}");
-            Console.WriteLine($"*************************************************************************************************************");
-        }
+        }    
     }
 }
diff --git a/Server/ServerApp/Predictor/PredictionController.cs b/Server/ServerApp/Predictor/PredictionController.cs
index 59e6a88..3fce1be 100644
--- a/Server/ServerApp/Predictor/PredictionController.cs
+++ b/Server/ServerApp/Predictor/PredictionController.cs
@@ -93,24 +93,32 @@ namespace ServerApp.Predictor
             this.FeatureExtractor = new FeatureExtractor(this.DataParser, this.Configuration);
 
             DirectoryInfo di = new DirectoryInfo(Configuration.ModelDataPath);
+            _log.Debug($"Looking for directory: {Configuration.ModelDataPath}");
+            if (! di.Exists)
+            {
+                _log.Info($"Creating the model data directory: {Configuration.ModelDataPath}");
+                di.Create();
+            }
             FileInfo[] files = di.GetFiles();
-            if (Array.FindAll(files, f => Regex.IsMatch(f.Name, @"[-]?\d+_\d+.zip")).GroupBy(f => f.Name.Split("_.".ToCharArray())[0]).OrderBy(f => DateTime.FromBinary(Convert.ToInt64(f.Key))).Any())
+            if (Array.FindAll(files, f => Regex.IsMatch(f.Name, @"[-]?\d+_\d+.zip")).GroupBy(f => f.Name.Split("_.".ToCharArray())[0]).OrderBy(f => DateTime.FromBinary(Convert.ToInt64(f.Key))).Count() == Configuration.PredictorCount)
             {
                 _log.Info("Found existing predictors, loading the newest.");
-                this.Load(Array.FindAll(files, f => Regex.IsMatch(f.Name, @"[-]?\d+_\d+.zip")).GroupBy(f => f.Name.Split("_.".ToCharArray())[0]).OrderBy(f => DateTime.FromBinary(Convert.ToInt64(f.Key))).Last().Select(f => f.Name.Split("_".ToCharArray())[0]).First());
+                var predictorID = Array.FindAll(files, f => Regex.IsMatch(f.Name, @"[-]?\d+_\d+.zip")).GroupBy(f => f.Name.Split("_.".ToCharArray())[0]).OrderBy(f => DateTime.FromBinary(Convert.ToInt64(f.Key))).Last().Select(f => f.Name.Split("_".ToCharArray())[0]).First();
+                this.Load(predictorID);
             }
             else
             {
                 _log.Info("No predictors found, creating new ones");
                 for (int i = 0; i < this.Configuration.PredictorCount; i++)
                 {
-                    predictors[i] = new NaiveBayesClassifier();
+                    predictors[i] = new SdcaMaximumEntropyClassifier();
+                }
+                lock (predictorsLock)
+                {
+                    this.Predictors = predictors;
                 }
             }
-            lock (predictorsLock)
-            {
-                this.Predictors = predictors;
-            }
+            
             PredictorConfiguration.SaveConfig(PredictorConfiguration.DEFAULT_CONFIG_PATH, Configuration);
         }
         public List<string> GetPredictors()
@@ -120,7 +128,12 @@ namespace ServerApp.Predictor
 
         public int Load(string predictorID)
         {
-            DirectoryInfo di = new DirectoryInfo(Configuration.ModelDataPath);
+            DirectoryInfo di = new DirectoryInfo(Configuration.ModelDataPath); 
+            if (!di.Exists)
+            {
+                _log.Warn("The model data directory could not be found.");
+                return 2;
+            }
             FileInfo[] files = di.GetFiles($"{predictorID}_*.zip");
             if (Array.FindAll(files, f => Regex.IsMatch(f.Name, $@"{predictorID}_\d+.zip")).Any()){
                 IPredictor[] newPredictors = new IPredictor[this.Configuration.PredictorCount];
@@ -128,7 +141,7 @@ namespace ServerApp.Predictor
                 {
                     for (int i = 0; i < this.Configuration.PredictorCount; i++)
                     {
-                        newPredictors[i] = new NaiveBayesClassifier(Array.Find(files, f => Regex.IsMatch(f.Name, $"{predictorID}_{i}.zip")).FullName);
+                        newPredictors[i] = new SdcaMaximumEntropyClassifier(Array.Find(files, f => Regex.IsMatch(f.Name, $"{predictorID}_{i}.zip")).FullName);
                     }
                     files = di.GetFiles($"{predictorID}.txt");
                     var dataFilenames = File.ReadLines(files[0].FullName); 
@@ -154,6 +167,11 @@ namespace ServerApp.Predictor
         public void Save()
         {
             DirectoryInfo di = new DirectoryInfo(Configuration.ModelDataPath);
+            if (!di.Exists)
+            {
+                _log.Warn("The model data directory could not be found.");
+                return;
+            }
 
             lock (predictorsLock)
             {
@@ -168,8 +186,13 @@ namespace ServerApp.Predictor
         public int Rollback()
         {
             DirectoryInfo di = new DirectoryInfo(Configuration.ModelDataPath);
+            if (!di.Exists)
+            {
+                _log.Warn("The model data directory could not be found.");
+                return 2;
+            }
             FileInfo[] files = di.GetFiles();
-            if (Array.FindAll(files, f => Regex.IsMatch(f.Name, @"[-]?\d+_\d+.zip")).GroupBy(f => f.Name.Split("_.".ToCharArray())[0]).OrderBy(f => DateTime.FromBinary(Convert.ToInt64(f.Key))).Any())
+            if (Array.FindAll(files, f => Regex.IsMatch(f.Name, @"[-]?\d+_\d+.zip")).GroupBy(f => f.Name.Split("_.".ToCharArray())[0]).OrderBy(f => DateTime.FromBinary(Convert.ToInt64(f.Key))).Count() == Configuration.PredictorCount)
             {
                 string RollbackedPredictorID = Array.FindAll(files, f => Regex.IsMatch(f.Name, @"[-]?\d+_\d+.zip")).GroupBy(f => f.Name.Split("_.".ToCharArray())[0]).OrderBy(f => DateTime.FromBinary(Convert.ToInt64(f.Key))).Last().Select(f => f.Name.Split("_".ToCharArray())[0]).First();
                 this.Delete(this.PredictorID);
@@ -184,6 +207,11 @@ namespace ServerApp.Predictor
         private void Delete(string predictorID)
         {
             DirectoryInfo di = new DirectoryInfo(Configuration.ModelDataPath);
+            if (!di.Exists)
+            {
+                _log.Warn("The model data directory could not be found.");
+                return;
+            }
 
             for (int i = 0; i < this.Configuration.PredictorCount; i++)
             {
diff --git a/Server/ServerApp/Predictor/SdcaMaximumEntropyClassifier.cs b/Server/ServerApp/Predictor/SdcaMaximumEntropyClassifier.cs
new file mode 100644
index 0000000..702c14d
--- /dev/null
+++ b/Server/ServerApp/Predictor/SdcaMaximumEntropyClassifier.cs
@@ -0,0 +1,60 @@
+﻿//
+// Author: Roman Kalivoda
+//
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using log4net;
+using Microsoft.ML;
+
+namespace ServerApp.Predictor
+{
+    /// <summary>
+    /// Implementation of the LightGBM classifier in ML.NET.
+    /// </summary>
+    class SdcaMaximumEntropyClassifier : AbstractClassificationPredictor
+    {
+        private static readonly ILog _log = LogManager.GetLogger(typeof(SdcaMaximumEntropyClassifier));
+
+        /// <summary>
+        /// Instantiates new <c>MLContext</c>.
+        /// </summary>
+        public SdcaMaximumEntropyClassifier()
+        {
+            this._mlContext = new MLContext();
+        }
+
+        /// <summary>
+        /// Loads existing model from file
+        /// </summary>
+        /// <param name="filename">path to the model file.</param>
+        public SdcaMaximumEntropyClassifier(string filename) : this()
+        {
+            DataViewSchema modelSchema;
+            this._trainedModel = _mlContext.Model.Load(filename, out modelSchema);
+            // TODO check if the loaded model has valid input and output schema
+            this._predictionEngine = _mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(this._trainedModel);
+        }
+
+        public override void Fit(IEnumerable<ModelInput> trainInput)
+        {
+            this._trainingDataView = _mlContext.Data.LoadFromEnumerable(trainInput);
+            var pipeline = _mlContext.Transforms.Conversion.MapValueToKey(nameof(ModelInput.Label))
+                .Append(_mlContext.Transforms.Conversion.ConvertType(nameof(ModelInput.Hour)))
+                .Append(_mlContext.Transforms.Concatenate("Features", 
+                new[] { nameof(ModelInput.Temp), nameof(ModelInput.Rain), nameof(ModelInput.Wind), nameof(ModelInput.Hour) }))
+                .Append(_mlContext.Transforms.NormalizeMeanVariance("Features", useCdf:false))
+                .AppendCacheCheckpoint(_mlContext)
+                .Append(_mlContext.MulticlassClassification.Trainers.SdcaMaximumEntropy())
+                .Append(_mlContext.Transforms.Conversion.MapKeyToValue(nameof(ModelOutput.PredictedLabel)));
+
+            var cvResults = _mlContext.MulticlassClassification.CrossValidate(this._trainingDataView, pipeline);
+            _log.Debug("Cross-validated the trained model");
+            this._trainedModel = cvResults.OrderByDescending(fold => fold.Metrics.MicroAccuracy).Select(fold => fold.Model).First();
+            _log.Info($"Selected the model #{cvResults.OrderByDescending(fold => fold.Metrics.MicroAccuracy).Select(fold => fold.Fold).First()} as the best.");
+            this._predictionEngine = _mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(this._trainedModel);
+
+        }    
+    }
+}
diff --git a/Server/ServerApp/Program.cs b/Server/ServerApp/Program.cs
index db25f2f..0ab5455 100644
--- a/Server/ServerApp/Program.cs
+++ b/Server/ServerApp/Program.cs
@@ -50,7 +50,7 @@ namespace ServerApp
 			DataDownloader dd = DataDownloadAndRetrievalTest(config);
 
 			WeatherAsStringTest(dd);
-			Console.ReadLine();
+			//Console.ReadLine();
 
 			// xml building test
 			//XMLTest();
@@ -73,10 +73,10 @@ namespace ServerApp
 			Thread inputThread = new Thread(ca.AcceptCommand);
 			inputThread.Start();
 
-			// connection test
-			//ConnectionTest(controller, config);
+            // connection test
+            ConnectionTest(controller, config);
 
-			Console.ReadLine();
+            //Console.ReadLine();
         }
 
 
