-
Notifications
You must be signed in to change notification settings - Fork 0
/
concurrent_neat_population.py
163 lines (134 loc) · 6.2 KB
/
concurrent_neat_population.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""Implements the core evolution algorithm."""
from __future__ import print_function
import itertools
import math
import concurrent.futures as cf
from neat.reporting import ReporterSet
from neat.math_util import mean
from neat.six_util import itervalues
class CompleteExtinctionException(Exception):
pass
class Population(object):
"""
This class implements the core evolution algorithm:
1. Evaluate fitness of all genomes.
2. Check to see if the termination criterion is satisfied; exit if it is.
3. Generate the next generation from the current population.
4. Partition the new generation into species based on genetic similarity.
5. Go to 1.
"""
def __init__(self, config, initial_state=None):
self.reporters = ReporterSet()
self.config = config
stagnation = config.stagnation_type(config.stagnation_config, self.reporters)
self.reproduction = config.reproduction_type(
config.reproduction_config, self.reporters, stagnation
)
if config.fitness_criterion == "max":
self.fitness_criterion = max
elif config.fitness_criterion == "min":
self.fitness_criterion = min
elif config.fitness_criterion == "mean":
self.fitness_criterion = mean
elif not config.no_fitness_termination:
raise RuntimeError(
"Unexpected fitness_criterion: {0!r}".format(config.fitness_criterion)
)
if initial_state is None:
# Create a population from scratch, then partition into species.
self.population = self.reproduction.create_new(
config.genome_type, config.genome_config, config.pop_size
)
self.species = config.species_set_type(
config.species_set_config, self.reporters
)
self.generation = 0
self.species.speciate(config, self.population, self.generation)
else:
self.population, self.species, self.generation = initial_state
self.best_genome = None
def add_reporter(self, reporter):
self.reporters.add(reporter)
def remove_reporter(self, reporter):
self.reporters.remove(reporter)
def run(self, fitness_function, n=None):
"""
Runs NEAT's genetic algorithm for at most n generations. If n
is None, run until solution is found or extinction occurs.
The user-provided fitness_function must take only two arguments:
1. The population as a list of (genome id, genome) tuples.
2. The current configuration object.
The return value of the fitness function is ignored, but it must assign
a Python float to the `fitness` member of each genome.
The fitness function is free to maintain external state, perform
evaluations in parallel, etc.
It is assumed that fitness_function does not modify the list of genomes,
the genomes themselves (apart from updating the fitness member),
or the configuration object.
"""
if self.config.no_fitness_termination and (n is None):
raise RuntimeError(
"Cannot have no generational limit with no fitness termination"
)
k = 0
while n is None or k < n:
k += 1
best = None
population_size = len([*self.population.values()])
chunksize = math.ceil(
population_size
/ (self.config.processes if self.config.processes else 6)
)
self.reporters.start_generation(self.generation)
# Evaluate all genomes using the user-provided function.
with cf.ProcessPoolExecutor(max_workers=self.config.processes) as executor:
for key, genome in executor.map(
fitness_function,
self.population.items(),
itertools.repeat(self.config, population_size),
chunksize=chunksize,
):
self.population[key].fitness = genome.fitness
# Gather and report statistics.
if best is None or genome.fitness > best.fitness:
best = genome
self.reporters.post_evaluate(
self.config, self.population, self.species, best
)
# Track the best genome ever seen.
if self.best_genome is None or best.fitness > self.best_genome.fitness:
self.best_genome = best
if not self.config.no_fitness_termination:
# End if the fitness threshold is reached.
fv = self.fitness_criterion(
g.fitness for g in itervalues(self.population)
)
if fv >= self.config.fitness_threshold:
self.reporters.found_solution(self.config, self.generation, best)
break
# Create the next generation from the current generation.
self.population = self.reproduction.reproduce(
self.config, self.species, self.config.pop_size, self.generation
)
# Check for complete extinction.
if not self.species.species:
self.reporters.complete_extinction()
# If requested by the user, create a completely new population,
# otherwise raise an exception.
if self.config.reset_on_extinction:
self.population = self.reproduction.create_new(
self.config.genome_type,
self.config.genome_config,
self.config.pop_size,
)
else:
raise CompleteExtinctionException()
# Divide the new population into species.
self.species.speciate(self.config, self.population, self.generation)
self.reporters.end_generation(self.config, self.population, self.species)
self.generation += 1
if self.config.no_fitness_termination:
self.reporters.found_solution(
self.config, self.generation, self.best_genome
)
return self.best_genome