Compare commits
3 Commits
74e02df205
...
lab4
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ec38a3385 | |||
| 4b2398ae05 | |||
| bacfa20061 |
50
lab4/draw_tree.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from graphviz import Digraph
|
||||
|
||||
|
||||
def make_pow2_sum_tree(n=8):
|
||||
dot = Digraph("FullTree")
|
||||
dot.attr(rankdir="TB") # направление сверху вниз
|
||||
dot.attr("node", shape="circle", style="filled", fillcolor="lightgray")
|
||||
|
||||
node_count = 0
|
||||
|
||||
def new_node(label):
|
||||
nonlocal node_count
|
||||
node_id = f"n{node_count}"
|
||||
node_count += 1
|
||||
dot.node(node_id, label)
|
||||
return node_id
|
||||
|
||||
def pow2_node(xi):
|
||||
n1 = new_node("pow2")
|
||||
n2 = new_node(xi)
|
||||
dot.edge(n1, n2)
|
||||
return n1
|
||||
|
||||
def plus(a, b):
|
||||
n = new_node("+")
|
||||
dot.edge(n, a)
|
||||
dot.edge(n, b)
|
||||
return n
|
||||
|
||||
all_terms = []
|
||||
for i in range(1, n + 1):
|
||||
terms = [pow2_node(f"x{j}") for j in range(1, i + 1)]
|
||||
s = terms[0]
|
||||
for t in terms[1:]:
|
||||
s = plus(s, t)
|
||||
all_terms.append(s)
|
||||
|
||||
root = all_terms[0]
|
||||
for t in all_terms[1:]:
|
||||
root = plus(root, t)
|
||||
|
||||
dot.node("root", "f(x)")
|
||||
dot.edge("root", root)
|
||||
|
||||
return dot
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
g = make_pow2_sum_tree(8)
|
||||
g.render("original_tree", format="png", cleanup=True)
|
||||
@@ -1,3 +0,0 @@
|
||||
from .chromosome import Chromosome
|
||||
|
||||
__all__ = ["Chromosome"]
|
||||
|
||||
123
lab4/gp/ga.py
@@ -6,18 +6,19 @@ from copy import deepcopy
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Callable
|
||||
|
||||
import graphviz
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from .chromosome import Chromosome
|
||||
from .node import Node
|
||||
from .types import Fitnesses, Population
|
||||
|
||||
type FitnessFn = Callable[[Chromosome], float]
|
||||
|
||||
type InitializePopulationFn = Callable[[int], Population]
|
||||
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]
|
||||
|
||||
|
||||
@@ -66,6 +67,7 @@ class Generation:
|
||||
number: int
|
||||
best: Chromosome
|
||||
best_fitness: float
|
||||
avg_fitness: float
|
||||
population: Population
|
||||
fitnesses: Fitnesses
|
||||
|
||||
@@ -132,7 +134,7 @@ def mutation(
|
||||
next_population = []
|
||||
for chrom in population:
|
||||
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
|
||||
|
||||
@@ -148,27 +150,45 @@ def eval_population(population: Population, fitness_func: FitnessFn) -> Fitnesse
|
||||
return np.array([fitness_func(chrom) for chrom in population])
|
||||
|
||||
|
||||
def render_tree_to_graphviz(
|
||||
node: Node, graph: graphviz.Digraph, node_id: str = "0"
|
||||
) -> None:
|
||||
"""Рекурсивно добавляет узлы дерева в graphviz граф."""
|
||||
graph.node(node_id, label=node.value.name)
|
||||
|
||||
for i, child in enumerate(node.children):
|
||||
child_id = f"{node_id}_{i}"
|
||||
render_tree_to_graphviz(child, graph, child_id)
|
||||
graph.edge(node_id, child_id)
|
||||
|
||||
|
||||
def save_generation(
|
||||
generation: Generation, history: list[Generation], config: GARunConfig
|
||||
) -> None:
|
||||
"""Сохраняет визуализацию лучшей хромосомы поколения в виде дерева."""
|
||||
os.makedirs(config.results_dir, exist_ok=True)
|
||||
|
||||
fig = plt.figure(figsize=(7, 7))
|
||||
fig.suptitle(
|
||||
f"Поколение #{generation.number}. "
|
||||
f"Лучшая особь: {generation.best_fitness:.0f}. "
|
||||
f"Среднее значение: {np.mean(generation.fitnesses):.0f}",
|
||||
fontsize=14,
|
||||
y=0.95,
|
||||
# Создаем граф для визуализации дерева
|
||||
dot = graphviz.Digraph(comment=f"Generation {generation.number}")
|
||||
dot.attr(rankdir="TB") # Top to Bottom direction
|
||||
dot.attr("node", shape="circle", style="filled", fillcolor="lightblue")
|
||||
|
||||
# Добавляем заголовок
|
||||
depth = generation.best.root.get_depth()
|
||||
title = (
|
||||
f"Поколение #{generation.number}\\n"
|
||||
f"Лучшая особь: {generation.best_fitness:.4f}\\n"
|
||||
f"Глубина дерева: {depth}"
|
||||
)
|
||||
dot.attr(label=title, labelloc="t", fontsize="14")
|
||||
|
||||
# Рисуем
|
||||
...
|
||||
# Рендерим дерево
|
||||
render_tree_to_graphviz(generation.best.root, dot)
|
||||
|
||||
filename = f"generation_{generation.number:03d}.png"
|
||||
path_png = os.path.join(config.results_dir, filename)
|
||||
fig.savefig(path_png, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
# Сохраняем
|
||||
filename = f"generation_{generation.number:03d}"
|
||||
filepath = os.path.join(config.results_dir, filename)
|
||||
dot.render(filepath, format="png", cleanup=True)
|
||||
|
||||
|
||||
def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
||||
@@ -216,6 +236,7 @@ def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
||||
number=generation_number,
|
||||
best=population[best_index],
|
||||
best_fitness=fitnesses[best_index],
|
||||
avg_fitness=float(np.mean(fitnesses)),
|
||||
# population=deepcopy(population),
|
||||
population=[],
|
||||
# fitnesses=deepcopy(fitnesses),
|
||||
@@ -301,41 +322,61 @@ def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
||||
end = time.perf_counter()
|
||||
|
||||
assert best is not None, "Best was never set"
|
||||
return GARunResult(
|
||||
result = GARunResult(
|
||||
len(history),
|
||||
best,
|
||||
history,
|
||||
(end - start) * 1000.0,
|
||||
)
|
||||
|
||||
# Автоматически строим графики истории фитнеса
|
||||
if config.save_generations:
|
||||
plot_fitness_history(result, save_dir=config.results_dir)
|
||||
|
||||
def plot_fitness_history(result: GARunResult, save_path: str | None = None) -> None:
|
||||
"""Рисует график изменения лучших и средних значений фитнеса по поколениям."""
|
||||
return result
|
||||
|
||||
|
||||
def plot_fitness_history(result: GARunResult, save_dir: str | None = None) -> None:
|
||||
"""Рисует графики изменения лучших и средних значений фитнеса по поколениям.
|
||||
|
||||
Создает два отдельных графика:
|
||||
- fitness_best.png - график лучших значений
|
||||
- fitness_avg.png - график средних значений
|
||||
"""
|
||||
generations = [gen.number for gen in result.history]
|
||||
best_fitnesses = [gen.best_fitness for gen in result.history]
|
||||
avg_fitnesses = [np.mean(gen.fitnesses) for gen in result.history]
|
||||
avg_fitnesses = [gen.avg_fitness for gen in result.history]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
# График лучших значений
|
||||
fig_best, ax_best = plt.subplots(figsize=(10, 6))
|
||||
ax_best.plot(generations, best_fitnesses, linewidth=2, color="blue")
|
||||
ax_best.set_xlabel("Поколение", fontsize=12)
|
||||
ax_best.set_ylabel("Лучшее значение фитнес-функции", fontsize=12)
|
||||
ax_best.set_title("Лучшее значение фитнеса по поколениям", fontsize=14)
|
||||
ax_best.grid(True, alpha=0.3)
|
||||
|
||||
ax.plot(
|
||||
generations, best_fitnesses, label="Лучшее значение", linewidth=2, color="blue"
|
||||
)
|
||||
ax.plot(
|
||||
generations,
|
||||
avg_fitnesses,
|
||||
label="Среднее значение",
|
||||
linewidth=2,
|
||||
color="orange",
|
||||
)
|
||||
|
||||
ax.set_xlabel("Поколение", fontsize=12)
|
||||
ax.set_ylabel("Значение фитнес-функции", fontsize=12)
|
||||
ax.legend(fontsize=11)
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
if save_path:
|
||||
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
||||
print(f"График сохранен в {save_path}")
|
||||
if save_dir:
|
||||
best_path = os.path.join(save_dir, "fitness_best.png")
|
||||
fig_best.savefig(best_path, dpi=150, bbox_inches="tight")
|
||||
print(f"График лучших значений сохранен в {best_path}")
|
||||
else:
|
||||
plt.show()
|
||||
plt.close(fig)
|
||||
|
||||
plt.close(fig_best)
|
||||
|
||||
# График средних значений
|
||||
fig_avg, ax_avg = plt.subplots(figsize=(10, 6))
|
||||
ax_avg.plot(generations, avg_fitnesses, linewidth=2, color="orange")
|
||||
ax_avg.set_xlabel("Поколение", fontsize=12)
|
||||
ax_avg.set_ylabel("Среднее значение фитнес-функции", fontsize=12)
|
||||
ax_avg.set_title("Среднее значение фитнеса по поколениям", fontsize=14)
|
||||
ax_avg.grid(True, alpha=0.3)
|
||||
|
||||
if save_dir:
|
||||
avg_path = os.path.join(save_dir, "fitness_avg.png")
|
||||
fig_avg.savefig(avg_path, dpi=150, bbox_inches="tight")
|
||||
print(f"График средних значений сохранен в {avg_path}")
|
||||
else:
|
||||
plt.show()
|
||||
|
||||
plt.close(fig_avg)
|
||||
|
||||
@@ -1,45 +1,58 @@
|
||||
import random
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Sequence
|
||||
|
||||
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
|
||||
|
||||
target_node = random.choice(operation_nodes)
|
||||
|
||||
target_node.prune(chromosome.terminals, max_depth=1)
|
||||
|
||||
return chromosome
|
||||
|
||||
|
||||
def grow_mutation(chromosome: Chromosome, max_depth: int) -> Chromosome:
|
||||
class GrowMutation(BaseMutation):
|
||||
"""Растущая мутация. Заменяет случайно выбранный узел на случайное поддерево."""
|
||||
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(
|
||||
chromosome.terminals, chromosome.operations, max_subtree_depth
|
||||
).root
|
||||
max_subtree_depth = self.max_depth - target_node.get_level() + 1
|
||||
|
||||
if target_node.parent:
|
||||
target_node.parent.replace_child(target_node, subtree)
|
||||
else:
|
||||
chromosome.root = subtree
|
||||
subtree = Chromosome.grow_init(
|
||||
chromosome.terminals, chromosome.operations, max_subtree_depth
|
||||
).root
|
||||
|
||||
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).
|
||||
|
||||
Выбирает случайный узел и заменяет его
|
||||
@@ -47,48 +60,72 @@ def node_replacement_mutation(chromosome: Chromosome) -> Chromosome:
|
||||
|
||||
Если подходящей альтернативы нет — возвращает копию без изменений.
|
||||
"""
|
||||
chromosome = chromosome.copy()
|
||||
|
||||
target_node = random.choice(chromosome.root.list_nodes())
|
||||
current_arity = target_node.value.arity
|
||||
def mutate(self, chromosome: Chromosome) -> Chromosome:
|
||||
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
|
||||
|
||||
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:
|
||||
"""Hoist-мутация (анти-bloat).
|
||||
class CombinedMutation(BaseMutation):
|
||||
"""Комбинированная мутация.
|
||||
|
||||
Выбирает случайное поддерево, затем внутри него — случайное поддерево меньшей глубины,
|
||||
и заменяет исходное поддерево на это внутреннее.
|
||||
|
||||
В результате дерево становится короче, сохраняя часть структуры.
|
||||
Принимает список (или словарь) мутаций и случайно выбирает одну из них
|
||||
для применения. Можно задать веса вероятностей.
|
||||
"""
|
||||
chromosome = chromosome.copy()
|
||||
|
||||
operation_nodes = [n for n in chromosome.root.list_nodes() if n.value.arity > 0]
|
||||
if not operation_nodes:
|
||||
return chromosome
|
||||
def __init__(
|
||||
self, mutations: Sequence[BaseMutation], probs: Sequence[float] | None = None
|
||||
):
|
||||
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)
|
||||
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 mutate(self, chromosome: Chromosome) -> Chromosome:
|
||||
mutation = random.choices(self.mutations, weights=self.probs, k=1)[0]
|
||||
return mutation(chromosome)
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
from typing import Sequence
|
||||
|
||||
from .chromosome import Chromosome
|
||||
from .primitive import Primitive
|
||||
|
||||
type Population = list[Chromosome]
|
||||
|
||||
|
||||
def ramped_initialization(
|
||||
chromosomes_per_variation: int,
|
||||
depths: list[int],
|
||||
terminals: Sequence[Primitive],
|
||||
operations: Sequence[Primitive],
|
||||
) -> Population:
|
||||
"""Комбинация методов grow и full инициализации хромосом для инициализации начальной
|
||||
популяции.
|
||||
"""
|
||||
population: Population = []
|
||||
|
||||
for depth in depths:
|
||||
population.extend(
|
||||
Chromosome.full_init(terminals, operations, depth)
|
||||
for _ in range(chromosomes_per_variation)
|
||||
)
|
||||
population.extend(
|
||||
Chromosome.grow_init(terminals, operations, depth)
|
||||
for _ in range(chromosomes_per_variation)
|
||||
)
|
||||
|
||||
return population
|
||||
72
lab4/main.py
@@ -1,29 +1,34 @@
|
||||
"""
|
||||
graphviz должен быть доступен в PATH (недостаточно просто установить через pip)
|
||||
|
||||
Можно проверить командой
|
||||
dot -V
|
||||
"""
|
||||
|
||||
import random
|
||||
from math import log
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from gp import Chromosome
|
||||
from gp.crossovers import crossover_subtree
|
||||
from gp.fitness import (
|
||||
MAEFitness,
|
||||
MSEFitness,
|
||||
NRMSEFitness,
|
||||
PenalizedFitness,
|
||||
RMSEFitness,
|
||||
)
|
||||
from gp.ga import GARunConfig, genetic_algorithm
|
||||
from gp.mutations import (
|
||||
grow_mutation,
|
||||
hoist_mutation,
|
||||
node_replacement_mutation,
|
||||
shrink_mutation,
|
||||
CombinedMutation,
|
||||
GrowMutation,
|
||||
HoistMutation,
|
||||
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, POW, SIN, SQUARE, SUB
|
||||
from gp.population import ramped_initialization
|
||||
from gp.primitive import Const, Var
|
||||
from gp.selection import roulette_selection, tournament_selection
|
||||
from gp.primitive import Var
|
||||
from gp.selection import tournament_selection
|
||||
|
||||
NUM_VARS = 8
|
||||
TEST_POINTS = 10000
|
||||
@@ -33,10 +38,7 @@ SEED = 17
|
||||
np.random.seed(SEED)
|
||||
random.seed(SEED)
|
||||
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)]
|
||||
# X = np.array(np.meshgrid(*axes)).T.reshape(-1, NUM_VARS)
|
||||
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)]
|
||||
|
||||
|
||||
@@ -53,36 +55,16 @@ def target_function(x: NDArray[np.float64]) -> NDArray[np.float64]:
|
||||
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 = PenalizedFitness(
|
||||
# target_function, lambda: X, base_fitness=fitness_function, lambda_=0.0001
|
||||
# )
|
||||
|
||||
|
||||
def adaptive_mutation(
|
||||
chromosome: Chromosome,
|
||||
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)
|
||||
|
||||
combined_mutation = CombinedMutation(
|
||||
mutations=[
|
||||
GrowMutation(max_depth=MAX_DEPTH),
|
||||
NodeReplacementMutation(),
|
||||
HoistMutation(),
|
||||
ShrinkMutation(),
|
||||
],
|
||||
probs=[0.4, 0.3, 0.15, 0.15],
|
||||
)
|
||||
|
||||
init_population = ramped_initialization(
|
||||
20, [i for i in range(MAX_DEPTH - 9, MAX_DEPTH + 1)], terminals, operations
|
||||
@@ -93,10 +75,7 @@ print("Population size:", len(init_population))
|
||||
config = GARunConfig(
|
||||
fitness_func=fitness_function,
|
||||
crossover_fn=lambda p1, p2: crossover_subtree(p1, p2, max_depth=MAX_DEPTH),
|
||||
mutation_fn=lambda chrom, gen_num: adaptive_mutation(
|
||||
chrom, gen_num, MAX_GENERATIONS, MAX_DEPTH
|
||||
),
|
||||
# selection_fn=roulette_selection,
|
||||
mutation_fn=combined_mutation,
|
||||
selection_fn=lambda p, f: tournament_selection(p, f, k=3),
|
||||
init_population=init_population,
|
||||
seed=SEED,
|
||||
@@ -105,6 +84,7 @@ config = GARunConfig(
|
||||
elitism=15,
|
||||
max_generations=MAX_GENERATIONS,
|
||||
log_every_generation=True,
|
||||
save_generations=[1, 10, 20, 30, 40, 50, 100, 150, 200],
|
||||
)
|
||||
|
||||
result = genetic_algorithm(config)
|
||||
|
||||
BIN
lab4/original_tree.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
@@ -3,6 +3,7 @@ name = "lab4"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"graphviz>=0.21",
|
||||
"matplotlib>=3.10.7",
|
||||
"numpy>=2.3.4",
|
||||
]
|
||||
|
||||
6
lab4/report/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
*
|
||||
|
||||
!**/
|
||||
!.gitignore
|
||||
!report.tex
|
||||
!img/**/*.png
|
||||
BIN
lab4/report/img/best_tree.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
lab4/report/img/original_tree.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
lab4/report/img/results/fitness_avg.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
lab4/report/img/results/fitness_best.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
lab4/report/img/results/generation_001.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
lab4/report/img/results/generation_010.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
lab4/report/img/results/generation_020.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
lab4/report/img/results/generation_030.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
lab4/report/img/results/generation_040.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
lab4/report/img/results/generation_050.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
lab4/report/img/results/generation_100.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
lab4/report/img/results/generation_150.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
lab4/report/img/results/generation_200.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
705
lab4/report/report.tex
Normal file
@@ -0,0 +1,705 @@
|
||||
\documentclass[a4paper, final]{article}
|
||||
%\usepackage{literat} % Нормальные шрифты
|
||||
\usepackage[14pt]{extsizes} % для того чтобы задать нестандартный 14-ый размер шрифта
|
||||
\usepackage{tabularx}
|
||||
\usepackage{booktabs}
|
||||
\usepackage[T2A]{fontenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[russian]{babel}
|
||||
\usepackage{amsmath}
|
||||
\usepackage[left=25mm, top=20mm, right=20mm, bottom=20mm, footskip=10mm]{geometry}
|
||||
\usepackage{ragged2e} %для растягивания по ширине
|
||||
\usepackage{setspace} %для межстрочно го интервала
|
||||
\usepackage{moreverb} %для работы с листингами
|
||||
\usepackage{indentfirst} % для абзацного отступа
|
||||
\usepackage{moreverb} %для печати в листинге исходного кода программ
|
||||
\usepackage{pdfpages} %для вставки других pdf файлов
|
||||
\usepackage{tikz}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{afterpage}
|
||||
\usepackage{longtable}
|
||||
\usepackage{float}
|
||||
\usepackage{xcolor}
|
||||
|
||||
|
||||
|
||||
|
||||
% \usepackage[paper=A4,DIV=12]{typearea}
|
||||
\usepackage{pdflscape}
|
||||
% \usepackage{lscape}
|
||||
|
||||
\usepackage{array}
|
||||
\usepackage{multirow}
|
||||
|
||||
\renewcommand\verbatimtabsize{4\relax}
|
||||
\renewcommand\listingoffset{0.2em} %отступ от номеров строк в листинге
|
||||
\renewcommand{\arraystretch}{1.4} % изменяю высоту строки в таблице
|
||||
\usepackage[font=small, singlelinecheck=false, justification=centering, format=plain, labelsep=period]{caption} %для настройки заголовка таблицы
|
||||
\usepackage{listings} %листинги
|
||||
\usepackage{xcolor} % цвета
|
||||
\usepackage{hyperref}% для гиперссылок
|
||||
\usepackage{enumitem} %для перечислений
|
||||
|
||||
\newcommand{\specialcell}[2][l]{\begin{tabular}[#1]{@{}l@{}}#2\end{tabular}}
|
||||
|
||||
|
||||
\setlist[enumerate,itemize]{leftmargin=1.2cm} %отступ в перечислениях
|
||||
|
||||
\hypersetup{colorlinks,
|
||||
allcolors=[RGB]{010 090 200}} %красивые гиперссылки (не красные)
|
||||
|
||||
% подгружаемые языки — подробнее в документации listings (это всё для листингов)
|
||||
% включаем кириллицу и добавляем кое−какие опции
|
||||
\lstset{tabsize=2,
|
||||
breaklines,
|
||||
basicstyle=\footnotesize,
|
||||
columns=fullflexible,
|
||||
flexiblecolumns,
|
||||
numbers=left,
|
||||
numberstyle={\footnotesize},
|
||||
keywordstyle=\color{blue},
|
||||
inputencoding=cp1251,
|
||||
extendedchars=true
|
||||
}
|
||||
|
||||
\textheight=24cm % высота текста
|
||||
\textwidth=16cm % ширина текста
|
||||
\oddsidemargin=0pt % отступ от левого края
|
||||
\topmargin=-1.5cm % отступ от верхнего края
|
||||
\parindent=24pt % абзацный отступ
|
||||
\parskip=5pt % интервал между абзацами
|
||||
\tolerance=2000 % терпимость к "жидким" строкам
|
||||
\flushbottom % выравнивание высоты страниц
|
||||
|
||||
|
||||
% Настройка листингов
|
||||
\lstset{
|
||||
language=python,
|
||||
extendedchars=\true,
|
||||
inputencoding=utf8,
|
||||
keepspaces=true,
|
||||
% captionpos=b, % подписи листингов снизу
|
||||
}
|
||||
|
||||
\begin{document} % начало документа
|
||||
|
||||
|
||||
|
||||
% НАЧАЛО ТИТУЛЬНОГО ЛИСТА
|
||||
\begin{center}
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\normalsize{МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ\\
|
||||
федеральное государственное автономное образовательное учреждение высшего образования «Санкт-Петербургский политехнический университет Петра Великого»\\[10pt]}
|
||||
\normalsize{Институт компьютерных наук и кибербезопасности}\\[10pt]
|
||||
\normalsize{Высшая школа технологий искусственного интеллекта}\\[10pt]
|
||||
\normalsize{Направление: 02.03.01 <<Математика и компьютерные науки>>}\\
|
||||
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\large{Лабораторная работа №4}\\
|
||||
\large{по дисциплине}\\
|
||||
\large{<<Генетические алгоритмы>>}\\
|
||||
\large{Вариант 18}\\
|
||||
|
||||
% \hfill \break
|
||||
\hfill \break
|
||||
\end{center}
|
||||
|
||||
\small{
|
||||
\begin{tabular}{lrrl}
|
||||
\!\!\!Студент, & \hspace{2cm} & & \\
|
||||
\!\!\!группы 5130201/20101 & \hspace{2cm} & \underline{\hspace{3cm}} &Тищенко А. А. \\\\
|
||||
\!\!\!Преподаватель & \hspace{2cm} & \underline{\hspace{3cm}} & Большаков А. А. \\\\
|
||||
&&\hspace{4cm}
|
||||
\end{tabular}
|
||||
\begin{flushright}
|
||||
<<\underline{\hspace{1cm}}>>\underline{\hspace{2.5cm}} 2025г.
|
||||
\end{flushright}
|
||||
}
|
||||
|
||||
\hfill \break
|
||||
% \hfill \break
|
||||
\begin{center} \small{Санкт-Петербург, 2025} \end{center}
|
||||
\thispagestyle{empty} % выключаем отображение номера для этой страницы
|
||||
|
||||
% КОНЕЦ ТИТУЛЬНОГО ЛИСТА
|
||||
\newpage
|
||||
|
||||
\tableofcontents
|
||||
|
||||
\newpage
|
||||
\section {Постановка задачи}
|
||||
В данной работе были поставлены следующие задачи:
|
||||
|
||||
\begin{itemize}
|
||||
\item Разработать эволюционный алгоритм, реализующий ГП для нахождения заданной по варианту функции.
|
||||
\begin{itemize}
|
||||
\item Структура для представления программы – древовидное представление.
|
||||
\item Терминальное множество: переменные $x_1, x_2, x_3, \ldots, x_n$, и константы в соответствии с заданием по варианту.
|
||||
\item Функциональное множество: $+$, $-$, $*$, $/$, $abs()$, $sin()$, $cos()$, $exp()$, возведение в степень.
|
||||
\item Фитнесс-функция – мера близости между реальными значениями выхода и требуемыми.
|
||||
\end{itemize}
|
||||
\item Представить графически найденное решение на каждой итерации.
|
||||
\item Сравнить найденное решение с представленным в условии задачи.
|
||||
\end{itemize}
|
||||
|
||||
\textbf{Индивидуальное задание вариант 18:}
|
||||
|
||||
\textbf{Дано:} Функция
|
||||
|
||||
$$f(x) = \sum_{i=1}^{n} \sum_{j=1}^{i} x_j^2, \text{ где } x_j \in [-5.536, 5.536] \text{ для всех } j = 1, \ldots, n, \text{ а } n = 8.$$
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Теоретические сведения}
|
||||
|
||||
\subsection{Генетическое программирование}
|
||||
|
||||
\textbf{Генетическое программирование} (ГП) — разновидность эволюционных алгоритмов, в которых особь представляет собой программу, автоматически создаваемую для решения задачи. В отличие от генетических алгоритмов с фиксированной структурой хромосом, в ГП особи имеют переменную длину, что требует специальных методов кодирования, инициализации и генетических операторов. Ключевая идея ГП — представление программы на высоком уровне абстракции с учётом структуры компьютерных программ.
|
||||
|
||||
Оценка программ выполняется с помощью фитнесс-функции, отражающей степень соответствия решения требованиям задачи. Обычно используются метрики ошибки: среднеквадратичная ошибка, абсолютная ошибка или другие функции рассогласования между вычисленным и ожидаемым значением. Чем ниже ошибка, тем выше приспособленность особи.
|
||||
|
||||
\subsection{Терминальное и функциональное множества}
|
||||
|
||||
Программы формируются из \textbf{переменных}, \textbf{констант} и \textbf{функций}, связанных синтаксическими правилами. Для их описания необходимо определить два базовых множества:
|
||||
\begin{itemize}
|
||||
\item \textbf{Терминальное множество}, включающее константы и переменные.
|
||||
\item \textbf{Функциональное множество}, состоящее из операторов и элементарных функций, таких как \( \exp(x) \), \( \sin(x) \) и других.
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{Терминальное множество}
|
||||
Терминальное множество включает:
|
||||
\begin{enumerate}
|
||||
\item Внешние входы программы.
|
||||
\item Константы, используемые в программе.
|
||||
\item Функции без аргументов.
|
||||
\end{enumerate}
|
||||
|
||||
Термин «терминал» используется потому, что эти элементы соответствуют концевым (висячим) узлам в древовидных структурах и терминалам формальных грамматик. Терминал предоставляет численное значение, не требуя входных аргументов, то есть имеет нулевую арность. В классическом ГП на основе деревьев множество числовых констант выбирается для всей популяции и остается неизменным.
|
||||
|
||||
\subsubsection{Функциональное множество}
|
||||
|
||||
Функциональное множество состоит из операторов и различных функций. Оно может быть очень широким и включать типичные конструкции языков программирования, такие как:
|
||||
|
||||
\begin{itemize}
|
||||
\item Логические функции: AND, OR, NOT;
|
||||
\item Арифметические операции: $+$, $-$, $\times$, $\div$;
|
||||
\item Трансцендентные функции: $\sin$, $\cos$, $\tan$, $\log$;
|
||||
\item Операции присваивания: $a := 2$;
|
||||
\item Условные операторы: if-then-else, switch/case;
|
||||
\item Операторы переходов: go to, jump, call;
|
||||
\item Операторы циклов: while, repeat-until, for;
|
||||
\item Подпрограммы и пользовательские функции.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Виды представления программ. Древовидное представление}
|
||||
|
||||
Среди наиболее распространённых структур для представления особей (потенциальных решений) в современном генетическом программировании можно выделить:
|
||||
|
||||
\begin{enumerate}
|
||||
\item \textbf{Древовидное представление} — классический подход, где программы представляются в виде деревьев с операторами в узлах и терминалами в листьях
|
||||
|
||||
\item \textbf{Линейная структура} — программы записываются как последовательности инструкций, аналогично ассемблерному коду
|
||||
|
||||
\item \textbf{Графоподобная структура} — расширенное представление, допускающее множественные связи и переиспользование компонентов
|
||||
\end{enumerate}
|
||||
|
||||
Древовидная форма представления является классической для ГП. Программа представляется в виде дерева, где внутренние узлы — это функции из функционального множества, а листья (терминальные узлы) — это переменные и константы из терминального
|
||||
множества. Такая структура позволяет гибко работать с выражениями различной длины
|
||||
и сложности
|
||||
|
||||
\subsection{Инициализация древовидных структур}
|
||||
|
||||
Сложность древовидных структур оценивается через максимальную глубину дерева $D_m$ или общее количество узлов. Процесс инициализации древовидных структур основан на случайном выборе функциональных и терминальных символов при заданном ограничении максимальной глубины. Рассмотрим пример с терминальным множеством:
|
||||
|
||||
Существуют два основных метода инициализации:
|
||||
|
||||
\subsubsection*{Полный метод (full)}
|
||||
|
||||
На всех уровнях, кроме последнего, выбираются только функциональные символы. Терминальные символы размещаются исключительно на уровне максимальной глубины $D_m$. Это гарантирует создание сбалансированных деревьев регулярной структуры.
|
||||
|
||||
\subsubsection*{Растущий метод (grow)}
|
||||
На каждом шаге случайным образом выбирается либо функциональный, либо терминальный символ. Выбор терминала прекращает рост ветви, что приводит к формированию нерегулярных деревьев с различной глубиной листьев.
|
||||
|
||||
\subsection{Оператор кроссинговера на древовидных структурах}
|
||||
|
||||
Для древовидной формы представления программ в генетическом программировании применяются три основных типа операторов кроссинговера:
|
||||
|
||||
\begin{enumerate}[label=\alph*)]
|
||||
\item Узловой ОК
|
||||
\item Кроссинговер поддеревьев
|
||||
\item Смешанный
|
||||
\end{enumerate}
|
||||
|
||||
\subsubsection{Узловой оператор кроссинговера}
|
||||
|
||||
В узловом операторе кроссинговера выбираются два родителя (два дерева) и внутри них — узлы. Первый родитель называется доминантом, второй — рецессивом. Узлы могут различаться по типу, поэтому сначала необходимо проверить, что выбранные узлы взаимозаменяемы. Если типы не совпадают, выбирается другой узел во втором родителе, и проверка повторяется. После этого осуществляется обмен выбранных узлов между деревьями.
|
||||
|
||||
\subsubsection{Кроссинговер поддеревьев}
|
||||
|
||||
В кроссинговере поддеревьев не происходит обмен отдельными узлами, а определяется обмен поддеревьями. Он осуществляется следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Выбираются два родителя (\textit{один — доминантный, другой — рецессивный}). Необходимо убедиться, что выбранные узлы взаимозаменяемы, то есть принадлежат одному типу. В противном случае выбирается другой узел в рецессивном дереве.
|
||||
\item Производится обмен соответствующими поддеревьями.
|
||||
\item Далее вычисляется предполагаемый размер потомков. Если он не превышает установленный порог, то обмен ветвями запоминается.
|
||||
\end{enumerate}
|
||||
|
||||
При смешанном операторе кроссинговера для некоторых узлов выполняется узловой ОК, а для других - кроссинговер поддеревьев. В целом ОК выполняется следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Выбор точек скрещивания \( P_1, P_2 \) в обоих родителях
|
||||
\item Выбор типа кроссинговера с заданной вероятностью:
|
||||
\begin{itemize}
|
||||
\item Первый тип (обмен подграфами) с вероятностью \( P_G \)
|
||||
\item Второй тип (линейный обмен) с вероятностью \( 1 - P_G \)
|
||||
\end{itemize}
|
||||
\item Если выбран первый тип и размер потомка не превышает порог, выполняется кроссинговер подграфами
|
||||
\item Если выбран второй тип и размер потомка не превышает порог, выполняется линейный кроссинговер
|
||||
\end{enumerate}
|
||||
|
||||
\subsection{Мутационные операторы для древовидных структур}
|
||||
|
||||
В контексте древовидного представления программ применяются следующие мутационные операторы:
|
||||
|
||||
\begin{enumerate}[label=\alph*)]
|
||||
\item Мутация узлов (узловая)
|
||||
\item Мутация с усечением (усекающая)
|
||||
\item Мутация с ростом (растущая)
|
||||
\item Hoist-мутация
|
||||
\end{enumerate}
|
||||
|
||||
\textbf{Процедура узловой мутации} включает следующие шаги:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Случайный выбор целевого узла в дереве программы и идентификация его типа
|
||||
\item Случайный выбор заменяющего узла того же типа из соответствующего множества (функционального или терминального)
|
||||
\item Замена исходного узла на выбранный вариант
|
||||
\end{enumerate}
|
||||
|
||||
\textbf{Алгоритм усекающей мутации} реализуется следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Выбор узла, который будет подвергнут мутации
|
||||
\item Случайный выбор терминального символа из допустимого множества
|
||||
\item Удаление поддерева, корнем которого является выбранный узел
|
||||
\item Замена удаленного поддерева терминальным символом
|
||||
\end{enumerate}
|
||||
|
||||
\textbf{Алгоритм растущей мутации} реализуется следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Определение узла, подвергаемого мутации
|
||||
\item Если узел является терминальным, выбирается другой узел; для нетерминального узла производится удаление всех исходящих ветвей
|
||||
\item Вычисление размера и сложности оставшейся части дерева
|
||||
\item Генерация нового случайного поддерева, размер которого не превышает заданного порогового значения, и его размещение вместо удалённой части
|
||||
\end{enumerate}
|
||||
|
||||
\textbf{Алгоритм Hoist-мутации} предназначен для борьбы с избыточным ростом деревьев (bloat) и реализуется следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Случайный выбор поддерева с функциональным узлом в корне
|
||||
\item Выбор случайного узла внутри этого поддерева (исключая корень выбранного поддерева)
|
||||
\item Замена исходного поддерева на поддерево, начинающееся с выбранного внутреннего узла
|
||||
\item В результате дерево становится короче, сохраняя при этом часть исходной структуры
|
||||
\end{enumerate}
|
||||
|
||||
Данная мутация всегда уменьшает размер дерева, что помогает контролировать сложность программ и предотвращает неконтролируемый рост деревьев в процессе эволюции.
|
||||
|
||||
\textbf{Комбинированная мутация.} В реализованном алгоритме используется стратегия комбинированной мутации, которая на каждом шаге случайно выбирает один из четырёх описанных операторов с заданными вероятностями:
|
||||
|
||||
\begin{itemize}
|
||||
\item Растущая мутация: $p = 0.40$
|
||||
\item Узловая мутация: $p = 0.30$
|
||||
\item Hoist-мутация: $p = 0.15$
|
||||
\item Усекающая мутация: $p = 0.15$
|
||||
\end{itemize}
|
||||
|
||||
Такой подход обеспечивает баланс между увеличением разнообразия популяции (растущая мутация), локальными изменениями (узловая мутация) и контролем размера деревьев (Hoist-мутация и усекающая мутация).
|
||||
|
||||
\subsection{Фитнес-функции в генетическом программировании}
|
||||
|
||||
В отличие от генетических алгоритмов, где фитнес-функция часто совпадает с исходной целевой функцией, в генетическом программировании фитнес-функция обычно измеряет степень соответствия между фактическими выходными значениями $y_i$ и целевыми значениями $d_i$. В качестве фитнес-функций часто используются метрики ошибок, такие как абсолютное отклонение или среднеквадратичная ошибка.
|
||||
|
||||
\newpage
|
||||
\section{Особенности реализации}
|
||||
|
||||
В рамках работы создана библиотека \texttt{gp} для генетического программирования с древовидным представлением программ. Реализация выполнена на языке Python с использованием NumPy для векторизованных вычислений.
|
||||
|
||||
\subsection{Примитивы и операции (primitive.py, ops.py)}
|
||||
|
||||
Базовый класс \texttt{Primitive} представляет атомарные элементы дерева программы:
|
||||
|
||||
\begin{lstlisting}
|
||||
@dataclass(frozen=True)
|
||||
class Primitive:
|
||||
name: str
|
||||
arity: int # арность: 0 для терминалов, >0 для операций
|
||||
operation_fn: OperationFn | None
|
||||
\end{lstlisting}
|
||||
|
||||
Реализованы конструкторы для создания терминалов и операций: \texttt{Var(name: str)}, \texttt{Const(name: str, val: Value)}, \texttt{Operation(name: str, arity: int, fn)}.
|
||||
|
||||
Модуль \texttt{ops.py} содержит набор безопасных векторизованных операций. Функция \texttt{make\_safe} оборачивает операции для обработки некорректных значений:
|
||||
|
||||
\begin{lstlisting}
|
||||
def make_safe(fn: Callable) -> Callable:
|
||||
def wrapped(args: Sequence[Value]) -> Value:
|
||||
with np.errstate(over="ignore", invalid="ignore",
|
||||
divide="ignore", under="ignore"):
|
||||
res = fn(args)
|
||||
res = np.nan_to_num(res, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
return np.clip(res, -1e6, 1e6)
|
||||
return wrapped
|
||||
\end{lstlisting}
|
||||
|
||||
Реализованы унарные операции (\texttt{NEG, SIN, COS, SQUARE, EXP}) и бинарные (\texttt{ADD, SUB, MUL, DIV, POW}). Для деления используется защита от деления на ноль, для возведения в степень -- ограничение показателя.
|
||||
|
||||
\subsection{Узлы дерева (node.py)}
|
||||
|
||||
Класс \texttt{Node} представляет узел дерева программы:
|
||||
|
||||
\begin{lstlisting}
|
||||
class Node:
|
||||
value: Primitive
|
||||
parent: Node | None
|
||||
children: list[Node]
|
||||
\end{lstlisting}
|
||||
|
||||
Реализованы методы для манипуляций с деревом: \texttt{add\_child}, \texttt{replace\_child}, \texttt{copy\_subtree}. Метод \texttt{list\_nodes} возвращает список всех узлов поддерева (обход в глубину). Для контроля размера реализован метод \texttt{prune}, который усекает дерево до заданной глубины, заменяя операции на случайные терминалы.
|
||||
|
||||
Вычисление программы выполняется методом \texttt{eval}, который рекурсивно вычисляет значения поддеревьев и применяет операцию узла:
|
||||
|
||||
\begin{lstlisting}
|
||||
def eval(self, context: Context) -> Value:
|
||||
return self.value.eval(
|
||||
[child.eval(context) for child in self.children],
|
||||
context
|
||||
)
|
||||
\end{lstlisting}
|
||||
|
||||
Для кроссовера реализована функция \texttt{swap\_subtrees(a: Node, b: Node)}, которая обменивает два поддерева, корректно обновляя ссылки на родителей.
|
||||
|
||||
\subsection{Хромосомы (chromosome.py)}
|
||||
|
||||
Класс \texttt{Chromosome} инкапсулирует дерево программы вместе с множествами терминалов и операций:
|
||||
|
||||
\begin{lstlisting}
|
||||
class Chromosome:
|
||||
terminals: Sequence[Primitive]
|
||||
operations: Sequence[Primitive]
|
||||
root: Node
|
||||
\end{lstlisting}
|
||||
|
||||
Реализованы два метода инициализации случайных деревьев:
|
||||
|
||||
\begin{itemize}
|
||||
\item \texttt{full\_init(terminals, operations, max\_depth)} -- полная инициализация, где на каждом уровне до максимальной глубины выбираются только операции, а на последнем -- только терминалы.
|
||||
\item \texttt{grow\_init(terminals, operations, max\_depth, terminal\_probability)} -- растущая инициализация с вероятностным выбором терминалов на каждом уровне, что создаёт деревья различной формы.
|
||||
\end{itemize}
|
||||
|
||||
Комбинация этих методов (\textit{ramped half-and-half}) реализована в функции \texttt{ramped\_initialization}, которая создаёт начальную популяцию из деревьев различных глубин, используя оба метода поровну.
|
||||
|
||||
\subsection{Кроссовер (crossovers.py)}
|
||||
|
||||
Реализован оператор кроссовера поддеревьев:
|
||||
|
||||
\begin{lstlisting}
|
||||
def crossover_subtree(parent1: Chromosome, parent2: Chromosome,
|
||||
max_depth: int) -> tuple[Chromosome, Chromosome]:
|
||||
\end{lstlisting}
|
||||
|
||||
Алгоритм выбирает случайные узлы в каждом родителе (кроме корня) и обменивает соответствующие поддеревья. Если глубина потомков превышает \texttt{max\_depth}, деревья усекаются методом \texttt{prune}.
|
||||
|
||||
\subsection{Мутации (mutations.py)}
|
||||
|
||||
Все мутации наследуются от базового класса \texttt{BaseMutation} с методом \texttt{mutate}. Реализованы четыре типа мутаций:
|
||||
|
||||
\begin{itemize}
|
||||
\item \texttt{NodeReplacementMutation} -- заменяет узел на другой той же арности
|
||||
\item \texttt{ShrinkMutation} -- заменяет случайную операцию на терминал (усечение)
|
||||
\item \texttt{GrowMutation} -- заменяет узел на случайное поддерево с контролем глубины
|
||||
\item \texttt{HoistMutation} -- заменяет поддерево на его случайную внутреннюю часть (уменьшает размер)
|
||||
\end{itemize}
|
||||
|
||||
Класс \texttt{CombinedMutation} позволяет комбинировать мутации с заданными вероятностями, случайно выбирая одну из них на каждом шаге.
|
||||
|
||||
\subsection{Фитнес-функции (fitness.py)}
|
||||
|
||||
Базовый класс \texttt{BaseFitness} определяет интерфейс для вычисления ошибки:
|
||||
|
||||
\begin{lstlisting}
|
||||
class BaseFitness(ABC):
|
||||
def __call__(self, chromosome: Chromosome) -> float:
|
||||
test_points = self.test_points_fn()
|
||||
context = {t: test_points[:, i]
|
||||
for i, t in enumerate(chromosome.terminals)}
|
||||
predicted = chromosome.root.eval(context)
|
||||
true_values = self.target_function(test_points)
|
||||
return self.fitness_fn(chromosome, predicted, true_values)
|
||||
\end{lstlisting}
|
||||
|
||||
Реализованы метрики ошибок: \texttt{MSEFitness} (среднеквадратичная), \texttt{RMSEFitness} (корень из MSE), \texttt{MAEFitness} (средняя абсолютная), \texttt{NRMSEFitness} (нормализованная RMSE). Класс \texttt{PenalizedFitness} добавляет штраф за размер и глубину дерева для борьбы с bloat.
|
||||
|
||||
\subsection{Селекция (selection.py)}
|
||||
|
||||
Реализованы три метода селекции:
|
||||
|
||||
\begin{itemize}
|
||||
\item \texttt{roulette\_selection} -- селекция рулеткой со сдвигом для обработки отрицательных значений
|
||||
\item \texttt{tournament\_selection(k)} -- турнирная селекция размера $k$
|
||||
\item \texttt{stochastic\_tournament\_selection(k, p\_best)} -- стохастическая турнирная с вероятностью выбора лучшего
|
||||
\end{itemize}
|
||||
|
||||
Для минимизации фитнес-функции используется инверсия знака при передаче фитнесов в селекцию.
|
||||
|
||||
\subsection{Генетический алгоритм (ga.py)}
|
||||
|
||||
Основная функция \texttt{genetic\_algorithm(config: GARunConfig)} реализует классический цикл ГА:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Вычисление фитнеса: \texttt{eval\_population(population, fitness\_func)}
|
||||
\item Сохранение элиты (если \texttt{config.elitism > 0})
|
||||
\item Селекция родителей: \texttt{config.selection\_fn(population, fitnesses)}
|
||||
\item Кроссовер с вероятностью $p_c$: попарный обмен поддеревьями
|
||||
\item Мутация с вероятностью $p_m$
|
||||
\item Замещение популяции с восстановлением элиты
|
||||
\end{enumerate}
|
||||
|
||||
Поддерживаются критерии остановки: по числу поколений, повторению лучшего результата, достижению порогового значения. История поколений сохраняется в виде списка объектов \texttt{Generation}.
|
||||
|
||||
Функция \texttt{save\_generation} использует библиотеку Graphviz для визуализации лучшего дерева поколения. Функция \texttt{plot\_fitness\_history} строит графики динамики лучших и средних значений фитнеса по поколениям и сохраняет их отдельно в \texttt{fitness\_best.png} и \texttt{fitness\_avg.png}.
|
||||
|
||||
\newpage
|
||||
\section{Результаты работы}
|
||||
|
||||
На Рис.~\ref{fig:gen1}--\ref{fig:lastgen} представлены результаты работы генетического алгоритма со следующими параметрами:
|
||||
\begin{itemize}
|
||||
\item $N = 400$ -- размер популяции.
|
||||
\item $10$ -- максимальная глубина дерева.
|
||||
\item $p_c = 0.85$ -- вероятность кроссинговера поддеревьев.
|
||||
\item $p_m = 0.15$ -- вероятность мутации, при этом использовалась комбинация различных вариантов:
|
||||
\begin{itemize}
|
||||
\item Растущая мутация: $p = 0.40$
|
||||
\item Узловая мутация: $p = 0.30$
|
||||
\item Hoist-мутация: $p = 0.15$
|
||||
\item Усекающая мутация: $p = 0.15$
|
||||
\end{itemize}
|
||||
\item $200$ -- максимальное количество поколений.
|
||||
\item $15$ -- количество "элитных" особей, переносимых без изменения в следующее поколение.
|
||||
\item $3$ -- размер турнира для селекции.
|
||||
\end{itemize}
|
||||
|
||||
На Рис.~\ref{fig:fitness_avg} и Рис.~\ref{fig:fitness_best} показаны графики изменения среднего и лучшего значения фитнеса по поколениям.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.95\linewidth]{img/results/fitness_avg.png}
|
||||
\caption{График среднего значения фитнеса по поколениям}
|
||||
\label{fig:fitness_avg}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.95\linewidth]{img/results/fitness_best.png}
|
||||
\caption{График лучшего значения фитнеса по поколениям}
|
||||
\label{fig:fitness_best}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.25\linewidth]{img/results/generation_001.png}
|
||||
\caption{Лучшая особь поколения №1}
|
||||
\label{fig:gen1}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.25\linewidth]{img/results/generation_010.png}
|
||||
\caption{Лучшая особь поколения №10}
|
||||
\label{fig:gen10}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.25\linewidth]{img/results/generation_020.png}
|
||||
\caption{Лучшая особь поколения №20}
|
||||
\label{fig:gen20}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.25\linewidth]{img/results/generation_030.png}
|
||||
\caption{Лучшая особь поколения №30}
|
||||
\label{fig:gen30}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{img/results/generation_040.png}
|
||||
\caption{Лучшая особь поколения №40}
|
||||
\label{fig:gen40}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_050.png}
|
||||
\caption{Лучшая особь поколения №50}
|
||||
\label{fig:gen50}
|
||||
\end{figure}
|
||||
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_100.png}
|
||||
\caption{Лучшая особь поколения №100}
|
||||
\label{fig:gen100}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_150.png}
|
||||
\caption{Лучшая особь поколения №150}
|
||||
\label{fig:gen300}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_200.png}
|
||||
\caption{Лучшая особь поколения №200}
|
||||
\label{fig:lastgen}
|
||||
\end{figure}
|
||||
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\subsection{Анализ результатов}
|
||||
|
||||
\subsubsection*{Сравнение полученных деревьев}
|
||||
|
||||
На Рис.~\ref{fig:original_tree} представлено исходное дерево, на Рис.~\ref{fig:best_tree} представлено лучшее дерево, найденное алгоритмом.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.9\linewidth]{img/original_tree.png}
|
||||
\caption{Дерево целевой функции}
|
||||
\label{fig:original_tree}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.9\linewidth]{img/best_tree.png}
|
||||
\caption{Лучшая особь, найденная алгоритмом}
|
||||
\label{fig:best_tree}
|
||||
\end{figure}
|
||||
|
||||
\subsubsection*{Сравнение полученных формул}
|
||||
|
||||
Перед сравнением, упростим исходную формулу, раскрыв знаки суммирования и перегруппировав слагаемые.
|
||||
|
||||
$$f(x) = \sum_{i=1}^{n} \sum_{j=1}^{i} x_j^2, \text{ для всех } j = 1, \ldots, n, \text{ при этом n }= 8.$$
|
||||
|
||||
$$
|
||||
f(x) = \underbrace{(x_1^2)
|
||||
+ (x_1^2 + x_2^2)
|
||||
+ \ldots
|
||||
+ (x_1^2 + x_2^2 + x_3^2 + x_4^2 + x_5^2 + x_6^2 + x_7^2 + x_8^2)}_{\text{ всего } n = 8 \text{ слагаемых}}
|
||||
$$
|
||||
|
||||
$$
|
||||
f(x) = 8 x_1^2 + 7 x_2^2 + 6 x_3^2 + 5 x_4^2 + 4 x_5^2 + 3 x_6^2 + 2 x_7^2 + x_8^2
|
||||
$$
|
||||
|
||||
В программе реализован метод преобразования особи (дереве) в строковую формулу. Вывод программы для лучшей особи представлен ниже:
|
||||
|
||||
\begin{lstlisting}[label={lst:}]
|
||||
(((((pow2(x3) + ((pow2(x1) + pow2(x2)) + pow2(x1))) + pow2(x6)) +
|
||||
((pow2(x2) + pow2(x2)) + ((sin(((x6 + x2) + sin(x6))) + ((pow2(x4) +
|
||||
pow2(x2)) + pow2(x4))) + (((pow2(x3) + pow2(x4)) + pow2(x7)) + (pow2(x6) +
|
||||
pow2(x4)))))) + (((pow2(x2) + ((pow2(x8) + pow2((x5 + x5))) + pow2(x3))) +
|
||||
pow2(x1)) + (pow2(x6) + pow2(x4)))) + (((((pow2(x3) + pow2(x3))
|
||||
+ ((pow2(x7) + pow2(x2)) + pow2(x1))) + pow2(x1)) + (pow2(x2) + ((pow2(x3) +
|
||||
pow2(x1)) + pow2(x1)))) + (sin(x2) + pow2(x1))))
|
||||
\end{lstlisting}
|
||||
|
||||
Программный метод автоматически обрамляет функции и переменные в скобки, чтобы правильно расставить приоритеты операций. Однако в данном случае они избыточны, поэтому их можно убрать:
|
||||
|
||||
\begin{lstlisting}[label={lst:}]
|
||||
pow2(x3) + pow2(x1) + pow2(x2) + pow2(x1) + pow2(x6) + pow2(x2) + pow2(x2) +
|
||||
sin(x6 + x2) + sin(x6) + pow2(x4) + pow2(x2) + pow2(x4) + pow2(x3) + pow2(x4) +
|
||||
pow2(x7) + pow2(x6) + pow2(x4) + pow2(x2) + pow2(x8) + pow2(x5 + x5) + pow2(x3) +
|
||||
pow2(x1) + pow2(x6) + pow2(x4) + pow2(x3) + pow2(x3) + pow2(x7) + pow2(x2) + pow2(x1) +
|
||||
pow2(x1) + pow2(x2) + pow2(x3) + pow2(x1) + pow2(x1) + sin(x2) + pow2(x1)
|
||||
\end{lstlisting}
|
||||
|
||||
Переставим слагаемые:
|
||||
|
||||
\begin{lstlisting}[label={lst:}]
|
||||
pow2(x1) + pow2(x1) + pow2(x1) + pow2(x1) + pow2(x1) + pow2(x1) + pow2(x1) + pow2(x1) +
|
||||
pow2(x2) + pow2(x2) + pow2(x2) + pow2(x2) + pow2(x2) + pow2(x2) + pow2(x2) +
|
||||
pow2(x3) + pow2(x3) + pow2(x3) + pow2(x3) + pow2(x3) + pow2(x3) +
|
||||
pow2(x4) + pow2(x4) + pow2(x4) + pow2(x4) + pow2(x4) +
|
||||
pow2(x5 + x5) +
|
||||
pow2(x6) + pow2(x6) + pow2(x6) +
|
||||
pow2(x7) + pow2(x7) +
|
||||
pow2(x8) +
|
||||
sin(x6 + x2) + sin(x6) + sin(x2)
|
||||
\end{lstlisting}
|
||||
|
||||
Заметим, что $(x_5 + x_5)^2 = (2x_5)^2 = 4x_5^2$, а также сгруппируем слагаемые, чтобы получить финальный вид формулы, найденной алгоритмом:
|
||||
|
||||
$$
|
||||
\hat{f}(x) = \textcolor{green!70!black}{8x_1^2 + 7x_2^2 + 6x_3^2 + 5x_4^2 + 4x_5^2 + 3x_6^2 + 2x_7^2 + x_8^2} + \textcolor{red!90!black}{sin(x_6 + x_2) + sin(x_6) + sin(x_2)}
|
||||
$$
|
||||
|
||||
Найденная формула полностью включает в себя целевую и содержит лишь несколько лишних слагаемых.
|
||||
|
||||
\newpage
|
||||
\section{Ответ на контрольный вопрос}
|
||||
|
||||
\textbf{Вопрос}: Опишите древовидное представление.
|
||||
|
||||
\textbf{Ответ}:
|
||||
|
||||
Древовидное представление — классический подход в генетическом программировании, где программы представляются в виде синтаксических деревьев. Внутренние узлы содержат функции из функционального множества (арифметические операции, математические функции), а листья — терминалы из терминального множества (переменные и константы). Вычисление происходит рекурсивно от листьев к корню. Сложность дерева оценивается через максимальную глубину $D_m$ (расстояние от корня до самого дальнего листа) или общее количество узлов.
|
||||
|
||||
Основные преимущества: естественное отображение синтаксической структуры математических выражений, гибкость в работе с выражениями различной длины и сложности, простота реализации генетических операторов (кроссовер поддеревьев, узловая мутация, растущая и усекающая мутации), автоматическое соблюдение синтаксической корректности при генерации и модификации программ. Инициализация выполняется полным методом (full) или растущим методом (grow), либо их комбинацией (ramped half-and-half).
|
||||
|
||||
|
||||
\newpage
|
||||
\section*{Заключение}
|
||||
\addcontentsline{toc}{section}{Заключение}
|
||||
|
||||
В ходе четвёртой лабораторной работы была успешно решена задача нахождения формулы целевой функции вида $f(x) = \sum_{i=1}^{n} \sum_{j=1}^{i} x_j^2$ с использованием генетического программирования:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Изучен теоретический материал о представлениях программ в генетическом программировании (древовидное, линейное, графовое) и специализированных операторах кроссинговера и мутации для древовидных структур;
|
||||
\item Создана программная библиотека \texttt{gp} на языке Python с реализацией древовидного представления хромосом, кроссовера поддеревьев, четырёх типов мутаций (узловая, усекающая, растущая, Hoist-мутация), турнирной селекции и безопасных векторизованных операций;
|
||||
\item Реализованы методы инициализации популяции (full, grow, ramped half-and-half), фитнес-функции на основе метрик ошибок (MSE, RMSE, MAE, NRMSE), механизм элитизма и визуализация деревьев с помощью Graphviz;
|
||||
\item Проведён эксперимент с популяцией из 400 особей на 10000 тестовых точках для 8 переменных. За 200 поколений (~5.9 минут) получено решение с MSE = 0.412 и RMSE = 0.642, полностью включающее целевую функцию с небольшими дополнительными слагаемыми.
|
||||
\end{enumerate}
|
||||
|
||||
|
||||
\newpage
|
||||
\section*{Список литературы}
|
||||
\addcontentsline{toc}{section}{Список литературы}
|
||||
|
||||
\vspace{-1.5cm}
|
||||
\begin{thebibliography}{0}
|
||||
\bibitem{vostrov}
|
||||
Методические указания по выполнению лабораторных работ к курсу «Генетические алгоритмы», 119 стр.
|
||||
\end{thebibliography}
|
||||
|
||||
\end{document}
|
||||
11
lab4/uv.lock
generated
@@ -69,6 +69,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphviz"
|
||||
version = "0.21"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kiwisolver"
|
||||
version = "1.4.9"
|
||||
@@ -108,12 +117,14 @@ name = "lab4"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "graphviz" },
|
||||
{ name = "matplotlib" },
|
||||
{ name = "numpy" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "graphviz", specifier = ">=0.21" },
|
||||
{ name = "matplotlib", specifier = ">=3.10.7" },
|
||||
{ name = "numpy", specifier = ">=2.3.4" },
|
||||
]
|
||||
|
||||