I have an interesting example for the NEAT and want to clarify what behavior is correct from NEAT's perspective and why (why the opposite is wrong, what are the consequences of choosing the different one).
So let we have an initial network of 3 nodes and 2 edges:
Initial Condition
Nodes: [A, B, C]
Edges: {
1: A->B
2: B->C
}
1st Gen
Then in the 1-st generation we get 2 mutants:
Mutant 1 (edge 1 got split)
Nodes: [A, B, C, D]
Edges: {
1: A->B DIS
2: B->C
3: A->D
4: D->B
}
Mutant 2 (edge 2 got split)
Nodes: [A, B, C, E]
Edges: {
1: A->B
2: B->C DIS
5: B->E
6: E->C
}
2nd Gen
In the second generation 2 if we mutate Mutant 1 (by splitting edge 2) and mutate Mutant 2 (by splitting edge 1) which result should we get?
Hypothesis 1: the same result:
Nodes: [A, B, C, D, E]
Edges: {
1: A->B DIS
2: B->C DIS
3: A->D
4: D->B
5: B->E
6: E->C
}
or...
Hypothesis 2: Two new mutants:
Nodes: [A, B, C, D, F]
Edges: {
1: A->B DIS
2: B->C DIS
3: A->D
4: D->B
7: B->F
8: F->C
}
and
Nodes: [A, B, C, E, G]
Edges: {
1: A->B DIS
2: B->C DIS
5: B->E
6: E->C
9: A->G
10: G->B
}
In case the second hypothesis is correct, how does it deal with crossover in the next run? Say these 2 mutants are breeded. We get :
Breeding in 2nd Hypothesis
Nodes: [A, B, C, D, E, F, G]
Edges: {
1: A->B DIS
2: B->C DIS
3: A->D
4: D->B
5: B->E
6: E->C
7: B->F
8: F->C
9: A->G
10: G->B
}
Looks like a too complicated genome for the 3-rd generation, doesn't it?
In case the first option is correct then actually innovation numbers are somewhat redundant in NEAT and can be done differently.
We can have node list as a list of strings (node names).
Then instead of assigning the innovation number to an edge we can use string value calculated like HASH(fromNodeName + toNodeName)
.
That way whenever the new link is created in any generation between 2 nodes it gets the same innovation number name for it.
When the node is created (by splitting an edge) its name can be taken right from the edge getting split and the innovation names of 2 new edges can be calculated like HASH(fromNodeName + splitEdgeName)
and HASH(splitEdgeName + toNodeName)
.
That way the algorithm has no global variables, no shared list of all innovations and can be simply parallelized