def _mutation(self, space: TreeSpace) -> None: """Mutates a number of individuals pre-selected through a tournament procedure. Args: space: A TreeSpace object. """ # Calculates a list of current trees' fitness fitness = [agent.fit for agent in space.agents] # Number of individuals to be mutated n_individuals = int(space.n_agents * self.p_mutation) # Gathers a list of selected individuals to be replaced selected = g.tournament_selection(fitness, n_individuals) # For every selected individual for s in selected: # Gathers individual number of nodes n_nodes = space.trees[s].n_nodes # Checks if the tree has more than one node if n_nodes > 1: # Prunes the amount of maximum nodes max_nodes = self._prune_nodes(n_nodes) # Mutatets the individual space.trees[s] = self._mutate(space, space.trees[s], max_nodes) # If there is only one node else: # Re-create it with a random tree space.trees[s] = space.grow(space.min_depth, space.max_depth)
def _reproduction(self, space: TreeSpace) -> None: """Reproducts a number of individuals pre-selected through a tournament procedure (p. 99). Args: space: A TreeSpace object. """ # Calculates a list of current trees' fitness fitness = [agent.fit for agent in space.agents] # Number of individuals to be reproducted n_individuals = int(space.n_agents * self.p_reproduction) # Gathers a list of selected individuals to be replaced selected = g.tournament_selection(fitness, n_individuals) # For every selected individual for s in selected: # Gathers the worst individual index worst = np.argmax(fitness) # Replace the individual by performing a deep copy on selected tree space.trees[worst] = copy.deepcopy(space.trees[s]) # We also need to copy the agent space.agents[worst] = copy.deepcopy(space.agents[s]) # Replaces the worst individual fitness with a minimum value fitness[worst] = 0
def _mutate(self, space: TreeSpace, tree: Node, max_nodes: int) -> Node: """Actually performs the mutation on a single tree (p. 105). Args: space: A TreeSpace object. trees: A Node instance to be mutated. max_nodes: Maximum number of nodes to be searched. Returns: (Node): A mutated tree. """ # Deep copying a new mutated tree from initial tree mutated_tree = copy.deepcopy(tree) # Calculates mutation point mutation_point = int(r.generate_uniform_random_number(2, max_nodes)) # Finds the node at desired mutation point sub_tree, flag = mutated_tree.find_node(mutation_point) # If the mutation point's parent is not a root (this may happen when the mutation point is a function), # and find_node() stops at a terminal node whose father is a root if sub_tree: # Creates a new random sub-tree branch = space.grow(space.min_depth, space.max_depth) # Checks if sub-tree should be positioned in the left if flag: # The left child will receive the sub-branch sub_tree.left = branch # And its flag will be True branch.flag = True # If `flag` is False else: # The right child will receive the sub-branch sub_tree.right = branch # And its flag will be False branch.flag = False # Connects the sub-branch to its parent branch.parent = sub_tree # Otherwise, if condition is false else: # The mutated tree will be a random tree mutated_tree = space.grow(space.min_depth, space.max_depth) return mutated_tree
def optimize_gp(target, n_trees, n_terminals, n_variables, n_iterations, min_depth, max_depth, functions, lb, ub, hyperparams): """Abstracts Opytimizer's Genetic Programming into a single method. Args: target (callable): The method to be optimized. n_trees (int): Number of agents. n_terminals (int): Number of terminals n_variables (int): Number of variables. n_iterations (int): Number of iterations. min_depth (int): Minimum depth of trees. max_depth (int): Maximum depth of trees. functions (list): Functions' nodes. lb (list): List of lower bounds. ub (list): List of upper bounds. hyperparams (dict): Dictionary of hyperparameters. Returns: A History object containing all optimization's information. """ # Creating the TreeSpace space = TreeSpace(n_trees=n_trees, n_terminals=n_terminals, n_variables=n_variables, n_iterations=n_iterations, min_depth=min_depth, max_depth=max_depth, functions=functions, lower_bound=lb, upper_bound=ub) # Creating the Function function = Function(pointer=target) # Creating GP's optimizer optimizer = GP(hyperparams=hyperparams) # Creating the optimization task task = Opytimizer(space=space, optimizer=optimizer, function=function) return task.start(store_best_only=True)
def _crossover(self, space: TreeSpace) -> None: """Crossover a number of individuals pre-selected through a tournament procedure (p. 101). Args: space: A TreeSpace object. """ # Calculates a list of current trees' fitness fitness = [agent.fit for agent in space.agents] # Number of individuals to be crossovered n_individuals = int(space.n_agents * self.p_crossover) # Checks if `n_individuals` is an odd number if n_individuals % 2 != 0: # If it is, increase it by one n_individuals += 1 # Gathers a list of selected individuals to be replaced selected = g.tournament_selection(fitness, n_individuals) # For every pair in selected individuals for s in g.n_wise(selected): # Calculates the amount of father and mother nodes father_nodes = space.trees[s[0]].n_nodes mother_nodes = space.trees[s[1]].n_nodes # Checks if both trees have more than one node if (father_nodes > 1) and (mother_nodes > 1): # Prunning father and mother nodes max_f_nodes = self._prune_nodes(father_nodes) max_m_nodes = self._prune_nodes(mother_nodes) # Apply the crossover operation space.trees[s[0]], space.trees[s[1]] = self._cross( space.trees[s[0]], space.trees[s[1]], max_f_nodes, max_m_nodes )
max_depth = 5 # Function nodes func_nodes = ["SUM", "SUB", "MUL", "DIV"] # Also defines the corresponding lower and upper bounds # Note that they have to be the same size as `n_variables` lower_bound = [0.1, 0.3, 0.5, 0.7, 0.9] upper_bound = [0.2, 0.4, 0.6, 0.8, 1.0] # Creates the TreeSpace s = TreeSpace( n_agents, n_variables, lower_bound, upper_bound, n_terminals, min_depth, max_depth, func_nodes, ) # Prints out some properties print(s.trees[0]) print(f"Position: {s.trees[0].position}") print(f"\nPre Order: {s.trees[0].pre_order}") print(f"\nPost Order: {s.trees[0].post_order}") print( f"\nNodes: {s.trees[0].n_nodes} | Leaves: {s.trees[0].n_leaves} | " f"Minimum Depth: {s.trees[0].min_depth} | Maximum Depth: {s.trees[0].max_depth}" )
max_depth = 5 # List of functions nodes functions = ['SUM', 'MUL', 'DIV'] # Finally, we define the lower and upper bounds # Note that they have to be the same size as n_variables lower_bound = [-10, -10] upper_bound = [10, 10] # Creating the TreeSpace object s = TreeSpace(n_trees=n_trees, n_terminals=n_terminals, n_variables=n_variables, n_iterations=n_iterations, min_depth=min_depth, max_depth=max_depth, functions=functions, lower_bound=lower_bound, upper_bound=upper_bound) # Hyperparameters for the optimizer hyperparams = { 'p_reproduction': 0.25, 'p_mutation': 0.1, 'p_crossover': 0.2, 'prunning_ratio': 0.0 } # Creating GP's optimizer p = GP(hyperparams=hyperparams)