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
| Concept | What It Means | Why It Matters |
|---|---|---|
| Features | Input data (Years of Experience) | What the model uses to make predictions |
| Label | Output/target value (Salary) | What the model learns to predict |
| Training Data | Historical data with known salaries | The "textbook" the model studies |
| Regression | Predicting continuous numerical values | Perfect for salary, price, temperature predictions |
| Pipeline | Series of data transformations + algorithm | Defines the complete training process |
| Prediction Engine | Makes predictions from trained model | Production-ready inference |
| FastTree | Decision tree-based regression algorithm | Works 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
Open Visual Studio 2022
Click Create a new project
Search for "ASP.NET Core Web API"
Select it and click Next
Configure the project:
Project Name:
ML_SalaryPredictionAPILocation: Choose your preferred folder
Solution Name:
ML_SalaryPredictionAPI(or change it)Click Next
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
Tools → NuGet Package Manager → Package Manager Console
Run these commands:
Install-Package Microsoft.ML -Version 4.0.1 Install-Package Microsoft.ML.FastTree -Version 4.0.1
Method 2: Using .NET CLI
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
Right-click project → Manage NuGet Packages
Browse tab → Search for "Microsoft.ML"
Select version 4.0.1 → Click Install
Search for "Microsoft.ML.FastTree"
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?
| Model | Purpose | When Used |
|---|---|---|
EmployeeData | Historical training data | During model training |
SalaryPrediction | Prediction output | Returning results to API users |
SalaryRequest | API input format | Receiving data from API clients |
🛠️ How: Step 3 - Create Models
Create the Models folder:
Right-click the project → Add → New Folder
Name it
Models
File 1: Models/EmployeeData.cs
Purpose: Represents historical training data with known salaries.
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.
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.
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:
YearsExperienceSeparation 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?
| Benefit | Explanation |
|---|---|
| Loose coupling | Controllers depend on abstractions, not concrete classes |
| Testability | Easy to mock for unit testing |
| Flexibility | Can swap implementations without changing controllers |
| Dependency Injection | Works perfectly with ASP.NET Core's DI container |
🛠️ How: Step 4 - Create Service Interface
Create Services folder:
Right-click the project → Add → New Folder
Name it
Services
File: Services/ISalaryPredictionService.cs
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
ISalaryPredictionServicewithout knowing which implementation they're usingMakes 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?
| Responsibility | What it Does |
|---|---|
| ML Context | Initializes the ML.NET environment |
| Training Data | Provides historical data for learning |
| Pipeline Building | Defines transformations and algorithm |
| Model Training | Fits the model to training data |
| Prediction Engine | Makes predictions on new data |
🛠️ How: Step 5 - Create the Service
File: Services/SalaryPredictionService.cs
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:
| Step | Operation | What it Does | Why |
|---|---|---|---|
| 1 | CopyColumns | Copies Salary to "Label" | ML.NET expects the target in "Label" column |
| 2 | Concatenate | Combines features into "Features" vector | ML algorithms need features as one array |
| 3 | FastTree | The learning algorithm | Finds 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?
| Reason | Explanation |
|---|---|
| Separation of Concerns | Controller handles HTTP, service handles ML logic |
| Testability | Can test ML logic without HTTP context |
| Reusability | Service can be used by other parts of application |
| Maintainability | Changes to ML logic don't affect API contract |
🛠️ How: Step 6 - Create the Controller
File: Controllers/SalaryController.cs
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 bodyISalaryPredictionService: Using interface for dependency injectionInput 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?
| Configuration | Purpose |
|---|---|
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
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
Run the application:
Press F5 or Ctrl+F5
Browser opens automatically to
https://localhost:5001
Find the POST endpoint:
Locate the
/api/SalaryendpointClick Try it out
Enter test data:
{ "yearsExperience": 5 }
Execute and see results:
{ "predictedSalary": 52000.0, "yearsExperience": 5, "currency": "USD", "datePredicted": "2024-01-15 14:30:00 UTC" }
Method 2: Using Postman
Create new POST request
URL:
https://localhost:5001/api/salaryHeaders:
Content-Type: application/jsonBody (raw JSON):
{ "yearsExperience": 3 }
Click Send
Method 3: Using curl
curl -X POST https://localhost:5001/api/salary \ -H "Content-Type: application/json" \ -d '{"yearsExperience": 4}'
Test Scenarios:
| Test Case | Input | Expected Output |
|---|---|---|
| Normal | yearsExperience: 2 | Salary ~ 30000 |
| Higher | yearsExperience: 7 | Salary ~ 70000 |
| Zero | yearsExperience: 0 | Salary ~ pattern predicts |
| Negative | yearsExperience: -1 | BadRequest error |
| Too High | yearsExperience: 100 | BadRequest error |
Expected Outputs (based on training data):
| Years Experience | Predicted 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
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
| Issue | Symptom | Solution |
|---|---|---|
| Package not found | Microsoft.ML not recognized | Install via NuGet Package Manager |
| FastTree not available | Reference error | Install Microsoft.ML.FastTree package |
| Port conflict | "Address already in use" | Change port in launchSettings.json |
| Training fails | Exception in constructor | Check training data has valid values |
| Prediction engine null | NullReferenceException | Check service is registered as Singleton |
| CORS error | Frontend cannot call API | Add CORS policy in Program.cs |
| Swagger not showing | 404 on /swagger | Add UseSwagger() in Program.cs |
| Model not trained | _predictionEngine is null | Service should be instantiated first |
Debug Tips
Check Logs:
Console output shows startup messages
Check if model training completes
Verify Training Data:
Console.WriteLine($"Training data count: {trainingData.Count}");
Test Prediction in Debug:
// Add in controller var testPrediction = _service.Predict(new SalaryRequest { YearsExperience = 5 }); Console.WriteLine($"Test prediction: {testPrediction.PredictedSalary}");
View Detailed Errors:
Set
ASPNETCORE_ENVIRONMENTtoDevelopmentDetailed 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
Post a Comment