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
20
21
22
23
24
25 public class RecurrentNetwork {
26
27 public static final Logger logger = LogManager.getLogger(RecurrentNetwork.class);
28
29
30 public static final int DEFAULT_MAX_ITERATIONS = 12;
31
32
33 public static final float DEFAULT_CONVERGENCE_THRESHOLD = 1e-3F;
34
35
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 }