|
| 1 | +"""Multi Objective CMA-es class""" |
| 2 | + |
| 3 | +""" |
| 4 | +Copyright (c) 2016, EPFL/Blue Brain Project |
| 5 | +
|
| 6 | + This file is part of BluePyOpt <https://github.com/BlueBrain/BluePyOpt> |
| 7 | +
|
| 8 | + This library is free software; you can redistribute it and/or modify it under |
| 9 | + the terms of the GNU Lesser General Public License version 3.0 as published |
| 10 | + by the Free Software Foundation. |
| 11 | +
|
| 12 | + This library is distributed in the hope that it will be useful, but WITHOUT |
| 13 | + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 14 | + FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
| 15 | + details. |
| 16 | +
|
| 17 | + You should have received a copy of the GNU Lesser General Public License |
| 18 | + along with this library; if not, write to the Free Software Foundation, Inc., |
| 19 | + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| 20 | +""" |
| 21 | + |
| 22 | +# pylint: disable=R0912, R0914 |
| 23 | + |
| 24 | +import logging |
| 25 | +import numpy |
| 26 | +import copy |
| 27 | +from math import log |
| 28 | + |
| 29 | +import deap |
| 30 | +from deap import base |
| 31 | +from deap import cma |
| 32 | + |
| 33 | +from .stoppingCriteria import MaxNGen |
| 34 | + |
| 35 | +from .utils import _bound |
| 36 | + |
| 37 | +logger = logging.getLogger('__main__') |
| 38 | + |
| 39 | +from deap.tools._hypervolume import hv as hv_c |
| 40 | + |
| 41 | + |
| 42 | +def get_hv(to_evaluate): |
| 43 | + i = to_evaluate[0] |
| 44 | + wobj = to_evaluate[1] |
| 45 | + ref = to_evaluate[2] |
| 46 | + return hv_c.hypervolume(numpy.concatenate((wobj[:i], wobj[i + 1:])), ref) |
| 47 | + |
| 48 | + |
| 49 | +def contribution(to_evaluate): |
| 50 | + def _reduce_method(meth): |
| 51 | + """Overwrite reduce""" |
| 52 | + return (getattr, (meth.__self__, meth.__func__.__name__)) |
| 53 | + |
| 54 | + import copyreg |
| 55 | + import types |
| 56 | + copyreg.pickle(types.MethodType, _reduce_method) |
| 57 | + import pebble |
| 58 | + |
| 59 | + with pebble.ProcessPool(max_tasks=1) as pool: |
| 60 | + tasks = pool.schedule(get_hv, kwargs={'to_evaluate': to_evaluate}) |
| 61 | + response = tasks.result() |
| 62 | + |
| 63 | + return response |
| 64 | + |
| 65 | + |
| 66 | +class CMA_MO(cma.StrategyMultiObjective): |
| 67 | + """Multiple objective covariance matrix adaption""" |
| 68 | + |
| 69 | + def __init__(self, |
| 70 | + centroids, |
| 71 | + offspring_size, |
| 72 | + sigma, |
| 73 | + max_ngen, |
| 74 | + IndCreator, |
| 75 | + RandIndCreator, |
| 76 | + map_function=None, |
| 77 | + use_scoop=False): |
| 78 | + """Constructor |
| 79 | +
|
| 80 | + Args: |
| 81 | + centroid (list): initial guess used as the starting point of |
| 82 | + the CMA-ES |
| 83 | + sigma (float): initial standard deviation of the distribution |
| 84 | + max_ngen (int): total number of generation to run |
| 85 | + IndCreator (fcn): function returning an individual of the pop |
| 86 | + """ |
| 87 | + |
| 88 | + if offspring_size is None: |
| 89 | + lambda_ = int(4 + 3 * log(len(RandIndCreator()))) |
| 90 | + else: |
| 91 | + lambda_ = offspring_size |
| 92 | + |
| 93 | + if centroids is None: |
| 94 | + starters = [RandIndCreator() for i in range(lambda_)] |
| 95 | + else: |
| 96 | + if len(centroids) != lambda_: |
| 97 | + from itertools import cycle |
| 98 | + generator = cycle(centroids) |
| 99 | + starters = [next(generator) for i in range(lambda_)] |
| 100 | + else: |
| 101 | + starters = centroids |
| 102 | + |
| 103 | + cma.StrategyMultiObjective.__init__(self, starters, sigma, |
| 104 | + mu=int(lambda_ * 0.5), |
| 105 | + lambda_=lambda_) |
| 106 | + |
| 107 | + self.population = [] |
| 108 | + self.problem_size = len(starters[0]) |
| 109 | + |
| 110 | + self.map_function = map_function |
| 111 | + self.use_scoop = use_scoop |
| 112 | + |
| 113 | + # Toolbox specific to this CMA-ES |
| 114 | + self.toolbox = base.Toolbox() |
| 115 | + self.toolbox.register("generate", self.generate, IndCreator) |
| 116 | + self.toolbox.register("update", self.update) |
| 117 | + |
| 118 | + if self.use_scoop: |
| 119 | + if self.map_function: |
| 120 | + raise Exception( |
| 121 | + 'Impossible to use scoop is providing self defined map ' |
| 122 | + 'function: %s' % self.map_function) |
| 123 | + from scoop import futures |
| 124 | + self.toolbox.register("map", futures.map) |
| 125 | + elif self.map_function: |
| 126 | + self.toolbox.register("map", self.map_function) |
| 127 | + |
| 128 | + # Set termination conditions |
| 129 | + self.active = True |
| 130 | + if max_ngen <= 0: |
| 131 | + max_ngen = 100 + 50 * (self.problem_size + 3) ** 2 / numpy.sqrt( |
| 132 | + self.lambda_) |
| 133 | + |
| 134 | + self.stopping_conditions = [MaxNGen(max_ngen)] |
| 135 | + |
| 136 | + def hyper_volume(self, front): |
| 137 | + |
| 138 | + wobj = numpy.array([ind.fitness.values for ind in front]) |
| 139 | + obj_ranges = (numpy.max(wobj, axis=0) - numpy.min(wobj, axis=0)) |
| 140 | + ref = numpy.max(wobj, axis=0) + 1 |
| 141 | + |
| 142 | + # Above 23 dim, hypervolume is too slow, so I settle for an approximation |
| 143 | + max_ndim = 23 |
| 144 | + if len(ref) > max_ndim: |
| 145 | + idxs = list(range(len(ref))) |
| 146 | + idxs = [idxs[k] for k in numpy.argsort(obj_ranges)] |
| 147 | + idxs = idxs[::-1] |
| 148 | + idxs = idxs[:max_ndim] |
| 149 | + wobj = wobj[:, idxs] |
| 150 | + ref = ref[idxs] |
| 151 | + |
| 152 | + to_evaluate = [] |
| 153 | + for i in range(len(front)): |
| 154 | + to_evaluate.append([i, numpy.copy(wobj), numpy.copy(ref)]) |
| 155 | + |
| 156 | + contrib_values = self.toolbox.map(contribution, to_evaluate) |
| 157 | + |
| 158 | + return list(contrib_values) |
| 159 | + |
| 160 | + def _select(self, candidates): |
| 161 | + if len(candidates) <= self.mu: |
| 162 | + return candidates, [] |
| 163 | + |
| 164 | + pareto_fronts = deap.tools.sortLogNondominated(candidates, |
| 165 | + len(candidates)) |
| 166 | + |
| 167 | + chosen = list() |
| 168 | + mid_front = None |
| 169 | + not_chosen = list() |
| 170 | + |
| 171 | + # Fill the next population (chosen) with the fronts until there is not enouch space |
| 172 | + # When an entire front does not fit in the space left we rely on the hypervolume |
| 173 | + # for this front |
| 174 | + # The remaining fronts are explicitly not chosen |
| 175 | + full = False |
| 176 | + for front in pareto_fronts: |
| 177 | + if len(chosen) + len(front) <= self.mu and not full: |
| 178 | + chosen += front |
| 179 | + elif mid_front is None and len(chosen) < self.mu: |
| 180 | + mid_front = front |
| 181 | + # With this front, we selected enough individuals |
| 182 | + full = True |
| 183 | + else: |
| 184 | + not_chosen += front |
| 185 | + |
| 186 | + k = self.mu - len(chosen) |
| 187 | + if k > 0: |
| 188 | + hyperv = self.hyper_volume(mid_front) |
| 189 | + _ = [mid_front[k] for k in numpy.argsort(hyperv)] |
| 190 | + chosen += _[:k] |
| 191 | + not_chosen += _[k:] |
| 192 | + |
| 193 | + return chosen, not_chosen |
| 194 | + |
| 195 | + def get_population(self, to_space): |
| 196 | + """Returns the population in the original parameter space""" |
| 197 | + pop = copy.deepcopy(self.population) |
| 198 | + for i, ind in enumerate(pop): |
| 199 | + for j, v in enumerate(ind): |
| 200 | + pop[i][j] = to_space[j](v) |
| 201 | + return pop |
| 202 | + |
| 203 | + def get_parents(self, to_space): |
| 204 | + """Returns the population in the original parameter space""" |
| 205 | + pop = copy.deepcopy(self.parents) |
| 206 | + for i, ind in enumerate(pop): |
| 207 | + for j, v in enumerate(ind): |
| 208 | + pop[i][j] = to_space[j](v) |
| 209 | + return pop |
| 210 | + |
| 211 | + def generate_new_pop(self, lbounds, ubounds): |
| 212 | + """Generate a new population bounded in the normalized space""" |
| 213 | + self.population = self.toolbox.generate() |
| 214 | + return _bound(self.population, lbounds, ubounds) |
| 215 | + |
| 216 | + def update_strategy(self): |
| 217 | + self.toolbox.update(self.population) |
| 218 | + |
| 219 | + def set_fitness(self, fitnesses): |
| 220 | + for f, ind in zip(fitnesses, self.population): |
| 221 | + ind.fitness.values = f |
| 222 | + |
| 223 | + def set_fitness_parents(self, fitnesses): |
| 224 | + for f, ind in zip(fitnesses, self.parents): |
| 225 | + ind.fitness.values = f |
| 226 | + |
| 227 | + def check_termination(self, ngen): |
| 228 | + stopping_params = { |
| 229 | + "ngen": ngen, |
| 230 | + "population": self.population, |
| 231 | + } |
| 232 | + |
| 233 | + [c.check(stopping_params) for c in self.stopping_conditions] |
| 234 | + for c in self.stopping_conditions: |
| 235 | + if c.criteria_met: |
| 236 | + logger.info('CMA stopped because of termination criteria: ' + |
| 237 | + ' '.join(type(c).__name__)) |
| 238 | + self.active = False |
0 commit comments