Core MACHINE LEARNING - ML.NET

 

Core MACHINE LEARNING - ML.NET 

SALARY PREDICTION WEBAPI

Part 1: Project Overview

❓ What are we building?

We're building a Salary Prediction Web API that uses Machine Learning (ML.NET) to predict an employee's salary based on their years of experience. The model learns from historical data (experience vs. salary) and then predicts salaries for new inputs.

🤔 Why are we building this?

  • Real-world application: HR departments and job seekers use similar models for salary estimation

  • Regression problem: This is a classic example of predicting a continuous value (salary)

  • Learn ML basics: Introduces regression algorithms, data pipelines, and prediction engines

🛠️ How: Key ML Concepts

ConceptWhat It MeansWhy It Matters
FeaturesInput data (Years of Experience)What the model uses to make predictions
LabelOutput/target value (Salary)What the model learns to predict
Training DataHistorical data with known salariesThe "textbook" the model studies
RegressionPredicting continuous numerical valuesPerfect for salary, price, temperature predictions
PipelineSeries of data transformations + algorithmDefines the complete training process
Prediction EngineMakes predictions from trained modelProduction-ready inference
FastTreeDecision tree-based regression algorithmWorks well with small datasets

Part 2: Project Setup

❓ Creating the Visual Studio project with the correct framework and installing required NuGet packages.

Creating the Visual Studio project with the correct framework and installing required NuGet packages.

🤔 Why these choices?

  • .NET 8.0: Latest LTS (Long-Term Support) version

  • Microsoft.ML 4.0.1: Core ML.NET library

  • Microsoft.ML.FastTree 4.0.1: FastTree regression algorithm

🛠️ Setup

