Machine Learning in .NET with F# and ML.NET 2.0
Tags: mlnet, fsharp, dotnet, machinelearning
Published: 2022-12-30 03:06:17

---
title: Machine Learning in .NET with F# and ML.NET 2.0
published: True
description: Let's build a simple machine learning training pipeline using ML.NET 2.0 and Polyglot Notebooks to explore machine learning in .NET with F#
tags: [MLNET, FSharp, Dotnet, MachineLearning]
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rpw1ymp6z4h3m3vmn098.png
canonical_url: https://accessibleai.dev/post/mlnet_fsharp_regression/
# Use a ratio of 100:42 for best results.
# published_at: 2022-12-30 02:57 +0000
---
_Cover image created by [Matt Eland](https://MattEland.dev) using MidJourney_

Machine learning is awesome, and [ML.NET](https://dotnet.microsoft.com/en-us/apps/machinelearning-ai/ml-dotnet) is a fantastic open-source library that lets us write dotnet code to perform machine learning tasks in our production applications. However, there aren't as many articles walking you through how to do machine learning using either F# or Polyglot Notebooks, so this article aims to fix that.

In this article I'll outline a simple pipeline that trains a regression machine learning model and saves it to a file for use later on. We'll look at how to load the model using F# and use it to generate new predictions for new data points.

To round things out, I'll be showing you how to do this all in a Polyglot Notebook, though you can skim over this aspect of the experiment as almost all of the code will work just fine in a normal `.fs` file outside of Polyglot Notebooks.

_Note: This article was intended for the [F# Advent 2022](https://sergeytihon.com/2022/10/28/f-advent-calendar-in-english-2022/) series of articles for an audience familiar with F#. If you're not familiar with F#, it's a functional programming language that's fully part of the dotnet family of languages. If you want some C# materials, see [my other content on ML.NET](https://accessibleai.dev/tags/ml.net/) for more content._

## What are Polyglot Notebooks?

Polyglot Notebooks are the new name of Microsoft's .NET Interactive Notebooks project. This project adds a new kernel to [Jupyter Notebooks](https://jupyter.org/) and allows you to write F#, C#, HTML, SQL, and other languages in an interactive data science notebook.

This allows you to write quick data science experiments without needing a full F# project. This also allows you to mix together F# code and Markdown code as well as see intermediate data values between cells.

The end result is a lightweight development experience that mixes rich markdown documentation, editable code, and inline results. This allows you to easily share small experiments with your team and allows for them to easily execute that code, make tweaks, and investigate the results on their own.

The rest of this article will focus on the ML.NET specific aspects of Machine Learning using F#. If you're curious about Polyglot Notebooks in more depth, or want detailed instructions for installing it, [check out my article and video on Polyglot Notebooks for C#](https://newdevsguide.com/2022/12/14/polyglot-notebooks-csharp/).

## What is ML.NET?

ML.NET allows you to train and use machine learning models on the dotnet platform. ML.NET is open-source and cross-platform. ML.NET is actively maintained and growing with the major ML.NET 2.0 release occurring just recently in late 2022. What's more, ML.NET is heading in incredibly exciting directions with many deep learning features on its horizon.

While the majority of the data science community does currently use Python for machine learning tasks, ML.NET exists for two major purposes:

- First, some developers prefer to write F# or C# code to Python code, and ML.NET lets you train models in dotnet
- Secondly, ML.NET supports [Open Neural Network eXchange (ONNX)](https://onnx.ai/) which allows cross-platform and cross-language model sharing. This lets you train a model in Python and deploy it using dotnet (or vice versa)

### Referencing ML.NET

Before we can do anything with ML.NET, we'll need to add a reference to that package.

In Polyglot Notebooks you can do this with the following F# code cell:

```fsharp
// Install the Microsoft.ML packages we're going to use for ML.NET
#r "nuget:Microsoft.ML"
```

Once you run this, the Notebook will download the relevant package and let you know it installed `Microsoft.ML` and the relevant version number (2.0.0 at time of writing this article).

![Installing the NuGet Package](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k3yxu86tzvb5vw0mc3fe.png)

If you are working in Visual Studio, you can use NuGet Package Manager to add the reference manually by searching for "Microsoft.ML" and then installing that dependency.

You can also run the following command in the .NET CLI: `dotnet add package Microsoft.ML`

### Working with F# Namespaces

Once you have ML.NET added to your project or notebook, add a notebook cell that uses the `open` keyword for a few namespaces:

```fsharp
open Microsoft.ML;
open Microsoft.ML.Data;
```

This allows us to use ML.NET types from F# in Polyglot Notebooks without needing to use fully-qualified names.

_Note: This could also be part of another cell if you want it to be. I tend to have my NuGet statements in a cell of their own and my `open` statements in their own cells since I tend to rapidly iterate between different cells and the NuGet steps take longer than other steps._

## Retrieving a Data Source

The exact experiment we'll be doing in this project is to take historical weather data for London and use it to predict rainfall in the future. The exact data source comes from [Kaggle.com](https://www.kaggle.com/datasets/emmanuelfwerr/london-weather-data) and is available for download and inspection.

The dataset includes the following columns:

> - **date** - recorded date of measurement - (int)
> - **cloud_cover** - cloud cover measurement in oktas - (float)
> - **sunshine** - sunshine measurement in hours (hrs) - (float)
> - **global_radiation** - irradiance measurement in Watt per square meter (W/m2) - (float)
> - **max_temp** - maximum temperature recorded in degrees Celsius (°C) - (float)
> - **mean_temp** - mean temperature in degrees Celsius (°C) - (float)
> - **min_temp** - minimum temperature recorded in degrees Celsius (°C) - (float)
> - **precipitation** - precipitation measurement in millimeters (mm) - (float)
> - **pressure** - pressure measurement in Pascals (Pa) - (float)
> - **snow_depth** - snow depth measurement in centimeters (cm) - (float)
> 
> Dataset and column descriptions from [Emmanuel F. Werr](https://www.kaggle.com/datasets/emmanuelfwerr/london-weather-data)

The first step you need to do is download this `London_Weather.csv` file and place it inside the same directory as your Jupyter Notebook.

If you are using code outside of a Jupyter Notebook, make note of the location on disk you save the file and provide that to your F# code instead in future steps.

### Representing Data in F# for ML.NET Input

In ML.NET we use a DataFrame object called an `IDataView` to store and manipulate data. This is similar to a Pandas DataFrame for those with a Python background.

One option for loading an `IDataView` involves declaring the type that each row will contain.

Here is an F# cell that defines a new `WeatherInput` record that can store the contents we care about for any given row.

```fsharp
type WeatherInput =
    {
        // Ignore date column
        [<LoadColumn(1)>]
        CloudCover: Single // in oktas
        [<LoadColumn(2)>]
        Sunshine: Single // in hours
        [<LoadColumn(3)>]
        GlobalRads: Single // Global radiation in Watt / square meter
        [<LoadColumn(4)>]
        MaxTemp: Single // Celsius
        [<LoadColumn(5)>]
        MeanTemp: Single // Celsius
        [<LoadColumn(6)>]
        MinTemp: Single // Celsius
        [<LoadColumn(7)>]
        Precipitation: Single // This is the precipitation in millimeters. This is what we want to predict
        [<LoadColumn(8)>]
        Pressure: Single // In Pascals
        // Ignore snow depth since that won't be known at time of prediction and is related to Precipitation
    }
```

This `WeatherInput` type will also be used later on for generating predictions from our trained model.

You likely noted the `[<LoadColumn(1)>]` syntax. This is a F# _Attribute_ which tells ML.NET the zero-based index of the column in the data source the property should map to. This is maybe my least favorite part of ML.NET code, though ML.NET does offer some alternatives to infer column types if you prefer. However, I'm covering the basic ML.NET syntax in this article.

One particular thing to note is that these columns are all `Single` or `Float32` values. ML.NET is very particular on the data types you give it and it doesn't like `double` values in particular, so this data choice is deliberate to avoid errors later on in the process.

Finally, note that I did not choose to represent the Date or SnowDepth columns since the Date column was one that I didn't want the model training process to be aware of. I chose to omit the depth of snow on that day because if I included that value, we'd need to provide it when generating rainfall predictions for the next day. Because we can't know the snowfall accurately in advance and it is tightly correlated to precipitation, I felt it would be most useful for the model training and prediction process to not know about it.

### Loading Data into an IDataView

Now that we have our data type declared, loading it into an `IDataView` can be done with a trivial amount of code:

```fsharp
// Create an ML Context. We'll use this for all ML operations
let context = MLContext();

// Load the data
let data = context.Data.LoadFromTextFile<WeatherInput>("london_weather.csv", hasHeader = true, separatorChar=',')
data.Schema
```

The first thing we do is create a new `MLContext` instance. Almost everything in ML.NET flows through a `MLContext` instance, so we need one going forward.

Next we load the data from a file using this context instance. ML.NET lets us use `hasHeader` to indicate that the  first row is just a header and should not be treated as data. We also must specify the comma delimiter via the `separatorChar` parameter in order to have rows loaded accurately.

Finally, the last line of code is just `data.Schema` which grabs the `Schema` property of the `IDataView`. We do this to visualize the loaded schema via Polyglot Notebooks. Polyglot Notebooks lets you specify values on the last line of any code cell and will automatically log out a display of that object below it as shown below:

![Viewing the Schema of the Loaded Data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9fuxlddxz0wqkfkc34zc.png)

If you wanted to proof your loaded data to make sure it was properly loaded, you could take a look at the first 3 rows using the `Preview` function and the `RowView` property of that preview:

```fsharp
let numRows = 3
data.Preview(numRows).RowView
```

This tip will save you a lot of issues. During the writing of this article, for example, I encountered a few issues causing rows to be loaded with all 0's for data and I hadn't realized it because I didn't preview my data.

![Viewing Loaded Data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1y4g7wro2j2p3v0x0e7q.png)
### Splitting the Data into a Train and Test Split

In Machine Learning it is customary to split your data into training and test splits in order to detect _overfit_ models that have memorized the training data but don't perform well against new data.

F# lets us split any `IDataView` into two parts using the `TrainTestSplit` method as shown in the code cell below:

```fsharp
// Split into train and test splits to detect overfitting
let split = context.Data.TrainTestSplit(data, testFraction = 0.2)

let testSet = split.TestSet   // 20 % of the data (0.2)
let trainSet = split.TrainSet // 80 % of the data (0.8)
```

If you wanted to use a different allocation of percentages with your train / test split, just tweak `testFraction` to be something else. However, values between 0.1 and 0.3 are recommended.

_Note: You don't have to split your data this way. You could also load two separate `IDataView` instances using LoadFromTextFile as shown above. Alternatively, you could give **all** of your training data to the model training step later on, but this is not recommended. The lack of a test dataset makes it harder to get accurate metrics of how your model performs against new data._

## Training a Regression Model in F# using ML.NET

Now that you have your training and test data loaded, we'll need to build out a featurization pipeline, select a model training algorithm, fit that algorithm to the training data, and evaluate its metrics.

### Creating a Featurization Pipeline

The next part is maybe the most intimidating part of learning ML.NET: creating a processing pipeline.

This pipeline is a sequence of discrete steps that take in input data and form it in a standardized way that can be interpreted by a machine learning model training process.

In order to train a model effectively, we'll need to do the following things:

- Ensure the value we're trying to predict is in a column called "Label"
- Ensure the features that contribute to that prediction are aggregated into a column called "Features"
- Normalize values between different columns so they use a standard scale
- Cache data transformations for more accurate and performant model fitting

Note that the normalization step is particularly important when different columns have different scales so all have an equal chance at impacting the trained model. In our data set we have columns in degrees Celsius, Watts, Pascals, and even hours.

We can represent this processing pipeline in F# code with ML.NET with the following code:

```fsharp
let featureArray = [|"CloudCover";"Sunshine";"GlobalRads";"MaxTemp";"MeanTemp";"MinTemp";"Pressure"|]

let processPipeline = EstimatorChain()
                        .Append(context.Transforms.CopyColumns("Label", "Precipitation"))
                        .Append(context.Transforms.NormalizeMeanVariance("CloudCover", "CloudCover"))
                        .Append(context.Transforms.NormalizeMeanVariance("Sunshine", "Sunshine"))
                        .Append(context.Transforms.NormalizeMeanVariance("GlobalRads", "GlobalRads"))
                        .Append(context.Transforms.NormalizeMeanVariance("MaxTemp", "MaxTemp"))
                        .Append(context.Transforms.NormalizeMeanVariance("MeanTemp", "MeanTemp"))
                        .Append(context.Transforms.NormalizeMeanVariance("MinTemp", "MinTemp"))
                        .Append(context.Transforms.NormalizeMeanVariance("Pressure", "Pressure"))
                        .Append(context.Transforms.Concatenate("Features", featureArray))
                        .AppendCacheCheckpoint context
```

This is a lot, but most of these should be fairly straightforward given the explanations above.

First of all, everything starts with a new `EstimatorChain`.

Next, we rename the `Precipitation` property in `WeatherInput` to the `Label` column during the pipeline process.

After that we take the numeric columns and normalize their ranges according to their average (mean) and variance. We do not need to rename these columns so you see the same names listed twice.

After that, we merge all of our features together into the Features column which our estimator will use in the next step. Features refers to anything that contributes to the label being predicted, so you could look at the word "features" as "contributing factors" in machine learning.

Finally we have ML.NET cache these results so that these values are calculated _before_ the training process occurs which improves the accuracy and performance of our training process.

### Selecting a Model

Next we need to select a regression model training algorithm from the `context.Regression.Trainers` group. 

The available options with the base ML.NET package are:

- `LbfgsPoissonRegression`
- `OnlineGradientDescent`
- `Sdca`

Of these three, gradient descent is the easiest to explain, so I chose to start with `OnlineGradientDescent`.

The code for creating a new `OnlineGradientDescent` trainer and adding it to a new pipeline is as follows:

```fsharp
// Select the algorithm we want
let trainer = context.Regression.Trainers.OnlineGradientDescent(labelColumnName = "Label", featureColumnName = "Features")

// Generate a training pipeline based on the processing pipeline mixed with the trainer
let trainingPipeline = processPipeline.Append trainer
```

Here we specify the algorithm we want (`OnlineGradientDescent`) as well as the name of the columns in our processing pipeline that contain the label and features.

If we wanted to, we could also customize the various _hyperparameters_ to the model, which include the following for `OnlineGradientDescent`:

- Learning Rate
- Whether learning rate decreases over time
- Loss Function
- l2 Regularization
- Number of Iterations

However, the focus of this article is to tour machine learning in ML.NET with F#, not to find the most optimal model to this sample problem.

### Training the Model

Now that we have our data loaded and the model training pipeline ready to go, we can fit the training pipeline to the training dataset:

```fsharp
// Train the model
let model = trainingPipeline.Fit trainSet
```

This synchronously uses the training pipeline with the training dataset to find the optimal model using the algorithm we selected. For me this took 0.1 second on my primary development machine with this dataset. This is significantly less time than it takes to even check if NuGet Packages are installed. This highlights ML.NET's performance, though admittedly on a small sample dataset.

Once the `Fit` process is complete, the resulting model is stored in `model` as an `ITransformer`.

We can use this trained model to generate new predictions, but before we do that, we should gather some metrics to determine how accurate the resulting model actually is.

## Evaluating a Regression Model in F#

To determine how good (or how bad) a model is, we have it generate predictions for our test dataset by calling `Transform` on the model. This transforms multiple input rows to rows with predictions using that trained model.

Since we know from our dataset the actual precipitation that occurred on those days, we can use this to gauge the accuracy of those predictions via the `Evaluate` function.

This will give us a set of metrics that we can then evaluate using our data science knowledge to determine how good the resulting model is.

My F# code for this is as follows:

```fsharp
let testResults = model.Transform testSet

let testMetrics = context.Regression.Evaluate(testResults, "Label", "Score")

testMetrics
```

Since I'm ending a Polyglot Interactive notebook cell with a single variable, I can see the metrics inline in a table following the cell:

![Model Metrics showing an RSquared of 0.06 and other metrics](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2gmfpka7seph6fxunnt3.png)

This lets us see the following standard regression metrics:

- **Mean Absolute Error (MAE)** - the average absolute distance the prediction was off from the source (error)
- **Mean Squared Error (MSE)** - the squared error. This is like MAE, but extreme values are amplified
- **Root Mean Squared Error (RMSE)** - the root MSE is similar to the MSE but its units are easier to work with
- **R Squared** - the coefficient of determination is a value indicating how useful this model is for predicting new values

_Note: **LossFunction** here is a duplicate of MSE. I suspect LossFunction selects one primary metric to use since all 4 of these metrics are different loss functions_

In these metrics you want your MAE, MSE, and RMSE to be as close to zero as possible (indicating no errors whatsoever) while you prefer your R Squared to be as close to 1 as possible.

These metrics aren't quite _terrible_, but they're not amazing either. To me, these metrics indicate an initial model to start with, but I'd want to investigate different hyperparameter settings for the model training process, different algorithms, and different processing steps in my pipeline. If I couldn't get a model to perform suitably well, you might even need to investigate your data source and see if there are significant data quality issues or just not enough information for an accurate model.

## Saving and Loading the Model

Once you have a model you are happy with, you should save that model to disk so you can load it and use it to generate predictions without needing to retrain it in the future.

ML.NET supports a number of different formats including zip files and ONNX files. To keep things simple, we'll just save our model to a zip file:

```fsharp
context.Model.Save(model, data.Schema, "Model.zip")
```

This writes the model to a `Model.zip` file alongside our Polyglot Interactive notebook or in your program's working directory if you're not using a Notebook for this code.

Once you have a model you'd like to use later on, you can load it just as easily:

```fsharp
let loadedModel, loadedSchema = context.Model.Load "Model.zip"
```

Here we load both the model and a schema definition for that model together. Often you'll find you don't need the schema definition at all, but it's part of the ML.NET API.

## Generating Predictions using ML.NET and F#

Now that we have a trained model, we can use it to generate predictions for new data.

In order to do this, we first create a `PredictionEngine` and tell it what type of rows it can expect and what type of rows we want it to give us.

The input type is the `WeatherInput` type we defined earlier when loading data into an `IDataView`, but output type will be something new:

```fsharp
[<CLIMutable>]
type WeatherPrediction = {
    [<ColumnName("Score")>] 
    Precipitation: Single
}
```

This represents a simple type that has a `Precipitation` property. We use the `ColumnName` attribute to map from F#'s default "Score" column to something more easily understandable.

Finally, we need to put `CLIMutable` on our output type in order for ML.NET to be able to mutate values after instance creation and to satisfy some compiler requirements. Realizing I needed the `CLIMutable` attribute to get the output type working properly was probably the hardest aspect of working with F# and ML.NET together, so hopefully my learning journey helps you on your own path.

Now that we have our `WeatherPrediction` reference type, we can use it to create a `PredictionEngine` with the following F# code:

```fsharp
let predictionEngine = context.Model.CreatePredictionEngine<WeatherInput, WeatherPrediction>(model)
```

Next, you can create a fake set of conditions and use them to get a prediction for that day based on the trained model:

```fsharp
let conditions: WeatherInput = {
    CloudCover = 4f; 
    Sunshine=5.8f; 
    GlobalRads=50f; 
    MaxTemp=5.2f; 
    MinTemp=1.6f; 
    MeanTemp=3.7f; 
    Pressure=101170f;
    Precipitation=0f; // Needed for the compiler, but its value is ignored
}

let prediction = predictionEngine.Predict(conditions)

prediction
```

This yields a `WeatherPrediction` object with the predicted precipitation for that given `WeatherInput`. Putting `prediction` as the last value in a cell causes Polyglot Notebooks to display it for our convenience as well:

![Predicted Precipitation of 1.0180497 centimeters](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9bwrg451p8t6u9vwmrpx.png)

In other words, for our weather scenario our model predicts it's going to rain 1.018 centimeters given the conditions and predictions we have for the other weather factors.

## Closing Thoughts

We just walked through all the basic steps you'd need to perform a basic experiment in ML.NET 2.0 using F# and Polyglot Notebooks.

I found Polyglot Notebooks to be incredibly helpful when generating the code for this article.

This code is very similar to the C# code I'd write to accomplish the same tasks, so I think that if I did this task frequently in F# that I would likely start introducing more functions. I mention this primarily because it feels odd to write an F# article without using the `|>` operator outside of this sentence and without declaring any functions of my own.

However, F# did get us the ability to quickly declare types, ignore a lot of the verbosity in the generics syntax prevalent in ML.NET around prediction engines, and focus entirely on the task we wanted to accomplish.

With this final model that we have, we can save it to disk, load it upon application start up, and integrate it into any professional dotnet application in a repeatable and performant way.