def train( population: Population, games: list, iterations: int, debug: bool = False, duration: int = 0, unused_cpu: int = 0, ): """Train the population on the requested number of iterations.""" from environment.env_training import TrainingEnv population.log("\n===> TRAINING <===\n") game_config = deepcopy(population.config) if duration > 0: game_config.game.duration = duration trainer = TrainingEnv( unused_cpu=unused_cpu, # Use two cores less to keep laptop usable game_config=game_config, games=games, ) trainer.evaluate_and_evolve( pop=population, n=iterations, parallel=not debug, save_interval=1 if debug else 10, )
def trace_most_fit( population: Population, genome: Genome, games: list, debug: bool = False, duration: int = 0, unused_cpu: int = 0, ): """Create a trace evaluation for the given genome on the provided games.""" from environment.env_visualizing import VisualizingEnv game_config = deepcopy(population.config) if duration > 0: game_config.game.duration = duration population.log("\n===> CREATING GENOME TRACE <===\n") population.log(f"Creating traces for games: {games}") visualizer = VisualizingEnv( game_config=game_config, games=games, unused_cpu=unused_cpu, ) for g in games: # TODO: Bug in warm-up of network if multiple games evaluated visualizer.set_games([g]) visualizer.trace_genomes( pop=population, given_genome=genome, parallel=not debug, )
def test_limit(self): """ Test calculating the limit. """ viz = Population(drawable.Drawable) self.assertEqual((-0.75, -0.25), viz._limit(0.5))
def test_limit_height_one(self): """ Test that when drawing with a population height of 1, the function accepts it. """ viz = Population(drawable.Drawable) self.assertTrue(viz._limit(1))
def blueprint( population: Population, games: list, debug: bool = False, duration: int = 0, unused_cpu: int = 0, ): """Create a blueprint evaluation for the given population on the first 5 games.""" from environment.env_visualizing import VisualizingEnv population.log("\n===> CREATING BLUEPRINTS <===\n") population.log(f"Creating blueprints for games: {games}") game_config = deepcopy(population.config) if duration > 0: game_config.game.duration = duration visualizer = VisualizingEnv( game_config=game_config, games=games, unused_cpu=unused_cpu, ) visualizer.blueprint_genomes( pop=population, parallel=not debug, )
def evolve(pop: Population, pop_name: str): """Evolve with the set constraints in mind.""" # Create the next generation from the current generation pop.population = pop.reproduction.reproduce( config=pop.config, species=pop.species, generation=pop.generation, logger=pop.log, ) # Constraint each of the population's new genomes to the given topology for g in pop.population.values(): enforce_topology(pop_name, genome=g) # Check for complete extinction if not pop.species.species: pop.reporters.complete_extinction(logger=pop.log) # If requested by the user, create a completely new population, otherwise raise an exception pop.population = pop.reproduction.create_new( config=pop.config, num_genomes=pop.config.population.pop_size) # Divide the new population into species pop.species.speciate(config=pop.config, population=pop.population, generation=pop.generation, logger=pop.log) # Add to each of the species its elites pop.update_species_fitness_hist() # Increment generation count pop.generation += 1
def train_population(pop: Population, games: list, unused_cpu: int = 2): """Evaluate the given population on a training set.""" multi_env = get_multi_env(pop=pop, game_config=pop.config) multi_env.set_games(games, noise=False) pool = mp.Pool(mp.cpu_count() - unused_cpu) manager = mp.Manager() return_dict = manager.dict() pbar = tqdm(total=len(pop.population), desc="Evaluating") def update(*_): pbar.update() for genome_id, genome in pop.population.items(): pool.apply_async(func=multi_env.eval_genome, args=((genome_id, genome), return_dict), callback=update) pool.close() # Close the pool pool.join() # Postpone continuation until everything is finished pbar.close() # Calculate the fitness from the given return_dict fitness = calc_pop_fitness( fitness_cfg=pop.config.evaluation, game_cfg=pop.config.game, game_obs=return_dict, gen=pop.generation, ) for i, genome in pop.population.items(): genome.fitness = fitness[i] # Save the results pop.generation += 1 pop.save()
def test_gap_size_one_row(self): """ Test that that the gap size of one row is 0. """ viz = Population(drawable.Drawable) self.assertEqual(0, viz._gap_size((0, 1), 1))
def evaluate_generations(name, experiment_id, folder=None, hops: int = 10, unused_cpu: int = 2): """ Evaluate the population across its lifetime. At each generation, the ten best genomes are evaluated together with the elite genome of the past five generations. :param name: Name of the population :param experiment_id: Experiment for which the population is trained (and now will be evaluated) :param folder: Population-folder (~experiment level) :param hops: Number of generations between each saved population :param unused_cpu: Number of CPU cores not used """ # Fetch population and evaluation games folder = folder if folder else get_folder(experiment_id) pop = Population( name=name, folder_name=folder, log_print=False, use_backup=True, ) _, game_ids_eval = get_game_ids(experiment_id=experiment_id) # Perform the evaluations max_gen = pop.generation for gen in tqdm(range(0, max_gen + 1, hops)): # Load in the current generation if not pop.load(gen=gen): raise Exception( f"Population {name} is not trained for generation {gen}") # Collect the used genomes if gen > 5: genomes = sorted([g for g in pop.population.values()], key=lambda x: x.fitness if x.fitness else 0, reverse=True)[:10] for i in range(1, 6): keys = [g.key for g in genomes] g = copy.deepcopy(pop.best_genome_hist[gen - i] [1]) # Copy since chance of mutation while g.key in keys: # Already added to genomes, update keys g.key += 1 genomes.append(g) else: # No history yet, use only the ten most fit genomes from the current generation genomes = sorted([g for g in pop.population.values()], key=lambda x: x.fitness if x.fitness else 0, reverse=True)[:15] # Evaluate the selected genomes evaluate( population=pop, games=game_ids_eval, genomes=genomes, unused_cpu=unused_cpu, overwrite=True, )
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 evaluate_training(experiment_id: int, pop_folder: str, folder: str = None, max_v: int = 50): """Evaluate the fitness of a population's elite each training generation.""" if pop_folder[-1] != '/': pop_folder += '/' folder = folder if folder else get_folder(experiment_id) if folder[-1] != '/': folder += '/' # Get dummy population pop = Population( name=f"{pop_folder}v1", folder_name=folder, log_print=False, use_backup=True, ) max_gen = pop.generation # Initialize data container training_fitness = dict() for g in range(0, max_gen + 1, HOPS): training_fitness[g] = [] # Pull the training scores print( f"\n===> PULLING TRAINING FITNESS OF THE {pop_folder} POPULATIONS <===" ) pbar = tqdm(range(int(max_v * (max_gen / HOPS + 1)))) for v in range(1, max_v + 1): name = f"{pop_folder}v{v}" pop = Population( name=name, folder_name=folder, log_print=False, use_backup=True, ) # Perform the evaluations max_gen = pop.generation for gen in range(0, max_gen + 1, HOPS): if not pop.load(gen=gen): raise Exception( f"Population {name} is not trained for generation {gen}") training_fitness[gen].append( pop.best_genome.fitness if pop.best_genome else 0) pbar.update() pbar.close() # Plot the result path = get_subfolder(f'population_backup/storage/{folder}{pop_folder}', 'evaluation') update_dict(f'{path}training', training_fitness, overwrite=True) path_images = get_subfolder(path, 'images') plot_result(d=training_fitness, ylabel="fitness", title="Average training fitness", save_path=f'{path_images}training')
def test_limit_order(self): """ Test that the limit is a tuple in ascending order. """ viz = Population(drawable.Drawable) limit = viz._limit(0.5) self.assertEqual(tuple, type(limit)) self.assertLess(limit[0], limit[1])
def test_gap_size_two_rows(self): """ Test that that the gap size of two rows is equivalent to the gap between the limits. """ viz = Population(drawable.Drawable) lim = (0, 1) self.assertEqual(lim[1] - lim[0], viz._gap_size(lim, 2)) lim = (0.2, 0.8) self.assertEqual(lim[1] - lim[0], viz._gap_size(lim, 2))
def visualize_best_genome(pop: Population): """Visualize the population's fittest genome.""" pop.log( f"Most fit genome: {pop.best_genome.key} with fitness: {round(pop.best_genome.fitness, 3)}" ) name = f"genome_{pop.best_genome.key}" sf = get_subfolder(f'population/storage/{pop.folder_name}/{pop}/', 'images') sf = get_subfolder(sf, f'gen{pop.generation:05d}') draw_net(config=pop.config.genome, genome=pop.best_genome, debug=True, filename=f'{sf}{name}', view=False)
def evaluate_genome_list(self, genome_list, pop: Population, parallel: bool = True, overwrite: bool = False): """ Evaluate the population for a single evaluation-process. :param genome_list: List of genomes that will be evaluated :param pop: The population to which the genomes belong (used to setup the network and query the config) :param parallel: Evaluate the given genomes in parallel :param overwrite: Overwrite the evaluation dictionary if it already exists """ # Create the environment which is responsible for evaluating the genomes multi_env = get_multi_env(pop=pop, game_config=self.game_config) # Evaluate on all the games multi_env.set_games(self.games, noise=False) # Fetch requested genomes genomes = [(g.key, g) for g in genome_list] if parallel: pool = mp.Pool(mp.cpu_count() - self.unused_cpu) manager = mp.Manager() return_dict = manager.dict() for genome in genomes: pool.apply_async(func=multi_env.eval_genome, args=(genome, return_dict)) pool.close() # Close the pool pool.join() # Postpone continuation until everything is finished else: return_dict = dict() for genome in tqdm(genomes, desc="sequential evaluating"): multi_env.eval_genome(genome, return_dict) # Create the evaluation for each of the genomes eval_result = dict() for k in return_dict.keys(): # Create answer based on game.close() eval_result[str(k)] = create_answer(return_dict[k]) # Append fitness to answer eval_result[str(k)][D_FITNESS] = calc_pop_fitness( fitness_cfg=pop.config.evaluation, game_cfg=pop.config.game, game_obs={k: return_dict[k]}, gen=pop.generation)[k] pop.add_evaluation_result(eval_result, overwrite=overwrite)
def test_gap_size_multiple_rows(self): """ Test that that the gap size of multiple rows fills the space between the limits. """ viz = Population(drawable.Drawable) lim, rows = (0, 1), 4 self.assertEqual(lim[1], lim[0] + viz._gap_size(lim, rows) * (rows - 1)) lim, rows = (0.2, 0.8), 5 self.assertEqual(lim[1], lim[0] + viz._gap_size(lim, rows) * (rows - 1))
def visualize_genome( population: Population, genome: Genome, debug: bool = True, show: bool = False, ): """Visualize the requested genome.""" print("\n===> VISUALIZING GENOME <===\n") print(f"Genome {genome.key} with size: {genome.size()}") population.visualize_genome( debug=debug, genome=genome, show=show, )
def test_limit_large_height(self): """ Test that when drawing with a large population height, the function raises a ValueError. """ viz = Population(drawable.Drawable) self.assertRaises(ValueError, viz._limit, 2)
def test_gap_size_float_rows(self): """ Test that when getting the gap size and the number of rows is a float, the function raises a TypeError. """ viz = Population(drawable.Drawable) self.assertRaises(TypeError, viz._gap_size, (0, 1), 1.2)
def test_gap_size_zero_rows(self): """ Test that when getting the gap size and the number of rows is zero, the function raises a ValueError. """ viz = Population(drawable.Drawable) self.assertRaises(ValueError, viz._gap_size, (0, 1), 0)
def get_population(): """Get a dummy population with minimal configuration.""" cfg = Config() cfg.game.duration = 1 # Small duration cfg.game.fps = 10 # Games of low accuracy but faster cfg.genome.rnn_prob_gru = 0 cfg.genome.rnn_prob_gru_nr = 1 # Always mutate a GRU-NR-node cfg.genome.rnn_prob_gru_nu = 0 cfg.genome.rnn_prob_lstm = 0 cfg.genome.rnn_prob_simple_rnn = 0 cfg.population.pop_size = 2 # Keep a small population cfg.population.compatibility_thr = float( 'inf') # Make sure that population does not expand cfg.update() # Create the population pop = Population( name='delete_me', folder_name='test_scenario_neat_gru', config=cfg, log_print=False, ) # Mutate population such that at least one genome has a GRU-NR-node pop.population[1].mutate_add_node( cfg.genome) # Population starts count at 1 return pop
def test_init_save_drawable(self): """ Test that when creating the population, the drawable is saved. """ viz = drawable.Drawable(plt.figure(figsize=(10, 10))) popviz = Population(viz) self.assertEqual(viz, popviz.drawable)
def test_init_none_rows(self): """ Test that when creating the population, the drawable initializes the number of rows to ``None``. """ viz = drawable.Drawable(plt.figure(figsize=(10, 10))) popviz = Population(viz) self.assertEqual(None, popviz.rows)
def test_init_empty_populations(self): """ Test that when creating the population, the drawable creates an empty list for populations. """ viz = drawable.Drawable(plt.figure(figsize=(10, 10))) popviz = Population(viz) self.assertEqual([], popviz.populations)
def main(pop: Population, d: int = 10, range_width: int = 20, genome: Genome = None, cpu: int = 2, experiment_id: int = 1, overwrite: bool = False, ): """ Analyse the given single-hidden single-GRU genome. Analysis is performed on following number of genomes: 3 * ((2 * range + 1) ** 2) * 10 = 1'323 * 20 = 5'043 * 50 = 30'603 :param pop: Population on which the analysis is performed :param genome: The genome that is being mutated :param d: Divisor, indicating hops of 1/d :param range_width: Width of the parameter-range taken into account (note: true range is range_width/d) :param overwrite: Decide if re-evaluation :param experiment_id: Experiment-games used for evaluation :param cpu: Number of CPU cores not used for simulation """ # Get genome if not defined if not genome: genome = pop.best_genome # Create a cache-population cfg = copy.deepcopy(pop.config) cfg.population.pop_size = 2 # Dummy candidates, to be removed cfg.update() # Create the populations name = f"genome{genome.key}_divisor{d}_range{range_width}" pop_cache = Population( name=name, folder_name='../../cache_populations', # I do the hack hack config=cfg, overwrite=overwrite, ) if len(pop_cache.population) == 2: create_genomes(genome=genome, pop=pop_cache, d=d, range_width=range_width) # Evaluate the populations if pop_cache.population[0].fitness is None: evaluate_population( pop=pop_cache, cfg=cfg, cpu=cpu, experiment_id=experiment_id, ) # Evaluate the results - Create 3d array of the results visualize_score(pop=pop, pop_cache=pop_cache, genome=genome, d=d, range_width=range_width)
def draw_population(self, *args, **kwargs): """ Draw a population chart on this :class:`~Drawable`. The arguments and keyword arguments are those supported by the :class:`~population.population.Population`'s :func:`~population.population.Population.draw` method. :return: A list of drawn scatter points, separated by column. :rtype: list of list of :class:`matplotlib.collections.PathCollection` """ self.population = self.population if self.population else Population(self) return self.population.draw(*args, **kwargs)
def specie_representatives(pop: Population, show: bool = True, del_cache: bool = True): """Show for each of the current species their representative's architecture.""" species = pop.species.species elite_id = dict() for sid, s in sorted(species.items()): elite_id[sid] = s.representative.key pop.visualize_genome( debug=False, # Keep the networks simple genome=s.representative, show=False, ) hor = min(len(elite_id), 5) vert = max(len(elite_id) // 5 + 1, 1) plt.figure(figsize=(5 * hor, 5 * vert)) plt.tight_layout() path = get_subfolder( f"population{'_backup' if pop.use_backup else ''}/storage/{pop.folder_name}/{pop}/", "images") path_architectures = get_subfolder(path, "architectures") for i, (sid, eid) in enumerate(elite_id.items()): plt.subplot(vert, hor, i + 1) img = mpimg.imread(f'{path_architectures}genome_{eid}.png') plt.imshow(img) plt.title(f'Specie {sid}') plt.axis('off') # Save the result path_species = get_subfolder(path, 'species') plt.savefig(f'{path_species}representatives_gen{pop.generation}.png', bbox_inches='tight') if show: plt.show() plt.close() if del_cache: for eid in elite_id.values(): path = f'{path_architectures}genome_{eid}.png' if os.path.exists(path): os.remove(path)
def pull(pop_name: str, pop_folder: str, gid: int = None, backup_pop: bool = True): """Pull a genome from a specified population""" pop = Population( name=pop_name, folder_name=pop_folder, use_backup=backup_pop, ) genome = pop.population[gid] if gid else pop.best_genome store_genome(genome=genome)
def evaluate_population(pop: Population, cfg: Config, cpu: int, experiment_id: int): """Evaluate the given population.""" pop.log(f"{pop.name} - Evaluating the population...") _, game_ids_eval = get_game_ids(experiment_id=experiment_id) multi_env = get_multi_env(pop=pop, game_config=cfg) multi_env.set_games(game_ids_eval, noise=False) pool = mp.Pool(mp.cpu_count() - cpu) manager = mp.Manager() return_dict = manager.dict() pbar = tqdm(total=len(pop.population), desc="Evaluating") def update(*_): pbar.update() for genome_id, genome in pop.population.items(): pool.apply_async(func=multi_env.eval_genome, args=((genome_id, genome), return_dict), callback=update) pool.close() # Close the pool pool.join() # Postpone continuation until everything is finished pbar.close() # Calculate the fitness from the given return_dict pop.log(f"{pop.name} - Calculating fitness scores...") fitness = calc_pop_fitness( fitness_cfg=pop.config.evaluation, game_cfg=cfg.game, game_obs=return_dict, gen=pop.generation, ) for i, genome in pop.population.items(): genome.fitness = fitness[i] # Get the fittest genome best = None for g in pop.population.values(): if best is None or g.fitness > best.fitness: best = g pop.best_genome = best # Save the results pop.save() # Visualize most fit genome visualize_genome( debug=True, genome=best, population=pop, ) # Trace the most fit genome trace_most_fit( debug=False, games=game_ids_eval, genome=best, population=pop, unused_cpu=cpu, )
def trace( population: Population, games: list, debug: bool = False, duration: int = 0, unused_cpu: int = 0, ): """Create a trace evaluation for the given population on the provided games.""" from environment.env_visualizing import VisualizingEnv population.log("\n===> CREATING TRACES <===\n") population.log(f"Creating traces for games: {games}") game_config = deepcopy(population.config) if duration > 0: game_config.game.duration = duration visualizer = VisualizingEnv( game_config=game_config, games=games, unused_cpu=unused_cpu, ) visualizer.trace_genomes(pop=population, parallel=not debug)