ML.NET
OptunaSharp.Integration.ML.NET provides two integration surfaces: MLNetEstimator for
cross-validation search over ML.NET pipelines, and six alternative parameter-importance evaluators
backed by ML.NET tree ensembles. It targets .NET 8 and .NET 10 and references Microsoft.ML and
Microsoft.ML.FastTree. LightGBM has its own integration package.
dotnet add package OptunaSharp.Integration.ML.NET
Search an ML.NET pipeline
MLNetEstimator bundles an MLContext, an IDataView, a pipeline factory, a scorer, and fixed
cross-validation splits. OptunaSearchCV in Core samples the parameter distributions and asks the
estimator to evaluate each candidate. The scorer must return a value where higher is better.
This is a complete console example. Integer distributions return boxed long values, so the
factory converts them before assigning ML.NET's integer options.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.ML;
using Microsoft.ML.Data;
using OptunaSharp.Distributions;
using OptunaSharp.Integration;
var ml = new MLContext(seed: 0);
var rows = Enumerable.Range(0, 180).Select(i => new RegressionRow
{
Features = new[] { i / 180f, (i % 17) / 17f },
Label = 2 * (i / 180f) - (i % 17) / 17f
});
IDataView data = ml.Data.LoadFromEnumerable(rows);
IEstimator<ITransformer> BuildPipeline(IReadOnlyDictionary<string, object?> parameters)
=> ml.Regression.Trainers.FastTree(
labelColumnName: "Label",
featureColumnName: "Features",
numberOfLeaves: Convert.ToInt32(parameters["leaves"]),
numberOfTrees: 20,
learningRate: Convert.ToDouble(parameters["learning_rate"]));
double Score(MLContext context, ITransformer model, IDataView validation)
=> context.Regression.Evaluate(model.Transform(validation)).RSquared;
var estimator = new MLNetEstimator(ml, data, BuildPipeline, Score, folds: 3, seed: 0);
var space = new Dictionary<string, BaseDistribution>
{
["leaves"] = new IntDistribution(4, 24),
["learning_rate"] = new FloatDistribution(0.05, 0.3, log: true)
};
var search = new OptunaSearchCV(estimator, space, nTrials: 5, randomState: 0);
search.Fit();
Console.WriteLine($"Best cross-validation R²: {search.BestScore}");
IDataView predictions = estimator.Predict(data);
ml.Model.Save(estimator.BestModel!, data.Schema, "best-model.zip");
public sealed class RegressionRow
{
[VectorType(2)]
public float[] Features { get; set; } = Array.Empty<float>();
public float Label { get; set; }
}
The final prediction call above uses training data only to demonstrate the API. Evaluate model
quality on a separate held-out dataset. For a loss scorer, return its negative; a caller-supplied
Study must also have StudyDirection.Maximize.
Data, folds, errors, and refitting
| Option or operation | Current behavior |
|---|---|
folds |
Defaults to 5; values below 2 throw NumSharp.ValueError |
seed |
Passed to ML.NET's fold splitter; separate from OptunaSearchCV.randomState and trainer seeds |
samplingKeyColumnName |
Forwarded to ML.NET's CrossValidationSplit; key related rows consistently when choosing a split |
| Pipeline factory | Called once per fold for each candidate, and again for the final refit; return a fresh pipeline |
errorScore |
Fit/scoring exceptions become this fold score (default NaN); factory exceptions occur outside that handler |
returnTrainScore |
Also scores each fitted fold model on its training split |
refit: true |
Fits the winning configuration on the full data; stores it in BestModel |
Predict / Transform / Score |
Operate on the refitted model; throw before refit |
Fold definitions are created in the estimator constructor and reused across candidates. Folds run
sequentially within CrossValidate; trial-level parallelism is controlled by OptunaSearchCV.nJobs.
When enabling parallel trials, ensure your pipeline factory, scorer, and captured application state
can be invoked concurrently. Refit happens after the search.
OptunaSearchCV aggregates fold results using a NaN-aware mean. A mixture of valid and failed folds
can therefore produce a score from fewer folds. Inspect trial attributes and CvResults when
diagnosing failures. Refitting is not covered by the per-fold errorScore handler.
The native scikit-learn-style guide describes the shared OptunaSearchCV result
surface and ICrossValidationEstimator contract. This ML.NET backend uses ML.NET's trainers and
splits; it does not promise numeric parity with Python scikit-learn. It also does not implement
incremental partial_fit pruning; see training-loop pruning.
The fold error handler also catches TrialPruned thrown by fitting or scoring and converts it to
errorScore. Putting a pruning callback inside this adapter's pipeline therefore does not by
itself mark the Optuna trial pruned. A custom ICrossValidationEstimator that supports pruning
must allow that exception to reach Study.Optimize.
Parameter importance backends
These evaluators fit a surrogate model to completed optimization trials, not to your original
training data. Pass them to ImportanceFunctions.GetParamImportances.
| Evaluators | Namespace | Ensemble and interpretation |
|---|---|---|
FastForestFanovaImportanceEvaluator, FastForestMeanDecreaseImpurityImportanceEvaluator |
OptunaSharp.Importance.Forests |
Bagged random forest; same model family as canonical Optuna importance, different fitted trees and values |
FastTreeFanovaImportanceEvaluator, FastTreeMeanDecreaseImpurityImportanceEvaluator |
OptunaSharp.Importance.Booster |
Experimental gradient-boosted surrogate; residual trees change the meaning of the importance |
FastTreeTweedieFanovaImportanceEvaluator, FastTreeTweedieMeanDecreaseImpurityImportanceEvaluator |
OptunaSharp.Importance.Booster |
Experimental Tweedie-loss booster; no Optuna value-parity claim |
using OptunaSharp.Importance;
using OptunaSharp.Importance.Forests;
var importance = ImportanceFunctions.GetParamImportances(
search.Study, new FastForestFanovaImportanceEvaluator(seed: 0));
Each evaluator accepts (nTrees = 64, maxDepth = 64, seed = null), its corresponding complete
trainer options object, or an Action<Options> that modifies reproducible defaults. For example:
var evaluator = new FastForestFanovaImportanceEvaluator(options =>
{
options.NumberOfTrees = 100;
options.FeatureFraction = 0.7;
options.Seed = 0;
options.NumberOfThreads = 1;
});
The evaluator owns the label and feature column names. Weight and row-group columns do not exist
in its transformed-trial schema; setting them is rejected. Full options otherwise control the
trainer, so keep a fixed seed and one training thread when you need reproducibility. The fANOVA
and MDI calculations reuse Core's implementation, but ML.NET grows different trees. Core's
FanovaImportanceEvaluator and MeanDecreaseImpurityImportanceEvaluator remain the canonical
scikit-learn-compatible implementations; the ML.NET satellite is optional.
Implementation and verification
The estimator defines fold handling and refit behavior. The end-to-end test fits a real FastTree pipeline, searches its parameters, refits, predicts, and scores. The search tests cover direction validation, reproducibility, result attributes, and refit options.