Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: TDEV-1050: Split ML.NET skill - Regression sub-skill

Predict a continuous value from multiple input tags using ML.NET FastTree Regression. The AI generates the full C# Script Class with multi-feature training pipeline, connects it to live tags, creates output tags, and configures model persistence.

Table of Contents

...

When to Use This Skill

  • User chose Regression — FastTree in the ML.NET router skill (Step 0)
  • Predicting a continuous value from 2–5 input features
  • User goals: energy consumption prediction, process output modeling, production rate from multiple inputs

Prerequisites

  • Solution open with input feature tags and label tag already created and receiving data
  • Solution target platform set to Multiplatform (ML.NET requires .NET 8+)
  • The user has confirmed: (1) which tags are features, (2) which tag is the label (value to predict), (3) what the predicted value represents

MCP Tools and Tables

Category

Items

Tools

get_table_schema, write_objects, get_objects

Tables

UnsTags, ScriptsClasses, ScriptsTasks

...

Step 1: Create Output Tags

Code Block
get_table_schema('UnsTags')
Code Block
{
  "table_type": "UnsTags",
  "data": [
    { "Name": "<AssetPath>/ML/PredictedValue", "DataType": "Double", "Description": "Model predicted value" },
    { "Name": "<AssetPath>/ML/LastPrediction", "DataType": "DateTime", "Description": "Timestamp of last prediction" }
  ]
}

Replace <AssetPath> with the actual asset folder path (e.g., Plant/Reactor1).

...

Step 2: Create the Script Class

ML.NET Namespace Declaration

Warning

Critical: The field AddMLNetNamespaces does not exist and is silently ignored. Always use NamespaceDeclarations.

Code Block
"NamespaceDeclarations": "Microsoft.ML;Microsoft.ML.Data;Microsoft.ML.Transforms;Microsoft.ML.Transforms.TimeSeries;Microsoft.ML.Transforms.Text;Microsoft.ML.Trainers;Microsoft.ML.TimeSeries"

Tag References Inside Script Classes

Always use @Tag. prefix. ML.NET expects float but FrameworX tags use double — always cast with (float) when feeding ML.NET and (double) when writing to tags.

FastTree Regression Pipeline

Code Block
languagec#
var pipeline = mlContext.Transforms.Concatenate("Features", "Feature1", "Feature2", "Feature3")
    .Append(mlContext.Regression.Trainers.FastTree(
        labelColumnName: "Label",
        featureColumnName: "Features",
        numberOfLeaves: 20,
        numberOfTrees: 100,
        minimumExampleCountPerLeaf: 10,
        learningRate: 0.2));

Output: float Score (predicted continuous value)

Full Class Example — Regression

Code Block
languagec#
public class ProcessData
{
    public float Feature1 { get; set; }  // e.g., Temperature
    public float Feature2 { get; set; }  // e.g., Pressure
    public float Feature3 { get; set; }  // e.g., Flow
    public float Label { get; set; }     // e.g., EnergyConsumption (what we predict)
}

public class RegressionPrediction
{
    public float Score { get; set; }
}

private static MLContext mlContext = new MLContext(seed: 0);
private static ITransformer model;
private static IDataView lastTrainingDataView;
private static PredictionEngine<ProcessData, RegressionPrediction> predictionEngine;
private static bool modelTrained = false;
private static List<ProcessData> trainingBuffer = new List<ProcessData>();
private const int MinTrainingSize = 200;
private static readonly string ModelPath = Path.Combine(@Info.GetExecutionPath(), "<ClassName>.mlnet");

public void Predict(double input1, double input2, double input3, double label)
{
    trainingBuffer.Add(new ProcessData
    {
        Feature1 = (float)input1,
        Feature2 = (float)input2,
        Feature3 = (float)input3,
        Label = (float)label
    });

    if (!modelTrained && trainingBuffer.Count >= MinTrainingSize)
        TrainModel();

    if (modelTrained)
        RunPrediction(input1, input2, input3);
}

public void LoadModel()
{
    if (File.Exists(ModelPath))
    {
        model = mlContext.Model.Load(ModelPath, out _);
        predictionEngine = mlContext.Model.CreatePredictionEngine<ProcessData, RegressionPrediction>(model);
        modelTrained = true;
    }
}

