3 Commits

Author SHA1 Message Date
7ec38a3385 Отчёт 2025-11-08 23:22:02 +03:00
4b2398ae05 рисование дерева 2025-11-08 23:21:42 +03:00
bacfa20061 mutation 2025-11-07 12:54:27 +03:00
24 changed files with 974 additions and 176 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,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)

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View File

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

@@ -0,0 +1,6 @@
*
!**/
!.gitignore
!report.tex
!img/**/*.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

705
lab4/report/report.tex Normal file
View 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
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" },
]
[[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" },
]