рисование дерева

This commit is contained in:
2025-11-08 20:51:46 +03:00
parent bacfa20061
commit 4b2398ae05
8 changed files with 153 additions and 81 deletions

50
lab4/draw_tree.py Normal file
View 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)

View File

@@ -1,3 +0,0 @@
from .chromosome import Chromosome
__all__ = ["Chromosome"]

View File

@@ -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)

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View File

@@ -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
View File

@@ -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" },
] ]