Skip to content

Commit

Permalink
Merge pull request #263 from jonpsy/nsga2-better
Browse files Browse the repository at this point in the history
Improvise NSGA2.
  • Loading branch information
zoq authored Apr 14, 2021
2 parents 1b6c1d9 + 33974ce commit 1e70823
Show file tree
Hide file tree
Showing 4 changed files with 365 additions and 77 deletions.
3 changes: 3 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
* CheckArbitraryFunctionTypeAPI extended for MOO support
([#283](https://github.com/mlpack/ensmallen/pull/283)).

* Refactor NSGA2
([#263](https://github.com/mlpack/ensmallen/pull/263)).

### ensmallen 2.16.1: "Severely Dented Can Of Polyurethane"
###### 2021-03-02
* Fix test compilation issue when `ENS_USE_OPENMP` is set
Expand Down
23 changes: 15 additions & 8 deletions include/ensmallen_bits/nsga2/nsga2.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,15 +188,16 @@ class NSGA2 {
typename std::enable_if<I == sizeof...(ArbitraryFunctionType), void>::type
EvaluateObjectives(std::vector<MatType>&,
std::tuple<ArbitraryFunctionType...>&,
std::vector<arma::Col<double> >&);
std::vector<arma::Col<typename MatType::elem_type> >&);

template<std::size_t I = 0,
typename MatType,
typename ...ArbitraryFunctionType>
typename std::enable_if<I < sizeof...(ArbitraryFunctionType), void>::type
EvaluateObjectives(std::vector<MatType>& population,
std::tuple<ArbitraryFunctionType...>& objectives,
std::vector<arma::Col<double> >& calculatedObjectives);
std::vector<arma::Col<typename MatType::elem_type> >&
calculatedObjectives);

/**
* Reproduce candidates from the elite population to generate a new
Expand Down Expand Up @@ -281,11 +282,15 @@ class NSGA2 {
* Assigns crowding distance metric for sorting.
*
* @param front The previously generated Pareto fronts.
* @param objectives The set of objectives.
* @param crowdingDistance The previously calculated objectives.
* @param calculatedObjectives The previously calculated objectives.
* @param crowdingDistance The crowding distance for each individual in
* the population.
*/
void CrowdingDistanceAssignment(const std::vector<size_t>& front,
std::vector<double>& crowdingDistance);
template <typename MatType>
void CrowdingDistanceAssignment(
const std::vector<size_t>& front,
std::vector<arma::Col<typename MatType::elem_type>>& calculatedObjectives,
std::vector<typename MatType::elem_type>& crowdingDistance);

/**
* The operator used in the crowding distance based sorting.
Expand All @@ -299,13 +304,15 @@ class NSGA2 {
* @param idxQ The index of the second cadidate from the elite population
* being sorted.
* @param ranks The previously calculated ranks.
* @param crowdingDistance The previously calculated objectives.
* @param crowdingDistance The crowding distance for each individual in
* the population.
* @return true if the first candidate is preferred, otherwise, false.
*/
template<typename MatType>
bool CrowdingOperator(size_t idxP,
size_t idxQ,
const std::vector<size_t>& ranks,
const std::vector<double>& crowdingDistance);
const std::vector<typename MatType::elem_type>& crowdingDistance);

//! The number of objectives being optimised for.
size_t numObjectives;
Expand Down
155 changes: 104 additions & 51 deletions include/ensmallen_bits/nsga2/nsga2_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ inline NSGA2::NSGA2(const size_t populationSize,
const double epsilon,
const arma::vec& lowerBound,
const arma::vec& upperBound) :
numObjectives(0),
numVariables(0),
populationSize(populationSize),
maxGenerations(maxGenerations),
crossoverProb(crossoverProb),
Expand All @@ -45,6 +47,8 @@ inline NSGA2::NSGA2(const size_t populationSize,
const double epsilon,
const double lowerBound,
const double upperBound) :
numObjectives(0),
numVariables(0),
populationSize(populationSize),
maxGenerations(maxGenerations),
crossoverProb(crossoverProb),
Expand All @@ -61,7 +65,7 @@ template<typename MatType,
typename... CallbackTypes>
typename MatType::elem_type NSGA2::Optimize(
std::tuple<ArbitraryFunctionType...>& objectives,
MatType& iterate,
MatType& iterateIn,
CallbackTypes&&... callbacks)
{
// Make sure for evolution to work at least four candidates are present.
Expand All @@ -71,6 +75,17 @@ typename MatType::elem_type NSGA2::Optimize(
" least 4, and, a multiple of 4!");
}

// Convenience typedefs.
typedef typename MatType::elem_type ElemType;
typedef typename MatTypeTraits<MatType>::BaseMatType BaseMatType;

BaseMatType& iterate = (BaseMatType&) iterateIn;

// Make sure that we have the methods that we need. Long name...
traits::CheckArbitraryFunctionTypeAPI<ArbitraryFunctionType...,
BaseMatType>();
RequireDenseFloatingPointType<BaseMatType>();

// Check if lower bound is a vector of a single dimension.
if (lowerBound.n_rows == 1)
lowerBound = lowerBound(0, 0) * arma::ones(iterate.n_rows, iterate.n_cols);
Expand All @@ -85,9 +100,6 @@ typename MatType::elem_type NSGA2::Optimize(
assert(upperBound.n_rows == iterate.n_rows && "The dimensions of "
"upperBound are not the same as the dimensions of iterate.");

// Convenience typedefs.
typedef typename MatType::elem_type ElemType;

numObjectives = sizeof...(ArbitraryFunctionType);
numVariables = iterate.n_rows;

Expand All @@ -98,13 +110,14 @@ typename MatType::elem_type NSGA2::Optimize(

// Population size reserved to 2 * populationSize + 1 to accommodate
// for the size of intermediate candidate population.
std::vector<MatType> population;
std::vector<BaseMatType> population;
population.reserve(2 * populationSize + 1);

// Pareto fronts, initialized during non-dominated sorting.
// Stores indices of population belonging to a certain front.
std::vector<std::vector<size_t> > fronts;
// Initialised in CrowdingDistanceAssignment.
std::vector<double> crowdingDistance;
std::vector<ElemType> crowdingDistance;
// Initialised during non-dominated sorting.
std::vector<size_t> ranks;

Expand All @@ -115,8 +128,17 @@ typename MatType::elem_type NSGA2::Optimize(
// starting point.
for (size_t i = 0; i < populationSize; i++)
{
population.push_back(arma::randu<MatType>(iterate.n_rows,
population.push_back(arma::randu<BaseMatType>(iterate.n_rows,
iterate.n_cols) - 0.5 + iterate);

// Constrain all genes to be between bounds.
for (size_t geneIdx = 0; geneIdx < numVariables; geneIdx++)
{
if (population[i](geneIdx) < lowerBound(geneIdx))
population[i](geneIdx) = lowerBound(geneIdx);
else if (population[i](geneIdx) > upperBound(geneIdx))
population[i](geneIdx) = upperBound(geneIdx);
}
}

Info << "NSGA2 initialized successfully. Optimization started." << std::endl;
Expand Down Expand Up @@ -146,56 +168,63 @@ typename MatType::elem_type NSGA2::Optimize(

// Perform fast non dominated sort on P_t ∪ G_t.
ranks.resize(population.size());
FastNonDominatedSort<MatType>(fronts, ranks, calculatedObjectives);
FastNonDominatedSort<BaseMatType>(fronts, ranks, calculatedObjectives);

// Perform crowding distance assignment.
crowdingDistance.resize(population.size());

std::fill(crowdingDistance.begin(), crowdingDistance.end(), 0.);
for (size_t fNum = 0; fNum < fronts.size(); fNum++)
{
CrowdingDistanceAssignment(fronts[fNum], crowdingDistance);
CrowdingDistanceAssignment<BaseMatType>(
fronts[fNum], calculatedObjectives, crowdingDistance);
}

// Sort based on crowding distance.
std::sort(population.begin(), population.end(),
[this, ranks, crowdingDistance, population](MatType candidateP,
MatType candidateQ)
{
size_t idxP, idxQ;
for (size_t i = 0; i < population.size(); i++)
{
if (arma::approx_equal(population[i], candidateP, "absdiff", epsilon))
idxP = i;

if (arma::approx_equal(population[i], candidateQ, "absdiff", epsilon))
idxQ = i;
}

return CrowdingOperator(idxP, idxQ, ranks, crowdingDistance);
}
[this, ranks, crowdingDistance, population]
(BaseMatType candidateP, BaseMatType candidateQ)
{
size_t idxP{}, idxQ{};
for (size_t i = 0; i < population.size(); i++)
{
if (arma::approx_equal(population[i], candidateP, "absdiff", epsilon))
idxP = i;

if (arma::approx_equal(population[i], candidateQ, "absdiff", epsilon))
idxQ = i;
}

return CrowdingOperator<BaseMatType>(idxP, idxQ, ranks, crowdingDistance);
}
);

// Yield a new population P_{t+1} of size populationSize.
// Discards unfit population from the R_{t} to yield P_{t+1}.
population.resize(populationSize);
}

// Set the candidates from the best front as the output.
std::vector<MatType> front;
std::vector<BaseMatType> front;

for (size_t f: fronts[0])
front.push_back(population[f]);

bestFront.resize(front.size());
// bestFront is stored, can be obtained by the Front() getter.
bestFront = front;
std::transform(front.begin(), front.end(), bestFront.begin(),
[&](const BaseMatType& individual)
{
return arma::conv_to<arma::mat>::from(individual);
});

// Assign iterate to first element of the best front.
iterate = bestFront[0];
iterate = front[0];

Callback::EndOptimization(*this, objectives, iterate, callbacks...);

ElemType performance = std::numeric_limits<ElemType>::max();

for(arma::Col<ElemType> objective: calculatedObjectives)
for (const arma::Col<ElemType>& objective: calculatedObjectives)
if (arma::accu(objective) < performance)
performance = arma::accu(objective);

Expand All @@ -210,7 +239,7 @@ typename std::enable_if<I == sizeof...(ArbitraryFunctionType), void>::type
NSGA2::EvaluateObjectives(
std::vector<MatType>&,
std::tuple<ArbitraryFunctionType...>&,
std::vector<arma::Col<double> >&)
std::vector<arma::Col<typename MatType::elem_type> >&)
{
// Nothing to do here.
}
Expand All @@ -223,7 +252,7 @@ typename std::enable_if<I < sizeof...(ArbitraryFunctionType), void>::type
NSGA2::EvaluateObjectives(
std::vector<MatType>& population,
std::tuple<ArbitraryFunctionType...>& objectives,
std::vector<arma::Col<double> >& calculatedObjectives)
std::vector<arma::Col<typename MatType::elem_type> >& calculatedObjectives)
{
for (size_t i = 0; i < populationSize; i++)
{
Expand Down Expand Up @@ -344,7 +373,7 @@ inline void NSGA2::FastNonDominatedSort(

size_t i = 0;

while (fronts[i].size() > 0)
while (!fronts[i].empty())
{
std::vector<size_t> nextFront;

Expand All @@ -365,6 +394,8 @@ inline void NSGA2::FastNonDominatedSort(
i++;
fronts.push_back(nextFront);
}
// Remove the empty final set.
fronts.pop_back();
}

//! Check if a candidate Pareto dominates another candidate.
Expand Down Expand Up @@ -393,37 +424,59 @@ inline bool NSGA2::Dominates(
}

//! Assign crowding distance to the population.
inline void NSGA2::CrowdingDistanceAssignment(const std::vector<size_t>& front,
std::vector<double>& crowdingDistance)
template <typename MatType>
inline void NSGA2::CrowdingDistanceAssignment(
const std::vector<size_t>& front,
std::vector<arma::Col<typename MatType::elem_type>>& calculatedObjectives,
std::vector<typename MatType::elem_type>& crowdingDistance)
{
if (front.size() > 0)
{
for (size_t elem: front)
crowdingDistance[elem] = 0;
// Convenience typedefs.
typedef typename MatType::elem_type ElemType;

size_t fSize = front.size();
size_t fSize = front.size();
// Stores the sorted indices of the fronts.
arma::uvec sortedIdx = arma::regspace<arma::uvec>(0, 1, fSize - 1);

for (size_t m = 0; m < numObjectives; m++)
{
crowdingDistance[front[0]] = std::numeric_limits<double>::max();
crowdingDistance[front[fSize - 1]] = std::numeric_limits<double>::max();
for (size_t m = 0; m < numObjectives; m++)
{
// Cache fValues of individuals for current objective.
arma::Col<ElemType> fValues(fSize);
std::transform(front.begin(), front.end(), fValues.begin(),
[&](const size_t& individual)
{
return calculatedObjectives[individual](m);
});

for (size_t i = 1; i < fSize - 1 ; i++)
{
crowdingDistance[front[i]] += (crowdingDistance[front[i - 1]] -
crowdingDistance[front[i + 1]]) /
(std::numeric_limits<double>::max() -
std::numeric_limits<double>::min());
}
// Sort front indices by ascending fValues for current objective.
std::sort(sortedIdx.begin(), sortedIdx.end(),
[&](const size_t& frontIdxA, const size_t& frontIdxB)
{
return (fValues(frontIdxA) < fValues(frontIdxB));
});

crowdingDistance[front[sortedIdx(0)]] =
std::numeric_limits<ElemType>::max();
crowdingDistance[front[sortedIdx(fSize - 1)]] =
std::numeric_limits<ElemType>::max();
ElemType minFval = fValues(sortedIdx(0));
ElemType maxFval = fValues(sortedIdx(fSize - 1));
ElemType scale =
std::abs(maxFval - minFval) == 0. ? 1. : std::abs(maxFval - minFval);

for (size_t i = 1; i < fSize - 1; i++)
{
crowdingDistance[front[sortedIdx(i)]] +=
(fValues(sortedIdx(i + 1)) - fValues(sortedIdx(i - 1))) / scale;
}
}
}

//! Comparator for crowding distance based sorting.
template<typename MatType>
inline bool NSGA2::CrowdingOperator(size_t idxP,
size_t idxQ,
const std::vector<size_t>& ranks,
const std::vector<double>& crowdingDistance)
const std::vector<typename MatType::elem_type>& crowdingDistance)
{
if (ranks[idxP] < ranks[idxQ])
return true;
Expand Down
Loading

0 comments on commit 1e70823

Please sign in to comment.