I've implemented the Nelder-Mead optimization algorithm in C++. I found this document to be a very good explanation of how the algorithm works, but I'll try my best to explain it (with an example) here so it's clear for everyone.
The problem I'm trying to optimize is very simple : I'm trying to find the best a,b,c
coefficients for a quadratic function to fit points on a quadratic function of form f(x) = 1.5*x^2 + 3.2*x + 7
. So, of course, the optimal parameters I'm looking for are a=1.5, b=3.2, c=7
.
In order to compute the error between the coefficients that I have and the expected coefficients, I use the following function :
#include "Eigen/Dense"
static double quadraticEquationError(Array<double, 1, 3> parameters)
{
double searchedParamters[] = {1.5, 3.2, 7};
auto x = Array<double, 100, 1>().setLinSpaced(100, 0, 90);
auto expected = searchedParamters[0] * x*x + searchedParamters[1] * x + searchedParamters[2];
auto result = parameters(0, 0) * x * x + parameters(0, 1) * x + parameters(0, 2);
auto diff = result.matrix() - expected.matrix();
return diff.norm();
}
It's very simple, but it's also a way for me to check if my algorithm works. The goal of this error function is to return 0, where I would have found my optimal parameters.
The Nelder-Mead algorithm takes as input an error function and some initial parameters (in my case, a=0, b=0, c=0
). Then, it generates a simplex of N+1 dimensions (here, N = 3 since we are looking for 3 parameters) where each vertex of the simplex represents a new parameters set. After that, depending on the values of the worst, second worst and best sets, it replaces the worst point by a new value.
There are five operations that can be used : reflection, expansion, inside contraction, outside contraction and shrinking. The following figure shows the result of these operations for a 2D simplex (of 3 points).
In order to figure out which operation to use, we can look at the following diagram :
Progressively, the error will go down until a convergence criterion is reached. In my case, I decided to stop when the best error is under a certain value.
So, I hope this is pretty clear as to how the algorithm works, here's my code :
main.cpp
#include "library.h"
#include <iostream>
#include "Eigen/Dense"
#include "optimization/NelderMead.h"
using namespace Optimization;
static double quadraticEquationError(Array<double, 1, 3> parameters)
{
double searchedParamters[] = {1.5, 3.2, 7};
auto x = Array<double, 100, 1>().setLinSpaced(100, 0, 90);
auto expected = searchedParamters[0] * x*x + searchedParamters[1] * x + searchedParamters[2];
auto result = parameters(0, 0) * x * x + parameters(0, 1) * x + parameters(0, 2);
auto diff = result.matrix() - expected.matrix();
return diff.norm();
}
int main() {
Array<double, 1, 3> initialParameters;
initialParameters.row(0) << 0.0, 0.0, 0.0;
auto nelderMead = NelderMead<3>(quadraticEquationError, initialParameters, 1, 2);
nelderMead.optimize();
return 0;
}
optimization/NelderMead.h
#include "Eigen/Dense"
#include <vector>
#include <limits>
#include <cstring>
#include <iostream>
using namespace Eigen;
namespace Optimization
{
template<int nDims>
class NelderMead {
public:
typedef Array<double, 1, nDims> FunctionParameters;
typedef std::function<double(FunctionParameters)> ErrorFunction;
NelderMead(ErrorFunction errorFunction, FunctionParameters initial,
double minError, double initialEdgeLength,
double shrinkCoeff = 1, double contractionCoeff = 0.5,
double reflectionCoeff = 1, double expansionCoeff = 1)
: errorFunction(errorFunction), minError(minError),
shrinkCoeff(shrinkCoeff), contractionCoeff(contractionCoeff),
reflectionCoeff(reflectionCoeff), expansionCoeff(expansionCoeff),
worstValueId(-1), secondWorstValueId(-1), bestValueId(-1)
{
this->errors = std::vector(nDims + 1, std::numeric_limits<double>::max());
const double b = initialEdgeLength / (nDims * SQRT2) * (sqrt(nDims + 1) - 1);
const double a = initialEdgeLength / SQRT2;
this->values = initial.replicate(nDims + 1, 1);
for (int i = 0; i < nDims; i++)
{
FunctionParameters simplexRow;
simplexRow.setConstant(b);
simplexRow(0, i) = a;
simplexRow += initial;
this->values.row(i+1) = simplexRow;
}
}
void optimize()
{
for (int i = 0; i < nDims+1; i++)
{
this->errors.at(i) = this->errorFunction(this->values.row(i));
}
this->invalidateIdsCache();
while (this->errors.at(this->bestValueId) > this->minError)
{
step();
auto bestError = this->errorFunction(this->best());
auto worstError = this->errorFunction(this->worst());
std::cout << "Best error " << std::to_string(bestError) << " with : " << this->best();
std::cout << " Worst error " << std::to_string(worstError) << " with : " << this->worst();
std::cout << '\n';
}
}
void step()
{
auto meanWithoutWorst = this->getMeanWithoutWorst();
auto reflectionOfWorst = this->getReflectionOfWorst(meanWithoutWorst);
auto reflectionError = this->errorFunction(reflectionOfWorst);
FunctionParameters newValue = reflectionOfWorst;
double newError = reflectionError;
bool shrink = false;
if (reflectionError < this->errors.at(this->bestValueId))
{
auto expansionValue = this->expansion(meanWithoutWorst, reflectionOfWorst);
double expansionError = this->errorFunction(expansionValue);
if (expansionError < this->errors.at(this->bestValueId))
{
newValue = expansionValue;
newError = expansionError;
}
}
else if (reflectionError > this->errors.at(this->worstValueId))
{
newValue = this->insideContraction(meanWithoutWorst);
newError = this->errorFunction(newValue);
if (newError > this->errors.at(this->worstValueId)) { shrink = true; }
}
else if (reflectionError > this->errors.at(this->secondWorstValueId))
{
newValue = this->outsideContraction(meanWithoutWorst);
newError = this->errorFunction(newValue);
if (newError > reflectionError) { shrink = true; }
}
else
{
newValue = reflectionOfWorst;
newError = reflectionError;
}
if (shrink)
{
this->shrink();
this->invalidateIdsCache();
return;
}
this->values.row(this->worstValueId) = newValue;
this->errors.at(this->worstValueId) = newError;
this->invalidateIdsCache();
}
inline FunctionParameters worst() { return this->values.row(this->worstValueId); }
inline FunctionParameters best() { return this->values.row(this->bestValueId); }
private:
void shrink()
{
auto bestVertex = this->values.row(this->bestValueId);
for (int i = 0; i < nDims + 1; i++)
{
if (i == this->bestValueId) { continue; }
this->values.row(i) = bestVertex + this->shrinkCoeff * (this->values.row(i) - bestVertex);
this->errors.at(i) = this->errorFunction(this->values.row(i));
}
}
inline FunctionParameters expansion(FunctionParameters meanWithoutWorst, FunctionParameters reflection)
{
return reflection + this->expansionCoeff * (reflection - meanWithoutWorst);
}
inline FunctionParameters insideContraction(FunctionParameters meanWithoutWorst)
{
return meanWithoutWorst - this->contractionCoeff * (meanWithoutWorst - this->worst());
}
inline FunctionParameters outsideContraction(FunctionParameters meanWithoutWorst)
{
return meanWithoutWorst + this->contractionCoeff * (meanWithoutWorst - this->worst());
}
FunctionParameters getReflectionOfWorst(FunctionParameters meanWithoutWorst)
{
return meanWithoutWorst + this->reflectionCoeff * (meanWithoutWorst - this->worst());
}
FunctionParameters getMeanWithoutWorst()
{
FunctionParameters mean(0);
for (int i = 0; i < nDims + 1; i++)
{
if (i == this->worstValueId) { continue; }
mean += this->values.row(i);
}
// Not divided by nDims+1 because there's one ignored value.
mean /= nDims;
return mean;
}
void invalidateIdsCache() {
double worstError = std::numeric_limits<double>::min();
int worstId = -1;
double secondWorstError = std::numeric_limits<double>::max();
int secondWorstId = -1;
double bestError = std::numeric_limits<double>::max();
int bestId = -1;
for (int i = 0; i < nDims + 1; i++)
{
auto error = this->errors.at(i);
if (error > worstError)
{
secondWorstError = worstError;
secondWorstId = worstId;
worstError = error;
worstId = i;
}
else if (error > secondWorstError)
{
secondWorstError = error;
secondWorstId = i;
}
if (error < bestError)
{
bestError = error;
bestId = i;
}
}
// If we deal with a problem in 1D, it won't be set.
if (secondWorstId == -1)
{
secondWorstId = worstId;
}
this->bestValueId = bestId;
this->worstValueId = worstId;
this->secondWorstValueId = secondWorstId;
}
ErrorFunction errorFunction;
Array<double, nDims + 1, nDims> values;
std::vector<double> errors;
int worstValueId;
int secondWorstValueId;
int bestValueId;
double minError;
double shrinkCoeff;
double expansionCoeff;
double contractionCoeff;
double reflectionCoeff;
const double SQRT2 = sqrt(2);
};
}
Now, the thing is I don't know much about C++, but I know a lot about C# so I think my code structure is fine. I tried to use templates and typedefs to make code more readable/generic, but I'd like my C++ skills to improve a lot since I'm starting a new job soon that will require me to write C++ (my employer knows I'm not super strong in C++ but I'd like to get up to speed).
I'm looking for parts of my code that aren't using the full potential of C++, although I'm open to any part of the code reviewed.