Intermediate

Building Apps with ML.NET

Use ML.NET for data and image classification in a real .NET (Wired Brain Coffee) application.

Application project: Wired Brain Coffee (Blazor .NET 9 application)


Table of Contents

  1. Understanding Machine Learning and ML.NET
  2. Using Data Classification
  3. Working with Image Classification
  4. Final Project Structure
  5. Summary and Key Concepts

1. Understanding Machine Learning and ML.NET

What is Machine Learning?

Machine learning (ML) is a branch of artificial intelligence. It enables machines to learn so they can perform tasks autonomously. In other words, machine learning lets you program the unprogrammable.

Examples of Problems Solved by ML

With classic programming (if/else, switch, loops), the following questions are difficult to answer:

QuestionML Scenario
Is there a coffee in this image?Image classification
What language is this text written in?Data/Text classification
Are comments on our site negative?Binary classification
What should the price of our new product be?Regression
Does this X-ray show a bone fracture?Image classification

Machine Learning Workflow

flowchart LR
    A["๐ŸŽฏ Define\nthe Problem"] --> B["๐Ÿ“Š Prepare\nthe Data"]
    B --> C["๐Ÿ”ง Choose\nan Algorithm"]
    C --> D["โš™๏ธ Tune\nParameters"]
    D --> E["โœ… Evaluate\nthe Model"]
    E --> F{Good\nenough?}
    F -- No --> C
    F -- Yes --> G["๐Ÿš€ Use\nthe Model"]

    style A fill:#4A90D9,color:#fff
    style B fill:#4A90D9,color:#fff
    style C fill:#E8A838,color:#fff
    style D fill:#E8A838,color:#fff
    style E fill:#E8A838,color:#fff
    style F fill:#aaa,color:#fff
    style G fill:#27AE60,color:#fff

Note: The orange steps (algorithm selection, parameter tuning, evaluation) require machine learning expertise. This is exactly where AutoML comes in.

Understanding ML.NET

ML.NET is the open-source machine learning framework for .NET developers.

graph TD
    A["๐ŸŒ ML.NET\n(Microsoft.ML)"] --> B["Open-source"]
    A --> C["NuGet package: Microsoft.ML"]
    A --> D["Works offline\n(cloud + on-premises)"]
    A --> E["Cross-platform\nWindows / Linux / macOS"]
    A --> F["Resource: dot.net/ml"]

Basic ML.NET Code Flow

// 1. Create an ML context
var mlContext = new MLContext();

// 2. Load training data
var data = mlContext.Data.LoadFromTextFile<ModelInput>(dataPath, separatorChar: ';', hasHeader: true);

// 3. Build a transformation and training pipeline
var pipeline = mlContext.Transforms.Text.FeaturizeText("Features", "Message")
    .Append(mlContext.MulticlassClassification.Trainers.LbfgsMaximumEntropy());

// 4. Train the model
var model = pipeline.Fit(data);

// 5. Use the model to make predictions
var predEngine = mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(model);
var prediction = predEngine.Predict(sampleData);

The Algorithm Selection Problem

For a simple binary classification with ML.NET, here are some available algorithms:

Binary Classification:
โ”œโ”€โ”€ LbfgsLogisticRegression
โ”œโ”€โ”€ SdcaLogisticRegression
โ”œโ”€โ”€ AveragedPerceptron
โ”œโ”€โ”€ FastTree
โ”œโ”€โ”€ FastForest
โ”œโ”€โ”€ LightGbm
โ””โ”€โ”€ ... (and many more)

Choosing the right algorithm requires specialized expertise. This is why AutoML exists.

AutoML and Model Builder

graph BT
    A["ML.NET\nMicrosoft.ML"] --> B["AutoML\nMicrosoft.ML.AutoML"]
    B --> C["Model Builder\n(Visual Studio 2022 Extension)"]
    B --> D["ML.NET CLI\n(command line)"]

    style A fill:#0078D4,color:#fff
    style B fill:#E8A838,color:#fff
    style C fill:#27AE60,color:#fff
    style D fill:#27AE60,color:#fff

AutoML (Automated Machine Learning):

  • Provides methods and processes to make ML accessible to non-experts
  • You define the problem and prepare the data
  • AutoML chooses the algorithm, tunes parameters, and evaluates the model

Model Builder:

  • Visual Studio extension with a graphical interface
  • Built on AutoML and ML.NET
  • Official documentation: โ€œYou donโ€™t need machine learning expertise to use Model Builderโ€

Setting Up the Environment

Prerequisites

ComponentDetails
Visual Studio 2022Community, Professional, or Enterprise โ€” visualstudio.com
WorkloadASP.NET and web development
ExtensionML.NET Model Builder 2022 (in Individual components)

โš ๏ธ Make sure to install the extension with the 2022 suffix (not the Visual Studio 2019 extension).

Verifying the Installation

  1. Open the Visual Studio Installer
  2. Click Modify
  3. Verify the ASP.NET and web development workload
  4. Go to Individual components > search for ML.NET
  5. Confirm that ML.NET Model Builder 2022 is installed

Exploring the Starter Project

The WiredBrain.WebApp project is a Blazor application (.NET 9) used as a base throughout the course.

Implemented ML Scenarios

graph LR
    App["๐Ÿง  WiredBrain.WebApp\n(Blazor)"] --> LD["๐Ÿ“ Language Detection\nData Classification"]
    App --> CD["โ˜• Coffee Detection\nImage Classification"]
