733 lines
30 KiB
Python
733 lines
30 KiB
Python
# Dual Annealing implementation.
|
|
# Copyright (c) 2018 Sylvain Gubian <sylvain.gubian@pmi.com>,
|
|
# Yang Xiang <yang.xiang@pmi.com>
|
|
# Author: Sylvain Gubian, Yang Xiang, PMP S.A.
|
|
|
|
"""
|
|
A Dual Annealing global optimization algorithm
|
|
"""
|
|
|
|
import numpy as np
|
|
from scipy.optimize import OptimizeResult
|
|
from scipy.optimize import minimize, Bounds
|
|
from scipy.special import gammaln
|
|
from scipy._lib._util import check_random_state
|
|
from scipy.optimize._constraints import new_bounds_to_old
|
|
|
|
__all__ = ['dual_annealing']
|
|
|
|
|
|
class VisitingDistribution:
|
|
"""
|
|
Class used to generate new coordinates based on the distorted
|
|
Cauchy-Lorentz distribution. Depending on the steps within the strategy
|
|
chain, the class implements the strategy for generating new location
|
|
changes.
|
|
|
|
Parameters
|
|
----------
|
|
lb : array_like
|
|
A 1-D NumPy ndarray containing lower bounds of the generated
|
|
components. Neither NaN or inf are allowed.
|
|
ub : array_like
|
|
A 1-D NumPy ndarray containing upper bounds for the generated
|
|
components. Neither NaN or inf are allowed.
|
|
visiting_param : float
|
|
Parameter for visiting distribution. Default value is 2.62.
|
|
Higher values give the visiting distribution a heavier tail, this
|
|
makes the algorithm jump to a more distant region.
|
|
The value range is (1, 3]. Its value is fixed for the life of the
|
|
object.
|
|
rand_gen : {`~numpy.random.RandomState`, `~numpy.random.Generator`}
|
|
A `~numpy.random.RandomState`, `~numpy.random.Generator` object
|
|
for using the current state of the created random generator container.
|
|
|
|
"""
|
|
TAIL_LIMIT = 1.e8
|
|
MIN_VISIT_BOUND = 1.e-10
|
|
|
|
def __init__(self, lb, ub, visiting_param, rand_gen):
|
|
# if you wish to make _visiting_param adjustable during the life of
|
|
# the object then _factor2, _factor3, _factor5, _d1, _factor6 will
|
|
# have to be dynamically calculated in `visit_fn`. They're factored
|
|
# out here so they don't need to be recalculated all the time.
|
|
self._visiting_param = visiting_param
|
|
self.rand_gen = rand_gen
|
|
self.lower = lb
|
|
self.upper = ub
|
|
self.bound_range = ub - lb
|
|
|
|
# these are invariant numbers unless visiting_param changes
|
|
self._factor2 = np.exp((4.0 - self._visiting_param) * np.log(
|
|
self._visiting_param - 1.0))
|
|
self._factor3 = np.exp((2.0 - self._visiting_param) * np.log(2.0)
|
|
/ (self._visiting_param - 1.0))
|
|
self._factor4_p = np.sqrt(np.pi) * self._factor2 / (self._factor3 * (
|
|
3.0 - self._visiting_param))
|
|
|
|
self._factor5 = 1.0 / (self._visiting_param - 1.0) - 0.5
|
|
self._d1 = 2.0 - self._factor5
|
|
self._factor6 = np.pi * (1.0 - self._factor5) / np.sin(
|
|
np.pi * (1.0 - self._factor5)) / np.exp(gammaln(self._d1))
|
|
|
|
def visiting(self, x, step, temperature):
|
|
""" Based on the step in the strategy chain, new coordinates are
|
|
generated by changing all components is the same time or only
|
|
one of them, the new values are computed with visit_fn method
|
|
"""
|
|
dim = x.size
|
|
if step < dim:
|
|
# Changing all coordinates with a new visiting value
|
|
visits = self.visit_fn(temperature, dim)
|
|
upper_sample, lower_sample = self.rand_gen.uniform(size=2)
|
|
visits[visits > self.TAIL_LIMIT] = self.TAIL_LIMIT * upper_sample
|
|
visits[visits < -self.TAIL_LIMIT] = -self.TAIL_LIMIT * lower_sample
|
|
x_visit = visits + x
|
|
a = x_visit - self.lower
|
|
b = np.fmod(a, self.bound_range) + self.bound_range
|
|
x_visit = np.fmod(b, self.bound_range) + self.lower
|
|
x_visit[np.fabs(
|
|
x_visit - self.lower) < self.MIN_VISIT_BOUND] += 1.e-10
|
|
else:
|
|
# Changing only one coordinate at a time based on strategy
|
|
# chain step
|
|
x_visit = np.copy(x)
|
|
visit = self.visit_fn(temperature, 1)[0]
|
|
if visit > self.TAIL_LIMIT:
|
|
visit = self.TAIL_LIMIT * self.rand_gen.uniform()
|
|
elif visit < -self.TAIL_LIMIT:
|
|
visit = -self.TAIL_LIMIT * self.rand_gen.uniform()
|
|
index = step - dim
|
|
x_visit[index] = visit + x[index]
|
|
a = x_visit[index] - self.lower[index]
|
|
b = np.fmod(a, self.bound_range[index]) + self.bound_range[index]
|
|
x_visit[index] = np.fmod(b, self.bound_range[
|
|
index]) + self.lower[index]
|
|
if np.fabs(x_visit[index] - self.lower[
|
|
index]) < self.MIN_VISIT_BOUND:
|
|
x_visit[index] += self.MIN_VISIT_BOUND
|
|
return x_visit
|
|
|
|
def visit_fn(self, temperature, dim):
|
|
""" Formula Visita from p. 405 of reference [2] """
|
|
x, y = self.rand_gen.normal(size=(dim, 2)).T
|
|
|
|
factor1 = np.exp(np.log(temperature) / (self._visiting_param - 1.0))
|
|
factor4 = self._factor4_p * factor1
|
|
|
|
# sigmax
|
|
x *= np.exp(-(self._visiting_param - 1.0) * np.log(
|
|
self._factor6 / factor4) / (3.0 - self._visiting_param))
|
|
|
|
den = np.exp((self._visiting_param - 1.0) * np.log(np.fabs(y)) /
|
|
(3.0 - self._visiting_param))
|
|
|
|
return x / den
|
|
|
|
|
|
class EnergyState:
|
|
"""
|
|
Class used to record the energy state. At any time, it knows what is the
|
|
currently used coordinates and the most recent best location.
|
|
|
|
Parameters
|
|
----------
|
|
lower : array_like
|
|
A 1-D NumPy ndarray containing lower bounds for generating an initial
|
|
random components in the `reset` method.
|
|
upper : array_like
|
|
A 1-D NumPy ndarray containing upper bounds for generating an initial
|
|
random components in the `reset` method
|
|
components. Neither NaN or inf are allowed.
|
|
callback : callable, ``callback(x, f, context)``, optional
|
|
A callback function which will be called for all minima found.
|
|
``x`` and ``f`` are the coordinates and function value of the
|
|
latest minimum found, and `context` has value in [0, 1, 2]
|
|
"""
|
|
# Maximum number of trials for generating a valid starting point
|
|
MAX_REINIT_COUNT = 1000
|
|
|
|
def __init__(self, lower, upper, callback=None):
|
|
self.ebest = None
|
|
self.current_energy = None
|
|
self.current_location = None
|
|
self.xbest = None
|
|
self.lower = lower
|
|
self.upper = upper
|
|
self.callback = callback
|
|
|
|
def reset(self, func_wrapper, rand_gen, x0=None):
|
|
"""
|
|
Initialize current location is the search domain. If `x0` is not
|
|
provided, a random location within the bounds is generated.
|
|
"""
|
|
if x0 is None:
|
|
self.current_location = rand_gen.uniform(self.lower, self.upper,
|
|
size=len(self.lower))
|
|
else:
|
|
self.current_location = np.copy(x0)
|
|
init_error = True
|
|
reinit_counter = 0
|
|
while init_error:
|
|
self.current_energy = func_wrapper.fun(self.current_location)
|
|
if self.current_energy is None:
|
|
raise ValueError('Objective function is returning None')
|
|
if (not np.isfinite(self.current_energy) or np.isnan(
|
|
self.current_energy)):
|
|
if reinit_counter >= EnergyState.MAX_REINIT_COUNT:
|
|
init_error = False
|
|
message = (
|
|
'Stopping algorithm because function '
|
|
'create NaN or (+/-) infinity values even with '
|
|
'trying new random parameters'
|
|
)
|
|
raise ValueError(message)
|
|
self.current_location = rand_gen.uniform(self.lower,
|
|
self.upper,
|
|
size=self.lower.size)
|
|
reinit_counter += 1
|
|
else:
|
|
init_error = False
|
|
# If first time reset, initialize ebest and xbest
|
|
if self.ebest is None and self.xbest is None:
|
|
self.ebest = self.current_energy
|
|
self.xbest = np.copy(self.current_location)
|
|
# Otherwise, we keep them in case of reannealing reset
|
|
|
|
def update_best(self, e, x, context):
|
|
self.ebest = e
|
|
self.xbest = np.copy(x)
|
|
if self.callback is not None:
|
|
val = self.callback(x, e, context)
|
|
if val is not None:
|
|
if val:
|
|
return ('Callback function requested to stop early by '
|
|
'returning True')
|
|
|
|
def update_current(self, e, x):
|
|
self.current_energy = e
|
|
self.current_location = np.copy(x)
|
|
|
|
|
|
class StrategyChain:
|
|
"""
|
|
Class that implements within a Markov chain the strategy for location
|
|
acceptance and local search decision making.
|
|
|
|
Parameters
|
|
----------
|
|
acceptance_param : float
|
|
Parameter for acceptance distribution. It is used to control the
|
|
probability of acceptance. The lower the acceptance parameter, the
|
|
smaller the probability of acceptance. Default value is -5.0 with
|
|
a range (-1e4, -5].
|
|
visit_dist : VisitingDistribution
|
|
Instance of `VisitingDistribution` class.
|
|
func_wrapper : ObjectiveFunWrapper
|
|
Instance of `ObjectiveFunWrapper` class.
|
|
minimizer_wrapper: LocalSearchWrapper
|
|
Instance of `LocalSearchWrapper` class.
|
|
rand_gen : {None, int, `numpy.random.Generator`,
|
|
`numpy.random.RandomState`}, optional
|
|
|
|
If `seed` is None (or `np.random`), the `numpy.random.RandomState`
|
|
singleton is used.
|
|
If `seed` is an int, a new ``RandomState`` instance is used,
|
|
seeded with `seed`.
|
|
If `seed` is already a ``Generator`` or ``RandomState`` instance then
|
|
that instance is used.
|
|
energy_state: EnergyState
|
|
Instance of `EnergyState` class.
|
|
|
|
"""
|
|
|
|
def __init__(self, acceptance_param, visit_dist, func_wrapper,
|
|
minimizer_wrapper, rand_gen, energy_state):
|
|
# Local strategy chain minimum energy and location
|
|
self.emin = energy_state.current_energy
|
|
self.xmin = np.array(energy_state.current_location)
|
|
# Global optimizer state
|
|
self.energy_state = energy_state
|
|
# Acceptance parameter
|
|
self.acceptance_param = acceptance_param
|
|
# Visiting distribution instance
|
|
self.visit_dist = visit_dist
|
|
# Wrapper to objective function
|
|
self.func_wrapper = func_wrapper
|
|
# Wrapper to the local minimizer
|
|
self.minimizer_wrapper = minimizer_wrapper
|
|
self.not_improved_idx = 0
|
|
self.not_improved_max_idx = 1000
|
|
self._rand_gen = rand_gen
|
|
self.temperature_step = 0
|
|
self.K = 100 * len(energy_state.current_location)
|
|
|
|
def accept_reject(self, j, e, x_visit):
|
|
r = self._rand_gen.uniform()
|
|
pqv_temp = 1.0 - ((1.0 - self.acceptance_param) *
|
|
(e - self.energy_state.current_energy) / self.temperature_step)
|
|
if pqv_temp <= 0.:
|
|
pqv = 0.
|
|
else:
|
|
pqv = np.exp(np.log(pqv_temp) / (
|
|
1. - self.acceptance_param))
|
|
|
|
if r <= pqv:
|
|
# We accept the new location and update state
|
|
self.energy_state.update_current(e, x_visit)
|
|
self.xmin = np.copy(self.energy_state.current_location)
|
|
|
|
# No improvement for a long time
|
|
if self.not_improved_idx >= self.not_improved_max_idx:
|
|
if j == 0 or self.energy_state.current_energy < self.emin:
|
|
self.emin = self.energy_state.current_energy
|
|
self.xmin = np.copy(self.energy_state.current_location)
|
|
|
|
def run(self, step, temperature):
|
|
self.temperature_step = temperature / float(step + 1)
|
|
self.not_improved_idx += 1
|
|
for j in range(self.energy_state.current_location.size * 2):
|
|
if j == 0:
|
|
if step == 0:
|
|
self.energy_state_improved = True
|
|
else:
|
|
self.energy_state_improved = False
|
|
x_visit = self.visit_dist.visiting(
|
|
self.energy_state.current_location, j, temperature)
|
|
# Calling the objective function
|
|
e = self.func_wrapper.fun(x_visit)
|
|
if e < self.energy_state.current_energy:
|
|
# We have got a better energy value
|
|
self.energy_state.update_current(e, x_visit)
|
|
if e < self.energy_state.ebest:
|
|
val = self.energy_state.update_best(e, x_visit, 0)
|
|
if val is not None:
|
|
if val:
|
|
return val
|
|
self.energy_state_improved = True
|
|
self.not_improved_idx = 0
|
|
else:
|
|
# We have not improved but do we accept the new location?
|
|
self.accept_reject(j, e, x_visit)
|
|
if self.func_wrapper.nfev >= self.func_wrapper.maxfun:
|
|
return ('Maximum number of function call reached '
|
|
'during annealing')
|
|
# End of StrategyChain loop
|
|
|
|
def local_search(self):
|
|
# Decision making for performing a local search
|
|
# based on strategy chain results
|
|
# If energy has been improved or no improvement since too long,
|
|
# performing a local search with the best strategy chain location
|
|
if self.energy_state_improved:
|
|
# Global energy has improved, let's see if LS improves further
|
|
e, x = self.minimizer_wrapper.local_search(self.energy_state.xbest,
|
|
self.energy_state.ebest)
|
|
if e < self.energy_state.ebest:
|
|
self.not_improved_idx = 0
|
|
val = self.energy_state.update_best(e, x, 1)
|
|
if val is not None:
|
|
if val:
|
|
return val
|
|
self.energy_state.update_current(e, x)
|
|
if self.func_wrapper.nfev >= self.func_wrapper.maxfun:
|
|
return ('Maximum number of function call reached '
|
|
'during local search')
|
|
# Check probability of a need to perform a LS even if no improvement
|
|
do_ls = False
|
|
if self.K < 90 * len(self.energy_state.current_location):
|
|
pls = np.exp(self.K * (
|
|
self.energy_state.ebest - self.energy_state.current_energy) /
|
|
self.temperature_step)
|
|
if pls >= self._rand_gen.uniform():
|
|
do_ls = True
|
|
# Global energy not improved, let's see what LS gives
|
|
# on the best strategy chain location
|
|
if self.not_improved_idx >= self.not_improved_max_idx:
|
|
do_ls = True
|
|
if do_ls:
|
|
e, x = self.minimizer_wrapper.local_search(self.xmin, self.emin)
|
|
self.xmin = np.copy(x)
|
|
self.emin = e
|
|
self.not_improved_idx = 0
|
|
self.not_improved_max_idx = self.energy_state.current_location.size
|
|
if e < self.energy_state.ebest:
|
|
val = self.energy_state.update_best(
|
|
self.emin, self.xmin, 2)
|
|
if val is not None:
|
|
if val:
|
|
return val
|
|
self.energy_state.update_current(e, x)
|
|
if self.func_wrapper.nfev >= self.func_wrapper.maxfun:
|
|
return ('Maximum number of function call reached '
|
|
'during dual annealing')
|
|
|
|
|
|
class ObjectiveFunWrapper:
|
|
|
|
def __init__(self, func, maxfun=1e7, *args):
|
|
self.func = func
|
|
self.args = args
|
|
# Number of objective function evaluations
|
|
self.nfev = 0
|
|
# Number of gradient function evaluation if used
|
|
self.ngev = 0
|
|
# Number of hessian of the objective function if used
|
|
self.nhev = 0
|
|
self.maxfun = maxfun
|
|
|
|
def fun(self, x):
|
|
self.nfev += 1
|
|
return self.func(x, *self.args)
|
|
|
|
|
|
class LocalSearchWrapper:
|
|
"""
|
|
Class used to wrap around the minimizer used for local search
|
|
Default local minimizer is SciPy minimizer L-BFGS-B
|
|
"""
|
|
|
|
LS_MAXITER_RATIO = 6
|
|
LS_MAXITER_MIN = 100
|
|
LS_MAXITER_MAX = 1000
|
|
|
|
def __init__(self, search_bounds, func_wrapper, *args, **kwargs):
|
|
self.func_wrapper = func_wrapper
|
|
self.kwargs = kwargs
|
|
self.jac = self.kwargs.get('jac', None)
|
|
self.hess = self.kwargs.get('hess', None)
|
|
self.hessp = self.kwargs.get('hessp', None)
|
|
self.kwargs.pop("args", None)
|
|
self.minimizer = minimize
|
|
bounds_list = list(zip(*search_bounds))
|
|
self.lower = np.array(bounds_list[0])
|
|
self.upper = np.array(bounds_list[1])
|
|
|
|
# If no minimizer specified, use SciPy minimize with 'L-BFGS-B' method
|
|
if not self.kwargs:
|
|
n = len(self.lower)
|
|
ls_max_iter = min(max(n * self.LS_MAXITER_RATIO,
|
|
self.LS_MAXITER_MIN),
|
|
self.LS_MAXITER_MAX)
|
|
self.kwargs['method'] = 'L-BFGS-B'
|
|
self.kwargs['options'] = {
|
|
'maxiter': ls_max_iter,
|
|
}
|
|
self.kwargs['bounds'] = list(zip(self.lower, self.upper))
|
|
else:
|
|
if callable(self.jac):
|
|
def wrapped_jac(x):
|
|
return self.jac(x, *args)
|
|
self.kwargs['jac'] = wrapped_jac
|
|
if callable(self.hess):
|
|
def wrapped_hess(x):
|
|
return self.hess(x, *args)
|
|
self.kwargs['hess'] = wrapped_hess
|
|
if callable(self.hessp):
|
|
def wrapped_hessp(x, p):
|
|
return self.hessp(x, p, *args)
|
|
self.kwargs['hessp'] = wrapped_hessp
|
|
|
|
def local_search(self, x, e):
|
|
# Run local search from the given x location where energy value is e
|
|
x_tmp = np.copy(x)
|
|
mres = self.minimizer(self.func_wrapper.fun, x, **self.kwargs)
|
|
if 'njev' in mres:
|
|
self.func_wrapper.ngev += mres.njev
|
|
if 'nhev' in mres:
|
|
self.func_wrapper.nhev += mres.nhev
|
|
# Check if is valid value
|
|
is_finite = np.all(np.isfinite(mres.x)) and np.isfinite(mres.fun)
|
|
in_bounds = np.all(mres.x >= self.lower) and np.all(
|
|
mres.x <= self.upper)
|
|
is_valid = is_finite and in_bounds
|
|
|
|
# Use the new point only if it is valid and return a better results
|
|
if is_valid and mres.fun < e:
|
|
return mres.fun, mres.x
|
|
else:
|
|
return e, x_tmp
|
|
|
|
|
|
def dual_annealing(func, bounds, args=(), maxiter=1000,
|
|
minimizer_kwargs=None, initial_temp=5230.,
|
|
restart_temp_ratio=2.e-5, visit=2.62, accept=-5.0,
|
|
maxfun=1e7, seed=None, no_local_search=False,
|
|
callback=None, x0=None):
|
|
"""
|
|
Find the global minimum of a function using Dual Annealing.
|
|
|
|
Parameters
|
|
----------
|
|
func : callable
|
|
The objective function to be minimized. Must be in the form
|
|
``f(x, *args)``, where ``x`` is the argument in the form of a 1-D array
|
|
and ``args`` is a tuple of any additional fixed parameters needed to
|
|
completely specify the function.
|
|
bounds : sequence or `Bounds`
|
|
Bounds for variables. There are two ways to specify the bounds:
|
|
|
|
1. Instance of `Bounds` class.
|
|
2. Sequence of ``(min, max)`` pairs for each element in `x`.
|
|
|
|
args : tuple, optional
|
|
Any additional fixed parameters needed to completely specify the
|
|
objective function.
|
|
maxiter : int, optional
|
|
The maximum number of global search iterations. Default value is 1000.
|
|
minimizer_kwargs : dict, optional
|
|
Keyword arguments to be passed to the local minimizer
|
|
(`minimize`). An important option could be ``method`` for the minimizer
|
|
method to use.
|
|
If no keyword arguments are provided, the local minimizer defaults to
|
|
'L-BFGS-B' and uses the already supplied bounds. If `minimizer_kwargs`
|
|
is specified, then the dict must contain all parameters required to
|
|
control the local minimization. `args` is ignored in this dict, as it is
|
|
passed automatically. `bounds` is not automatically passed on to the
|
|
local minimizer as the method may not support them.
|
|
initial_temp : float, optional
|
|
The initial temperature, use higher values to facilitates a wider
|
|
search of the energy landscape, allowing dual_annealing to escape
|
|
local minima that it is trapped in. Default value is 5230. Range is
|
|
(0.01, 5.e4].
|
|
restart_temp_ratio : float, optional
|
|
During the annealing process, temperature is decreasing, when it
|
|
reaches ``initial_temp * restart_temp_ratio``, the reannealing process
|
|
is triggered. Default value of the ratio is 2e-5. Range is (0, 1).
|
|
visit : float, optional
|
|
Parameter for visiting distribution. Default value is 2.62. Higher
|
|
values give the visiting distribution a heavier tail, this makes
|
|
the algorithm jump to a more distant region. The value range is (1, 3].
|
|
accept : float, optional
|
|
Parameter for acceptance distribution. It is used to control the
|
|
probability of acceptance. The lower the acceptance parameter, the
|
|
smaller the probability of acceptance. Default value is -5.0 with
|
|
a range (-1e4, -5].
|
|
maxfun : int, optional
|
|
Soft limit for the number of objective function calls. If the
|
|
algorithm is in the middle of a local search, this number will be
|
|
exceeded, the algorithm will stop just after the local search is
|
|
done. Default value is 1e7.
|
|
seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional
|
|
If `seed` is None (or `np.random`), the `numpy.random.RandomState`
|
|
singleton is used.
|
|
If `seed` is an int, a new ``RandomState`` instance is used,
|
|
seeded with `seed`.
|
|
If `seed` is already a ``Generator`` or ``RandomState`` instance then
|
|
that instance is used.
|
|
Specify `seed` for repeatable minimizations. The random numbers
|
|
generated with this seed only affect the visiting distribution function
|
|
and new coordinates generation.
|
|
no_local_search : bool, optional
|
|
If `no_local_search` is set to True, a traditional Generalized
|
|
Simulated Annealing will be performed with no local search
|
|
strategy applied.
|
|
callback : callable, optional
|
|
A callback function with signature ``callback(x, f, context)``,
|
|
which will be called for all minima found.
|
|
``x`` and ``f`` are the coordinates and function value of the
|
|
latest minimum found, and ``context`` has value in [0, 1, 2], with the
|
|
following meaning:
|
|
|
|
- 0: minimum detected in the annealing process.
|
|
- 1: detection occurred in the local search process.
|
|
- 2: detection done in the dual annealing process.
|
|
|
|
If the callback implementation returns True, the algorithm will stop.
|
|
x0 : ndarray, shape(n,), optional
|
|
Coordinates of a single N-D starting point.
|
|
|
|
Returns
|
|
-------
|
|
res : OptimizeResult
|
|
The optimization result represented as a `OptimizeResult` object.
|
|
Important attributes are: ``x`` the solution array, ``fun`` the value
|
|
of the function at the solution, and ``message`` which describes the
|
|
cause of the termination.
|
|
See `OptimizeResult` for a description of other attributes.
|
|
|
|
Notes
|
|
-----
|
|
This function implements the Dual Annealing optimization. This stochastic
|
|
approach derived from [3]_ combines the generalization of CSA (Classical
|
|
Simulated Annealing) and FSA (Fast Simulated Annealing) [1]_ [2]_ coupled
|
|
to a strategy for applying a local search on accepted locations [4]_.
|
|
An alternative implementation of this same algorithm is described in [5]_
|
|
and benchmarks are presented in [6]_. This approach introduces an advanced
|
|
method to refine the solution found by the generalized annealing
|
|
process. This algorithm uses a distorted Cauchy-Lorentz visiting
|
|
distribution, with its shape controlled by the parameter :math:`q_{v}`
|
|
|
|
.. math::
|
|
|
|
g_{q_{v}}(\\Delta x(t)) \\propto \\frac{ \\
|
|
\\left[T_{q_{v}}(t) \\right]^{-\\frac{D}{3-q_{v}}}}{ \\
|
|
\\left[{1+(q_{v}-1)\\frac{(\\Delta x(t))^{2}} { \\
|
|
\\left[T_{q_{v}}(t)\\right]^{\\frac{2}{3-q_{v}}}}}\\right]^{ \\
|
|
\\frac{1}{q_{v}-1}+\\frac{D-1}{2}}}
|
|
|
|
Where :math:`t` is the artificial time. This visiting distribution is used
|
|
to generate a trial jump distance :math:`\\Delta x(t)` of variable
|
|
:math:`x(t)` under artificial temperature :math:`T_{q_{v}}(t)`.
|
|
|
|
From the starting point, after calling the visiting distribution
|
|
function, the acceptance probability is computed as follows:
|
|
|
|
.. math::
|
|
|
|
p_{q_{a}} = \\min{\\{1,\\left[1-(1-q_{a}) \\beta \\Delta E \\right]^{ \\
|
|
\\frac{1}{1-q_{a}}}\\}}
|
|
|
|
Where :math:`q_{a}` is a acceptance parameter. For :math:`q_{a}<1`, zero
|
|
acceptance probability is assigned to the cases where
|
|
|
|
.. math::
|
|
|
|
[1-(1-q_{a}) \\beta \\Delta E] < 0
|
|
|
|
The artificial temperature :math:`T_{q_{v}}(t)` is decreased according to
|
|
|
|
.. math::
|
|
|
|
T_{q_{v}}(t) = T_{q_{v}}(1) \\frac{2^{q_{v}-1}-1}{\\left( \\
|
|
1 + t\\right)^{q_{v}-1}-1}
|
|
|
|
Where :math:`q_{v}` is the visiting parameter.
|
|
|
|
.. versionadded:: 1.2.0
|
|
|
|
References
|
|
----------
|
|
.. [1] Tsallis C. Possible generalization of Boltzmann-Gibbs
|
|
statistics. Journal of Statistical Physics, 52, 479-487 (1998).
|
|
.. [2] Tsallis C, Stariolo DA. Generalized Simulated Annealing.
|
|
Physica A, 233, 395-406 (1996).
|
|
.. [3] Xiang Y, Sun DY, Fan W, Gong XG. Generalized Simulated
|
|
Annealing Algorithm and Its Application to the Thomson Model.
|
|
Physics Letters A, 233, 216-220 (1997).
|
|
.. [4] Xiang Y, Gong XG. Efficiency of Generalized Simulated
|
|
Annealing. Physical Review E, 62, 4473 (2000).
|
|
.. [5] Xiang Y, Gubian S, Suomela B, Hoeng J. Generalized
|
|
Simulated Annealing for Efficient Global Optimization: the GenSA
|
|
Package for R. The R Journal, Volume 5/1 (2013).
|
|
.. [6] Mullen, K. Continuous Global Optimization in R. Journal of
|
|
Statistical Software, 60(6), 1 - 45, (2014).
|
|
:doi:`10.18637/jss.v060.i06`
|
|
|
|
Examples
|
|
--------
|
|
The following example is a 10-D problem, with many local minima.
|
|
The function involved is called Rastrigin
|
|
(https://en.wikipedia.org/wiki/Rastrigin_function)
|
|
|
|
>>> import numpy as np
|
|
>>> from scipy.optimize import dual_annealing
|
|
>>> func = lambda x: np.sum(x*x - 10*np.cos(2*np.pi*x)) + 10*np.size(x)
|
|
>>> lw = [-5.12] * 10
|
|
>>> up = [5.12] * 10
|
|
>>> ret = dual_annealing(func, bounds=list(zip(lw, up)))
|
|
>>> ret.x
|
|
array([-4.26437714e-09, -3.91699361e-09, -1.86149218e-09, -3.97165720e-09,
|
|
-6.29151648e-09, -6.53145322e-09, -3.93616815e-09, -6.55623025e-09,
|
|
-6.05775280e-09, -5.00668935e-09]) # random
|
|
>>> ret.fun
|
|
0.000000
|
|
|
|
"""
|
|
|
|
if isinstance(bounds, Bounds):
|
|
bounds = new_bounds_to_old(bounds.lb, bounds.ub, len(bounds.lb))
|
|
|
|
if x0 is not None and not len(x0) == len(bounds):
|
|
raise ValueError('Bounds size does not match x0')
|
|
|
|
lu = list(zip(*bounds))
|
|
lower = np.array(lu[0])
|
|
upper = np.array(lu[1])
|
|
# Check that restart temperature ratio is correct
|
|
if restart_temp_ratio <= 0. or restart_temp_ratio >= 1.:
|
|
raise ValueError('Restart temperature ratio has to be in range (0, 1)')
|
|
# Checking bounds are valid
|
|
if (np.any(np.isinf(lower)) or np.any(np.isinf(upper)) or np.any(
|
|
np.isnan(lower)) or np.any(np.isnan(upper))):
|
|
raise ValueError('Some bounds values are inf values or nan values')
|
|
# Checking that bounds are consistent
|
|
if not np.all(lower < upper):
|
|
raise ValueError('Bounds are not consistent min < max')
|
|
# Checking that bounds are the same length
|
|
if not len(lower) == len(upper):
|
|
raise ValueError('Bounds do not have the same dimensions')
|
|
|
|
# Wrapper for the objective function
|
|
func_wrapper = ObjectiveFunWrapper(func, maxfun, *args)
|
|
|
|
# minimizer_kwargs has to be a dict, not None
|
|
minimizer_kwargs = minimizer_kwargs or {}
|
|
|
|
minimizer_wrapper = LocalSearchWrapper(
|
|
bounds, func_wrapper, *args, **minimizer_kwargs)
|
|
|
|
# Initialization of random Generator for reproducible runs if seed provided
|
|
rand_state = check_random_state(seed)
|
|
# Initialization of the energy state
|
|
energy_state = EnergyState(lower, upper, callback)
|
|
energy_state.reset(func_wrapper, rand_state, x0)
|
|
# Minimum value of annealing temperature reached to perform
|
|
# re-annealing
|
|
temperature_restart = initial_temp * restart_temp_ratio
|
|
# VisitingDistribution instance
|
|
visit_dist = VisitingDistribution(lower, upper, visit, rand_state)
|
|
# Strategy chain instance
|
|
strategy_chain = StrategyChain(accept, visit_dist, func_wrapper,
|
|
minimizer_wrapper, rand_state, energy_state)
|
|
need_to_stop = False
|
|
iteration = 0
|
|
message = []
|
|
# OptimizeResult object to be returned
|
|
optimize_res = OptimizeResult()
|
|
optimize_res.success = True
|
|
optimize_res.status = 0
|
|
|
|
t1 = np.exp((visit - 1) * np.log(2.0)) - 1.0
|
|
# Run the search loop
|
|
while not need_to_stop:
|
|
for i in range(maxiter):
|
|
# Compute temperature for this step
|
|
s = float(i) + 2.0
|
|
t2 = np.exp((visit - 1) * np.log(s)) - 1.0
|
|
temperature = initial_temp * t1 / t2
|
|
if iteration >= maxiter:
|
|
message.append("Maximum number of iteration reached")
|
|
need_to_stop = True
|
|
break
|
|
# Need a re-annealing process?
|
|
if temperature < temperature_restart:
|
|
energy_state.reset(func_wrapper, rand_state)
|
|
break
|
|
# starting strategy chain
|
|
val = strategy_chain.run(i, temperature)
|
|
if val is not None:
|
|
message.append(val)
|
|
need_to_stop = True
|
|
optimize_res.success = False
|
|
break
|
|
# Possible local search at the end of the strategy chain
|
|
if not no_local_search:
|
|
val = strategy_chain.local_search()
|
|
if val is not None:
|
|
message.append(val)
|
|
need_to_stop = True
|
|
optimize_res.success = False
|
|
break
|
|
iteration += 1
|
|
|
|
# Setting the OptimizeResult values
|
|
optimize_res.x = energy_state.xbest
|
|
optimize_res.fun = energy_state.ebest
|
|
optimize_res.nit = iteration
|
|
optimize_res.nfev = func_wrapper.nfev
|
|
optimize_res.njev = func_wrapper.ngev
|
|
optimize_res.nhev = func_wrapper.nhev
|
|
optimize_res.message = message
|
|
return optimize_res
|