Package comocma :: Module como
[hide private]
[frames] | no frames]

Source Code for Module comocma.como

   1  #!/usr/bin/env python3 
   2  # -*- coding: utf-8 -*- 
   3  """ 
   4  This module contains the implementation of the Multiobjective framework called 
   5  Sofomore, and its instantiation with cma-es to obtain COMO-CMA-ES, defined in  
   6  the paper [Toure, Cheikh, et al. "Uncrowded Hypervolume Improvement:  
   7          COMO-CMA-ES and the Sofomore framework."  
   8          GECCO'19-Genetic and Evolutionary Computation Conference. 2019.]. 
   9   
  10  Only the bi-objective framework is functional and has been thoroughly tested. 
  11  """ 
  12   
  13  from __future__ import division, print_function, unicode_literals 
  14  __author__ = "Cheikh Toure and Nikolaus Hansen" 
  15  __license__ = "BSD 3-clause" 
  16  __version__ = "0.5.1" 
  17  del division, print_function, unicode_literals 
  18   
  19  import ast 
  20  import numpy as np 
  21  import cma 
  22  from cma import interfaces 
  23  from .nondominatedarchive import NonDominatedList 
  24  from moarchiving import BiobjectiveNondominatedSortedList 
  25  import warnings 
  26  import cma.utilities.utils 
  27  import os 
  28  from .sofomore_logger import SofomoreDataLogger 
