def test_genomes01(self): """> Test the distance and internal representations of the two given nodes.""" # Folder must be root to load in make_net properly if os.getcwd().split('\\')[-1] == 'tests': os.chdir('..') # Fetch the used genomes cfg = get_config() genomes = dict() genomes[0] = get_deep_genome0(cfg) genomes[1] = get_deep_genome1(cfg) # Setup an initial genome-cache cache = GenomeDistanceCache(cfg.genome) cache.warm_up(genomes) # Get the genome-distance result = cache(genome0=genomes[0], genome1=genomes[1]) self.assertGreater(result, 0) # Calculation: # 6 disjoint nodes # 2 identical nodes (outputs) # 9 disjoint connections # 1 identical connection node_distance = (6 * cfg.genome.compatibility_disjoint_node + 2 * 0) / 5 conn_distance = (9 * cfg.genome.compatibility_disjoint_conn + 1 * 0) / 6 self.assertEqual(result, node_distance + conn_distance) # Check if all different representations self.assertNotEqual(cache.node_cache.index_map[5], cache.node_cache.index_map[8]) self.assertNotEqual(cache.node_cache.index_map[6], cache.node_cache.index_map[9]) self.assertNotEqual(cache.node_cache.index_map[7], cache.node_cache.index_map[10])
def test_genomes03(self): """> Test the distance and internal representations of the two given nodes.""" # Folder must be root to load in make_net properly if os.getcwd().split('\\')[-1] == 'tests': os.chdir('..') # Fetch the used genomes cfg = get_config() genomes = dict() genomes[0] = get_deep_genome0(cfg) genomes[3] = get_deep_genome3(cfg) # Setup an initial genome-cache cache = GenomeDistanceCache(cfg.genome) cache.warm_up(genomes) # Get the genome-distance result = cache(genome0=genomes[0], genome1=genomes[3]) self.assertGreater(result, 0) # Calculation: # 2 disjoint nodes # 4 identical nodes (outputs) # 5 disjoint connections (1 completely removed, 2x2 others connected to different genes) # 4 identical connections node_distance = (2 * cfg.genome.compatibility_disjoint_node + 4 * 0) / 5 conn_distance = (5 * cfg.genome.compatibility_disjoint_conn + 4 * 0) / 6 self.assertEqual(result, node_distance + conn_distance) # Check if all different representations self.assertEqual(cache.node_cache.index_map[5], cache.node_cache.index_map[14]) self.assertEqual(cache.node_cache.index_map[6], cache.node_cache.index_map[15]) self.assertNotEqual(cache.node_cache.index_map[7], cache.node_cache.index_map[16])
def test_distance(self): """> Test if the distance holds for two completely different genomes.""" # Folder must be root to load in make_net properly if os.getcwd().split('\\')[-1] == 'tests': os.chdir('..') # Fetch the used genomes cfg = get_config() genomes = dict() genomes[0] = get_genome0(cfg) genomes[3] = get_genome3(cfg) # Setup an initial genome-cache cache = GenomeDistanceCache(cfg.genome) cache.warm_up(genomes) # Get the genome-distance result = cache(genome0=genomes[0], genome1=genomes[3]) self.assertGreater(result, 0) # Calculation: # 2 disjoint nodes # 2 identical nodes (outputs) # 5 disjoint connections (1 completely removed, 2x2 others connected to different genes) # 1 identical connection node_distance = (2 * cfg.genome.compatibility_disjoint_node + 2 * 0) / 3 conn_distance = (5 * cfg.genome.compatibility_disjoint_conn + 1 * 0) / 4 self.assertEqual(result, node_distance + conn_distance)
def test_zero_distance(self): """> Test if False is returned when comparing nodes of different type.""" # Folder must be root to load in make_net properly if os.getcwd().split('\\')[-1] == 'tests': os.chdir('..') # Fetch the used genomes (each type of genome twice) cfg = get_config() genomes = dict() genomes[0] = get_genome0(cfg) genomes[1] = get_genome0(cfg) genomes[1].key = 1 # Update genomes[2] = get_genome1(cfg) genomes[3] = get_genome1(cfg) genomes[3].key = 3 # Update genomes[4] = get_genome2(cfg) genomes[5] = get_genome2(cfg) genomes[5].key = 5 # Update # Setup an initial genome-cache cache = GenomeDistanceCache(cfg.genome) cache.warm_up(genomes) # Get the genome-distance self.assertEqual(cache(genome0=genomes[0], genome1=genomes[1]), 0) self.assertEqual(cache(genome0=genomes[2], genome1=genomes[3]), 0) self.assertEqual(cache(genome0=genomes[4], genome1=genomes[5]), 0)
def test_same_internal_representation(self): """> Test if the merged hidden nodes have the same internal representation.""" # Folder must be root to load in make_net properly if os.getcwd().split('\\')[-1] == 'tests': os.chdir('..') # Fetch the used genomes cfg = get_config() genomes = dict() genomes[0] = get_genome0(cfg) genomes[1] = get_genome1(cfg) # Setup an initial genome-cache cache = GenomeDistanceCache(cfg.genome) cache.warm_up(genomes) # Hidden nodes with keys 2 and 3 should have the same internal representation since they represent the same node self.assertEqual(cache.node_cache.index_map[2], cache.node_cache.index_map[3])
def test_distance_similar_genome(self): """> Test if two different genomes with exact same architecture have zero distance.""" # Folder must be root to load in make_net properly if os.getcwd().split('\\')[-1] == 'tests': os.chdir('..') # Fetch the used genomes cfg = get_config() genomes = dict() genomes[0] = get_genome0(cfg) genomes[1] = get_genome1(cfg) # Setup an initial genome-cache cache = GenomeDistanceCache(cfg.genome) cache.warm_up(genomes) # Get the genome-distance self.assertEqual(cache(genome0=genomes[0], genome1=genomes[1]), 0)
def elite_architecture(pop: Population, show: bool = False, del_cache: bool = True): """ Visualize each architectural change in the population's elites. :param pop: Population object :param show: Show the result :param del_cache: Remove intermediate architecture-results when images are created """ # Initialize the architecture-list with the first genome distance = GenomeDistanceCache(config=pop.config.genome) new_architectures = [(0, pop.best_genome_hist[0][1]) ] # Only interested in genome itself for gen in range(1, pop.generation): gen_genome = pop.best_genome_hist[gen][1] if distance( gen_genome, new_architectures[-1] [1]) > 2: # Take only the more significant changes into account new_architectures.append((gen, gen_genome)) new_architectures.append( (pop.generation - 1, pop.best_genome_hist[pop.generation - 1][1])) # Create the architectures of the unique genomes for _, g in new_architectures: pop.visualize_genome( debug=False, # Keep the networks simple genome=g, show=False, ) # Combine in one figure hor = min(len(new_architectures), 5) vert = max((len(new_architectures) - 1) // 5 + 1, 1) plt.figure(figsize=(5 * hor, 5 * vert)) plt.tight_layout() f_images = get_subfolder( f"population{'_backup' if pop.use_backup else ''}/storage/{pop.folder_name}/{pop}/", "images") f_architectures = get_subfolder(f_images, "architectures") for i, (gen, g) in enumerate(new_architectures): plt.subplot(vert, hor, i + 1) img = mpimg.imread(f'{f_architectures}genome_{g.key}.png') plt.imshow(img) plt.title(f'Generation {gen}') plt.axis('off') # Save the result f_elites = get_subfolder(f_images, "elites") plt.savefig(f'{f_elites}/architecture_timeline.png', bbox_inches='tight') if show: plt.show() plt.close() if del_cache: for (_, g) in new_architectures: path = f'{f_architectures}genome_{g.key}.png' if os.path.exists(path): os.remove(path)
def speciate(self, config: Config, population, generation, logger=None): """ Place genomes into species by genetic similarity. :note: This method assumes the current representatives of the species are from the old generation, and that after speciation has been performed, the old representatives should be dropped and replaced with representatives from the new generation. """ assert isinstance(population, dict) unspeciated = set( iterkeys(population)) # Initially the full population distances = GenomeDistanceCache( config.genome) # Cache memorizing distances between genomes # distances.warm_up(population) # No warm-up since only comparison with representatives! (expensive!) new_representatives = dict( ) # Updated representatives for each specie (least difference with previous) members = dict( ) # Dictionary containing for each specie its corresponding members # Update the representatives for each specie as the genome closest to the previous representative for sid, s in iteritems(self.species): candidates = [] for gid in unspeciated: genome = population[gid] d = distances(s.representative, genome) candidates.append((d, genome)) # Sort the genomes based on their distance towards the specie and take the closest genome sorted_repr = sorted(candidates, key=lambda x: x[0]) new_repr = sorted_repr[0][1] new_representatives[sid] = new_repr.key members[sid] = [new_repr.key] unspeciated.remove(new_repr.key) # Partition the population into species based on their genetic similarity while unspeciated: # Pull unspeciated genome gid = unspeciated.pop() genome = population[gid] # Find the species with the most similar representative specie_distance = [] candidates = [] for sid, rid in iteritems(new_representatives): rep = population[rid] d = distances(rep, genome) specie_distance.append((d, sid)) if d < config.population.get_compatibility_threshold( n_species=len(new_representatives)): candidates.append((d, sid)) # There are species close enough; add genome to most similar specie if candidates: _, sid = min(candidates, key=lambda x: x[0]) members[sid].append(gid) else: # No specie is similar enough, create new specie with this genome as its representative sid = next(self.indexer) new_representatives[sid] = gid members[sid] = [gid] # Update the species collection based on the newly defined speciation self.genome_to_species = dict() for sid, rid in iteritems(new_representatives): s = self.species.get(sid) # One of the newly defined species if s is None: s = Species(sid, generation) self.species[sid] = s # Append the newly added members to the genome_to_species mapping specie_members = members[sid] for gid in specie_members: self.genome_to_species[gid] = sid # Update the specie's current representative and members member_dict = dict((genome_id, population[genome_id]) for genome_id in specie_members) s.update(representative=population[rid], members=member_dict) # Report over the current species (distances) self.reporters.info( f'Genetic distance:' f'\n\t- Maximum: {max(itervalues(distances.distances)):.5f}' f'\n\t- Mean: {mean(itervalues(distances.distances)):.5f}' f'\n\t- Minimum: {min(itervalues(distances.distances)):.5f}' f'\n\t- Standard deviation: {stdev(itervalues(distances.distances)):.5f}', logger=logger, print_result=False, ) print( f'Maximum genetic distance: {round(max(itervalues(distances.distances)), 3)}' ) # Print reduced version # Report over the most complex genome most_complex = sorted([(g.size(), gid) for gid, g in population.items()], key=lambda x: x[0], reverse=True)[0] gid = most_complex[1] size = most_complex[0] specie_id = self.genome_to_species[gid] distance = distances(population[gid], self.species[specie_id].representative) self.reporters.info( f"Most complex genome '{gid}' " f"of size {size} " f"belongs to specie '{specie_id}' " f"with distance to representative of {round(distance, 3)}", logger=logger, )