/// <summary> /// Constructs with the provided input/output node count, activation function library, /// node and connection lists. /// </summary> public NetworkDefinition(int inputNodeCount, int outputNodeCount, IActivationFunctionLibrary activationFnLib, INodeList nodeList, IConnectionList connectionList) { _inputNodeCount = inputNodeCount; _outputNodeCount = outputNodeCount; _activationFnLib = activationFnLib; _nodeList = nodeList; _connectionList = connectionList; _isAcyclic = !CyclicNetworkTest.IsNetworkCyclic(this); }
private static void InternalDecode(INetworkDefinition networkDef, out List <Neuron> neuronList, out List <Connection> connectionList) { // Build a list of neurons. INodeList nodeDefList = networkDef.NodeList; int nodeCount = nodeDefList.Count; neuronList = new List <Neuron>(nodeCount); // A dictionary of neurons keyed on their innovation ID. var neuronDictionary = new Dictionary <uint, Neuron>(nodeCount); // Loop neuron genes. IActivationFunctionLibrary activationFnLib = networkDef.ActivationFnLibrary; for (int i = 0; i < nodeCount; i++) { // Create a Neuron, add it to the neuron list and add an entry into neuronDictionary - // required for next loop. INetworkNode nodeDef = nodeDefList[i]; // Note that we explicitly translate between the two NeuronType enums even though // they define the same types and could therefore be cast from one to the other. // We do this to keep genome and phenome classes completely separated and also to // prevent bugs - e.g. if one of the enums is changed then TranslateNeuronType() will // need to be modified to prevent exceptions at runtime. Otherwise a silent bug may // be introduced. Neuron neuron = new Neuron(nodeDef.Id, nodeDef.NodeType, activationFnLib.GetFunction(nodeDef.ActivationFnId), nodeDef.AuxState); neuronList.Add(neuron); neuronDictionary.Add(nodeDef.Id, neuron); } // Build a list of connections. IConnectionList connectionDefList = networkDef.ConnectionList; int connectionCount = connectionDefList.Count; connectionList = new List <Connection>(connectionCount); // Loop connection genes. for (int i = 0; i < connectionCount; i++) { INetworkConnection connDef = connectionDefList[i]; connectionList.Add( new Connection(neuronDictionary[connDef.SourceNodeId], neuronDictionary[connDef.TargetNodeId], connDef.Weight)); } }
/// <summary> /// Create an IOGraph that represents the structure described by the provided INetworkDefinition. /// </summary> public IOGraph CreateGraph(INetworkDefinition networkDef) { // Perform depth analysis of network. NetworkDepthInfo depthInfo; if (networkDef.IsAcyclic) { AcyclicNetworkDepthAnalysis depthAnalysis = new AcyclicNetworkDepthAnalysis(); depthInfo = depthAnalysis.CalculateNodeDepths(networkDef); } else { CyclicNetworkDepthAnalysis depthAnalysis = new CyclicNetworkDepthAnalysis(); depthInfo = depthAnalysis.CalculateNodeDepths(networkDef); } // Create an IOGraph, allocating storage for the node lists. INodeList nodeList = networkDef.NodeList; int nodeCount = nodeList.Count; int inputCount = networkDef.InputNodeCount + 1; // + to count bias as an input layer node. int outputCount = networkDef.OutputNodeCount; int hiddenCount = nodeCount - (inputCount + outputCount); IOGraph ioGraph = new IOGraph(inputCount, outputCount, hiddenCount, 0f, depthInfo._networkDepth); // We also build a dictionary of nodes keyed by innovation ID. This is used later // to assign connections to nodes. Dictionary <uint, GraphNode> nodeDict = new Dictionary <uint, GraphNode>(nodeCount); // Loop input nodes. int idx = 0; for (int i = 0; i < inputCount; i++, idx++) { // Create node, assign it a tag and add it to the node dictionary and the // input node list of the IOGraph. uint innovationId = nodeList[idx].Id; GraphNode node = new GraphNode(innovationId.ToString()); node.AuxData = CreateGraphNodeAuxData(nodeList[idx]); node.Depth = depthInfo._nodeDepthArr[idx]; nodeDict.Add(innovationId, node); ioGraph.InputNodeList.Add(node); } // Loop output nodes. for (int i = 0; i < outputCount; i++, idx++) { // Create node, assign it a tag and add it to the node dictionary and the // output node list of the IOGraph. uint innovationId = nodeList[idx].Id; GraphNode node = new GraphNode(innovationId.ToString()); node.AuxData = CreateGraphNodeAuxData(nodeList[idx]); node.Depth = depthInfo._nodeDepthArr[idx]; nodeDict.Add(innovationId, node); ioGraph.OutputNodeList.Add(node); } // Loop hidden nodes. for (; idx < nodeCount; idx++) { // Create node, assign it a tag and add it to the node dictionary and the // hidden node list of the IOGraph. uint innovationId = nodeList[idx].Id; GraphNode node = new GraphNode(innovationId.ToString()); node.AuxData = CreateGraphNodeAuxData(nodeList[idx]); node.Depth = depthInfo._nodeDepthArr[idx]; nodeDict.Add(innovationId, node); ioGraph.HiddenNodeList.Add(node); } // Loop connections. Build GraphConnection objects and connect them to their source // and target nodes. double maxAbsWeight = 0.1; // Start at a non-zero value to prevent possibility of a divide by zero occuring. IConnectionList connectionList = networkDef.ConnectionList; int connCount = connectionList.Count; for (int i = 0; i < connCount; i++) { // Create connection object and assign its source and target nodes. INetworkConnection connection = connectionList[i]; GraphNode sourceNode = nodeDict[connection.SourceNodeId]; GraphNode targetNode = nodeDict[connection.TargetNodeId]; GraphConnection conn = new GraphConnection(sourceNode, targetNode, (float)connection.Weight); // Add the connection to the connection lists on the source and target nodes. sourceNode.OutConnectionList.Add(conn); targetNode.InConnectionList.Add(conn); // Track weight range oevr all connections. double absWeight = Math.Abs(connection.Weight); if (absWeight > maxAbsWeight) { maxAbsWeight = absWeight; } } ioGraph.ConnectionWeightRange = (float)maxAbsWeight; return(ioGraph); }
/// <summary> /// Creates a AcyclicNetwork from an INetworkDefinition. /// </summary> public static FastAcyclicNetwork CreateFastAcyclicNetwork(INetworkDefinition networkDef) { Debug.Assert(!CyclicNetworkTest.IsNetworkCyclic(networkDef), "Attempt to decode a cyclic network into a FastAcyclicNetwork."); // Determine the depth of each node in the network. // Node depths are used to separate the nodes into depth based layers, these layers can then be // used to determine the order in which signals are propagated through the network. AcyclicNetworkDepthAnalysis depthAnalysis = new AcyclicNetworkDepthAnalysis(); NetworkDepthInfo netDepthInfo = depthAnalysis.CalculateNodeDepths(networkDef); // Construct an array of NodeInfo, ordered by node depth. // Create/populate NodeInfo array. int[] nodeDepthArr = netDepthInfo._nodeDepthArr; INodeList nodeList = networkDef.NodeList; int nodeCount = nodeList.Count; NodeInfo[] nodeInfoByDepth = new NodeInfo[nodeCount]; for (int i = 0; i < nodeCount; i++) { nodeInfoByDepth[i]._nodeId = nodeList[i].Id; nodeInfoByDepth[i]._definitionIdx = i; nodeInfoByDepth[i]._nodeDepth = nodeDepthArr[i]; } // Sort NodeInfo array. // We use an IComparer here because an anonymous method is not accepted on the method overload that accepts // a sort range, which we use to avoid sorting the input and bias nodes. Sort() performs an unstable sort therefore // we must restrict the range of the sort to ensure the input and bias node indexes are unchanged. Restricting the // sort to the required range is also more efficient (less items to sort). int inputAndBiasCount = networkDef.InputNodeCount + 1; Array.Sort(nodeInfoByDepth, inputAndBiasCount, nodeCount - inputAndBiasCount, NodeDepthComparer.__NodeDepthComparer); // Array of live node indexes indexed by their index in the original network definition. This allows us to // locate the position of input and output nodes in their new positions in the live network data structures. int[] newIdxByDefinitionIdx = new int[nodeCount]; // Dictionary of live node indexes keyed by node ID. This allows us to convert the network definition connection // endpoints from node IDs to indexes into the live/runtime network data structures. Dictionary <uint, int> newIdxById = new Dictionary <uint, int>(nodeCount); // Populate both the lookup array and dictionary. for (int i = 0; i < nodeCount; i++) { NodeInfo nodeInfo = nodeInfoByDepth[i]; newIdxByDefinitionIdx[nodeInfo._definitionIdx] = i; newIdxById.Add(nodeInfo._nodeId, i); } // Make a copy of the sub-range of newIdxByDefinitionIdx that represents the output nodes. int outputCount = networkDef.OutputNodeCount; int[] outputNeuronIdxArr = new int[outputCount]; // Note. 'inputAndBiasCount' holds the index of the first output node. Array.Copy(newIdxByDefinitionIdx, inputAndBiasCount, outputNeuronIdxArr, 0, outputCount); // Construct arrays with additional 'per node' data/refs (activation functions, activation fn auxiliary data). IActivationFunctionLibrary activationFnLibrary = networkDef.ActivationFnLibrary; IActivationFunction[] nodeActivationFnArr = new IActivationFunction[nodeCount]; double[][] nodeAuxArgsArray = new double[nodeCount][]; for (int i = 0; i < nodeCount; i++) { int definitionIdx = nodeInfoByDepth[i]._definitionIdx; nodeActivationFnArr[i] = activationFnLibrary.GetFunction(nodeList[definitionIdx].ActivationFnId); nodeAuxArgsArray[i] = nodeList[definitionIdx].AuxState; } //=== Create array of FastConnection(s). // Loop the connections and lookup the node IDs for each connection's end points using newIdxById. IConnectionList connectionList = networkDef.ConnectionList; int connectionCount = connectionList.Count; FastConnection[] fastConnectionArray = new FastConnection[connectionCount]; for (int i = 0; i < connectionCount; i++) { INetworkConnection conn = connectionList[i]; fastConnectionArray[i]._srcNeuronIdx = newIdxById[conn.SourceNodeId]; fastConnectionArray[i]._tgtNeuronIdx = newIdxById[conn.TargetNodeId]; fastConnectionArray[i]._weight = conn.Weight; } // Sort fastConnectionArray by source node index. This allows us to activate the connections in the // order they are present within the network (by depth). We also secondary sort by target index to // improve CPU cache coherency of the data (in order accesses that are as close to each other as possible). Array.Sort(fastConnectionArray, delegate(FastConnection x, FastConnection y) { if (x._srcNeuronIdx < y._srcNeuronIdx) { return(-1); } if (x._srcNeuronIdx > y._srcNeuronIdx) { return(1); } // Secondary sort on target index. if (x._tgtNeuronIdx < y._tgtNeuronIdx) { return(-1); } if (x._tgtNeuronIdx > y._tgtNeuronIdx) { return(1); } // Connections are equal (this should not actually happen). return(0); }); // Create an array of LayerInfo(s). Each LayerInfo contains the index + 1 of both the last node and last // connection in that layer. // The array is in order of depth, from layer zero (bias and inputs nodes) to the last layer // (usually output nodes, but not necessarily if there is a dead end pathway with a high number of hops). // Note. There is guaranteed to be at least one connection with a source at a given depth level, this is // because for there to be a layer N there must necessarily be a connection from a node in layer N-1 // to a node in layer N. int netDepth = netDepthInfo._networkDepth; LayerInfo[] layerInfoArr = new LayerInfo[netDepth]; // Scanning over nodes can start at inputAndBiasCount instead of zero, // because we know that all nodes prior to that index are at depth zero. int nodeIdx = inputAndBiasCount; int connIdx = 0; for (int currDepth = 0; currDepth < netDepth; currDepth++) { // Scan for last node at the current depth. for (; nodeIdx < nodeCount && nodeInfoByDepth[nodeIdx]._nodeDepth == currDepth; nodeIdx++) { ; } // Scan for last connection at the current depth. for (; connIdx < fastConnectionArray.Length && nodeInfoByDepth[fastConnectionArray[connIdx]._srcNeuronIdx]._nodeDepth == currDepth; connIdx++) { ; } // Store node and connection end indexes for the layer. layerInfoArr[currDepth]._endNodeIdx = nodeIdx; layerInfoArr[currDepth]._endConnectionIdx = connIdx; } return(new FastAcyclicNetwork(nodeActivationFnArr, nodeAuxArgsArray, fastConnectionArray, layerInfoArr, outputNeuronIdxArr, nodeCount, networkDef.InputNodeCount, networkDef.OutputNodeCount)); }
/// <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); }