/* * Encog(tm) Core v3.4 - Java Version * http://www.heatonresearch.com/encog/ * https://github.com/encog/encog-java-core * Copyright 2008-2016 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.freeform; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.encog.Encog; import org.encog.engine.network.activation.ActivationFunction; import org.encog.engine.network.activation.ActivationTANH; import org.encog.mathutil.randomize.ConsistentRandomizer; import org.encog.ml.BasicML; import org.encog.ml.MLClassification; import org.encog.ml.MLContext; import org.encog.ml.MLEncodable; import org.encog.ml.MLError; import org.encog.ml.MLRegression; import org.encog.ml.MLResettable; import org.encog.ml.data.MLData; import org.encog.ml.data.MLDataSet; import org.encog.ml.data.basic.BasicMLData; import org.encog.neural.freeform.basic.BasicActivationSummationFactory; import org.encog.neural.freeform.basic.BasicFreeformConnectionFactory; import org.encog.neural.freeform.basic.BasicFreeformLayerFactory; import org.encog.neural.freeform.basic.BasicFreeformNeuronFactory; import org.encog.neural.freeform.factory.FreeformConnectionFactory; import org.encog.neural.freeform.factory.FreeformLayerFactory; import org.encog.neural.freeform.factory.FreeformNeuronFactory; import org.encog.neural.freeform.factory.InputSummationFactory; import org.encog.neural.freeform.task.ConnectionTask; import org.encog.neural.freeform.task.NeuronTask; import org.encog.neural.networks.BasicNetwork; import org.encog.util.EngineArray; import org.encog.util.obj.ObjectCloner; import org.encog.util.simple.EncogUtility; /** * Implements a freefrom neural network. A freeform neural network can represent * much more advanced structures than the flat networks that the Encog * BasicNetwork implements. However, while freeform networks are more advanced * than the BasicNetwork, they are also much slower. * * Freeform networks allow just about any neuron to be connected to another * neuron. You can have neuron layers if you want, but they are not required. * */ public class FreeformNetwork extends BasicML implements MLContext, Cloneable, MLRegression, MLEncodable, MLResettable, MLClassification, MLError { /** * The serial ID. */ private static final long serialVersionUID = 1L; /** * Construct an Elmann recurrent neural network. * * @param input * The input count. * @param hidden1 * The hidden count. * @param output * The output count. * @param af * The activation function. * @return The newly created network. */ public static FreeformNetwork createElman(final int input, final int hidden1, final int output, final ActivationFunction af) { final FreeformNetwork network = new FreeformNetwork(); final FreeformLayer inputLayer = network.createInputLayer(input); final FreeformLayer hiddenLayer1 = network.createLayer(hidden1); final FreeformLayer outputLayer = network.createOutputLayer(output); network.connectLayers(inputLayer, hiddenLayer1, af, 1.0, false); network.connectLayers(hiddenLayer1, outputLayer, af, 1.0, false); network.createContext(hiddenLayer1, hiddenLayer1); network.reset(); return network; } /** * Create a feedforward freeform neural network. * * @param input * The input count. * @param hidden1 * The first hidden layer count, zero if none. * @param hidden2 * The second hidden layer count, zero if none. * @param output * The output count. * @param af * The activation function. * @return The newly crated network. */ public static FreeformNetwork createFeedforward(final int input, final int hidden1, final int hidden2, final int output, final ActivationFunction af) { final FreeformNetwork network = new FreeformNetwork(); FreeformLayer lastLayer = network.createInputLayer(input); FreeformLayer currentLayer; if (hidden1 > 0) { currentLayer = network.createLayer(hidden1); network.connectLayers(lastLayer, currentLayer, af, 1.0, false); lastLayer = currentLayer; } if (hidden2 > 0) { currentLayer = network.createLayer(hidden2); network.connectLayers(lastLayer, currentLayer, af, 1.0, false); lastLayer = currentLayer; } currentLayer = network.createOutputLayer(output); network.connectLayers(lastLayer, currentLayer, af, 1.0, false); network.reset(); return network; } /** * The input layer. */ private FreeformLayer inputLayer; /** * The output layer. */ private FreeformLayer outputLayer; /** * The connection factory. */ private final FreeformConnectionFactory connectionFactory = new BasicFreeformConnectionFactory(); /** * The layer factory. */ private final FreeformLayerFactory layerFactory = new BasicFreeformLayerFactory(); /** * The neuron factory. */ private final FreeformNeuronFactory neuronFactory = new BasicFreeformNeuronFactory(); /** * The input summation factory. */ private final InputSummationFactory summationFactory = new BasicActivationSummationFactory(); /** * Default constructor. Typically should not be directly used. */ public FreeformNetwork() { } /** * Craete a freeform network from a basic network. * * @param network * The basic network to use. */ public FreeformNetwork(final BasicNetwork network) { if (network.getLayerCount() < 2) { throw new FreeformNetworkError( "The BasicNetwork must have at least two layers to be converted."); } // handle each layer FreeformLayer previousLayer = null; FreeformLayer currentLayer; for (int currentLayerIndex = 0; currentLayerIndex < network .getLayerCount(); currentLayerIndex++) { // create the layer currentLayer = this.layerFactory.factor(); // Is this the input layer? if (this.inputLayer == null) { this.inputLayer = currentLayer; } // Add the neurons for this layer for (int i = 0; i < network.getLayerNeuronCount(currentLayerIndex); i++) { // obtain the summation object. InputSummation summation = null; if (previousLayer != null) { summation = this.summationFactory.factor(network .getActivation(currentLayerIndex)); } // add the new neuron currentLayer.add(this.neuronFactory.factorRegular(summation)); } // Fully connect this layer to previous if (previousLayer != null) { connectLayersFromBasic(network, currentLayerIndex - 1, previousLayer, currentLayerIndex, currentLayer, currentLayerIndex, false); } // Add the bias neuron // The bias is added after connections so it has no inputs if (network.isLayerBiased(currentLayerIndex)) { final FreeformNeuron biasNeuron = this.neuronFactory .factorRegular(null); biasNeuron.setBias(true); biasNeuron.setActivation(network .getLayerBiasActivation(currentLayerIndex)); currentLayer.add(biasNeuron); } // update previous layer previousLayer = currentLayer; currentLayer = null; } // finally, set the output layer. this.outputLayer = previousLayer; } /** * {@inheritDoc} */ @Override public double calculateError(final MLDataSet data) { return EncogUtility.calculateRegressionError(this, data); } /** * {@inheritDoc} */ @Override public int classify(final MLData input) { final MLData output = compute(input); return EngineArray.maxIndex(output.getData()); } /** * {@inheritDoc} */ @Override public void clearContext() { performNeuronTask(new NeuronTask() { @Override public void task(final FreeformNeuron neuron) { if (neuron instanceof FreeformContextNeuron) { neuron.setActivation(0); } } }); } /** * Return a clone of this neural network. Including structure, weights and * bias values. This is a deep copy. * * @return A cloned copy of the neural network. */ @Override public Object clone() { final BasicNetwork result = (BasicNetwork) ObjectCloner.deepCopy(this); return result; } /** * {@inheritDoc} */ @Override public MLData compute(final MLData input) { // Allocate result final MLData result = new BasicMLData(this.outputLayer.size()); // Copy the input for (int i = 0; i < input.size(); i++) { this.inputLayer.setActivation(i, input.getData(i)); } // Request calculation of outputs for (int i = 0; i < this.outputLayer.size(); i++) { final FreeformNeuron outputNeuron = this.outputLayer.getNeurons() .get(i); outputNeuron.performCalculation(); result.setData(i, outputNeuron.getActivation()); } updateContext(); return result; } /** * Connect two layers. These layers will be connected with a TANH activation * function in a non-recurrent way. A bias activation of 1.0 will be used, * if needed. * * @param source * The source layer. * @param target * The target layer. */ public void connectLayers(final FreeformLayer source, final FreeformLayer target) { connectLayers(source, target, new ActivationTANH(), 1.0, false); } /** * Connect two layers. * * @param source * The source layer. * @param target * The target layer. * @param theActivationFunction * The activation function to use. * @param biasActivation * The bias activation to use. * @param isRecurrent * True, if this is a recurrent connection. */ public void connectLayers(final FreeformLayer source, final FreeformLayer target, final ActivationFunction theActivationFunction, final double biasActivation, final boolean isRecurrent) { // create bias, if requested if (biasActivation > Encog.DEFAULT_DOUBLE_EQUAL) { // does the source already have a bias? if (source.hasBias()) { throw new FreeformNetworkError( "The source layer already has a bias neuron, you cannot create a second."); } final FreeformNeuron biasNeuron = this.neuronFactory .factorRegular(null); biasNeuron.setActivation(biasActivation); biasNeuron.setBias(true); source.add(biasNeuron); } // create connections for (final FreeformNeuron targetNeuron : target.getNeurons()) { // create the summation for the target InputSummation summation = targetNeuron.getInputSummation(); // do not create a second input summation if (summation == null) { summation = this.summationFactory.factor(theActivationFunction); targetNeuron.setInputSummation(summation); } // connect the source neurons to the target neuron for (final FreeformNeuron sourceNeuron : source.getNeurons()) { final FreeformConnection connection = this.connectionFactory .factor(sourceNeuron, targetNeuron); sourceNeuron.addOutput(connection); targetNeuron.addInput(connection); } } } /** * Connect two layers, assume bias activation of 1.0 and non-recurrent * connection. * * @param source * The source layer. * @param target * The target layer. * @param theActivationFunction * The activation function. */ public void ConnectLayers(final FreeformLayer source, final FreeformLayer target, final ActivationFunction theActivationFunction) { connectLayers(source, target, theActivationFunction, 1.0, false); } /** * Connect layers from a BasicNetwork. Used internally only. * * @param network * The BasicNetwork. * @param fromLayerIdx * The from layer index. * @param source * The from layer. * @param sourceIdx * The source index. * @param target * The target. * @param targetIdx * The target index. * @param isRecurrent * True, if this is recurrent. */ private void connectLayersFromBasic(final BasicNetwork network, final int fromLayerIdx, final FreeformLayer source, final int sourceIdx, final FreeformLayer target, final int targetIdx, final boolean isRecurrent) { for (int targetNeuronIdx = 0; targetNeuronIdx < target.size(); targetNeuronIdx++) { for (int sourceNeuronIdx = 0; sourceNeuronIdx < source.size(); sourceNeuronIdx++) { final FreeformNeuron sourceNeuron = source.getNeurons().get( sourceNeuronIdx); final FreeformNeuron targetNeuron = target.getNeurons().get( targetNeuronIdx); // neurons with no input (i.e. bias neurons) if (targetNeuron.getInputSummation() == null) { continue; } final FreeformConnection connection = this.connectionFactory .factor(sourceNeuron, targetNeuron); sourceNeuron.addOutput(connection); targetNeuron.addInput(connection); final double weight = network.getWeight(fromLayerIdx, sourceNeuronIdx, targetNeuronIdx); connection.setWeight(weight); } } } /** * Create a context connection, such as those used by Jordan/Elmann. * * @param source * The source layer. * @param target * The target layer. * @return The newly created context layer. */ public FreeformLayer createContext(final FreeformLayer source, final FreeformLayer target) { final double biasActivation = 0.0; ActivationFunction activatonFunction = null; if (source.getNeurons().get(0).getOutputs().size() < 1) { throw new FreeformNetworkError( "A layer cannot have a context layer connected if there are no other outbound connections from the source layer. Please connect the source layer somewhere else first."); } activatonFunction = source.getNeurons().get(0).getInputSummation() .getActivationFunction(); // first create the context layer final FreeformLayer result = this.layerFactory.factor(); for (int i = 0; i < source.size(); i++) { final FreeformNeuron neuron = source.getNeurons().get(i); if (neuron.isBias()) { final FreeformNeuron biasNeuron = this.neuronFactory .factorRegular(null); biasNeuron.setBias(true); biasNeuron.setActivation(neuron.getActivation()); result.add(biasNeuron); } else { final FreeformNeuron contextNeuron = this.neuronFactory .factorContext(neuron); result.add(contextNeuron); } } // now connect the context layer to the target layer connectLayers(result, target, activatonFunction, biasActivation, false); return result; } /** * Create the input layer. * * @param neuronCount * The input neuron count. * @return The newly created layer. */ public FreeformLayer createInputLayer(final int neuronCount) { if (neuronCount < 1) { throw new FreeformNetworkError( "Input layer must have at least one neuron."); } this.inputLayer = createLayer(neuronCount); return this.inputLayer; } /** * Create a hidden layer. * * @param neuronCount * The neuron count. * @return The newly created layer. */ public FreeformLayer createLayer(final int neuronCount) { if (neuronCount < 1) { throw new FreeformNetworkError( "Layer must have at least one neuron."); } final FreeformLayer result = this.layerFactory.factor(); // Add the neurons for this layer for (int i = 0; i < neuronCount; i++) { result.add(this.neuronFactory.factorRegular(null)); } return result; } /** * Create the output layer. * * @param neuronCount * The neuron count. * @return The newly created output layer. */ public FreeformLayer createOutputLayer(final int neuronCount) { if (neuronCount < 1) { throw new FreeformNetworkError( "Output layer must have at least one neuron."); } this.outputLayer = createLayer(neuronCount); return this.outputLayer; } /** * {@inheritDoc} */ @Override public void decodeFromArray(final double[] encoded) { int index = 0; final Set<FreeformNeuron> visited = new HashSet<FreeformNeuron>(); final List<FreeformNeuron> queue = new ArrayList<FreeformNeuron>(); // first copy outputs to queue for (final FreeformNeuron neuron : this.outputLayer.getNeurons()) { queue.add(neuron); } while (queue.size() > 0) { // pop a neuron off the queue final FreeformNeuron neuron = queue.get(0); queue.remove(0); visited.add(neuron); // find anymore neurons and add them to the queue. if (neuron.getInputSummation() != null) { for (final FreeformConnection connection : neuron .getInputSummation().list()) { connection.setWeight(encoded[index++]); final FreeformNeuron nextNeuron = connection.getSource(); if (!visited.contains(nextNeuron)) { queue.add(nextNeuron); } } } } } /** * {@inheritDoc} */ @Override public int encodedArrayLength() { int result = 0; final Set<FreeformNeuron> visited = new HashSet<FreeformNeuron>(); final List<FreeformNeuron> queue = new ArrayList<FreeformNeuron>(); // first copy outputs to queue for (final FreeformNeuron neuron : this.outputLayer.getNeurons()) { queue.add(neuron); } while (queue.size() > 0) { // pop a neuron off the queue final FreeformNeuron neuron = queue.get(0); queue.remove(0); visited.add(neuron); // find anymore neurons and add them to the queue. if (neuron.getInputSummation() != null) { for (final FreeformConnection connection : neuron .getInputSummation().list()) { result++; final FreeformNeuron nextNeuron = connection.getSource(); if (!visited.contains(nextNeuron)) { queue.add(nextNeuron); } } } } return result; } /** * {@inheritDoc} */ @Override public void encodeToArray(final double[] encoded) { int index = 0; final Set<FreeformNeuron> visited = new HashSet<FreeformNeuron>(); final List<FreeformNeuron> queue = new ArrayList<FreeformNeuron>(); // first copy outputs to queue for (final FreeformNeuron neuron : this.outputLayer.getNeurons()) { queue.add(neuron); } while (queue.size() > 0) { // pop a neuron off the queue final FreeformNeuron neuron = queue.get(0); queue.remove(0); visited.add(neuron); // find anymore neurons and add them to the queue. if (neuron.getInputSummation() != null) { for (final FreeformConnection connection : neuron .getInputSummation().list()) { encoded[index++] = connection.getWeight(); final FreeformNeuron nextNeuron = connection.getSource(); if (!visited.contains(nextNeuron)) { queue.add(nextNeuron); } } } } } /** * {@inheritDoc} */ @Override public int getInputCount() { return this.inputLayer.sizeNonBias(); } /** * {@inheritDoc} */ @Override public int getOutputCount() { return this.outputLayer.sizeNonBias(); } /** * @return The output layer. */ public FreeformLayer getOutputLayer() { return this.outputLayer; } /** * Perform the specified connection task. This task will be performed over * all connections. * * @param task * The connection task. */ public void performConnectionTask(final ConnectionTask task) { final Set<FreeformNeuron> visited = new HashSet<FreeformNeuron>(); for (final FreeformNeuron neuron : this.outputLayer.getNeurons()) { performConnectionTask(visited, neuron, task); } } /** * Perform the specified connection task. * * @param visited * The list of visited neurons. * @param parentNeuron * The parent neuron. * @param task * The task. */ private void performConnectionTask(final Set<FreeformNeuron> visited, final FreeformNeuron parentNeuron, final ConnectionTask task) { visited.add(parentNeuron); // does this neuron have any inputs? if (parentNeuron.getInputSummation() != null) { // visit the inputs for (final FreeformConnection connection : parentNeuron .getInputSummation().list()) { task.task(connection); final FreeformNeuron neuron = connection.getSource(); // have we already visited this neuron? if (!visited.contains(neuron)) { performConnectionTask(visited, neuron, task); } } } } /** * Perform the specified neuron task. This task will be executed over all * neurons. * * @param task The neuron task to perform. */ public void performNeuronTask(final NeuronTask task) { final Set<FreeformNeuron> visited = new HashSet<FreeformNeuron>(); for (final FreeformNeuron neuron : this.outputLayer.getNeurons()) { performNeuronTask(visited, neuron, task); } } /** * Perform the specified neuron task. * @param visited The visited list. * @param parentNeuron The neuron to start with. * @param task The task to perform. */ private void performNeuronTask(final Set<FreeformNeuron> visited, final FreeformNeuron parentNeuron, final NeuronTask task) { visited.add(parentNeuron); task.task(parentNeuron); // does this neuron have any inputs? if (parentNeuron.getInputSummation() != null) { // visit the inputs for (final FreeformConnection connection : parentNeuron .getInputSummation().list()) { final FreeformNeuron neuron = connection.getSource(); // have we already visited this neuron? if (!visited.contains(neuron)) { performNeuronTask(visited, neuron, task); } } } } /** * {@inheritDoc} */ @Override public void reset() { reset((int) (System.currentTimeMillis() % Integer.MAX_VALUE)); } /** * {@inheritDoc} */ @Override public void reset(final int seed) { final ConsistentRandomizer randomizer = new ConsistentRandomizer(-1, 1, seed); /** * {@inheritDoc} */ performConnectionTask(new ConnectionTask() { @Override public void task(final FreeformConnection connection) { connection.setWeight(randomizer.nextDouble()); } }); } /** * Allocate temp training space. * @param neuronSize The number of elements to allocate on each neuron. * @param connectionSize The number of elements to allocate on each connection. */ public void tempTrainingAllocate(final int neuronSize, final int connectionSize) { performNeuronTask(new NeuronTask() { @Override public void task(final FreeformNeuron neuron) { neuron.allocateTempTraining(neuronSize); if (neuron.getInputSummation() != null) { for (final FreeformConnection connection : neuron .getInputSummation().list()) { connection.allocateTempTraining(connectionSize); } } } }); } /** * Clear the temp training data. */ public void tempTrainingClear() { performNeuronTask(new NeuronTask() { @Override public void task(final FreeformNeuron neuron) { neuron.clearTempTraining(); if (neuron.getInputSummation() != null) { for (final FreeformConnection connection : neuron .getInputSummation().list()) { connection.clearTempTraining(); } } } }); } /** * Update context. */ public void updateContext() { performNeuronTask(new NeuronTask() { @Override public void task(final FreeformNeuron neuron) { neuron.updateContext(); } }); } /** * {@inheritDoc} */ @Override public void updateProperties() { // not needed } }