Basic evolution¶
This section focuses on using the library in an evolutionary context. It showcases:
how to include
abrain.Genomeinto another classhow to use the configuration
how to actually produce novel solutions
First we import the relevant modules from the library (among others)
5from abrain import Config, Genome, ANN3D as ANN, Point3D as Point
As with the previous example, we need Genome,
ANN3D and Point3D to encode/decode a 3D Artificial
Neural Network.
Config is responsible for statically stored settings and
persistent configuration files. In this specific case, we also need
Strings for one particular value.
Helper classes¶
To showcase real use of the genome, we define a trivial wrapper containing two fields:
11class MyGenome:
12 def __init__(self, abrain_genome: Genome, nested_field: float):
13 self.abrain_genome = abrain_genome
14 self.nested_field = nested_field
15
16 @staticmethod
17 def random(data: Genome.Data):
18 return MyGenome(Genome.random(data), data.rng.uniform(-1, 1))
19
20 def mutate(self, data: Genome.Data):
21 if data.rng.random() < .9:
22 self.abrain_genome.mutate(data)
23 else:
24 self.nested_field += data.rng.normalvariate(0, 1)
25
26 def mutated(self, data: Genome.Data):
27 copy = self.copy()
28 copy.mutate(data)
29 copy.abrain_genome.update_lineage(data, [self.abrain_genome])
30 return copy
31
32 def copy(self):
33 return MyGenome(
34 self.abrain_genome.copy(),
35 self.nested_field
36 )
The presented pattern consists of the two essential functions random (to
generate the initial population) and mutated (to create a mutated copy of a
genome). The mutate function performs the bulk of the work by delegating
to field-wise mutators (including mutate()).
We then define an individual, in the sense of an evolutionary algorithm, as the
composition of a genome and a fitness (trivially based on the ANN’s depth).
For completeness, we provide a serialization method which relies on
to_json().
39class Individual:
40 _inputs = [Point(x, -1, z) for x, z in [(0, 0), (-1, -1), (1, 1)]]
41 _outputs = [Point(x, 1, z) for x, z in [(0, 0), (1, -1), (-1, 1)]]
42
43 def __init__(self, genome: MyGenome):
44 self.genome = genome
45 self.fitness = None
46
47 def evaluate(self):
48 if self.fitness is None:
49 ann = ANN.build(self._inputs, self._outputs,
50 self.genome.abrain_genome)
51 self.fitness = self.genome.nested_field * ann.stats().depth
52
53 def write(self, file):
54 json.dump(dict(
55 abrain_genome=self.genome.abrain_genome.to_json(),
56 float_field=self.genome.nested_field,
57 fitness=self.fitness
58 ), file)
The main¶
The following sections describe the components of a trivial EA and how to use the various parts of abrain to smoothly implement them.
Configuration¶
The following lines showcase how the end-user can tweak the various fields in
Config:
67 Config.functionSet = Config.Strings(['sin', 'abs', 'id'])
68 Config.allowPerceptrons = False
69 Config.iterations = 4
70 Config.write(output_folder.joinpath("config.json"))
71 Config.show()
Most such fields use elementary python types (int, float, str, bool)
and can thus be trivially manipulated. A few other use composite types
encapsulated, for type-safety, in a C++ object. Those are exposed as nested classes
under Config (
Strings,
MutationRates,
ESHNOutputs,
OutputFunctions,
FBounds)
and can be used to generate new values.
Additionally, the configuration can be written to a file, read()
back and displayed on the screen (for the log).
Variables¶
The initial state of this trivial EA is just as straightforward. The only thing of note is the highlighted statement where we create the shared genome data.
The actual generation of the initial population simply consists of delegating the work to the dedicated function in our wrapper genome.
75 pop_size = 10 if is_test else 100
76 genome_shared_data = Genome.Data.create_for_eshn_cppn(
77 dimension=3, seed=0,
78 with_lineage=True
79 )
80 population = [Individual(MyGenome.random(genome_shared_data))
81 for _ in range(pop_size)]