Files
ensmallen/tests/moead_test.cpp
T

618 lines
19 KiB
C++

/**
* @file moead_test.cpp
* @author Nanubala Gnana Sai
*
* ensmallen is free software; you may redistribute it and/or modify it under
* the terms of the 3-clause BSD license. You should have received a copy of
* the 3-clause BSD license along with ensmallen. If not, see
* http://www.opensource.org/licenses/BSD-3-Clause for more information.
*/
#include <ensmallen.hpp>
#include "catch.hpp"
#include "test_function_tools.hpp"
using namespace ens;
using namespace ens::test;
using namespace std;
/**
* Checks if low <= value <= high. Used by MOEADFonsecaFlemingTest.
*
* @param value The value being checked.
* @param low The lower bound.
* @param high The upper bound.
* @param roundoff To round off precision.
* @tparam The type of elements in the population set.
* @return true if value lies in the range [low, high].
* @return false if value does not lie in the range [low, high].
*/
template<typename ElemType>
bool IsInBounds(const ElemType& value,
const ElemType& low,
const ElemType& high,
const ElemType& roundoff)
{
return !(value < (low - roundoff)) && !((high + roundoff) < value);
}
/**
* Optimize for the Schaffer N.1 function using NSGA-II optimizer.
* Tests for data of type double.
*/
TEST_CASE("MOEADSchafferN1DoubleTest", "[MOEADTest]")
{
SchafferFunctionN1<arma::mat> SCH;
const double lowerBound = -1000;
const double upperBound = 1000;
const double expectedLowerBound = 0.0;
const double expectedUpperBound = 2.0;
DefaultMOEAD opt(
300, // Population size.
300, // Max generations.
1.0, // Crossover probability.
0.9, // Probability of sampling from neighbor.
20, // Neighborhood size.
20, // Perturbation index.
0.5, // Differential weight.
2, // Max childrens to replace parents.
1E-10, // epsilon.
lowerBound, // Lower bound.
upperBound // Upper bound.
);
typedef decltype(SCH.objectiveA) ObjectiveTypeA;
typedef decltype(SCH.objectiveB) ObjectiveTypeB;
// We allow a few trials in case of poor convergence.
bool success = false;
for (size_t trial = 0; trial < 3; ++trial)
{
arma::mat coords = SCH.GetInitialPoint();
std::tuple<ObjectiveTypeA, ObjectiveTypeB> objectives = SCH.GetObjectives();
opt.Optimize(objectives, coords);
arma::cube paretoSet= opt.ParetoSet();
bool allInRange = true;
for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
{
double val = arma::as_scalar(paretoSet.slice(solutionIdx));
if (!IsInBounds<double>(val, expectedLowerBound, expectedUpperBound, 0.1))
{
allInRange = false;
break;
}
}
if (allInRange)
{
success = true;
break;
}
}
REQUIRE(success == true);
}
/**
* Optimize for the Schaffer N.1 function using NSGA-II optimizer.
* Tests for data of type double.
*/
TEST_CASE("MOEADSchafferN1TestVectorDoubleBounds", "[MOEADTest]")
{
// This test can be a little flaky, so we try it a few times.
SchafferFunctionN1<arma::mat> SCH;
const arma::vec lowerBound = {-1000};
const arma::vec upperBound = {1000};
const double expectedLowerBound = 0.0;
const double expectedUpperBound = 2.0;
DefaultMOEAD opt(
300, // Population size.
300, // Max generations.
1.0, // Crossover probability.
0.9, // Probability of sampling from neighbor.
20, // Neighborhood size.
20, // Perturbation index.
0.5, // Differential weight.
2, // Max childrens to replace parents.
1E-10, // epsilon.
lowerBound, // Lower bound.
upperBound // Upper bound.
);
typedef decltype(SCH.objectiveA) ObjectiveTypeA;
typedef decltype(SCH.objectiveB) ObjectiveTypeB;
bool success = false;
for (size_t trial = 0; trial < 3; ++trial)
{
arma::mat coords = SCH.GetInitialPoint();
std::tuple<ObjectiveTypeA, ObjectiveTypeB> objectives = SCH.GetObjectives();
opt.Optimize(objectives, coords);
arma::cube paretoSet = opt.ParetoSet();
bool allInRange = true;
for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
{
double val = arma::as_scalar(paretoSet.slice(solutionIdx));
if (!IsInBounds<double>(val, expectedLowerBound, expectedUpperBound, 0.1))
{
allInRange = false;
break;
}
}
if (allInRange)
{
success = true;
break;
}
}
REQUIRE(success == true);
}
/**
* Optimize for the Fonseca Fleming function using NSGA-II optimizer.
* Tests for data of type double.
*/
TEST_CASE("MOEADFonsecaFlemingDoubleTest", "[MOEADTest]")
{
FonsecaFlemingFunction<arma::mat> FON;
const double lowerBound = -4;
const double upperBound = 4;
const double expectedLowerBound = -1.0 / sqrt(3);
const double expectedUpperBound = 1.0 / sqrt(3);
DefaultMOEAD opt(
300, // Max generations.
300, // Max generations.
1.0, // Crossover probability.
0.9, // Probability of sampling from neighbor.
20, // Neighborhood size.
20, // Perturbation index.
0.5, // Differential weight.
2, // Max childrens to replace parents.
1E-10, // epsilon.
lowerBound, // Lower bound.
upperBound // Upper bound.
);
typedef decltype(FON.objectiveA) ObjectiveTypeA;
typedef decltype(FON.objectiveB) ObjectiveTypeB;
arma::mat coords = FON.GetInitialPoint();
std::tuple<ObjectiveTypeA, ObjectiveTypeB> objectives = FON.GetObjectives();
opt.Optimize(objectives, coords);
arma::cube paretoSet = opt.ParetoSet();
bool allInRange = true;
for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
{
const arma::mat solution = paretoSet.slice(solutionIdx);
double valX = arma::as_scalar(solution(0));
double valY = arma::as_scalar(solution(1));
double valZ = arma::as_scalar(solution(2));
if (!IsInBounds<double>(valX, expectedLowerBound, expectedUpperBound, 0.1) ||
!IsInBounds<double>(valY, expectedLowerBound, expectedUpperBound, 0.1) ||
!IsInBounds<double>(valZ, expectedLowerBound, expectedUpperBound, 0.1))
{
allInRange = false;
break;
}
}
REQUIRE(allInRange);
}
/**
* Optimize for the Fonseca Fleming function using NSGA-II optimizer.
* Tests for data of type double.
*/
TEST_CASE("MOEADFonsecaFlemingTestVectorDoubleBounds", "[MOEADTest]")
{
FonsecaFlemingFunction<arma::mat> FON;
const arma::vec lowerBound = {-4, -4, -4};
const arma::vec upperBound = {4, 4, 4};
const double expectedLowerBound = -1.0 / sqrt(3);
const double expectedUpperBound = 1.0 / sqrt(3);
DefaultMOEAD opt(
300, // Max generations.
300, // Max generations.
1.0, // Crossover probability.
0.9, // Probability of sampling from neighbor.
20, // Neighborhood size.
20, // Perturbation index.
0.5, // Differential weight.
2, // Max childrens to replace parents.
1E-10, // epsilon.
lowerBound, // Lower bound.
upperBound // Upper bound.
);
typedef decltype(FON.objectiveA) ObjectiveTypeA;
typedef decltype(FON.objectiveB) ObjectiveTypeB;
arma::mat coords = FON.GetInitialPoint();
std::tuple<ObjectiveTypeA, ObjectiveTypeB> objectives = FON.GetObjectives();
opt.Optimize(objectives, coords);
arma::cube paretoSet = opt.ParetoSet();
bool allInRange = true;
for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
{
const arma::mat solution = paretoSet.slice(solutionIdx);
double valX = arma::as_scalar(solution(0));
double valY = arma::as_scalar(solution(1));
double valZ = arma::as_scalar(solution(2));
if (!IsInBounds<double>(valX, expectedLowerBound, expectedUpperBound, 0.1) ||
!IsInBounds<double>(valY, expectedLowerBound, expectedUpperBound, 0.1) ||
!IsInBounds<double>(valZ, expectedLowerBound, expectedUpperBound, 0.1))
{
allInRange = false;
break;
}
}
REQUIRE(allInRange);
}
/**
* Optimize for the Schaffer N.1 function using NSGA-II optimizer.
* Tests for data of type float.
*/
TEST_CASE("MOEADSchafferN1FloatTest", "[MOEADTest]")
{
SchafferFunctionN1<arma::fmat> SCH;
const double lowerBound = -1000;
const double upperBound = 1000;
const double expectedLowerBound = 0.0;
const double expectedUpperBound = 2.0;
DefaultMOEAD opt(
300, // Population size.
300, // Max generations.
1.0, // Crossover probability.
0.9, // Probability of sampling from neighbor.
20, // Neighborhood size.
20, // Perturbation index.
0.5, // Differential weight.
2, // Max childrens to replace parents.
1E-10, // epsilon.
lowerBound, // Lower bound.
upperBound // Upper bound.
);
typedef decltype(SCH.objectiveA) ObjectiveTypeA;
typedef decltype(SCH.objectiveB) ObjectiveTypeB;
// We allow a few trials in case of poor convergence.
bool success = false;
for (size_t trial = 0; trial < 3; ++trial)
{
arma::fmat coords = SCH.GetInitialPoint();
std::tuple<ObjectiveTypeA, ObjectiveTypeB> objectives = SCH.GetObjectives();
opt.Optimize(objectives, coords);
arma::fcube paretoSet = arma::conv_to<arma::fcube>::from(opt.ParetoSet());
bool allInRange = true;
for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
{
float val = arma::as_scalar(paretoSet.slice(solutionIdx));
if (!IsInBounds<float>(val, expectedLowerBound, expectedUpperBound, 0.1))
{
allInRange = false;
break;
}
}
if (allInRange)
{
success = true;
break;
}
}
REQUIRE(success == true);
}
/**
* Optimize for the Schaffer N.1 function using NSGA-II optimizer.
* Tests for data of type float.
*/
TEST_CASE("MOEADSchafferN1TestVectorFloatBounds", "[MOEADTest]")
{
// This test can be a little flaky, so we try it a few times.
SchafferFunctionN1<arma::fmat> SCH;
const arma::vec lowerBound = {-1000};
const arma::vec upperBound = {1000};
const double expectedLowerBound = 0.0;
const double expectedUpperBound = 2.0;
DefaultMOEAD opt(
300, // Population size.
300, // Max generations.
1.0, // Crossover probability.
0.9, // Probability of sampling from neighbor.
20, // Neighborhood size.
20, // Perturbation index.
0.5, // Differential weight.
2, // Max childrens to replace parents.
1E-10, // epsilon.
lowerBound, // Lower bound.
upperBound // Upper bound.
);
typedef decltype(SCH.objectiveA) ObjectiveTypeA;
typedef decltype(SCH.objectiveB) ObjectiveTypeB;
bool success = false;
for (size_t trial = 0; trial < 3; ++trial)
{
arma::fmat coords = SCH.GetInitialPoint();
std::tuple<ObjectiveTypeA, ObjectiveTypeB> objectives = SCH.GetObjectives();
opt.Optimize(objectives, coords);
arma::fcube paretoSet = arma::conv_to<arma::fcube>::from(opt.ParetoSet());
bool allInRange = true;
for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
{
float val = arma::as_scalar(paretoSet.slice(solutionIdx));
if (!IsInBounds<float>(val, expectedLowerBound, expectedUpperBound, 0.1))
{
allInRange = false;
break;
}
}
if (allInRange)
{
success = true;
break;
}
}
REQUIRE(success == true);
}
/**
* Optimize for the Fonseca Fleming function using NSGA-II optimizer.
* Tests for data of type float.
*/
TEST_CASE("MOEADFonsecaFlemingFloatTest", "[MOEADTest]")
{
FonsecaFlemingFunction<arma::fmat> FON;
const double lowerBound = -4;
const double upperBound = 4;
const float expectedLowerBound = -1.0 / sqrt(3);
const float expectedUpperBound = 1.0 / sqrt(3);
DefaultMOEAD opt(
300, // Max generations.
300, // Max generations.
1.0, // Crossover probability.
0.9, // Probability of sampling from neighbor.
20, // Neighborhood size.
20, // Perturbation index.
0.5, // Differential weight.
2, // Max childrens to replace parents.
1E-10, // epsilon.
lowerBound, // Lower bound.
upperBound // Upper bound.
);
typedef decltype(FON.objectiveA) ObjectiveTypeA;
typedef decltype(FON.objectiveB) ObjectiveTypeB;
arma::fmat coords = FON.GetInitialPoint();
std::tuple<ObjectiveTypeA, ObjectiveTypeB> objectives = FON.GetObjectives();
opt.Optimize(objectives, coords);
arma::fcube paretoSet = arma::conv_to<arma::fcube>::from(opt.ParetoSet());
bool allInRange = true;
for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
{
const arma::fmat solution = paretoSet.slice(solutionIdx);
float valX = arma::as_scalar(solution(0));
float valY = arma::as_scalar(solution(1));
float valZ = arma::as_scalar(solution(2));
if (!IsInBounds<float>(valX, expectedLowerBound, expectedUpperBound, 0.1) ||
!IsInBounds<float>(valY, expectedLowerBound, expectedUpperBound, 0.1) ||
!IsInBounds<float>(valZ, expectedLowerBound, expectedUpperBound, 0.1))
{
allInRange = false;
break;
}
}
REQUIRE(allInRange);
}
/**
* Optimize for the Fonseca Fleming function using NSGA-II optimizer.
* Tests for data of type float.
*/
TEST_CASE("MOEADFonsecaFlemingTestVectorFloatBounds", "[MOEADTest]")
{
FonsecaFlemingFunction<arma::fmat> FON;
const arma::vec lowerBound = {-4, -4, -4};
const arma::vec upperBound = {4, 4, 4};
const float expectedLowerBound = -1.0 / sqrt(3);
const float expectedUpperBound = 1.0 / sqrt(3);
DefaultMOEAD opt(
300, // Max generations.
300, // Max generations.
1.0, // Crossover probability.
0.9, // Probability of sampling from neighbor.
20, // Neighborhood size.
20, // Perturbation index.
0.5, // Differential weight.
2, // Max childrens to replace parents.
1E-10, // epsilon.
lowerBound, // Lower bound.
upperBound // Upper bound.
);
typedef decltype(FON.objectiveA) ObjectiveTypeA;
typedef decltype(FON.objectiveB) ObjectiveTypeB;
arma::fmat coords = FON.GetInitialPoint();
std::tuple<ObjectiveTypeA, ObjectiveTypeB> objectives = FON.GetObjectives();
opt.Optimize(objectives, coords);
arma::fcube paretoSet = arma::conv_to<arma::fcube>::from(opt.ParetoSet());
bool allInRange = true;
for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
{
const arma::fmat solution = paretoSet.slice(solutionIdx);
float valX = arma::as_scalar(solution(0));
float valY = arma::as_scalar(solution(1));
float valZ = arma::as_scalar(solution(2));
if (!IsInBounds<float>(valX, expectedLowerBound, expectedUpperBound, 0.1) ||
!IsInBounds<float>(valY, expectedLowerBound, expectedUpperBound, 0.1) ||
!IsInBounds<float>(valZ, expectedLowerBound, expectedUpperBound, 0.1))
{
allInRange = false;
break;
}
}
REQUIRE(allInRange);
}
/**
* Test against the first problem of ZDT Test Suite. ZDT-1 is a 30
* variable-2 objective problem with a convex Pareto Front.
*
* NOTE: For the sake of runtime, only ZDT-1 is tested against the
* algorithm. Others have been tested separately.
*/
TEST_CASE("MOEADZDTONETest", "[MOEADTest]")
{
//! Parameters taken from original ZDT Paper.
ZDT1<> ZDT_ONE(100);
const double lowerBound = 0;
const double upperBound = 1;
DefaultMOEAD opt(
300, // Population size.
150, // Max generations.
1.0, // Crossover probability.
0.9, // Probability of sampling from neighbor.
20, // Neighborhood size.
20, // Perturbation index.
0.5, // Differential weight.
2, // Max childrens to replace parents.
1E-10, // epsilon.
lowerBound, // Lower bound.
upperBound // Upper bound.
);
typedef decltype(ZDT_ONE.objectiveF1) ObjectiveTypeA;
typedef decltype(ZDT_ONE.objectiveF2) ObjectiveTypeB;
arma::mat coords = ZDT_ONE.GetInitialPoint();
std::tuple<ObjectiveTypeA, ObjectiveTypeB> objectives = ZDT_ONE.GetObjectives();
opt.Optimize(objectives, coords);
//! Refer the ZDT_ONE implementation for g objective implementation.
//! The optimal g value is taken from the docs of ZDT_ONE.
size_t numVariables = coords.size();
double sum = arma::accu(coords(arma::span(1, numVariables - 1), 0));
double g = 1. + 9. * sum / (static_cast<double>(numVariables - 1));
REQUIRE(g == Approx(1.0).margin(0.99));
}
/**
* Check if the final population lies in the optimal region in variable space.
*
* @param paretoSet The final population in variable space.
*/
bool VariableBoundsCheck(const arma::cube& paretoSet)
{
bool inBounds = true;
const arma::mat regions{
{0.0, 0.182228780, 0.4093136748,
0.6183967944, 0.8233317983},
{0.0830015349, 0.2577623634, 0.4538821041,
0.6525117038, 0.8518328654}
};
for (size_t pointIdx = 0; pointIdx < paretoSet.n_slices; ++pointIdx)
{
const arma::mat& point = paretoSet.slice(pointIdx);
const double firstVariable = point(0, 0);
const bool notInRegion0 = !IsInBounds<double>(firstVariable, regions(0, 0), regions(1, 0), 1e-2);
const bool notInRegion1 = !IsInBounds<double>(firstVariable, regions(0, 1), regions(1, 1), 1e-2);
const bool notInRegion2 = !IsInBounds<double>(firstVariable, regions(0, 2), regions(1, 2), 1e-2);
const bool notInRegion3 = !IsInBounds<double>(firstVariable, regions(0, 3), regions(1, 3), 1e-2);
const bool notInRegion4 = !IsInBounds<double>(firstVariable, regions(0, 4), regions(1, 4), 1e-2);
if (notInRegion0 && notInRegion1 && notInRegion2 && notInRegion3 && notInRegion4)
{
inBounds = false;
break;
}
}
return inBounds;
}
/**
* Test DirichletMOEAD against the third problem of ZDT Test Suite. ZDT-3 is a 30
* variable-2 objective problem with disconnected Pareto Fronts.
*/
TEST_CASE("MOEADDIRICHLETZDT3Test", "[MOEADTest]")
{
//! Parameters taken from original ZDT Paper.
ZDT3<> ZDT_THREE(300);
const double lowerBound = 0;
const double upperBound = 1;
DirichletMOEAD opt(
300, // Population size.
300, // Max generations.
1.0, // Crossover probability.
0.9, // Probability of sampling from neighbor.
20, // Neighborhood size.
20, // Perturbation index.
0.5, // Differential weight.
2, // Max childrens to replace parents.
1E-10, // epsilon.
lowerBound, // Lower bound.
upperBound // Upper bound.
);
typedef decltype(ZDT_THREE.objectiveF1) ObjectiveTypeA;
typedef decltype(ZDT_THREE.objectiveF2) ObjectiveTypeB;
arma::mat coords = ZDT_THREE.GetInitialPoint();
std::tuple<ObjectiveTypeA, ObjectiveTypeB> objectives = ZDT_THREE.GetObjectives();
opt.Optimize(objectives, coords);
const arma::cube& finalPopulation = opt.ParetoSet();
REQUIRE(VariableBoundsCheck(finalPopulation));
}