29 30 31 -class Sofomore(interfaces.OOOptimizer):
32 """ 33 Sofomore framework for multiobjective optimization, with the 34 ask-and-tell interface. 35 See: [Toure, Cheikh, et al. "Uncrowded Hypervolume Improvement: 36 COMO-CMA-ES and the Sofomore framework." 37 GECCO'19-Genetic and Evolutionary Computation Conference. 2019.]. 38 39 Calling Sequences 40 ================= 41 42 - ``moes = Sofomore(list_of_solvers_instances, reference_point, opts)`` 43 44 - ``moes = Sofomore(list_of_solvers_instances, reference_point)`` 45 46 - ``moes = Sofomore(list_of_solvers_instances, 47 reference_point).optimize(objective_fcts)`` 48 49 Arguments 50 ========= 51 `list_of_solvers_instances` 52 list of instances of single-objective solvers. 53 It's generally created via a factory function. 54 Let's take the example of a factory function that returns cma-es 55 instances, called `get_cmas`. Then: 56 ``list_of_solvers_instances = get_cmas(11 * [x0], sigma0)`` 57 creates a list of 11 cma-es instances of initial mean `x0` and initial 58 step-size `sigma0`. 59 A single-objective solver instance must have the following 60 attributes and methods: 61 - `incumbent`: an attribute that gives an estimate of 62 the single-objective solver. 63 - `objective_values`: an attribute that stores the objective 64 values of the incumbent. 65 - `stop`: a method returning a dictionary representing the 66 termination status. 67 - `ask`: generates new candidate solutions. 68 - `tell`: passes the objective values and updates the states of the 69 single-objective solvers. 70 71 72 `opts` 73 opts, a dictionary with optional settings related to the 74 Sofomore framework. It mainly contains the following keys: 75 - 'archive': default value is `True`. 76 If its value is `True`, tracks the non-dominated 77 points that dominate the reference point, among the points evaluated 78 so far during the optimization. 79 The archive will not interfere with the optimization 80 process. 81 - 'indicator_front': default value is `None`. Used via 82 `self.indicator_front = IndicatorFront(self.opts['indicator_front'])` 83 within the __init__ method of Sofomore. See the class IndicatorFront 84 for more details. 85 - 'restart': used to define in __init__ the attribute `restart` via 86 `self.restart = self.opts['restart']`. 87 - 'update_order': default value is a function that takes a natural 88 integer as input and returns a random number between 0 and 1. 89 It is used as a `key value` in: `sorted(..., key = ...)`, and guides the 90 order in which the kernels will be updated during the optimization. 91 92 93 `reference_point` 94 reference point of the multiobjective optimization. 95 Its default value is `None` but should be set by the user 96 beforehand to guarantee an optimal p-distribution convergence of 97 the Hypervolume indicator of `p` points towards the Pareto set/front. 98 It can be changed dynamically by the user if needed. 99 100 101 Main interface / usage 102 ====================== 103 The interface is inherited from the generic `OOOptimizer` 104 class, which is the same interface used by the `pycma` module. An object 105 instance is generated as following:: 106 107 >>> import cma, comocma 108 >>> import numpy as np 109 >>> reference_point = [11, 11] 110 >>> num_kernels = 11 # the number of points we seek to have on the Pareto front 111 >>> dimension = 10 # the dimension of the search space 112 >>> x0 = dimension * [0] # an initial mean for cma 113 >>> sigma0 = 0.2 # initial step-size for cma 114 >>> list_of_solvers_instances = comocma.get_cmas(num_kernels * [x0], sigma0, {'verbose':-9}) 115 >>> # `comocma.get_cmas` is a factory function that returns `num_kernels` cma-es instances 116 >>> moes = comocma.Sofomore(list_of_solvers_instances, 117 ... reference_point) #instantiation of our MO optimizer 118 119 The least verbose interface is via the optimize method:: 120 >>> fitness = comocma.FitFun(cma.ff.sphere, lambda x: cma.ff.sphere(x-1)) # a callable bi-objective function 121 >>> moes.optimize(fitness) # doctest:+ELLIPSIS 122 Iterat #Fevals Hypervolume axis ratios sigmas min&max stds*** 123 124 More verbosely, the optimization of the callable multiobjective function 125 `fitness` is done via the `ask-and-tell` interface:: 126 127 >>> moes = comocma.Sofomore(list_of_solvers_instances, reference_point) 128 >>> while not moes.stop() and moes.countiter < 30: 129 ... solutions = moes.ask() # `ask` delivers new candidate solutions 130 ... objective_values = [fitness(x) for x in solutions] 131 ... moes.tell(solutions, objective_values) 132 ... # `tell` updates the MO instance by passing the respective function values. 133 ... moes.disp() # display data on the evolution of the optimization 134 135 One iteration of the `optimize` interface is equivalent to one step in the 136 loop of the `ask-and-tell` interface. But for the latter, the prototyper has 137 more controls to analyse and guide the optimization, due to the access of 138 the instance between the `ask` and the `tell` calls. 139 140 Attributes and Properties 141 ========================= 142 143 - `archive`: list of non-dominated points among all points evaluated 144 so far, which dominate the reference point. 145 - `countevals`: the number of function evaluations. 146 - `countiter`: the number of iterations. 147 - `dimension`: is the dimension of the search space. 148 - `indicator_front`: the indicator used as a changing fitness inside an 149 iteration of Sofomore. By default it is the UHVI of all the objective values 150 of the kernels' incumbents, except the kernel being optimized. 151 See the class `IndicatorFront` for more details. 152 - `isarchive`: a boolean accessible via `self.opts['archive']`. If `True`, 153 we keep track of the archive, otherwise we don't. 154 - `kernels`: initialized with `list_of_solvers_instances`, 155 and is the list of single-objective solvers. 156 - `key_sort_indices`: default value is `self.opts['update_order']`. 157 It is a function used as a key to sort some kernels' indices, in order to 158 select the first indices during the call of the `ask` method. 159 - `logger`: an attribute that accounts for the way we log the Sofomore data 160 during an optimization. `self.logger` is an instance of the SofomoreDataLogger 161 class. 162 - `NDA`: it is the non-dominated archiving method used within Sofomore. 163 By default the class `BiobjectiveNondominatedSortedList` is used 164 in two objectives and the class (non tested yet) `NonDominatedList` is used 165 for three or more objectives. 166 - `opts`: passed options. 167 - `offspring`: list of tuples of the index of a kernel with its 168 corresponding candidate solutions, that we generally get with the cma's 169 `ask` method. 170 - `pareto_front_cut`: list of non-dominated points among the incumbents of 171 `self.kernels`, which dominate the reference point. 172 - `pareto_set_cut`: preimage of `pareto_front_cut`. 173 - `reference_point`: the current reference point. 174 - `restart`: accessible via `self.opts['restart']`. If not `None`, a callback 175 function that adds kernels after a kernel of the running Sofomore instance 176 stops. Its default value is `None`, meaning that we do not do any restart. 177 - `_active_indices`: the indices of the kernels that have not stopped yet. 178 - `_last_stopped_kernel_id`: the index of the last stopped kernel. 179 - `_told_indices`: the kernels' indices for which we will evaluate the 180 objective values of their incumbents, before the next call of the `tell` 181 method. And before the first call of `tell`, they are the indices of all the 182 initial kernels (i.e. `range(len(self))`). And before another call of 183 `tell`, they are the indices of the kernels from which we have sampled new 184 candidate solutions during the penultimate `ask` method. 185 Note that we should call the `ask` method before any call of the `tell` 186 method. 187 188 """
189 - def __init__(self, 190 list_of_solvers_instances, # usually come from a factory function 191 # creating single solvers' instances 192 reference_point=None, 193 opts=None, # keeping an archive, decide whether we use restart, etc. 194 ):
195 """ 196 Initialization: 197 - `list_of_solvers_instances` is a list of single-objective 198 solvers' instances 199 - The reference_point is set by the user during the 200 initialization. 201 - `opts` is a dictionary updating the values of 'indicator_front', 202 'archive', 'restart', 'update_order'; that respond respectfully to 203 the changing fitness we will choose within an iteration of Sofomore, 204 whether or not keeping an archive, how to do the restart in case of 205 any restart, and the order of update of the kernels. It also has 206 the keys 'verb_filename', 'verb_log' and 'verb_disp'; that 207 respectfully indicate the name of the filename containing the Sofomore 208 data, the logging of the Sofomore data every 'verb_log' iterations 209 and the display of the data via `self.disp()` every 'verb_disp' 210 iterations. 211 """ 212 assert len(list_of_solvers_instances) > 0 213 self.kernels = list_of_solvers_instances 214 self.dimension = self.kernels[0].N 215 self._active_indices = list(range(len(self))) 216 217 for kernel in self.kernels: 218 if not hasattr(kernel, 'objective_values'): 219 kernel.objective_values = None 220 self.reference_point = reference_point 221 defopts = {'archive': True, 'restart': None, 'verb_filename': 'outsofomore' + os.sep, 222 'verb_log': 1, 'verb_disp': 100, 'update_order': sort_random, 223 'indicator_front': None # 'archive' or any attribute containing a list of f-pairs 224 } 225 if opts is None: 226 opts = {} 227 if isinstance(opts, dict): 228 unvalid_keys = [k for k in opts.keys() if k not in defopts.keys()] 229 if len(unvalid_keys) > 0: 230 unvalid_key_not_str = [k for k in unvalid_keys if not isinstance(k, str)] 231 if len(unvalid_key_not_str) > 0: 232 raise ValueError("All the options's keys must be of TYPE str") 233 unvalid_keys_str = ', '.join(unvalid_keys) 234 raise ValueError('The following keys of opts are not valid: ' + unvalid_keys_str + '.') 235 236 defopts.update(opts) 237 else: 238 warnings.warn("options should be either a dictionary or None.") 239 self.opts = defopts 240 self.restart = self.opts['restart'] 241 self.isarchive = self.opts['archive'] 242 if self.isarchive: 243 self.archive = [] 244 self.NDA = None # the callable for nondominated archiving 245 self.indicator_front = IndicatorFront(self.opts['indicator_front']) 246 self.offspring = [] 247 self._told_indices = range(len(self)) 248 249 self.key_sort_indices = self.opts['update_order'] 250 self.countiter = 0 251 self.countevals = 0 252 self._remaining_indices_to_ask = range(len(self)) # where we look when calling `ask` 253 self.logger = SofomoreDataLogger(self.opts['verb_filename'], 254 modulo=self.opts['verb_log']).register(self) 255 self.best_hypervolume_pareto_front = 0.0 256 self.epsilon_hypervolume_pareto_front = 0.1 # the minimum positive convergence gap 257 self._ratio_nondom_offspring_incumbent = len(self) * [0] 258 259 self._last_stopped_kernel_id = None
260
261 - def __iter__(self):
262 """ 263 make `self` iterable. 264 """ 265 return iter(self.kernels)
266
267 - def __getitem__(self, i):
268 """ 269 make `self` subscriptable. 270 """ 271 return self.kernels[i]
272
273 - def __len__(self):
274 """return length of the `Sofomore` instance by calling ``len(.)``. 275 276 The length is the number of (active and inactive) kernels 277 and hence consistent with subscription like 278 ``[moes[i] for i in range(len(moes)) if i in moes._active_indices]``. 279 """ 280 return len(self.kernels)
281
282 - def _UHVI_indicator_archive(self, kernel):
283 """return archive for uncrowded hypervolume improvement indicator for `kernel`. 284 285 `kernel` can also be the respective index in `self`. 286 """ 287 # TODO: checking the list of f_pairs for copies and 288 # looking at contributing_hypervolume is cheaper? 289 try: # allow kernel index as shortcut for kernel 290 kernel = self[kernel] 291 except TypeError: 292 pass 293 return self.NDA([k.objective_values for k in self 294 if k != kernel and k.objective_values is not None], 295 self.reference_point)
296
297 - def _UHVI_indicator(self, kernel):
298 """return indicator function(!) for uncrowded hypervolume improvement for `kernel`. 299 300 >>> import comocma, cma 301 >>> list_of_solvers_instances = comocma.get_cmas(13 * [5 * [1]], 0.7, {'verbose':-9}) 302 >>> fitness = comocma.FitFun(cma.ff.sphere, lambda x: cma.ff.sphere(x-1)) 303 >>> moes = comocma.Sofomore(list_of_solvers_instances, [11, 11]) 304 >>> moes.optimize(fitness, iterations=37) # doctest:+ELLIPSIS 305 Iterat #Fevals Hypervolume axis ratios sigmas min&max stds*** 306 >>> moes._UHVI_indicator(moes[1])(moes[2].objective_values) # doctest:+ELLIPSIS 307 *** 308 >>> moes._UHVI_indicator(1)(moes[2].objective_values) # doctest:+ELLIPSIS 309 *** 310 311 both return the UHVI indicator function for kernel 1 and evaluate 312 kernel 2 on it:: 313 314 >>> [[moes._UHVI_indicator(k)(k.objective_values)] for k in moes] # doctest:+ELLIPSIS 315 *** 316 317 is the list of UHVI values for all kernels where kernels occupying the 318 very same objective value have indicator value zero. 319 """ 320 return self._UHVI_indicator_archive(kernel).hypervolume_improvement
321
322 - def sorted(self, key=None, reverse=True, **kwargs):
323 """return a reversed sorted list of kernels. 324 325 By default kernels are reversed sorted by HV contribution or UHVI 326 (which we aim to maximize) in the set of kernels. Exact copies have 327 zero or negative UHVI value. 328 329 >>> import comocma, cma 330 >>> list_of_solvers_instances = comocma.get_cmas(13 * [5 * [1]], 0.7, {'verbose':-9}) 331 >>> fitness = comocma.FitFun(cma.ff.sphere, lambda x: cma.ff.sphere(x-1)) 332 >>> moes = comocma.Sofomore(list_of_solvers_instances, [11, 11]) 333 >>> moes.optimize(fitness, iterations=31) # doctest:+ELLIPSIS 334 Iterat #Fevals Hypervolume axis ratios sigmas min&max stds*** 335 >>> moes.sorted(key = lambda k: moes.archive.contributing_hypervolume( 336 ... k.objective_values)) # doctest:+ELLIPSIS 337 [<comocma.como.CmaKernel object at*** 338 339 sorts w.r.t. archive contribution (clones may get positive contribution). 340 341 """ 342 def hv_improvement(kernel): 343 if kernel.objective_values is None: 344 return float('-inf') 345 return self._UHVI_indicator(kernel)(kernel.objective_values)
346 if key is None: 347 key = hv_improvement 348 return sorted(self, key=key, reverse=reverse, **kwargs)
349
350 - def ask(self, number_to_ask=1):
351 """ 352 get the kernels' incumbents to be evaluated and sample new candidate solutions from 353 `number_to_ask` kernels. 354 The sampling is done by calling the `ask` method of the 355 `cma.CMAEvolutionStrategy` class. 356 The indices of the considered kernels' incumbents are given by the 357 `_told_indices` attribute. 358 359 To get the `number_to_ask` kernels, we use the function `self.key_sort_indices` as 360 a key to sort `self._remaining_indices_to_ask` (which is the list of 361 kernels' indices wherein we choose the first `number_to_ask` elements. 362 And if `number_to_ask` is larger than `len(self._remaining_indices_to_ask)`, 363 we select the list `self._remaining_indices_to_ask` extended with the 364 first `number_to_ask - len(self._remaining_indices_to_ask)` elements 365 of `range(len(self))`, sorted with `self.key_sort_indices` as key. 366 367 Arguments 368 --------- 369 - `number_to_ask`: the number of kernels for which we sample solutions 370 from, it is of TYPE int or str and is smaller or equal to `len(self)`. 371 The unique case where it is a str instance is when it is equal to 372 "all", meaning that all the kernels are asked. That case is mainly used 373 on parallel mode by distributing the evaluations of the kernels at the 374 same time before calling the `tell` method. The opposite is when 375 `number_to_ask` is equal to 1, which is the exact COMO-CMA-ES algorithm, 376 where the evaluations are done sequentially by updating the kernels one 377 by one. 378 379 Return 380 ------ 381 The list of the kernels' incumbents to be evaluated, extended with a 382 list of N-dimensional (N is the dimension of the search space) 383 candidate solutions generated from `number_to_ask` kernels 384 to be evaluated. 385 386 :See: the `ask` method from the class `cma.CMAEvolutionStrategy`, 387 in `evolution_strategy.py` from the `cma` module. 388 389 """ 390 # TODO: be specific about the serial and the parallel case 391 if number_to_ask == "all": 392 number_to_ask = len(self._active_indices) 393 assert number_to_ask > 0 394 if number_to_ask > len(self._active_indices): 395 number_to_ask = len(self._active_indices) 396 warnings.warn("value larger than the number of active kernels {}. ".format( 397 len(self._active_indices)) + "Set to {}.".format(len(self._active_indices))) 398 self.offspring = [] 399 res = [self.kernels[i].incumbent for i in self._told_indices] 400 indices_to_ask = self._indices_to_ask(number_to_ask) 401 for ikernel in indices_to_ask: 402 kernel = self.kernels[ikernel] 403 offspring = kernel.ask() 404 res.extend(offspring) 405 self.offspring += [(ikernel, offspring)] 406 407 return res
408
409 - def tell(self, solutions, objective_values):
410 """ 411 pass objective function values to update the state variables of some 412 kernels, `self._told_indices` and eventually `self.archive`. 413 Arguments 414 --------- 415 `solutions` 416 list or array of points (of type `numpy.ndarray`), most presumably 417 before delivered by the `ask()` method. 418 `objective_values` 419 list of multiobjective function values (of type `list`) 420 corresponding to the respective points in `solutions`. 421 `constraints_values` 422 list of list of constraint values: each element is a list containing 423 the values of one constraint function, that are obtained by evaluation 424 on `solutions`. 425 426 427 Details 428 ------- 429 To update a kernel, `tell()` applies the kernel's `tell` method 430 to the kernel's corresponding candidate solutions (offspring) along 431 with the "changing" fitness `- self.indicator_front.hypervolume_improvement`. 432 433 :See: 434 - the `tell` method from the class `cma.CMAEvolutionStrategy`, 435 in `evolution_strategy.py` from the `cma` module. 436 - the `hypervolume_improvement` method from the class 437 `BiobjectiveNondominatedSortedList`, in the module `moarchiving.py` 438 - the `hypervolume_improvement` method from the class 439 `NonDominatedList`, in the module `nondominatedarchive.py` 440 """ 441 if len(solutions) == 0: # when asking a terminated kernel for example 442 return 443 if self.NDA is None: 444 self.NDA = BiobjectiveNondominatedSortedList if len( 445 objective_values[0]) == 2 else NonDominatedList 446 447 objective_values = np.asarray(objective_values).tolist() 448 449 for i in range(len(self._told_indices)): 450 self.kernels[self._told_indices[i]].objective_values = objective_values[i] 451 452 if self.reference_point is None: 453 pass #write here the max among the kernel.objective_values 454 455 start = len(self._told_indices) # position of the first offspring 456 self._told_indices = [] 457 for ikernel, offspring in self.offspring: 458 self.indicator_front.set_kernel(self[ikernel], self) # use reference_point and list_attribute 459 hypervolume_improvements = [self.indicator_front.hypervolume_improvement(point) 460 for point in objective_values[start:start+len(offspring)]] 461 kernel = self.kernels[ikernel] 462 if kernel.fit.median0 is not None and kernel.fit.median0 >= 0: 463 # make sure the median reference comes from the right side of the empirical front 464 # was: ikernel in self._active_indices and kernel.objective_values not in self.pareto_front_cut: 465 # a hack to prevent early termination of dominated kernels 466 # from the `tolfunrel` condition. 467 # TODO: clean implementation, proposal: 468 # if self.indicator_front.hypervolume_improvement(kernel.objective_values) <= 0: # kernel.fit.median0 >= 0 is the same 469 # kernel.stop(reset='tolfunrel') # to be implemented 470 kernel.fit.median0 = None 471 kernel.tell(offspring, [-float(u) for u in hypervolume_improvements]) 472 473 # investigate whether `kernel` hits its stopping criteria 474 if kernel.stop(): 475 self._active_indices.remove(ikernel) # ikernel must be in `_active_indices` 476 self._last_stopped_kernel_id = ikernel 477 478 if self.restart is not None: 479 kernel_to_add = self.restart(self) 480 self._told_indices += [len(self)] 481 self.add(kernel_to_add) 482 483 try: 484 kernel.logger.add() 485 except: 486 pass 487 kernel._last_offspring_f_values = objective_values[start:start+len(offspring)] 488 489 start += len(offspring) 490 491 self._told_indices += [u for (u,v) in self.offspring] 492 493 current_hypervolume = self.pareto_front_cut.hypervolume 494 epsilon = abs(current_hypervolume - self.best_hypervolume_pareto_front) 495 if epsilon: 496 self.epsilon_hypervolume_pareto_front = min(self.epsilon_hypervolume_pareto_front, 497 epsilon) 498 self.best_hypervolume_pareto_front = max(self.best_hypervolume_pareto_front, 499 current_hypervolume) 500 501 if self.isarchive: 502 if not self.archive: 503 self.archive = self.NDA(objective_values, self.reference_point) 504 else: 505 self.archive.add_list(objective_values) 506 self.countiter += 1 507 self.countevals += len(objective_values)
508 509 510 @property
511 - def pareto_front_cut(self):
512 """ 513 return the non-dominated solutions dominating the reference point, 514 among the kernels' objective values. 515 It's the image of `self.pareto_set_cut`. 516 """ 517 return self.NDA([kernel.objective_values for kernel in self.kernels \ 518 if kernel.objective_values is not None], 519 self.reference_point)
520 521 @property
522 - def pareto_set_cut(self):
523 """ 524 return the non-dominated solutions whose images dominate the 525 reference point, among the kernels' incumbents. 526 It's the pre-image of `self.pareto_front_cut`. 527 """ 528 return [kernel.incumbent for kernel in self.kernels if \ 529 kernel.objective_values in self.pareto_front_cut]
530 531 @property
532 - def pareto_front_uncut(self):
533 """ 534 provisorial, return _all_ non-dominated objective values irrespectively 535 536 of the current reference point. Only points whose contributing HV 537 does not depend on the current reference point still have the same 538 cHV in the resulting `list`. HV improvements in general may change. 539 """ 540 f_pairs = [k.objective_values for k in self 541 if k.objective_values is not None] 542 # TODO: this should preferably all be done in self.NDA 543 def reference(f_pairs): 544 reference = [] 545 for i in [0, 1]: # make a reference that is dominated by all f_pairs 546 reference[i] = max(f[i] for f in f_pairs) 547 reference[i] = 1.1**np.sign(reference[i]) * reference[i] + 1 548 return reference
549 if not f_pairs: 550 if self.reference_point in (None, (), [], {}): # should never happen 551 warnings.warn("Sofomore.pareto_front_uncut: self.reference_point = %s" 552 " (#kernels=%d) which should never happen" 553 % (str(self.reference_point), len(self))) 554 return [] 555 return self.NDA(f_pairs, self.reference_point) 556 return self.NDA(f_pairs, reference(f_pairs)) 557
558 - def stop(self):
559 """ 560 return a nonempty dictionary when all kernels stop, containing all the 561 termination status. Therefore it's solely ... on the kernels' `stop` 562 method, which also return dictionaries. 563 Return 564 ------ 565 For example with 5 kernels, stop should return either None, or a `dict` 566 of the form: 567 {0: dict0, 568 1: dict1, 569 2: dict2, 570 3: dict2, 571 4: dict4}, 572 where each index `i` is a key which value is the `dict` instance 573 `self.kernels[i].stop()` 574 """ 575 res = {} 576 for i in range(len(self)): 577 if self.kernels[i].stop(): 578 res[i] = self.kernels[i].stop() 579 else: 580 return False 581 return res
582 583 @property
584 - def termination_status(self):
585 """ 586 return a dictionary of the current termination states of the kernels. 587 588 """ 589 res = {} 590 for i in range(len(self)): 591 res[i] = self.kernels[i].stop() 592 return res
593
594 - def add(self, kernels):
595 """ 596 add `kernels` of type `list` to `self.kernels`. 597 Generally, `kernels` are created from a factory function. 598 If `kernels` is of length 1, the brackets can be omitted. 599 """ 600 if not isinstance(kernels, list): 601 kernels = [kernels] 602 self.kernels += kernels 603 # update `_active_indices` from scratch: inactive kernels might be added 604 self._active_indices = [idx for idx in range(len(self)) if \ 605 not self.kernels[idx].stop()] 606 self._ratio_nondom_offspring_incumbent = len(self) * [0] # len(self) changed
607
608 - def remove(self, kernels):
609 """ 610 remove elements of the `kernels` (type `list`) that belong to 611 `self.kernels`, and update the `_active_indices` attribute. 612 If `kernels` is of length 1, the brackets can be omitted. 613 """ 614 if not isinstance(kernels, list): 615 kernels = [kernels] 616 for kernel in kernels: 617 if kernel in self.kernels: 618 self.kernels.remove(kernel) 619 620 self._active_indices = [idx for idx in range(len(self)) if \ 621 not self.kernels[idx].stop()]
622 623 @property
624 - def median_stds(self):
625 """ 626 """ 627 tab = [] 628 res = [] 629 for kernel in self.kernels: 630 conv = np.sqrt(kernel.dC) 631 vec = [] 632 for i in range(len(kernel.pc)): 633 xi = max(kernel.sigma_vec*kernel.pc[i], kernel.sigma_vec*conv[i]) 634 vec += [kernel.sigma * xi / kernel.sigma0] 635 tab += [sorted(vec)] 636 for i in range(len(tab[0])): 637 vec = [u[i] for u in tab] 638 res += [np.median(vec)] 639 return res
640 641 @property
642 - def max_max_stds(self):
643 """ 644 """ 645 res = 0.0 646 for kernel in self.kernels: 647 conv = np.sqrt(kernel.dC) 648 vec = [] 649 for i in range(len(kernel.pc)): 650 xi = max(kernel.sigma_vec*kernel.pc[i], kernel.sigma_vec*conv[i]) 651 vec += [kernel.sigma * xi / kernel.sigma0] 652 res = max(res, max(vec)) 653 return res
654
655 - def _indices_to_ask(self, number_to_ask):
656 """ 657 """ 658 sorted_indices = sorted(self._remaining_indices_to_ask, key = self.key_sort_indices) 659 indices_to_ask = [] 660 remaining_indices = [] 661 if number_to_ask <= len(sorted_indices): 662 indices_to_ask = sorted_indices[:number_to_ask] 663 remaining_indices = sorted_indices[number_to_ask:] 664 else: 665 val = number_to_ask - len(sorted_indices) 666 indices_to_ask = sorted_indices 667 sorted_indices = sorted(self._active_indices, key = self.key_sort_indices) 668 indices_to_ask += sorted_indices[:val] 669 remaining_indices = sorted_indices[val:] 670 671 self._remaining_indices_to_ask = remaining_indices 672 return indices_to_ask
673
674 - def inactivate(self, kernel):
675 """ 676 inactivate `kernel`, assuming that it's an element of `self.kernels`, 677 or an index in `range(len(self))`. 678 When inactivated, `kernel` is no longer updated, it is ignored. 679 However we do not remove it from `self.kernels`, meaning that `kernel` 680 might still play a role, due to its eventual trace in `self.pareto_front_cut`. 681 682 """ 683 if kernel in self.kernels: 684 ikernel = self.kernels.index(kernel) 685 686 try: 687 self._active_indices.remove(ikernel) 688 self.kernels[ikernel].opts['termination_callback'] += (lambda _: 'kernel turned off',) 689 except (AttributeError, TypeError, KeyError, ValueError): 690 warnings.warn("check again if `opts['termination_callback']` is"+ 691 " correctly used, or if the kernel is not already"+ 692 " turned off.")
693
694 - def activate(self, kernel):
695 """ 696 activate `kernel` when it was inactivated beforehand. Otherwise 697 it remains quiet. 698 699 We expect the kernel's `stop` method in interest to look like: 700 kernel.stop() = {'callback': ['kernel turned off']} 701 """ 702 raise NotImplementedError 703 if kernel in self.kernels: 704 ikernel = self.kernels.index(kernel) 705 new_list = [callback for callback in self.kernels[ikernel].opts['termination_callback']\ 706 if callback(kernel) == 'kernel turned off'] 707 kernel.opts['termination_callback'] = new_list 708 if not kernel.stop(): 709 self._active_indices += [ikernel]
710 711 # The following methods 'disp_annotation' and 'disp' are from the 'cma' 712 # module
713 - def disp_annotation(self):
714 """ 715 copy-pasted from `cma.evolution_strategy`. 716 print annotation line for `disp` ()""" 717 self.has_been_called = True 718 print('Iterat #Fevals Hypervolume axis ratios ' 719 ' sigmas min&max stds\n'+'(median)'.rjust(42) + 720 '(median)'.rjust(10) + '(median)'.rjust(12))
721
722 - def disp(self, modulo=None):
723 """ 724 copy-pasted from `cma.evolution_strategy`. 725 print current state variables in a single-line. 726 copy-pasted from `cma.evolution_strategy` module 727 728 Prints only if ``iteration_counter % modulo == 0``. 729 730 :See also: `disp_annotation`. 731 """ 732 if modulo is None: 733 modulo = self.opts['verb_disp'] 734 735 # console display 736 737 if modulo: 738 if not hasattr(self, 'has_been_called'): 739 self.disp_annotation() 740 741 if self.countiter > 0 and (self.stop() or self.countiter < 4 742 or self.countiter % modulo < 1): 743 try: 744 print(' '.join((repr(self.countiter).rjust(5), 745 repr(self.countevals).rjust(6), 746 '%.15e' % (self.pareto_front_cut.hypervolume), 747 '%4.1e' % (np.median([kernel.D.max() / kernel.D.min() 748 if not kernel.opts['CMA_diagonal'] or kernel.countiter > kernel.opts['CMA_diagonal'] 749 else max(kernel.sigma_vec*1) / min(kernel.sigma_vec*1) \ 750 for kernel in self.kernels])), 751 '%6.2e' % (np.median([kernel.sigma for kernel in self.kernels])), 752 '%6.0e' % (np.median([kernel.sigma * min(kernel.sigma_vec * kernel.dC**0.5) \ 753 for kernel in self.kernels])), 754 '%6.0e' % (np.median([kernel.sigma * max(kernel.sigma_vec * kernel.dC**0.5) \ 755 for kernel in self.kernels])) 756 ))) 757 except AttributeError: 758 pass 759 # if self.countiter < 4: 760 # try: 761 # sys.stdout.flush() : error in matlab: 762 # Python Error: AttributeError: 'MexPrinter' object has no attribute 'flush' 763 764 # except AttributeError: 765 # pass 766 return self
767
768 -class IndicatorFront:
769 """with `hypervolume_improvement` method based on a varying empirical front. 770 771 The front is either all kernels but one or based on the 772 `list_attribute` of `moes` (like `archive`) as given on 773 initialization. 774 775 Usage:: 776 >>> import comocma, cma 777 >>> list_of_solvers_instances = comocma.get_cmas(13 * [5 * [0.4]], 0.7, {'verbose':-9}) 778 >>> fitness = comocma.FitFun(cma.ff.sphere, lambda x: cma.ff.sphere(x-1)) 779 >>> moes = comocma.Sofomore(list_of_solvers_instances, [11, 11]) 780 >>> moes.front_observed = IndicatorFront() 781 >>> moes.optimize(fitness, iterations=47) # doctest:+ELLIPSIS 782 Iterat #Fevals Hypervolume axis ratios sigmas min&max stds*** 783 >>> moes = comocma.Sofomore(list_of_solvers_instances, [11, 11]) 784 >>> moes.front_observed = IndicatorFront(list_attribute='archive') 785 >>> moes.optimize(fitness, iterations=37) # doctest:+ELLIPSIS 786 Iterat #Fevals Hypervolume axis ratios sigmas min&max stds*** 787 >>> moes.front_observed.set_kernel(moes[3], moes) 788 >>> f_points = [moes.front_observed.hypervolume_improvement(point) 789 ... for point in moes[3]._last_offspring_f_values] 790 791 """
792 - def __init__(self, list_attribute=None, NDA=None):
793 """``getattr(moes, list_attribute)`` contains the list to create the front. 794 795 `NDA` is a non-dominated archive with a `hypervolume_improvement` method. 796 797 """ 798 self.list_attribute = list_attribute 799 self.NDA = NDA or BiobjectiveNondominatedSortedList 800 self.kernel = None # current active kernel 801 self.front = None # instance of NDA
802
803 - def hypervolume_improvement(self, point):
804 return self.front.hypervolume_improvement(point)
805
806 - def set_kernel(self, kernel, moes, lazy=True):
807 """Set empirical front for evolving the given kernel. 808 809 By default, make changes only when kernel has changed. 810 811 Details: ``moes.reference_point`` and, in case, its attribute 812 with name `self.list_attribute: str` is used. 813 """ 814 if lazy and kernel == self.kernel: 815 return 816 if self.list_attribute: 817 self.front = self.NDA(getattr(moes, self.list_attribute), 818 moes.reference_point) 819 else: 820 self.front = self.NDA([k.objective_values for k in moes if k != kernel], 821 moes.reference_point) 822 self.kernel = kernel
823 824 cma_kernel_default_options_replacements = { 825 'conditioncov_alleviate': [np.inf, np.inf], 826 'verbose': -1, 827 'tolx': 1e-4, 828 'tolfunrel': 0, 829 'tolfun': 0, 830 'tolfunhist': 0, 831 'tolstagnation': 1e23, # should become float('inf'), 832 }
833 834 -def get_cmas(x_starts, sigma_starts, inopts=None, number_created_kernels=0):
835 """ 836 Factory function that produces `len(x_starts)` instances of type `cmaKernel`. 837 838 Parameters 839 ---------- 840 x_starts : TYPE list or list of lists or list of or ndarrays or ndarray 841 The initial means of the returned cmas. 842 sigma_starts : TYPE float or list of floats 843 The initial step-sizes of the returned cmas. 844 inopts : TYPE dict or list of dicts, optional 845 The cmas' options. 846 number_created_kernels : TYPE int, optional 847 Used as the starting index for the returned cma's names. 848 The values of the options' key 'verb_filenameprefix' rely upon this 849 argument `number_created_kernels`. 850 851 Returns 852 ------- 853 A list of `CmaKernel` instances. 854 855 Example:: 856 >>> import comocma 857 >>> dimension = 10 858 >>> sigma0 = 0.5 859 >>> num_kernels = 11 860 >>> cma_opts = {'tolx': 10**-4, 'popsize': 32} 861 >>> list_of_solvers = comocma.get_cmas(num_kernels * [dimension * [0]], sigma0, cma_opts) 862 863 produce `num_kernels` cma instances. 864 """ 865 866 if x_starts is not None and len(x_starts): 867 try: 868 x_starts = x_starts.tolist() 869 except: 870 pass 871 try: 872 x_starts = [u.tolist() for u in x_starts] 873 except: 874 pass 875 if not isinstance(x_starts[0], list): 876 x_starts = [x_starts] 877 878 kernels = [] 879 num_kernels = len(x_starts) 880 if not isinstance(sigma_starts, list): 881 sigma_starts = num_kernels * [sigma_starts] 882 if inopts is None: 883 inopts = {} 884 list_of_opts = [] 885 if isinstance(inopts, list): 886 list_of_opts = inopts 887 else: 888 list_of_opts = [dict(inopts) for _ in range(num_kernels)] 889 890 # repairing the initial values: 891 for i in range(len(x_starts)): 892 try: 893 mybounds = list_of_opts[i]['bounds'] 894 if isinstance(mybounds, str): 895 mybounds = ast.literal_eval(mybounds) 896 bounds_transform = cma.constraints_handler.BoundTransform(mybounds) 897 x_starts[i] = bounds_transform.repair(x_starts[i]) 898 except KeyError: 899 pass 900 901 for i in range(num_kernels): 902 defopts = cma.CMAOptions() 903 defopts.update(cma_kernel_default_options_replacements) 904 if isinstance(list_of_opts[i], dict): 905 defopts.update(list_of_opts[i]) 906 defopts.update({'verb_filenameprefix': 'cma_kernels' + os.sep + 907 str(number_created_kernels+i)}) 908 909 kernels += [CmaKernel(x_starts[i], sigma_starts[i], defopts)] 910 911 return kernels
912
913 -class CmaKernel(cma.CMAEvolutionStrategy):
914 """ 915 inheriting from the `cma.CMAEvolutionStrategy` class, by adding the property 916 `incumbent`, the attributes `objective_values` and `_last_offspring_f_values`. 917 """
918 - def __init__(self, x0, sigma0, inopts=None):
919 """ 920 Arguments 921 ========= 922 `x0` 923 initial solution, starting point. `x0` is given as "phenotype" 924 which means, if:: 925 926 opts = {'transformation': [transform, inverse]} 927 928 is given and ``inverse is None``, the initial mean is not 929 consistent with `x0` in that ``transform(mean)`` does not 930 equal to `x0` unless ``transform(mean)`` equals ``mean``. 931 `sigma0` 932 initial standard deviation. The problem variables should 933 have been scaled, such that a single standard deviation 934 on all variables is useful and the optimum is expected to 935 lie within about `x0` +- ``3*sigma0``. See also options 936 `scaling_of_variables`. Often one wants to check for 937 solutions close to the initial point. This allows, 938 for example, for an easier check of consistency of the 939 objective function and its interfacing with the optimizer. 940 In this case, a much smaller `sigma0` is advisable. 941 `inopts` 942 options, a dictionary with optional settings, 943 see class `cma.CMAOptions`. 944 """ 945 cma.CMAEvolutionStrategy.__init__(self, x0, sigma0, inopts) 946 self.objective_values = None # the objective value of self's incumbent 947 # (see below for definition of incumbent) 948 self._last_offspring_f_values = None # the fvalues of its offspring
949 # used in the last call of `tell`. 950 951 @property
952 - def incumbent(self):
953 """ 954 it gives the 'repaired' mean of a cma-es. For a problem with bound 955 constraints, `self.incumbent` in inside the bounds. 956 """ 957 return self.boundary_handler.repair(self.mean)
958
959 - def _copy_light(self, sigma=None, inopts=None):
960 """tentative copy of self, versatile (interface and functionalities may change). 961 962 This may not work depending on the used sampler. 963 964 Copy mean and sample distribution parameters and input options. 965 966 Do not copy evolution paths, termination status or other state variables. 967 """ 968 es = super(CmaKernel, self)._copy_light(sigma, inopts) 969 970 es.objective_values = self.objective_values 971 es._last_offspring_f_values = self._last_offspring_f_values 972 return es
973
974 -class FitFun:
975 """ 976 Define a callable multiobjective function from single objective ones. 977 Example: 978 fitness = comocma.FitFun(cma.ff.sphere, lambda x: cma.ff.sphere(x-1)). 979 """
980 - def __init__(self, *args):
981 self.callables = args
982 - def __call__(self, x):
983 return [f(x) for f in self.callables]
984
985 986 # In the following, we define functions for sorting indices to pick in the `tell` method. 987 # This is useful in the case where num_to_ask is equal to 1, 988 # so that the kernels are updated in the `tell` method one by one. 989 990 -def sort_random(i):
991 """ 992 Used for the update order of a Sofomore instance. 993 Example:: 994 moes = Sofomore(list_of_instances, reference_point, {'update_order': sort_random}) 995 randomly picks the kernels to update in the `tell` method of Sofomore. 996 997 """ 998 return np.random.rand()
999
1000 -def sort_increasing(i):
1001 """ 1002 Example:: 1003 moes = Sofomore(list_of_instances, reference_point, {'update_order': sort_increasing}) 1004 updates respectively `self[0]`, `self[1]`, ..., `self[-1]` 1005 in the `tell` method of Sofomore. 1006 """ 1007 return i
1008
1009 -def sort_decreasing(i):
1010 """ 1011 Example:: 1012 moes = Sofomore(list_of_instances, reference_point, {'update_order': sort_decreasing}) 1013 updates respectively `self[-1]`, `self[-2]`, ..., `self[0]` 1014 in the `tell` method of Sofomore. 1015 """ 1016 return -i
1017
1018 -def sort_even_odds(i):
1019 """ 1020 Example:: 1021 moes = Sofomore(list_of_instances, reference_point, {'update_order': sort_even_odds}) 1022 pick the kernels with even indices before the kernels with odd indices in 1023 the `tell` method of Sofomore. 1024 """ 1025 return i % 2
1026
1027 -def sort_odds_even(i):
1028 """ 1029 Example:: 1030 moes = Sofomore(list_of_instances, reference_point, {'update_order': sort_odds_even}) 1031 pick the kernels with odd indices before the kernels with even indices 1032 in the `tell` method of Sofomore. 1033 """ 1034 return - (i % 2)
1035