Step 1: Create the Project in Visual Studio 2022

  1. Open Visual Studio 2022

  2. Click Create a new project

  3. Search for "ASP.NET Core Web API"

  4. Select it and click Next

  5. Configure the project:

    • Project Name: ML_SalaryPredictionAPI

    • Location: Choose your preferred folder

    • Solution Name: ML_SalaryPredictionAPI (or change it)

    • Click Next

  6. Additional Information:

    • Framework: .NET 8.0 (Long-Term Support)

    • Check ✅ "Use controllers" (we'll use traditional controllers)

    • Check ✅ "Enable OpenAPI support" (for Swagger)

    • Uncheck ❌ "Use minimal APIs"

    • Click Create

Step 2: Install NuGet Packages

Method 1: Using Package Manager Console

text
Tools → NuGet Package Manager → Package Manager Console

Run these commands:

powershell
Install-Package Microsoft.ML -Version 4.0.1
Install-Package Microsoft.ML.FastTree -Version 4.0.1

Method 2: Using .NET CLI

bash
dotnet add package Microsoft.ML --version 4.0.1
dotnet add package Microsoft.ML.FastTree --version 4.0.1

Method 3: Using Visual Studio UI

  1. Right-click project → Manage NuGet Packages

  2. Browse tab → Search for "Microsoft.ML"

  3. Select version 4.0.1 → Click Install

  4. Search for "Microsoft.ML.FastTree"

  5. Select version 4.0.1 → Click Install


Part 3: Data Models

❓ What are Data Models?

Data models are C# classes that define the shape of your data. They tell ML.NET what data looks like during training and prediction.

🤔 Why do we need three separate models?

ModelPurposeWhen Used
EmployeeDataHistorical training dataDuring model training
SalaryPredictionPrediction outputReturning results to API users
SalaryRequestAPI input formatReceiving data from API clients

🛠️ How: Step 3 - Create Models

Create the Models folder:

  1. Right-click the project → AddNew Folder

  2. Name it Models


File 1: Models/EmployeeData.cs

Purpose: Represents historical training data with known salaries.

csharp
namespace ML_SalaryPredictionAPI.Models;

/// <summary>
/// Represents historical employee data used for training the ML model
/// This is the "training data" that the model learns from
/// </summary>
public class EmployeeData
{
    /// <summary>
    /// Feature: Years of work experience
    /// This is what the model uses to make predictions
    /// In ML.NET, features are the input variables
    /// </summary>
    public float YearsExperience { get; set; }

    /// <summary>
    /// Label: Actual salary amount
    /// This is what the model tries to predict (the "correct answer")
    /// During training, the model learns patterns between features and labels
    /// </summary>
    public float Salary { get; set; }
}

Comments Explained:

  • Feature: Input variable (YearsExperience)

  • Label: Output/target variable (Salary) - the "truth" we want to predict

  • The model learns: "If someone has X years experience, their salary is approximately Y"


File 2: Models/SalaryPrediction.cs

Purpose: Defines the structure of prediction results returned to the API user.

csharp
using Microsoft.ML.Data;

namespace ML_SalaryPredictionAPI.Models;

/// <summary>
/// Represents the prediction output returned to the API consumer
/// Contains the predicted salary from the ML model
/// </summary>
public class SalaryPrediction
{
    /// <summary>
    /// ML.NET stores regression predictions in a column named "Score"
    /// The ColumnName attribute maps this property to the Score column
    /// </summary>
    [ColumnName("Score")]
    public float PredictedSalary { get; set; }
}

Comments Explained:

  • [ColumnName("Score")]: ML.NET automatically names prediction results "Score"

  • This attribute maps our property to the correct ML.NET output

  • Regression problems predict continuous values (like salary)


File 3: Models/SalaryRequest.cs

Purpose: Defines what the API expects from the user/client.

csharp
namespace ML_SalaryPredictionAPI.Models;

/// <summary>
/// Input model for API requests
/// Defines what data the client needs to send
/// </summary>
public class SalaryRequest
{
    /// <summary>
    /// Years of experience provided by the API consumer
    /// This is the only input needed for salary prediction
    /// </summary>
    public float YearsExperience { get; set; }
}

Comments Explained:

  • This is the API contract - what users send to our endpoint

  • Only one parameter required: YearsExperience

  • Separation of concerns: API input is different from training data


Part 4: Service Interface

❓ What is an Interface?

An interface defines a contract for behavior. It specifies what methods a class must implement, without defining how.

🤔 Why use an Interface?

BenefitExplanation
Loose couplingControllers depend on abstractions, not concrete classes
TestabilityEasy to mock for unit testing
FlexibilityCan swap implementations without changing controllers
Dependency InjectionWorks perfectly with ASP.NET Core's DI container

🛠️ How: Step 4 - Create Service Interface

Create Services folder:

  1. Right-click the project → AddNew Folder

  2. Name it Services


File: Services/ISalaryPredictionService.cs

csharp
using ML_SalaryPredictionAPI.Models;

namespace ML_SalaryPredictionAPI.Services;

/// <summary>
/// Interface defining the contract for salary prediction services
/// Any class implementing this must provide a Predict method
/// </summary>
public interface ISalaryPredictionService
{
    /// <summary>
    /// Predicts salary based on years of experience
    /// </summary>
    /// <param name="request">Contains years of experience</param>
    /// <returns>Predicted salary</returns>
    SalaryPrediction Predict(SalaryRequest request);
}

Comments Explained:

  • This is a contract that all prediction services must follow

  • Enables dependency injection: Controllers can use ISalaryPredictionService without knowing which implementation they're using

  • Makes the code pluggable: We could switch to a different ML algorithm later


Part 5: Prediction Service

❓ What is the Prediction Service?

This is the core ML logic - it builds, trains, and uses the salary prediction model.

🤔 Why is this class important?

ResponsibilityWhat it Does
ML ContextInitializes the ML.NET environment
Training DataProvides historical data for learning
Pipeline BuildingDefines transformations and algorithm
Model TrainingFits the model to training data
Prediction EngineMakes predictions on new data

🛠️ How: Step 5 - Create the Service

File: Services/SalaryPredictionService.cs

csharp
using Microsoft.ML;
using ML_SalaryPredictionAPI.Models;

namespace ML_SalaryPredictionAPI.Services;

/// <summary>
/// Implementation of salary prediction service using ML.NET
/// Handles model training and prediction
/// </summary>
/// <remarks>
/// This service:
/// 1. Loads training data (experience -> salary)
/// 2. Trains a regression model using FastTree algorithm
/// 3. Creates a prediction engine for production use
/// 4. Provides Predict method for API consumption
/// </remarks>
public class SalaryPredictionService : ISalaryPredictionService
{
    // ============================================
    // PRIVATE FIELDS
    // ============================================

    /// <summary>
    /// Prediction Engine: Thread-safe? No! 
    /// Since we're using it in a singleton service, we need to be careful
    /// In this implementation, we're fine because we only call Predict sequentially
    /// For production with high concurrency, use PredictionEnginePool
    /// </summary>
    private readonly PredictionEngine<EmployeeData, SalaryPrediction> _predictionEngine;

    // ============================================
    // CONSTRUCTOR: Training happens here!
    // ============================================

    /// <summary>
    /// Constructor trains the model when the service is created
    /// This happens ONCE at application startup
    /// </summary>
    public SalaryPredictionService()
    {
        // STEP 1: Create ML Context
        // MLContext is the entry point for all ML.NET operations
        // Think of it as the "factory" that creates everything ML-related
        MLContext mlContext = new MLContext();

        // STEP 2: Prepare Training Data
        // This is our "textbook" - the model studies these examples
        // Each row shows: "with X years experience, salary was Y"
        List<EmployeeData> trainingData = new()
        {
            // Data Pattern: Experience -> Salary
            // Shows a clear trend: more experience = higher salary
            new EmployeeData { YearsExperience = 1, Salary = 25000 },
            new EmployeeData { YearsExperience = 2, Salary = 30000 },
            new EmployeeData { YearsExperience = 3, Salary = 38000 },
            new EmployeeData { YearsExperience = 4, Salary = 45000 },
            new EmployeeData { YearsExperience = 5, Salary = 52000 },
            new EmployeeData { YearsExperience = 6, Salary = 60000 },
            new EmployeeData { YearsExperience = 7, Salary = 70000 },
            new EmployeeData { YearsExperience = 8, Salary = 82000 }
        };

        // STEP 3: Convert to IDataView
        // IDataView is ML.NET's internal data format for efficient processing
        IDataView data = mlContext.Data.LoadFromEnumerable(trainingData);

        // STEP 4: Build the Training Pipeline
        // The pipeline is a "recipe" of transformations + algorithm
        var pipeline =
            // 4a: Copy Salary column to "Label"
            // ML.NET expects the target value to be in a column named "Label"
            mlContext.Transforms.CopyColumns(
                outputColumnName: "Label",  // ML.NET standard name for target
                inputColumnName: nameof(EmployeeData.Salary)  // Our Salary column
            )
            
            // 4b: Create Features
            // ML algorithms work with vectors of numbers (Features)
            // YearsExperience is a single feature
            .Append(
                mlContext.Transforms.Concatenate(
                    "Features",  // ML.NET standard name for input features
                    nameof(EmployeeData.YearsExperience)  // Our feature
                )
            )

            // 4c: Add Regression Algorithm (FastTree)
            // FastTree is a decision tree-based algorithm
            // Good for: small datasets, non-linear relationships
            .Append(
                mlContext.Regression.Trainers.FastTree(
                    labelColumnName: "Label",      // What to predict
                    featureColumnName: "Features", // Input features
                    numberOfLeaves: 20,            // Tree complexity (higher = more complex)
                    numberOfTrees: 100,            // Number of trees in forest (higher = more accurate)
                    minimumExampleCountPerLeaf: 1  // Minimum samples per leaf
                )
            );

        // STEP 5: Train the Model
        // Fit() executes the pipeline on the training data
        // This is where the "learning" happens!
        ITransformer model = pipeline.Fit(data);

        // STEP 6: Create Prediction Engine
        // The prediction engine is used for making predictions on new data
        // It encapsulates the model for easy prediction
        _predictionEngine = mlContext.Model
            .CreatePredictionEngine<EmployeeData, SalaryPrediction>(model);
    }

    // ============================================
    // PUBLIC METHODS
    // ============================================

    /// <summary>
    /// Predicts salary based on years of experience
    /// </summary>
    /// <param name="request">Contains years of experience</param>
    /// <returns>Predicted salary</returns>
    /// <remarks>
    /// Step-by-step process:
    /// 1. Convert API request to training data format
    /// 2. Use prediction engine to make prediction
    /// 3. Return result
    /// </remarks>
    public SalaryPrediction Predict(SalaryRequest request)
    {
        // Convert the API request to the format the prediction engine expects
        EmployeeData employee = new EmployeeData
        {
            YearsExperience = request.YearsExperience
        };

        // Use the prediction engine to make the prediction
        // The engine uses the trained model to predict salary
        return _predictionEngine.Predict(employee);
    }
}

🔍 Deep Dive: Understanding the Pipeline

Each Step Explained:

StepOperationWhat it DoesWhy
1CopyColumnsCopies Salary to "Label"ML.NET expects the target in "Label" column
2ConcatenateCombines features into "Features" vectorML algorithms need features as one array
3FastTreeThe learning algorithmFinds patterns between Features and Label

FastTree Parameters Explained:

  • numberOfLeaves: 20 - Number of leaves per tree. More leaves = more complex model.

  • numberOfTrees: 100 - Number of trees in the forest. More trees = better accuracy but slower.

  • minimumExampleCountPerLeaf: 1 - Minimum samples per leaf. Low values = more detailed model.


Part 6: API Controller

❓ What is an API Controller?

The controller handles HTTP requests, receives input, calls the prediction service, and returns results.

🤔 Why separate controller from service?

ReasonExplanation
Separation of ConcernsController handles HTTP, service handles ML logic
TestabilityCan test ML logic without HTTP context
ReusabilityService can be used by other parts of application
MaintainabilityChanges to ML logic don't affect API contract

🛠️ How: Step 6 - Create the Controller

File: Controllers/SalaryController.cs

csharp
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ML_SalaryPredictionAPI.Models;
using ML_SalaryPredictionAPI.Services;

namespace ML_SalaryPredictionAPI.Controllers;

/// <summary>
/// API Controller for salary prediction
/// Provides endpoints for predicting salaries based on experience
/// </summary>
[Route("api/[controller]")]  // URL: /api/salary
[ApiController]
public class SalaryController : ControllerBase
{
    // ============================================
    // PRIVATE FIELDS
    // ============================================

    /// <summary>
    /// The prediction service injected via dependency injection
    /// Using the interface for loose coupling
    /// </summary>
    private readonly ISalaryPredictionService _service;

    // ============================================
    // CONSTRUCTOR: Dependency Injection
    // ============================================

    /// <summary>
    /// Constructor receives the service via dependency injection
    /// </summary>
    /// <param name="service">The salary prediction service</param>
    public SalaryController(ISalaryPredictionService service)
    {
        _service = service ?? throw new ArgumentNullException(nameof(service));
    }

    // ============================================
    // ENDPOINT METHODS
    // ============================================

    /// <summary>
    /// Predicts salary based on years of experience
    /// </summary>
    /// <param name="request">Contains years of experience</param>
    /// <returns>Predicted salary</returns>
    /// <remarks>
    /// Sample request:
    /// POST /api/salary
    /// {
    ///     "yearsExperience": 5
    /// }
    /// 
    /// Sample response:
    /// {
    ///     "predictedSalary": 52000.0
    /// }
    /// </remarks>
    [HttpPost]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public IActionResult Predict([FromBody] SalaryRequest request)
    {
        // Validate input
        if (request.YearsExperience < 0)
        {
            return BadRequest(new 
            { 
                error = "Years of experience cannot be negative",
                code = "INVALID_INPUT" 
            });
        }

        if (request.YearsExperience > 50)
        {
            return BadRequest(new 
            { 
                error = "Years of experience seems too high. Please enter a realistic value.",
                code = "INVALID_INPUT" 
            });
        }

        // Make prediction
        SalaryPrediction prediction = _service.Predict(request);

        // Return result with additional context
        return Ok(new
        {
            predictedSalary = Math.Round(prediction.PredictedSalary, 2),
            yearsExperience = request.YearsExperience,
            currency = "USD",
            datePredicted = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC")
        });
    }
}

Comments Explained:

  • [Route("api/[controller]")]: Replaces [controller] with "Salary" → /api/salary

  • [ApiController]: Enables automatic model validation

  • [FromBody]: Tells ASP.NET to read the request from the request body

  • ISalaryPredictionService: Using interface for dependency injection

  • Input validation: Prevents invalid values (negative or unrealistic experience)


Part 7: Program Configuration

❓ What is Program.cs?

The entry point of the application. It configures services and the HTTP pipeline.

🤔 Why each configuration?

ConfigurationPurpose
AddControllers()Registers API controllers
AddSwaggerGen()Enables API documentation
AddSingleton()Registers service as single instance
UseSwagger()Serves API documentation
MapControllers()Maps controller routes

🛠️ How: Step 7 - Configure Program.cs

File: Program.cs

csharp
using ML_SalaryPredictionAPI.Services;

var builder = WebApplication.CreateBuilder(args);

// ============================================
// STEP 1: Add Services to Container
// ============================================

// Add controllers
builder.Services.AddControllers();

// Add Swagger/OpenAPI for API documentation
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
    {
        Title = "Salary Prediction API",
        Version = "v1",
        Description = "API for predicting salaries based on years of experience using ML.NET",
        Contact = new Microsoft.OpenApi.Models.OpenApiContact
        {
            Name = "Your Team",
            Email = "support@yourcompany.com"
        }
    });
});

