/// <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; }
/// <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; }