New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Neural evolution algorithms implementation (CNE, NEAT, HyperNEAT) #686
Changes from 1 commit
dc27518
88788d6
6ab9add
ca73f95
30ec0ba
7fdd7ca
e4fbabd
3c8aa62
cbb79a2
65f093d
eb16ce2
c6976ac
8919bb9
ee548b6
faf47a2
adc45b2
95c2e53
1ee07a0
39ef07b
74fb4a0
d0883f4
a605eef
e059af0
6c11f32
3c10995
57d4165
6a770ec
cce115b
2a9899c
1a2cfd8
dacc2e6
2a4c715
6f901a1
a80f5f7
3db85fd
8168779
554a279
1fe7d73
ff45785
48e15c4
fc982b9
66b0f41
109f05b
8054f00
5ef61fe
f213dd8
f234ebd
96ccef5
24f9baf
4decb0e
c46436e
000d6aa
2599077
408b40f
0fc792c
847db95
10b99ab
da55507
beb8ba8
3b66431
9480379
4483fc8
190e639
4cd0054
668fa6e
62d5885
e176571
dec32ac
bc178c3
432331b
0944516
d66bb14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -107,6 +107,7 @@ class NEAT { | |
} | ||
|
||
// Mutate: add new link to genome. | ||
// TODO: what if created looped link? It will influence the depth calculation in genome class!! | ||
void MutateAddLink(Genome& genome, double mutateAddLinkProb) { | ||
// Whether mutate or not. | ||
double p = mlpack::math::Random(); | ||
|
@@ -135,7 +136,7 @@ class NEAT { | |
aLinkInnovations[innovIdx].newLinkInnovId, | ||
mlpack::math::RandNormal(0, 1), // TODO: make the distribution an argument for control? | ||
true); | ||
genome.aLinkGenes.push_back(linkGene); | ||
genome.AddLink(linkGene); | ||
return; | ||
} | ||
|
||
|
@@ -146,7 +147,7 @@ class NEAT { | |
linkInnov.newLinkInnovId, | ||
mlpack::math::RandNormal(0, 1), // TODO: make the distribution an argument for control? | ||
true); | ||
genome.aLinkGenes.push_back(linkGene); | ||
genome.AddLink(linkGene); | ||
} | ||
|
||
// Mutate: add new neuron to genome. | ||
|
@@ -173,7 +174,7 @@ class NEAT { | |
SIGMOID, // TODO: make it random?? | ||
0, | ||
0); | ||
genome.aNeuronGenes.push_back(neuronGene); | ||
genome.AddHiddenNeuron(neuronGene); | ||
|
||
LinkGene inputLink(genome.aLinkGenes[linkIdx].FromNeuronId(), | ||
aNeuronInnovations[innovIdx].newNeuronId, | ||
|
@@ -185,8 +186,8 @@ class NEAT { | |
aNeuronInnovations[innovIdx].newOutputLinkInnovId, | ||
genome.aLinkGenes[linkIdx].Weight(), | ||
true); | ||
genome.aLinkGenes.push_back(inputLink); | ||
genome.aLinkGenes.push_back(outputLink); | ||
genome.AddLink(inputLink); | ||
genome.AddLink(outputLink); | ||
return; | ||
} | ||
|
||
|
@@ -198,7 +199,7 @@ class NEAT { | |
SIGMOID, // TODO: make it random?? | ||
0, | ||
0); | ||
genome.aNeuronGenes.push_back(neuronGene); | ||
genome.AddHiddenNeuron(neuronGene); | ||
|
||
LinkGene inputLink(genome.aLinkGenes[linkIdx].FromNeuronId(), | ||
neuronInnov.newNeuronId, | ||
|
@@ -210,114 +211,213 @@ class NEAT { | |
neuronInnov.newOutputLinkInnovId, | ||
genome.aLinkGenes[linkIdx].Weight(), | ||
true); | ||
genome.aLinkGenes.push_back(inputLink); | ||
genome.aLinkGenes.push_back(outputLink); | ||
genome.AddLink(inputLink); | ||
genome.AddLink(outputLink); | ||
} | ||
|
||
// Crossover link weights. | ||
void Crossover(Genome& momGenome, Genome& dadGenome, Genome& childGenome) { | ||
// Figure out which is the better genome. | ||
Genome* g1 = NULL; // Save the better one. | ||
Genome* g2 = NULL; | ||
if (momGenome.Fitness() < dadGenome.Fitness()) { // NOTICE: we assume smaller is better. | ||
g1 = &momGenome; | ||
g2 = &dadGenome; | ||
} else if (dadGenome.Fitness() < momGenome.Fitness()) { | ||
g1 = &dadGenome; | ||
g2 = &momGenome; | ||
} else if (momGenome.NumLink() < dadGenome.NumLink()) { | ||
g1 = &momGenome; | ||
g2 = &dadGenome; | ||
} else if (dadGenome.NumLink() < momGenome.NumLink()) { | ||
g1 = &dadGenome; | ||
g2 = &momGenome; | ||
} else if (mlpack::math::Random() < 0.5) { | ||
g1 = &momGenome; | ||
g2 = &dadGenome; | ||
} else { | ||
g1 = &dadGenome; | ||
g2 = &momGenome; | ||
} | ||
|
||
// NOTICE: assume momGenome is the better genome. | ||
// NOTICE: assume childGenome is empty. | ||
void CrossoverLinkAndNeuron(Genome& momGenome, Genome& dadGenome, Genome& childGenome) { | ||
// Add input and output neuron genes to child genome. | ||
for (ssize_t i=0; i<(g1->NumInput() + g1->NumOutput()); ++i) { | ||
childGenome.aNeuronGenes.push_back(g1->aNeuronGenes[i]); | ||
for (ssize_t i=0; i<(momGenome.NumInput() + momGenome.NumOutput()); ++i) { | ||
childGenome.aNeuronGenes.push_back(momGenome.aNeuronGenes[i]); | ||
} | ||
|
||
// Iterate to add link genes and neuron genes to child genome. | ||
for (ssize_t i=0; i<g1->NumLink(); ++i) { | ||
ssize_t innovId = g1->aLinkGenes[i].InnovationId(); | ||
ssize_t idx = g2->GetLinkIndex(innovId); | ||
for (ssize_t i=0; i<momGenome.NumLink(); ++i) { | ||
ssize_t innovId = momGenome.aLinkGenes[i].InnovationId(); | ||
ssize_t idx = dadGenome.GetLinkIndex(innovId); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the names of the genomes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! |
||
|
||
if (idx == -1 && g1->aLinkGenes[i].Enabled()) { // exceed or not match | ||
childGenome.aLinkGenes.push_back(g1->aLinkGenes[i]); | ||
if (idx == -1 && momGenome.aLinkGenes[i].Enabled()) { // exceed or disjoint | ||
childGenome.AddLink(momGenome.aLinkGenes[i]); | ||
|
||
// Add from neuron | ||
ssize_t idxInChild = childGenome.GetNeuronIndex(g1->aLinkGenes[i].FromNeuronId()); | ||
ssize_t idxInParent = g1->GetNeuronIndex(g1->aLinkGenes[i].FromNeuronId()); | ||
ssize_t idxInChild = childGenome.GetNeuronIndex(momGenome.aLinkGenes[i].FromNeuronId()); | ||
ssize_t idxInParent = momGenome.GetNeuronIndex(momGenome.aLinkGenes[i].FromNeuronId()); | ||
if (idxInChild == -1) { | ||
childGenome.aNeuronGenes.push_back(g1->aNeuronGenes[idxInParent]); | ||
childGenome.AddHiddenNeuron(momGenome.aNeuronGenes[idxInParent]); | ||
} | ||
|
||
// Add to neuron | ||
idxInChild = childGenome.GetNeuronIndex(g1->aLinkGenes[i].ToNeuronId()); | ||
idxInParent = g1->GetNeuronIndex(g1->aLinkGenes[i].ToNeuronId()); | ||
idxInChild = childGenome.GetNeuronIndex(momGenome.aLinkGenes[i].ToNeuronId()); | ||
idxInParent = momGenome.GetNeuronIndex(momGenome.aLinkGenes[i].ToNeuronId()); | ||
if (idxInChild == -1) { | ||
childGenome.aNeuronGenes.push_back(g1->aNeuronGenes[idxInParent]); | ||
childGenome.AddHiddenNeuron(momGenome.aNeuronGenes[idxInParent]); | ||
} | ||
|
||
continue; | ||
} | ||
|
||
if (idx != -1 && g1->aLinkGenes[i].Enabled() && mlpack::math::Random() < 0.5) { | ||
childGenome.aLinkGenes.push_back(g1->aLinkGenes[i]); | ||
if (idx != -1 && momGenome.aLinkGenes[i].Enabled() && mlpack::math::Random() < 0.5) { | ||
childGenome.AddLink(momGenome.aLinkGenes[i]); | ||
|
||
// Add from neuron | ||
ssize_t idxInChild = childGenome.GetNeuronIndex(g1->aLinkGenes[i].FromNeuronId()); | ||
ssize_t idxInParent = g1->GetNeuronIndex(g1->aLinkGenes[i].FromNeuronId()); | ||
ssize_t idxInChild = childGenome.GetNeuronIndex(momGenome.aLinkGenes[i].FromNeuronId()); | ||
ssize_t idxInParent = momGenome.GetNeuronIndex(momGenome.aLinkGenes[i].FromNeuronId()); | ||
if (idxInChild == -1) { | ||
childGenome.aNeuronGenes.push_back(g1->aNeuronGenes[idxInParent]); | ||
childGenome.AddHiddenNeuron(momGenome.aNeuronGenes[idxInParent]); | ||
} | ||
|
||
// Add to neuron | ||
idxInChild = childGenome.GetNeuronIndex(g1->aLinkGenes[i].ToNeuronId()); | ||
idxInParent = g1->GetNeuronIndex(g1->aLinkGenes[i].ToNeuronId()); | ||
idxInChild = childGenome.GetNeuronIndex(momGenome.aLinkGenes[i].ToNeuronId()); | ||
idxInParent = momGenome.GetNeuronIndex(momGenome.aLinkGenes[i].ToNeuronId()); | ||
if (idxInChild == -1) { | ||
childGenome.aNeuronGenes.push_back(g1->aNeuronGenes[idxInParent]); | ||
childGenome.AddHiddenNeuron(momGenome.aNeuronGenes[idxInParent]); | ||
} | ||
|
||
continue; | ||
} | ||
|
||
if (idx != -1 && g1->aLinkGenes[i].Enabled() && mlpack::math::Random() >= 0.5) { | ||
childGenome.aLinkGenes.push_back(g2->aLinkGenes[idx]); | ||
if (idx != -1 && momGenome.aLinkGenes[i].Enabled() && mlpack::math::Random() >= 0.5) { | ||
childGenome.AddLink(dadGenome.aLinkGenes[idx]); | ||
|
||
// Add from neuron TODO: make it a function?? check whether crossover is correct. | ||
ssize_t idxInChild = childGenome.GetNeuronIndex(g2->aLinkGenes[idx].FromNeuronId()); | ||
ssize_t idxInParent = g2->GetNeuronIndex(g2->aLinkGenes[idx].FromNeuronId()); | ||
ssize_t idxInChild = childGenome.GetNeuronIndex(dadGenome.aLinkGenes[idx].FromNeuronId()); | ||
ssize_t idxInParent = dadGenome.GetNeuronIndex(dadGenome.aLinkGenes[idx].FromNeuronId()); | ||
if (idxInChild == -1) { | ||
childGenome.aNeuronGenes.push_back(g2->aNeuronGenes[idxInParent]); | ||
childGenome.AddHiddenNeuron(dadGenome.aNeuronGenes[idxInParent]); | ||
} | ||
|
||
// Add to neuron | ||
idxInChild = childGenome.GetNeuronIndex(g2->aLinkGenes[idx].ToNeuronId()); | ||
idxInParent = g2->GetNeuronIndex(g2->aLinkGenes[idx].ToNeuronId()); | ||
idxInChild = childGenome.GetNeuronIndex(dadGenome.aLinkGenes[idx].ToNeuronId()); | ||
idxInParent = dadGenome.GetNeuronIndex(dadGenome.aLinkGenes[idx].ToNeuronId()); | ||
if (idxInChild == -1) { | ||
childGenome.aNeuronGenes.push_back(g2->aNeuronGenes[idxInParent]); | ||
childGenome.AddHiddenNeuron(dadGenome.aNeuronGenes[idxInParent]); | ||
} | ||
|
||
continue; | ||
} | ||
} | ||
} | ||
|
||
void Crossover(Genome& genome1, Genome& genome2, Genome& childGenome) { | ||
if (Species::CompareGenome(genome1, genome2)) { // genome1 is better | ||
CrossoverLinkAndNeuron(genome1, genome2, childGenome); | ||
} else { | ||
CrossoverLinkAndNeuron(genome2, genome1, childGenome); | ||
} | ||
} | ||
|
||
// Measure two genomes' disjoint (including exceed). | ||
// TODO: we can seperate into disjoint and exceed. | ||
// But currently maybe it is enough. | ||
double Disjoint(Genome& genome1, Genome& genome2) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, it should work for now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let us distinguish between disjoint and excess here. At the same time, we can decrease the runtime, by not looping over the genomes twice:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I plan to change this after NEAT passed Cart Pole problem. As I saw multiple implementations that the parameters for Exceed and Disjoint are the same. Thus, we can keep the current function for now and it won't influence the performance. After that, it can easily be changed. |
||
double numDisjoint = 0; | ||
|
||
for (ssize_t i=0; i<genome1.NumLink(); ++i) { | ||
ssize_t innovId = genome1.aLinkGenes[i].InnovationId(); | ||
ssize_t idx = genome2.GetLinkIndex(innovId); | ||
if (idx == -1 && genome1.aLinkGenes[i].Enabled()) { | ||
++numDisjoint; | ||
} | ||
} | ||
|
||
for (ssize_t i=0; i<genome2.NumLink(); ++i) { | ||
ssize_t innovId = genome2.aLinkGenes[i].InnovationId(); | ||
ssize_t idx = genome1.GetLinkIndex(innovId); | ||
if (idx == -1 && genome2.aLinkGenes[i].Enabled()) { | ||
++numDisjoint; | ||
} | ||
} | ||
|
||
ssize_t largerGenomeSize = std::max(genome1.NumLink(), genome2.NumLink()); | ||
double deltaD = numDisjoint / largerGenomeSize; | ||
return deltaD; | ||
} | ||
|
||
// Measure two genomes' weight difference. | ||
double WeightDiff(Genome& genome1, Genome& genome2) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can integrate the weight difference into the function I proposed above. |
||
double deltaW = 0; | ||
ssize_t coincident = 0; | ||
|
||
for (ssize_t i=0; i<genome1.NumLink(); ++i) { | ||
ssize_t innovId = genome1.aLinkGenes[i].InnovationId(); | ||
ssize_t idx = genome2.GetLinkIndex(innovId); | ||
if (idx != -1 && genome1.aLinkGenes[i].Enabled()) { | ||
deltaW += std::abs(genome1.aLinkGenes[i].Weight() - genome2.aLinkGenes[idx].Weight()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If genome2 is disabled, isn't the weight difference, just the weight of genome1? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to the NEAT paper, only calculate the difference of matched links. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh! I misunderstood. I think you are right. |
||
++coincident; | ||
} | ||
} | ||
|
||
deltaW = deltaW / coincident; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should check if matching or coincident is > 0. |
||
return deltaW; | ||
} | ||
|
||
// Whether two genome belong to same species or not. | ||
bool IsSameSpecies(Genome& genome1, Genome& genome2) { | ||
double deltaD = Disjoint(genome1, genome2); | ||
double deltaW = WeightDiff(genome1, genome2); | ||
double delta = aCoeffDisjoint * deltaD + aCoeffWeightDiff * deltaW; | ||
|
||
if (delta < aCompatThreshold) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where does this |
||
return true; | ||
} else { | ||
return false; | ||
} | ||
} | ||
|
||
// Add genome to existing species or create new species. | ||
void AddGenomeToSpecies(Genome& genome) { | ||
for (ssize_t i=0; i<aPopulation.NumSpecies(); ++i) { | ||
if (aPopulation.aSpecies[i].SpeciesSize() > 0) { | ||
if (IsSameSpecies(aPopulation.aSpecies[i].aGenomes[0], genome)) { // each first genome in species is the representative genome. | ||
aPopulation.aSpecies[i].AddGenome(genome); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
Species newSpecies = Species(); | ||
newSpecies.AddGenome(genome); | ||
newSpecies.Id(aPopulation.NextSpeciesId()); // NOTICE: changed species id. | ||
aPopulation.AddSpecies(newSpecies); | ||
} | ||
|
||
// Set adjusted fitness. | ||
void AdjustFitness() { | ||
for (ssize_t i=0; i<aPopulation.NumSpecies(); ++i) { | ||
if (aPopulation.aSpecies[i].SpeciesSize() > 0) { | ||
for (ssize_t j=0; j<aPopulation.aSpecies[i].SpeciesSize(); ++j) { | ||
double fitness = aPopulation.aSpecies[i].aGenomes[j].Fitness(); | ||
ssize_t speciesSize = aPopulation.aSpecies[i].SpeciesSize(); | ||
double adjustedFitness = fitness / speciesSize; | ||
aPopulation.aSpecies[i].aGenomes[j].AdjustedFitness(adjustedFitness); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Distribute genomes into species. | ||
void Speciate() { | ||
|
||
} | ||
|
||
// Initialize population. | ||
void InitPopulation() { | ||
aPopulation = Population(aSeedGenome, aPopulationSize); | ||
} | ||
|
||
// Reproduce next generation of population. | ||
void Reproduce() { | ||
|
||
} | ||
|
||
// Evolve. | ||
|
||
private: | ||
// Task. | ||
TaskType aTask; | ||
|
||
// Seed genome. It is used for init species. | ||
Genome aSeedGenome; | ||
|
||
// Population to evolve. | ||
Population aPopulation; | ||
|
||
// Population size. | ||
ssize_t aPopulationSize; | ||
|
||
// List of link innovations. | ||
std::vector<LinkInnovation> aLinkInnovations; | ||
|
||
|
@@ -330,6 +430,18 @@ class NEAT { | |
// Next link id. | ||
ssize_t aNextLinkInnovId; | ||
|
||
// Max number of generation to evolve. | ||
ssize_t aMaxGeneration; | ||
|
||
// Efficient for disjoint. | ||
double aCoeffDisjoint; | ||
|
||
// Efficient for weight difference. | ||
double aCoeffWeightDiff; | ||
|
||
// Threshold for judge whether belong to same species. | ||
double aCompatThreshold; | ||
|
||
}; | ||
|
||
} // namespace ne | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NEAT doesn't change the activation function, but it could be interesting to see if that would increase the performance. However, we have to find a good way, to abstract that functionality from the rest of the code. I think, it's good for now to go with a static activation function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I agree.