// ============================================
// STEP 2: Register Custom Services
// ============================================

// Register SalaryPredictionService as Singleton
// Singleton: One instance shared across entire application
// The model is trained ONCE when the service is created
builder.Services.AddSingleton<ISalaryPredictionService, SalaryPredictionService>();

// Add CORS (optional - allows frontend applications to call our API)
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll",
        policy =>
        {
            policy.AllowAnyOrigin()
                  .AllowAnyMethod()
                  .AllowAnyHeader();
        });
});

// ============================================
// STEP 3: Build the Application
// ============================================

var app = builder.Build();

// ============================================
// STEP 4: Configure HTTP Pipeline
// ============================================

// Swagger in development only
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "Salary Prediction API v1");
        c.RoutePrefix = string.Empty;  // Makes Swagger available at root URL
    });
}

// Redirect HTTP to HTTPS for security
app.UseHttpsRedirection();

// Enable CORS
app.UseCors("AllowAll");

// Add authorization (none for now, but placeholder for future)
app.UseAuthorization();

// Map controller routes
app.MapControllers();

// Optional: Redirect root to Swagger
app.MapGet("/", () => Results.Redirect("/index.html"));

// ============================================
// STEP 5: Run the Application
// ============================================

// Log startup message
var logger = app.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Salary Prediction API started successfully!");
logger.LogInformation("Swagger UI available at the root URL");

