Current section
Files
Jump to
Current section
Files
lib/sampler/cmaes_simple.ex
defmodule Scout.Sampler.CmaesSimple do
@moduledoc """
Simple CMA-ES-inspired sampler for continuous optimization.
Much simpler than full CMA-ES but captures the key ideas:
- Population-based search
- Adaptive step size
- Learning from best performers
"""
def init(opts) do
%{
population_size: Map.get(opts, :population_size, 10),
elite_ratio: Map.get(opts, :elite_ratio, 0.5),
goal: Map.get(opts, :goal, :minimize),
step_size: Map.get(opts, :step_size, 1.0),
momentum: Map.get(opts, :momentum, 0.9),
min_obs: Map.get(opts, :min_obs, 5),
# Track mean and covariance
mean: %{},
velocity: %{},
generation: 0
}
end
def next(space_fun, ix, history, state) do
spec = space_fun.(ix)
# Use random sampling for initial population
if length(history) < state.min_obs do
params = Scout.SearchSpace.sample(spec)
{params, state}
else
# Extract parameter keys and ranges
param_keys = Map.keys(spec)
ranges = extract_ranges(spec)
# Get elite samples from history
elite = get_elite(history, state)
# Update distribution mean based on elite
new_mean = if elite == [] do
# Initialize mean at center of search space
Map.new(param_keys, fn k ->
{min_val, max_val} = Map.get(ranges, k, {0.0, 1.0})
{k, (min_val + max_val) / 2.0}
end)
else
compute_mean(elite, param_keys)
end
# Update velocity (momentum)
new_velocity = Map.new(param_keys, fn k ->
old_v = Map.get(state.velocity, k, 0.0)
old_m = Map.get(state.mean, k, Map.get(new_mean, k))
new_m = Map.get(new_mean, k)
{k, state.momentum * old_v + (1 - state.momentum) * (new_m - old_m)}
end)
# Sample around mean with adaptive step size
params = sample_from_distribution(new_mean, new_velocity, state.step_size, spec, ranges)
# Adaptive step size based on success rate
new_step_size = adapt_step_size(state.step_size, elite, history, state.goal)
# Update state
new_state = %{state |
mean: new_mean,
velocity: new_velocity,
step_size: new_step_size,
generation: state.generation + 1
}
{params, new_state}
end
end
defp extract_ranges(spec) do
Map.new(spec, fn {k, v} ->
case v do
{:uniform, min, max} -> {k, {min, max}}
{:log_uniform, min, max} -> {k, {:math.log(min), :math.log(max)}}
{:int, min, max} -> {k, {min, max}}
_ -> {k, {0.0, 1.0}}
end
end)
end
defp get_elite(history, state) do
n_elite = max(1, round(length(history) * state.elite_ratio))
sorted = case state.goal do
:minimize -> Enum.sort_by(history, & &1.score)
_ -> Enum.sort_by(history, & &1.score, :desc)
end
Enum.take(sorted, n_elite)
end
defp compute_mean(elite, param_keys) do
n = length(elite)
Map.new(param_keys, fn k ->
sum = Enum.reduce(elite, 0.0, fn trial, acc ->
acc + (Map.get(trial.params, k, 0.0) || 0.0)
end)
{k, sum / n}
end)
end
defp sample_from_distribution(mean, velocity, step_size, spec, ranges) do
Map.new(spec, fn {k, type} ->
mean_val = Map.get(mean, k, 0.0)
vel_val = Map.get(velocity, k, 0.0)
{min_val, max_val} = Map.get(ranges, k, {0.0, 1.0})
# Sample with momentum and noise
base = mean_val + vel_val * step_size
noise = :rand.normal() * step_size * abs(max_val - min_val) * 0.1
raw_val = base + noise
# Apply type constraints
val = case type do
{:uniform, min, max} ->
max(min, min(max, raw_val))
{:log_uniform, min, max} ->
log_val = max(:math.log(min), min(:math.log(max), raw_val))
:math.exp(log_val)
{:int, min, max} ->
round(max(min, min(max, raw_val)))
_ ->
raw_val
end
{k, val}
end)
end
defp adapt_step_size(current_step, elite, history, goal) do
# Simple 1/5 success rule adaptation
if length(history) < 10 do
current_step
else
recent = Enum.take(history, -10)
recent_best = case goal do
:minimize -> Enum.min_by(recent, & &1.score)
_ -> Enum.max_by(recent, & &1.score)
end
# Check if recent best is better than historical average
avg_score = Enum.sum(Enum.map(history, & &1.score)) / length(history)
improvement = case goal do
:minimize -> recent_best.score < avg_score * 0.8
_ -> recent_best.score > avg_score * 1.2
end
if improvement do
# Good progress, increase step size
min(current_step * 1.1, 2.0)
else
# Poor progress, decrease step size
max(current_step * 0.9, 0.1)
end
end
end
end