Table of Contents

User-defined pruner

A pruner decides when to stop an unpromising trial early, based on the intermediate values it has reported. This recipe shows how to write your own by subclassing BasePruner — here, an aggressive pruner that stops any trial in last place among the completed trials at the same step.

The pruner interface

Subclass BasePruner and implement one method:

public abstract bool Prune(Study study, FrozenTrial trial);   // true => prune this trial

Inside it you have the whole study: reach other trials with study.GetTrials(...), and read a trial's reported values from FrozenTrial.IntermediateValues (a step→value map) and FrozenTrial.LastStep.

Example: a LastPlacePruner

The pruner starts considering a trial only after a warmup of warmupSteps steps and warmupTrials completed trials, then prunes it if its latest score is worse than every completed trial's score at the same step.

using System;
using System.Linq;
using OptunaSharp;
using OptunaSharp.Pruners;

public sealed class LastPlacePruner : BasePruner
{
    private readonly int _warmupSteps;
    private readonly int _warmupTrials;

    public LastPlacePruner(int warmupSteps, int warmupTrials)
    {
        _warmupSteps = warmupSteps;
        _warmupTrials = warmupTrials;
    }

    public override bool Prune(Study study, FrozenTrial trial)
    {
        long? step = trial.LastStep;     // null until this trial has reported a value
        if (step is null)
            return false;

        double thisScore = trial.IntermediateValues[step.Value];

        // Scores from other completed trials at the same step.
        var completed = study.GetTrials(deepcopy: false, states: new[] { TrialState.Complete });
        var otherScores = completed
            .Where(t => t.IntermediateValues.ContainsKey(step.Value))
            .Select(t => t.IntermediateValues[step.Value])
            .OrderBy(s => s)
            .ToList();

        // Prune once past warmup, and only if strictly worse than all peers.
        if (step.Value >= _warmupSteps && otherScores.Count > _warmupTrials)
        {
            if (thisScore < otherScores[0])
            {
                Console.WriteLine($"prune() True: Trial {trial.Number}, Step {step}, Score {thisScore}");
                return true;
            }
        }
        return false;
    }
}

Confirm it works with a maximizing objective that reports a score at each of five steps:

double Objective(Trial trial)
{
    string loss  = trial.SuggestCategorical("loss", new[] { "hinge", "log_loss", "perceptron" });
    double alpha = trial.SuggestFloat("alpha", 1e-5, 1e-3, log: true);

    double score = 0;
    for (int step = 0; step < 5; step++)
    {
        score = TrainOneStepAndScore(loss, alpha, step);   // your model here
        trial.Report(score, step);
        if (trial.ShouldPrune())
            throw new TrialPruned();
    }
    return score;
}

var pruner = new LastPlacePruner(warmupSteps: 1, warmupTrials: 5);
var study = Optuna.CreateStudy(direction: StudyDirection.Maximize, pruner: pruner);
study.Optimize(Objective, nTrials: 50);

When ShouldPrune() returns true, throw TrialPruned; OptunaSharp records the trial as Pruned and takes its last reported intermediate value as the trial's value.

Relationship to Optuna

Optuna (Python) OptunaSharp
class MyPruner(optuna.pruners.BasePruner) class MyPruner : BasePruner
def prune(self, study, trial) -> bool public override bool Prune(Study study, FrozenTrial trial)
trial.last_step trial.LastStep
trial.intermediate_values[step] trial.IntermediateValues[step]
study.get_trials(deepcopy=False, states=(TrialState.COMPLETE,)) study.GetTrials(deepcopy: false, states: new[] { TrialState.Complete })
raise optuna.TrialPruned() throw new TrialPruned()

See also

Pruners · Wilcoxon pruner · User-defined sampler · the API reference.