Skip to content

Commit 250036a

Browse files
Factored speciation scheme out of Population into its own class.
Corrected lingering usage of Population._create_population.
1 parent d97792a commit 250036a

File tree

3 files changed

+72
-53
lines changed

3 files changed

+72
-53
lines changed

neat/population.py

Lines changed: 12 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
import time
77

88
from neat.config import Config
9-
from neat.indexer import Indexer
109
from neat.reporting import ReporterSet, StatisticsReporter, StdOutReporter
11-
from neat.species import Species
10+
from neat.species import SpeciesSet
1211

1312

1413
class CompleteExtinctionException(Exception):
@@ -45,17 +44,16 @@ def __init__(self, config, initial_population=None):
4544
self.add_reporter(StdOutReporter())
4645

4746
self.config = config
48-
self.species_indexer = Indexer(1)
49-
self.reproduction = config.reproduction_type(self.config, self.reporters)
47+
self.reproduction = config.reproduction_type(config, self.reporters)
5048

51-
self.species = []
49+
self.species = SpeciesSet(config)
5250
self.generation = -1
5351
self.total_evaluations = 0
5452

5553
# Create a population if one is not given, then partition into species.
5654
if initial_population is None:
5755
initial_population = self.reproduction.create_new(config.pop_size)
58-
self._speciate(initial_population)
56+
self.species.speciate(initial_population)
5957

6058
def add_reporter(self, reporter):
6159
self.reporters.add(reporter)
@@ -86,39 +84,6 @@ def save_checkpoint(self, filename=None, checkpoint_type="user"):
8684
random.getstate())
8785
pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)
8886

89-
def _speciate(self, population):
90-
"""
91-
Place genomes into species by genetic similarity.
92-
93-
Note that this method assumes the current representatives of the species are from the old
94-
generation, and that after speciation has been performed, the old representatives should be
95-
dropped and replaced with representatives from the new generation. If you violate this
96-
assumption, you should make sure other necessary parts of the code are updated to reflect
97-
the new behavior.
98-
"""
99-
for individual in population:
100-
# Find the species with the most similar representative.
101-
min_distance = None
102-
closest_species = None
103-
for s in self.species:
104-
distance = individual.distance(s.representative)
105-
if distance < self.config.compatibility_threshold \
106-
and (min_distance is None or distance < min_distance):
107-
closest_species = s
108-
min_distance = distance
109-
110-
if closest_species:
111-
closest_species.add(individual)
112-
else:
113-
# No species is similar enough, create a new species for this individual.
114-
self.species.append(Species(individual, self.species_indexer.get_next()))
115-
116-
# Only keep non-empty species.
117-
self.species = [s for s in self.species if s.members]
118-
119-
# Select a random current member as the new representative.
120-
for s in self.species:
121-
s.representative = random.choice(s.members)
12287

