/// <summary> /// Activate the network. Activation reads input signals from InputSignalArray and writes output signals /// to OutputSignalArray. /// </summary> public void Activate() { ReadOnlySpan <int> srcIds = _connIds.GetSourceIdSpan(); ReadOnlySpan <int> tgtIds = _connIds.GetTargetIdSpan(); ReadOnlySpan <double> weights = _weightArr.AsSpan(); Span <double> activations = _activationArr.AsSpan(); // Reset hidden and output node activation levels, ready for next activation. // Note. this reset is performed here instead of after the below loop because this resets the output // node values, which are the outputs of the network as a whole following activation; hence // they need to be remain unchanged until they have been read by the caller of Activate(). activations.Slice(_inputCount).Clear(); // Process all layers in turn. int conIdx = 0; int nodeIdx = _inputCount; // Loop through network layers. for (int layerIdx = 0; layerIdx < _layerInfoArr.Length - 1; layerIdx++) { LayerInfo layerInfo = _layerInfoArr[layerIdx]; // Push signals through the current layer's connections to the target nodes (that are all in 'downstream' layers). for (; conIdx < layerInfo.EndConnectionIdx; conIdx++) { // Get the connection source signal, multiply it by the connection weight, add the result // to the target node's current pre-activation level, and store the result. activations[tgtIds[conIdx]] = Math.FusedMultiplyAdd( activations[srcIds[conIdx]], weights[conIdx], activations[tgtIds[conIdx]]); } // Activate the next layer's nodes. This is possible because we know that all connections that // target these nodes have been processed, either during processing on the current layer's // connections, or earlier layers. This means that the final output value/signal (i.e post // activation function output) is available for all connections and nodes in the lower/downstream // layers. // // Pass the pre-activation levels through the activation function. // Note. The resulting post-activation levels are stored in _activationArr. layerInfo = _layerInfoArr[layerIdx + 1]; _activationFn( ref activations[nodeIdx], layerInfo.EndNodeIdx - nodeIdx); // Update nodeIdx to point at first node in the next layer. nodeIdx = layerInfo.EndNodeIdx; } // Copy the output signals from _activationArr into _outputArr. // These signals are scattered through _activationArr, and here we bring them together into a // contiguous segment of memory that is indexable by output index. for (int i = 0; i < _outputNodeIdxArr.Length; i++) { _outputArr[i] = _activationArr[_outputNodeIdxArr[i]]; } }
/// <summary> /// Activate the network. Activation reads input signals from InputSignalArray and writes output signals /// to OutputSignalArray. /// </summary> public void Activate() { ReadOnlySpan <int> srcIds = _connIds.GetSourceIdSpan(); ReadOnlySpan <int> tgtIds = _connIds.GetTargetIdSpan(); ReadOnlySpan <double> weights = _weightArr.AsSpan(); Span <double> activations = _activationMem.Span; Span <double> outputs = _outputMem.Span; ref int srcIdsRef = ref MemoryMarshal.GetReference(srcIds);
/// <summary> /// Activate the network for a fixed number of iterations defined by the 'maxIterations' parameter /// at construction time. Activation reads input signals from InputSignalArray and writes output signals /// to OutputSignalArray. /// </summary> public void Activate() { ReadOnlySpan <int> srcIds = _connIds.GetSourceIdSpan(); ReadOnlySpan <int> tgtIds = _connIds.GetTargetIdSpan(); ReadOnlySpan <double> weights = _weightArr.AsSpan(); Span <double> preActivations = _preActivationMem.Span; Span <double> postActivations = _postActivationMem.Span; // Note. Here we skip over the activations corresponding to the input neurons, as these have no // incoming connections, and therefore have fixed post-activation values and are never activated. int nonInputCount = _totalNodeCount - _inputCount; Span <double> preActivationsNonInputs = preActivations.Slice(_inputCount); Span <double> postActivationsNonInputs = postActivations.Slice(_inputCount); ref int srcIdsRef = ref MemoryMarshal.GetReference(srcIds);
private static ConnectionIds CopyAndMapIds( Span <DirectedConnection> connSpan, INodeIdMap nodeIdMap) { int count = connSpan.Length; var connIds = new ConnectionIds(count); var srcIds = connIds.GetSourceIdSpan(); var tgtIds = connIds.GetTargetIdSpan(); for (int i = 0; i < count; i++) { srcIds[i] = nodeIdMap.Map(connSpan[i].SourceId); tgtIds[i] = nodeIdMap.Map(connSpan[i].TargetId); } return(connIds); }
private static void CopyAndMapIds( DirectedConnection[] connArr, INodeIdMap nodeIdMap, out ConnectionIds connIds) { int count = connArr.Length; connIds = new ConnectionIds(count); var srcIds = connIds.GetSourceIdSpan(); var tgtIds = connIds.GetTargetIdSpan(); for (int i = 0; i < count; i++) { srcIds[i] = nodeIdMap.Map(connArr[i].SourceId); tgtIds[i] = nodeIdMap.Map(connArr[i].TargetId); } }
/// <summary> /// Split each IWeightedDirectedConnection in a list into an array of DirectedConnections(s), and an array of weights. /// Map the node IDs to indexes as we go. /// </summary> private static void CopyAndMapIds( Span <WeightedDirectedConnection <T> > connSpan, INodeIdMap nodeIdMap, out ConnectionIds connIds, out T[] weightArr) { int count = connSpan.Length; connIds = new ConnectionIds(count); var srcIds = connIds.GetSourceIdSpan(); var tgtIds = connIds.GetTargetIdSpan(); weightArr = new T[count]; for (int i = 0; i < count; i++) { srcIds[i] = nodeIdMap.Map(connSpan[i].SourceId); tgtIds[i] = nodeIdMap.Map(connSpan[i].TargetId); weightArr[i] = connSpan[i].Weight; } }
/// <summary> /// Activate the network for a fixed number of iterations defined by the 'maxIterations' parameter /// at construction time. Activation reads input signals from InputSignalArray and writes output signals /// to OutputSignalArray. /// </summary> public void Activate() { ReadOnlySpan <int> srcIds = _connIds.GetSourceIdSpan(); ReadOnlySpan <int> tgtIds = _connIds.GetTargetIdSpan(); ReadOnlySpan <double> weights = _weightArr.AsSpan(); Span <double> preActivations = _preActivationArr.AsSpan(); Span <double> postActivations = _postActivationArr.AsSpan(); // Note. Here we skip over the activations corresponding to the input neurons, as these have no // incoming connections, and therefore have fixed post-activation values and are never activated. int nonInputCount = _preActivationArr.Length - _inputCount; Span <double> preActivationsNonInputs = preActivations.Slice(_inputCount); Span <double> postActivationsNonInputs = postActivations.Slice(_inputCount); // Activate the network for a fixed number of timesteps. for (int i = 0; i < _cyclesPerActivation; i++) { // Loop connections. Get each connection's input signal, apply the weight and add the result to // the pre-activation signal of the target neuron. for (int j = 0; j < srcIds.Length; j++) { preActivations[tgtIds[j]] = Math.FusedMultiplyAdd( postActivations[srcIds[j]], weights[j], preActivations[tgtIds[j]]); } // Pass the pre-activation levels through the activation function, storing the results in the // post-activation span/array. _activationFn( ref preActivationsNonInputs[0], ref postActivationsNonInputs[0], nonInputCount); // Reset the elements of _preActivationArray that represent the output and hidden nodes. preActivationsNonInputs.Clear(); } }
/// <summary> /// Creates a new directed acyclic graph instance from the provided graph structure, accompanying graph /// layer information, and node ID mappings. /// </summary> /// <param name="digraph">A directed graph structure.</param> /// <param name="depthInfo">Depth/layer information, describing what layer each node of the graph is within.</param> /// <param name="newIdByOldId">Returns a set of node ID mappings. These describe a mapping from the /// non-contiguous node ID space of <paramref name="digraph"/>, to the contiguous node ID space of the /// returned <see cref="DirectedGraphAcyclic"/> /// contiguous space.</param> /// <param name="connectionIndexMap">Returns a set of connection index mappings. The connections of /// <paramref name="digraph"/> are re-ordered based on the layer/depth of each connection's source node; /// this structure conveys the new index of each connection given its original/old index.</param> /// <param name="timsortWorkArr">A re-usable working array for use as temporary storage by the timsort algorithm.</param> /// <param name="timsortWorkVArr">A secondary re-usable working array for use as temporary storage by the timsort algorithm.</param> /// <returns>A new instance of <see cref="DirectedGraphAcyclic"/>.</returns> public static DirectedGraphAcyclic CreateDirectedGraphAcyclic( DirectedGraph digraph, GraphDepthInfo depthInfo, out int[] newIdByOldId, out int[] connectionIndexMap, ref int[]?timsortWorkArr, ref int[]?timsortWorkVArr) { int inputCount = digraph.InputCount; int outputCount = digraph.OutputCount; // Assert that all input nodes are at depth zero. // Any input node with a non-zero depth must have an input connection, and this is not supported. Debug.Assert(SpanUtils.Equal(depthInfo._nodeDepthArr.AsSpan(0, inputCount), 0)); // Compile a mapping from current node IDs to new IDs (based on node depth in the graph). newIdByOldId = CompileNodeIdMap(depthInfo, digraph.TotalNodeCount, inputCount, ref timsortWorkArr, ref timsortWorkVArr); // Map the connection node IDs. ConnectionIds connIds = digraph.ConnectionIds; MapIds(connIds, newIdByOldId); // Init connection index map. int connCount = connIds.Length; connectionIndexMap = new int[connCount]; for (int i = 0; i < connCount; i++) { connectionIndexMap[i] = i; } // Sort the connections based on sourceID, targetId; this will arrange the connections based on the depth // of the source nodes. // Note. This sort routine will also sort a secondary array, i.e. keep the items in both arrays aligned; // here we use this to create connectionIndexMap. ConnectionSorter <int> .Sort(connIds, connectionIndexMap); // Make a copy of the sub-range of newIdByOldId that represents the output nodes. // This is required later to be able to locate the output nodes now that they have been sorted by depth. int[] outputNodeIdxArr = new int[outputCount]; newIdByOldId.AsSpan(inputCount, outputCount).CopyTo(outputNodeIdxArr); // 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 (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 graphDepth = depthInfo._graphDepth; LayerInfo[] layerInfoArr = new LayerInfo[graphDepth]; // Note. Scanning over nodes can start at inputCount instead of zero, because all nodes prior to that index // are input nodes and are therefore at depth zero. (input nodes are never the target of a connection, // therefore are always guaranteed to be at the start of a connectivity graph, and thus at depth zero). int nodeCount = digraph.TotalNodeCount; int nodeIdx = inputCount; int connIdx = 0; int[] nodeDepthArr = depthInfo._nodeDepthArr; ReadOnlySpan <int> srcIds = connIds.GetSourceIdSpan(); for (int currDepth = 0; currDepth < graphDepth; currDepth++) { // Scan for last node at the current depth. for (; nodeIdx < nodeCount && nodeDepthArr[nodeIdx] == currDepth; nodeIdx++) { ; } // Scan for last connection at the current depth. for (; connIdx < srcIds.Length && nodeDepthArr[srcIds[connIdx]] == currDepth; connIdx++) { ; } // Store node and connection end indexes for the layer. layerInfoArr[currDepth] = new LayerInfo(nodeIdx, connIdx); } // Construct and return. return(new DirectedGraphAcyclic( inputCount, outputCount, nodeCount, connIds, layerInfoArr, outputNodeIdxArr)); }