-
Notifications
You must be signed in to change notification settings - Fork 100
/
Copy pathoptimisationsCMA.py
393 lines (321 loc) · 13.3 KB
/
optimisationsCMA.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
"""CMA Optimisation class"""
"""
Copyright (c) 2016-2022, EPFL/Blue Brain Project
This file is part of BluePyOpt <https://github.com/BlueBrain/BluePyOpt>
This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License version 3.0 as published
by the Free Software Foundation.
This library is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
details.
You should have received a copy of the GNU Lesser General Public License
along with this library; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
import logging
import numpy
import pickle
import random
import functools
import shutil
import os
import time
import deap.tools
from .CMA_SO import CMA_SO
from .CMA_MO import CMA_MO
from . import utils
import bluepyopt.optimisations
logger = logging.getLogger("__main__")
def _ind_convert_space(ind, convert_fcn):
"""util function to pass the individual from normalized to real space and
inversely"""
return [f(x) for f, x in zip(convert_fcn, ind)]
class DEAPOptimisationCMA(bluepyopt.optimisations.Optimisation):
"""Optimisation class for CMA-based evolution strategies"""
def __init__(
self,
evaluator=None,
use_scoop=False,
seed=1,
offspring_size=None,
centroids=None,
sigma=0.4,
map_function=None,
hof=None,
selector_name="single_objective",
weight_hv=0.5,
fitness_reduce=numpy.sum,
use_stagnation_criterion=True,
):
"""Constructor
Args:
evaluator (Evaluator): Evaluator object
use_scoop (bool): use scoop map for parallel computation
seed (float): Random number generator seed
offspring_size (int): Number of offspring individuals in each
generation
centroids (list): list of initial guesses used as the starting
points of the CMA-ES
sigma (float): initial standard deviation of the distribution
map_function (function): Function used to map (parallelize) the
evaluation function calls
hof (hof): Hall of Fame object
selector_name (str): The selector used in the evolutionary
algorithm, possible values are 'single_objective' or
'multi_objective'
weight_hv (float): between 0 and 1. Weight given to the
hyper-volume contribution when computing the score of an
individual in MO-CMA. The weight of the fitness contribution
is computed as 1 - weight_hv.
fitness_reduce (fcn): function used to reduce the objective values
to a single fitness score
use_stagnation_criterion (bool): whether to use the stagnation
stopping criterion on top of the maximum generation criterion
"""
super(DEAPOptimisationCMA, self).__init__(evaluator=evaluator)
self.use_scoop = use_scoop
self.seed = seed
self.map_function = map_function
self.hof = hof
if self.hof is None:
self.hof = deap.tools.HallOfFame(10)
self.offspring_size = offspring_size
self.fitness_reduce = fitness_reduce
self.centroids = centroids
self.sigma = sigma
if weight_hv > 1.0 or weight_hv < 0.0:
raise Exception("weight_hv has to be between 0 and 1.")
self.weight_hv = weight_hv
self.selector_name = selector_name
if self.selector_name == "single_objective":
self.cma_creator = CMA_SO
elif self.selector_name == "multi_objective":
self.cma_creator = CMA_MO
else:
raise Exception(
"The selector_name has to be 'single_objective' "
"or 'multi_objective'. Not "
"{}".format(self.selector_name)
)
self.use_stagnation_criterion = use_stagnation_criterion
# Number of objective values
self.problem_size = len(self.evaluator.params)
# Number of parameters
self.ind_size = len(self.evaluator.objectives)
# Create a DEAP toolbox
self.toolbox = deap.base.Toolbox()
# Bounds for the parameters
self.lbounds = [p.lower_bound for p in self.evaluator.params]
self.ubounds = [p.upper_bound for p in self.evaluator.params]
# Instantiate functions converting individuals from the original
# parameter space to (and from) a normalized space bounded to [-1.;1]
self.ubounds = numpy.asarray(self.ubounds)
self.lbounds = numpy.asarray(self.lbounds)
bounds_radius = (self.ubounds - self.lbounds) / 2.0
bounds_mean = (self.ubounds + self.lbounds) / 2.0
self.to_norm = []
self.to_space = []
for r, m in zip(bounds_radius, bounds_mean):
self.to_norm.append(
functools.partial(
lambda param, bm, br: (param - bm) / br,
bm=m,
br=r)
)
self.to_space.append(
functools.partial(
lambda param, bm, br: (param * br) + bm,
bm=m,
br=r
)
)
# Overwrite the bounds with -1. and 1.
self.lbounds = numpy.full(self.problem_size, -1.0)
self.ubounds = numpy.full(self.problem_size, 1.0)
self.setup_deap()
# In case initial guesses were provided, rescale them to the norm space
if self.centroids is not None:
self.centroids = [
self.toolbox.Individual(_ind_convert_space(ind, self.to_norm))
for ind in centroids
]
def setup_deap(self):
"""Set up optimisation"""
# Set random seed
random.seed(self.seed)
numpy.random.seed(self.seed)
# Register the 'uniform' function
self.toolbox.register(
"uniformparams",
utils.uniform,
self.lbounds,
self.ubounds,
self.ind_size
)
# Register the individual format
self.toolbox.register(
"Individual",
functools.partial(
utils.WSListIndividual,
obj_size=self.ind_size,
reduce_fcn=self.fitness_reduce,
),
)
# A Random Individual is created by ListIndividual and parameters are
# initially picked by 'uniform'
self.toolbox.register(
"RandomInd",
deap.tools.initIterate,
self.toolbox.Individual,
self.toolbox.uniformparams,
)
# Register the population format. It is a list of individuals
self.toolbox.register(
"population", deap.tools.initRepeat, list, self.toolbox.RandomInd
)
# Register the evaluation function for the individuals
self.toolbox.register(
"evaluate",
self.evaluator.init_simulator_and_evaluate_with_lists
)
import copyreg
import types
copyreg.pickle(types.MethodType, utils.reduce_method)
if self.use_scoop:
if self.map_function:
raise Exception(
"Impossible to use scoop is providing self defined map "
"function: %s" % self.map_function
)
from scoop import futures
self.toolbox.register("map", futures.map)
elif self.map_function:
self.toolbox.register("map", self.map_function)
def run(
self,
max_ngen=0,
cp_frequency=1,
cp_period=None,
continue_cp=False,
cp_filename=None,
terminator=None,
):
""" Run the optimizer until a stopping criteria is met.
Args:
max_ngen(int): Total number of generation to run
cp_frequency(int): generations between checkpoints
cp_period(float): minimum time (in s) between checkpoint.
None to save checkpoint independently of the time between them
continue_cp(bool): whether to continue
cp_filename(string): path to checkpoint filename
terminator (multiprocessing.Event): exit loop when is set.
Not taken into account if None.
"""
if cp_filename:
cp_filename_tmp = cp_filename + '.tmp'
stats = self.get_stats()
if continue_cp:
# A file name has been given, then load the data from the file
cp = pickle.load(open(cp_filename, "rb"))
gen = cp["generation"]
self.hof = cp["halloffame"]
logbook = cp["logbook"]
history = cp["history"]
random.setstate(cp["rndstate"])
numpy.random.set_state(cp["np_rndstate"])
CMA_es = cp["CMA_es"]
CMA_es.map_function = self.map_function
else:
history = deap.tools.History()
logbook = deap.tools.Logbook()
logbook.header = ["gen", "nevals"] + stats.fields
# Instantiate the CMA strategy centered on the centroids
CMA_es = self.cma_creator(
centroids=self.centroids,
offspring_size=self.offspring_size,
sigma=self.sigma,
max_ngen=max_ngen,
IndCreator=self.toolbox.Individual,
RandIndCreator=self.toolbox.RandomInd,
map_function=self.map_function,
use_scoop=self.use_scoop,
use_stagnation_criterion=self.use_stagnation_criterion,
)
if self.selector_name == "multi_objective":
CMA_es.weight_hv = self.weight_hv
to_evaluate = CMA_es.get_parents(self.to_space)
fitness = self.toolbox.map(self.toolbox.evaluate, to_evaluate)
fitness = list(map(list, fitness))
CMA_es.set_fitness_parents(fitness)
gen = 1
pop = CMA_es.get_population(self.to_space)
param_names = []
if hasattr(self.evaluator, "param_names"):
param_names = self.evaluator.param_names
time_last_save = time.time()
# Run until a termination criteria is met
while utils.run_next_gen(CMA_es.active, terminator):
logger.info("Generation {}".format(gen))
# Generate the new populations
n_out = CMA_es.generate_new_pop(
lbounds=self.lbounds, ubounds=self.ubounds
)
logger.debug(
"Number of individuals outside of bounds: {} ({:.2f}%)".format(
n_out,
100.0 * n_out / len(CMA_es.population)
)
)
# Get all the individuals in the original space for evaluation
to_evaluate = CMA_es.get_population(self.to_space)
# Compute the fitness
fitness = self.toolbox.map(self.toolbox.evaluate, to_evaluate)
fitness = list(map(list, fitness))
nevals = len(to_evaluate)
CMA_es.set_fitness(fitness)
# Update the hall of fame, history and logbook
pop = CMA_es.get_population(self.to_space)
utils.update_history_and_hof(self.hof, history, pop)
record = utils.record_stats(stats, logbook, gen, pop, nevals)
logger.info(logbook.stream)
# Update the CMA strategy using the new fitness and check if
# termination conditions were reached
CMA_es.update_strategy()
CMA_es.check_termination(gen)
if (
cp_filename and
cp_frequency and
gen % cp_frequency == 0 and
(cp_period is None or time.time() - time_last_save > cp_period)
):
# Map function shouldn't be pickled
temp_mf = CMA_es.map_function
CMA_es.map_function = None
cp = dict(
population=pop,
generation=gen,
halloffame=self.hof,
history=history,
logbook=logbook,
rndstate=random.getstate(),
np_rndstate=numpy.random.get_state(),
CMA_es=CMA_es,
param_names=param_names,
)
pickle.dump(cp, open(cp_filename_tmp, "wb"))
if os.path.isfile(cp_filename_tmp):
shutil.copy(cp_filename_tmp, cp_filename)
logger.debug("Wrote checkpoint to %s", cp_filename)
CMA_es.map_function = temp_mf
time_last_save = time.time()
gen += 1
return pop, self.hof, logbook, history
def get_stats(self):
"""Get the stats that will be saved during optimisation"""
stats = deap.tools.Statistics(key=lambda ind: ind.fitness.reduce)
stats.register("avg", numpy.mean)
stats.register("std", numpy.std)
stats.register("min", numpy.min)
stats.register("max", numpy.max)
return stats