Document visualization workflow for lab 5 report
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,4 +3,6 @@
|
|||||||
!**/
|
!**/
|
||||||
!*.gitignore
|
!*.gitignore
|
||||||
!*.py
|
!*.py
|
||||||
!lab4/*
|
!.gitkeep
|
||||||
|
!lab4/*
|
||||||
|
!lab5/report/report.tex
|
||||||
|
|||||||
3
lab5/__init__.py
Normal file
3
lab5/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""Evolution strategy toolkit for lab 5."""
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
423
lab5/es.py
Normal file
423
lab5/es.py
Normal 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
129
lab5/experiments.py
Normal 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
33
lab5/functions.py
Normal 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
|
||||||
34
lab5/generate_report_figures.py
Normal file
34
lab5/generate_report_figures.py
Normal 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
85
lab5/main.py
Normal 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
0
lab5/report/img/.gitkeep
Normal file
367
lab5/report/report.tex
Normal file
367
lab5/report/report.tex
Normal 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}
|
||||||
Reference in New Issue
Block a user