Basic evolution

This section focuses on using the library in an evolutionary context. It showcases:

  • how to include abrain.Genome into another class

  • how 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)]