ScenarioML TypeDescription
Language detectionData/Text classificationDetect the language of entered text
Coffee detectionImage classificationDetect whether an image contains coffee

2. Using Data Classification

Understanding the Task

Data classification allows classifying data into two or more categories. In this module, the goal is to detect the language of a text.

  • Text classification: for raw textual data only
  • Data classification: for tabular data (numbers + text)

Both can be used for this language detection scenario.

Languages to Detect

LanguageExample Message
๐Ÿ‡ฉ๐Ÿ‡ช GermanIch programmiere gerne mit C#, F#, und Machine Learning mit ML.NET...
๐Ÿ‡ซ๐Ÿ‡ท FrenchJ'aime programmer en C#, F#, et l'apprentissage machine avec ML.NET...
๐Ÿ‡ฎ๐Ÿ‡น ItalianMi piace programmare con C#, F#, e il Machine Learning con ML.NET...
๐Ÿ‡ฌ๐Ÿ‡ง EnglishI like programming with C#, F#, and Machine Learning with ML.NET...

Creating a Model Builder Configuration File

To add machine learning to a .NET project:

  1. Right-click on the project in Solution Explorer
  2. Select Add > Machine Learning Model
  3. Name the file (e.g., LanguageDetectionModel)
  4. Click Add

This menu item is only available if the ML.NET Model Builder 2022 extension is installed.

The .mbconfig file created is actually a JSON file:

{
  "Scenario": "Classification",
  "DataSource": {
    "Version": 3,
    "Type": "TabularFile",
    "FilePath": "D:\\TrainingData\\languageDetection.csv",
    "Delimiter": ";",
    "HasHeader": true,
    "ColumnProperties": [
      {
        "ColumnName": "Language",
        "ColumnPurpose": "Label",
        "ColumnDataFormat": "String"
      },
      {
        "ColumnName": "Message",
        "ColumnPurpose": "Feature",
        "ColumnDataFormat": "String"
      }
    ]
  },
  "Environment": {
    "Type": "LocalCPU",
    "Version": 1
  }
}

Selecting a Scenario

The Model Builder interface presents the ML workflow on the left:

flowchart TD
    S1["1๏ธโƒฃ Scenario"] --> S2["2๏ธโƒฃ Environment"]
    S2 --> S3["3๏ธโƒฃ Data"]
    S3 --> S4["4๏ธโƒฃ Train"]
    S4 --> S5["5๏ธโƒฃ Evaluate"]
    S5 --> S6["6๏ธโƒฃ Consume"]

Available Scenario Categories

Model Builder Scenarios:
โ”‚
โ”œโ”€โ”€ ๐Ÿ“Š Tabular (CSV or SQL data)
โ”‚   โ”œโ”€โ”€ Data classification         โ† our choice for language detection
โ”‚   โ”œโ”€โ”€ Value prediction (regression)
โ”‚   โ””โ”€โ”€ Recommendation
โ”‚
โ”œโ”€โ”€ ๐Ÿ–ผ๏ธ Computer Vision
โ”‚   โ”œโ”€โ”€ Image classification        โ† used in module 3
โ”‚   โ””โ”€โ”€ Object detection (Preview)
โ”‚
โ””โ”€โ”€ ๐Ÿ’ฌ Natural Language Processing (Preview)
    โ”œโ”€โ”€ Text classification
    โ””โ”€โ”€ ...

For our language detection scenario: Data classification with Local (CPU) environment.

Specifying Training Data

Training CSV File Format

The languageDetection.csv file uses ; as separator:

Language;Message
German;Ich programmiere gerne mit C#, F#, und Machine Learning mit ML.NET finde ich echt richtig gut.
French;J'aime programmer en C#, F#, et l'apprentissage machine avec ML.NET est vraiment bon.
Italian;Mi piace programmare con C#, F#, e il Machine Learning con ML.NET รจ davvero buono.
English;I like programming with C#, F#, and Machine Learning with ML.NET is really good.
German;Wenn ich mal nicht am Computer sitze, dann geniesse ich draussen die Sonne.
French;Quand je ne suis pas assis devant l'ordinateur, je profite du soleil dehors.
Italian;Quando non sono seduto al computer, mi godo il sole fuori.
English;When I am not sitting at the computer, I enjoy the sun outside.
German;Hallo, ich bin Thomas. Wer bist Du?
French;Salut, je m'appelle Thomas. Qui รชtes-vous?
Italian;Ciao, sono Thomas. Chi sei?
English;Hi, I'm Thomas. Who are you?

Column Roles (Column Purposes)

PurposeDescription
LabelThe column to predict (only one per model) โ€” here: Language
FeatureColumn used to make the prediction โ€” here: Message
IgnoreColumn not needed for prediction

Advanced Data Options

In Advanced data options > Data formatting:

  • Enable โ€œDoes the data contain column headers?โ€ โ†’ Yes

In Column settings:

  • Language โ†’ Purpose: Label
  • Message โ†’ Purpose: Feature

Training the Model

Training Parameters

ParameterRecommended ValueDescription
Time to train20+ secondsLonger = more algorithms explored
MetricDefaultUsed to find the best model
TrainersAll includedOption to exclude algorithms

Model Builder automatically selects a training time based on dataset size. Longer durations allow exploring more models.

