Skip to content

Commit d8e1908

Browse files
committed
refactor
1 parent 7538212 commit d8e1908

File tree

13 files changed

+619
-187
lines changed

13 files changed

+619
-187
lines changed

bore/decorators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def value_and_gradient_fn(x):
4141

4242
# Equivalent to `tfp.math.value_and_gradient(value_fn, x)`, with the
4343
# only difference that the gradients preserve their `dtype` rather than
44-
# casting to tf.float32, which is problematic for scipy optimize
44+
# casting to `tf.float32`, which is problematic for scipy.optimize
4545
with tf.GradientTape(watch_accessed_variables=False) as tape:
4646
tape.watch(x)
4747
val = value_fn(x)

bore/engine.py

Lines changed: 85 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@
44
from scipy.optimize import minimize
55

66
from tensorflow.keras.losses import BinaryCrossentropy
7-
# from tensorflow.keras.initializers import GlorotUniform
87

98
from .types import DenseConfigurationSpace, DenseConfiguration
109
from .models import DenseSequential
1110
from .decorators import unbatch, value_and_gradient, numpy_io
1211
from .optimizers import multi_start
1312

14-
# from hpbandster.core.master import Master
1513
from hpbandster.optimizers.hyperband import HyperBand
1614
from hpbandster.core.base_config_generator import base_config_generator
1715

1816

