Working versions in C++ and python.

This commit is contained in:
Jerome 2023-04-02 16:27:42 +02:00
parent a43498d439
commit f4f908b2fb
6 changed files with 361 additions and 2 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
*.o
*.exe
differential_evolution
Makefile
Doxyfile

View file

@ -1,2 +1,79 @@
# differential_evolution
Differential evolution algorithm in C++ and python
# Differential Evolution for global numerical optimization
Simple implementations of the differential evolution algorithm in C++ and python from the algorithm described in https://en.wikipedia.org/wiki/Differential_evolution .
## Dependencies
It requires the **Eigen** library for the C++ version, and the **numpy** package for python.
## Examples
### C++
The **Eigen** library is used to deal with vectors. The variable type `double` has been chosen to implement the algorithm, and the `Eigen::VectorXd` type is used for vectors.
If another type of variable needs to be used, such as a type in `boost::multiprecision`, the code can easily be adapted to use a template parameter instead of `double`.
The prototype of the function `differential_evolution_minimize` is the following :
OptimizationResult differential_evolution_minimize(
std::function<double(Eigen::VectorXd const&)> f, // function to optimize
Eigen::VectorXd const& lb, // lower bounds of initial domain
Eigen::VectorXd const& ub, // upper bounds of initial domain
double tol = 1e-6, // tolerance on standard deviation of function values
unsigned int n_iter_max = 1000, // maximum number of iterations
unsigned int n_individuals = 0, // number of individuals to use
double crossover_proba = 0.9, // crossover probability : in [0;1]
double differential_weight = 0.8 // differential weight : in [0;2]
);
The file `main_diff_evolution.cpp` shows an example of usage of the function `differential_evolution_minimize` :
#include <iostream>
#define M_PI 3.14159265358979323846
#include <cmath>
#include <Eigen/Dense>
#include "differential_evolution.hpp"
using namespace std;
typedef double real_t;
real_t ackley(Eigen::VectorXd const& x) {
real_t sum1 = 0.0;
real_t sum2 = 0.0;
for(unsigned int i = 0 ; i < x.size() ; ++i) {
sum1 += x(i) * x(i);
sum2 += std::cos(2.0 * M_PI * x(i));
}
return -20.0 * std::exp(-0.2 * std::sqrt(sum1 / x.size())) - std::exp(sum2 / x.size()) + 20.0 + std::exp(1.0);
}
int main() {
unsigned int n_dims = 5;
Eigen::VectorXd lb = Eigen::VectorXd::Constant(n_dims, -5.0);
Eigen::VectorXd ub = Eigen::VectorXd::Constant(n_dims, 5.0);
differential_evolution::OptimizationResult res = differential_evolution::differential_evolution_minimize(ackley, lb, ub);
cout << "x = " << res.x.transpose() << endl;
cout << "f(x) = " << res.fx << endl;
cout << "N iter = " << res.n_iter << endl;
cout << "converged = " << res.converged << endl;
return 0;
}
# Python
The script contains an example of usage of the function `differential_evolution_minimize` :
def ackley(X):
x = X[0]
y = X[1]
return -20*np.exp(-0.2*np.sqrt(0.5*(x**2 + y**2))) - np.exp(0.5*(np.cos(2*np.pi*x) + np.cos(2*np.pi*y))) + np.exp(1.0) + 20.0
res = differential_evolution_minimize(ackley, lb=[-5., -5.], ub=[5., 5.])
print(f'x_min = {res["x"]}')
print(f'f(x_min) = {res["fx"]}')
print(f'N iter = {res["n_iter"]}')
print(f'Converged = {res["converged"]}')

105
differential_evolution.cpp Normal file
View file