private void TrainModel()
{
    lastTrainingDataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
    var pipeline = mlContext.Transforms.Concatenate("Features",
            nameof(ProcessData.Feature1),
            nameof(ProcessData.Feature2),
            nameof(ProcessData.Feature3))
        .Append(mlContext.Regression.Trainers.FastTree(
            labelColumnName: "Label",
            featureColumnName: "Features",
            numberOfLeaves: 20,
            numberOfTrees: 100,
            minimumExampleCountPerLeaf: 10,
            learningRate: 0.2));

    model = pipeline.Fit(lastTrainingDataView);
    predictionEngine = mlContext.Model.CreatePredictionEngine<ProcessData, RegressionPrediction>(model);
    modelTrained = true;
    SaveModel();
}

private void SaveModel()
{
    mlContext.Model.Save(model, lastTrainingDataView.Schema, ModelPath);
}

private void RunPrediction(double input1, double input2, double input3)
{
    var input = new ProcessData
    {
        Feature1 = (float)input1,
        Feature2 = (float)input2,
        Feature3 = (float)input3
    };
    var result = predictionEngine.Predict(input);

    @Tag.<AssetPath>/ML/PredictedValue.Value = (double)result.Score;
    @Tag.<AssetPath>/ML/LastPrediction.Value = DateTime.Now;
}

Note on training data: The Predict() method accepts a label parameter during training. This is the known actual value the model learns to predict. After training, the label is not needed for prediction-only calls. The AI should adapt the method signature based on whether the user has a label tag or trains from historical data.

...

Step 3: Write the Class via MCP

Code Block
get_table_schema('ScriptsClasses')
Code Block
{
  "table_type": "ScriptsClasses",
  "data": [
    {
      "Name": "<ClassName>",
      "Code": "CSharp",
      "Domain": "Server",
      "ClassContent": "Methods",
      "NamespaceDeclarations": "Microsoft.ML;Microsoft.ML.Data;Microsoft.ML.Transforms;Microsoft.ML.Transforms.TimeSeries;Microsoft.ML.Transforms.Text;Microsoft.ML.Trainers;Microsoft.ML.TimeSeries",
      "Contents": "<AI-generated C# code from full class example above>"
    }
  ]
}
Warning

Field names matter: Code = language (not Language), Contents = code body (not Code). Wrong names = silent data loss.

...

Step 4: Create the Trigger

Task (Periodic) — best for multi-input models

Code Block
get_table_schema('ScriptsTasks')
Code Block
{
  "table_type": "ScriptsTasks",
  "data": [
    {
      "Name": "ML_Predict_Periodic",
      "Language": "CSharp",
      "Execution": "Periodic",
      "Period": 5000,
      "Code": "@Script.Class.<ClassName>.Predict(\n    @Tag.Plant/Reactor1/Temperature.Value,\n    @Tag.Plant/Reactor1/Pressure.Value,\n    @Tag.Plant/Reactor1/Flow.Value,\n    @Tag.Plant/Reactor1/EnergyConsumption.Value);"
    }
  ]
}
Warning

The @ prefix is mandatory when referencing runtime objects inside ScriptsTasks. Without it: CS0234.

ServerStartup — always wire LoadModel

Read the existing ServerStartup task first (document object — read-modify-write), then append:

Code Block
languagec#
Script.Class.<ClassName>.LoadModel();

...

Step 5: Verify

  1. Confirm Multiplatform — ML.NET requires .NET 8+. Instruct the user: “Solution → Settings → Target Platform = Multiplatform, then Product → Modify.”
  2. Do NOT start the runtime automatically.
  3. Wait for training — needs MinTrainingSize data points (default 200 for regression)
  4. Check output tags — verify LastPrediction updates and PredictedValue is reasonable

...

Common Pitfalls

Mistake

Why It Happens

How to Avoid

Missing ML.NET namespaces

Used AddMLNetNamespaces: true

Always set NamespaceDeclarations

CS0234 in ScriptsTasks

Missing @ prefix

Always @Script.Class.Name.Predict()

CS0246 Class not found

Build order issue

Set BuildOrder: "1" on ML Script Class

System.Math load error

.NET 4.8 target

Switch to Multiplatform

Wrong data types

ML.NET=float, tags=double

Cast (float)/(double)

Model lost on restart

SaveModel/LoadModel missing

Include both + ServerStartup

PredictionEngine null

Forgot to create after loading

Call CreatePredictionEngine in LoadModel

Feature count mismatch

Concatenate names don't match data class

Use nameof() for feature names in Concatenate

...

Children Display