From cfae423f11610f41627be9413bf79e4cf92539a1 Mon Sep 17 00:00:00 2001 From: Arity-T Date: Fri, 7 Nov 2025 00:08:08 +0300 Subject: [PATCH] best for now RMSE: 30.937 --- lab4/gp/mutations.py | 59 +++++++++++++++++++++++++++++++++++++++++++ lab4/gp/node.py | 1 + lab4/gp/selection.py | 60 ++++++++++++++++++++++++++++++++++++++++++++ lab4/main.py | 30 ++++++++++++++-------- 4 files changed, 140 insertions(+), 10 deletions(-) diff --git a/lab4/gp/mutations.py b/lab4/gp/mutations.py index 5b1a16f..cd125bc 100644 --- a/lab4/gp/mutations.py +++ b/lab4/gp/mutations.py @@ -37,3 +37,62 @@ def grow_mutation(chromosome: Chromosome, max_depth: int) -> Chromosome: chromosome.root = subtree return chromosome + + +def node_replacement_mutation(chromosome: Chromosome) -> Chromosome: + """Мутация замены операции (Node Replacement Mutation). + + Выбирает случайный узел с операцией (arity > 0) и заменяет его + на случайную другую операцию той же арности, сохраняя поддеревья. + + Если подходящей альтернативы нет — возвращает копию без изменений. + """ + chromosome = chromosome.copy() + + 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) + current_arity = target_node.value.arity + + same_arity_ops = [ + op + for op in chromosome.operations + if op.arity == current_arity and op != target_node.value + ] + if not same_arity_ops: + return chromosome + + new_operation = random.choice(same_arity_ops) + + target_node.value = new_operation + + return chromosome + + +def hoist_mutation(chromosome: Chromosome) -> Chromosome: + """Hoist-мутация (анти-bloat). + + Выбирает случайное поддерево, затем внутри него — случайное поддерево меньшей глубины, + и заменяет исходное поддерево на это внутреннее. + + В результате дерево становится короче, сохраняя часть структуры. + """ + chromosome = chromosome.copy() + + 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 diff --git a/lab4/gp/node.py b/lab4/gp/node.py index fc65fca..58b5a6d 100644 --- a/lab4/gp/node.py +++ b/lab4/gp/node.py @@ -36,6 +36,7 @@ class Node: return node def list_nodes(self) -> list[Node]: + """Список всех узлов поддерева, начиная с текущего (aka depth-first-search).""" nodes: list[Node] = [self] for child in self.children: nodes.extend(child.list_nodes()) diff --git a/lab4/gp/selection.py b/lab4/gp/selection.py index ec0c253..d22bfea 100644 --- a/lab4/gp/selection.py +++ b/lab4/gp/selection.py @@ -26,3 +26,63 @@ def roulette_selection(population: Population, fitnesses: Fitnesses) -> Populati selected.append(population[idx]) return selected + + +def tournament_selection( + population: Population, + fitnesses: Fitnesses, + k: int = 3, +) -> Population: + """Турнирная селекция. + + В каждом турнире случайно выбирается k особей, и побеждает та, + у которой лучшее (наибольшее) значение фитнеса. Для минимизации + значения фитнеса нужно предварительно инвертировать. + + Args: + population: список особей (Population) + fitnesses: список или массив фитнесов (Fitnesses) + k: размер турнира + + Returns: + Новая популяция того же размера + """ + size = len(population) + selected = [] + for _ in range(size): + idxs = np.random.choice(size, size=k, replace=False) + + fits = fitnesses[idxs] + + winner_idx = idxs[np.argmax(fits)] + selected.append(population[winner_idx]) + return selected + + +def stochastic_tournament_selection( + population: Population, + fitnesses: Fitnesses, + k: int = 3, + p_best: float = 0.75, +) -> Population: + """Стохастическая турнирная селекция. + + Побеждает лучший в турнире с вероятностью p_best, иначе выбирается + случайный участник турнира. + """ + size = len(population) + selected = [] + + for _ in range(size): + idxs = np.random.choice(size, size=k, replace=False) + fits = fitnesses[idxs] + order = np.argsort(-fits) + + if np.random.random() < p_best: + winner_idx = idxs[order[0]] + else: + winner_idx = np.random.choice(idxs[1:]) if k > 1 else idxs[0] + + selected.append(population[winner_idx]) + + return selected diff --git a/lab4/main.py b/lab4/main.py index d386d10..1dd82e1 100644 --- a/lab4/main.py +++ b/lab4/main.py @@ -15,16 +15,21 @@ from gp.fitness import ( RMSEFitness, ) from gp.ga import GARunConfig, genetic_algorithm -from gp.mutations import grow_mutation, shrink_mutation +from gp.mutations import ( + grow_mutation, + hoist_mutation, + node_replacement_mutation, + shrink_mutation, +) from gp.ops import ADD, COS, DIV, EXP, MUL, NEG, POW, SIN, SQUARE, SUB from gp.population import ramped_initialization from gp.primitive import Const, Var -from gp.selection import roulette_selection +from gp.selection import roulette_selection, tournament_selection NUM_VARS = 9 TEST_POINTS = 10000 -MAX_DEPTH = 13 -MAX_GENERATIONS = 500 +MAX_DEPTH = 15 +MAX_GENERATIONS = 200 np.random.seed(17) random.seed(17) X = np.random.uniform(-5.536, 5.536, size=(TEST_POINTS, NUM_VARS)) @@ -81,7 +86,7 @@ def target_function(x: NDArray[np.float64]) -> NDArray[np.float64]: # fitness_function = PenalizedFitness( # target_function, lambda: X, base_fitness=fitness, lambda_=0.003 # ) -fitness_function = HuberFitness(target_function, lambda: X) +fitness_function = RMSEFitness(target_function, lambda: X) # fitness_function = PenalizedFitness( # target_function, lambda: X, base_fitness=fitness, lambda_=0.003 # ) @@ -103,9 +108,13 @@ def adaptive_mutation( r = random.random() - # 50% grow, 50% shrink - if r < 0.5: + 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) @@ -151,14 +160,15 @@ config = GARunConfig( mutation_fn=lambda chrom, gen_num: adaptive_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), init_population=ramped_initialization( - 15, [4, 5, 6, 6, 7, 7, 8, 9, 10, 11], terminals, operations + 10, [4, 5, 6, 6, 7, 7, 8, 9, 10, 11], terminals, operations ), seed=17, pc=0.9, pm=0.3, - elitism=30, + elitism=10, max_generations=MAX_GENERATIONS, log_every_generation=True, )