Training Results

After training (~20 seconds), 69 models were explored. The best selected model:

Best model: LbfgsMaximumEntropyMulti
Training duration: 18.9 seconds
Models explored: 69

Files Generated by Model Builder

WiredBrain.WebApp/
โ”œโ”€โ”€ LanguageDetectionModel.mbconfig        โ† JSON configuration
โ”œโ”€โ”€ LanguageDetectionModel.mlnet           โ† trained binary model
โ”œโ”€โ”€ LanguageDetectionModel.consumption.cs  โ† C# code to use the model
โ”œโ”€โ”€ LanguageDetectionModel.training.cs     โ† C# code to retrain
โ””โ”€โ”€ LanguageDetectionModel.evaluate.cs     โ† C# code to evaluate (PFI)

Generated Transformation Pipeline (training.cs)

public static IEstimator<ITransformer> BuildPipeline(MLContext mlContext)
{
    var pipeline = mlContext.Transforms.Text
        // 1. Transform text into numerical features
        .FeaturizeText(inputColumnName: @"Message", outputColumnName: @"Message")
        // 2. Concatenate features
        .Append(mlContext.Transforms.Concatenate(@"Features", new[] { @"Message" }))
        // 3. Map textual labels to numerical keys
        .Append(mlContext.Transforms.Conversion.MapValueToKey(
            outputColumnName: @"Language",
            inputColumnName: @"Language",
            addKeyValueAnnotationsAsText: false))
        // 4. Train with LbfgsLogisticRegression (One-vs-All)
        .Append(mlContext.MulticlassClassification.Trainers.OneVersusAll(
            binaryEstimator: mlContext.BinaryClassification.Trainers.LbfgsLogisticRegression(
                new LbfgsLogisticRegressionBinaryTrainer.Options()
                {
                    L1Regularization = 0.03125F,
                    L2Regularization = 0.0324988F,
                    LabelColumnName = @"Language",
                    FeatureColumnName = @"Features"
                }),
            labelColumnName: @"Language"))
        // 5. Convert predicted keys back to textual labels
        .Append(mlContext.Transforms.Conversion.MapKeyToValue(
            outputColumnName: @"PredictedLabel",
            inputColumnName: @"PredictedLabel"));

    return pipeline;
}

Evaluating the Model

In the Evaluate section of Model Builder, you can test the model directly:

Test MessageResultProbability
I enjoy learning with Visual Studio CodeEnglish86%
Ich lerne gerne mit ML.NETGerman90%
J'aime apprendre avec C#French56%

If the model is insufficient, go back to Train to increase training time or exclude underperforming algorithms.

Evaluation via Permutation Feature Importance (PFI)

The LanguageDetectionModel.evaluate.cs file contains a method to calculate feature importance:

public static List<Tuple<string, double>> CalculatePFI(
    MLContext mlContext,
    IDataView trainData,
    ITransformer model,
    string labelColumnName)
{
    var preprocessedTrainData = model.Transform(trainData);

    var permutationFeatureImportance =
        mlContext.MulticlassClassification
        .PermutationFeatureImportance(
            model,
            preprocessedTrainData,
            labelColumnName: labelColumnName);

    var featureImportanceMetrics = permutationFeatureImportance
        .Select((kvp) => new { kvp.Key, kvp.Value.MacroAccuracy })
        .OrderByDescending(f => Math.Abs(f.MacroAccuracy.Mean));

    var featurePFI = new List<Tuple<string, double>>();
    foreach (var feature in featureImportanceMetrics)
    {
        var pfiValue = Math.Abs(feature.MacroAccuracy.Mean);
        featurePFI.Add(new Tuple<string, double>(feature.Key, pfiValue));
    }

    return featurePFI;
}

Consuming the Model

Auto-Generated Classes

// LanguageDetectionModel.consumption.cs โ€” auto-generated by ML.NET Model Builder

public partial class LanguageDetectionModel
{
    // Input class
    public class ModelInput
    {
        [LoadColumn(0)]
        [ColumnName(@"Language")]
        public string Language { get; set; }

        [LoadColumn(1)]
        [ColumnName(@"Message")]
        public string Message { get; set; }
    }

    // Output class
    public class ModelOutput
    {
        [ColumnName(@"Language")]
        public uint Language { get; set; }

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

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

        [ColumnName(@"PredictedLabel")]
        public string PredictedLabel { get; set; }   // โ† predicted label

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

    // Path to the binary model file
    private static string MLNetModelPath = Path.GetFullPath("LanguageDetectionModel.mlnet");

    // Lazy PredictionEngine (thread-safe initialization)
    public static readonly Lazy<PredictionEngine<ModelInput, ModelOutput>> PredictEngine =
        new Lazy<PredictionEngine<ModelInput, ModelOutput>>(() => CreatePredictEngine(), true);

    private static PredictionEngine<ModelInput, ModelOutput> CreatePredictEngine()
    {
        var mlContext = new MLContext();
        ITransformer mlModel = mlContext.Model.Load(MLNetModelPath, out var _);
        return mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(mlModel);
    }

