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 threshold.ex
Raw

lib/pruner/threshold.ex

defmodule Scout.Pruner.ThresholdPruner do
@moduledoc """
Prunes trials that don't meet specified thresholds at certain steps.
Equivalent to Optuna's ThresholdPruner. Useful when you have domain knowledge
about expected performance at specific checkpoints.
"""
@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_n_warmup_steps 0
def init(opts \\ %{}) do
%{
thresholds: Map.get(opts, :thresholds, %{}), # Map of step => threshold
n_warmup_steps: Map.get(opts, :n_warmup_steps, @default_n_warmup_steps),
direction: Map.get(opts, :direction, :minimize) # :minimize or :maximize
}
end
def should_prune?(_study_id, _trial_id, step, value, state) do
# Don't prune during warmup
if step < state.n_warmup_steps do
{false, state}
else
# Check if we have a threshold for this step
case Map.get(state.thresholds, step) do
nil ->
# No threshold for this step
{false, state}
threshold ->
# Check if value meets threshold based on direction
should_prune = case state.direction do
:minimize -> value > threshold # Prune if worse than threshold
:maximize -> value < threshold # Prune if worse than threshold
end
{should_prune, state}
end
end
end
@doc """
Creates a threshold pruner with linear interpolation between specified points.
## Example
# Define thresholds at specific steps
Scout.Pruner.ThresholdPruner.with_interpolation(
[{10, 0.8}, {20, 0.7}, {30, 0.6}],
direction: :minimize
)
"""
def with_interpolation(threshold_points, opts \\ []) do
# Sort points by step
sorted_points = Enum.sort_by(threshold_points, &elem(&1, 0))
# Generate interpolated thresholds
thresholds = generate_interpolated_thresholds(sorted_points)
init(%{
thresholds: thresholds,
n_warmup_steps: Keyword.get(opts, :n_warmup_steps, 0),
direction: Keyword.get(opts, :direction, :minimize)
})
end
defp generate_interpolated_thresholds([]), do: %{}
defp generate_interpolated_thresholds([{step, value}]), do: %{step => value}
defp generate_interpolated_thresholds(points) do
points
|> Enum.chunk_every(2, 1, :discard)
|> Enum.flat_map(fn [{step1, val1}, {step2, val2}] ->
# Linear interpolation between points
for s <- step1..step2 do
if s == step1 do
{s, val1}
else
t = (s - step1) / (step2 - step1)
{s, val1 + t * (val2 - val1)}
end
end
end)
|> Enum.into(%{})
end
@doc """
Creates a threshold pruner with exponential decay thresholds.
## Example
# Start at 1.0, decay to 0.1 over 100 steps
Scout.Pruner.ThresholdPruner.with_exponential_decay(
initial_threshold: 1.0,
final_threshold: 0.1,
decay_steps: 100,
direction: :minimize
)
"""
def with_exponential_decay(opts) do
initial = Keyword.fetch!(opts, :initial_threshold)
final = Keyword.fetch!(opts, :final_threshold)
steps = Keyword.fetch!(opts, :decay_steps)
direction = Keyword.get(opts, :direction, :minimize)
n_warmup = Keyword.get(opts, :n_warmup_steps, 0)
# Calculate decay rate
decay_rate = :math.log(final / initial) / steps
# Generate thresholds
thresholds = for s <- 1..steps, into: %{} do
threshold = initial * :math.exp(decay_rate * s)
{s, threshold}
end
init(%{
thresholds: thresholds,
n_warmup_steps: n_warmup,
direction: direction
})
end
@doc """
Creates a threshold pruner with step function thresholds.
## Example
# Different thresholds for different phases
Scout.Pruner.ThresholdPruner.with_step_function([
{0, 10, 0.9}, # Steps 0-10: threshold 0.9
{11, 20, 0.8}, # Steps 11-20: threshold 0.8
{21, 50, 0.7} # Steps 21-50: threshold 0.7
])
"""
def with_step_function(ranges, opts \\ []) do
thresholds = ranges
|> Enum.flat_map(fn {start_step, end_step, threshold} ->
for s <- start_step..end_step, do: {s, threshold}
end)
|> Enum.into(%{})
init(%{
thresholds: thresholds,
n_warmup_steps: Keyword.get(opts, :n_warmup_steps, 0),
direction: Keyword.get(opts, :direction, :minimize)
})
end
end