/*
* Encog(tm) Core v2.5 - Java Version
* http://www.heatonresearch.com/encog/
* http://code.google.com/p/encog-java/
* Copyright 2008-2010 Heaton Research, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* For more information on Heaton Research copyrights, licenses
* and trademarks visit:
* http://www.heatonresearch.com/copyright
*/
package org.encog.neural.networks.training.neat;
import java.util.Collections;
import java.util.List;
import org.encog.mathutil.randomize.RangeRandomizer;
import org.encog.neural.NeuralNetworkError;
import org.encog.neural.networks.synapse.neat.NEATLink;
import org.encog.neural.networks.synapse.neat.NEATNeuron;
import org.encog.neural.networks.synapse.neat.NEATNeuronType;
import org.encog.neural.pattern.NEATPattern;
import org.encog.persist.annotations.EGAttribute;
import org.encog.persist.annotations.EGReference;
import org.encog.solve.genetic.genes.Gene;
import org.encog.solve.genetic.genome.BasicGenome;
import org.encog.solve.genetic.genome.Chromosome;
/**
* Implements a NEAT genome. This is a "blueprint" for creating a neural
* network.
*
* NeuroEvolution of Augmenting Topologies (NEAT) is a genetic algorithm for the
* generation of evolving artificial neural networks. It was developed by Ken
* Stanley while at The University of Texas at Austin.
*
* http://www.cs.ucf.edu/~kstanley/
*
*/
public class NEATGenome extends BasicGenome implements Cloneable {
/**
* The adjustment factor for disjoint genes.
*/
public static final double TWEAK_DISJOINT = 1;
/**
* The adjustment factor for excess genes.
*/
public static final double TWEAK_EXCESS = 1;
/**
* The adjustment factor for matched genes.
*/
public static final double TWEAK_MATCHED = 0.4;
/**
* The number of inputs.
*/
@EGAttribute
private int inputCount;
/**
* The chromsome that holds the links.
*/
@EGReference
private Chromosome linksChromosome;
/**
* THe network depth.
*/
@EGAttribute
private int networkDepth;
/**
* The chromosome that holds the neurons.
*/
@EGReference
private Chromosome neuronsChromosome;
/**
* The number of outputs.
*/
@EGAttribute
private int outputCount;
/**
* The species id.
*/
@EGAttribute
private long speciesID;
/**
* Default constructor, used mainly for persistence.
*/
public NEATGenome() {
super(null);
}
/**
* Construct a genome by copying another.
*
* @param other
* The other genome.
*/
public NEATGenome(final NEATGenome other) {
super(other.getGeneticAlgorithm());
this.neuronsChromosome = new Chromosome();
this.linksChromosome = new Chromosome();
getChromosomes().add(this.neuronsChromosome);
getChromosomes().add(this.linksChromosome);
setGenomeID(other.getGenomeID());
this.networkDepth = other.networkDepth;
setScore(other.getScore());
setAdjustedScore(other.getAdjustedScore());
setAmountToSpawn(other.getAmountToSpawn());
this.inputCount = other.inputCount;
this.outputCount = other.outputCount;
this.speciesID = other.speciesID;
// copy neurons
for (final Gene gene : other.getNeurons().getGenes()) {
final NEATNeuronGene oldGene = (NEATNeuronGene) gene;
final NEATNeuronGene newGene = new NEATNeuronGene(oldGene
.getNeuronType(), oldGene.getId(), oldGene.getSplitY(),
oldGene.getSplitX(), oldGene.isRecurrent(), oldGene
.getActivationResponse());
getNeurons().add(newGene);
}
// copy links
for (final Gene gene : other.getLinks().getGenes()) {
final NEATLinkGene oldGene = (NEATLinkGene) gene;
final NEATLinkGene newGene = new NEATLinkGene(oldGene
.getFromNeuronID(), oldGene.getToNeuronID(), oldGene
.isEnabled(), oldGene.getInnovationId(), oldGene
.getWeight(), oldGene.isRecurrent());
getLinks().add(newGene);
}
}
/**
* Create a NEAT gnome.
*
* @param training
* The owner object.
* @param genomeID
* The genome id.
* @param neurons
* The neurons.
* @param links
* The links.
* @param inputCount
* The input count.
* @param outputCount
* The output count.
*/
public NEATGenome(final NEATTraining training, final long genomeID,
final Chromosome neurons, final Chromosome links,
final int inputCount, final int outputCount) {
super(training);
setGenomeID(genomeID);
this.linksChromosome = links;
this.neuronsChromosome = neurons;
setAmountToSpawn(0);
setAdjustedScore(0);
this.inputCount = inputCount;
this.outputCount = outputCount;
getChromosomes().add(this.neuronsChromosome);
getChromosomes().add(this.linksChromosome);
}
/**
* Construct a genome, do not provide links and neurons.
*
* @param training
* The owner object.
* @param id
* The genome id.
* @param inputCount
* The input count.
* @param outputCount
* The output count.
*/
public NEATGenome(final NEATTraining training, final long id,
final int inputCount, final int outputCount) {
super(training);
setGenomeID(id);
setAdjustedScore(0);
this.inputCount = inputCount;
this.outputCount = outputCount;
setAmountToSpawn(0);
this.speciesID = 0;
final double inputRowSlice = 0.8 / (inputCount);
this.neuronsChromosome = new Chromosome();
this.linksChromosome = new Chromosome();
getChromosomes().add(this.neuronsChromosome);
getChromosomes().add(this.linksChromosome);
for (int i = 0; i < inputCount; i++) {
this.neuronsChromosome.add(new NEATNeuronGene(NEATNeuronType.Input,
i, 0, 0.1 + i * inputRowSlice));
}
this.neuronsChromosome.add(new NEATNeuronGene(NEATNeuronType.Bias,
inputCount, 0, 0.9));
final double outputRowSlice = 1 / (double) (outputCount + 1);
for (int i = 0; i < outputCount; i++) {
this.neuronsChromosome.add(new NEATNeuronGene(
NEATNeuronType.Output, i + inputCount + 1, 1, (i + 1)
* outputRowSlice));
}
for (int i = 0; i < inputCount + 1; i++) {
for (int j = 0; j < outputCount; j++) {
this.linksChromosome.add(new NEATLinkGene(
((NEATNeuronGene) this.neuronsChromosome.get(i))
.getId(), ((NEATNeuronGene) getNeurons().get(
inputCount + j + 1)).getId(), true, inputCount
+ outputCount + 1 + getNumGenes(),
RangeRandomizer.randomize(-1, 1), false));
}
}
}
/**
* Mutate the genome by adding a link to this genome.
*
* @param mutationRate
* The mutation rate.
* @param chanceOfLooped
* The chance of a self-connected neuron.
* @param numTrysToFindLoop
* The number of tries to find a loop.
* @param numTrysToAddLink
* The number of tries to add a link.
*/
void addLink(final double mutationRate, final double chanceOfLooped,
final int numTrysToFindLoop, final int numTrysToAddLink) {
// should we even add the link
if (Math.random() > mutationRate) {
return;
}
int countTrysToFindLoop = numTrysToFindLoop;
int countTrysToAddLink = numTrysToFindLoop;
// the link will be between these two neurons
long neuron1ID = -1;
long neuron2ID = -1;
boolean recurrent = false;
// a self-connected loop?
if (Math.random() < chanceOfLooped) {
// try to find(randomly) a neuron to add a self-connected link to
while ((countTrysToFindLoop--) > 0) {
final NEATNeuronGene neuronGene = chooseRandomNeuron(false);
// no self-links on input or bias neurons
if (!neuronGene.isRecurrent()
&& (neuronGene.getNeuronType() != NEATNeuronType.Bias)
&& (neuronGene.getNeuronType() != NEATNeuronType.Input)) {
neuron1ID = neuronGene.getId();
neuron2ID = neuronGene.getId();
neuronGene.setRecurrent(true);
recurrent = true;
countTrysToFindLoop = 0;
}
}
} else {
// try to add a regular link
while ((countTrysToAddLink--) > 0) {
final NEATNeuronGene neuron1 = chooseRandomNeuron(true);
final NEATNeuronGene neuron2 = chooseRandomNeuron(false);
if (!isDuplicateLink(neuron1ID, neuron2ID)
&& (neuron1.getId() != neuron2.getId())
&& (neuron2.getNeuronType() != NEATNeuronType.Bias)) {
neuron1ID = neuron1.getId();
neuron2ID = neuron2.getId();
break;
}
}
}
// did we fail to find a link
if ((neuron1ID < 0) || (neuron2ID < 0)) {
return;
}
// check to see if this innovation has already been tried
final NEATInnovation innovation = ((NEATTraining) getGeneticAlgorithm())
.getInnovations().checkInnovation(neuron1ID, neuron1ID,
NEATInnovationType.NewLink);
// see if this is a recurrent(backwards) link
final NEATNeuronGene neuronGene
= (NEATNeuronGene) this.neuronsChromosome
.get(getElementPos(neuron1ID));
if (neuronGene.getSplitY() > neuronGene.getSplitY()) {
recurrent = true;
}
// is this a new innovation?
if (innovation == null) {
// new innovation
((NEATTraining) getGeneticAlgorithm()).getInnovations()
.createNewInnovation(neuron1ID, neuron2ID,
NEATInnovationType.NewLink);
final long id2 = ((NEATTraining) getGeneticAlgorithm())
.getPopulation().assignInnovationID();
final NEATLinkGene linkGene = new NEATLinkGene(neuron1ID,
neuron2ID, true, id2, RangeRandomizer.randomize(-1, 1),
recurrent);
this.linksChromosome.add(linkGene);
} else {
// existing innovation
final NEATLinkGene linkGene = new NEATLinkGene(neuron1ID,
neuron2ID, true, innovation.getInnovationID(),
RangeRandomizer.randomize(-1, 1), recurrent);
this.linksChromosome.add(linkGene);
}
}
/**
* Mutate the genome by adding a neuron.
*
* @param mutationRate
* The mutation rate.
* @param numTrysToFindOldLink
* The number of tries to find a link to split.
*/
void addNeuron(final double mutationRate, final int numTrysToFindOldLink) {
// should we add a neuron?
if (Math.random() > mutationRate) {
return;
}
int countTrysToFindOldLink = numTrysToFindOldLink;
// the link to split
NEATLinkGene splitLink = null;
final int sizeBias = this.inputCount + this.outputCount + 10;
// if there are not at least
int upperLimit;
if (this.linksChromosome.size() < sizeBias) {
upperLimit = getNumGenes() - 1 - (int) Math.sqrt(getNumGenes());
} else {
upperLimit = getNumGenes() - 1;
}
while ((countTrysToFindOldLink--) > 0) {
// choose a link, use the square root to prefer the older links
final int i = RangeRandomizer.randomInt(0, upperLimit);
final NEATLinkGene link = (NEATLinkGene) this.linksChromosome
.get(i);
// get the from neuron
final long fromNeuron = link.getFromNeuronID();
if ((link.isEnabled())
&& (!link.isRecurrent())
&& (((NEATNeuronGene) getNeurons().get(
getElementPos(fromNeuron))).getNeuronType()
!= NEATNeuronType.Bias)) {
splitLink = link;
break;
}
}
if (splitLink == null) {
return;
}
splitLink.setEnabled(false);
final double originalWeight = splitLink.getWeight();
final long from = splitLink.getFromNeuronID();
final long to = splitLink.getToNeuronID();
final NEATNeuronGene fromGene = (NEATNeuronGene) getNeurons().get(
getElementPos(from));
final NEATNeuronGene toGene = (NEATNeuronGene) getNeurons().get(
getElementPos(to));
final double newDepth = (fromGene.getSplitY() + toGene.getSplitY()) / 2;
final double newWidth = (fromGene.getSplitX() + toGene.getSplitX()) / 2;
// has this innovation already been tried?
NEATInnovation innovation = ((NEATTraining) getGeneticAlgorithm())
.getInnovations().checkInnovation(from, to,
NEATInnovationType.NewNeuron);
// prevent chaining
if (innovation != null) {
final long neuronID = innovation.getNeuronID();
if (alreadyHaveThisNeuronID(neuronID)) {
innovation = null;
}
}
if (innovation == null) {
// this innovation has not been tried, create it
final long newNeuronID = ((NEATTraining) getGeneticAlgorithm())
.getInnovations().createNewInnovation(from, to,
NEATInnovationType.NewNeuron,
NEATNeuronType.Hidden, newWidth, newDepth);
this.neuronsChromosome.add(new NEATNeuronGene(
NEATNeuronType.Hidden, newNeuronID, newDepth, newWidth));
// add the first link
final long link1ID = ((NEATTraining) getGeneticAlgorithm())
.getPopulation().assignInnovationID();
((NEATTraining) getGeneticAlgorithm()).getInnovations()
.createNewInnovation(from, newNeuronID,
NEATInnovationType.NewLink);
final NEATLinkGene link1 = new NEATLinkGene(from, newNeuronID,
true, link1ID, 1.0, false);
this.linksChromosome.add(link1);
// add the second link
final long link2ID = ((NEATTraining) getGeneticAlgorithm())
.getPopulation().assignInnovationID();
((NEATTraining) getGeneticAlgorithm()).getInnovations()
.createNewInnovation(newNeuronID, to,
NEATInnovationType.NewLink);
final NEATLinkGene link2 = new NEATLinkGene(newNeuronID, to, true,
link2ID, originalWeight, false);
this.linksChromosome.add(link2);
}
else {
// existing innovation
final long newNeuronID = innovation.getNeuronID();
final NEATInnovation innovationLink1
= ((NEATTraining) getGeneticAlgorithm())
.getInnovations().checkInnovation(from, newNeuronID,
NEATInnovationType.NewLink);
final NEATInnovation innovationLink2
= ((NEATTraining) getGeneticAlgorithm())
.getInnovations().checkInnovation(newNeuronID, to,
NEATInnovationType.NewLink);
if ((innovationLink1 == null) || (innovationLink2 == null)) {
throw new NeuralNetworkError("NEAT Error");
}
final NEATLinkGene link1 = new NEATLinkGene(from, newNeuronID,
true, innovationLink1.getInnovationID(), 1.0, false);
final NEATLinkGene link2 = new NEATLinkGene(newNeuronID, to, true,
innovationLink2.getInnovationID(), originalWeight, false);
this.linksChromosome.add(link1);
this.linksChromosome.add(link2);
final NEATNeuronGene newNeuron = new NEATNeuronGene(
NEATNeuronType.Hidden, newNeuronID, newDepth, newWidth);
this.neuronsChromosome.add(newNeuron);
}
return;
}
/**
* Do we already have this neuron id?
*
* @param id
* The id to check for.
* @return True if we already have this neuron id.
*/
public boolean alreadyHaveThisNeuronID(final long id) {
for (final Gene gene : this.neuronsChromosome.getGenes()) {
final NEATNeuronGene neuronGene = (NEATNeuronGene) gene;
if (neuronGene.getId() == id) {
return true;
}
}
return false;
}
/**
* Choose a random neuron.
*
* @param includeInput
* Should the input neurons be included.
* @return The random neuron.
*/
private NEATNeuronGene chooseRandomNeuron(final boolean includeInput) {
int start;
if (includeInput) {
start = 0;
} else {
start = this.inputCount + 1;
}
final int neuronPos = RangeRandomizer.randomInt(start, getNeurons()
.size() - 1);
final NEATNeuronGene neuronGene
= (NEATNeuronGene) this.neuronsChromosome
.get(neuronPos);
return neuronGene;
}
/**
* Convert the genes to an actual network.
*/
public void decode() {
final NEATPattern pattern = new NEATPattern();
final List<NEATNeuron> neurons = pattern.getNeurons();
for (final Gene gene : getNeurons().getGenes()) {
final NEATNeuronGene neuronGene = (NEATNeuronGene) gene;
final NEATNeuron neuron = new NEATNeuron(
neuronGene.getNeuronType(), neuronGene.getId(), neuronGene
.getSplitY(), neuronGene.getSplitX(), neuronGene
.getActivationResponse());
neurons.add(neuron);
}
// now to create the links.
for (final Gene gene : getLinks().getGenes()) {
final NEATLinkGene linkGene = (NEATLinkGene) gene;
if (linkGene.isEnabled()) {
int element = getElementPos(linkGene.getFromNeuronID());
final NEATNeuron fromNeuron = neurons.get(element);
element = getElementPos(linkGene.getToNeuronID());
final NEATNeuron toNeuron = neurons.get(element);
final NEATLink link = new NEATLink(linkGene.getWeight(),
fromNeuron, toNeuron, linkGene.isRecurrent());
fromNeuron.getOutputboundLinks().add(link);
toNeuron.getInboundLinks().add(link);
}
}
pattern
.setNEATActivationFunction(((NEATTraining) getGeneticAlgorithm())
.getNeatActivationFunction());
pattern.setActivationFunction(((NEATTraining) getGeneticAlgorithm())
.getOutputActivationFunction());
pattern.setInputNeurons(this.inputCount);
pattern.setOutputNeurons(this.outputCount);
pattern
.setSnapshot(((NEATTraining) getGeneticAlgorithm())
.isSnapshot());
setOrganism(pattern.generate());
}
/**
* Convert the network to genes. Not currently supported.
*/
public void encode() {
}
/**
* Get the compatibility score with another genome. Used to determine
* species.
*
* @param genome
* The other genome.
* @return The score.
*/
public double getCompatibilityScore(final NEATGenome genome) {
double numDisjoint = 0;
double numExcess = 0;
double numMatched = 0;
double weightDifference = 0;
int g1 = 0;
int g2 = 0;
while ((g1 < this.linksChromosome.size() - 1)
|| (g2 < this.linksChromosome.size() - 1)) {
if (g1 == this.linksChromosome.size() - 1) {
g2++;
numExcess++;
continue;
}
if (g2 == genome.getLinks().size() - 1) {
g1++;
numExcess++;
continue;
}
// get innovation numbers for each gene at this point
final long id1 = ((NEATLinkGene) this.linksChromosome.get(g1))
.getInnovationId();
final long id2 = ((NEATLinkGene) genome.getLinks().get(g2))
.getInnovationId();
// innovation numbers are identical so increase the matched score
if (id1 == id2) {
g1++;
g2++;
numMatched++;
// get the weight difference between these two genes
weightDifference += Math
.abs(((NEATLinkGene) this.linksChromosome.get(g1))
.getWeight()
- ((NEATLinkGene) genome.getLinks().get(g2))
.getWeight());
}
// innovation numbers are different so increment the disjoint score
if (id1 < id2) {
numDisjoint++;
g1++;
}
if (id1 > id2) {
++numDisjoint;
++g2;
}
}
int longest = genome.getNumGenes();
if (getNumGenes() > longest) {
longest = getNumGenes();
}
final double score = (NEATGenome.TWEAK_EXCESS * numExcess / longest)
+ (NEATGenome.TWEAK_DISJOINT * numDisjoint / longest)
+ (NEATGenome.TWEAK_MATCHED * weightDifference / numMatched);
return score;
}
/**
* Get the specified neuron's index.
*
* @param neuronID
* The neuron id to check for.
* @return The index.
*/
private int getElementPos(final long neuronID) {
for (int i = 0; i < getNeurons().size(); i++) {
final NEATNeuronGene neuronGene = (NEATNeuronGene) this.neuronsChromosome
.getGene(i);
if (neuronGene.getId() == neuronID) {
return i;
}
}
return -1;
}
/**
* @return The number of input neurons.
*/
public int getInputCount() {
return this.inputCount;
}
/**
* @return THe links chromosome.
*/
public Chromosome getLinks() {
return this.linksChromosome;
}
/**
* @return The network depth.
*/
public int getNetworkDepth() {
return this.networkDepth;
}
/**
* @return The neurons chromosome.
*/
public Chromosome getNeurons() {
return this.neuronsChromosome;
}
/**
* @return The number of genes in the links chromosome.
*/
public int getNumGenes() {
return this.linksChromosome.size();
}
/**
* @return The output count.
*/
public int getOutputCount() {
return this.outputCount;
}
/**
* @return The species ID.
*/
public long getSpeciesID() {
return this.speciesID;
}
/**
* Get the specified split y.
*
* @param nd
* The neuron.
* @return The split y.
*/
public double getSplitY(final int nd) {
return ((NEATNeuronGene) this.neuronsChromosome.get(nd)).getSplitY();
}
/**
* Determine if this is a duplicate link.
*
* @param fromNeuronID
* The from neuron id.
* @param toNeuronID
* The to neuron id.
* @return True if this is a duplicate link.
*/
public boolean isDuplicateLink(final long fromNeuronID,
final long toNeuronID) {
for (final Gene gene : getLinks().getGenes()) {
final NEATLinkGene linkGene = (NEATLinkGene) gene;
if ((linkGene.getFromNeuronID() == fromNeuronID)
&& (linkGene.getToNeuronID() == toNeuronID)) {
return true;
}
}
return false;
}
/**
* Mutate the activation response.
*
* @param mutateRate
* The mutation rate.
* @param maxPertubation
* The maximum to perturb it by.
*/
public void mutateActivationResponse(final double mutateRate,
final double maxPertubation) {
for (final Gene gene : this.neuronsChromosome.getGenes()) {
if (Math.random() < mutateRate) {
final NEATNeuronGene neuronGene = (NEATNeuronGene) gene;
neuronGene.setActivationResponse(neuronGene
.getActivationResponse()
+ RangeRandomizer.randomize(-1, 1) * maxPertubation);
}
}
}
/**
* Mutate the weights.
*
* @param mutateRate
* The mutation rate.
* @param probNewMutate
* The probability of a whole new weight.
* @param maxPertubation
* The max perturbation.
*/
public void mutateWeights(final double mutateRate,
final double probNewMutate, final double maxPertubation) {
for (final Gene gene : this.linksChromosome.getGenes()) {
final NEATLinkGene linkGene = (NEATLinkGene) gene;
if (Math.random() < mutateRate) {
if (Math.random() < probNewMutate) {
linkGene.setWeight(RangeRandomizer.randomize(-1, 1));
} else {
linkGene
.setWeight(linkGene.getWeight()
+ RangeRandomizer.randomize(-1, 1)
* maxPertubation);
}
}
}
}
/**
* @param networkDepth
* the networkDepth to set
*/
public void setNetworkDepth(final int networkDepth) {
this.networkDepth = networkDepth;
}
/**
* Set the species id.
*
* @param species
* The species id.
*/
public void setSpeciesID(final long species) {
this.speciesID = species;
}
/**
* Sort the genes.
*/
public void sortGenes() {
Collections.sort(this.linksChromosome.getGenes());
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
final StringBuilder result = new StringBuilder();
result.append("[NEATGenome:");
result.append(getGenomeID());
result.append(",fitness=");
result.append(getScore());
result.append(")");
return result.toString();
}
}