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 |
---|---|---|
|
@@ -12,9 +12,6 @@ | |
#include <map> | ||
|
||
#include <mlpack/core.hpp> | ||
#include <mlpack/methods/ann/activation_functions/logistic_function.hpp> | ||
#include <mlpack/methods/ann/activation_functions/rectifier_function.hpp> | ||
#include <mlpack/methods/ann/activation_functions/tanh_function.hpp> | ||
|
||
#include "link_gene.hpp" | ||
#include "neuron_gene.hpp" | ||
|
@@ -44,15 +41,13 @@ class Genome { | |
const std::vector<LinkGene>& linkGenes, | ||
ssize_t numInput, | ||
ssize_t numOutput, | ||
ssize_t depth, | ||
double fitness, | ||
double adjustedFitness): | ||
aId(id), | ||
aNeuronGenes(neuronGenes), | ||
aLinkGenes(linkGenes), | ||
aNumInput(numInput), | ||
aNumOutput(numOutput), | ||
aDepth(depth), | ||
aFitness(fitness), | ||
aAdjustedFitness(adjustedFitness) | ||
{} | ||
|
@@ -64,7 +59,6 @@ class Genome { | |
aLinkGenes = genome.aLinkGenes; | ||
aNumInput = genome.aNumInput; | ||
aNumOutput = genome.aNumOutput; | ||
aDepth = genome.aDepth; | ||
aFitness = genome.aFitness; | ||
aAdjustedFitness = genome.aAdjustedFitness; | ||
} | ||
|
@@ -80,7 +74,6 @@ class Genome { | |
aLinkGenes = genome.aLinkGenes; | ||
aNumInput = genome.aNumInput; | ||
aNumOutput = genome.aNumOutput; | ||
aDepth = genome.aDepth; | ||
aFitness = genome.aFitness; | ||
aAdjustedFitness = genome.aAdjustedFitness; | ||
} | ||
|
@@ -120,12 +113,6 @@ class Genome { | |
// Set output length. | ||
void NumOutput(ssize_t numOutput) { aNumOutput = numOutput; } | ||
|
||
// Get depth. | ||
ssize_t Depth() const { return aDepth; } | ||
|
||
// Set depth. | ||
void Depth(ssize_t depth) { aDepth = depth; } | ||
|
||
// Set fitness. | ||
void Fitness(double fitness) { aFitness = fitness; } | ||
|
||
|
@@ -150,9 +137,6 @@ class Genome { | |
|
||
// Whether specified neuron id exist in this genome. | ||
bool HasNeuronId(ssize_t id) const { | ||
assert(id > 0); | ||
assert(NumNeuron() > 0); | ||
|
||
for (ssize_t i=0; i<NumNeuron(); ++i) { | ||
if (aNeuronGenes[i].Id() == id) { | ||
return true; | ||
|
@@ -216,137 +200,86 @@ class Genome { | |
return false; | ||
} | ||
|
||
// Calculate Neuron depth. | ||
ssize_t NeuronDepth(ssize_t id, ssize_t depth) { | ||
// Network contains loop. | ||
ssize_t loopDepth = NumNeuron() - NumInput() - NumOutput() + 1; // If contains loop in network. | ||
if (depth > loopDepth) { | ||
return loopDepth; | ||
} | ||
|
||
// Find all links that output to this neuron id. | ||
std::vector<int> inputLinksIndex; | ||
for (ssize_t i=0; i<NumLink(); ++i) { | ||
if (aLinkGenes[i].ToNeuronId() == id) { | ||
inputLinksIndex.push_back(i); | ||
} | ||
} | ||
|
||
// INPUT or BIAS or isolated neurons. | ||
if (inputLinksIndex.size() == 0) { | ||
return 0; | ||
} | ||
|
||
// Recursively get neuron depth. | ||
ssize_t currentDepth; | ||
ssize_t maxDepth = depth; | ||
|
||
for (ssize_t i=0; i<inputLinksIndex.size(); ++i) { | ||
currentDepth = NeuronDepth(aLinkGenes[inputLinksIndex[i]].FromNeuronId(), depth + 1); | ||
if (currentDepth > maxDepth) { | ||
maxDepth = currentDepth; | ||
} | ||
// Set neurons' input and output to zero. | ||
void Flush() { | ||
for (ssize_t i=0; i<aNeuronGenes.size(); ++i) { | ||
aNeuronGenes[i].Activation(0); | ||
aNeuronGenes[i].Input(0); | ||
} | ||
|
||
return maxDepth; | ||
} | ||
|
||
// Calculate Genome depth. | ||
// It is the max depth of all output neuron genes. | ||
ssize_t GenomeDepth() { | ||
ssize_t numNeuron = NumNeuron(); | ||
|
||
// If empty genome. | ||
if (numNeuron == 0) { | ||
aDepth = 0; | ||
return aDepth; | ||
} | ||
// Sort neuron genes by depth. | ||
static bool CompareNeuronGene(NeuronGene ln, NeuronGene rn) { | ||
return (ln.Depth() < rn.Depth()); | ||
} | ||
void SortHiddenNeuronGenes() { | ||
std::sort(aNeuronGenes.begin() + NumInput() + NumOutput(), aNeuronGenes.end(), CompareNeuronGene); | ||
} | ||
|
||
// If no hidden neuron, depth is 1. | ||
if (aNumInput + aNumOutput == numNeuron) { | ||
aDepth = 1; | ||
return aDepth; | ||
} | ||
|
||
// Find all OUTPUT neuron id. | ||
std::vector<ssize_t> outputNeuronsId; | ||
for (ssize_t i=0; i<NumNeuron(); ++i) { | ||
if (aNeuronGenes[i].Type() == OUTPUT) { | ||
outputNeuronsId.push_back(aNeuronGenes[i].Id()); | ||
// Sort link genes by toNeuron's depth. | ||
void SortLinkGenes() { | ||
struct DepthAndLink | ||
{ | ||
double depth; | ||
LinkGene link; | ||
|
||
DepthAndLink(double d, LinkGene& l) : depth(d), link(l) {} | ||
|
||
bool operator < (const DepthAndLink& dL) const | ||
{ | ||
return (depth < dL.depth); | ||
} | ||
}; | ||
|
||
std::vector<double> toNeuronDepths; | ||
for (ssize_t i=0; i<aLinkGenes.size(); ++i) { | ||
NeuronGene toNeuron = GetNeuronById(aLinkGenes[i].ToNeuronId()); | ||
toNeuronDepths.push_back(toNeuron.Depth()); | ||
} | ||
|
||
// Get max depth of all output neurons. | ||
ssize_t genomeDepth = 0; | ||
for (ssize_t i=0; i<outputNeuronsId.size(); ++i) { | ||
ssize_t outputNeuronDepth = NeuronDepth(outputNeuronsId[i], 0); | ||
if (outputNeuronDepth > genomeDepth) { | ||
genomeDepth = outputNeuronDepth; | ||
} | ||
std::vector<DepthAndLink> depthAndLinks; | ||
ssize_t linkGenesSize = aLinkGenes.size(); | ||
for (ssize_t i=0; i<linkGenesSize; ++i) { | ||
depthAndLinks.push_back(DepthAndLink(toNeuronDepths[i], aLinkGenes[i])); | ||
} | ||
aDepth = genomeDepth; | ||
|
||
return aDepth; | ||
} | ||
std::sort(depthAndLinks.begin(), depthAndLinks.end()); | ||
|
||
// Set neurons' input and output to zero. | ||
void Flush() { | ||
for (ssize_t i=0; i<aNeuronGenes.size(); ++i) { | ||
aNeuronGenes[i].aActivation = 0; | ||
aNeuronGenes[i].aInput = 0; | ||
for (ssize_t i=0; i<linkGenesSize; ++i) { | ||
aLinkGenes[i] = depthAndLinks[i].link; | ||
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 think, we could speed this up, if we would just store the index of all linkGenes, an use another list, that contains all linkGenes as a reference. |
||
} | ||
} | ||
|
||
// Activate genome. The last dimension of input is always 1 for bias. 0 means no bias. | ||
// NOTICE: make sure depth is set before activate. | ||
void Activate(std::vector<double>& input) { | ||
assert(input.size() == aNumInput); | ||
//Flush(); | ||
|
||
// Set inputs. | ||
for (ssize_t i=0; i<aNumInput; ++i) { | ||
aNeuronGenes[i].aActivation = input[i]; // assume INPUT, BIAS, OUTPUT, HIDDEN sequence | ||
} | ||
|
||
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 we reset the activation at the beginning, we can't use the activation at time (t - 1) from a unit that has a recurrent connection, right? I'm not sure we have to reset the activation at all, we could just overwrite the existing. 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. @zoq Yeah, seems reasonable. I add the Flush() step to make sure it will be correct for activation. But I also think it seems not a required step. |
||
// Construct neuron id: index dictionary. | ||
std::map<ssize_t, ssize_t> neuronIdToIndex; | ||
SortLinkGenes(); | ||
|
||
// Set all neurons' input to be 0. | ||
for (ssize_t i=0; i<NumNeuron(); ++i) { | ||
neuronIdToIndex.insert(std::pair<ssize_t, ssize_t>(aNeuronGenes[i].Id(), i)); | ||
aNeuronGenes[i].Input(0); | ||
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 we reset the input, we also lose the input of a recurrent connection, for the next iteration. 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. A recurrent connection's input is the neuron's activation, so I think it won't lose actually. |
||
} | ||
|
||
// Activate layer by layer. | ||
for (ssize_t i=0; i<aDepth; ++i) { | ||
// Loop links to calculate neurons' input sum. | ||
for (ssize_t j=0; j<aLinkGenes.size(); ++j) { | ||
aNeuronGenes[neuronIdToIndex.at(aLinkGenes[j].ToNeuronId())].aInput += | ||
aLinkGenes[j].Weight() * | ||
aNeuronGenes[neuronIdToIndex.at(aLinkGenes[j].FromNeuronId())].aActivation * | ||
((int) aLinkGenes[j].Enabled()); | ||
} | ||
// Set input neurons. | ||
for (ssize_t i=0; i<aNumInput; ++i) { | ||
aNeuronGenes[i].Activation(input[i]); // assume INPUT, BIAS, OUTPUT, HIDDEN sequence | ||
} | ||
|
||
// Loop neurons to calculate neurons' activation. | ||
for (ssize_t j=aNumInput; j<aNeuronGenes.size(); ++j) { | ||
double x = aNeuronGenes[j].aInput; // TODO: consider bias. Difference? | ||
aNeuronGenes[j].aInput = 0; | ||
|
||
double y = 0; | ||
switch (aNeuronGenes[j].Type()) { // TODO: more cases. | ||
case SIGMOID: | ||
y = ann::LogisticFunction::fn(x); | ||
break; | ||
case TANH: | ||
y = ann::TanhFunction::fn(x); | ||
break; | ||
case RELU: | ||
y = ann::RectifierFunction::fn(x); | ||
break; | ||
case LINEAR: | ||
y = x; | ||
default: | ||
y = ann::LogisticFunction::fn(x); | ||
break; | ||
// Activate hidden and output neurons. | ||
for (ssize_t i = 0; i < NumLink(); ++i) { | ||
if (aLinkGenes[i].Enabled()) { | ||
ssize_t toNeuronIdx = GetNeuronIndex(aLinkGenes[i].ToNeuronId()); | ||
ssize_t fromNeuronIdx = GetNeuronIndex(aLinkGenes[i].FromNeuronId()); | ||
double input = aNeuronGenes[toNeuronIdx].Input() + | ||
aNeuronGenes[fromNeuronIdx].Activation() * aLinkGenes[i].Weight(); | ||
aNeuronGenes[toNeuronIdx].Input(input); | ||
|
||
if (i == NumLink() - 1) { | ||
aNeuronGenes[toNeuronIdx].CalcActivation(); | ||
} else if (GetNeuronIndex(aLinkGenes[i + 1].ToNeuronId()) != toNeuronIdx) { | ||
aNeuronGenes[toNeuronIdx].CalcActivation(); | ||
} | ||
aNeuronGenes[j].aActivation = y; | ||
} | ||
} | ||
} | ||
|
@@ -355,7 +288,7 @@ class Genome { | |
std::vector<double> Output() { | ||
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's use a reference here, also, it might be a good idea, to use an arma::vec here:
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. Revised. 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. Yeah, right. I did some testing, and encounter an unexpected behaviour. This are the modifications I made: https://gist.github.com/zoq/181c036cb4b903f8e7cb2977639ee6b8 I've also, printed the activation function type in CalcActivation(), which is the RELU function, but I expected it to be SIGMOID, do we randomly choose the activation function, somewhere? The seed genome uses the SIGMOID for all neurons except for the BIAS neuron. |
||
std::vector<double> output; | ||
for (ssize_t i=0; i<aNumOutput; ++i) { | ||
output.push_back(aNeuronGenes[aNumInput + i].aActivation); | ||
output.push_back(aNeuronGenes[aNumInput + i].Activation()); | ||
} | ||
return output; | ||
} | ||
|
@@ -390,9 +323,6 @@ class Genome { | |
// Output length. | ||
ssize_t aNumOutput; | ||
|
||
// Network maximum depth. | ||
ssize_t aDepth; | ||
|
||
// Genome fitness. | ||
double aFitness; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -191,6 +191,8 @@ class NEAT { | |
if (!genome.aLinkGenes[linkIdx].Enabled()) return; | ||
|
||
genome.aLinkGenes[linkIdx].Enabled(false); | ||
NeuronGene fromNeuron = genome.GetNeuronById(genome.aLinkGenes[linkIdx].FromNeuronId()); | ||
NeuronGene toNeuron = genome.GetNeuronById(genome.aLinkGenes[linkIdx].ToNeuronId()); | ||
|
||
// Check innovation already exist or not. | ||
ssize_t splitLinkInnovId = genome.aLinkGenes[linkIdx].InnovationId(); | ||
|
@@ -199,6 +201,7 @@ class NEAT { | |
NeuronGene neuronGene(aNeuronInnovations[innovIdx].newNeuronId, | ||
HIDDEN, | ||
SIGMOID, // TODO: make it random?? | ||
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. 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 commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I agree. |
||
(fromNeuron.Depth() + toNeuron.Depth()) / 2, | ||
0, | ||
0); | ||
genome.AddHiddenNeuron(neuronGene); | ||
|
@@ -224,6 +227,7 @@ class NEAT { | |
NeuronGene neuronGene(neuronInnov.newNeuronId, | ||
HIDDEN, | ||
SIGMOID, // TODO: make it random?? | ||
(fromNeuron.Depth() + toNeuron.Depth()) / 2, | ||
0, | ||
0); | ||
genome.AddHiddenNeuron(neuronGene); | ||
|
@@ -696,7 +700,6 @@ class NEAT { | |
////printf("breed 8\n"); | ||
childGenome.NumInput(childGenome.NumInput()); | ||
childGenome.NumOutput(childGenome.NumOutput()); | ||
childGenome.GenomeDepth(); | ||
return true; | ||
} | ||
|
||
|
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.
Maybe it's a good idea, to add a GetDepthById function, in this case we could avoid to copy the object.