app.Run();

Part 8: Testing the API

❓ What is testing?

Testing verifies our API works correctly and returns accurate predictions.

🤔 Why test?

  • Validate functionality: Ensure the API does what it should

  • Catch errors: Find issues before users do

  • Document behavior: Tests serve as documentation

🛠️ How: Step 8 - Test the API

Method 1: Using Swagger UI

  1. Run the application:

    • Press F5 or Ctrl+F5

    • Browser opens automatically to https://localhost:5001

  2. Find the POST endpoint:

    • Locate the /api/Salary endpoint

    • Click Try it out

  3. Enter test data:

    json
    {
      "yearsExperience": 5
    }
  4. Execute and see results:

    json
    {
      "predictedSalary": 52000.0,
      "yearsExperience": 5,
      "currency": "USD",
      "datePredicted": "2024-01-15 14:30:00 UTC"
    }

Method 2: Using Postman

  1. Create new POST request

  2. URL: https://localhost:5001/api/salary

  3. Headers: Content-Type: application/json

  4. Body (raw JSON):

    json
    {
      "yearsExperience": 3
    }
  5. Click Send

Method 3: Using curl

bash
curl -X POST https://localhost:5001/api/salary \
     -H "Content-Type: application/json" \
     -d '{"yearsExperience": 4}'

