Working data struct for use in FastCyclicNetwork and sub-classes. Represents a single connection - its weight and source/target neurons.
Пример #1
0
        /// <summary>
        /// Constructs a FastCyclicNetwork with the provided pre-built FastConnection array and 
        /// associated data.
        /// </summary>
        public FastCyclicNetwork(FastConnection[] connectionArray,
                                 IActivationFunction[] neuronActivationFnArray,
                                 double[][] neuronAuxArgsArray,
                                 int neuronCount,
                                 int inputNeuronCount,
                                 int outputNeuronCount,
                                 int timestepsPerActivation)
        {
            _connectionArray = connectionArray;
            _neuronActivationFnArray = neuronActivationFnArray;
            _neuronAuxArgsArray = neuronAuxArgsArray;

            // Create neuron pre- and post-activation signal arrays.
            _preActivationArray = new double[neuronCount];
            _postActivationArray = new double[neuronCount];

            // Wrap sub-ranges of the neuron signal arrays as input and output arrays for IBlackBox.
            // Offset is 1 to skip bias neuron (The value at index 1 is the first black box input).
            _inputSignalArrayWrapper = new SignalArray(_postActivationArray, 1, inputNeuronCount);

            // Offset to skip bias and input neurons. Output neurons follow input neurons in the arrays.
            _outputSignalArrayWrapper = new SignalArray(_postActivationArray, inputNeuronCount+1, outputNeuronCount);

            // Store counts for use during activation.
            _inputNeuronCount = inputNeuronCount;
            _inputAndBiasNeuronCount = inputNeuronCount+1;
            _outputNeuronCount = outputNeuronCount;
            _timestepsPerActivation = timestepsPerActivation;

            // Initialise the bias neuron's fixed output value.
            _postActivationArray[0] = 1.0;
        }
