Merge pull request #8515 from imapp-pl/yul-phaser-classic-genetic-algorithm

[yul-phaser] Classic genetic algorithm
This commit is contained in:
chriseth
2020-04-15 12:01:51 +02:00
committed by GitHub
20 changed files with 881 additions and 32 deletions
+3 -3
View File
@@ -187,16 +187,16 @@ Population AlgorithmRunner::randomiseDuplicates(
if (_population.individuals().size() == 0)
return _population;
vector<Chromosome> chromosomes{_population.individuals()[0].chromosome};
vector<Individual> individuals{_population.individuals()[0]};
size_t duplicateCount = 0;
for (size_t i = 1; i < _population.individuals().size(); ++i)
if (_population.individuals()[i].chromosome == _population.individuals()[i - 1].chromosome)
++duplicateCount;
else
chromosomes.push_back(_population.individuals()[i].chromosome);
individuals.push_back(_population.individuals()[i]);
return (
Population(_population.fitnessMetric(), chromosomes) +
Population(_population.fitnessMetric(), individuals) +
Population::makeRandom(_population.fitnessMetric(), duplicateCount, _minChromosomeLength, _maxChromosomeLength)
);
}
+80 -18
View File
@@ -43,24 +43,86 @@ Population RandomAlgorithm::runNextRound(Population _population)
Population GenerationalElitistWithExclusivePools::runNextRound(Population _population)
{
double elitePoolSize = 1.0 - (m_options.mutationPoolSize + m_options.crossoverPoolSize);
RangeSelection elite(0.0, elitePoolSize);
RangeSelection elitePool(0.0, elitePoolSize);
RandomSelection mutationPoolFromElite(m_options.mutationPoolSize / elitePoolSize);
RandomPairSelection crossoverPoolFromElite(m_options.crossoverPoolSize / elitePoolSize);
std::function<Mutation> mutationOperator = alternativeMutations(
m_options.randomisationChance,
geneRandomisation(m_options.percentGenesToRandomise),
alternativeMutations(
m_options.deletionVsAdditionChance,
geneDeletion(m_options.percentGenesToAddOrDelete),
geneAddition(m_options.percentGenesToAddOrDelete)
)
);
std::function<Crossover> crossoverOperator = randomPointCrossover();
return
_population.select(elite) +
_population.select(elite).mutate(
RandomSelection(m_options.mutationPoolSize / elitePoolSize),
alternativeMutations(
m_options.randomisationChance,
geneRandomisation(m_options.percentGenesToRandomise),
alternativeMutations(
m_options.deletionVsAdditionChance,
geneDeletion(m_options.percentGenesToAddOrDelete),
geneAddition(m_options.percentGenesToAddOrDelete)
)
)
) +
_population.select(elite).crossover(
RandomPairSelection(m_options.crossoverPoolSize / elitePoolSize),
randomPointCrossover()
);
_population.select(elitePool) +
_population.select(elitePool).mutate(mutationPoolFromElite, mutationOperator) +
_population.select(elitePool).crossover(crossoverPoolFromElite, crossoverOperator);
}
Population ClassicGeneticAlgorithm::runNextRound(Population _population)
{
Population elite = _population.select(RangeSelection(0.0, m_options.elitePoolSize));
Population rest = _population.select(RangeSelection(m_options.elitePoolSize, 1.0));
Population selectedPopulation = select(_population, rest.individuals().size());
Population crossedPopulation = Population::combine(
selectedPopulation.symmetricCrossoverWithRemainder(
PairsFromRandomSubset(m_options.crossoverChance),
symmetricRandomPointCrossover()
)
);
std::function<Mutation> mutationOperator = mutationSequence({
geneRandomisation(m_options.mutationChance),
geneDeletion(m_options.deletionChance),
geneAddition(m_options.additionChance),
});
RangeSelection all(0.0, 1.0);
Population mutatedPopulation = crossedPopulation.mutate(all, mutationOperator);
return elite + mutatedPopulation;
}
Population ClassicGeneticAlgorithm::select(Population _population, size_t _selectionSize)
{
if (_population.individuals().size() == 0)
return _population;
size_t maxFitness = 0;
for (auto const& individual: _population.individuals())
maxFitness = max(maxFitness, individual.fitness);
size_t rouletteRange = 0;
for (auto const& individual: _population.individuals())
// Add 1 to make sure that every chromosome has non-zero probability of being chosen
rouletteRange += maxFitness + 1 - individual.fitness;
vector<Individual> selectedIndividuals;
for (size_t i = 0; i < _selectionSize; ++i)
{
uint32_t ball = SimulationRNG::uniformInt(0, rouletteRange - 1);
size_t cumulativeFitness = 0;
for (auto const& individual: _population.individuals())
{
size_t pocketSize = maxFitness + 1 - individual.fitness;
if (ball < cumulativeFitness + pocketSize)
{
selectedIndividuals.push_back(individual);
break;
}
cumulativeFitness += pocketSize;
}
}
assert(selectedIndividuals.size() == _selectionSize);
return Population(_population.fitnessMetric(), selectedIndividuals);
}
+55
View File
@@ -139,4 +139,59 @@ private:
Options m_options;
};
/**
* A typical genetic algorithm that works in three distinct phases, each resulting in a new,
* modified population:
* - selection: chromosomes are selected from the population with probability proportional to their
* fitness. A chromosome can be selected more than once. The new population has the same size as
* the old one.
* - crossover: first, for each chromosome we decide whether it undergoes crossover or not
* (according to crossover chance parameter). Then each selected chromosome is randomly paired
* with one other selected chromosome. Each pair produces a pair of children and gets replaced by
* it in the population.
* - mutation: we go over each gene in the population and independently decide whether to mutate it
* or not (according to mutation chance parameters). This is repeated for every mutation type so
* one gene can undergo mutations of multiple types in a single round.
*
* This implementation also has the ability to preserve the top chromosomes in each round.
*/
class ClassicGeneticAlgorithm: public GeneticAlgorithm
{
public:
struct Options
{
double elitePoolSize; ///< Percentage of the population treated as the elite.
double crossoverChance; ///< The chance of a particular chromosome being selected for crossover.
double mutationChance; ///< The chance of a particular gene being randomised in @a geneRandomisation mutation.
double deletionChance; ///< The chance of a particular gene being deleted in @a geneDeletion mutation.
double additionChance; ///< The chance of a particular gene being added in @a geneAddition mutation.
bool isValid() const
{
return (
0 <= elitePoolSize && elitePoolSize <= 1.0 &&
0 <= crossoverChance && crossoverChance <= 1.0 &&
0 <= mutationChance && mutationChance <= 1.0 &&
0 <= deletionChance && deletionChance <= 1.0 &&
0 <= additionChance && additionChance <= 1.0
);
}
};
ClassicGeneticAlgorithm(Options const& _options):
m_options(_options)
{
assert(_options.isValid());
}
Options const& options() const { return m_options; }
Population runNextRound(Population _population) override;
private:
static Population select(Population _population, size_t _selectionSize);
Options m_options;
};
}
+42 -7
View File
@@ -95,10 +95,22 @@ function<Mutation> phaser::alternativeMutations(
};
}
function<Mutation> phaser::mutationSequence(vector<function<Mutation>> _mutations)
{
return [=](Chromosome const& _chromosome)
{
Chromosome mutatedChromosome = _chromosome;
for (size_t i = 0; i < _mutations.size(); ++i)
mutatedChromosome = _mutations[i](move(mutatedChromosome));
return mutatedChromosome;
};
}
namespace
{
Chromosome buildChromosomesBySwappingParts(
ChromosomePair fixedPointSwap(
Chromosome const& _chromosome1,
Chromosome const& _chromosome2,
size_t _crossoverPoint
@@ -109,11 +121,19 @@ Chromosome buildChromosomesBySwappingParts(
auto begin1 = _chromosome1.optimisationSteps().begin();
auto begin2 = _chromosome2.optimisationSteps().begin();
auto end1 = _chromosome1.optimisationSteps().end();
auto end2 = _chromosome2.optimisationSteps().end();
return Chromosome(
vector<string>(begin1, begin1 + _crossoverPoint) +
vector<string>(begin2 + _crossoverPoint, _chromosome2.optimisationSteps().end())
);
return {
Chromosome(
vector<string>(begin1, begin1 + _crossoverPoint) +
vector<string>(begin2 + _crossoverPoint, end2)
),
Chromosome(
vector<string>(begin2, begin2 + _crossoverPoint) +
vector<string>(begin1 + _crossoverPoint, end1)
),
};
}
}
@@ -129,7 +149,22 @@ function<Crossover> phaser::randomPointCrossover()
assert(minPoint <= minLength);
size_t randomPoint = SimulationRNG::uniformInt(minPoint, minLength);
return buildChromosomesBySwappingParts(_chromosome1, _chromosome2, randomPoint);
return get<0>(fixedPointSwap(_chromosome1, _chromosome2, randomPoint));
};
}
function<SymmetricCrossover> phaser::symmetricRandomPointCrossover()
{
return [=](Chromosome const& _chromosome1, Chromosome const& _chromosome2)
{
size_t minLength = min(_chromosome1.length(), _chromosome2.length());
// Don't use position 0 (because this just swaps the values) unless it's the only choice.
size_t minPoint = (minLength > 0? 1 : 0);
assert(minPoint <= minLength);
size_t randomPoint = SimulationRNG::uniformInt(minPoint, minLength);
return fixedPointSwap(_chromosome1, _chromosome2, randomPoint);
};
}
@@ -142,6 +177,6 @@ function<Crossover> phaser::fixedPointCrossover(double _crossoverPoint)
size_t minLength = min(_chromosome1.length(), _chromosome2.length());
size_t concretePoint = static_cast<size_t>(round(minLength * _crossoverPoint));
return buildChromosomesBySwappingParts(_chromosome1, _chromosome2, concretePoint);
return get<0>(fixedPointSwap(_chromosome1, _chromosome2, concretePoint));
};
}
+10
View File
@@ -28,8 +28,11 @@
namespace solidity::phaser
{
using ChromosomePair = std::tuple<Chromosome, Chromosome>;
using Mutation = Chromosome(Chromosome const&);
using Crossover = Chromosome(Chromosome const&, Chromosome const&);
using SymmetricCrossover = ChromosomePair(Chromosome const&, Chromosome const&);
// MUTATIONS
@@ -55,12 +58,19 @@ std::function<Mutation> alternativeMutations(
std::function<Mutation> _mutation2
);
/// Creates a mutation operator that sequentially applies all the operators given in @a _mutations.
std::function<Mutation> mutationSequence(std::vector<std::function<Mutation>> _mutations);
// CROSSOVER
/// Creates a crossover operator that randomly selects a number between 0 and 1 and uses it as the
/// position at which to perform perform @a fixedPointCrossover.
std::function<Crossover> randomPointCrossover();
/// Symmetric version of @a randomPointCrossover(). Creates an operator that returns a pair
/// containing both possible results for the same crossover point.
std::function<SymmetricCrossover> symmetricRandomPointCrossover();
/// Creates a crossover operator that always chooses a point that lies at @a _crossoverPoint
/// percent of the length of the shorter chromosome. Then creates a new chromosome by
/// splitting both inputs at the crossover point and stitching output from the first half or first
+38
View File
@@ -17,6 +17,7 @@
#include <tools/yulPhaser/PairSelections.h>
#include <tools/yulPhaser/Selections.h>
#include <tools/yulPhaser/SimulationRNG.h>
#include <cmath>
@@ -47,6 +48,43 @@ vector<tuple<size_t, size_t>> RandomPairSelection::materialise(size_t _poolSize)
return selection;
}
vector<tuple<size_t, size_t>> PairsFromRandomSubset::materialise(size_t _poolSize) const
{
vector<size_t> selectedIndices = RandomSubset(m_selectionChance).materialise(_poolSize);
if (selectedIndices.size() % 2 != 0)
{
if (selectedIndices.size() < _poolSize && SimulationRNG::bernoulliTrial(0.5))
{
do
{
size_t extraIndex = SimulationRNG::uniformInt(0, selectedIndices.size() - 1);
if (find(selectedIndices.begin(), selectedIndices.end(), extraIndex) == selectedIndices.end())
selectedIndices.push_back(extraIndex);
} while (selectedIndices.size() % 2 != 0);
}
else
selectedIndices.erase(selectedIndices.begin() + SimulationRNG::uniformInt(0, selectedIndices.size() - 1));
}
assert(selectedIndices.size() % 2 == 0);
vector<tuple<size_t, size_t>> selectedPairs;
for (size_t i = selectedIndices.size() / 2; i > 0; --i)
{
size_t position1 = SimulationRNG::uniformInt(0, selectedIndices.size() - 1);
size_t value1 = selectedIndices[position1];
selectedIndices.erase(selectedIndices.begin() + position1);
size_t position2 = SimulationRNG::uniformInt(0, selectedIndices.size() - 1);
size_t value2 = selectedIndices[position2];
selectedIndices.erase(selectedIndices.begin() + position2);
selectedPairs.push_back({value1, value2});
}
assert(selectedIndices.size() == 0);
return selectedPairs;
}
vector<tuple<size_t, size_t>> PairMosaicSelection::materialise(size_t _poolSize) const
{
if (_poolSize < 2)
+22
View File
@@ -69,6 +69,28 @@ private:
double m_selectionSize;
};
/**
* A selection that goes over all elements in a container, for each one independently decides
* whether to select it or not and then randomly combines those elements into pairs. If the number
* of elements is odd, randomly decides whether to take one more or exclude one.
*
* Each element has the same chance of being selected and can be selected at most once.
* The number of selected elements is random and can be different with each call to
* @a materialise().
*/
class PairsFromRandomSubset: public PairSelection
{
public:
explicit PairsFromRandomSubset(double _selectionChance):
m_selectionChance(_selectionChance) {}
std::vector<std::tuple<size_t, size_t>> materialise(size_t _poolSize) const override;
private:
double m_selectionChance;
};
/**
* A selection that selects pairs of elements at specific, fixed positions indicated by a repeating
* "pattern". If the positions in the pattern exceed the size of the container, they are capped at
+46
View File
@@ -58,6 +58,7 @@ map<Algorithm, string> const AlgorithmToStringMap =
{
{Algorithm::Random, "random"},
{Algorithm::GEWEP, "GEWEP"},
{Algorithm::Classic, "classic"},
};
map<string, Algorithm> const StringToAlgorithmMap = invertMap(AlgorithmToStringMap);
@@ -107,6 +108,11 @@ GeneticAlgorithmFactory::Options GeneticAlgorithmFactory::Options::fromCommandLi
_arguments.count("gewep-genes-to-add-or-delete") > 0 ?
_arguments["gewep-genes-to-add-or-delete"].as<double>() :
optional<double>{},
_arguments["classic-elite-pool-size"].as<double>(),
_arguments["classic-crossover-chance"].as<double>(),
_arguments["classic-mutation-chance"].as<double>(),
_arguments["classic-deletion-chance"].as<double>(),
_arguments["classic-addition-chance"].as<double>(),
};
}
@@ -151,6 +157,16 @@ unique_ptr<GeneticAlgorithm> GeneticAlgorithmFactory::build(
/* percentGenesToAddOrDelete = */ percentGenesToAddOrDelete,
});
}
case Algorithm::Classic:
{
return make_unique<ClassicGeneticAlgorithm>(ClassicGeneticAlgorithm::Options{
/* elitePoolSize = */ _options.classicElitePoolSize,
/* crossoverChance = */ _options.classicCrossoverChance,
/* mutationChance = */ _options.classicMutationChance,
/* deletionChance = */ _options.classicDeletionChance,
/* additionChance = */ _options.classicAdditionChance,
});
}
default:
assertThrow(false, solidity::util::Exception, "Invalid Algorithm value.");
}
@@ -475,6 +491,36 @@ Phaser::CommandLineDescription Phaser::buildCommandLineDescription()
;
keywordDescription.add(gewepAlgorithmDescription);
po::options_description classicGeneticAlgorithmDescription("CLASSIC GENETIC ALGORITHM", lineLength, minDescriptionLength);
classicGeneticAlgorithmDescription.add_options()
(
"classic-elite-pool-size",
po::value<double>()->value_name("<FRACTION>")->default_value(0),
"Percentage of population to regenerate using mutations in each round."
)
(
"classic-crossover-chance",
po::value<double>()->value_name("<FRACTION>")->default_value(0.75),
"Chance of a chromosome being selected for crossover."
)
(
"classic-mutation-chance",
po::value<double>()->value_name("<FRACTION>")->default_value(0.01),
"Chance of a gene being mutated."
)
(
"classic-deletion-chance",
po::value<double>()->value_name("<PROBABILITY>")->default_value(0.01),
"Chance of a gene being deleted."
)
(
"classic-addition-chance",
po::value<double>()->value_name("<PROBABILITY>")->default_value(0.01),
"Chance of a random gene being added."
)
;
keywordDescription.add(classicGeneticAlgorithmDescription);
po::options_description randomAlgorithmDescription("RANDOM ALGORITHM", lineLength, minDescriptionLength);
randomAlgorithmDescription.add_options()
(
+6
View File
@@ -58,6 +58,7 @@ enum class Algorithm
{
Random,
GEWEP,
Classic,
};
enum class MetricChoice
@@ -101,6 +102,11 @@ public:
double gewepDeletionVsAdditionChance;
std::optional<double> gewepGenesToRandomise;
std::optional<double> gewepGenesToAddOrDelete;
double classicElitePoolSize;
double classicCrossoverChance;
double classicMutationChance;
double classicDeletionChance;
double classicAdditionChance;
static Options fromCommandLine(boost::program_options::variables_map const& _arguments);
};
+36
View File
@@ -117,6 +117,37 @@ Population Population::crossover(PairSelection const& _selection, function<Cross
return Population(m_fitnessMetric, crossedIndividuals);
}
tuple<Population, Population> Population::symmetricCrossoverWithRemainder(
PairSelection const& _selection,
function<SymmetricCrossover> _symmetricCrossover
) const
{
vector<int> indexSelected(m_individuals.size(), false);
vector<Individual> crossedIndividuals;
for (auto const& [i, j]: _selection.materialise(m_individuals.size()))
{
auto children = _symmetricCrossover(
m_individuals[i].chromosome,
m_individuals[j].chromosome
);
crossedIndividuals.emplace_back(move(get<0>(children)), *m_fitnessMetric);
crossedIndividuals.emplace_back(move(get<1>(children)), *m_fitnessMetric);
indexSelected[i] = true;
indexSelected[j] = true;
}
vector<Individual> remainder;
for (size_t i = 0; i < indexSelected.size(); ++i)
if (!indexSelected[i])
remainder.emplace_back(m_individuals[i]);
return {
Population(m_fitnessMetric, crossedIndividuals),
Population(m_fitnessMetric, remainder),
};
}
namespace solidity::phaser
{
@@ -132,6 +163,11 @@ Population operator+(Population _a, Population _b)
}
Population Population::combine(std::tuple<Population, Population> _populationPair)
{
return get<0>(_populationPair) + get<1>(_populationPair);
}
bool Population::operator==(Population const& _other) const
{
// We consider populations identical only if they share the same exact instance of the metric.
+8 -4
View File
@@ -81,6 +81,9 @@ public:
_fitnessMetric,
chromosomesToIndividuals(*_fitnessMetric, std::move(_chromosomes))
) {}
explicit Population(std::shared_ptr<FitnessMetric> _fitnessMetric, std::vector<Individual> _individuals):
m_fitnessMetric(std::move(_fitnessMetric)),
m_individuals{sortedIndividuals(std::move(_individuals))} {}
static Population makeRandom(
std::shared_ptr<FitnessMetric> _fitnessMetric,
@@ -97,8 +100,13 @@ public:
Population select(Selection const& _selection) const;
Population mutate(Selection const& _selection, std::function<Mutation> _mutation) const;
Population crossover(PairSelection const& _selection, std::function<Crossover> _crossover) const;
std::tuple<Population, Population> symmetricCrossoverWithRemainder(
PairSelection const& _selection,
std::function<SymmetricCrossover> _symmetricCrossover
) const;
friend Population operator+(Population _a, Population _b);
static Population combine(std::tuple<Population, Population> _populationPair);
std::shared_ptr<FitnessMetric> fitnessMetric() { return m_fitnessMetric; }
std::vector<Individual> const& individuals() const { return m_individuals; }
@@ -112,10 +120,6 @@ public:
friend std::ostream& operator<<(std::ostream& _stream, Population const& _population);
private:
explicit Population(std::shared_ptr<FitnessMetric> _fitnessMetric, std::vector<Individual> _individuals):
m_fitnessMetric(std::move(_fitnessMetric)),
m_individuals{sortedIndividuals(std::move(_individuals))} {}
static std::vector<Individual> chromosomesToIndividuals(
FitnessMetric& _fitnessMetric,
std::vector<Chromosome> _chromosomes
+10
View File
@@ -20,6 +20,7 @@
#include <tools/yulPhaser/SimulationRNG.h>
#include <cmath>
#include <numeric>
using namespace std;
using namespace solidity::phaser;
@@ -58,3 +59,12 @@ vector<size_t> RandomSelection::materialise(size_t _poolSize) const
return selection;
}
vector<size_t> RandomSubset::materialise(size_t _poolSize) const
{
vector<size_t> selection;
for (size_t index = 0; index < _poolSize; ++index)
if (SimulationRNG::bernoulliTrial(m_selectionChance))
selection.push_back(index);
return selection;
}
+22
View File
@@ -118,4 +118,26 @@ private:
double m_selectionSize;
};
/**
* A selection that goes over all elements in a container, for each one independently deciding
* whether to select it or not. Each element has the same chance of being selected and can be
* selected at most once. The order of selected elements is the same as the order of elements in
* the container. The number of selected elements is random and can be different with each call
* to @a materialise().
*/
class RandomSubset: public Selection
{
public:
explicit RandomSubset(double _selectionChance):
m_selectionChance(_selectionChance)
{
assert(0.0 <= _selectionChance && _selectionChance <= 1.0);
}
std::vector<size_t> materialise(size_t _poolSize) const override;
private:
double m_selectionChance;
};
}