    // Main prediction method
    public static ModelOutput Predict(ModelInput input)
    {
        var predEngine = PredictEngine.Value;
        return predEngine.Predict(input);
    }
}

Blazor Component โ€” LanguageDetection.razor (final version)

@page "/languagedetection"
@using WiredBrain_WebApp
@rendermode InteractiveServer

<PageTitle>Language Detection</PageTitle>

<h1>Language Detection</h1>
<h2>Scenario: Data classification (or alternatively text classification)</h2>

<div>Please enter your text here:</div>

<textarea class="form-control" style="height:150px;margin-top:5px"
          spellcheck="false"
          @bind="inputText"
          @bind:event="oninput"
          @bind:after="TextChanged" />

<div style="margin-top:20px;">
    Detected language:
    <span style="font-weight:bold">@detectedLanguage</span>
</div>

@if (allLabels is not null)
{
    <ul>
        @foreach (var label in allLabels)
        {
            <li>@label.Key - @(Math.Round(label.Value * 100, 2))%</li>
        }
    </ul>
}

@code {
    private string? inputText;
    private string? detectedLanguage = "None";
    private IOrderedEnumerable<KeyValuePair<string, float>>? allLabels;

    private void TextChanged()
    {
        if (inputText is null or "")
        {
            detectedLanguage = "None";
            allLabels = null;
            return;
        }

        // 1. Create input object
        var input = new LanguageDetectionModel.ModelInput
        {
            Message = inputText
        };

        // 2. Make the prediction (single label)
        var output = LanguageDetectionModel.Predict(input);
        detectedLanguage = output.PredictedLabel;

        // 3. Get all labels with their scores
        allLabels = LanguageDetectionModel.PredictAllLabels(input);
    }
}

Prediction Call Flow

sequenceDiagram
    participant U as User (Browser)
    participant R as LanguageDetection.razor
    participant M as LanguageDetectionModel
    participant E as PredictionEngine

    U->>R: Enters text (oninput)
    R->>R: TextChanged() triggered
    R->>M: Predict(ModelInput { Message })
    M->>E: predEngine.Predict(input)
    E-->>M: ModelOutput { PredictedLabel, Score[] }
    M-->>R: output.PredictedLabel
    R->>M: PredictAllLabels(input)
    M->>E: predEngine.Predict(input)
    E-->>M: Scores with labels
    M-->>R: IOrderedEnumerable<KeyValuePair<string, float>>
    R-->>U: Displays detected language + all percentages

Required NuGet Packages (module 2)

<!-- WiredBrain.WebApp.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.ML" Version="4.0.0" />
  </ItemGroup>

  <ItemGroup Label="LanguageDetectionModel">
    <None Include="LanguageDetectionModel.mlnet">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

Predicting All Labels

The PredictAllLabels method returns an IOrderedEnumerable<KeyValuePair<string, float>> where:

