def evolve(inner_objective): def objective(ind): ind.fitness = -inner_objective(ind) return ind if individual_type == "SingleGenome": pop = cgp.Population(**population_params, genome_params=genome_params) elif individual_type == "MultiGenome": pop = cgp.Population(**population_params, genome_params=[genome_params]) else: raise NotImplementedError ea = cgp.ea.MuPlusLambda(**ea_params) history = {} history["fitness_champion"] = [] def recording_callback(pop): history["fitness_champion"].append(pop.champion.fitness) cgp.evolve(objective, pop, ea, **evolve_params, callback=recording_callback) return history
def test_assert_mutation_rate(rng_seed, genome_params, mutation_rate): with pytest.raises(ValueError): cgp.Population(5, -0.1, rng_seed, genome_params) with pytest.raises(ValueError): cgp.Population(5, 1.1, rng_seed, genome_params) # assert that no error is thrown for a suitable mutation rate cgp.Population(5, mutation_rate, rng_seed, genome_params)
def test_history_recording(population_params, genome_params, ea_params): pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) evolve_params = {"max_generations": 2, "termination_fitness": 1.0} history = {} history["fitness"] = np.empty( (evolve_params["max_generations"], population_params["n_parents"])) history["fitness_champion"] = np.empty(evolve_params["max_generations"]) history["champion"] = [] def recording_callback(pop): history["fitness"][pop.generation] = pop.fitness_parents() history["fitness_champion"][pop.generation] = pop.champion.fitness history["champion"].append(pop.champion) cgp.evolve(objective_history_recording, pop, ea, **evolve_params, callback=recording_callback) assert np.all(history["fitness"] == pytest.approx(1.0)) assert np.all(history["fitness_champion"] == pytest.approx(1.0)) assert "champion" in history
def test_fitness_parents(population_params, genome_params, rng): pop = cgp.Population(**population_params, genome_params=genome_params) fitness_values = rng.rand(population_params["n_parents"]) for fitness, parent in zip(fitness_values, pop.parents): parent.fitness = fitness assert np.all(pop.fitness_parents() == pytest.approx(fitness_values))
def population_simple_fitness(population_params, genome_params): pop = cgp.Population(**population_params, genome_params=genome_params) for i, parent in enumerate(pop.parents): parent.fitness = float(i) return pop
def test_parent_individuals_are_assigned_correct_indices( population_params, genome_params): pop = cgp.Population(**population_params, genome_params=genome_params) for idx, ind in enumerate(pop.parents): assert ind.idx == idx
def main(): objective_params = {"n_runs_per_individual": 10} population_params = {"n_parents": 10, "mutation_rate": 0.04, "seed": 42} genome_params = { "n_inputs": 2, "n_outputs": 1, "n_columns": 16, "n_rows": 1, "levels_back": None, "primitives": ( cgp.Add, cgp.Sub, cgp.Mul, cgp.Div, cgp.ConstantFloat, ), } ea_params = { "n_offsprings": 4, "tournament_size": 1, "n_processes": 1, # was 4 } evolve_params = {"max_generations": 1000, "min_fitness": 0.0} pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) history = {"fitness_parents": [], "fitness_champion": []} def recording_callback(pop): history["fitness_parents"].append(pop.fitness_parents()) history["fitness_champion"].append(pop.champion.fitness) obj = functools.partial( objective, seed=population_params["seed"], n_runs_per_individual=objective_params["n_runs_per_individual"], ) cgp.evolve(pop, obj, ea, **evolve_params, print_progress=True, callback=recording_callback) display_msg( "History: {}".format(history), True, ) display_msg( "Champion: {}".format(pop.champion.to_sympy()), True, )
def test_individual_init(population_params, genome_params): dna = [ ID_INPUT_NODE, ID_NON_CODING_GENE, ID_NON_CODING_GENE, 0, 0, 0, 1, 1, 1, 2, 2, 2, ID_OUTPUT_NODE, 3, ID_NON_CODING_GENE, ] def individual_init(ind): ind.genome.dna = dna return ind genome_params = { "n_inputs": 1, "n_outputs": 1, "n_columns": 3, "n_rows": 1, "levels_back": None, "primitives": (cgp.Add, cgp.Sub, cgp.ConstantFloat), } # without passing individual_init comparison fails pop = cgp.Population(**population_params, genome_params=genome_params) for ind in pop.parents: with pytest.raises(AssertionError): assert ind.genome.dna == dna # with passing individual_init comparison succeeds pop = cgp.Population(**population_params, genome_params=genome_params, individual_init=individual_init) for ind in pop.parents: assert ind.genome.dna == dna
def test_create_new_parent_population(population_params, genome_params, ea_params): pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) # Create new parent population from the parents and assert that # we picked the first three individuals new_parents = ea._create_new_parent_population(3, pop.parents) assert new_parents == pop.parents[:3]
def test_mutate(population_params, genome_params): population_params["mutation_rate"] = 0.999999 pop = cgp.Population(**population_params, genome_params=genome_params) offspring = pop.parents offspring_original = copy.deepcopy(offspring) offspring = pop.mutate(offspring) assert np.any( [off_orig != off_mutated for off_orig, off_mutated in zip(offspring_original, offspring)] )
def test_sort(population_params, genome_params, ea_params): def objective(individual): individual.fitness = float(individual.idx) return individual pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) ea.initialize_fitness_parents(pop, objective) sorted_parents = ea._sort(pop.parents) # Assert that the sorting inverted the list of parents (because the fitness is equal to the id) assert sorted_parents == pop.parents[::-1]
def test_initialize_fitness_parents(population_params, genome_params, ea_params): def objective(individual): individual.fitness = -1.0 return individual pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) ea.initialize_fitness_parents(pop, objective) assert all([not ind.fitness_is_None() for ind in pop.parents])
def test_raise_fitness_has_wrong_type(population_params, genome_params, ea_params): def objective(individual): individual.fitness = int( 5.0) # should raise error since fitness should be float return individual pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) with pytest.raises(ValueError): ea.initialize_fitness_parents(pop, objective)
def test_objective_must_set_valid_fitness(population_params, genome_params, ea_params): def objective(ind): # missing ind.fitness assignement return ind pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) with pytest.raises(RuntimeError): cgp.evolve(objective, pop, ea, max_generations=10) with pytest.raises(RuntimeError): pop.champion.fitness
def test_mutate(population_params, genome_params, ea_params): ea_params["mutation_rate"] = 0.5 pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) offspring = pop.parents offspring_original = copy.deepcopy(offspring) offspring = ea.mutate(offspring, pop.rng) assert np.any([ off_orig != off_mutated for off_orig, off_mutated in zip(offspring_original, offspring) ])
def evolve(seed): objective_params = {"n_runs_per_individual": 3, "n_total_steps": 2000} genome_params = { "n_inputs": 2, "primitives": ( cgp.Add, cgp.Sub, cgp.Mul, cgp.Div, cgp.ConstantFloat, ConstantFloatZeroPointOne, ConstantFloatTen, ), } ea_params = {"n_processes": 4} evolve_params = { "max_generations": int(args["--max-generations"]), "termination_fitness": 100.0, } pop = cgp.Population(genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) history = {} history["expr_champion"] = [] history["fitness_champion"] = [] def recording_callback(pop): history["expr_champion"].append(pop.champion.to_sympy()) history["fitness_champion"].append(pop.champion.fitness) obj = functools.partial( objective, seed=seed, n_runs_per_individual=objective_params["n_runs_per_individual"], n_total_steps=objective_params["n_total_steps"], ) pop = cgp.evolve(obj, pop, ea, **evolve_params, print_progress=True, callback=recording_callback) return history, pop.champion
def test_fitness_contains_nan(population_params, genome_params): def objective(individual): if np.random.rand() < 0.5: individual.fitness = np.nan else: individual.fitness = np.random.rand() return individual pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(10, 10, 1) ea.initialize_fitness_parents(pop, objective) ea.step(pop, objective)
def test_ncolumns_zero(population_params): sympy = pytest.importorskip("sympy") genome_params = { "n_inputs": 1, "n_outputs": 1, "n_columns": 0, "n_rows": 1, "primitives": (cgp.Mul, cgp.Sub, cgp.Add, cgp.ConstantFloat), } pop = cgp.Population(**population_params, genome_params=genome_params) for ind in pop: sympy_expr = ind.to_sympy() assert sympy_expr == sympy.sympify("x_0")
def test_hurdles(population_params, genome_params, ea_params): # make sure all offsprings are assigned fitness None population_params["n_parents"] = 3 ea_params["mutation_rate"] = 1.0 ea_params["n_offsprings"] = 3 ea_params["hurdle_percentile"] = [0.1, 0.0] def objective_one(ind): # assign low fitness to individuals 4 and 5 to check blocking via hurdle if ind.idx in (4, 5): ind.fitness = -float(ind.idx) else: ind.fitness = float(ind.idx) return ind def objective_two(ind): if ind.idx == 4: ind.fitness = -((1.0 + float(ind.idx))**2) else: ind.fitness = (1.0 + float(ind.idx))**2 return ind pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) ea.initialize_fitness_parents(pop, [objective_one, objective_two]) # while initializing parents, both objectives should have been # evaluated for all parents; the parents fitness is hence the sum # of both objectives parents_expected = [(0, 1), (1, 5), (2, 11)] for ind, ind_expected in zip(pop.parents, parents_expected): assert ind.idx == ind_expected[0] assert ind.fitness == pytest.approx(ind_expected[1]) # code below implements `ea.step`, but keeps offsprings around to # check combined population offsprings = ea._create_new_offspring_generation(pop) combined = offsprings + pop.parents combined = ea._compute_fitness(combined, [objective_one, objective_two]) combined = ea._sort(combined) # individual 4 has higher fitness as individual 5 as the latter # didn't make it past the first hurdle combined_expected = [(3, 19), (2, 11), (1, 5), (0, 1), (4, -29), (5, -5)] assert len(combined) == len(combined_expected) for ind, ind_expected in zip(combined, combined_expected): assert ind.idx == ind_expected[0] assert ind.fitness == pytest.approx(ind_expected[1])
def evolution(f_target): """Execute CGP on a regression task for a given target function. Parameters ---------- f_target : Callable Target function Returns ------- dict Dictionary containing the history of the evolution Individual Individual with the highest fitness in the last generation """ population_params = {"n_parents": 10, "seed": 818821} genome_params = { "n_inputs": 2, "n_outputs": 1, "n_columns": 12, "n_rows": 2, "levels_back": 5, "primitives": (cgp.Add, cgp.Sub, cgp.Mul, cgp.Div, cgp.ConstantFloat), } ea_params = {"n_offsprings": 10, "tournament_size": 2, "mutation_rate": 0.03, "n_processes": 2} evolve_params = {"max_generations": int(args["--max-generations"]), "termination_fitness": 0.0} # create population that will be evolved pop = cgp.Population(**population_params, genome_params=genome_params) # create instance of evolutionary algorithm ea = cgp.ea.MuPlusLambda(**ea_params) # define callback for recording of fitness over generations history = {} history["fitness_parents"] = [] def recording_callback(pop): history["fitness_parents"].append(pop.fitness_parents()) # the objective passed to evolve should only accept one argument, # the individual obj = functools.partial(objective, target_function=f_target, seed=population_params["seed"]) # Perform the evolution cgp.evolve(pop, obj, ea, **evolve_params, print_progress=True, callback=recording_callback) return history, pop.champion
def evolution(data, population_params, genome_params, ea_params, evolve_params, learning_rate, alpha, fitness_mode): """Execute CGP for given target function. Parameters ---------- data: List(dict) Fields in dict: data_train: np.array data_validate: np.array initial_weights: np.array -> pre defined so that every individual (resp learning rule) has the same starting condition pc0: First principal components (Eigenvectors of the co-variance matrix) population_params: dict with n_parents, mutation_rate, seed genome_params: dict with n_inputs, n_outputs, n_columns, n_rows, levels_back, primitives (allowed function gene values) ea_params: dict with n_offsprings, n_breeding, tournament_size, n_processes, evolve_params: dict with max_generations, min_fitness alpha: Hyperparameter weighting the second term of the fitness function fitness_mode: str ("angle" or "variance") Returns ------- dict Dictionary containing the history of the evolution Individual Individual with the highest fitness in the last generation """ pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) history = {} history["fitness_parents"] = [] history["champion_sympy_expression"] = [] def recording_callback(pop): history["fitness_parents"].append(pop.fitness_parents()) history["champion_sympy_expression"].append( pop.champion.to_sympy() ) obj = functools.partial( objective, data=data, learning_rate=learning_rate, alpha=alpha, mode=fitness_mode) cgp.evolve( pop, obj, ea, **evolve_params, print_progress=True, callback=recording_callback, ) return history, pop.champion
def test_offspring_individuals_are_assigned_correct_indices( population_params, genome_params, ea_params): def objective(ind): ind.fitness = 0.0 return ind pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) ea.initialize_fitness_parents(pop, objective) offsprings = ea._create_new_offspring_generation(pop) for idx, ind in enumerate(offsprings): assert ind.idx == len(pop.parents) + idx
def test_update_n_objective_calls_mutation_rate_one(population_params, genome_params, ea_params): def objective(individual): individual.fitness = float(individual.idx) return individual ea_params["mutation_rate"] = 1.0 pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) ea.initialize_fitness_parents(pop, objective) n_objective_calls_expected = population_params["n_parents"] n_step_calls = 100 for idx_current_step in range(n_step_calls): ea.step(pop, objective) n_objective_calls_expected += ea_params["n_offsprings"] assert ea.n_objective_calls == n_objective_calls_expected
def test_fitness_contains_and_maintains_nan(population_params, genome_params, ea_params, rng_seed): def objective(individual): rng = np.random.RandomState(rng_seed) if rng.rand() < 0.95: individual.fitness = np.nan else: individual.fitness = rng.rand() return individual pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) ea.initialize_fitness_parents(pop, objective) ea.step(pop, objective) assert any([np.isnan(ind.fitness) for ind in pop])
def test_finite_max_generations_or_max_objective_calls(population_params, genome_params, ea_params): def objective(individual): individual.fitness = float(individual.idx) return individual pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) evolve_params = { "max_generations": np.inf, "min_fitness": 0, "max_objective_calls": np.inf, } with pytest.raises(ValueError): cgp.evolve(pop, objective, ea, **evolve_params)
def test_local_search_is_only_applied_to_best_k_individuals( population_params, local_search_params, ea_params): torch = pytest.importorskip("torch") def inner_objective(f): return torch.nn.MSELoss()(torch.DoubleTensor([[1.1]]), f(torch.zeros(1, 1, dtype=torch.double))) def objective(ind): if not ind.fitness_is_None(): return ind f = ind.to_torch() ind.fitness = -inner_objective(f).item() return ind genome_params = { "n_inputs": 1, "n_outputs": 1, "n_columns": 1, "n_rows": 1, "levels_back": None, "primitives": (cgp.Parameter, ), } k_local_search = 2 pop = cgp.Population(**population_params, genome_params=genome_params) local_search = functools.partial(cgp.local_search.gradient_based, objective=inner_objective, **local_search_params) ea = cgp.ea.MuPlusLambda(**ea_params, local_search=local_search, k_local_search=k_local_search) ea.initialize_fitness_parents(pop, objective) ea.step(pop, objective) for idx in range(k_local_search): assert pop[idx].genome._parameter_names_to_values[ "<p1>"] != pytest.approx(1.0) for idx in range(k_local_search, population_params["n_parents"]): assert pop[idx].genome._parameter_names_to_values[ "<p1>"] == pytest.approx(1.0)
def test_min_fitness_deprecation(population_params, genome_params, ea_params): def objective(individual): individual.fitness = 1.0 return individual pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) with pytest.warns(DeprecationWarning): cgp.evolve(pop, objective, ea, min_fitness=2.0, max_generations=10) with pytest.raises(RuntimeError): cgp.evolve(pop, objective, ea, min_fitness=2.0, termination_fitness=1.5, max_generations=10)
def test_offspring_individuals_are_assigned_correct_parent_indices( population_params, genome_params, ea_params): def objective(ind): ind.fitness = 0.0 return ind population_params["n_parents"] = 1 pop = cgp.Population(**population_params, genome_params=genome_params) ea_params["tournament_size"] = 1 ea = cgp.ea.MuPlusLambda(**ea_params) ea.initialize_fitness_parents(pop, objective) offsprings = ea._create_new_offspring_generation(pop) for ind in offsprings: assert ind.parent_idx == 0
def test_step(population_params, genome_params, ea_params): def objective(individual): individual.fitness = float(individual.idx) return individual pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params) ea.initialize_fitness_parents(pop, objective) old_parent_ids = sorted([ind.idx for ind in pop.parents]) ea.step(pop, objective) new_parent_ids = sorted([ind.idx for ind in pop.parents]) # After one step, the new parent population should have IDs that # are offset from the old parent ids by n_offsprings # This is by construction in this test because the fitness is equal to the id assert all([ new_id == old_id + ea_params["n_offsprings"] for new_id, old_id in zip(new_parent_ids, old_parent_ids) ])
def test_speedup_parallel_evolve(population_params, genome_params, ea_params): # use 4 parents and 4 offsprings to achieve even load on 2, 4 # cores population_params["n_parents"] = 4 ea_params["n_offsprings"] = 4 evolve_params = {"max_generations": 5, "min_fitness": np.inf} # Number of calls to objective: Number of parents + (Number of # parents + offspring) * (N_generations - 1) Initially, we need to # compute the fitness for all parents. Then we compute the fitness # for each parents and offspring in each iteration. n_calls_objective = population_params["n_parents"] + ( population_params["n_parents"] + ea_params["n_offsprings"]) * (evolve_params["max_generations"] - 1) np.random.seed(population_params["seed"]) time_per_objective_call = 0.25 # Serial execution for n_processes in [1, 2, 4]: pop = cgp.Population(**population_params, genome_params=genome_params) ea = cgp.ea.MuPlusLambda(**ea_params, n_processes=n_processes) t0 = time.time() cgp.evolve(pop, _objective_speedup_parallel_evolve, ea, **evolve_params) T = time.time() - t0 if n_processes == 1: T_baseline = T # assert that total execution time is roughly equal to # number of objective calls x time per call; serves as a # baseline for subsequent parallel evolutions assert T == pytest.approx(n_calls_objective * time_per_objective_call, rel=0.25) else: # assert that multiprocessing roughly follows a linear speedup. assert T == pytest.approx(T_baseline / n_processes, rel=0.25)