12388
def run(self, fitness_function, n):
12489
"""
@@ -143,21 +108,21 @@ def run(self, fitness_function, n):
143108

144109
# Collect a list of all members from all species.
145110
population = []
146-
for s in self.species:
111+
for s in self.species.species:
147112
population.extend(s.members)
148113

149114
# Evaluate all individuals in the population using the user-provided function.
150115
# TODO: Add an option to only evaluate each genome once, to reduce number of
151116
# fitness evaluations in cases where the fitness is known to be the same if the
152117
# genome doesn't change--in these cases, evaluating unmodified elites in each
153118
# generation is a waste of time. The user can always take care of this in their
154-
# fitness function in the for now if they wish.
119+
# fitness function in the time being if they wish.
155120
fitness_function(population)
156121
self.total_evaluations += len(population)
157122

158123
# Gather and report statistics.
159124
best = max(population)
160-
self.reporters.post_evaluate(population, self.species, best)
125+
self.reporters.post_evaluate(population, self.species.species, best)
161126

162127
# Save the best genome from the current generation if requested.
163128
if self.config.save_best:
@@ -170,25 +135,25 @@ def run(self, fitness_function, n):
170135
break
171136

172137
# Create the next generation from the current generation.
173-
self.species, new_population = self.reproduction.reproduce(self.species, self.config.pop_size)
138+
new_population = self.reproduction.reproduce(self.species, self.config.pop_size)
174139

175140
# Check for complete extinction
176-
if not self.species:
141+
if not self.species.species:
177142
self.reporters.complete_extinction()
178143

179144
# If requested by the user, create a completely new population,
180145
# otherwise raise an exception.
181146
if self.config.reset_on_extinction:
182-
new_population = self._create_population()
147+
new_population = self.reproduction.create_new(self.config.pop_size)
183148
else:
184149
raise CompleteExtinctionException()
185150

186151
# Update species age.
187-
for s in self.species:
152+
for s in self.species.species:
188153
s.age += 1
189154

190155
# Divide the new population into species.
191-
self._speciate(new_population)
156+
self.species.speciate(new_population)
192157

193158
# Save checkpoints if necessary.
194159
if self.config.checkpoint_time_interval is not None:

neat/reproduction.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@ def create_new(self, num_genomes):
3434
return new_genomes
3535

3636
def reproduce(self, species, pop_size):
37+
# TODO: I don't like this modification of the species object,
38+
# because it requires internal knowledge of the object.
39+
3740
# Filter out stagnated species and collect the set of non-stagnated species members.
3841
remaining_species = {}
3942
species_fitness = []
4043
avg_adjusted_fitness = 0.0
41-
for s, stagnant in self.stagnation.update(species):
44+
for s, stagnant in self.stagnation.update(species.species):
4245
if stagnant:
4346
self.reporters.species_stagnant(s)
4447
else:
@@ -56,7 +59,8 @@ def reproduce(self, species, pop_size):
5659

5760
# No species left.
5861
if not remaining_species:
59-
return [], []
62+
species.species = []
63+
return []
6064

6165
avg_adjusted_fitness /= len(species_fitness)
6266
self.reporters.info("Average adjusted fitness: {:.3f}".format(avg_adjusted_fitness))
@@ -80,7 +84,7 @@ def reproduce(self, species, pop_size):
8084
self.reporters.info('Species fitness : {0!r}'.format([sfitness for s, sfitness in species_fitness]))
8185

8286
new_population = []
83-
new_species = []
87+
species.species = []
8488
for spawn, (s, sfitness) in zip(spawn_amounts, species_fitness):
8589
# If elitism is enabled, each species always at least gets to retain its elites.
8690
spawn = max(spawn, self.elitism)
@@ -91,7 +95,7 @@ def reproduce(self, species, pop_size):
9195
# The species has at least one member for the next generation, so retain it.
9296
old_members = s.members
9397
s.members = []
94-
new_species.append(s)
98+
species.species.append(s)
9599

96100
# Sort members in order of descending fitness.
97101
old_members.sort(reverse=True)
@@ -123,6 +127,7 @@ def reproduce(self, species, pop_size):
123127
new_population.append(child.mutate())
124128

125129
# Sort species by ID (purely for ease of reading the reported list).
126-
new_species.sort(key=lambda sp: sp.ID)
130+
# TODO: This should probably be done by the species object.
131+
species.species.sort(key=lambda sp: sp.ID)
127132

128-
return new_species, new_population
133+
return new_population

neat/species.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import random
2+
3+
from neat.indexer import Indexer
4+
5+
16
class Species(object):
27
""" A collection of genetically similar individuals."""
38

@@ -13,3 +18,47 @@ def __init__(self, representative, ID):
1318
def add(self, individual):
1419
individual.species_id = self.ID
1520
self.members.append(individual)
21+
22+
23+
class SpeciesSet(object):
24+
"""
25+
Encapsulates the speciation scheme.
26+
"""
27+
def __init__(self, config):
28+
self.config = config
29+
self.indexer = Indexer(1)
30+
self.species = []
31+
32+
def speciate(self, population):
33+
"""
34+
Place genomes into species by genetic similarity.
35+
36+
Note that this method assumes the current representatives of the species are from the old
37+
generation, and that after speciation has been performed, the old representatives should be
38+
dropped and replaced with representatives from the new generation. If you violate this
39+
assumption, you should make sure other necessary parts of the code are updated to reflect
40+
the new behavior.
41+
"""
42+
for individual in population:
43+
# Find the species with the most similar representative.
44+
min_distance = None
45+
closest_species = None
46+
for s in self.species:
47+
distance = individual.distance(s.representative)
48+
if distance < self.config.compatibility_threshold \
49+
and (min_distance is None or distance < min_distance):
50+
closest_species = s
51+
min_distance = distance
52+
53+
if closest_species is not None:
54+
closest_species.add(individual)
55+
else:
56+
# No species is similar enough, create a new species for this individual.
57+
self.species.append(Species(individual, self.indexer.get_next()))
58+
59+
# Only keep non-empty species.
60+
self.species = [s for s in self.species if s.members]
61+
62+
# Select a random current member as the new representative.
63+
for s in self.species:
64+
s.representative = random.choice(s.members)

0 commit comments

Comments
 (0)