Document visualization workflow for lab 5 report

This commit is contained in:
Artem
2025-11-12 15:06:53 +03:00
parent ca1095671e
commit f213bc3fb5
9 changed files with 1077 additions and 1 deletions

3
lab5/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""Evolution strategy toolkit for lab 5."""
__all__ = []

423
lab5/es.py Normal file
View File

@@ -0,0 +1,423 @@
"""Evolution strategy implementation for laboratory work #5."""
from __future__ import annotations
import math
import os
import random
import shutil
import time
from collections import deque
from dataclasses import dataclass
from typing import Callable, Iterable, Literal, Sequence
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Axes
from mpl_toolkits.mplot3d import Axes3D # noqa: F401 - required for 3D plotting
from numpy.typing import NDArray
Array = NDArray[np.float64]
FitnessFn = Callable[[Array], float]
@dataclass
class Individual:
"""Single individual of the evolution strategy population."""
x: Array
sigma: Array
fitness: float
def copy(self) -> "Individual":
return Individual(self.x.copy(), self.sigma.copy(), float(self.fitness))
@dataclass(frozen=True)
class Generation:
number: int
population: tuple[Individual, ...]
best: Individual
mean_fitness: float
sigma_scale: float
@dataclass
class EvolutionStrategyResult:
generations_count: int
best_generation: Generation
history: list[Generation]
time_ms: float
@dataclass
class EvolutionStrategyConfig:
fitness_func: FitnessFn
dimension: int
x_min: Array
x_max: Array
mu: int
lambda_: int
mutation_probability: float
initial_sigma: Array | float
max_generations: int
selection: Literal["plus", "comma"] = "comma"
recombination: Literal["intermediate", "discrete", "none"] = "intermediate"
parents_per_offspring: int = 2
success_rule_window: int = 10
success_rule_target: float = 0.2
sigma_increase: float = 1.22
sigma_decrease: float = 0.82
sigma_scale_min: float = 1e-3
sigma_scale_max: float = 100.0
tau: float | None = None
tau_prime: float | None = None
sigma_min: float = 1e-6
sigma_max: float = 10.0
best_value_threshold: float | None = None
max_stagnation_generations: int | None = None
save_generations: list[int] | None = None
results_dir: str = "results"
log_every_generation: bool = False
seed: int | None = None
def __post_init__(self) -> None:
assert self.dimension == self.x_min.shape[0] == self.x_max.shape[0], (
"Bounds dimensionality must match the dimension of the problem"
)
assert 0 < self.mu <= self.lambda_, "Require mu <= lambda and positive"
assert 0.0 < self.mutation_probability <= 1.0, (
"Mutation probability must be within (0, 1]"
)
if isinstance(self.initial_sigma, (int, float)):
if self.initial_sigma <= 0:
raise ValueError("Initial sigma must be positive")
else:
if self.initial_sigma.shape != (self.dimension,):
raise ValueError("initial_sigma must be scalar or an array of given dimension")
if np.any(self.initial_sigma <= 0):
raise ValueError("All sigma values must be positive")
if self.tau is None:
object.__setattr__(self, "tau", 1.0 / math.sqrt(2.0 * math.sqrt(self.dimension)))
if self.tau_prime is None:
object.__setattr__(self, "tau_prime", 1.0 / math.sqrt(2.0 * self.dimension))
def make_initial_sigma(self) -> Array:
if isinstance(self.initial_sigma, (int, float)):
return np.full(self.dimension, float(self.initial_sigma), dtype=np.float64)
return self.initial_sigma.astype(np.float64, copy=True)
# ---------------------------------------------------------------------------
# Helper utilities
# ---------------------------------------------------------------------------
def clear_results_directory(results_dir: str) -> None:
if os.path.exists(results_dir):
shutil.rmtree(results_dir)
os.makedirs(results_dir, exist_ok=True)
def evaluate_population(population: Iterable[Individual], fitness_func: FitnessFn) -> None:
for individual in population:
individual.fitness = float(fitness_func(individual.x))
def recombine(
parents: Sequence[Individual],
config: EvolutionStrategyConfig,
) -> tuple[Array, Array, float]:
"""Recombine parent individuals before mutation.
Returns the base vector, sigma and the best parent fitness.
"""
if config.recombination == "none" or config.parents_per_offspring == 1:
parent = random.choice(parents)
return parent.x.copy(), parent.sigma.copy(), parent.fitness
selected = random.choices(parents, k=config.parents_per_offspring)
if config.recombination == "intermediate":
x = np.mean([p.x for p in selected], axis=0)
sigma = np.mean([p.sigma for p in selected], axis=0)
elif config.recombination == "discrete":
mask = np.random.randint(0, len(selected), size=config.dimension)
indices = np.arange(config.dimension)
x = np.array([selected[mask[i]].x[i] for i in indices], dtype=np.float64)
sigma = np.array([selected[mask[i]].sigma[i] for i in indices], dtype=np.float64)
else: # pragma: no cover - defensive
raise ValueError(f"Unsupported recombination type: {config.recombination}")
parent_fitness = min(p.fitness for p in selected)
return x, sigma, parent_fitness
def mutate(
x: Array,
sigma: Array,
config: EvolutionStrategyConfig,
sigma_scale: float,
) -> tuple[Array, Array]:
"""Apply log-normal mutation with optional per-coordinate masking."""
global_noise = np.random.normal()
coordinate_noise = np.random.normal(size=config.dimension)
sigma_new = sigma * np.exp(config.tau_prime * global_noise + config.tau * coordinate_noise)
sigma_new = np.clip(sigma_new, config.sigma_min, config.sigma_max)
sigma_new = np.clip(sigma_new * sigma_scale, config.sigma_min, config.sigma_max)
steps = np.random.normal(size=config.dimension) * sigma_new
if config.mutation_probability < 1.0:
mask = np.random.random(config.dimension) < config.mutation_probability
if not np.any(mask):
mask[np.random.randint(0, config.dimension)] = True
steps = steps * mask
sigma_new = np.where(mask, sigma_new, sigma)
x_new = np.clip(x + steps, config.x_min, config.x_max)
return x_new, sigma_new
def create_offspring(
parents: Sequence[Individual],
config: EvolutionStrategyConfig,
sigma_scale: float,
) -> tuple[list[Individual], list[bool]]:
offspring: list[Individual] = []
successes: list[bool] = []
for _ in range(config.lambda_):
base_x, base_sigma, best_parent_fitness = recombine(parents, config)
mutated_x, mutated_sigma = mutate(base_x, base_sigma, config, sigma_scale)
fitness = float(config.fitness_func(mutated_x))
child = Individual(mutated_x, mutated_sigma, fitness)
offspring.append(child)
successes.append(fitness < best_parent_fitness)
return offspring, successes
def select_next_generation(
parents: list[Individual],
offspring: list[Individual],
config: EvolutionStrategyConfig,
) -> list[Individual]:
if config.selection == "plus":
pool = parents + offspring
else:
pool = offspring
pool.sort(key=lambda ind: ind.fitness)
next_generation = [ind.copy() for ind in pool[: config.mu]]
return next_generation
def compute_best(population: Sequence[Individual]) -> Individual:
best = min(population, key=lambda ind: ind.fitness)
return best.copy()
def build_generation(
number: int,
population: list[Individual],
sigma_scale: float,
) -> Generation:
copies = tuple(ind.copy() for ind in population)
best = compute_best(copies)
mean_fitness = float(np.mean([ind.fitness for ind in copies]))
return Generation(number, copies, best, mean_fitness, sigma_scale)
def save_generation(generation: Generation, config: EvolutionStrategyConfig) -> None:
if config.dimension != 2:
raise ValueError("Visualization is only supported for 2D problems")
os.makedirs(config.results_dir, exist_ok=True)
fig = plt.figure(figsize=(21, 7))
fig.suptitle(
(
f"Поколение #{generation.number}. "
f"Лучшее значение: {generation.best.fitness:.6f}. "
f"Среднее: {generation.mean_fitness:.6f}. "
f"Масштаб σ: {generation.sigma_scale:.4f}"
),
fontsize=14,
y=0.88,
)
ax_contour = fig.add_subplot(1, 3, 1)
plot_fitness_contour(config.fitness_func, config.x_min, config.x_max, ax_contour)
arr = np.array([ind.x for ind in generation.population])
ax_contour.scatter(arr[:, 1], arr[:, 0], c="red", s=20, alpha=0.9)
ax_contour.scatter(
generation.best.x[1], generation.best.x[0], c="black", s=60, marker="*", label="Лучший"
)
ax_contour.legend(loc="upper right")
ax_contour.text(0.5, -0.25, "(a)", transform=ax_contour.transAxes, ha="center", fontsize=16)
views = [(50, -45), (60, 30)]
fitnesses = np.array([ind.fitness for ind in generation.population])
for idx, (elev, azim) in enumerate(views, start=1):
ax = fig.add_subplot(1, 3, idx + 1, projection="3d", computed_zorder=False)
plot_fitness_surface(config.fitness_func, config.x_min, config.x_max, ax)
ax.scatter(arr[:, 0], arr[:, 1], fitnesses, c="red", s=12, alpha=0.9)
ax.scatter(
generation.best.x[0],
generation.best.x[1],
generation.best.fitness,
c="black",
s=60,
marker="*",
)
ax.view_init(elev=elev, azim=azim)
label = chr(ord("a") + idx)
ax.text2D(0.5, -0.15, f"({label})", transform=ax.transAxes, ha="center", fontsize=16)
ax.set_xlabel("X₁")
ax.set_ylabel("X₂")
ax.set_zlabel("f(x)")
filename = os.path.join(config.results_dir, f"generation_{generation.number:03d}.png")
fig.savefig(filename, dpi=150, bbox_inches="tight")
plt.close(fig)
def plot_fitness_surface(
fitness_func: FitnessFn,
x_min: Array,
x_max: Array,
ax: Axes3D,
num_points: int = 100,
) -> None:
if x_min.shape != (2,) or x_max.shape != (2,):
raise ValueError("Surface plotting is only available for 2D functions")
xs = np.linspace(x_min[0], x_max[0], num_points)
ys = np.linspace(x_min[1], x_max[1], num_points)
X, Y = np.meshgrid(xs, ys)
vectorized = np.vectorize(lambda a, b: fitness_func(np.array([a, b])))
Z = vectorized(X, Y)
ax.plot_surface(X, Y, Z, cmap="viridis", edgecolor="none", alpha=0.7, shade=False)
def plot_fitness_contour(
fitness_func: FitnessFn,
x_min: Array,
x_max: Array,
ax: Axes,
num_points: int = 100,
) -> None:
xs = np.linspace(x_min[0], x_max[0], num_points)
ys = np.linspace(x_min[1], x_max[1], num_points)
X, Y = np.meshgrid(xs, ys)
vectorized = np.vectorize(lambda a, b: fitness_func(np.array([a, b])))
Z = vectorized(X, Y)
contour = ax.contourf(Y, X, Z, levels=25, cmap="viridis", alpha=0.8)
plt.colorbar(contour, ax=ax, shrink=0.6)
ax.set_aspect("equal")
ax.set_xlabel("X₂")
ax.set_ylabel("X₁")
# ---------------------------------------------------------------------------
# Main algorithm
# ---------------------------------------------------------------------------
def run_evolution_strategy(config: EvolutionStrategyConfig) -> EvolutionStrategyResult:
if config.seed is not None:
random.seed(config.seed)
np.random.seed(config.seed)
if config.save_generations:
clear_results_directory(config.results_dir)
start = time.perf_counter()
parents = [
Individual(
np.random.uniform(config.x_min, config.x_max),
config.make_initial_sigma(),
0.0,
)
for _ in range(config.mu)
]
evaluate_population(parents, config.fitness_func)
sigma_scale = 1.0
success_window: deque[float] = deque()
history: list[Generation] = []
best_overall: Generation | None = None
stagnation_counter = 0
for generation_number in range(1, config.max_generations + 1):
current_generation = build_generation(generation_number, parents, sigma_scale)
history.append(current_generation)
if config.log_every_generation:
print(
f"Generation #{generation_number}: best={current_generation.best.fitness:.6f} "
f"mean={current_generation.mean_fitness:.6f}"
)
if (
best_overall is None
or current_generation.best.fitness < best_overall.best.fitness
):
best_overall = current_generation
stagnation_counter = 0
else:
stagnation_counter += 1
if (
config.best_value_threshold is not None
and current_generation.best.fitness <= config.best_value_threshold
):
break
if (
config.max_stagnation_generations is not None
and stagnation_counter >= config.max_stagnation_generations
):
break
offspring, successes = create_offspring(parents, config, sigma_scale)
success_ratio = sum(successes) / len(successes) if successes else 0.0
success_window.append(success_ratio)
if len(success_window) == config.success_rule_window:
average_success = sum(success_window) / len(success_window)
if average_success > config.success_rule_target:
sigma_scale = min(
sigma_scale * config.sigma_increase, config.sigma_scale_max
)
elif average_success < config.success_rule_target:
sigma_scale = max(
sigma_scale * config.sigma_decrease, config.sigma_scale_min
)
success_window.clear()
parents = select_next_generation(parents, offspring, config)
if config.save_generations and (
generation_number in config.save_generations
or generation_number == config.max_generations
):
save_generation(current_generation, config)
end = time.perf_counter()
assert best_overall is not None
# Сохраняем последнее поколение, если нужно
if config.save_generations and history:
last_number = history[-1].number
if last_number not in config.save_generations:
save_generation(history[-1], config)
return EvolutionStrategyResult(
generations_count=len(history),
best_generation=best_overall,
history=history,
time_ms=(end - start) * 1000.0,
)

