This commit is contained in:
2025-11-07 12:54:27 +03:00
parent 74e02df205
commit bacfa20061
3 changed files with 110 additions and 95 deletions

View File

@@ -17,7 +17,7 @@ type FitnessFn = Callable[[Chromosome], float]
type InitializePopulationFn = Callable[[int], Population] type InitializePopulationFn = Callable[[int], Population]
type CrossoverFn = Callable[[Chromosome, Chromosome], tuple[Chromosome, Chromosome]] type CrossoverFn = Callable[[Chromosome, Chromosome], tuple[Chromosome, Chromosome]]
type MutationFn = Callable[[Chromosome, int], Chromosome] type MutationFn = Callable[[Chromosome], Chromosome]
type SelectionFn = Callable[[Population, Fitnesses], Population] type SelectionFn = Callable[[Population, Fitnesses], Population]
@@ -132,7 +132,7 @@ def mutation(
next_population = [] next_population = []
for chrom in population: for chrom in population:
next_population.append( next_population.append(
mutation_fn(chrom, gen_num) if np.random.random() <= pm else chrom mutation_fn(chrom) if np.random.random() <= pm else chrom
) )
return next_population return next_population

View File

@@ -1,45 +1,58 @@
import random import random
from abc import ABC, abstractmethod
from typing import Sequence
from .chromosome import Chromosome from .chromosome import Chromosome
def shrink_mutation(chromosome: Chromosome) -> Chromosome: class BaseMutation(ABC):
@abstractmethod
def mutate(self, chromosome: Chromosome) -> Chromosome: ...
def __call__(self, chromosome: Chromosome) -> Chromosome:
chromosome = chromosome.copy()
return self.mutate(chromosome)
class ShrinkMutation(BaseMutation):
"""Усекающая мутация. Заменяет случайно выбранную операцию на случайный терминал.""" """Усекающая мутация. Заменяет случайно выбранную операцию на случайный терминал."""
chromosome = chromosome.copy()
operation_nodes = [n for n in chromosome.root.list_nodes() if n.value.arity > 0] def mutate(self, chromosome: Chromosome) -> Chromosome:
operation_nodes = [n for n in chromosome.root.list_nodes() if n.value.arity > 0]
if not operation_nodes:
return chromosome
target_node = random.choice(operation_nodes)
target_node.prune(chromosome.terminals, max_depth=1)
if not operation_nodes:
return chromosome return chromosome
target_node = random.choice(operation_nodes)
target_node.prune(chromosome.terminals, max_depth=1) class GrowMutation(BaseMutation):
return chromosome
def grow_mutation(chromosome: Chromosome, max_depth: int) -> Chromosome:
"""Растущая мутация. Заменяет случайно выбранный узел на случайное поддерево.""" """Растущая мутация. Заменяет случайно выбранный узел на случайное поддерево."""
chromosome = chromosome.copy()
target_node = random.choice(chromosome.root.list_nodes()) def __init__(self, max_depth: int):
self.max_depth = max_depth
max_subtree_depth = max_depth - target_node.get_level() + 1 def mutate(self, chromosome: Chromosome) -> Chromosome:
target_node = random.choice(chromosome.root.list_nodes())
subtree = Chromosome.grow_init( max_subtree_depth = self.max_depth - target_node.get_level() + 1
chromosome.terminals, chromosome.operations, max_subtree_depth
).root
if target_node.parent: subtree = Chromosome.grow_init(
target_node.parent.replace_child(target_node, subtree) chromosome.terminals, chromosome.operations, max_subtree_depth
else: ).root
chromosome.root = subtree
return chromosome if target_node.parent:
target_node.parent.replace_child(target_node, subtree)
else:
chromosome.root = subtree
return chromosome
def node_replacement_mutation(chromosome: Chromosome) -> Chromosome: class NodeReplacementMutation(BaseMutation):
"""Мутация замены операции (Node Replacement Mutation). """Мутация замены операции (Node Replacement Mutation).
Выбирает случайный узел и заменяет его Выбирает случайный узел и заменяет его
@@ -47,48 +60,72 @@ def node_replacement_mutation(chromosome: Chromosome) -> Chromosome:
Если подходящей альтернативы нет — возвращает копию без изменений. Если подходящей альтернативы нет — возвращает копию без изменений.
""" """
chromosome = chromosome.copy()
target_node = random.choice(chromosome.root.list_nodes()) def mutate(self, chromosome: Chromosome) -> Chromosome:
current_arity = target_node.value.arity target_node = random.choice(chromosome.root.list_nodes())
current_arity = target_node.value.arity
same_arity = [
op
for op in list(chromosome.operations) + list(chromosome.terminals)
if op.arity == current_arity and op != target_node.value
]
if not same_arity:
return chromosome
new_operation = random.choice(same_arity)
target_node.value = new_operation
same_arity = [
op
for op in list(chromosome.operations) + list(chromosome.terminals)
if op.arity == current_arity and op != target_node.value
]
if not same_arity:
return chromosome return chromosome
new_operation = random.choice(same_arity)
target_node.value = new_operation class HoistMutation(BaseMutation):
def mutate(self, chromosome: Chromosome) -> Chromosome:
"""Hoist-мутация (анти-bloat).
return chromosome Выбирает случайное поддерево, затем внутри него — случайное поддерево меньшей
глубины, и заменяет исходное поддерево на это внутреннее.
В результате дерево становится короче, сохраняя часть структуры.
"""
operation_nodes = [n for n in chromosome.root.list_nodes() if n.value.arity > 0]
if not operation_nodes:
return chromosome
outer_subtree = random.choice(operation_nodes)
outer_nodes = outer_subtree.list_nodes()[1:] # исключаем корень
inner_subtree = random.choice(outer_nodes).copy_subtree()
if outer_subtree.parent:
outer_subtree.parent.replace_child(outer_subtree, inner_subtree)
else:
chromosome.root = inner_subtree
return chromosome
def hoist_mutation(chromosome: Chromosome) -> Chromosome: class CombinedMutation(BaseMutation):
"""Hoist-мутация (анти-bloat). """Комбинированная мутация.
Выбирает случайное поддерево, затем внутри него — случайное поддерево меньшей глубины, Принимает список (или словарь) мутаций и случайно выбирает одну из них
и заменяет исходное поддерево на это внутреннее. для применения. Можно задать веса вероятностей.
В результате дерево становится короче, сохраняя часть структуры.
""" """
chromosome = chromosome.copy()
operation_nodes = [n for n in chromosome.root.list_nodes() if n.value.arity > 0] def __init__(
if not operation_nodes: self, mutations: Sequence[BaseMutation], probs: Sequence[float] | None = None
return chromosome ):
if probs is not None:
assert abs(sum(probs) - 1.0) < 1e-8, (
"Сумма вероятностей должна быть равна 1"
)
assert len(probs) == len(mutations), (
"Число вероятностей должно совпадать с числом мутаций"
)
self.mutations = mutations
self.probs = probs
outer_subtree = random.choice(operation_nodes) def mutate(self, chromosome: Chromosome) -> Chromosome:
outer_nodes = outer_subtree.list_nodes()[1:] # исключаем корень mutation = random.choices(self.mutations, weights=self.probs, k=1)[0]
return mutation(chromosome)
inner_subtree = random.choice(outer_nodes).copy_subtree()
if outer_subtree.parent:
outer_subtree.parent.replace_child(outer_subtree, inner_subtree)
else:
chromosome.root = inner_subtree
return chromosome

View File

@@ -15,10 +15,11 @@ from gp.fitness import (
) )
from gp.ga import GARunConfig, genetic_algorithm from gp.ga import GARunConfig, genetic_algorithm
from gp.mutations import ( from gp.mutations import (
grow_mutation, CombinedMutation,
hoist_mutation, GrowMutation,
node_replacement_mutation, HoistMutation,
shrink_mutation, NodeReplacementMutation,
ShrinkMutation,
) )
from gp.ops import ADD, COS, DIV, EXP, MUL, NEG, POW, SIN, SQUARE, SUB from gp.ops import ADD, COS, DIV, EXP, MUL, NEG, POW, SIN, SQUARE, SUB
from gp.population import ramped_initialization from gp.population import ramped_initialization
@@ -36,7 +37,6 @@ X = np.random.uniform(-5.536, 5.536, size=(TEST_POINTS, NUM_VARS))
# axes = [np.linspace(-5.536, 5.536, TEST_POINTS) for _ in range(NUM_VARS)] # axes = [np.linspace(-5.536, 5.536, TEST_POINTS) for _ in range(NUM_VARS)]
# X = np.array(np.meshgrid(*axes)).T.reshape(-1, NUM_VARS) # X = np.array(np.meshgrid(*axes)).T.reshape(-1, NUM_VARS)
operations = [SQUARE, SIN, COS, EXP, ADD, SUB, MUL, DIV, POW] operations = [SQUARE, SIN, COS, EXP, ADD, SUB, MUL, DIV, POW]
# operations = [SQUARE, ADD, SUB, MUL]
terminals = [Var(f"x{i}") for i in range(1, NUM_VARS + 1)] terminals = [Var(f"x{i}") for i in range(1, NUM_VARS + 1)]
@@ -53,36 +53,16 @@ def target_function(x: NDArray[np.float64]) -> NDArray[np.float64]:
return np.sum(prefix_sums, axis=1) return np.sum(prefix_sums, axis=1)
# fitness_function = MSEFitness(target_function, lambda: X)
# fitness_function = HuberFitness(target_function, lambda: X, delta=0.5)
# fitness_function = PenalizedFitness(
# target_function, lambda: X, base_fitness=fitness, lambda_=0.1
# )
# fitness_function = NRMSEFitness(target_function, lambda: X)
fitness_function = RMSEFitness(target_function, lambda: X) fitness_function = RMSEFitness(target_function, lambda: X)
combined_mutation = CombinedMutation(
# fitness_function = PenalizedFitness( mutations=[
# target_function, lambda: X, base_fitness=fitness_function, lambda_=0.0001 GrowMutation(max_depth=MAX_DEPTH),
# ) NodeReplacementMutation(),
HoistMutation(),
ShrinkMutation(),
def adaptive_mutation( ],
chromosome: Chromosome, probs=[0.4, 0.3, 0.15, 0.15],
generation: int, )
max_generations: int,
max_depth: int,
) -> Chromosome:
r = random.random()
if r < 0.4:
return grow_mutation(chromosome, max_depth=max_depth)
elif r < 0.7:
return node_replacement_mutation(chromosome)
elif r < 0.85:
return hoist_mutation(chromosome)
return shrink_mutation(chromosome)
init_population = ramped_initialization( init_population = ramped_initialization(
20, [i for i in range(MAX_DEPTH - 9, MAX_DEPTH + 1)], terminals, operations 20, [i for i in range(MAX_DEPTH - 9, MAX_DEPTH + 1)], terminals, operations
@@ -93,9 +73,7 @@ print("Population size:", len(init_population))
config = GARunConfig( config = GARunConfig(
fitness_func=fitness_function, fitness_func=fitness_function,
crossover_fn=lambda p1, p2: crossover_subtree(p1, p2, max_depth=MAX_DEPTH), crossover_fn=lambda p1, p2: crossover_subtree(p1, p2, max_depth=MAX_DEPTH),
mutation_fn=lambda chrom, gen_num: adaptive_mutation( mutation_fn=combined_mutation,
chrom, gen_num, MAX_GENERATIONS, MAX_DEPTH
),
# selection_fn=roulette_selection, # selection_fn=roulette_selection,
selection_fn=lambda p, f: tournament_selection(p, f, k=3), selection_fn=lambda p, f: tournament_selection(p, f, k=3),
init_population=init_population, init_population=init_population,