@ -0,0 +1,105 @@
#include "differential_evolution.hpp"
#include <iostream>
#include <vector>
namespace differential_evolution {
double std_dev(Eigen::VectorXd const& vec) {
double mean = vec.mean();
Eigen::VectorXd deviations = (vec.array() - mean).square();
return sqrt(deviations.sum() / vec.size());
}
unsigned int get_random_number_nmax(unsigned int nmax) {
return (unsigned int)((Eigen::VectorXd::Random(1)[0] + 1.0)*0.5 * nmax) % nmax;
}
OptimizationResult differential_evolution_minimize(std::function<double(Eigen::VectorXd const&)> f, Eigen::VectorXd const& lb, Eigen::VectorXd const& ub, double tol, unsigned int n_iter_max, unsigned int n_individuals, double crossover_proba, double differential_weight) {
// Result structure
OptimizationResult res;
res.n_iter = 0;
res.converged = false;
// Pre checks
if(lb.size() != ub.size()) {
std::cout << "Error : lb and ub must have the same sizes ! lb : " << lb.size() << ", ub : " << ub.size() << std::endl;
res.x = (lb + ub)/2.0;
return res;
}
unsigned int n_dims = lb.size();// Number of dimensions
if(n_individuals < 4) // Default value for n_individuals
n_individuals = 10*n_dims;
// Initialize the population
std::vector<Eigen::VectorXd> population(n_individuals);
Eigen::VectorXd population_values(n_individuals);
for(unsigned int i = 0 ; i < n_individuals ; ++i) {
Eigen::VectorXd x = Eigen::VectorXd::Random(n_dims).array()*(ub - lb).array() + lb.array();
population[i] = x;
population_values[i] = f(x);
}
// Main loop
for(res.n_iter = 0 ; res.n_iter < n_iter_max ; ++res.n_iter) {
// for each individual
for(unsigned int ix = 0 ; ix < n_individuals ; ++ix) {
Eigen::VectorXd x = population[ix];
// pick 3 other individuals a, b, and c
unsigned int ia = ix;
unsigned int ib = ix;
unsigned int ic = ix;
while(ia == ix)
ia = get_random_number_nmax(n_individuals);
while(ib == ix or ib == ia)
ib = get_random_number_nmax(n_individuals);
while(ic == ix or ic == ia or ic == ib)
ic = get_random_number_nmax(n_individuals);
Eigen::VectorXd a = population[ia];
Eigen::VectorXd b = population[ib];
Eigen::VectorXd c = population[ic];
// random coordinate of the individual
unsigned int jr = (unsigned int)(Eigen::VectorXd::Random(1)[0] * n_dims);
// Compute candidate individual
Eigen::VectorXd y = x;
for(unsigned int j = 0 ; j < n_dims ; ++j) {
double ri = (Eigen::VectorXd::Random(1)[0] + 1.0)*0.5;
if(ri < crossover_proba or j == jr)// always replace position jr, otherwise randomly decide whether to update it or not
y[j] = a[j] + differential_weight*(b[j] - c[j]);
}
// if candidate individual is better than current, replace current with candidate
double fy = f(y);
if(fy <= population_values[ix]) {
population[ix] = y;
population_values[ix] = fy;
}
}
// convergence criterion : if standard value of all function values is sufficiently small
if(std_dev(population_values) <= tol) {
res.converged = true;
break;
}
}
// return the best solution
res.x = population[0];
res.fx = population_values[0];
for(unsigned int i = 0 ; i < n_individuals ; ++i) {
if(population_values[i] < res.fx) {
res.x = population[i];
res.fx = population_values[i];
}
}
return res;
}
} // namespace differential_evolution

View file

@ -0,0 +1,38 @@
#ifndef H_differential_evolution
#define H_differential_evolution
#include <functional>
#include <Eigen/Dense>
namespace differential_evolution {
/// @brief Result of an optimization algorithm.
struct OptimizationResult {
Eigen::VectorXd x; //!< Minimum of the function.
double fx; //!< Function value at the minimum.
unsigned int n_iter = 0; //!< Number of iterations.
bool converged = false; //!< Whether the algorithm converged.
};
/// @brief Global minimization of a function using the differential evolution algorithm.
/// @param f The function to minimize.
/// @param lb Lower bounds of the parameters.
/// @param ub Upper bounds of the parameters.
/// @param tol Tolerance on the standard deviation of the function value among all individuals of the population.
/// @param n_iter_max Maximum number of iterations.
/// @param n_individuals Number of individuals in the population. By default, 10 * lb.size(). Must be greater than 4.
/// @param crossover_proba Crossover probability. Between 0 and 1.
/// @param differential_weight Differential weight. Between 0 and 2.
/// @return The result structure containing the minimum of the function and the function value at the solution. If res.converged is false, either the algorithm did not converge, or an error occured at the beginning of the function.
OptimizationResult differential_evolution_minimize( std::function<double(Eigen::VectorXd const&)> f,
Eigen::VectorXd const& lb,
Eigen::VectorXd const& ub,
double tol = 1e-6,
unsigned int n_iter_max = 1000,
unsigned int n_individuals = 0,
double crossover_proba = 0.9,
double differential_weight = 0.8);
}// namespace differential_evolution
#endif

100
differential_evolution.py Normal file
View file