Test Scenarios:

Test CaseInputExpected Output
NormalyearsExperience: 2Salary ~ 30000
HigheryearsExperience: 7Salary ~ 70000
ZeroyearsExperience: 0Salary ~ pattern predicts
NegativeyearsExperience: -1BadRequest error
Too HighyearsExperience: 100BadRequest error

Expected Outputs (based on training data):

Years ExperiencePredicted Salary
1~25,000
2~30,000
3~38,000
4~45,000
5~52,000
6~60,000
7~70,000
8~82,000

Part 9: Complete Project Structure

text
ML_SalaryPredictionAPI/
│
├── Controllers/
│   └── SalaryController.cs          # API endpoints
│
├── Models/
│   ├── EmployeeData.cs              # Training data model
│   ├── SalaryPrediction.cs          # Prediction output model
│   └── SalaryRequest.cs             # API input model
│
├── Services/
│   ├── ISalaryPredictionService.cs  # Service interface
│   └── SalaryPredictionService.cs   # ML.NET implementation
│
├── Properties/
│   └── launchSettings.json          # Launch configuration
│
├── Program.cs                       # Application entry point
├── appsettings.json                 # Configuration
├── appsettings.Development.json     # Dev configuration
└── ML_SalaryPredictionAPI.csproj    # Project file

