View Javadoc
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  	// tag::training_dataset[]
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  	// end::training_dataset[]
61  
62  	// tag::fitness_max_score[]
63  	/**
64  	 * 4 bits * range between odd-even (distance from -1 to 1) * number of sequences
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  	// end::fitness_max_score[]
69  
70  	public static void main(final String[] args) {
71  		final RecurrentParityExample recurrentParityExample = new RecurrentParityExample();
72  
73  		// tag::config_mutation_default_values[]
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  		// end::config_mutation_default_values[]
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 	// tag::parity_configuration[]
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 	// end::parity_configuration[]
138 
139 	// tag::config_selection[]
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 	// end::config_selection[]
150 
151 	// tag::config_mutation[]
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 	// end::config_mutation[]
170 
171 	// tag::parity_fitness[]
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 	// end::parity_fitness[]
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 }