@ -0,0 +1,100 @@
from random import randint
import numpy as np
def differential_evolution_minimize(f, lb, ub, tol=1e-6, n_iter_max=1000, n_individuals=0, crossover_proba=0.9, differential_weight=0.8):
''' Minimizes the function f using the differential evolution global optimization algorithm.
f function to minimize
lb Nx1 vector of lower bounds of the N-dimensional domain
ub Nx1 vector of upper bounds of the N-dimensional domain
tol tolerance on average function value difference between successive iterations
n_iter_max maximum number of iterations to perform
n_individuals number of individuals to use in the population. By default, =10*n_dims
crossover_proba probability of performing a cross-over for a given coordinate of an individual. Must be in [0 ; 1]
differential_weight coefficient in front of the differential term (b_i - c_i). Usually in [0 ; 2]
'''
res = {'x': None, 'fx': None, 'n_iter': n_iter_max, 'converged': False}
# Pre checks
if len(lb) != len(ub):
print(f'Error : lb and ub must have the same sizes ! lb : {len(lb)}, ub : {len(ub)}')
return res
lb = np.asarray(lb)
ub = np.asarray(ub)
n_dims = len(lb)
if n_individuals < 4:
n_individuals = 10*n_dims
# initialize the population
population = []
population_values = []
for i in range(n_individuals):
x = np.random.rand(n_dims)*(ub - lb) + lb
population.append(x)
population_values.append(f(x))
for iter in range(n_iter_max):
# for each individual
for ix, x in enumerate(population):
# pick 3 other individuals a, b, and c
ia = ix
ib = ix
ic = ix
while ia == ix:
ia = int(np.random.rand() * n_individuals)
while ib == ix or ib == ia:
ib = int(np.random.rand() * n_individuals)
while ic == ix or ic == ia or ic == ib:
ic = int(np.random.rand() * n_individuals)
a = population[ia]
b = population[ib]
c = population[ic]
# random coordinate of the individual
jr = int(np.random.rand() * n_dims)
# Compute candidate individual
y = x.copy()
for j in range(n_dims):
ri = np.random.rand()
if ri < crossover_proba or j == jr:# always replace position jr, otherwise randomly decide whether to update it or not
y[j] = a[j] + differential_weight*(b[j] - c[j])
# if candidate individual is better than current, replace current with candidate
fy = f(y)
if fy <= population_values[ix]:
population[ix] = y.copy()
population_values[ix] = fy
# convergence criterion : if standard value of all function values is sufficiently small
if np.std(np.asarray(population_values)) <= tol:
res['n_iter'] = iter
res['converged'] = True
break
# return the best
res['x'] = population[0]
res['fx'] = population_values[0]
for i in range(n_individuals):
if population_values[i] < res['fx']:
res['x'] = population[i]
res['fx'] = population_values[i]
return res
if __name__ == '__main__':
def ackley(X):
x = X[0]
y = X[1]
return -20*np.exp(-0.2*np.sqrt(0.5*(x**2 + y**2))) - np.exp(0.5*(np.cos(2*np.pi*x) + np.cos(2*np.pi*y))) + np.exp(1.0) + 20.0
res = differential_evolution_minimize(ackley, lb=[-5., -5.], ub=[5., 5.])
print(f'x_min = {res["x"]}')
print(f'f(x_min) = {res["fx"]}')
print(f'N iter = {res["n_iter"]}')
print(f'Converged = {res["converged"]}')

34
main_diff_evolution.cpp Normal file
View file

@ -0,0 +1,34 @@
#include <iostream>
#define M_PI 3.14159265358979323846
#include <cmath>
#include <Eigen/Dense>
#include "differential_evolution.hpp"
using namespace std;
typedef double real_t;
real_t ackley(Eigen::VectorXd const& x) {
real_t sum1 = 0.0;
real_t sum2 = 0.0;
for(unsigned int i = 0 ; i < x.size() ; ++i) {
sum1 += x(i) * x(i);
sum2 += std::cos(2.0 * M_PI * x(i));
}
return -20.0 * std::exp(-0.2 * std::sqrt(sum1 / x.size())) - std::exp(sum2 / x.size()) + 20.0 + std::exp(1.0);
}
int main() {
unsigned int n_dims = 5;
Eigen::VectorXd lb = Eigen::VectorXd::Constant(n_dims, -5.0);
Eigen::VectorXd ub = Eigen::VectorXd::Constant(n_dims, 5.0);
differential_evolution::OptimizationResult res = differential_evolution::differential_evolution_minimize(ackley, lb, ub);
cout << "x = " << res.x.transpose() << endl;
cout << "f(x) = " << res.fx << endl;
cout << "N iter = " << res.n_iter << endl;
cout << "converged = " << res.converged << endl;
return 0;
}