1 package net.bmahe.genetics4j.samples.neat;
2
3 import java.time.Duration;
4 import java.util.ArrayList;
5 import java.util.List;
6 import java.util.Map;
7 import java.util.Objects;
8 import java.util.Set;
9 import java.util.random.RandomGenerator;
10
11 import org.apache.logging.log4j.LogManager;
12 import org.apache.logging.log4j.Logger;
13
14 import net.bmahe.genetics4j.core.EASystem;
15 import net.bmahe.genetics4j.core.EASystemFactory;
16 import net.bmahe.genetics4j.core.Fitness;
17 import net.bmahe.genetics4j.core.evolutionlisteners.EvolutionListeners;
18 import net.bmahe.genetics4j.core.spec.EAConfiguration;
19 import net.bmahe.genetics4j.core.spec.EAConfiguration.Builder;
20 import net.bmahe.genetics4j.core.spec.EvolutionResult;
21 import net.bmahe.genetics4j.core.spec.Optimization;
22 import net.bmahe.genetics4j.core.spec.mutation.CreepMutation;
23 import net.bmahe.genetics4j.core.spec.mutation.MultiMutations;
24 import net.bmahe.genetics4j.core.spec.mutation.MutationPolicy;
25 import net.bmahe.genetics4j.core.spec.mutation.RandomMutation;
26 import net.bmahe.genetics4j.core.spec.selection.SelectionPolicy;
27 import net.bmahe.genetics4j.core.spec.selection.Tournament;
28 import net.bmahe.genetics4j.core.spec.statistics.distributions.NormalDistribution;
29 import net.bmahe.genetics4j.core.termination.Terminations;
30 import net.bmahe.genetics4j.neat.Activations;
31 import net.bmahe.genetics4j.neat.Connection;
32 import net.bmahe.genetics4j.neat.NeatEAExecutionContexts;
33 import net.bmahe.genetics4j.neat.NeatUtils;
34 import net.bmahe.genetics4j.neat.RecurrentNetwork;
35 import net.bmahe.genetics4j.neat.chromosomes.NeatChromosome;
36 import net.bmahe.genetics4j.neat.spec.NeatChromosomeSpec;
37 import net.bmahe.genetics4j.neat.spec.combination.NeatCombination;
38 import net.bmahe.genetics4j.neat.spec.mutation.AddConnection;
39 import net.bmahe.genetics4j.neat.spec.mutation.AddNode;
40 import net.bmahe.genetics4j.neat.spec.mutation.DeleteConnection;
41 import net.bmahe.genetics4j.neat.spec.mutation.DeleteNode;
42 import net.bmahe.genetics4j.neat.spec.mutation.NeatConnectionWeight;
43 import net.bmahe.genetics4j.neat.spec.mutation.SwitchStateMutation;
44 import net.bmahe.genetics4j.neat.spec.selection.NeatSelection;
45 import net.bmahe.genetics4j.neat.util.GraphvizFormatter;
46
47 public final class RecurrentParityExample {
48
49 private static final Logger logger = LogManager.getLogger(RecurrentParityExample.class);
50
51 public RecurrentParityExample() {
52 }
53
54 private static final RandomGenerator random = RandomGenerator.getDefault();
55
56
57 private static final List<List<Integer>> TRAINING_SEQUENCES = random.ints(100)
58 .mapToObj(i -> random.doubles(4).mapToInt(j -> j <= 0.5 ? 0 : 1).boxed().toList())
59 .toList();
60
61
62
63
64
65
66 private static final float MAX_THEORETICAL_FITNESS = 4f * 2f * TRAINING_SEQUENCES.size();
67 private static final float FITNESS_TERMINATION_THRESHOLD = MAX_THEORETICAL_FITNESS * 0.99f;
68
69
70 public static void main(final String[] args) {
71 final RecurrentParityExample recurrentParityExample = new RecurrentParityExample();
72
73
74 final double randomWeightsProb = 0.05;
75 final double switchMutationProb = 0.005;
76 final double addNodeProb = 0.015;
77 final double deleteNodeProb = 0.005;
78 final double addConnectionProb = 0.035;
79 final double deleteConnectionProb = 0.005;
80
81
82 recurrentParityExample.run(
83 randomWeightsProb,
84 switchMutationProb,
85 addNodeProb,
86 deleteNodeProb,
87 addConnectionProb,
88 deleteConnectionProb);
89 }
90
91 public EvolutionResult<Float> run(final double randomWeightsProb, final double switchMutationProb,
92 final double addNodeProb, final double deleteNodeProb, final double addConnectionProb,
93 final double deleteConnectionProb) {
94
95 final List<MutationPolicy> mutations = buildMutations(
96 randomWeightsProb,
97 switchMutationProb,
98 addNodeProb,
99 deleteNodeProb,
100 addConnectionProb,
101 deleteConnectionProb);
102
103 final EAConfiguration<Float> configuration = buildParityConfiguration(mutations);
104 final var executionContext = NeatEAExecutionContexts.<Float>standard()
105 .populationSize(500)
106 .addEvolutionListeners(EvolutionListeners.ofLogTopN(logger, 3))
107 .build();
108
109 final EASystem<Float> eaSystem = EASystemFactory.from(configuration, executionContext);
110 final var result = eaSystem.evolve();
111
112 logger.info("Best fitness: {}", result.bestFitness());
113 describeBestChromosome(result.bestIndividual().genotype().getChromosome(0, NeatChromosome.class));
114
115 return result;
116 }
117
118
119 private EAConfiguration<Float> buildParityConfiguration(final List<MutationPolicy> mutations) {
120 Objects.requireNonNull(mutations);
121
122 final Builder<Float> builder = new EAConfiguration.Builder<>();
123 builder.chromosomeSpecs(NeatChromosomeSpec.of(2, 1, -3, 3))
124 .parentSelectionPolicy(buildSelectionPolicy())
125 .combinationPolicy(NeatCombination.build())
126 .mutationPolicies(mutations)
127 .fitness(createParityFitness(false))
128 .optimization(Optimization.MAXIMIZE)
129 .termination(
130 Terminations.<Float>or(
131 Terminations.ofMaxTime(Duration.ofMinutes(5)),
132 Terminations.ofStableFitness(200),
133 Terminations.ofMaxGeneration(2000),
134 Terminations.ofFitnessAtLeast(FITNESS_TERMINATION_THRESHOLD)));
135 return builder.build();
136 }
137
138
139
140 private SelectionPolicy buildSelectionPolicy() {
141 return NeatSelection.builder()
142 .minSpeciesSize(3)
143 .perSpeciesKeepRatio(0.20f)
144 .speciesSelection(Tournament.of(3))
145 .speciesPredicate(
146 (i1, i2) -> NeatUtils.compatibilityDistance(i1.genotype(), i2.genotype(), 0, 2, 2, 1f) < 3)
147 .build();
148 }
149
150
151
152 private List<MutationPolicy> buildMutations(final double randomWeightsProb, final double switchMutationProb,
153 final double addNodeProb, final double deleteNodeProb, final double addConnectionProb,
154 final double deleteConnectionProb) {
155 return List.of(
156 RandomMutation.of(randomWeightsProb),
157 MultiMutations.of(
158 CreepMutation.of(0.85f, NormalDistribution.of(0.0, 0.333)),
159 NeatConnectionWeight.builder().populationMutationProbability(0.85f).build()),
160 SwitchStateMutation.of(switchMutationProb),
161 AddNode.of(addNodeProb),
162 DeleteNode.of(deleteNodeProb),
163 AddConnection.builder()
164 .populationMutationProbability(addConnectionProb)
165 .allowOutputNodeAsSource(true)
166 .build(),
167 DeleteConnection.of(deleteConnectionProb));
168 }
169
170
171
172 private Fitness<Float> createParityFitness(final boolean verbose) {
173 return genotype -> {
174 final NeatChromosome chromosome = genotype.getChromosome(0, NeatChromosome.class);
175
176 if (chromosome.getConnections().isEmpty()) {
177 return 0f;
178 }
179
180 final RecurrentNetwork network = buildNetwork(chromosome);
181 final Integer outputNodeIndex = chromosome.getOutputNodeIds().getFirst();
182
183 float cumulativeError = 0F;
184 for (final List<Integer> sequence : TRAINING_SEQUENCES) {
185 network.resetState();
186
187 Map<Integer, Float> outputs = Map.of();
188 List<Integer> input = new ArrayList<>(4);
189 for (final int bit : sequence) {
190 input.add(bit);
191 final float expectedValue = expectedParity(input);
192
193 outputs = network.step(Map.of(0, encodeBit(bit), 1, 1.0f));
194
195 final float prediction = outputs.getOrDefault(outputNodeIndex, 0.0f);
196 final float error = Math.abs(expectedValue - prediction);
197 cumulativeError += error;
198
199 if (verbose) {
200 logger.info("Sequence {} -> expected {}, predicted {}", input, expectedValue, prediction);
201 }
202 }
203
204 }
205
206 return MAX_THEORETICAL_FITNESS - cumulativeError;
207 };
208 }
209
210
211 private RecurrentNetwork buildNetwork(final NeatChromosome chromosome) {
212 final Set<Integer> inputIndices = Set.copyOf(chromosome.getInputNodeIds());
213 final Set<Integer> outputIndices = Set.copyOf(chromosome.getOutputNodeIds());
214 final List<Connection> connections = chromosome.getConnections();
215
216 return new RecurrentNetwork(inputIndices, outputIndices, connections, Activations.tanhFloat, 12, 1e-4f, 0.0f);
217 }
218
219 private float encodeBit(final int bit) {
220 return bit == 0 ? -1.0f : 1.0f;
221 }
222
223 private float expectedParity(final List<Integer> bits) {
224 final long ones = bits.stream().filter(bit -> bit == 1).count();
225 return ones % 2 == 0 ? -1.0f : 1.0f;
226 }
227
228 private void describeBestChromosome(final NeatChromosome chromosome) {
229 final RecurrentNetwork network = buildNetwork(chromosome);
230 final Integer outputNodeIndex = chromosome.getOutputNodeIds().getFirst();
231
232 for (final List<Integer> sequence : TRAINING_SEQUENCES.subList(0, 10)) {
233 network.resetState();
234 Map<Integer, Float> outputs = Map.of();
235 for (final int bit : sequence) {
236 outputs = network.step(Map.of(0, encodeBit(bit), 1, 1.0f));
237 }
238
239 final float prediction = outputs.getOrDefault(outputNodeIndex, 0.0f);
240 final float expectedParity = expectedParity(sequence);
241 logger.info("Sequence {} -> Prediction: {} - Expected: {}", sequence, prediction, expectedParity);
242 }
243
244 final GraphvizFormatter graphvizFormatter = new GraphvizFormatter();
245 final String bestNetworkStr = graphvizFormatter.format(chromosome, Map.of(0, "Input", 1, "bias=1", 2, "Output"));
246 logger.info("{}", bestNetworkStr);
247
248 }
249 }