View Javadoc
1   package net.bmahe.genetics4j.neat;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.HashMap;
6   import java.util.HashSet;
7   import java.util.List;
8   import java.util.Map;
9   import java.util.Objects;
10  import java.util.Set;
11  import java.util.function.Function;
12  
13  import org.apache.commons.collections4.CollectionUtils;
14  import org.apache.commons.lang3.Validate;
15  import org.apache.logging.log4j.LogManager;
16  import org.apache.logging.log4j.Logger;
17  
18  /**
19   * Implements a recurrent neural network evaluator for NEAT chromosomes.
20   *
21   * <p>Unlike {@link FeedForwardNetwork}, this implementation can execute arbitrary directed graphs that include
22   * recurrent (cyclic) connections. Activations are propagated iteratively until they converge or a maximum number of
23   * iterations is reached, making it suitable for tasks that rely on short-term memory or feedback loops.</p>
24   */
25  public class RecurrentNetwork {
26  
27  	public static final Logger logger = LogManager.getLogger(RecurrentNetwork.class);
28  
29  	/** Default maximum number of recurrent iterations. */
30  	public static final int DEFAULT_MAX_ITERATIONS = 12;
31  
32  	/** Default convergence threshold applied to node deltas between iterations. */
33  	public static final float DEFAULT_CONVERGENCE_THRESHOLD = 1e-3F;
34  
35  	/** Default value assigned to non-input nodes before the first iteration. */
36  	public static final float DEFAULT_INITIAL_STATE_VALUE = 0.0F;
37  
38  	private final Set<Integer> inputNodeIndices;
39  	private final Set<Integer> outputNodeIndices;
40  	private final List<Connection> connections;
41  
42  	private final Map<Integer, Set<Connection>> backwardConnections;
43  	private final List<Integer> evaluatedNodeIndices;
44  	private final Set<Integer> allNodeIndices;
45  
46  	private final Function<Float, Float> activationFunction;
47  	private final int maxIterations;
48  	private final float convergenceThreshold;
49  	private final float initialStateValue;
50  	private final Map<Integer, Float> nodeState;
51  
52  	public RecurrentNetwork(final Set<Integer> _inputNodeIndices,
53  			final Set<Integer> _outputNodeIndices,
54  			final List<Connection> _connections,
55  			final Function<Float, Float> _activationFunction) {
56  		this(_inputNodeIndices,
57  				_outputNodeIndices,
58  				_connections,
59  				_activationFunction,
60  				DEFAULT_MAX_ITERATIONS,
61  				DEFAULT_CONVERGENCE_THRESHOLD,
62  				DEFAULT_INITIAL_STATE_VALUE);
63  	}
64  
65  	public RecurrentNetwork(final Set<Integer> _inputNodeIndices,
66  			final Set<Integer> _outputNodeIndices,
67  			final List<Connection> _connections,
68  			final Function<Float, Float> _activationFunction,
69  			final int _maxIterations,
70  			final float _convergenceThreshold,
71  			final float _initialStateValue) {
72  		Validate.isTrue(CollectionUtils.isNotEmpty(_inputNodeIndices));
73  		Validate.isTrue(CollectionUtils.isNotEmpty(_outputNodeIndices));
74  		Objects.requireNonNull(_connections);
75  		Objects.requireNonNull(_activationFunction);
76  		Validate.isTrue(_maxIterations > 0, "maxIterations must be strictly positive");
77  		Validate.isTrue(_convergenceThreshold >= 0.0f, "convergenceThreshold must be non-negative");
78  
79  		this.inputNodeIndices = Collections.unmodifiableSet(new HashSet<>(_inputNodeIndices));
80  		this.outputNodeIndices = Collections.unmodifiableSet(new HashSet<>(_outputNodeIndices));
81  		this.connections = Collections.unmodifiableList(new ArrayList<>(_connections));
82  		this.activationFunction = _activationFunction;
83  		this.maxIterations = _maxIterations;
84  		this.convergenceThreshold = _convergenceThreshold;
85  		this.initialStateValue = _initialStateValue;
86  
87  		final Map<Integer, Set<Connection>> backward = NeatUtils.computeBackwardConnections(this.connections);
88  		this.backwardConnections = Collections.unmodifiableMap(backward);
89  
90  		final Set<Integer> allNodes = new HashSet<>(this.inputNodeIndices);
91  		allNodes.addAll(this.outputNodeIndices);
92  		for (final Connection connection : this.connections) {
93  			allNodes.add(connection.fromNodeIndex());
94  			allNodes.add(connection.toNodeIndex());
95  		}
96  
97  		this.allNodeIndices = Collections.unmodifiableSet(allNodes);
98  		this.evaluatedNodeIndices = Collections.unmodifiableList(
99  				this.allNodeIndices.stream()
100 						.filter(nodeIndex -> this.inputNodeIndices.contains(nodeIndex) == false)
101 						.sorted()
102 						.toList());
103 		this.nodeState = new HashMap<>();
104 		resetState();
105 	}
106 
107 	public Map<Integer, Float> compute(final Map<Integer, Float> inputValues) {
108 		resetState();
109 		return step(inputValues);
110 	}
111 
112 	public Map<Integer, Float> step(final Map<Integer, Float> inputValues) {
113 		Objects.requireNonNull(inputValues);
114 		Validate.isTrue(
115 				inputValues.size() == inputNodeIndices.size(),
116 				"Missing input values: expected %d entries but found %d",
117 				inputNodeIndices.size(),
118 				inputValues.size());
119 
120 		for (final Integer inputNodeIndex : inputNodeIndices) {
121 			if (inputValues.containsKey(inputNodeIndex) == false) {
122 				throw new IllegalArgumentException("Input vector missing values for input node " + inputNodeIndex);
123 			}
124 		}
125 
126 		final Map<Integer, Float> previousValues = new HashMap<>(nodeState);
127 		for (final Integer inputNodeIndex : inputNodeIndices) {
128 			previousValues.put(inputNodeIndex, inputValues.get(inputNodeIndex));
129 		}
130 
131 		Map<Integer, Float> currentValues = previousValues;
132 
133 		for (int iteration = 0; iteration < maxIterations; iteration++) {
134 			final Map<Integer, Float> nextValues = new HashMap<>(currentValues);
135 
136 			float maxDelta = 0.0F;
137 			for (final Integer nodeIndex : evaluatedNodeIndices) {
138 				final Set<Connection> incomingConnections = backwardConnections.getOrDefault(nodeIndex, Set.of());
139 
140 				float sum = 0.0F;
141 				for (final Connection incomingConnection : incomingConnections) {
142 					final float weight = incomingConnection.weight();
143 					final float incomingValue = currentValues
144 							.getOrDefault(incomingConnection.fromNodeIndex(), initialStateValue);
145 					sum += weight * incomingValue;
146 				}
147 
148 				final float newValue = activationFunction.apply(sum);
149 				final float previousValue = currentValues.getOrDefault(nodeIndex, initialStateValue);
150 				nextValues.put(nodeIndex, newValue);
151 				final float delta = Math.abs(newValue - previousValue);
152 				if (delta > maxDelta) {
153 					maxDelta = delta;
154 				}
155 			}
156 
157 			currentValues = nextValues;
158 
159 			if (maxDelta <= convergenceThreshold) {
160 				break;
161 			}
162 		}
163 
164 		nodeState.clear();
165 		nodeState.putAll(currentValues);
166 
167 		return buildOutputValues(currentValues);
168 	}
169 
170 	public void resetState() {
171 		nodeState.clear();
172 		for (final Integer nodeIndex : allNodeIndices) {
173 			nodeState.put(nodeIndex, initialStateValue);
174 		}
175 	}
176 
177 	private Map<Integer, Float> buildOutputValues(final Map<Integer, Float> nodeValues) {
178 		final Map<Integer, Float> outputValues = new HashMap<>();
179 		for (final Integer outputNodeIndex : outputNodeIndices) {
180 			final Float value = nodeValues.get(outputNodeIndex);
181 			if (value == null) {
182 				throw new IllegalArgumentException("Missing output value for node " + outputNodeIndex);
183 			}
184 			outputValues.put(outputNodeIndex, value);
185 		}
186 		return outputValues;
187 	}
188 }