#!/usr/bin/env python """A minimalistic implemention of CMA-ES without using `numpy`. The Covariance Matrix Adaptation Evolution Strategy, CMA-ES, serves for numerical nonlinear function minimization. The **main functionality** is implemented in 1. class `CMAES`, and 2. function `fmin` which is a small single-line-usage wrapper around `CMAES`. This code has two **purposes**: 1. for READING and UNDERSTANDING the basic flow and the details of the CMA-ES *algorithm*. The source code is meant to be read. For a quick glance, study the first few code lines of `fmin` and the code of method `CMAES.tell`, where all the real work is done in about 20 lines of code (search "def tell" in the source). Otherwise, reading from the top is a feasible option, where the codes of `fmin`, `CMAES.__init__`, `CMAES.ask`, `CMAES.tell` are of particular interest. 2. apply CMA-ES when the python module `numpy` is not available. When `numpy` is available, `cma.fmin` or `cma.CMAEvolutionStrategy` are preferred to run "serious" simulations. The latter code has many more lines, but usually executes faster, offers a richer user interface, better termination options, boundary and noise handling, injection, automated restarts... Dependencies: `math.exp`, `math.log` and `random.normalvariate` (modules `matplotlib.pylab` and `sys` are optional). Testing: call ``python purecma.py`` at the OS shell. Tested with Python 2.6, 2.7, 3.3, 3.5, 3.6. URL: http://github.com/CMA-ES/pycma Last change: September, 2017, version 3.0.0 :Author: Nikolaus Hansen, 2010-2011, 2017 This code is released into the public domain (that is, you may use and modify it however you like). """ from __future__ import division # such that 1/2 != 0 from __future__ import print_function # available since 2.6, not needed ___author__ = "Nikolaus Hansen" __license__ = "public domain" from sys import stdout as _stdout # not strictly necessary import warnings as _warnings from math import log, exp from random import normalvariate as random_normalvariate try: from .interfaces import OOOptimizer, BaseDataLogger as _BaseDataLogger except (ImportError, ValueError): OOOptimizer, _BaseDataLogger = object, object try: from .recombination_weights import RecombinationWeights except (ImportError, ValueError): RecombinationWeights = None del division, print_function #, absolute_import, unicode_literals, with_statement __version__ = '3.0.0' __author__ = 'Nikolaus Hansen' __docformat__ = 'reStructuredText' def fmin(objective_fct, xstart, sigma, args=(), maxfevals='1e3 * N**2', ftarget=None, verb_disp=100, verb_log=1, verb_save=1000): """non-linear non-convex minimization procedure, a functional interface to CMA-ES. Parameters ========== `objective_fct`: `callable` a function that takes as input a `list` of floats (like [3.0, 2.2, 1.1]) and returns a single `float` (a scalar). The objective is to find ``x`` with ``objective_fct(x)`` to be as small as possible. `xstart`: `list` or sequence list of numbers (like `[3.2, 2, 1]`), initial solution vector, its length defines the search space dimension. `sigma`: `float` initial step-size, standard deviation in any coordinate `args`: `tuple` or sequence additional (optional) arguments passed to `objective_fct` `ftarget`: `float` target function value `maxfevals`: `int` or `str` maximal number of function evaluations, a string is evaluated with ``N`` as search space dimension `verb_disp`: `int` display on console every `verb_disp` iteration, 0 for never `verb_log`: `int` data logging every `verb_log` iteration, 0 for never `verb_save`: `int` save logged data every ``verb_save * verb_log`` iteration Return ====== The `tuple` (``xmin``:`list`, ``es``:`CMAES`), where ``xmin`` is the best seen (evaluated) solution and ``es`` is the correspoding `CMAES` instance. Consult ``help(es.result)`` of property `result` for further results. Example ======= The following example minimizes the function `ff.elli`: >>> try: import cma.purecma as purecma ... except ImportError: import purecma >>> def felli(x): ... return sum(10**(6 * i / (len(x)-1)) * xi**2 ... for i, xi in enumerate(x)) >>> res = purecma.fmin(felli, 3 * [0.5], 0.3, verb_disp=100) # doctest:+SKIP evals: ax-ratio max(std) f-value 7: 1.0 3.4e-01 240.2716966 14: 1.0 3.9e-01 2341.50170536 700: 247.9 2.4e-01 0.629102574062 1400: 1185.9 5.3e-07 4.83466373808e-13 1421: 1131.2 2.9e-07 5.50167024417e-14 termination by {'tolfun': 1e-12} best f-value = 2.72976881789e-14 solution = [5.284564665206811e-08, 2.4608091035303e-09, -1.3582873173543187e-10] >>> print(res[0]) # doctest:+SKIP [5.284564665206811e-08, 2.4608091035303e-09, -1.3582873173543187e-10] >>> res[1].result[1]) # doctest:+SKIP 2.72976881789e-14 >>> res[1].logger.plot() # doctest:+SKIP Details ======= After importing `purecma`, this call: >>> es = purecma.fmin(pcma.ff.elli, 10 * [0.5], 0.3, verb_save=0)[1] # doctest:+SKIP and these lines: >>> es = purecma.CMAES(10 * [0.5], 0.3) >>> es.optimize(purecma.ff.elli, callback=es.logger.add) # doctest:+SKIP do pretty much the same. The `verb_save` parameter to `fmin` adds the possibility to plot the saved data *during* the execution from a different Python shell like ``pcma.CMAESDataLogger().load().plot()``. For example, with ``verb_save == 3`` every third time the logger records data they are saved to disk as well. :See: `CMAES`, `OOOptimizer`. """ es = CMAES(xstart, sigma, maxfevals=maxfevals, ftarget=ftarget) if verb_log: # prepare data logging es.logger = CMAESDataLogger(verb_log).add(es, force=True) while not es.stop(): X = es.ask() # get a list of sampled candidate solutions fit = [objective_fct(x, *args) for x in X] # evaluate candidates es.tell(X, fit) # update distribution parameters # that's it! The remainder is managing output behavior only. es.disp(verb_disp) if verb_log: if es.counteval / es.params.lam % verb_log < 1: es.logger.add(es) if verb_save and (es.counteval / es.params.lam % (verb_save * verb_log) < 1): es.logger.save() if verb_disp: # do not print by default to allow silent verbosity es.disp(1) print('termination by', es.stop()) print('best f-value =', es.result[1]) print('solution =', es.result[0]) if verb_log: es.logger.add(es, force=True) es.logger.save() if verb_save else None return [es.best.x if es.best.f < objective_fct(es.xmean) else es.xmean, es] class CMAESParameters(object): """static "internal" parameter setting for `CMAES` """ default_popsize = '4 + int(3 * log(N))' def __init__(self, N, popsize=None, RecombinationWeights=None): """set static, fixed "strategy" parameters once and for all. Input parameter ``RecombinationWeights`` may be set to the class `RecombinationWeights`. """ self.dimension = N self.chiN = N**0.5 * (1 - 1. / (4 * N) + 1. / (21 * N**2)) # Strategy parameter setting: Selection self.lam = eval(safe_str(popsize if popsize else CMAESParameters.default_popsize, {'int': 'int', 'log': 'log', 'N': N})) self.mu = int(self.lam / 2) # number of parents/points/solutions for recombination if RecombinationWeights: self.weights = RecombinationWeights(self.lam) self.mueff = self.weights.mueff else: # set non-negative recombination weights "manually" _weights = [log(self.lam / 2 + 0.5) - log(i + 1) if i < self.mu else 0 for i in range(self.lam)] w_sum = sum(_weights[:self.mu]) self.weights = [w / w_sum for w in _weights] # sum is one now self.mueff = sum(self.weights[:self.mu])**2 / \ sum(w**2 for w in self.weights[:self.mu]) # variance-effectiveness of sum w_i x_i # Strategy parameter setting: Adaptation self.cc = (4 + self.mueff/N) / (N+4 + 2 * self.mueff/N) # time constant for cumulation for C self.cs = (self.mueff + 2) / (N + self.mueff + 5) # time constant for cumulation for sigma control self.c1 = 2 / ((N + 1.3)**2 + self.mueff) # learning rate for rank-one update of C self.cmu = min([1 - self.c1, 2 * (self.mueff - 2 + 1/self.mueff) / ((N + 2)**2 + self.mueff)]) # and for rank-mu update self.damps = 2 * self.mueff/self.lam + 0.3 + self.cs # damping for sigma, usually close to 1 if RecombinationWeights: self.weights.finalize_negative_weights(N, self.c1, self.cmu) # gap to postpone eigendecomposition to achieve O(N**2) per eval # 0.5 is chosen such that eig takes 2 times the time of tell in >=20-D self.lazy_gap_evals = 0.5 * N * self.lam * (self.c1 + self.cmu)**-1 / N**2 class CMAES(OOOptimizer): # could also inherit from object """class for non-linear non-convex numerical minimization with CMA-ES. The class implements the interface define in `OOOptimizer`, namely the methods `__init__`, `ask`, `tell`, `stop`, `disp` and property `result`. Examples -------- The Jupyter notebook or IPython are the favorite environments to execute these examples, both in ``%pylab`` mode. All examples minimize the function `elli`, output is not shown. First we need to import the module we want to use. We import `purecma` from `cma` as (aliased to) ``pcma``:: from cma import purecma as pcma The shortest example uses the inherited method `OOOptimizer.optimize`:: es = pcma.CMAES(8 * [0.1], 0.5).optimize(pcma.ff.elli) See method `CMAES.__init__` for a documentation of the input parameters to `CMAES`. We might have a look at the result:: print(es.result[0]) # best solution and print(es.result[1]) # its function value `result` is a property of `CMAES`. In order to display more exciting output, we may use the `CMAESDataLogger` instance in the `logger` attribute of `CMAES`:: es.logger.plot() # if matplotlib is available Virtually the same example can be written with an explicit loop instead of using `optimize`, see also `fmin`. This gives insight into the `CMAES` class interface and entire control over the iteration loop:: pcma.fmin?? # print source, works in jupyter/ipython only es = pcma.CMAES(9 * [0.5], 0.3) # calls CMAES.__init__() # this loop resembles the method optimize while not es.stop(): # iterate X = es.ask() # get candidate solutions f = [pcma.ff.elli(x) for x in X] # evaluate solutions es.tell(X, f) # do all the real work es.disp(20) # display info every 20th iteration es.logger.add(es) # log another "data line" # final output print('termination by', es.stop()) print('best f-value =', es.result[1]) print('best solution =', es.result[0]) print('potentially better solution xmean =', es.result[5]) print("let's check f(xmean) = ", pcma.ff.elli(es.result[5])) es.logger.plot() # if matplotlib is available A very similar example which may also save the logged data within the loop is the implementation of function `fmin`. Details ------- Most of the work is done in the method `tell`. The property `result` contains more useful output. :See: `fmin`, `OOOptimizer.optimize` """ def __init__(self, xstart, sigma, # mandatory popsize=CMAESParameters.default_popsize, ftarget=None, maxfevals='100 * popsize + ' # 100 iterations plus... '150 * (N + 3)**2 * popsize**0.5', randn=random_normalvariate): """Instantiate `CMAES` object instance using `xstart` and `sigma`. Parameters ---------- `xstart`: `list` of numbers (like ``[3, 2, 1.2]``), initial solution vector `sigma`: `float` initial step-size (standard deviation in each coordinate) `popsize`: `int` or `str` population size, number of candidate samples per iteration `maxfevals`: `int` or `str` maximal number of function evaluations, a string is evaluated with ``N`` as search space dimension `ftarget`: `float` target function value `randn`: `callable` normal random number generator, by default `random.normalvariate` Details: this method initializes the dynamic state variables and creates a `CMAESParameters` instance for static parameters. """ # process some input parameters and set static parameters N = len(xstart) # number of objective variables/problem dimension self.params = CMAESParameters(N, popsize) self.maxfevals = eval(safe_str(maxfevals, known_words={'N': N, 'popsize': self.params.lam})) self.ftarget = ftarget # stop if fitness <= ftarget self.randn = randn # initializing dynamic state variables self.xmean = xstart[:] # initial point, distribution mean, a copy self.sigma = sigma self.pc = N * [0] # evolution path for C self.ps = N * [0] # and for sigma self.C = DecomposingPositiveMatrix(N) # covariance matrix self.counteval = 0 # countiter should be equal to counteval / lam self.fitvals = [] # for bookkeeping output and termination self.best = BestSolution() self.logger = CMAESDataLogger() # for convenience and output def ask(self): """sample lambda candidate solutions distributed according to:: m + sigma * Normal(0,C) = m + sigma * B * D * Normal(0,I) = m + B * D * sigma * Normal(0,I) and return a `list` of the sampled "vectors". """ self.C.update_eigensystem(self.counteval, self.params.lazy_gap_evals) candidate_solutions = [] for _k in range(self.params.lam): # repeat lam times z = [self.sigma * eigenval**0.5 * self.randn(0, 1) for eigenval in self.C.eigenvalues] y = dot(self.C.eigenbasis, z) candidate_solutions.append(plus(self.xmean, y)) return candidate_solutions def tell(self, arx, fitvals): """update the evolution paths and the distribution parameters m, sigma, and C within CMA-ES. Parameters ---------- `arx`: `list` of "row vectors" a list of candidate solution vectors, presumably from calling `ask`. ``arx[k][i]`` is the i-th element of solution vector k. `fitvals`: `list` the corresponding objective function values, to be minimised """ ### bookkeeping and convenience short cuts self.counteval += len(fitvals) # evaluations used within tell N = len(self.xmean) par = self.params xold = self.xmean # not a copy, xmean is assigned anew later ### Sort by fitness arx = [arx[k] for k in argsort(fitvals)] # sorted arx self.fitvals = sorted(fitvals) # used for termination and display only self.best.update(arx[0], self.fitvals[0], self.counteval) ### recombination, compute new weighted mean value self.xmean = dot(arx[0:par.mu], par.weights[:par.mu], transpose=True) # = [sum(self.weights[k] * arx[k][i] for k in range(self.mu)) # for i in range(N)] ### Cumulation: update evolution paths y = minus(self.xmean, xold) z = dot(self.C.invsqrt, y) # == C**(-1/2) * (xnew - xold) csn = (par.cs * (2 - par.cs) * par.mueff)**0.5 / self.sigma for i in range(N): # update evolution path ps self.ps[i] = (1 - par.cs) * self.ps[i] + csn * z[i] ccn = (par.cc * (2 - par.cc) * par.mueff)**0.5 / self.sigma # turn off rank-one accumulation when sigma increases quickly hsig = (sum(x**2 for x in self.ps) / N # ||ps||^2 / N is 1 in expectation / (1-(1-par.cs)**(2*self.counteval/par.lam)) # account for initial value of ps < 2 + 4./(N+1)) # should be smaller than 2 + ... for i in range(N): # update evolution path pc self.pc[i] = (1 - par.cc) * self.pc[i] + ccn * hsig * y[i] ### Adapt covariance matrix C # minor adjustment for the variance loss from hsig c1a = par.c1 * (1 - (1-hsig**2) * par.cc * (2-par.cc)) self.C.multiply_with(1 - c1a - par.cmu * sum(par.weights)) # C *= 1 - c1 - cmu * sum(w) self.C.addouter(self.pc, par.c1) # C += c1 * pc * pc^T, so-called rank-one update for k, wk in enumerate(par.weights): # so-called rank-mu update if wk < 0: # guaranty positive definiteness wk *= N * (self.sigma / self.C.mahalanobis_norm(minus(arx[k], xold)))**2 self.C.addouter(minus(arx[k], xold), # C += wk * cmu * dx * dx^T wk * par.cmu / self.sigma**2) ### Adapt step-size sigma cn, sum_square_ps = par.cs / par.damps, sum(x**2 for x in self.ps) self.sigma *= exp(min(1, cn * (sum_square_ps / N - 1) / 2)) # self.sigma *= exp(min(1, cn * (sum_square_ps**0.5 / par.chiN - 1))) def stop(self): """return satisfied termination conditions in a dictionary, generally speaking like ``{'termination_reason':value, ...}``, for example ``{'tolfun':1e-12}``, or the empty `dict` ``{}``. """ res = {} if self.counteval <= 0: return res if self.counteval >= self.maxfevals: res['maxfevals'] = self.maxfevals if self.ftarget is not None and len(self.fitvals) > 0 \ and self.fitvals[0] <= self.ftarget: res['ftarget'] = self.ftarget if self.C.condition_number > 1e14: res['condition'] = self.C.condition_number if len(self.fitvals) > 1 \ and self.fitvals[-1] - self.fitvals[0] < 1e-12: res['tolfun'] = 1e-12 if self.sigma * max(self.C.eigenvalues)**0.5 < 1e-11: # remark: max(D) >= max(diag(C))**0.5 res['tolx'] = 1e-11 return res @property def result(self): """the `tuple` ``(xbest, f(xbest), evaluations_xbest, evaluations, iterations, xmean, stds)`` """ return (self.best.x, self.best.f, self.best.evals, self.counteval, int(self.counteval / self.params.lam), self.xmean, [self.sigma * C_ii**0.5 for C_ii in self.C.diag]) def disp(self, verb_modulo=1): """`print` some iteration info to `stdout` """ if verb_modulo is None: verb_modulo = 20 if not verb_modulo: return iteration = self.counteval / self.params.lam if iteration == 1 or iteration % (10 * verb_modulo) < 1: print('evals: ax-ratio max(std) f-value') if iteration <= 2 or iteration % verb_modulo < 1: print(str(self.counteval).rjust(5) + ': ' + ' %6.1f %8.1e ' % (self.C.condition_number**0.5, self.sigma * max(self.C.diag)**0.5) + str(self.fitvals[0])) _stdout.flush() # ----------------------------------------------- class CMAESDataLogger(_BaseDataLogger): # could also inherit from object """data logger for class `CMAES`, that can record and plot data. Examples ======== The data may come from `fmin` or `CMAES` and the simulation may still be running in a different Python shell. Use the default logger from `CMAES`: >>> try: import cma.purecma as pcma ... except ImportError: import purecma as pcma >>> es = pcma.CMAES(3 * [0.1], 1) >>> isinstance(es.logger, pcma.CMAESDataLogger) # type(es.logger) True >>> while not es.stop(): ... X = es.ask() ... es.tell(X, [pcma.ff.elli(x) for x in X]) ... es.logger.add(es) # doctest: +SKIP >>> es.logger.save() >>> # es.logger.plot() # Load and plot previously generated data: >>> logger = pcma.CMAESDataLogger().load() >>> logger.filename == "_CMAESDataLogger_datadict.py" True >>> # logger.plot() TODO: the recorded data are kept in memory and keep growing, which may well lead to performance issues for (very?) long runs. Ideally, it should be possible to dump data to a file and clear the memory and also to downsample data to prevent plotting of long runs to take forever. ``"], 'key': "`` or ``"]}"`` is the place where to prepend/append new data in the file. """ plotted = 0 """plot count for all instances""" def __init__(self, verb_modulo=1): """`verb_modulo` controls whether and when logging takes place for each call to the method `add` """ # _BaseDataLogger.__init__(self) # not necessary self.filename = "_CMAESDataLogger_datadict.py" self.optim = None self.modulo = verb_modulo self._data = {'eval': [], 'iter': [], 'stds': [], 'D': [], 'sigma': [], 'fit': [], 'xmean': [], 'more_data': []} self.counter = 0 # number of calls of add def add(self, es=None, force=False, more_data=None): """append some logging data from CMAES class instance `es`, if ``number_of_times_called modulo verb_modulo`` equals zero """ es = es or self.optim if not isinstance(es, CMAES): raise RuntimeWarning('logged object must be a CMAES instance,' ' was %s' % type(es)) dat = self._data # a convenient alias self.counter += 1 if force and self.counter == 1: self.counter = 0 if (self.modulo and (len(dat['eval']) == 0 or es.counteval != dat['eval'][-1]) and (self.counter < 4 or force or int(self.counter) % self.modulo == 0)): dat['eval'].append(es.counteval) dat['iter'].append(es.counteval / es.params.lam) dat['stds'].append([es.C[i][i]**0.5 for i in range(len(es.C))]) dat['D'].append(sorted(ev**0.5 for ev in es.C.eigenvalues)) dat['sigma'].append(es.sigma) dat['fit'].append(es.fitvals[0] if hasattr(es, 'fitvals') and es.fitvals else None) dat['xmean'].append([x for x in es.xmean]) if more_data is not None: dat['more_data'].append(more_data) return self def plot(self, fig_number=322): """plot the stored data in figure `fig_number`. Dependencies: `matlabplotlib.pylab` """ from matplotlib import pylab from matplotlib.pylab import ( gcf, gca, figure, plot, xlabel, grid, semilogy, text, draw, show, ion, subplot as _subplot, tight_layout, rcParamsDefault, xlim, ylim ) def title_(*args, **kwargs): kwargs.setdefault('size', rcParamsDefault['axes.labelsize']) pylab.title(*args, **kwargs) def subtitle(*args, **kwargs): kwargs.setdefault('horizontalalignment', 'center') text(0.5 * (xlim()[1] - xlim()[0]), 0.9 * ylim()[1], *args, **kwargs) def legend_(*args, **kwargs): kwargs.setdefault('framealpha', 0.3) kwargs.setdefault('fancybox', True) kwargs.setdefault('fontsize', rcParamsDefault['font.size'] - 2) pylab.legend(*args, **kwargs) def subplot(*args, **kwargs): with _warnings.catch_warnings(): _warnings.simplefilter("ignore") _subplot(*args, **kwargs) # catch unjustified deprecation warning because subplot and add_subplot are not separate if isinstance(fig_number, int): figure(fig_number) dat = self._data # dictionary with entries as given in __init__ if not dat or not dat['eval'] or len(dat['eval']) <= 2: return try: # a hack to get the presumable population size lambda strpopsize = ' (evaluations / %s)' % str(dat['eval'][-2] - dat['eval'][-3]) except IndexError: strpopsize = '' # plot fit, Delta fit, sigma subplot(221) gca().clear() if dat['fit'][0] is None: # plot is fine with None, but comput- dat['fit'][0] = dat['fit'][1] # tations need numbers # should be reverted later, but let's be lazy assert dat['fit'].count(None) == 0 fmin = min(dat['fit']) imin = dat['fit'].index(fmin) dat['fit'][imin] = max(dat['fit']) + 1 fmin2 = min(dat['fit']) dat['fit'][imin] = fmin semilogy(dat['iter'], [f - fmin if f - fmin > 1e-19 else None for f in dat['fit']], 'c', linewidth=1, label='f-min(f)') semilogy(dat['iter'], [max((fmin2 - fmin, 1e-19)) if f - fmin <= 1e-19 else None for f in dat['fit']], 'C1*') semilogy(dat['iter'], [abs(f) for f in dat['fit']], 'b', label='abs(f-value)') semilogy(dat['iter'], dat['sigma'], 'g', label='sigma') semilogy(dat['iter'][imin], abs(fmin), 'r*', label='abs(min(f))') if dat['more_data']: gca().twinx() plot(dat['iter'], dat['more_data']) grid(True) legend_(*[[v[i] for i in [1, 0, 2, 3]] # just a reordering for v in gca().get_legend_handles_labels()]) # plot xmean subplot(222) gca().clear() plot(dat['iter'], dat['xmean']) for i in range(len(dat['xmean'][-1])): text(dat['iter'][0], dat['xmean'][0][i], str(i)) text(dat['iter'][-1], dat['xmean'][-1][i], str(i)) subtitle('mean solution') grid(True) # plot squareroot of eigenvalues if dat['D'][-1][0] != dat['D'][-1][-1]: subplot(223) gca().clear() semilogy(dat['iter'], dat['D'], 'm') xlabel('iterations' + strpopsize) title_('Axis lengths') grid(True) # plot stds subplot(224) # if len(gcf().axes) > 1: # sca(pylab.gcf().axes[1]) # else: # twinx() gca().clear() semilogy(dat['iter'], dat['stds']) for i in range(len(dat['stds'][-1])): text(dat['iter'][-1], dat['stds'][-1][i], str(i)) title_('Coordinate-wise STDs w/o sigma') grid(True) xlabel('iterations' + strpopsize) _stdout.flush() tight_layout() # avoid superfluous padding # draw(), show() # canvas.draw seem to do the job better ion() # may prevent that everything stops until figure is closed? # todo: if in the same cell, the subplots are small until the cell is finished gcf().canvas.draw() CMAESDataLogger.plotted += 1 def save(self, name=None): """save data to file `name` or ``self.filename``""" with open(name or self.filename, 'w') as f: f.write(repr(self._data)) def load(self, name=None): """load data from file `name` or ``self.filename``""" from ast import literal_eval with open(name or self.filename, 'r') as f: self._data = literal_eval(f.read()) return self #_____________________________________________________________________ #_________________ Fitness (Objective) Functions _____________________ class ff(object): # instead of a submodule """versatile collection of test functions in static methods""" @staticmethod # syntax available since 2.4 def elli(x): """ellipsoid test objective function""" n = len(x) aratio = 1e3 return sum(x[i]**2 * aratio**(2.*i/(n-1)) for i in range(n)) @staticmethod def sphere(x): """sphere, ``sum(x**2)``, test objective function""" return sum(x[i]**2 for i in range(len(x))) @staticmethod def tablet(x): """discus test objective function""" return sum(xi**2 for xi in x) + (1e6-1) * x[0]**2 @staticmethod def rosenbrock(x): """Rosenbrock test objective function""" n = len(x) if n < 2: raise ValueError('dimension must be greater one') return sum(100 * (x[i]**2 - x[i+1])**2 + (x[i] - 1)**2 for i in range(n-1)) #_____________________________________________________________________ #_______________________ Helper Class&Functions ______________________ # class BestSolution(object): """container to keep track of the best solution seen""" def __init__(self, x=None, f=None, evals=None): """take `x`, `f`, and `evals` to initialize the best solution """ self.x, self.f, self.evals = x, f, evals def update(self, x, f, evals=None): """update the best solution if ``f < self.f`` """ if self.f is None or f < self.f: self.x = x self.f = f self.evals = evals return self @property def all(self): """``(x, f, evals)`` of the best seen solution""" return self.x, self.f, self.evals class SquareMatrix(list): # inheritance from numpy.ndarray is not recommended """rudimental square matrix class""" def __init__(self, dimension): """initialize with identity matrix""" for i in range(dimension): self.append(dimension * [0]) self[i][i] = 1 def multiply_with(self, factor): """multiply matrix in place with `factor`""" for row in self: for j in range(len(row)): row[j] *= factor return self def addouter(self, b, factor=1): """Add in place `factor` times outer product of vector `b`, without any dimensional consistency checks. """ for i, row in enumerate(self): for j in range(len(row)): row[j] += factor * b[i] * b[j] return self @property def diag(self): """diagonal of the matrix as a copy (save to change) """ return [self[i][i] for i in range(len(self)) if i < len(self[i])] class DecomposingPositiveMatrix(SquareMatrix): """Symmetric matrix maintaining its own eigendecomposition. If ``isinstance(C, DecomposingPositiveMatrix)``, the eigendecomposion (the return value of `eig`) is stored in the attributes `eigenbasis` and `eigenvalues` such that the i-th eigenvector is:: [row[i] for row in C.eigenbasis] # or equivalently [C.eigenbasis[j][i] for j in range(len(C.eigenbasis))] with eigenvalue ``C.eigenvalues[i]`` and hence:: C = C.eigenbasis x diag(C.eigenvalues) x C.eigenbasis^T """ def __init__(self, dimension): SquareMatrix.__init__(self, dimension) self.eigenbasis = eye(dimension) self.eigenvalues = dimension * [1] self.condition_number = 1 self.invsqrt = eye(dimension) self.updated_eval = 0 def update_eigensystem(self, current_eval, lazy_gap_evals): """Execute eigendecomposition of `self` if ``current_eval > lazy_gap_evals + last_updated_eval``. Assumes (for sake of simplicity) that `self` is positive definite and hence raises a `RuntimeError` otherwise. """ if current_eval <= self.updated_eval + lazy_gap_evals: return self self._enforce_symmetry() # probably not necessary with eig self.eigenvalues, self.eigenbasis = eig(self) # O(N**3) if min(self.eigenvalues) <= 0: raise RuntimeError( "The smallest eigenvalue is <= 0 after %d evaluations!" "\neigenvectors:\n%s \neigenvalues:\n%s" % (current_eval, str(self.eigenbasis), str(self.eigenvalues))) self.condition_number = max(self.eigenvalues) / min(self.eigenvalues) # now compute invsqrt(C) = C**(-1/2) = B D**(-1/2) B' # this is O(n^3) and takes about 25% of the time of eig for i in range(len(self)): for j in range(i+1): self.invsqrt[i][j] = self.invsqrt[j][i] = sum( self.eigenbasis[i][k] * self.eigenbasis[j][k] / self.eigenvalues[k]**0.5 for k in range(len(self))) self.updated_eval = current_eval return self def mahalanobis_norm(self, dx): """return ``(dx^T * C^-1 * dx)**0.5`` """ return sum(xi**2 for xi in dot(self.invsqrt, dx))**0.5 def _enforce_symmetry(self): for i in range(len(self)): for j in range(i): self[i][j] = self[j][i] = (self[i][j] + self[j][i]) / 2 return self def eye(dimension): """return identity matrix as `list` of "vectors" (lists themselves)""" m = [dimension * [0] for i in range(dimension)] # m = N * [N * [0]] fails because it gives N times the same reference for i in range(dimension): m[i][i] = 1 return m def dot(A, b, transpose=False): """ usual dot product of "matrix" A with "vector" b. ``A[i]`` is the i-th row of A. With ``transpose=True``, A transposed is used. """ if not transpose: return [sum(A[i][j] * b[j] for j in range(len(b))) for i in range(len(A))] else: return [sum(A[j][i] * b[j] for j in range(len(b))) for i in range(len(A[0]))] def plus(a, b): """add vectors, return a + b """ return [a[i] + b[i] for i in range(len(a))] def minus(a, b): """subtract vectors, return a - b""" return [a[i] - b[i] for i in range(len(a))] def argsort(a): """return index list to get `a` in order, ie ``a[argsort(a)[i]] == sorted(a)[i]`` """ return sorted(range(len(a)), key=a.__getitem__) # a.__getitem__(i) is a[i] def safe_str(s, known_words=None): """return ``s`` as `str` safe to `eval` or raise an exception. Strings in the `dict` `known_words` are replaced by their values surrounded with a space, which the caller considers safe to evaluate with `eval` afterwards. Known issues: >>> try: from cma.purecma import safe_str ... except ImportError: from purecma import safe_str >>> safe_str('int(p)', {'int': 'int', 'p': 3.1}) # fine ' int ( 3.1 )' >>> safe_str('int(n)', {'int': 'int', 'n': 3.1}) # unexpected ' i 3.1 t ( 3.1 )' """ safe_chars = ' 0123456789.,+-*()[]e<>=' if s != str(s): return str(s) if not known_words: known_words = {} stest = s[:] # test this string sret = s[:] # return this string for word in sorted(known_words.keys(), key=len, reverse=True): stest = stest.replace(word, ' ') sret = sret.replace(word, " %s " % known_words[word]) for c in stest: if c not in safe_chars: raise ValueError('"%s" is not a safe string' ' (known words are %s)' % (s, str(known_words))) return sret #____________________________________________________________ #____________________________________________________________ # # C and B are arrays rather than matrices, because they are # addressed via B[i][j], matrices can only be addressed via B[i,j] # tred2(N, B, diagD, offdiag); # tql2(N, diagD, offdiag, B); # Symmetric Householder reduction to tridiagonal form, translated from # JAMA package. def eig(C): """eigendecomposition of a symmetric matrix. Return the eigenvalues and an orthonormal basis of the corresponding eigenvectors, ``(EVals, Basis)``, where - ``Basis[i]``: `list`, is the i-th row of ``Basis`` - the i-th column of ``Basis``, ie ``[Basis[j][i] for j in range(len(Basis))]`` is the i-th eigenvector with eigenvalue ``EVals[i]`` Details: much slower than `numpy.linalg.eigh`. """ # class eig(object): # def __call__(self, C): # Householder transformation of a symmetric matrix V into tridiagonal # form. # -> n : dimension # -> V : symmetric nxn-matrix # <- V : orthogonal transformation matrix: # tridiag matrix == V * V_in * V^t # <- d : diagonal # <- e[0..n-1] : off diagonal (elements 1..n-1) # Symmetric tridiagonal QL algorithm, iterative # Computes the eigensystem from a tridiagonal matrix in roughtly 3N^3 # operations # -> n : Dimension. # -> d : Diagonale of tridiagonal matrix. # -> e[1..n-1] : off-diagonal, output from Householder # -> V : matrix output von Householder # <- d : eigenvalues # <- e : garbage? # <- V : basis of eigenvectors, according to d # tred2(N, B, diagD, offdiag); B=C on input # tql2(N, diagD, offdiag, B); #import numpy as np #return np.linalg.eigh(C) # return sorted EVs try: num_opt = False # True doesn't work (yet) if num_opt: import numpy as np except ImportError: num_opt = False # private void tred2 (int n, double V[][], double d[], double e[]) { def tred2(n, V, d, e): # This is derived from the Algol procedures tred2 by # Bowdler, Martin, Reinsch, and Wilkinson, Handbook for # Auto. Comp., Vol.ii-Linear Algebra, and the corresponding # Fortran subroutine in EISPACK. # num_opt = False # factor 1.5 in 30-D d[:] = V[n-1][:] # d is output argument if num_opt: # V = np.asarray(V, dtype=float) e = np.asarray(e, dtype=float) # Householder reduction to tridiagonal form. for i in range(n-1, 0, -1): # Scale to avoid under/overflow. h = 0.0 if not num_opt: scale = 0.0 for k in range(i): scale = scale + abs(d[k]) else: scale = sum(np.abs(d[0:i])) if scale == 0.0: e[i] = d[i-1] for j in range(i): d[j] = V[i-1][j] V[i][j] = 0.0 V[j][i] = 0.0 else: # Generate Householder vector. if not num_opt: for k in range(i): d[k] /= scale h += d[k] * d[k] else: d[:i] /= scale h = np.dot(d[:i], d[:i]) f = d[i-1] g = h**0.5 if f > 0: g = -g e[i] = scale * g h -= f * g d[i-1] = f - g if not num_opt: for j in range(i): e[j] = 0.0 else: e[:i] = 0.0 # Apply similarity transformation to remaining columns. for j in range(i): f = d[j] V[j][i] = f g = e[j] + V[j][j] * f if not num_opt: for k in range(j+1, i): g += V[k][j] * d[k] e[k] += V[k][j] * f e[j] = g else: e[j+1:i] += V.T[j][j+1:i] * f e[j] = g + np.dot(V.T[j][j+1:i], d[j+1:i]) f = 0.0 if not num_opt: for j in range(i): e[j] /= h f += e[j] * d[j] else: e[:i] /= h f += np.dot(e[:i], d[:i]) hh = f / (h + h) if not num_opt: for j in range(i): e[j] -= hh * d[j] else: e[:i] -= hh * d[:i] for j in range(i): f = d[j] g = e[j] if not num_opt: for k in range(j, i): V[k][j] -= (f * e[k] + g * d[k]) else: V.T[j][j:i] -= (f * e[j:i] + g * d[j:i]) d[j] = V[i-1][j] V[i][j] = 0.0 d[i] = h # end for i-- # Accumulate transformations. for i in range(n-1): V[n-1][i] = V[i][i] V[i][i] = 1.0 h = d[i+1] if h != 0.0: if not num_opt: for k in range(i+1): d[k] = V[k][i+1] / h else: d[:i+1] = V.T[i+1][:i+1] / h for j in range(i+1): if not num_opt: g = 0.0 for k in range(i+1): g += V[k][i+1] * V[k][j] for k in range(i+1): V[k][j] -= g * d[k] else: g = np.dot(V.T[i+1][0:i+1], V.T[j][0:i+1]) V.T[j][:i+1] -= g * d[:i+1] if not num_opt: for k in range(i+1): V[k][i+1] = 0.0 else: V.T[i+1][:i+1] = 0.0 if not num_opt: for j in range(n): d[j] = V[n-1][j] V[n-1][j] = 0.0 else: d[:n] = V[n-1][:n] V[n-1][:n] = 0.0 V[n-1][n-1] = 1.0 e[0] = 0.0 # Symmetric tridiagonal QL algorithm, taken from JAMA package. # private void tql2 (int n, double d[], double e[], double V[][]) { # needs roughly 3N^3 operations def tql2(n, d, e, V): # This is derived from the Algol procedures tql2, by # Bowdler, Martin, Reinsch, and Wilkinson, Handbook for # Auto. Comp., Vol.ii-Linear Algebra, and the corresponding # Fortran subroutine in EISPACK. # num_opt = False # True doesn't work if not num_opt: for i in range(1, n): # (int i = 1; i < n; i++): e[i-1] = e[i] else: e[0:n-1] = e[1:n] e[n-1] = 0.0 f = 0.0 tst1 = 0.0 eps = 2.0**-52.0 for l in range(n): # (int l = 0; l < n; l++) { # Find small subdiagonal element tst1 = max(tst1, abs(d[l]) + abs(e[l])) m = l while m < n: if abs(e[m]) <= eps*tst1: break m += 1 # If m == l, d[l] is an eigenvalue, # otherwise, iterate. if m > l: iiter = 0 while 1: # do { iiter += 1 # (Could check iteration count here.) # Compute implicit shift g = d[l] p = (d[l+1] - g) / (2.0 * e[l]) r = (p**2 + 1)**0.5 # hypot(p, 1.0) if p < 0: r = -r d[l] = e[l] / (p + r) d[l+1] = e[l] * (p + r) dl1 = d[l+1] h = g - d[l] if not num_opt: for i in range(l+2, n): d[i] -= h else: d[l+2:n] -= h f = f + h # Implicit QL transformation. p = d[m] c = 1.0 c2 = c c3 = c el1 = e[l+1] s = 0.0 s2 = 0.0 # hh = V.T[0].copy() # only with num_opt for i in range(m-1, l-1, -1): # (int i = m-1; i >= l; i--) { c3 = c2 c2 = c s2 = s g = c * e[i] h = c * p r = (p**2 + e[i]**2)**0.5 # hypot(p,e[i]) e[i+1] = s * r s = e[i] / r c = p / r p = c * d[i] - s * g d[i+1] = h + s * (c * g + s * d[i]) # Accumulate transformation. if not num_opt: # overall factor 3 in 30-D for k in range(n): # (int k = 0; k < n; k++){ h = V[k][i+1] V[k][i+1] = s * V[k][i] + c * h V[k][i] = c * V[k][i] - s * h else: # about 20% faster in 10-D hh = V.T[i+1].copy() # hh[:] = V.T[i+1][:] V.T[i+1] = s * V.T[i] + c * hh V.T[i] = c * V.T[i] - s * hh # V.T[i] *= c # V.T[i] -= s * hh p = -s * s2 * c3 * el1 * e[l] / dl1 e[l] = s * p d[l] = c * p # Check for convergence. if abs(e[l]) <= eps*tst1: break # } while (Math.abs(e[l]) > eps*tst1); d[l] += f e[l] = 0.0 # Sort eigenvalues and corresponding vectors. if 11 < 3: for i in range(n-1): # (int i = 0; i < n-1; i++) { k = i p = d[i] for j in range(i+1, n): # (int j = i+1; j < n; j++) { if d[j] < p: # NH find smallest k>i k = j p = d[j] if k != i: d[k] = d[i] # swap k and i d[i] = p for j in range(n): # (int j = 0; j < n; j++) { p = V[j][i] V[j][i] = V[j][k] V[j][k] = p # tql2 N = len(C[0]) V = [C[i][:] for i in range(N)] d = N * [0] e = N * [0] tred2(N, V, d, e) tql2(N, d, e, V) return d, V # sorting of V-columns in place is non-trivial def test(): """test of the `purecma` module, called ``if __name__ == "__main__"``. Currently only based on `doctest`: >>> try: import cma.purecma as pcma ... except ImportError: import purecma as pcma >>> import random >>> random.seed(8) >>> xmin, es = pcma.fmin(pcma.ff.rosenbrock, 4 * [0.5], 0.5, ... verb_disp=0, verb_log=1) >>> print(es.counteval) 1712 >>> print(es.best.evals) 1704 >>> assert es.best.f < 1e-12 >>> random.seed(5) >>> es = pcma.CMAES(4 * [0.5], 0.5) >>> es.params = pcma.CMAESParameters(es.params.dimension, ... es.params.lam, ... pcma.RecombinationWeights) >>> while not es.stop(): ... X = es.ask() ... es.tell(X, [pcma.ff.rosenbrock(x) for x in X]) >>> print("%s, %s" % (pcma.ff.rosenbrock(es.result[0]) < 1e-13, ... es.result[2] < 1600)) True, True Large population size: >>> random.seed(4) >>> es = pcma.CMAES(3 * [1], 1) >>> es.params = pcma.CMAESParameters(es.params.dimension, 300, ... pcma.RecombinationWeights) >>> es.logger = pcma.CMAESDataLogger() >>> try: ... es = es.optimize(pcma.ff.elli, verb_disp=0) ... except AttributeError: # OOOptimizer.optimize is not available ... while not es.stop(): ... X = es.ask() ... es.tell(X, [pcma.ff.elli(x) for x in X]) >>> assert es.result[1] < 1e13 >>> print(es.result[2]) 9300 """ import doctest print('launching doctest...') print(doctest.testmod(report=True, verbose=0)) # module test #_____________________________________________________________________ #_____________________________________________________________________ # if __name__ == "__main__": test() # fmin(ff.rosenbrock, 10 * [0.5], 0.5)