  • Key = label (e.g., โ€œEnglishโ€)
  • Value = score between 0 and 1 (0% to 100%)
public static IOrderedEnumerable<KeyValuePair<string, float>> PredictAllLabels(ModelInput input)
{
    var predEngine = PredictEngine.Value;
    var result = predEngine.Predict(input);
    return GetSortedScoresWithLabels(result);
}

public static IOrderedEnumerable<KeyValuePair<string, float>> GetSortedScoresWithLabels(ModelOutput result)
{
    var unlabeledScores = result.Score;
    var labelNames = GetLabels(result);

    Dictionary<string, float> labeledScores = new Dictionary<string, float>();
    for (int i = 0; i < labelNames.Count(); i++)
    {
        var labelName = labelNames.ElementAt(i);
        labeledScores.Add(labelName.ToString(), unlabeledScores[i]);
    }

    return labeledScores.OrderByDescending(c => c.Value);
}

private static IEnumerable<string> GetLabels(ModelOutput result)
{
    var schema = PredictEngine.Value.OutputSchema;
    var labelColumn = schema.GetColumnOrNull("Language");

    if (labelColumn == null)
        throw new Exception("Language column not found.");

    var keyNames = new VBuffer<ReadOnlyMemory<char>>();
    labelColumn.Value.GetKeyValues(ref keyNames);
    return keyNames.DenseValues().Select(x => x.ToString());
}

Display in Blazor Component

@if (allLabels is not null)
{
    <ul>
        @foreach (var label in allLabels)
        {
            <!-- Example: English - 86.42% -->
            <li>@label.Key - @(Math.Round(label.Value * 100, 2))%</li>
        }
    </ul>
}

Retraining the Model

To add a new language (e.g., Spanish), simply update the training CSV without changing the application code.

Adding Spanish to the CSV File (languageDetection_vNext.csv)

Spanish;Me gusta programar con C#, F#, y me gusta mucho el aprendizaje automรกtico con ML.NET.
Spanish;Cuando no estoy frente al ordenador, disfruto del sol al aire libre.
Spanish;ยฟQuiรฉn registrรณ este cรณdigo? ยฟFuiste tรบ? Oh, fui yo.
Spanish;Anoche empecรฉ a leer un libro nuevo. ยฟTรบ tambiรฉn estรกs leyendo un libro?
Spanish;Gracias por visitar thomasclaudiushuber.com.
Spanish;Hola, soy Thomas. ยฟY tรบ quiรฉn eres?

Retraining Steps

flowchart LR
    A["๐Ÿ“ Add Spanish data\nto CSV"] --> B["Double-click\nLanguageDetectionModel.mbconfig"]
    B --> C["Go to Train step"]
    C --> D["Click\n'Train again'"]
    D --> E["New best model:\nSdcaMaximumEntropyMulti"]
    E --> F["โœ… Model now detects\n5 languages"]

Result: After retraining, the model now detects: German, French, Italian, English and Spanish โ€” without any code change!

Model comparison:

TrainingSelected AlgorithmLanguages
1stLbfgsMaximumEntropyMultiDE, FR, IT, EN
2nd (with ES)SdcaMaximumEntropyMultiDE, FR, IT, EN, ES

Retraining via Code

// LanguageDetectionModel.training.cs

public static void Train(
    string outputModelPath,
    string inputDataFilePath = RetrainFilePath,
    char separatorChar = RetrainSeparatorChar,
    bool hasHeader = RetrainHasHeader,
    bool allowQuoting = RetrainAllowQuoting)
{
    var mlContext = new MLContext();
    var data = LoadIDataViewFromFile(mlContext, inputDataFilePath, separatorChar, hasHeader, allowQuoting);
    var model = RetrainModel(mlContext, data);
    SaveModel(mlContext, model, data, outputModelPath);
}

public static ITransformer RetrainModel(MLContext mlContext, IDataView trainData)
{
    var pipeline = BuildPipeline(mlContext);
    var model = pipeline.Fit(trainData);
    return model;
}

public static void SaveModel(MLContext mlContext, ITransformer model, IDataView data, string modelSavePath)
{
    DataViewSchema dataViewSchema = data.Schema;
    using (var fs = File.Create(modelSavePath))
    {
        mlContext.Model.Save(model, dataViewSchema, fs);
    }
}

3. Working with Image Classification

Understanding the Task

The goal is to enable Wired Brain Coffee to detect images containing coffee. We want to classify images into 3 categories: coffee, goat, other.

Training Image Data Structure

Images/
โ”œโ”€โ”€ coffee/     (15 coffee images)
โ”‚   โ”œโ”€โ”€ image1.jpg
โ”‚   โ”œโ”€โ”€ image2.jpg
โ”‚   โ””โ”€โ”€ ...
โ”œโ”€โ”€ goat/       (6 goat images)
โ”‚   โ”œโ”€โ”€ image1.jpg
โ”‚   โ””โ”€โ”€ ...
โ””โ”€โ”€ other/      (7 miscellaneous images)
    โ”œโ”€โ”€ image1.jpg
    โ””โ”€โ”€ ...

The subfolder names automatically become the modelโ€™s labels.

Selecting the Scenario and Configuring the GPU

Environment Options for Image Classification

EnvironmentSpeedRequirements
Local (CPU)SlowNone
Local (GPU)Fast โœ…NVIDIA CUDA 10.1 + cuDNN 7.6.4
AzureVariableAzure subscription

GPU Prerequisites (NVIDIA CUDA)

GPU configuration for ML.NET Model Builder:
โ”œโ”€โ”€ CUDA 10.1          โ†’ developer.nvidia.com/cuda-10.1-download-archive
โ”œโ”€โ”€ cuDNN 7.6.4        โ†’ developer.nvidia.com/cudnn (exact version required)
โ””โ”€โ”€ Compatible GPU     โ†’ NVIDIA GeForce with CUDA support
    (e.g.: GTX 1650, RTX 30xx, etc.)

CUDA = Compute Unified Device Architecture: framework for accessing raw GPU computing power.
cuDNN = CUDA Deep Neural Network: library of primitives for deep neural networks, built on CUDA.

Steps in Model Builder

