Packages

Production-ready hyperparameter optimization for Elixir with high Optuna parity. Leverages BEAM fault tolerance, real-time dashboards, and native distributed computing.

Current section

Files

Jump to
scout lib pruner patient.ex
Raw

lib/pruner/patient.ex

defmodule Scout.Pruner.PatientPruner do
@moduledoc """
Prunes trials that haven't improved for a specified number of steps.
Equivalent to Optuna's PatientPruner. Allows trials to continue even if they're
temporarily underperforming, only pruning after sustained lack of improvement.
"""
@behaviour Scout.Pruner
# Default implementation for missing callbacks
def assign_bracket(_trial_index, state), do: {0, state}
def keep?(_study_id, _trial_id, _bracket, _step, state), do: {true, state}
@default_patience 10
@default_min_delta 0.0
def init(opts \\ %{}) do
%{
patience: Map.get(opts, :patience, @default_patience),
min_delta: Map.get(opts, :min_delta, @default_min_delta),
best_values: %{}, # Track best value per trial
steps_without_improvement: %{} # Track patience counter per trial
}
end
def should_prune?(_study_id, trial_id, _step, value, state) do
# Get current best value for this trial
best_value = Map.get(state.best_values, trial_id, :infinity)
# Check if this is an improvement (assuming minimization)
improved = value < (best_value - state.min_delta)
# Update state
{new_best, new_counter} = if improved do
# New best value found, reset counter
{value, 0}
else
# No improvement, increment counter
counter = Map.get(state.steps_without_improvement, trial_id, 0) + 1
{best_value, counter}
end
# Update state maps
new_state = %{state |
best_values: Map.put(state.best_values, trial_id, new_best),
steps_without_improvement: Map.put(state.steps_without_improvement, trial_id, new_counter)
}
# Prune if patience exceeded
should_prune = new_counter >= state.patience
# Clean up state if trial is being pruned
final_state = if should_prune do
%{new_state |
best_values: Map.delete(new_state.best_values, trial_id),
steps_without_improvement: Map.delete(new_state.steps_without_improvement, trial_id)
}
else
new_state
end
{should_prune, final_state}
end
@doc """
Cleans up state for a completed trial.
"""
def cleanup_trial(state, trial_id) do
%{state |
best_values: Map.delete(state.best_values, trial_id),
steps_without_improvement: Map.delete(state.steps_without_improvement, trial_id)
}
end
@doc """
Gets the current patience counter for a trial.
"""
def get_patience_counter(state, trial_id) do
Map.get(state.steps_without_improvement, trial_id, 0)
end
@doc """
Gets the best value seen so far for a trial.
"""
def get_best_value(state, trial_id) do
Map.get(state.best_values, trial_id)
end
end