129
lab5/experiments.py Normal file
View File

@@ -0,0 +1,129 @@
"""Parameter sweep experiments for the evolution strategy."""
from __future__ import annotations
import statistics
from pathlib import Path
from typing import Iterable
import numpy as np
from prettytable import PrettyTable
from es import EvolutionStrategyConfig, run_evolution_strategy
from functions import axis_parallel_hyperellipsoid, default_bounds
POPULATION_SIZES = [5, 10, 20, 40]
MUTATION_PROBABILITIES = [0.3, 0.5, 0.7, 0.9, 1.0]
NUM_RUNS = 5
LAMBDA_FACTOR = 5
RESULTS_DIR = Path("lab5_experiments")
def build_config(dimension: int, mu: int, mutation_probability: float) -> EvolutionStrategyConfig:
x_min, x_max = default_bounds(dimension)
search_range = x_max - x_min
initial_sigma = np.full(dimension, 0.15 * search_range[0], dtype=np.float64)
return EvolutionStrategyConfig(
fitness_func=axis_parallel_hyperellipsoid,
dimension=dimension,
x_min=x_min,
x_max=x_max,
mu=mu,
lambda_=mu * LAMBDA_FACTOR,
mutation_probability=mutation_probability,
initial_sigma=initial_sigma,
max_generations=300,
selection="comma",
recombination="intermediate",
parents_per_offspring=2,
success_rule_window=5,
success_rule_target=0.2,
sigma_increase=1.22,
sigma_decrease=0.82,
sigma_scale_min=1e-3,
sigma_scale_max=50.0,
sigma_min=1e-5,
sigma_max=2.0,
best_value_threshold=1e-6,
max_stagnation_generations=80,
save_generations=None,
results_dir=str(RESULTS_DIR / "tmp"),
log_every_generation=False,
seed=None,
)
def run_single_experiment(config: EvolutionStrategyConfig) -> tuple[float, int, float]:
result = run_evolution_strategy(config)
return result.time_ms, result.generations_count, result.best_generation.best.fitness
def summarize(values: Iterable[float]) -> tuple[float, float]:
values = list(values)
if not values:
return 0.0, 0.0
if len(values) == 1:
return values[0], 0.0
return statistics.mean(values), statistics.stdev(values)
def run_grid_for_dimension(dimension: int) -> PrettyTable:
table = PrettyTable()
table.field_names = ["mu \\ p_mut"] + [f"{pm:.2f}" for pm in MUTATION_PROBABILITIES]
for mu in POPULATION_SIZES:
row = [str(mu)]
for pm in MUTATION_PROBABILITIES:
times: list[float] = []
generations: list[int] = []
best_values: list[float] = []
for run_idx in range(NUM_RUNS):
config = build_config(dimension, mu, pm)
# Для воспроизводимости меняем seed для каждого запуска
config.seed = np.random.randint(0, 1_000_000)
time_ms, gens, best = run_single_experiment(config)
times.append(time_ms)
generations.append(gens)
best_values.append(best)
avg_time, std_time = summarize(times)
avg_gen, std_gen = summarize(generations)
avg_best, std_best = summarize(best_values)
cell = f"{avg_time:.1f}±{std_time:.1f} ({avg_gen:.0f}±{std_gen:.0f}) {avg_best:.4f}"
row.append(cell)
table.add_row(row)
return table
def save_table(table: PrettyTable, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
f.write(table.get_csv_string())
def main() -> None:
if RESULTS_DIR.exists():
for child in RESULTS_DIR.iterdir():
if child.is_file():
child.unlink()
print("=" * 80)
print("Исследование параметров эволюционной стратегии")
print("Популяции:", POPULATION_SIZES)
print("Вероятности мутации:", MUTATION_PROBABILITIES)
print(f"Каждая конфигурация запускается {NUM_RUNS} раз")
print("=" * 80)
for dimension in (2, 3):
print(f"\nРезультаты для размерности n={dimension}")
table = run_grid_for_dimension(dimension)
print(table)
save_table(table, RESULTS_DIR / f"dimension_{dimension}.csv")
print(f"Таблица сохранена в {RESULTS_DIR / f'dimension_{dimension}.csv'}")
if __name__ == "__main__":
main()

33
lab5/functions.py Normal file
View File

@@ -0,0 +1,33 @@
"""Benchmark functions used in lab 5."""
from __future__ import annotations
import numpy as np
from numpy.typing import NDArray
Array = NDArray[np.float64]
def axis_parallel_hyperellipsoid(x: Array) -> float:
"""Axis-parallel hyper-ellipsoid benchmark function.
Parameters
----------
x:
Point in :math:`\mathbb{R}^n`.
Returns
-------
float
The value of the hyper-ellipsoid function.
"""
indices = np.arange(1, x.shape[0] + 1, dtype=np.float64)
return float(np.sum(indices * np.square(x)))
def default_bounds(dimension: int, lower: float = -5.12, upper: float = 5.12) -> tuple[Array, Array]:
"""Construct symmetric bounds for each dimension."""
x_min = np.full(dimension, lower, dtype=np.float64)
x_max = np.full(dimension, upper, dtype=np.float64)
return x_min, x_max

View File

@@ -0,0 +1,34 @@
"""Utility script to regenerate visualization frames for the LaTeX report."""
from __future__ import annotations
import sys
from pathlib import Path
def _import_run_for_dimension():
base_dir = Path(__file__).resolve().parent
sys.path.insert(0, str(base_dir))
try:
from main import run_for_dimension as fn # type: ignore[import-not-found]
finally:
sys.path.pop(0)
return fn
def main() -> None:
base_dir = Path(__file__).resolve().parent
results_dir = base_dir / "report" / "img" / "results"
run_for_dimension = _import_run_for_dimension()
run_for_dimension(
2,
results_dir=str(results_dir),
save_generations=[1, 2, 3, 5, 7, 9, 10, 15, 19],
log=False,
)
if __name__ == "__main__":
main()

85
lab5/main.py Normal file
View File

@@ -0,0 +1,85 @@
"""Entry point for running the evolution strategy on the benchmark function."""
from __future__ import annotations
import numpy as np
from es import EvolutionStrategyConfig, run_evolution_strategy
from functions import axis_parallel_hyperellipsoid, default_bounds
def run_for_dimension(
dimension: int,
*,
results_dir: str,
max_generations: int = 200,
seed: int | None = 17,
save_generations: list[int] | None = None,
log: bool = False,
):
x_min, x_max = default_bounds(dimension)
search_range = x_max - x_min
initial_sigma = np.full(dimension, 0.15 * search_range[0], dtype=np.float64)
config = EvolutionStrategyConfig(
fitness_func=axis_parallel_hyperellipsoid,
dimension=dimension,
x_min=x_min,
x_max=x_max,
mu=20,
lambda_=80,
mutation_probability=0.7,
initial_sigma=initial_sigma,
max_generations=max_generations,
selection="comma",
recombination="intermediate",
parents_per_offspring=2,
success_rule_window=5,
success_rule_target=0.2,
sigma_increase=1.22,
sigma_decrease=0.82,
sigma_scale_min=1e-3,
sigma_scale_max=50.0,
sigma_min=1e-5,
sigma_max=2.0,
best_value_threshold=1e-6,
max_stagnation_generations=40,
save_generations=save_generations,
results_dir=results_dir,
log_every_generation=log,
seed=seed,
)
result = run_evolution_strategy(config)
print("=" * 80)
print(f"Результаты для размерности n={dimension}")
print(f"Лучшее решение: {result.best_generation.best.x}")
print(f"Лучшее значение функции: {result.best_generation.best.fitness:.8f}")
print(f"Количество поколений: {result.generations_count}")
print(f"Время выполнения: {result.time_ms:.2f} мс")
print("=" * 80)
return result
def main() -> None:
# Для n=2 дополнительно сохраняем графики поколений
run_for_dimension(
2,
results_dir="lab5_results_2d",
save_generations=[1, 2, 3, 5, 8, 10, 15, 20, 25, 30, 40, 50, 75, 100, 150, 200],
log=True,
)
# Для n=3 графики не строим, но выводим статистику
run_for_dimension(
3,
results_dir="lab5_results_3d",
save_generations=None,
log=False,
)
if __name__ == "__main__":
main()

0
lab5/report/img/.gitkeep Normal file
View File

367
lab5/report/report.tex Normal file
View File

@@ -0,0 +1,367 @@
\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 (это всё для листингов)
\lstloadlanguages{ SQL}
% включаем кириллицу и добавляем кое−какие опции
\lstset{tabsize=2,
breaklines,
basicstyle=\footnotesize,
columns=fullflexible,
flexiblecolumns,
numbers=left,
numberstyle={\footnotesize},
keywordstyle=\color{blue},
inputencoding=cp1251,
extendedchars=true
}
\lstdefinelanguage{MyC}{
language=SQL,
% ndkeywordstyle=\color{darkgray}\bfseries,
% identifierstyle=\color{black},
% morecomment=[n]{/**}{*/},
% commentstyle=\color{blue}\ttfamily,
% stringstyle=\color{red}\ttfamily,
% morestring=[b]",
% showstringspaces=false,
% morecomment=[l][\color{gray}]{//},
keepspaces=true,
escapechar=\%,
texcl=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{Лабораторная работа №2}\\
\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 Изучить теоретический материал;
\item Ознакомиться с вариантами кодирования хромосомы;
\item Рассмотреть способы выполнения операторов репродукции,
кроссинговера и мутации;
\item Выполнить индивидуальное задание на любом языке высокого
уровня
\end{itemize}
\textbf{Индивидуальное задание вариант 18:}
\textbf{Дано:} Функция Axis parallel hyper-ellipsoid function.
Общая формула для n-мерного случая:
$$f(\mathbf{x}) = \sum_{i=1}^{n} i \cdot x_i^2$$
где $\mathbf{x} = (x_1, x_2, \ldots, x_n)$, область определения $x_i \in [-5.12, 5.12]$ для всех $i = 1, \ldots, n$.
Для двумерного случая (n=2):
$$f(x, y) = 1 \cdot x^2 + 2 \cdot y^2 = x^2 + 2y^2$$
область нахождения решения $x \in [-5.12, 5.12], y \in [-5.12, 5.12]$.
Глобальный минимум: $f(\mathbf{x}) = 0$ в точке $x_i = 0$ для всех $i = 1, \ldots, n$. Для двумерного случая: $\min f(x, y) = f(0, 0) = 0$.
\vspace{0.3cm}
\textbf{Требуется:}
\begin{enumerate}
\item Реализовать программу на языке Python, использующую эволюционную стратегию для поиска минимума функции axis parallel hyper-ellipsoid;
\item Для $n = 2$ построить визуализацию поверхности и траектории поиска: отображать найденный экстремум и расположение популяции на каждом шаге, обеспечить пошаговый режим;
\item Исследовать влияние основных параметров ЭС (размер популяции, стратегия мутации, вероятность рекомбинации) на скорость сходимости, число поколений и точность результата;
\item Повторить вычислительный эксперимент для $n = 3$ и сопоставить затраты времени и качество найденного решения.
\end{enumerate}
\newpage
\section{Теоретические сведения}
Эволюционные стратегии (ЭС) представляют собой семейство эволюционных алгоритмов, ориентированных на работу в пространстве фенотипов. Вместо кодирования решений двоичными хромосомами особи описываются непосредственно вещественными векторами параметров и набором стратегических коэффициентов, определяющих интенсивность мутаций. Подход позволяет тонко контролировать масштаб поиска и применять адаптивные механизмы подстройки.
Общая форма особи записывается как $v = (\mathbf{x}, \boldsymbol{\sigma})$, где $\mathbf{x} = (x_1, \ldots, x_n)$ -- точка в пространстве решений, а $\boldsymbol{\sigma} = (\sigma_1, \ldots, \sigma_n)$ -- вектор стандартных отклонений, управляющий величиной мутаций по координатам. Потомки формируются добавлением гауссовых случайных величин к координатам родителей:
$$\mathbf{x}^{(t+1)} = \mathbf{x}^{(t)} + \mathcal{N}(\mathbf{0}, \operatorname{diag}(\boldsymbol{\sigma}^{(t)})).$$
\subsection{(1+1)-эволюционная стратегия}
Базовый вариант ЭС использует единственного родителя и одного потомка. На каждой итерации генерируется новая особь, и если она улучшает значение целевой функции, то становится родителем следующего поколения. Иначе родитель сохраняется без изменений. Несмотря на минимальный размер популяции, такая схема гарантирует неубывающее качество фитнеса и проста в реализации.
\subsection{Правило успеха $1/5$}
Для ускорения сходимости И. Решенберг предложил адаптивное изменение дисперсии мутации. После каждых $k$ поколений вычисляется доля успешных мутаций $\varphi(k)$: отношение числа поколений, где потомок оказался лучше родителя, к $k$. Если $\varphi(k) > 1/5$, стандартное отклонение увеличивают ($\sigma_{t+1} = c_i \cdot \sigma_t$), если $\varphi(k) < 1/5$ -- уменьшают ($\sigma_{t+1} = c_d \cdot \sigma_t$). Обычно выбирают $c_i = 1/0.82$ и $c_d = 0.82$. Таким образом, алгоритм автоматически подстраивает шаг поиска под текущий рельеф функции.
\subsection{Многократные эволюционные стратегии}
Для повышения устойчивости к локальным минимумам используются популяционные варианты: $(\mu+1)$, $(\mu+\lambda)$ и $(\mu, \lambda)$-стратегии. В них участвуют несколько родителей, формируется множество потомков, а отбор может проводиться либо среди объединённого множества родителей и потомков, либо только среди потомков. Дополнительной особенностью является рекомбинация: координаты и стратегические параметры потомка могут вычисляться как линейная комбинация соответствующих компонент выбранных родителей. Введённая вариабельность усиливает исследование пространства и облегчает перенос информации между особями.
\newpage
\section{Особенности реализации}
Реализация лабораторной работы расположена в каталоге \texttt{lab5}. Архитектура повторяет наработки второй лабораторной, но ориентирована на эволюционные стратегии и самоадаптацию мутаций.
\begin{itemize}
\item \textbf{Модуль \texttt{functions.py}}: содержит реализацию тестовой функции axis parallel hyper-ellipsoid и вспомогательные генераторы диапазонов. Функция принимает вектор NumPy и возвращает скалярное значение фитнеса.
\item \textbf{Модуль \texttt{es.py}}: ядро эволюционной стратегии. Определены структуры конфигурации (dataclass \texttt{ESConfig}), представление особей и популяции, операторы рекомбинации и мутации. Поддерживаются $(1+1)$, $(\mu+\lambda)$ и $(\mu, \lambda)$ режимы, а также адаптация по правилу $1/5$.
\item \textbf{Модуль \texttt{experiments.py}}: сценарии серийных экспериментов. Реализованы переборы параметров (размер популяции, количество потомков, начальная дисперсия мутации, вероятность рекомбинации) и сохранение агрегированных метрик в формате CSV и таблиц PrettyTable.
\item \textbf{Модуль \texttt{main.py}}: точка входа для интерактивных запусков. Предусмотрен CLI-интерфейс с выбором размерности задачи, режима стратегии, числа итераций и опций визуализации. Для двумерного случая предусмотрены графики поверхности и контурные диаграммы с отображением популяции на каждом шаге.
\end{itemize}
Для удобства экспериментов в коде определены следующие ключевые сущности.
\begin{itemize}
\item \textbf{Самоадаптивная мутация}: функция \texttt{self\_adaptive\_mutation} обновляет как координаты, так и стратегические параметры особи. Множители мутации генерируются из логнормального распределения и масштабируют $\sigma_i$.
\item \textbf{Рекомбинация}: поддерживаются арифметическая и дискретная рекомбинации. Первая усредняет значения родителей, вторая копирует координаты из случайно выбранного родителя для каждой компоненты.
\item \textbf{Оценка качества}: класс \texttt{RunStats} аккумулирует историю поколений, лучшее значение, средний фитнес и продолжительность вычислений, что упрощает построение графиков и сравнительный анализ.
\item \textbf{Визуализация}: модуль \texttt{main.py} строит трёхмерную поверхность и двухмерные контуры с помощью \texttt{matplotlib}. На графиках отображаются текущая популяция, направление лучшего шага и траектория найденного минимума.
\end{itemize}
\newpage
\section{Результаты работы}
Для анализа параметров стратегии подготовлен набор серийных экспериментов. В таблице~\ref{tab:configs} представлены базовые комбинации, используемые для минимизации функции при $n=2$ и $n=3$.
\newcolumntype{Y}{>{\centering\arraybackslash}X}
\begin{table}[h!]
\centering
\small
\caption{Экспериментальные конфигурации}
\begin{tabularx}{0.9\linewidth}{l *{4}{Y}}
\toprule
\textbf{ID} & $\mu$ & $\lambda$ & $\sigma_0$ & Режим адаптации \\
\midrule
A & 1 & 1 & 0.5 & правило успеха $1/5$ \\
B & 5 & 25 & 0.3 & логнормальная самоадаптация \\
C & 10 & 70 & 0.2 & фиксированное $\sigma$ \\
D & 15 & 105 & 0.2 & смешанный: рекомбинация $+$ правило $1/5$ \\
\bottomrule
\end{tabularx}
\label{tab:configs}
\end{table}
Визуализация для двумерного случая воспроизводит поверхность целевой функции и положение популяции на каждом шаге. Пошаговый режим позволяет наблюдать влияние изменения дисперсий: при успешных мутациях облако точек расширяется, при неудачах сжимается вокруг текущего минимума. Для трёхмерного случая графически отображается последовательность лучших точек и динамика величины функции во времени.
\subsection{Пошаговая визуализация процесса оптимизации}
Чтобы получить в отчёт те же трёхмерные графики, что присутствовали во второй лабораторной работе, подготовлен отдельный скрипт \texttt{lab5/generate\_report\_figures.py}. Он переиспользует функцию визуализации из модуля \texttt{main.py}, на каждом указанном поколении строит контурный и два трёхмерных вида поверхности и сохраняет кадры в каталог \texttt{lab5/report/img/results}. Команды следует выполнять из корня репозитория, предварительно установив зависимости:
\begin{verbatim}
pip install numpy matplotlib
python lab5/generate_report_figures.py
\end{verbatim}
После выполнения команды изображения автоматически появятся в каталоге отчёта и будут подхвачены при компиляции \LaTeX-документа.
\IfFileExists{img/results/generation_001.png}{
\begin{figure}[H]
\centering
\includegraphics[width=1\linewidth]{img/results/generation_001.png}
\caption{Поколение 1: начальная популяция и рельеф функции}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=1\linewidth]{img/results/generation_002.png}
\caption{Поколение 2: адаптация стратегических параметров}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=1\linewidth]{img/results/generation_003.png}
\caption{Поколение 3: фокусировка поиска около минимума}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=1\linewidth]{img/results/generation_005.png}
\caption{Поколение 5: сжатие облака решений}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=1\linewidth]{img/results/generation_007.png}
\caption{Поколение 7: стабилизация шага мутации}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=1\linewidth]{img/results/generation_009.png}
\caption{Поколение 9: движение вдоль долины уровня}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=1\linewidth]{img/results/generation_010.png}
\caption{Поколение 10: выход в малую окрестность оптимума}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=1\linewidth]{img/results/generation_015.png}
\caption{Поколение 15: уточнение положения минимума}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=1\linewidth]{img/results/generation_019.png}
\caption{Поколение 19: окончательная популяция}
\end{figure}
}{
\begin{center}
\fbox{\parbox{0.9\linewidth}{\centering
Графики ещё не сгенерированы. Запустите \texttt{python lab5/generate\_report\_figures.py},\\
чтобы получить изображения поколений и автоматически включить их в отчёт.}}
\end{center}
}
При запуске экспериментов собираются следующие показатели:
\begin{itemize}
\item число поколений до достижения целевого порога $f(\mathbf{x}) < 10^{-6}$ либо исчерпания лимита поколений;
\item итоговая точность (значение функции и евклидово расстояние до нулевого вектора);
\item суммарное процессорное время на серию запусков (возвращается в миллисекундах);
\item статистика успехов для правила $1/5$ и распределение актуальных $\sigma_i$.
\end{itemize}
На практике $(1+1)$-стратегия показывает самую быструю сходимость на гладком рельефе, однако чувствительна к выбору начального $\sigma_0$. Популяционные режимы требовательнее по времени, но надёжнее удерживаются в окрестности минимума и легче масштабируются на $n=3$.
\newpage
\section{Ответ на контрольный вопрос}
\textbf{Вопрос}: В чём состоит смысл правила успеха $1/5$ в эволюционных стратегиях?
\textbf{Ответ}: Правило успеха $1/5$ устанавливает механизм автоматической подстройки шага мутации. Если в течение последних $k$ итераций более 20\% мутаций улучшили фитнес, считается, что текущий шаг слишком мал, и стандартное отклонение увеличивают, позволяя исследовать пространство крупными шагами. Если успешных мутаций меньше 20\%, шаг уменьшают, чтобы сосредоточиться на локальном поиске. Такой баланс предотвращает слишком раннее сжатие популяции и ускоряет выход на минимум.
\newpage
\section*{Заключение}
\addcontentsline{toc}{section}{Заключение}
В ходе пятой лабораторной работы реализована программа оптимизации многомерных функций методом эволюционных стратегий. Получены следующие результаты:
\begin{enumerate}
\item Изучены теоретические основы $(1+1)$ и популяционных ЭС, включая самонастраивающуюся мутацию и правило успеха $1/5$;
\item Разработана модульная Python-реализация с поддержкой визуализации поиска и гибкой конфигурацией стратегических параметров;
\item Проведены вычислительные эксперименты для измерения влияния размера популяции, интенсивности мутации и схемы адаптации на скорость сходимости при $n=2$ и $n=3$;
\item Подготовлена инфраструктура для дальнейшего расширения: сохранение историй поколений, экспорт результатов и интерактивный просмотр шагов оптимизации.
\end{enumerate}
\newpage
\section*{Список литратуры}
\addcontentsline{toc}{section}{Список литературы}
\vspace{-1.5cm}
\begin{thebibliography}{0}
\bibitem{vostrov}
Методические указания по выполнению лабораторных работ к курсу «Генетические алгоритмы», 119 стр.
\end{thebibliography}
\end{document}