  1. Right-click on the project โ†’ Add > Machine Learning Model
  2. Name the model: CoffeeDetectionModel
  3. Click Add
  4. Select the scenario: Image classification
  5. Select the environment: Local (GPU) (or CPU if no NVIDIA GPU)

Specifying Training Data

The Images/ folder must have this structure (subfolders = labels):

Images/
โ”œโ”€โ”€ coffee/   โ† label "coffee"
โ”œโ”€โ”€ goat/     โ† label "goat"
โ””โ”€โ”€ other/    โ† label "other"

Advanced Options: Validation Data

SplitUsage
80%Training data
20%Validation data

Training the Model

Additional NuGet Packages for Image Classification

<!-- WiredBrain.WebApp.csproj โ€” additions for module 3 -->
<ItemGroup>
  <PackageReference Include="Microsoft.ML" Version="4.0.0" />
  <PackageReference Include="Microsoft.ML.Vision" Version="4.0.0" />
  <PackageReference Include="SciSharp.TensorFlow.Redist" Version="2.3.1" />
</ItemGroup>

<ItemGroup Label="CoffeeDetectionModel">
  <None Include="CoffeeDetectionModel.mlnet">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
</ItemGroup>

Training Pipeline for Image Classification (CoffeeDetectionModel.training.cs)

public static IEstimator<ITransformer> BuildPipeline(MLContext mlContext)
{
    var pipeline = mlContext.Transforms.Conversion
        // 1. Map textual labels to numerical keys
        .MapValueToKey(
            outputColumnName: @"Label",
            inputColumnName: @"Label",
            addKeyValueAnnotationsAsText: false)
        // 2. Train with ImageClassification (transfer learning)
        .Append(mlContext.MulticlassClassification.Trainers.ImageClassification(
            labelColumnName: @"Label",
            scoreColumnName: @"Score",
            featureColumnName: @"ImageSource"))
        // 3. Convert predicted keys back to textual labels
        .Append(mlContext.Transforms.Conversion.MapKeyToValue(
            outputColumnName: @"PredictedLabel",
            inputColumnName: @"PredictedLabel"));

    return pipeline;
}

Loading Images from a Folder

public static IDataView LoadImageFromFolder(MLContext mlContext, string folder)
{
    var res = new List<ModelInput>();
    var allowedImageExtensions = new[] { ".png", ".jpg", ".jpeg", ".gif" };
    DirectoryInfo rootDirectoryInfo = new DirectoryInfo(folder);
    DirectoryInfo[] subDirectories = rootDirectoryInfo.GetDirectories();

    if (subDirectories.Length == 0)
        throw new Exception("No subdirectories found");

    foreach (DirectoryInfo directory in subDirectories)
    {
        var imageList = directory.EnumerateFiles()
            .Where(f => allowedImageExtensions.Contains(f.Extension.ToLower()));

        if (imageList.Count() > 0)
        {
            res.AddRange(imageList.Select(i => new ModelInput
            {
                Label = directory.Name,           // subfolder name = label
                ImageSource = File.ReadAllBytes(i.FullName),  // image as byte[]
            }));
        }
    }

    return mlContext.Data.LoadFromEnumerable(res);
}

Generated Files for Image Classification

WiredBrain.WebApp/
โ”œโ”€โ”€ CoffeeDetectionModel.mbconfig        โ† JSON configuration
โ”œโ”€โ”€ CoffeeDetectionModel.mlnet           โ† trained binary model
โ”œโ”€โ”€ CoffeeDetectionModel.consumption.cs  โ† code to use the model
โ””โ”€โ”€ CoffeeDetectionModel.training.cs     โ† code to retrain

๐Ÿ“Œ No .evaluate.cs file for Image Classification (unlike Data Classification).

Evaluating the Model

In the Evaluate section of Model Builder, you can test with images:

Tested ImageResultAccuracy
Coffee in a cupcoffee100%
Thomas drinking coffee (image 8)coffee97%
Goat image (image 16)goat97%
โ€œOtherโ€ imageother~85%+
Thomas photo (not in training)other62%
Wired Brain Coffee logocoffee82%

Recommendation: Always test with images outside the training dataset for a realistic evaluation.

Consuming the Model

Auto-Generated Classes (CoffeeDetectionModel.consumption.cs)

public partial class CoffeeDetectionModel
{
    // Input class โ€” image is passed as byte[]
    public class ModelInput
    {
        [LoadColumn(0)]
        [ColumnName(@"Label")]
        public string Label { get; set; }

        [LoadColumn(1)]
        [ColumnName(@"ImageSource")]
        public byte[] ImageSource { get; set; }   // โ† image as byte array
    }

    // Output class
    public class ModelOutput
    {
        [ColumnName(@"Label")]
        public uint Label { get; set; }

        [ColumnName(@"ImageSource")]
        public byte[] ImageSource { get; set; }

        [ColumnName(@"PredictedLabel")]
        public string PredictedLabel { get; set; }   // โ† "coffee", "goat", or "other"

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

    private static string MLNetModelPath = Path.GetFullPath("CoffeeDetectionModel.mlnet");

    public static readonly Lazy<PredictionEngine<ModelInput, ModelOutput>> PredictEngine =
        new Lazy<PredictionEngine<ModelInput, ModelOutput>>(() => CreatePredictEngine(), true);

    private static PredictionEngine<ModelInput, ModelOutput> CreatePredictEngine()
    {
        var mlContext = new MLContext();
        ITransformer mlModel = mlContext.Model.Load(MLNetModelPath, out var _);
        return mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(mlModel);
    }

    public static ModelOutput Predict(ModelInput input)
    {
        var predEngine = PredictEngine.Value;
        return predEngine.Predict(input);
    }
}

Blazor Component โ€” CoffeeDetection.razor (final version)

@page "/coffeedetection"
@using WiredBrain_WebApp
@rendermode InteractiveServer

<PageTitle>Coffee Detection</PageTitle>

<h1>Coffee Detection</h1>
<h2>Scenario: Image classification</h2>

<!-- Select image file -->
<InputFile OnChange="LoadFileAsync" class="form-control" />

@if (errorMessage is not null)
{
    <div class="alert alert-danger" style="margin-top:10px">@errorMessage</div>
}

@if (base64ImageSource is not null)
{
    <div style="margin-top:10px">
        <img src="@base64ImageSource" />
    </div>
}

<div style="margin-top:20px;">
    Detected content:
    <span style="font-weight:bold">@detectedContent</span>
</div>

<!-- Display all labels with their percentages -->
@if (allLabels is not null)
{
    <ul>
        @foreach (var label in allLabels)
        {
            <li>@label.Key - @(Math.Round(label.Value * 100, 2))%</li>
        }
    </ul>
}

@code {
    private const long maxFileSize = 2 * 1024 * 1024; // 2 MB

    private string? base64ImageSource;
    private string? detectedContent = "None";
    private string? errorMessage;
    private IOrderedEnumerable<KeyValuePair<string, float>>? allLabels;

    private async Task LoadFileAsync(InputFileChangeEventArgs e)
    {
        base64ImageSource = null;
        detectedContent = "None";

        if (!IsFileSizeValid(e.File.Size)) return;

        // 1. Read image as byte[]
        byte[] image = await GetImageByteArray(e.File);

        // 2. Prepare base64 display
        SetBase64ImageSource(e.File.ContentType, image);

        // 3. Create input object
        var input = new CoffeeDetectionModel.ModelInput
        {
            ImageSource = image
        };

        // 4. Make the prediction
        var output = CoffeeDetectionModel.Predict(input);
        detectedContent = output.PredictedLabel;

        // 5. Get all labels with their scores
        allLabels = CoffeeDetectionModel.PredictAllLabels(input);
    }

    private async Task<byte[]> GetImageByteArray(IBrowserFile browserFile)
    {
        using var stream = browserFile.OpenReadStream(maxAllowedSize: maxFileSize);
        using var ms = new MemoryStream();
        await stream.CopyToAsync(ms);
        return ms.ToArray();
    }

    private void SetBase64ImageSource(string contentType, byte[] image)
    {
        var base64String = Convert.ToBase64String(image);
        base64ImageSource = $"data:{contentType};base64,{base64String}";
    }

    private bool IsFileSizeValid(long fileSize)
    {
        errorMessage = null;
        if (fileSize > maxFileSize)
        {
            var oneMegaByte = 1024m * 1024m;
            var fileSizeInMB = fileSize / oneMegaByte;
            errorMessage = $"File has {fileSizeInMB:N2}MB, but maximum upload size is 2MB";
        }
        return errorMessage is null;
    }
}

Call Flow for Image Classification

sequenceDiagram
    participant U as User (Browser)
    participant R as CoffeeDetection.razor
    participant M as CoffeeDetectionModel
    participant E as PredictionEngine