17+
minimize_multi_start = multi_start(minimizer_fn=minimize)
18+
19+
1920
def is_duplicate(x, xs, rtol=1e-5, atol=1e-8):
2021
# Clever ways of doing this would involve data structs. like KD-trees
2122
# or locality sensitive hashing (LSH), but these are premature
@@ -97,14 +98,15 @@ def __init__(self, config_space, gamma=1/3, num_random_init=10,
9798
self.logit = self._build_compile_network(num_layers, num_units,
9899
activation, optimizer)
99100
self.loss = self._build_loss(self.logit, normalize=normalize)
100-
self.minimizer = self._build_minimizer(num_restarts=num_restarts,
101-
method=method, ftol=ftol,
102-
max_iter=max_iter)
103101

104102
self.gamma = gamma
105103
self.num_random_init = num_random_init
106104
self.random_rate = random_rate
105+
107106
self.num_restarts = num_restarts
107+
self.method = method
108+
self.ftol = ftol
109+
self.max_iter = max_iter
108110

109111
self.batch_size = batch_size
110112
self.num_steps_per_iter = num_steps_per_iter
@@ -115,6 +117,35 @@ def __init__(self, config_space, gamma=1/3, num_random_init=10,
115117
self.seed = seed
116118
self.random_state = np.random.RandomState(seed)
117119

120+
def _array_from_dict(self, dct):
121+
config = DenseConfiguration(self.config_space, values=dct)
122+
return config.to_array()
123+
124+
def _dict_from_array(self, array):
125+
config = DenseConfiguration.from_array(self.config_space,
126+
array_dense=array)
127+
return config.get_dictionary()
128+
129+
def _get_dataset_size(self):
130+
return len(self.config_arrs)
131+
132+
def _load_data(self):
133+
X = np.vstack(self.config_arrs)
134+
y = np.hstack(self.losses)
135+
return X, y
136+
137+
def _load_labels(self, y):
138+
# TODO(LT): we can use clever data structures like heaps to make this
139+
# labelling constant-time, but this is probably a premature
140+
# optimization at this time...
141+
tau = np.quantile(y, q=self.gamma)
142+
return np.less(y, tau)
143+
144+
def _get_steps_per_epoch(self, dataset_size):
145+
steps_per_epoch = int(np.ceil(np.true_divide(dataset_size,
146+
self.batch_size)))
147+
return steps_per_epoch
148+
118149
@staticmethod
119150
def _build_compile_network(num_layers, num_units, activation, optimizer):
120151

@@ -142,52 +173,12 @@ def loss(x):
142173

143174
return loss
144175

145-
@staticmethod
146-
def _build_minimizer(num_restarts, method="L-BFGS-B", max_iter=100,
147-
ftol=1e-2):
148-
149-
@multi_start(num_restarts=num_restarts)
150-
def multi_start_minimizer(fn, x0, bounds):
151-
return minimize(fn, x0=x0, method=method, jac=True, bounds=bounds,
152-
options=dict(maxiter=max_iter, ftol=ftol))
153-
154-
return multi_start_minimizer
155-
156-
def _load_data(self):
157-
X = np.vstack(self.config_arrs)
158-
y = np.hstack(self.losses)
159-
return X, y
160-
161-
def _load_labels(self, y):
162-
tau = np.quantile(y, q=self.gamma)
163-
return np.less(y, tau)
164-
165-
def _get_steps_per_epoch(self, dataset_size):
166-
steps_per_epoch = int(np.ceil(np.true_divide(dataset_size,
167-
self.batch_size)))
168-
return steps_per_epoch
169-
170-
def get_config(self, budget):
171-
172-
dataset_size = len(self.config_arrs)
173-
174-
config_random = self.config_space.sample_configuration()
175-
config_random_dict = config_random.get_dictionary()
176-
177-
if dataset_size < self.num_random_init:
178-
self.logger.debug(f"Completed {dataset_size}/{self.num_random_init}"
179-
" initial runs. Returning random candidate...")
180-
return (config_random_dict, {})
181-
182-
if self.random_state.binomial(p=self.random_rate, n=1):
183-
self.logger.info("[Glob. maximum: skipped "
184-
f"(prob={self.random_rate:.2f})] "
185-
"Returning random candidate ...")
186-
return (config_random_dict, {})
176+
def _update_model(self):
187177

188178
X, y = self._load_data()
189179
z = self._load_labels(y)
190180

181+
dataset_size = self._get_dataset_size()
191182
steps_per_epoch = self._get_steps_per_epoch(dataset_size)
192183
num_epochs = self.num_steps_per_iter // steps_per_epoch
193184

@@ -203,15 +194,21 @@ def get_config(self, budget):
203194
f"num steps per iter: {self.num_steps_per_iter}, "
204195
f"num epochs: {num_epochs}")
205196

206-
# Maximize acquisition function
197+
def _get_maximum(self):
198+
207199
self.logger.debug("Beginning multi-start maximization with "
208200
f"{self.num_restarts} starts...")
209201

210-
results = self.minimizer(self.loss, self.bounds,
211-
random_state=self.random_state)
202+
results = minimize_multi_start(self.loss, self.bounds,
203+
num_restarts=self.num_restarts,
204+
method=self.method, jac=True,
205+
options=dict(maxiter=self.max_iter,
206+
ftol=self.ftol),
207+
random_state=self.random_state)
212208

213209
res_best = None
214210
for i, res in enumerate(results):
211+
# TODO(LT): This currently assumes `normalize=False`
215212
self.logger.debug(f"[Maximum {i+1:02d}/{self.num_restarts:02d}: "
216213
f"logit={-res.fun:.3f}] success: {res.success}, "
217214
f"iterations: {res.nit:02d}, status: {res.status}"
@@ -225,7 +222,32 @@ def get_config(self, budget):
225222
if res_best is None or res.fun < res_best.fun:
226223
res_best = res
227224

228-
if res_best is None:
225+
return res_best
226+
227+
def get_config(self, budget):
228+
229+
dataset_size = self._get_dataset_size()
230+
231+
config_random = self.config_space.sample_configuration()
232+
config_random_dict = config_random.get_dictionary()
233+
234+
if dataset_size < self.num_random_init:
235+
self.logger.debug(f"Completed {dataset_size}/{self.num_random_init}"
236+
" initial runs. Returning random candidate...")
237+
return (config_random_dict, {})
238+
239+
if self.random_state.binomial(p=self.random_rate, n=1):
240+
self.logger.info("[Glob. maximum: skipped "
241+
f"(prob={self.random_rate:.2f})] "
242+
"Returning random candidate ...")
243+
return (config_random_dict, {})
244+
245+
# Update model
246+
self._update_model()
247+
248+
# Maximize acquisition function
249+
opt = self._get_maximum()
250+
if opt is None:
229251
# TODO(LT): It's actually important to report what one of these
230252
# occurred...
231253
self.logger.warn("[Glob. maximum: not found!] Either optimization "
@@ -234,29 +256,28 @@ def get_config(self, budget):
234256
" Returning random candidate...")
235257
return (config_random_dict, {})
236258

237-
self.logger.info(f"[Glob. maximum: logit={-res_best.fun:.3f}, "
238-
f"prob={tf.sigmoid(-res_best.fun):.3f}, "
239-
f"rel. ratio={tf.sigmoid(-res_best.fun)/self.gamma:.3f}] "
240-
f"x={res_best.x}")
259+
# TODO(LT): This currently assumes `normalize=False`
260+
self.logger.info(f"[Glob. maximum: logit={-opt.fun:.3f}, "
261+
f"prob={tf.sigmoid(-opt.fun):.3f}, "
262+
f"rel. ratio={tf.sigmoid(-opt.fun)/self.gamma:.3f}] "
263+
f"x={opt.x}")
241264

242-
config_opt_arr = res_best.x
243-
config_opt = DenseConfiguration.from_array(self.config_space,
244-
array_dense=config_opt_arr)
245-
config_opt_dict = config_opt.get_dictionary()
265+
config_opt_arr = opt.x
266+
config_opt_dict = self._dict_from_array(config_opt_arr)
246267

247268
return (config_opt_dict, {})
248269

249270
def new_result(self, job, update_model=True):
250271

251272
super(DRE, self).new_result(job)
252273

253-
# TODO: ignoring this right now
274+
# TODO(LT): support multi-fidelity
254275
budget = job.kwargs["budget"]
255276

256-
loss = job.result["loss"]
257277
config_dict = job.kwargs["config"]
258-
config = DenseConfiguration(self.config_space, values=config_dict)
259-
config_arr = config.to_array()
278+
config_arr = self._array_from_dict(config_dict)
279+
280+
loss = job.result["loss"]
260281

261-
self.losses.append(loss)
262282
self.config_arrs.append(config_arr)
283+
self.losses.append(loss)

bore/optimizers.py

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import numpy as np
22

3-
from scipy.optimize import minimize
4-
from scipy.optimize import Bounds
3+
from scipy.optimize import minimize, Bounds
54
from sklearn.utils import check_random_state
65

76

@@ -19,56 +18,35 @@ def deduplicate(results, atol=1e-6):
1918
return results_unique
2019

2120

22-
def multi_start(num_restarts):
21+
def multi_start(minimizer_fn=minimize):
2322

24-
def decorator(minimizer_fn):
23+
def new_minimizer(fn, bounds, num_restarts, random_state=None, *args, **kwargs):
2524

26-
def new_minimizer(fn, bounds, random_state=None):
27-
# We deliberately don't use args/kwargs here which would increase
28-
# flexibility but also complexity. The aim here to to expose a
29-
# simplied interface so users can't accidently pass conflicting
30-
# arguments, e.g. `x0` which is the whole point of this decorator.
25+
assert "x0" not in kwargs, "`x0` should not be specified"
3126

32-
# TODO(LT): Allow alternative arbitary generator function callbacks
33-
# to support e.g. Gaussian sampling, low-discrepancy sequences, etc
34-
random_state = check_random_state(random_state)
27+
if not (num_restarts > 0):
28+
return []
3529

36-
if isinstance(bounds, Bounds):
37-
low = bounds.lb
38-
high = bounds.ub
39-
dims = len(low)
40-
assert dims == len(high), "lower and upper bounds sizes do not match"
41-
else:
42-
# assumes `bounds` is a list of tuples
43-
low, high = zip(*bounds)
44-
dims = len(bounds)
30+
# TODO(LT): Allow alternative arbitary generator function callbacks
31+
# to support e.g. Gaussian sampling, low-discrepancy sequences, etc
32+
random_state = check_random_state(random_state)
4533

46-
x_inits = random_state.uniform(low=low, high=high,
47-
size=(num_restarts, dims))
34+
if isinstance(bounds, Bounds):
35+
low = bounds.lb
36+
high = bounds.ub
37+
dims = len(low)
38+
assert dims == len(high), "lower and upper bounds sizes do not match"
39+
else:
40+
# assumes `bounds` is a list of tuples
41+
low, high = zip(*bounds)
42+
dims = len(bounds)
4843

49-
results = []
50-
for x_init in x_inits:
51-
res = minimizer_fn(fn, x0=x_init, bounds=bounds)
52-
results.append(res)
44+
results = new_minimizer(fn, bounds, num_restarts-1, random_state,
45+
*args, **kwargs)
46+
x0 = random_state.uniform(low=low, high=high, size=(dims,))
47+
result = minimizer_fn(fn, x0=x0, bounds=bounds, *args, **kwargs)
48+
results.append(result)
5349

54-
# TODO(LT): support reduction function callback? e.g. argmin which
55-
# is what one ultimately cares about. But perhaps suboptimal
56-
# points can be useful as well, e.g. to be queued up for
57-
# evaluation by idle workers.
58-
return results
50+
return results
5951

60-
return new_minimizer
61-
62-
return decorator
63-
64-
65-
@multi_start(num_restarts=10)
66-
def multi_start_lbfgs_minimizer(fn, x0, bounds):
67-
"""
68-
Wrapper around SciPy L-BFGS-B minimizer with a simplified interface and
69-
sensible defaults specified.
70-
"""
71-
# TODO(LT): L-BFGS-B has its own set of `tol` options so I suspect the
72-
# following `tol=1e-8` is completely ignored.
73-
return minimize(fn, x0=x0, method="L-BFGS-B", jac=True, bounds=bounds,
74-
tol=1e-8, options=dict(maxiter=10000))
52+
return new_minimizer

bore/types.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,9 @@ def get_bounds(self):
4242
# lowers.append(0.)
4343
# uppers.append(1.)
4444
# elif isinstance(hp, CS.UniformFloatHyperparameter):
45-
# # TODO(LT): These should never not be 0. and 1. respectively,
46-
# # so I am really overcomplicating things here...
4745
# lowers.append(hp._inverse_transform(hp.lower))
4846
# uppers.append(hp._inverse_transform(hp.upper))
4947
# elif isinstance(hp, CS.UniformIntegerHyperparameter):
50-
# # TODO(LT): These should never not be 0. and 1. respectively,
51-
# # so I am really overcomplicating things here...
5248
# lowers.append(hp._inverse_transform(hp.lower - 1))
5349
# uppers.append(hp._inverse_transform(hp.upper + 1))
5450
# else:
@@ -60,6 +56,8 @@ def get_bounds(self):
6056
# assert len(lowers) == self.size_dense
6157
# assert len(uppers) == self.size_dense
6258

59+
# All of the above commented code is equivalent to the following two
60+
# lines...
6361
lowers = np.zeros(self.size_dense)
6462
uppers = np.ones(self.size_dense)
6563

0 commit comments

Comments
 (0)