Skip to content

Commit

Permalink
refactor(SearchSpace): removes a lot of methods from SearchSpace (#150
Browse files Browse the repository at this point in the history
)
  • Loading branch information
eddiebergman authored Oct 21, 2024
1 parent 26724bc commit 3078fbc
Show file tree
Hide file tree
Showing 16 changed files with 361 additions and 180 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def eval(
if len(space.fidelities) > 0 and self.optimize_on_max_fidelity:
assert len(space.fidelities) == 1
fid_name, fid = next(iter(space.fidelities.items()))
_x = [space.from_dict({**e.hp_values(), fid_name: fid.upper}) for e in x]
_x = [space.from_dict({**e._values, fid_name: fid.upper}) for e in x]
else:
_x = list(x)

Expand Down
5 changes: 1 addition & 4 deletions neps/optimizers/grid_search/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,9 @@
import torch

from neps.optimizers.base_optimizer import BaseOptimizer, SampledConfig
from neps.search_spaces import Categorical, Constant, Float, Integer
from neps.search_spaces.architecture.graph_grammar import GraphParameter
from neps.search_spaces.domain import UNIT_FLOAT_DOMAIN
from neps.search_spaces.hyperparameters.categorical import Categorical
from neps.search_spaces.hyperparameters.constant import Constant
from neps.search_spaces.hyperparameters.float import Float
from neps.search_spaces.hyperparameters.integer import Integer

if TYPE_CHECKING:
from neps.search_spaces.search_space import SearchSpace
Expand Down
11 changes: 9 additions & 2 deletions neps/optimizers/multi_fidelity/mf_bo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
from copy import deepcopy
from typing import TYPE_CHECKING, Any, Literal

from neps.search_spaces.functions import sample_one_old


def update_fidelity(config: SearchSpace, fidelity: int | float) -> SearchSpace:
assert config.fidelity is not None
config.fidelity.set_value(fidelity)
# TODO: Place holder until we can get rid of passing around search spaces
# as configurations
assert config.fidelity_name is not None
config._values[config.fidelity_name] = fidelity
return config


Expand Down Expand Up @@ -93,7 +99,7 @@ def _fit_models(self) -> None:
train_y = deepcopy(self.rung_histories[rung]["perf"])
# extract only the pending configurations that are at `rung`
pending_df = pending_df[pending_df.rung == rung]
pending_x = deepcopy(pending_df.config.values.tolist())
pending_x = deepcopy(pending_df["config"].values.tolist())
# update fidelity
fidelities = [self.rung_map[rung]] * len(pending_x)
pending_x = list(map(update_fidelity, pending_x, fidelities))
Expand Down Expand Up @@ -196,7 +202,8 @@ def sample_new_config(
elif self.sampling_policy is not None:
config = self.sampling_policy.sample(**self.sampling_args)
else:
config = self.pipeline_space.sample(
config = sample_one_old(
self.pipeline_space,
patience=self.patience,
user_priors=self.use_priors,
ignore_fidelity=True,
Expand Down
50 changes: 33 additions & 17 deletions neps/optimizers/multi_fidelity/sampling_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from neps.sampling.priors import Prior
from neps.sampling.samplers import Sampler
from neps.search_spaces.encoding import ConfigEncoder
from neps.search_spaces.functions import sample_one_old

if TYPE_CHECKING:
from botorch.acquisition.analytic import SingleTaskGP
Expand Down Expand Up @@ -64,8 +65,11 @@ def __init__(self, pipeline_space: SearchSpace):
super().__init__(pipeline_space=pipeline_space)

def sample(self, *args: Any, **kwargs: Any) -> SearchSpace:
return self.pipeline_space.sample(
patience=self.patience, user_priors=False, ignore_fidelity=True
return sample_one_old(
self.pipeline_space,
patience=self.patience,
user_priors=False,
ignore_fidelity=True,
)


Expand All @@ -88,8 +92,12 @@ def sample(self, *args: Any, **kwargs: Any) -> SearchSpace:
user_priors = False
if np.random.uniform() < self.fraction_from_prior:
user_priors = True
return self.pipeline_space.sample(
patience=self.patience, user_priors=user_priors, ignore_fidelity=True

return sample_one_old(
self.pipeline_space,
patience=self.patience,
user_priors=user_priors,
ignore_fidelity=True,
)


Expand Down Expand Up @@ -140,9 +148,11 @@ def sample_neighbour(
)

while True:
# sampling a config
config = self.pipeline_space.sample(
patience=self.patience, user_priors=False, ignore_fidelity=False
config = sample_one_old(
self.pipeline_space,
patience=self.patience,
user_priors=False,
ignore_fidelity=False,
)
# computing distance from incumbent
d = compute_config_dist(config, incumbent)
Expand Down Expand Up @@ -188,8 +198,11 @@ def sample( # noqa: PLR0912, C901, PLR0915
logger.info(f"Sampling from {policy} with weights (i, p, r)={prob_weights}")

if policy == "prior":
config = self.pipeline_space.sample(
patience=self.patience, user_priors=True, ignore_fidelity=True
config = sample_one_old(
self.pipeline_space,
patience=self.patience,
user_priors=True,
ignore_fidelity=True,
)
elif policy == "inc":
if (
Expand All @@ -201,7 +214,7 @@ def sample( # noqa: PLR0912, C901, PLR0915
user_priors = False

if inc is None:
inc = self.pipeline_space.sample_default_configuration().clone()
inc = self.pipeline_space.from_dict(self.pipeline_space.default_config)
logger.warning(
"No incumbent config found, using default as the incumbent."
)
Expand Down Expand Up @@ -251,7 +264,8 @@ def sample( # noqa: PLR0912, C901, PLR0915
f"Crossing over with user_priors={user_priors} with p={probs}"
)
# sampling a configuration either randomly or from a prior
_config = self.pipeline_space.sample(
_config = sample_one_old(
self.pipeline_space,
patience=self.patience,
user_priors=user_priors,
ignore_fidelity=True,
Expand All @@ -274,9 +288,11 @@ def sample( # noqa: PLR0912, C901, PLR0915
f"{{'mutation', 'crossover', 'hypersphere', 'gaussian'}}"
)
else:
# random
config = self.pipeline_space.sample(
patience=self.patience, user_priors=False, ignore_fidelity=True
config = sample_one_old(
self.pipeline_space,
patience=self.patience,
user_priors=False,
ignore_fidelity=True,
)
return config

Expand Down Expand Up @@ -316,8 +332,8 @@ def update_model(
pending_x: list[SearchSpace],
decay_t: float | None = None,
) -> None:
x_train = self._encoder.encode([config.hp_values() for config in train_x])
x_pending = self._encoder.encode([config.hp_values() for config in pending_x])
x_train = self._encoder.encode([config._values for config in train_x])
x_pending = self._encoder.encode([config._values for config in pending_x])
y_train = torch.tensor(train_y, dtype=torch.float64, device=self.device)

# TODO: Most of this just copies BO and the duplication can be replaced
Expand Down Expand Up @@ -377,7 +393,7 @@ def sample(
"""
# sampling random configurations
samples = [
self.pipeline_space.sample(user_priors=False, ignore_fidelity=True)
sample_one_old(self.pipeline_space, user_priors=False, ignore_fidelity=True)
for _ in range(SAMPLE_THRESHOLD)
]

Expand Down
30 changes: 18 additions & 12 deletions neps/optimizers/multi_fidelity/successive_halving.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
Integer,
SearchSpace,
)
from neps.search_spaces.functions import sample_one_old

if TYPE_CHECKING:
from neps.state.optimizer import BudgetInfo
Expand Down Expand Up @@ -371,14 +372,14 @@ def sample_new_config(
) -> SearchSpace:
# Samples configuration from policy or random
if self.sampling_policy is None:
config = self.pipeline_space.sample(
return sample_one_old(
self.pipeline_space,
patience=self.patience,
user_priors=self.use_priors,
ignore_fidelity=True,
)
else:
config = self.sampling_policy.sample(**self.sampling_args)
return config

return self.sampling_policy.sample(**self.sampling_args)

def _generate_new_config_id(self) -> int:
if len(self.observed_configs) == 0:
Expand All @@ -405,22 +406,27 @@ def get_config_and_ids(self) -> tuple[RawConfig, str, str | None]:
Returns:
[type]: [description]
"""
fidelity_name = self.pipeline_space.fidelity_name
assert fidelity_name is not None

rung_to_promote = self.is_promotable()
if rung_to_promote is not None:
# promotes the first recorded promotable config in the argsort-ed rung
row = self.observed_configs.iloc[self.rung_promotions[rung_to_promote][0]]
config = row["config"].clone()
rung = rung_to_promote + 1
# assigning the fidelity to evaluate the config at
config.fidelity.set_value(self.rung_map[rung])

config_values = config._values
config_values[fidelity_name] = self.rung_map[rung]

# updating config IDs
previous_config_id = f"{row.name}_{rung_to_promote}"
config_id = f"{row.name}_{rung}"
else:
rung_id = self.min_rung
# using random instead of np.random to be consistent with NePS BO
rng = random.Random(None) # TODO: Seeding

if (
self.use_priors
and self.sample_default_first
Expand All @@ -431,10 +437,10 @@ def get_config_and_ids(self) -> tuple[RawConfig, str, str | None]:
rung_id = self.max_rung
logger.info("Next config will be evaluated at target fidelity.")
logger.info("Sampling the default configuration...")
config = self.pipeline_space.sample_default_configuration()

config = self.pipeline_space.from_dict(self.pipeline_space.default_config)
elif rng.random() < self.random_interleave_prob:
config = self.pipeline_space.sample(
config = sample_one_old(
self.pipeline_space,
patience=self.patience,
user_priors=False, # sample uniformly random
ignore_fidelity=True,
Expand All @@ -443,13 +449,13 @@ def get_config_and_ids(self) -> tuple[RawConfig, str, str | None]:
config = self.sample_new_config(rung=rung_id)

fidelity_value = self.rung_map[rung_id]
assert config.fidelity is not None
config.fidelity.set_value(fidelity_value)
config_values = config._values
config_values[fidelity_name] = fidelity_value

previous_config_id = None
config_id = f"{self._generate_new_config_id()}_{rung_id}"

return config.hp_values(), config_id, previous_config_id
return config_values, config_id, previous_config_id

def _enhance_priors(self, confidence_score: dict[str, float] | None = None) -> None:
"""Only applicable when priors are given along with a confidence.
Expand Down
4 changes: 2 additions & 2 deletions neps/optimizers/multi_fidelity_prior/priorband.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def find_incumbent(self, rung: int | None = None) -> SearchSpace:
else:
# THIS block should not ever execute, but for runtime anomalies, if no
# incumbent can be extracted, the prior is treated as the incumbent
inc = self.pipeline_space.sample_default_configuration()
inc = self.pipeline_space.from_dict(self.pipeline_space.default_config)
logger.warning(
"Treating the prior as the incumbent. "
"Please check if this should not happen."
Expand Down Expand Up @@ -259,7 +259,7 @@ def _prior_to_incumbent_ratio_dynamic(self, rung: int) -> tuple[float, float]:
# requires at least eta completed configurations to begin computing scores
if len(self.rung_histories[rung]["config"]) >= self.eta:
# retrieve the prior
prior = self.pipeline_space.sample_default_configuration()
prior = self.pipeline_space.from_dict(self.pipeline_space.default_config)
# retrieve the global incumbent
inc = self.find_incumbent()
# subsetting the top 1/eta configs from the rung
Expand Down
25 changes: 12 additions & 13 deletions neps/optimizers/multi_fidelity_prior/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any
from typing import Any

import numpy as np
import torch
Expand All @@ -10,14 +10,12 @@
Categorical,
Constant,
GraphParameter,
Float,
Integer,
SearchSpace,
)
from neps.search_spaces.encoding import ConfigEncoder
from neps.search_spaces.hyperparameters.float import Float
from neps.search_spaces.hyperparameters.integer import Integer

if TYPE_CHECKING:
import pandas as pd
from neps.search_spaces.functions import sample_one_old, pairwise_dist


def update_fidelity(config: SearchSpace, fidelity: int | float) -> SearchSpace:
Expand Down Expand Up @@ -98,7 +96,7 @@ def custom_crossover(
Returns a configuration where each HP in config1 has `crossover_prob`% chance of
getting config2's value of the corresponding HP. By default, crossover rate is 50%.
"""
_existing = config1.hp_values()
_existing = config1._values

for _ in range(patience):
child_config = {}
Expand All @@ -114,7 +112,8 @@ def custom_crossover(
# fail safe check to handle edge cases where config1=config2 or
# config1 extremely local to config2 such that crossover fails to
# generate new config in a discrete (sub-)space
return config1.sample(
return sample_one_old(
config1,
patience=patience,
user_priors=False,
ignore_fidelity=True,
Expand All @@ -130,8 +129,8 @@ def compute_config_dist(config1: SearchSpace, config2: SearchSpace) -> float:
the Hamming distance of the categorical subspace.
"""
encoder = ConfigEncoder.from_parameters({**config1.numerical, **config1.categoricals})
configs = encoder.encode([config1.hp_values(), config2.hp_values()])
dist = encoder.pdist(configs, square_form=False)
configs = encoder.encode([config1._values, config2._values])
dist = pairwise_dist(configs, encoder, square_form=False)
return float(dist.item())


Expand All @@ -146,16 +145,16 @@ def compute_scores(
# TODO: This could lifted up and just done in the class itself
# in a vectorized form.
encoder = ConfigEncoder.from_space(config, include_fidelity=include_fidelity)
encoded_config = encoder.encode([config.hp_values()])
encoded_config = encoder.encode([config._values])

prior_dist = Prior.from_space(
prior,
center_values=prior.hp_values(),
center_values=prior._values,
include_fidelity=include_fidelity,
)
inc_dist = Prior.from_space(
inc,
center_values=inc.hp_values(),
center_values=inc._values,
include_fidelity=include_fidelity,
)

Expand Down
Loading

0 comments on commit 3078fbc

Please sign in to comment.