    U->>R: Selects an image file (InputFile)
    R->>R: LoadFileAsync() triggered
    R->>R: Reads image โ†’ byte[]
    R->>R: SetBase64ImageSource() (display)
    R->>M: Predict(ModelInput { ImageSource: byte[] })
    M->>E: predEngine.Predict(input)
    E-->>M: ModelOutput { PredictedLabel: "coffee" }
    M-->>R: output.PredictedLabel
    R->>M: PredictAllLabels(input)
    M-->>R: IOrderedEnumerable (coffee: 98%, goat: 1.2%, other: 0.8%)
    R-->>U: Displays detected content + all percentages

Predicting All Labels

The mechanism is identical to Data Classification:

public static IOrderedEnumerable<KeyValuePair<string, float>> PredictAllLabels(ModelInput input)
{
    var predEngine = PredictEngine.Value;
    var result = predEngine.Predict(input);
    return GetSortedScoresWithLabels(result);
}

private static IEnumerable<string> GetLabels(ModelOutput result)
{
    var schema = PredictEngine.Value.OutputSchema;
    var labelColumn = schema.GetColumnOrNull("Label");

    if (labelColumn == null)
        throw new Exception("Label column not found.");

    var keyNames = new VBuffer<ReadOnlyMemory<char>>();
    labelColumn.Value.GetKeyValues(ref keyNames);
    return keyNames.DenseValues().Select(x => x.ToString());
}

Example result for a coffee image:

coffee - 98.00%
goat   -  1.20%
other  -  0.80%

4. Final Project Structure

Complete Architecture

WiredBrain.WebApp/
โ”‚
โ”œโ”€โ”€ ๐Ÿ“„ Program.cs                              โ† Blazor configuration
โ”œโ”€โ”€ ๐Ÿ“„ WiredBrain.WebApp.csproj                โ† NuGet packages
โ”‚
โ”œโ”€โ”€ ๐Ÿง  LanguageDetectionModel.mbconfig         โ† Model Builder config
โ”œโ”€โ”€ ๐Ÿง  LanguageDetectionModel.mlnet            โ† Binary model
โ”œโ”€โ”€ ๐Ÿ“ LanguageDetectionModel.consumption.cs   โ† Prediction API
โ”œโ”€โ”€ ๐Ÿ“ LanguageDetectionModel.training.cs      โ† Retraining code
โ”œโ”€โ”€ ๐Ÿ“ LanguageDetectionModel.evaluate.cs      โ† PFI evaluation
โ”‚
โ”œโ”€โ”€ ๐Ÿง  CoffeeDetectionModel.mbconfig           โ† Model Builder config
โ”œโ”€โ”€ ๐Ÿง  CoffeeDetectionModel.mlnet              โ† Binary model
โ”œโ”€โ”€ ๐Ÿ“ CoffeeDetectionModel.consumption.cs     โ† Prediction API
โ”œโ”€โ”€ ๐Ÿ“ CoffeeDetectionModel.training.cs        โ† Retraining code
โ”‚
โ””โ”€โ”€ Components/
    โ”œโ”€โ”€ Layout/
    โ”‚   โ”œโ”€โ”€ NavMenu.razor                      โ† Navigation menu
    โ”‚   โ””โ”€โ”€ MainLayout.razor
    โ””โ”€โ”€ Pages/
        โ”œโ”€โ”€ Home.razor                         โ† Home page
        โ”œโ”€โ”€ LanguageDetection.razor            โ† Scenario 1
        โ””โ”€โ”€ CoffeeDetection.razor              โ† Scenario 2

Program.cs Configuration

using WiredBrain.WebApp.Components;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

Comparison of the Two ML Scenarios

graph TB
    subgraph "๐Ÿ“ Language Detection"
        LD1["Input: text (string)"]
        LD2["Algorithm: LbfgsMaximumEntropyMulti\nor SdcaMaximumEntropyMulti"]
        LD3["Output: predicted language\n(German / French / Italian / English / Spanish)"]
        LD4["Data: CSV with columns\nLanguage + Message"]
        LD1 --> LD2 --> LD3
    end

