рисование дерева
This commit is contained in:
50
lab4/draw_tree.py
Normal file
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"]
|
|
||||||
|
|||||||
119
lab4/gp/ga.py
119
lab4/gp/ga.py
@@ -6,11 +6,12 @@ from copy import deepcopy
|
|||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
|
import graphviz
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from matplotlib import pyplot as plt
|
from matplotlib import pyplot as plt
|
||||||
from numpy.typing import NDArray
|
|
||||||
|
|
||||||
from .chromosome import Chromosome
|
from .chromosome import Chromosome
|
||||||
|
from .node import Node
|
||||||
from .types import Fitnesses, Population
|
from .types import Fitnesses, Population
|
||||||
|
|
||||||
type FitnessFn = Callable[[Chromosome], float]
|
type FitnessFn = Callable[[Chromosome], float]
|
||||||
@@ -66,6 +67,7 @@ class Generation:
|
|||||||
number: int
|
number: int
|
||||||
best: Chromosome
|
best: Chromosome
|
||||||
best_fitness: float
|
best_fitness: float
|
||||||
|
avg_fitness: float
|
||||||
population: Population
|
population: Population
|
||||||
fitnesses: Fitnesses
|
fitnesses: Fitnesses
|
||||||
|
|
||||||
@@ -148,27 +150,45 @@ def eval_population(population: Population, fitness_func: FitnessFn) -> Fitnesse
|
|||||||
return np.array([fitness_func(chrom) for chrom in population])
|
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(
|
def save_generation(
|
||||||
generation: Generation, history: list[Generation], config: GARunConfig
|
generation: Generation, history: list[Generation], config: GARunConfig
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Сохраняет визуализацию лучшей хромосомы поколения в виде дерева."""
|
||||||
os.makedirs(config.results_dir, exist_ok=True)
|
os.makedirs(config.results_dir, exist_ok=True)
|
||||||
|
|
||||||
fig = plt.figure(figsize=(7, 7))
|
# Создаем граф для визуализации дерева
|
||||||
fig.suptitle(
|
dot = graphviz.Digraph(comment=f"Generation {generation.number}")
|
||||||
f"Поколение #{generation.number}. "
|
dot.attr(rankdir="TB") # Top to Bottom direction
|
||||||
f"Лучшая особь: {generation.best_fitness:.0f}. "
|
dot.attr("node", shape="circle", style="filled", fillcolor="lightblue")
|
||||||
f"Среднее значение: {np.mean(generation.fitnesses):.0f}",
|
|
||||||
fontsize=14,
|
# Добавляем заголовок
|
||||||
y=0.95,
|
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)
|
filename = f"generation_{generation.number:03d}"
|
||||||
fig.savefig(path_png, dpi=150, bbox_inches="tight")
|
filepath = os.path.join(config.results_dir, filename)
|
||||||
plt.close(fig)
|
dot.render(filepath, format="png", cleanup=True)
|
||||||
|
|
||||||
|
|
||||||
def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
||||||
@@ -216,6 +236,7 @@ def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
|||||||
number=generation_number,
|
number=generation_number,
|
||||||
best=population[best_index],
|
best=population[best_index],
|
||||||
best_fitness=fitnesses[best_index],
|
best_fitness=fitnesses[best_index],
|
||||||
|
avg_fitness=float(np.mean(fitnesses)),
|
||||||
# population=deepcopy(population),
|
# population=deepcopy(population),
|
||||||
population=[],
|
population=[],
|
||||||
# fitnesses=deepcopy(fitnesses),
|
# fitnesses=deepcopy(fitnesses),
|
||||||
@@ -301,41 +322,61 @@ def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
|||||||
end = time.perf_counter()
|
end = time.perf_counter()
|
||||||
|
|
||||||
assert best is not None, "Best was never set"
|
assert best is not None, "Best was never set"
|
||||||
return GARunResult(
|
result = GARunResult(
|
||||||
len(history),
|
len(history),
|
||||||
best,
|
best,
|
||||||
history,
|
history,
|
||||||
(end - start) * 1000.0,
|
(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]
|
generations = [gen.number for gen in result.history]
|
||||||
best_fitnesses = [gen.best_fitness 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(
|
if save_dir:
|
||||||
generations, best_fitnesses, label="Лучшее значение", linewidth=2, color="blue"
|
best_path = os.path.join(save_dir, "fitness_best.png")
|
||||||
)
|
fig_best.savefig(best_path, dpi=150, bbox_inches="tight")
|
||||||
ax.plot(
|
print(f"График лучших значений сохранен в {best_path}")
|
||||||
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}")
|
|
||||||
else:
|
else:
|
||||||
plt.show()
|
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,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
|
|
||||||
20
lab4/main.py
20
lab4/main.py
@@ -1,16 +1,20 @@
|
|||||||
|
"""
|
||||||
|
graphviz должен быть доступен в PATH (недостаточно просто установить через pip)
|
||||||
|
|
||||||
|
Можно проверить командой
|
||||||
|
dot -V
|
||||||
|
"""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
from math import log
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
from gp import Chromosome
|
|
||||||
from gp.crossovers import crossover_subtree
|
from gp.crossovers import crossover_subtree
|
||||||
from gp.fitness import (
|
from gp.fitness import (
|
||||||
MAEFitness,
|
MAEFitness,
|
||||||
MSEFitness,
|
MSEFitness,
|
||||||
NRMSEFitness,
|
NRMSEFitness,
|
||||||
PenalizedFitness,
|
|
||||||
RMSEFitness,
|
RMSEFitness,
|
||||||
)
|
)
|
||||||
from gp.ga import GARunConfig, genetic_algorithm
|
from gp.ga import GARunConfig, genetic_algorithm
|
||||||
@@ -21,10 +25,10 @@ from gp.mutations import (
|
|||||||
NodeReplacementMutation,
|
NodeReplacementMutation,
|
||||||
ShrinkMutation,
|
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.population import ramped_initialization
|
||||||
from gp.primitive import Const, Var
|
from gp.primitive import Var
|
||||||
from gp.selection import roulette_selection, tournament_selection
|
from gp.selection import tournament_selection
|
||||||
|
|
||||||
NUM_VARS = 8
|
NUM_VARS = 8
|
||||||
TEST_POINTS = 10000
|
TEST_POINTS = 10000
|
||||||
@@ -34,8 +38,6 @@ SEED = 17
|
|||||||
np.random.seed(SEED)
|
np.random.seed(SEED)
|
||||||
random.seed(SEED)
|
random.seed(SEED)
|
||||||
X = np.random.uniform(-5.536, 5.536, size=(TEST_POINTS, NUM_VARS))
|
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, SIN, COS, EXP, ADD, SUB, MUL, DIV, POW]
|
||||||
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)]
|
||||||
|
|
||||||
@@ -74,7 +76,6 @@ 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=combined_mutation,
|
mutation_fn=combined_mutation,
|
||||||
# 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,
|
||||||
seed=SEED,
|
seed=SEED,
|
||||||
@@ -83,6 +84,7 @@ config = GARunConfig(
|
|||||||
elitism=15,
|
elitism=15,
|
||||||
max_generations=MAX_GENERATIONS,
|
max_generations=MAX_GENERATIONS,
|
||||||
log_every_generation=True,
|
log_every_generation=True,
|
||||||
|
save_generations=[1, 10, 20, 30, 40, 50, 100, 150, 200],
|
||||||
)
|
)
|
||||||
|
|
||||||
result = genetic_algorithm(config)
|
result = genetic_algorithm(config)
|
||||||
|
|||||||
BIN
lab4/original_tree.png
Normal file
BIN
lab4/original_tree.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
@@ -3,6 +3,7 @@ name = "lab4"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"graphviz>=0.21",
|
||||||
"matplotlib>=3.10.7",
|
"matplotlib>=3.10.7",
|
||||||
"numpy>=2.3.4",
|
"numpy>=2.3.4",
|
||||||
]
|
]
|
||||||
|
|||||||
11
lab4/uv.lock
generated
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "kiwisolver"
|
name = "kiwisolver"
|
||||||
version = "1.4.9"
|
version = "1.4.9"
|
||||||
@@ -108,12 +117,14 @@ name = "lab4"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "graphviz" },
|
||||||
{ name = "matplotlib" },
|
{ name = "matplotlib" },
|
||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "graphviz", specifier = ">=0.21" },
|
||||||
{ name = "matplotlib", specifier = ">=3.10.7" },
|
{ name = "matplotlib", specifier = ">=3.10.7" },
|
||||||
{ name = "numpy", specifier = ">=2.3.4" },
|
{ name = "numpy", specifier = ">=2.3.4" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user