Пример #2
0
        /// <summary>
        /// For saving and vizualization, we somtimes need to change the genome
        /// BackProp for instance, happens on the phenotype and so if we want to see/save those changes
        /// we need to send the data back to NeatGenome
        /// </summary>
        /// <param name="genomeToWrite">The genome whose weights are being changed</param>
        /// <param name="connections">we set the genome's weights to the weights of these connections</param>
        /// <returns></returns>
        public static NeatGenome ChangeGenomeWeights(NeatGenome genomeToWrite, FastConnection[] connections)
        {
            IConnectionList connectionList = genomeToWrite.ConnectionList;
            int connectionCount = connectionList.Count;

            for (int i = 0; i < connectionCount; i++)
            {
                genomeToWrite.ConnectionGeneList[i].Weight = connections[i]._weight;
            }
            return genomeToWrite;
        }
        /// <summary>
        ///     Calculates the output error for each node in the target layer and all hidden layers.  Note that this is a wrapper
        ///     method which takes a signal array, converts it to a double array, and passes that on to the method below.
        /// </summary>
        /// <param name="layers">The discrete layers in the ANN.</param>
        /// <param name="connections">Array of all connections in the ANN.</param>
        /// <param name="nodeActivationValues">The neuron activation values resulting from the last forward pass.</param>
        /// <param name="targetValues">The target values against which the network is being trained.</param>
        /// <param name="nodeActivationFunctions">The activation function for each neuron (this will only differ with HyperNEAT).</param>
        /// <returns>The errors for each output and hidden neuron.</returns>
        public static double[] CalculateErrorSignals(LayerInfo[] layers, FastConnection[] connections,
            double[] nodeActivationValues, ISignalArray outputValues, ISignalArray targetValues, IActivationFunction[] nodeActivationFunctions)
        {
            double[] targets = new double[targetValues.Length];

            // Load the target double array from the input signal array
            targetValues.CopyTo(targets, 0);

            // Return the error signals
            return CalculateErrorSignals(layers, connections, nodeActivationValues, (MappingSignalArray) outputValues, targets, nodeActivationFunctions);
        }
 /// <summary>
 /// Constructs a FastRelaxingCyclicNetwork with the provided pre-built FastConnection array and 
 /// associated data.
 /// </summary>
 public FastRelaxingCyclicNetwork(FastConnection[] connectionArray,
                                  IActivationFunction[] neuronActivationFnArray,
                                  double[][] neuronAuxArgsArray,
                                  int neuronCount,
                                  int inputNeuronCount,
                                  int outputNeuronCount,
                                  int maxTimesteps,
                                  double signalDeltaThreshold)
     : base(connectionArray, neuronActivationFnArray, neuronAuxArgsArray,
            neuronCount, inputNeuronCount, outputNeuronCount,
            maxTimesteps)
 {
     _signalDeltaThreshold = signalDeltaThreshold;
 }
        private static void InternalDecode(INetworkDefinition networkDef,
                                           int timestepsPerActivation,
                                           out FastConnection[] fastConnectionArray,
                                           out IActivationFunction[] activationFnArray,
                                           out double[][] neuronAuxArgsArray)
        {
            // Create an array of FastConnection(s) that represent the connectivity of the network.
            fastConnectionArray = CreateFastConnectionArray(networkDef);

            // TODO: Test/optimize heuristic - this is just back of envelope maths.
            // A rough heuristic to decide if we should sort fastConnectionArray by source neuron index.
            // The principle here is that each activation loop will be about 2x faster (unconfirmed) if we sort 
            // fastConnectionArray, but sorting takes about n*log2(n) operations. Therefore the decision to sort
            // depends on length of fastConnectionArray and _timestepsPerActivation.
            // Another factor here is that small networks will fit into CPU caches and therefore will not appear
            // to speed up - however the unsorted data will 'scramble' CPU caches more than they otherwise would 
            // have and thus may slow down other threads (so we just keep it simple).
            double len = fastConnectionArray.Length;
            double timesteps = timestepsPerActivation;
            if((len > 2) && (((len * Math.Log(len,2)) + ((timesteps * len)/2.0)) < (timesteps * len)))
            {   // Sort fastConnectionArray by source neuron index.
                Array.Sort(fastConnectionArray, delegate(FastConnection x, FastConnection y)
                {   // Use simple/fast diff method.
                    return x._srcNeuronIdx - y._srcNeuronIdx;
                });
            }

            // Construct an array of neuron activation functions. Skip bias and input neurons as
            // these don't have an activation function (because they aren't activated).
            INodeList nodeList = networkDef.NodeList;
            int nodeCount = nodeList.Count;
            IActivationFunctionLibrary activationFnLibrary = networkDef.ActivationFnLibrary;
            activationFnArray = new IActivationFunction[nodeCount];
            neuronAuxArgsArray = new double[nodeCount][];

            for(int i=0; i<nodeCount; i++) {
                activationFnArray[i] = activationFnLibrary.GetFunction(nodeList[i].ActivationFnId);
                neuronAuxArgsArray[i] = nodeList[i].AuxState;
            }
        }
        /// <summary>
        ///     Updates weights based on node error calculations using a given learning rate (momentum isn't taken into
        ///     consideration here).
        /// </summary>
        /// <param name="layers">The discrete layers in the ANN.</param>
        /// <param name="connections">Array of all connections in the ANN.</param>
        /// <param name="learningRate">The learning rate for all connections.</param>
        /// <param name="signalErrors">The errors for each output and hidden neuron.</param>
        /// <param name="nodeActivationValues">The activation function for each neuron (this will only differ with HyperNEAT).</param>
        public static void BackpropagateError(LayerInfo[] layers, FastConnection[] connections, double learningRate,
            double[] signalErrors, double[] nodeActivationValues, MappingSignalArray outputArray)
        {
            int conIdx = 0;

            // Iterate through every layer in a forward pass, calculating the new weights on each connection
            for (int layerIdx = 1; layerIdx < layers.Length; layerIdx++)
            {
                // Start at one layer below the current layer so we can access the source nodes
                LayerInfo layerInfo = layers[layerIdx - 1];
                // Handle the output layer as a special case, calculating the error against the given target
                if (layerIdx == layers.Length - 1)
                {
                    // Calculate the new weight for every connection in the current layer up to the last (i.e. "end")
                    // connection by adding its current weight to the product of the learning rate, target neuron error,
                    // and source neuron output
                    for (; conIdx < layerInfo._endConnectionIdx; conIdx++)
                    {
                        //double sigError = signalErrors[outputArray._map[connections[conIdx]._tgtNeuronIdx]];
                        double sigError = signalErrors[connections[conIdx]._tgtNeuronIdx];
                        connections[conIdx]._weight = connections[conIdx]._weight +
                                                      learningRate * sigError *
                                                      nodeActivationValues[connections[conIdx]._srcNeuronIdx];
                    }
                }
                else
                {
                    // Calculate the new weight for every connection in the current layer up to the last (i.e. "end")
                    // connection by adding its current weight to the product of the learning rate, target neuron error,
                    // and source neuron output
                    for (; conIdx < layerInfo._endConnectionIdx; conIdx++)
                    {
                        connections[conIdx]._weight = connections[conIdx]._weight +
                                                      learningRate * signalErrors[connections[conIdx]._tgtNeuronIdx] *
                                                      nodeActivationValues[connections[conIdx]._srcNeuronIdx];
                    }
                }
            }
        }
        /// <summary>
        ///     Calculates the output error for each node in the target layer and all hidden layers.
        /// </summary>
        /// <param name="layers">The discrete layers in the ANN.</param>
        /// <param name="connections">Array of all connections in the ANN.</param>
        /// <param name="nodeActivationValues">The neuron activation values resulting from the last forward pass.</param>
        /// <param name="targetValues">The target values against which the network is being trained.</param>
        /// <param name="nodeActivationFunctions">The activation function for each neuron (this will only differ with HyperNEAT).</param>
        /// <returns>The errors for each output and hidden neuron.</returns>
        public static double[] CalculateErrorSignals(LayerInfo[] layers, FastConnection[] connections,
            double[] nodeActivationValues, MappingSignalArray outputValues, double[] targetValues, IActivationFunction[] nodeActivationFunctions)
        {
            double[] signalErrors = new double[nodeActivationValues.Length];

            // Get the last connection
            int conIdx = connections.Length - 1;

            // Get the last of the output nodes
            int nodeIdx = nodeActivationValues.Length - 1;

            // Iterate through the layers in reverse, calculating the signal errors
            for (int layerIdx = layers.Length - 1; layerIdx > 0; layerIdx--)
            {
                // Handle the output layer as a special case, calculating the error against the given target
                if (layerIdx == layers.Length - 1)
                {
                    // Calculate the error for every output node with respect to its corresponding target value
                    for (; nodeIdx >= layers[layerIdx - 1]._endNodeIdx; nodeIdx--)
                    {

                        //int targetValuesID = outputValues._map[nodeIdx - outputValues.Length - 1] - outputValues.Length - 1;
                        int targetValuesID = 0;
                        for (int i = 0; i < targetValues.Length; i++)
                        {
                            if (outputValues._map[i] == nodeIdx)
                            {
                                targetValuesID = i;
                                break;
                            }
                        }
                        signalErrors[nodeIdx] =
                            (targetValues[targetValuesID] -
                             nodeActivationValues[nodeIdx])*
                            nodeActivationFunctions[nodeIdx].CalculateDerivative(
                                nodeActivationValues[nodeIdx]);
                        /*
                        int targetValuesID = (targetValues.Length - 1) - ((nodeActivationValues.Length - 1) - nodeIdx);
                            signalErrors[nodeIdx] =
                                (targetValues[targetValuesID] -
                                 outputValues[nodeIdx - layers[layerIdx - 1]._endNodeIdx])*
                                nodeActivationFunctions[nodeIdx].CalculateDerivative(
                                    outputValues[nodeIdx - layers[layerIdx - 1]._endNodeIdx]);
                        */
                    }
                }

                // Otherwise, we're on a hidden layer, so just compute the error with respect to the target
                // node's error in the layer above
                else
                {
                    // Calculate the error for each hidden node with respect to the error of the
                    // target node(s) of the next layer
                    for (; nodeIdx >= layers[layerIdx - 1]._endNodeIdx; nodeIdx--)
                    {
                        double deltas = 0;

                        // Calculate the sum of the products of the target node error and connection weight
                        while (connections[conIdx]._srcNeuronIdx == nodeIdx)
                        {
                            deltas += connections[conIdx]._weight*
                                      signalErrors[connections[conIdx]._tgtNeuronIdx];
                            conIdx--;
                        }

                        // The output error for the hidden node is the then the sum of the errors
                        // plus the derivative of the activation function with respect to the output
                        signalErrors[nodeIdx] = deltas*
                                                nodeActivationFunctions[nodeIdx].CalculateDerivative(
                                                    nodeActivationValues[nodeIdx]);
                    }
                }
            }

            return signalErrors;
        }
        /// <summary>
        /// Construct a FastAcyclicNetwork with provided network definition data structures.
        /// </summary>
        /// <param name="nodeActivationFnArr">Array of neuron activation functions.</param>
        /// <param name="nodeAuxArgsArr">Array of neuron activation function arguments.</param>
        /// <param name="connectionArr">Array of connections.</param>
        /// <param name="layerInfoArr">Array of layer information.</param>
        /// <param name="outputNodeIdxArr">An array that specifies the index of each output neuron within _activationArr.
        /// This is necessary because the neurons have been sorted by their depth in the network structure and are therefore
        /// no longer in their original positions. Note however that the bias and input neurons *are* in their original 
        /// positions as they are defined as being at depth zero.</param>
        /// <param name="nodeCount">Number of nodes in the network.</param>
        /// <param name="inputNodeCount">Number of input nodes in the network.</param>
        /// <param name="outputNodeCount">Number of output nodes in the network.</param>
        public FastAcyclicNetwork(IActivationFunction[] nodeActivationFnArr,
                                  double[][] nodeAuxArgsArr,
                                  FastConnection[] connectionArr,
                                  LayerInfo[] layerInfoArr,
                                  int[] outputNodeIdxArr,
                                  int nodeCount,
                                  int inputNodeCount,
                                  int outputNodeCount)
        {
            // Store refs to network structrue data.
            _nodeActivationFnArr = nodeActivationFnArr;
            _nodeAuxArgsArr = nodeAuxArgsArr;
            _connectionArr = connectionArr;
            _layerInfoArr = layerInfoArr;

            // Create working array for node activation signals.
            _activationArr = new double[nodeCount];

            // Wrap a sub-range of the _activationArr that holds the activation values for the input nodes.
            // Offset is 1 to skip bias neuron (The value at index 1 is the first black box input).
            _inputSignalArrayWrapper = new SignalArray(_activationArr, 1, inputNodeCount);

            // Wrap the output nodes. Nodes have been sorted by depth within the network therefore the output
            // nodes can no longer be guaranteed to be in a contiguous segment at a fixed location. As such their
            // positions are indicated by outputNodeIdxArr, and so we package up this array with the node signal
            // array to abstract away the level of indirection described by outputNodeIdxArr.
            _outputSignalArrayWrapper = new MappingSignalArray(_activationArr, outputNodeIdxArr);

            // Store counts for use during activation.
            _inputNodeCount = inputNodeCount;
            _inputAndBiasNodeCount = inputNodeCount+1;
            _outputNodeCount = outputNodeCount;

            // Initialise the bias neuron's fixed output value.
            _activationArr[0] = 1.0;
        }
        /// <summary>
        /// Create an array of FastConnection(s) representing the connectivity of the provided INetworkDefinition.
        /// </summary>
        private static FastConnection[] CreateFastConnectionArray(INetworkDefinition networkDef)
        {
            // We vary the decode logic depending on the size of the genome. The most CPU intensive aspect of
            // decoding is the conversion of the neuron IDs at connection endpoints into neuron indexes. For small
            // genomes we simply use the BinarySearch() method on NeuronGeneList for each lookup; Each lookup is
            // an operation with O(log n) time complexity. Thus for C connections and N neurons the number of operations
            // to perform all lookups is approximately = 2*C*Log(N)
            //
            // For larger genomes we invest time in building a Dictionary that maps neuron IDs to their indexes, this on the
            // basis that the time invested will be more than recovered in time saved performing lookups; The time complexity
            // of a dictionary lookup is near constant O(1). Thus number of operations is approximately = O(2*C*1) + the time
            // required to build the dictionary which is approximately O(N).
            //
            // Therefore the choice of lookup type is based on which of these two expressions gives the lowest value. 
            //
            //      Binary search.      LookupOps = 2 * C * Log2(N) * x
            //      Dictionary Search.  LookupOps = (N * y) + (2 * C * z)
            //
            // Where x, y and z are constants that adjust for the relative speeds of the lookup and dictionary building operations.
            // Note that the actual time required to perform these separate algorithms is actually a far more complex problem, and 
            // for modern CPUs exact times cannot be calculated because of large memory caches and superscalar architecture that
            // makes execution times in a real environment effectively non-deterministic. Thus these calculations are a rough 
            // guide/heuristic that estimate which algorithm will perform best. The constants can be found experimentally but will
            // tend to vary depending on factors such as CPU and memory architecture, .Net framework version and what other tasks the
            // CPU is currently doing which may affect our utilisation of memory caches.
            // TODO: Experimentally determine reasonably good values for constants x,y and z in some common real world runtime platform.
            IConnectionList connectionList = networkDef.ConnectionList;
            INodeList nodeList = networkDef.NodeList;
            int connectionCount = connectionList.Count;
            int nodeCount = nodeList.Count;

            FastConnection[] fastConnectionArray = new FastConnection[connectionCount];

            if((2.0 * connectionCount * Math.Log(nodeCount, 2.0)) < ((2.0 * connectionCount) + nodeCount))
            {
                // Binary search requires items to be sorted.
                Debug.Assert(nodeList.IsSorted());

                // Loop the connections and lookup the neuron IDs for each connection's end points using a binary search
                // on nGeneList. This is probably the quickest approach for small numbers of lookups.
                for(int i=0; i<connectionCount; i++)
                {   
                    INetworkConnection conn = connectionList[i];
                    fastConnectionArray[i]._srcNeuronIdx = nodeList.BinarySearch(conn.SourceNodeId);
                    fastConnectionArray[i]._tgtNeuronIdx = nodeList.BinarySearch(conn.TargetNodeId);
                    fastConnectionArray[i]._weight = conn.Weight;

                    // Check that the correct neuron indexes were found.
                    Debug.Assert( 
                           nodeList[fastConnectionArray[i]._srcNeuronIdx].Id == conn.SourceNodeId
                        && nodeList[fastConnectionArray[i]._tgtNeuronIdx].Id == conn.TargetNodeId);
                }
            }
            else
            {
                // Build dictionary of neuron indexes keyed on neuron innovation ID.
                Dictionary<uint,int> neuronIndexDictionary = new Dictionary<uint,int>(nodeCount);
                for(int i=0; i<nodeCount; i++) {
                    // ENHANCEMENT: Check if neuron innovation ID requires further manipulation to make a good hash code.
                    neuronIndexDictionary.Add(nodeList[i].Id, i);
                }

                // Loop the connections and lookup the neuron IDs for each connection's end points using neuronIndexDictionary.
                // This is probably the quickest approach for large numbers of lookups.
                for(int i=0; i<connectionCount; i++)
                {   
                    INetworkConnection conn = connectionList[i];
                    fastConnectionArray[i]._srcNeuronIdx = neuronIndexDictionary[conn.SourceNodeId];
                    fastConnectionArray[i]._tgtNeuronIdx = neuronIndexDictionary[conn.TargetNodeId];
                    fastConnectionArray[i]._weight = conn.Weight;

                    // Check that the correct neuron indexes were found.
                    Debug.Assert( 
                           nodeList[fastConnectionArray[i]._srcNeuronIdx].Id == conn.SourceNodeId
                        && nodeList[fastConnectionArray[i]._tgtNeuronIdx].Id == conn.TargetNodeId);
                }
            }

            return fastConnectionArray;
        }