Skip to content

Commit e53e77d

Browse files
authored
Merge pull request #318 from malun22/PSO
PSO: Infer the relative search space automatically
2 parents 1497b6d + 7ea4601 commit e53e77d

File tree

3 files changed

+35
-29
lines changed

3 files changed

+35
-29
lines changed

package/samplers/pso/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Particle Swarm Optimization (PSO) is a population-based stochastic optimizer ins
1515
1616
> Note: Multi-objective optimization is not supported.
1717
18+
> Note: PSO Sampler cannot process dynamic changes to the search space.
19+
1820
For details on the algorithm, see Kennedy and Eberhart (1995): [Particle Swarm Optimization](https://doi.org/10.1109/ICNN.1995.488968).
1921

2022
## APIs

package/samplers/pso/example.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
def objective(trial: optuna.Trial) -> float:
88
x = trial.suggest_float("x", -10, 10)
99
y = trial.suggest_float("y", -10, 10)
10+
_ = trial.suggest_categorical("z", ["x", "y"]) # Sampled by RandomSampler
1011
return x**2 + y**2
1112

1213

@@ -22,10 +23,6 @@ def objective(trial: optuna.Trial) -> float:
2223
package=package_name,
2324
registry_root="./package", # Path to the root of the optunahub-registry.
2425
).PSOSampler(
25-
{
26-
"x": optuna.distributions.FloatDistribution(-10, 10),
27-
"y": optuna.distributions.FloatDistribution(-10, 10),
28-
},
2926
n_particles=int(n_trials / n_generations),
3027
inertia=0.5,
3128
cognitive=1.5,
@@ -34,10 +31,6 @@ def objective(trial: optuna.Trial) -> float:
3431
else:
3532
# This is an example of how to load a sampler from your fork of the optunahub-registry.
3633
sampler = optunahub.load_module(package=package_name).PSOSampler(
37-
{
38-
"x": optuna.distributions.FloatDistribution(-10, 10),
39-
"y": optuna.distributions.FloatDistribution(-10, 10),
40-
},
4134
n_particles=int(n_trials / n_generations),
4235
inertia=0.5,
4336
cognitive=1.5,

package/samplers/pso/sampler.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class PSOSampler(optunahub.samplers.SimpleBaseSampler):
101101

102102
def __init__(
103103
self,
104+
*,
104105
search_space: Optional[Dict[str, BaseDistribution]] = None,
105106
n_particles: int = 10,
106107
inertia: float = 0.5,
@@ -116,6 +117,7 @@ def __init__(
116117
if cognitive < 0.0 or social < 0.0:
117118
raise ValueError("cognitive and social must be >= 0.0.")
118119

120+
self.search_space = search_space
119121
self.n_particles: int = n_particles
120122
self.inertia: float = inertia
121123
self.cognitive: float = cognitive
@@ -128,7 +130,6 @@ def __init__(
128130
self.dim: int = 0
129131
# Numeric-only names used for PSO vectorization.
130132
self.param_names: List[str] = [] # numeric param names
131-
self.categorical_param_names: List[str] = []
132133
self._numeric_dists: Dict[str, BaseDistribution] = {}
133134
self.lower_bound: np.ndarray = np.array([], dtype=float)
134135
self.upper_bound: np.ndarray = np.array([], dtype=float)
@@ -151,22 +152,18 @@ def _lazy_init(self, search_space: Dict[str, BaseDistribution]) -> None:
151152
"""Initialize internal state based on the current search space (numeric-only for PSO)."""
152153
# Split numeric vs. categorical distributions.
153154
self.param_names = []
154-
self.categorical_param_names = []
155-
self._numeric_dists = {}
156155

157-
for name, dist in search_space.items():
156+
self._numeric_dists = {
157+
name: dist
158+
for name, dist in search_space.items()
158159
if isinstance(
159160
dist,
160161
(optuna.distributions.FloatDistribution, optuna.distributions.IntDistribution),
161-
):
162-
self.param_names.append(name) # numeric params used by PSO
163-
self._numeric_dists[name] = dist
164-
elif isinstance(dist, optuna.distributions.CategoricalDistribution):
165-
self.categorical_param_names.append(name)
166-
else:
167-
# Unknown distribution types are ignored by PSO and will be sampled independently.
168-
self.categorical_param_names.append(name)
162+
)
163+
and not dist.single()
164+
}
169165

166+
self.param_names = sorted(self._numeric_dists.keys())
170167
self.dim = len(self.param_names)
171168

172169
if self.dim > 0:
@@ -194,6 +191,28 @@ def _lazy_init(self, search_space: Dict[str, BaseDistribution]) -> None:
194191

195192
self._initialized = True
196193

194+
def infer_relative_search_space(
195+
self, study: Study, _: FrozenTrial
196+
) -> Dict[str, BaseDistribution]:
197+
if self.search_space is not None:
198+
return self.search_space
199+
200+
inferred = self._intersection_search_space.calculate(study)
201+
202+
numeric = {
203+
n: d
204+
for n, d in inferred.items()
205+
if not d.single()
206+
and isinstance(
207+
d, (optuna.distributions.FloatDistribution, optuna.distributions.IntDistribution)
208+
)
209+
}
210+
211+
if numeric:
212+
self.search_space = numeric
213+
214+
return numeric
215+
197216
def sample_relative(
198217
self,
199218
study: Study,
@@ -210,15 +229,7 @@ def sample_relative(
210229
if len(search_space) == 0:
211230
return {}
212231

213-
# Re-init if the count of numeric params changed.
214-
numeric_count = sum(
215-
isinstance(
216-
dist,
217-
(optuna.distributions.FloatDistribution, optuna.distributions.IntDistribution),
218-
)
219-
for dist in search_space.values()
220-
)
221-
if not self._initialized or self.dim != numeric_count:
232+
if not self._initialized:
222233
self._lazy_init(search_space)
223234

224235
# Serve next precomputed numeric candidate if available.

0 commit comments

Comments
 (0)