Skip to content

Commit dd8a59f

Browse files
qiuyizcopybara-github
authored andcommitted
Add multiobjective experimenter factory and extend capabilities for state analyzers.
PiperOrigin-RevId: 647087713
1 parent ac3a421 commit dd8a59f

File tree

7 files changed

+115
-7
lines changed

7 files changed

+115
-7
lines changed

Diff for: vizier/_src/benchmarks/analyzers/convergence_curve.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -355,8 +355,9 @@ def __init__(
355355
Args:
356356
metric_informations:
357357
reference_value: Reference point value from which hypervolume is computed,
358-
with shape that is broadcastable with (dim,). If None, this computes the
359-
minimum of each objective as the reference point.
358+
with shape that is broadcastable with (dim,). Note that the sign is
359+
flipped for minimization metrics. If None, this computes the minimum of
360+
each objective as the reference point.
360361
num_vectors: Number of vectors from which hypervolume is computed.
361362
infer_origin_factor: When inferring the reference point, set origin to be
362363
minimum value - factor * (range).

Diff for: vizier/_src/benchmarks/analyzers/state_analyzer.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def to_curve(
9292
cls,
9393
states: list[benchmarks.BenchmarkState],
9494
flip_signs_for_min: bool = False,
95+
reference_value: Optional[np.ndarray] = None,
9596
) -> convergence_curve.ConvergenceCurve:
9697
"""Generates a ConvergenceCurve from a batch of BenchmarkStates.
9798
@@ -101,6 +102,7 @@ def to_curve(
101102
states: List of BenchmarkStates.
102103
flip_signs_for_min: If true, flip signs of curve when it is MINIMIZE
103104
metric.
105+
reference_value: Reference value for multiobjective hypervolume curve.
104106
105107
Returns:
106108
Convergence curve with batch size equal to length of states.
@@ -122,10 +124,15 @@ def to_curve(
122124
)
123125
state_trials = state.algorithm.supporter.GetTrials()
124126

127+
if problem_statement.is_single_objective:
128+
kwargs = {'flip_signs_for_min': flip_signs_for_min}
129+
else:
130+
kwargs = {'reference_value': reference_value}
131+
125132
converter = (
126133
convergence_curve.MultiMetricCurveConverter.from_metrics_config(
127134
problem_statement.metric_information,
128-
flip_signs_for_min=flip_signs_for_min,
135+
**kwargs,
129136
)
130137
)
131138
curve = converter.convert(state_trials)

Diff for: vizier/_src/benchmarks/analyzers/state_analyzer_test.py

+35
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import itertools
1818
import json
19+
import numpy as np
1920
from vizier import benchmarks as vzb
2021
from vizier import pyvizier as vz
2122
from vizier._src.algorithms.designers import grid
@@ -57,6 +58,40 @@ def test_empty_curve_error(self):
5758
with self.assertRaisesRegex(ValueError, 'Empty'):
5859
state_analyzer.BenchmarkStateAnalyzer.to_curve([])
5960

61+
def test_multiobj_curve_conversion(self):
62+
dim = 10
63+
experimenter_factories = {
64+
'sphere': experimenters.BBOBExperimenterFactory('Sphere', dim),
65+
'discus': experimenters.BBOBExperimenterFactory('Discus', dim),
66+
}
67+
multi_experimenter = experimenters.CombinedExperimenterFactory(
68+
base_factories=experimenter_factories
69+
)()
70+
71+
def _designer_factory(config: vz.ProblemStatement, seed: int):
72+
return random.RandomDesigner(config.search_space, seed=seed)
73+
74+
benchmark_state_factory = vzb.DesignerBenchmarkStateFactory(
75+
designer_factory=_designer_factory, experimenter=multi_experimenter
76+
)
77+
num_trials = 20
78+
runner = vzb.BenchmarkRunner(
79+
benchmark_subroutines=[vzb.GenerateAndEvaluate()],
80+
num_repeats=num_trials,
81+
)
82+
83+
states = []
84+
num_repeats = 3
85+
for i in range(num_repeats):
86+
bench_state = benchmark_state_factory(seed=i)
87+
runner.run(bench_state)
88+
states.append(bench_state)
89+
90+
curve = state_analyzer.BenchmarkStateAnalyzer.to_curve(
91+
states, reference_value=np.asarray([-1])
92+
)
93+
self.assertEqual(curve.ys.shape, (num_repeats, num_trials))
94+
6095
def test_different_curve_error(self):
6196
exp1 = experimenters.BBOBExperimenterFactory('Sphere', dim=2)()
6297
exp2 = experimenters.BBOBExperimenterFactory('Sphere', dim=3)()

Diff for: vizier/_src/benchmarks/experimenters/experimenter_factory.py

+41
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from vizier import pyvizier as vz
2727
from vizier._src.benchmarks.experimenters import discretizing_experimenter
2828
from vizier._src.benchmarks.experimenters import experimenter
29+
from vizier._src.benchmarks.experimenters import multiobjective_experimenter
2930
from vizier._src.benchmarks.experimenters import noisy_experimenter
3031
from vizier._src.benchmarks.experimenters import normalizing_experimenter
3132
from vizier._src.benchmarks.experimenters import numpy_experimenter
@@ -37,6 +38,7 @@
3738

3839
BBOB_FACTORY_KEY = 'bbob_factory'
3940
SINGLE_OBJECTIVE_FACTORY_KEY = 'single_objective_factory'
41+
MULTI_OBJECTIVE_FACTORY_KEY = 'multi_objective_factory'
4042

4143

4244
class ExperimenterFactory(abc.ABC):
@@ -248,3 +250,42 @@ def recover(
248250
return SingleObjectiveExperimenterFactory(
249251
base_factory=base_factory, **metadata_dict
250252
)
253+
254+
255+
@attr.define
256+
class CombinedExperimenterFactory(SerializableExperimenterFactory):
257+
"""Factory for a multi-objective Experimenter that combines multiple single-objective experimenters.
258+
259+
Attributes:
260+
base_factories:
261+
"""
262+
263+
base_factories: dict[str, SerializableExperimenterFactory] = attr.field()
264+
265+
def __call__(self) -> experimenter.Experimenter:
266+
"""Creates the MultiObjective Experimenter."""
267+
exptrs = {name: factory() for name, factory in self.base_factories.items()}
268+
return multiobjective_experimenter.MultiObjectiveExperimenter(exptrs)
269+
270+
def dump(self) -> vz.Metadata:
271+
metadata = vz.Metadata()
272+
metadata_dict = {
273+
name: factory.dump() for name, factory in self.base_factories.items()
274+
}
275+
metadata[MULTI_OBJECTIVE_FACTORY_KEY] = json.dumps(
276+
metadata_dict, cls=json_utils.NumpyEncoder
277+
)
278+
return metadata
279+
280+
@classmethod
281+
def recover(cls, metadata: vz.Metadata) -> 'CombinedExperimenterFactory':
282+
# TODO: Use generics to make this work.
283+
metadata_dict = json.loads(
284+
metadata[MULTI_OBJECTIVE_FACTORY_KEY], cls=json_utils.NumpyDecoder
285+
)
286+
return CombinedExperimenterFactory(
287+
base_factories={
288+
name: SerializableExperimenterFactory.recover(factory_dump)
289+
for name, factory_dump in metadata_dict.items()
290+
}
291+
)

Diff for: vizier/_src/benchmarks/experimenters/experimenter_factory_test.py

+23
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,29 @@ def testSingleObjectiveFactory(self):
7474
exptr.evaluate([t])
7575
self.assertEqual(t.status, pyvizier.TrialStatus.COMPLETED)
7676

77+
def testCombinedFactory(self):
78+
dim = 5
79+
experimenter_factories = {
80+
'sphere': experimenter_factory.BBOBExperimenterFactory('Sphere', dim),
81+
'discus': experimenter_factory.BBOBExperimenterFactory('Discus', dim),
82+
}
83+
exptr = experimenter_factory.CombinedExperimenterFactory(
84+
base_factories=experimenter_factories
85+
)()
86+
87+
parameters = exptr.problem_statement().search_space.parameters
88+
self.assertLen(parameters, dim)
89+
90+
t = pyvizier.Trial(
91+
parameters={
92+
param.name: float(index) for index, param in enumerate(parameters)
93+
}
94+
)
95+
exptr.evaluate([t])
96+
self.assertIn('sphere', t.final_measurement_or_die.metrics)
97+
self.assertIn('discus', t.final_measurement_or_die.metrics)
98+
self.assertEqual(t.status, pyvizier.TrialStatus.COMPLETED)
99+
77100
def testSingleObjectiveFactoryDiscrete(self):
78101
dim = 5
79102
bbob_factory = experimenter_factory.BBOBExperimenterFactory(

Diff for: vizier/_src/benchmarks/experimenters/multiobjective_experimenter.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ def __init__(
6161

6262
metric_infos = []
6363
# Keeps track of the underlying metric information name of each extpr.
64-
self._previous_names = {}
64+
self._exptr_to_metric = {}
6565
for name, exptr in exptrs.items():
6666
metric_info = exptr.problem_statement().metric_information.item()
67-
self._previous_names[name] = metric_info.name
67+
self._exptr_to_metric[name] = metric_info.name
6868
metric_info.name = name
6969
metric_infos.append(metric_info)
7070

@@ -80,12 +80,12 @@ def evaluate(self, suggestions: Sequence[pyvizier.Trial]):
8080
measurements = [pyvizier.Measurement() for _ in suggestions]
8181
for name, exptr in self._exptrs.items():
8282
exptr.evaluate(suggestions_copy)
83-
previous_name = self._previous_names[name]
83+
exptr_metric_name = self._exptr_to_metric[name]
8484
for idx, copied in enumerate(suggestions_copy):
8585
measurement = measurements[idx]
8686
assert copied.final_measurement is not None
8787
measurement.metrics[name] = copied.final_measurement.metrics[
88-
previous_name
88+
exptr_metric_name
8989
]
9090

9191
for suggestion, measurement in zip(suggestions, measurements):

Diff for: vizier/benchmarks/experimenters/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from vizier._src.benchmarks.experimenters.discretizing_experimenter import DiscretizingExperimenter
2626
from vizier._src.benchmarks.experimenters.experimenter import Experimenter
2727
from vizier._src.benchmarks.experimenters.experimenter_factory import BBOBExperimenterFactory
28+
from vizier._src.benchmarks.experimenters.experimenter_factory import CombinedExperimenterFactory
2829
from vizier._src.benchmarks.experimenters.experimenter_factory import ExperimenterFactory
2930
from vizier._src.benchmarks.experimenters.experimenter_factory import SerializableExperimenterFactory
3031
from vizier._src.benchmarks.experimenters.experimenter_factory import SingleObjectiveExperimenterFactory

0 commit comments

Comments
 (0)