Part 10: Troubleshooting 

Common Issues and Solutions

IssueSymptomSolution
Package not foundMicrosoft.ML not recognizedInstall via NuGet Package Manager
FastTree not availableReference errorInstall Microsoft.ML.FastTree package
Port conflict"Address already in use"Change port in launchSettings.json
Training failsException in constructorCheck training data has valid values
Prediction engine nullNullReferenceExceptionCheck service is registered as Singleton
CORS errorFrontend cannot call APIAdd CORS policy in Program.cs
Swagger not showing404 on /swaggerAdd UseSwagger() in Program.cs
Model not trained_predictionEngine is nullService should be instantiated first

Debug Tips

  1. Check Logs:

    • Console output shows startup messages

    • Check if model training completes

  2. Verify Training Data:

    csharp
    Console.WriteLine($"Training data count: {trainingData.Count}");
  3. Test Prediction in Debug:

    csharp
    // Add in controller
    var testPrediction = _service.Predict(new SalaryRequest { YearsExperience = 5 });
    Console.WriteLine($"Test prediction: {testPrediction.PredictedSalary}");
  4. View Detailed Errors:

    • Set ASPNETCORE_ENVIRONMENT to Development

    • Detailed error pages will appear


Summary: What You've Learned

✅ Core ML.NET Concepts

  • MLContext: Starting point for all ML operations

  • IDataView: ML.NET's data format for training

  • Pipeline: Series of transformations + algorithm

  • FastTree: Regression algorithm for small datasets

  • Prediction Engine: Makes predictions from trained model

✅ Development Practices

  • Layered Architecture: Models → Services → Controllers

  • Dependency Injection: Loose coupling and testability

  • Interface Design: Contracts for flexibility

  • Input Validation: Protect against invalid data

✅ API Design

  • RESTful Endpoints: POST /api/salary

  • Swagger Documentation: Self-documenting API

  • Error Handling: User-friendly error messages

  • Response Enrichment: Adding context (currency, timestamp)

Comments