Source code for gama.genetic_programming.mutation

"""
Contains mutation functions for genetic programming.
Each mutation function takes an individual and modifies it in-place.
"""
import random
from functools import partial
from typing import Callable, Optional, cast, List, Dict

from gama.genetic_programming.components import PrimitiveNode
from .components import Individual, DATA_TERMINAL
from .operations import random_primitive_node


[docs]def mut_replace_terminal(individual: Individual, primitive_set: dict) -> None: """ Mutates an Individual in-place by replacing one of its Terminals. Parameters ---------- individual: Individual Individual to mutate in-place. primitive_set: dict """ def terminal_replaceable(index_terminal): _, terminal = index_terminal return len(primitive_set[terminal.identifier]) > 1 terminals = list(filter(terminal_replaceable, enumerate(individual.terminals))) if len(terminals) == 0: raise ValueError("Individual has no terminals suitable for mutation.") terminal_index, old = random.choice(terminals) candidates = filter(lambda t: t.value != old.value, primitive_set[old.identifier]) new_terminal = random.choice(list(candidates)) individual.replace_terminal(terminal_index, new_terminal)
[docs]def mut_replace_primitive(individual: Individual, primitive_set: dict) -> None: """ Mutates an Individual in-place by replacing one of its Primitives. Parameters ---------- individual: Individual Individual to mutate in-place. primitive_set: dict """ def primitive_replaceable(index_primitive): _, primitive = index_primitive return len(primitive_set[primitive._primitive.output]) > 1 primitives = list(filter(primitive_replaceable, enumerate(individual.primitives))) if primitives == 0: raise ValueError("Individual has no primitives suitable for replacement.") primitive_index, old_primitive_node = random.choice(primitives) primitive_node = random_primitive_node( output_type=old_primitive_node._primitive.output, primitive_set=primitive_set, exclude=old_primitive_node._primitive, ) individual.replace_primitive(primitive_index, primitive_node)
[docs]def mut_shrink( individual: Individual, primitive_set: dict = None, shrink_by: Optional[int] = None ) -> None: """ Mutates an Individual in-place by removing any number of primitive nodes. Primitive nodes are removed from the preprocessing end. Parameters ---------- individual: Individual Individual to mutate in-place. primitive_set: dict, optional Not used. Present to create a matching function signature with other mutations. shrink_by: int, optional (default=None) Number of primitives to remove. Must be at least one greater than the number of primitives in `individual`. If None, a random number of primitives is removed. """ n_primitives = len(list(individual.primitives)) if shrink_by is not None and n_primitives <= shrink_by: raise ValueError(f"Can't shrink size {n_primitives} individual by {shrink_by}.") if shrink_by is None: shrink_by = random.randint(1, n_primitives - 1) current_primitive_node = individual.main_node primitives_left = n_primitives - 1 while primitives_left > shrink_by: current_primitive_node = cast(PrimitiveNode, current_primitive_node._data_node) primitives_left -= 1 current_primitive_node._data_node = DATA_TERMINAL
[docs]def mut_insert(individual: Individual, primitive_set: dict) -> None: """ Mutate an Individual in-place by inserting a PrimitiveNode at a random location. The new PrimitiveNode will not be inserted as root node. Parameters ---------- individual: Individual Individual to mutate in-place. primitive_set: dict """ parent_node = random.choice(list(individual.primitives)) new_primitive_node = random_primitive_node( output_type=DATA_TERMINAL, primitive_set=primitive_set ) new_primitive_node._data_node = parent_node._data_node parent_node._data_node = new_primitive_node
[docs]def random_valid_mutation_in_place( individual: Individual, primitive_set: dict, max_length: Optional[int] = None ) -> Callable: """ Apply a random valid mutation in place. The random mutation can be one of: - mut_random_primitive - mut_random_terminal, if the individual has at least one - mutShrink, if individual has at least two primitives - mutInsert, if it would not exceed `new_max_length` when specified. Parameters ---------- individual: Individual An individual to be mutated *in-place*. primitive_set: dict A dictionary defining the set of primitives and terminals. max_length: int, optional (default=None) If specified, impose a maximum length on the new individual. Returns ------- Callable The mutation function used. """ n_primitives = len(list(individual.primitives)) available_mutations: List[Callable[[Individual, Dict], None]] = [] if max_length is not None and n_primitives > max_length: available_mutations.append( partial(mut_shrink, shrink_by=n_primitives - max_length) ) else: available_mutations.append(mut_replace_primitive) if max_length is None or n_primitives < max_length: available_mutations.append(mut_insert) if n_primitives > 1: available_mutations.append(mut_shrink) replaceable_terminals = filter( lambda t: len(primitive_set[t.identifier]) > 1, individual.terminals ) if len(list(replaceable_terminals)) > 1: available_mutations.append(mut_replace_terminal) mut_fn = random.choice(available_mutations) mut_fn(individual, primitive_set) return mut_fn