    subgraph "โ˜• Coffee Detection"
        CD1["Input: image (byte[])"]
        CD2["Algorithm: ImageClassification\n(Transfer Learning / Deep Learning)"]
        CD3["Output: predicted category\n(coffee / goat / other)"]
        CD4["Data: image folders\nby label"]
        CD1 --> CD2 --> CD3
    end
AspectLanguage DetectionCoffee Detection
Scenario typeData ClassificationImage Classification
Data formatCSV (tabular text)Image folders
Input typestring (message)byte[] (image)
EnvironmentLocal CPULocal GPU (recommended)
Main packageMicrosoft.MLMicrosoft.ML + Microsoft.ML.Vision + SciSharp.TensorFlow.Redist
evaluate.cs fileโœ… YesโŒ No
Easy retrainingAdd CSV rowsAdd images to folders

5. Summary and Key Concepts

Glossary

TermDefinition
Machine Learning (ML)Branch of AI enabling machines to learn from data
ML.NETMicrosoftโ€™s open-source ML framework for .NET developers
AutoMLAutomated Machine Learning โ€” automates algorithm selection, tuning, and evaluation
Model BuilderVisual Studio graphical extension for creating ML models with ML.NET
MLContextCentral context for all ML.NET operations
IDataViewLazy and composable data structure for ML.NET
ITransformerRepresents a trained ML model capable of transforming data
PredictionEnginePrediction engine for single-item inference (one prediction at a time)
PipelineSequence of transformations + training in ML.NET
FeatureInput variable used to make a prediction
LabelOutput variable to predict
Binary ClassificationClassification into 2 categories (0/1, positive/negative)
Multiclass ClassificationClassification into 3+ categories
Transfer LearningReusing a pre-trained model for a new task
CUDACompute Unified Device Architecture โ€” NVIDIA framework for GPU computing
cuDNNCUDA Deep Neural Network โ€” library of primitives for deep learning
PFIPermutation Feature Importance โ€” measures feature importance
.mbconfigJSON configuration file for Model Builder
.mlnetBinary file containing the trained ML model

Complete ML Development Process with Model Builder

flowchart TD
    A["๐ŸŽฏ Define the problem\n(e.g.: detect language)"] --> B["๐Ÿ“Š Prepare the data\n(CSV or image folders)"]
    B --> C["โž• Add > Machine Learning Model\n(create the .mbconfig)"]
    C --> D["๐ŸŽฌ Select the scenario\n(Data / Image Classification)"]
    D --> E["๐Ÿ–ฅ๏ธ Choose the environment\n(CPU / GPU / Azure)"]
    E --> F["๐Ÿ“ Specify the data\n(CSV file or folder)"]
    F --> G["โšก Train the model\n(AutoML explores algorithms)"]
    G --> H["๐Ÿ” Evaluate the model\n(test examples)"]
    H --> I{Satisfied?}
    I -- No --> G
    I -- Yes --> J["๐Ÿ’ป Consume the model\n(copy generated code)"]
    J --> K["๐Ÿš€ Integrate into the application\n(Blazor, WPF, API, etc.)"]

    style A fill:#4A90D9,color:#fff
    style B fill:#4A90D9,color:#fff
    style C fill:#9B59B6,color:#fff
    style D fill:#9B59B6,color:#fff
    style E fill:#9B59B6,color:#fff
    style F fill:#9B59B6,color:#fff
    style G fill:#E8A838,color:#fff
    style H fill:#E8A838,color:#fff
    style I fill:#aaa,color:#fff
    style J fill:#27AE60,color:#fff
    style K fill:#27AE60,color:#fff

Key Points to Remember

  1. ML.NET is available via the Microsoft.ML NuGet package โ€” works offline, cross-platform.
  2. AutoML automates the complex parts: algorithm selection, hyperparameter tuning, evaluation.
  3. Model Builder enables using ML.NET without machine learning expertise, via a graphical interface.
  4. The .mbconfig file orchestrates the process โ€” it is an editable JSON.
  5. The .mlnet file is the binary model โ€” it must be copied to the output folder (CopyToOutputDirectory).
  6. PredictionEngine<TIn, TOut> is the single-item prediction engine โ€” initialized as Lazy<> for performance.
  7. Retraining a model = update the data + click โ€œTrain againโ€ โ€” without changing the application code.
  8. For Image Classification, ML.NET uses Transfer Learning via TensorFlow under the hood.
  9. PredictAllLabels() returns scores for all categories, not just the main prediction.
  10. To change the data path in a .mbconfig, edit the JSON directly or use the Model Builder interface.

Additional Resources


Search Terms

apps ยท ml.net ยท ml ยท platforms ยท deployment ยท machine ยท data ยท science ยท model ยท classification ยท image ยท builder ยท blazor ยท component ยท flow ยท generated ยท options ยท retraining ยท scenario ยท additional ยท auto-generated ยท call ยท classes ยท configuration

Interested in this course?

Contact us to book it or get a custom training plan for your team.