Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4fcf6562e | |||
|
|
93ab829cff | ||
|
|
9f591dadda | ||
| 7394e5b9fb | |||
| 1f80f2f7dc | |||
| b6c19c5240 | |||
| cf9fc98376 | |||
| 6400996fcf | |||
| 957de42e16 | |||
|
|
f213bc3fb5 | ||
| ca1095671e |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,4 +3,9 @@
|
||||
!**/
|
||||
!*.gitignore
|
||||
!*.py
|
||||
!lab4/*
|
||||
!.gitkeep
|
||||
!lab3/data.txt
|
||||
!lab4/*
|
||||
!lab5/report/report.tex
|
||||
!lab5/README.md
|
||||
!lab6/README.md
|
||||
|
||||
89
lab3/data.txt
Normal file
89
lab3/data.txt
Normal file
@@ -0,0 +1,89 @@
|
||||
1 11511.3889 42106.3889
|
||||
2 11503.0556 42855.2778
|
||||
3 11438.3333 42057.2222
|
||||
4 11438.3333 42057.2222
|
||||
5 11438.3333 42057.2222
|
||||
6 11785.2778 42884.4444
|
||||
7 11785.2778 42884.4444
|
||||
8 11785.2778 42884.4444
|
||||
9 11785.2778 42884.4444
|
||||
10 12363.3333 43189.1667
|
||||
11 11846.9444 42660.5556
|
||||
12 11503.0556 42855.2778
|
||||
13 11963.0556 43290.5556
|
||||
14 11963.0556 43290.5556
|
||||
15 12300.0000 42433.3333
|
||||
16 11973.0556 43026.1111
|
||||
17 11973.0556 43026.1111
|
||||
18 11461.1111 43252.7778
|
||||
19 11461.1111 43252.7778
|
||||
20 11461.1111 43252.7778
|
||||
21 11461.1111 43252.7778
|
||||
22 11600.0000 43150.0000
|
||||
23 12386.6667 43334.7222
|
||||
24 12386.6667 43334.7222
|
||||
25 11595.0000 43148.0556
|
||||
26 11595.0000 43148.0556
|
||||
27 11569.4444 43136.6667
|
||||
28 11310.2778 42929.4444
|
||||
29 11310.2778 42929.4444
|
||||
30 11310.2778 42929.4444
|
||||
31 11963.0556 43290.5556
|
||||
32 11416.6667 42983.3333
|
||||
33 11416.6667 42983.3333
|
||||
34 11595.0000 43148.0556
|
||||
35 12149.4444 42477.5000
|
||||
36 11595.0000 43148.0556
|
||||
37 11595.0000 43148.0556
|
||||
38 11108.6111 42373.8889
|
||||
39 11108.6111 42373.8889
|
||||
40 11108.6111 42373.8889
|
||||
41 11108.6111 42373.8889
|
||||
42 11183.3333 42933.3333
|
||||
43 12372.7778 42711.3889
|
||||
44 11583.3333 43150.0000
|
||||
45 11583.3333 43150.0000
|
||||
46 11583.3333 43150.0000
|
||||
47 11583.3333 43150.0000
|
||||
48 11583.3333 43150.0000
|
||||
49 11822.7778 42673.6111
|
||||
50 11822.7778 42673.6111
|
||||
51 12058.3333 42195.5556
|
||||
52 11003.6111 42102.5000
|
||||
53 11003.6111 42102.5000
|
||||
54 11003.6111 42102.5000
|
||||
55 11522.2222 42841.9444
|
||||
56 12386.6667 43334.7222
|
||||
57 12386.6667 43334.7222
|
||||
58 12386.6667 43334.7222
|
||||
59 11569.4444 43136.6667
|
||||
60 11569.4444 43136.6667
|
||||
61 11569.4444 43136.6667
|
||||
62 11155.8333 42712.5000
|
||||
63 11155.8333 42712.5000
|
||||
64 11155.8333 42712.5000
|
||||
65 11155.8333 42712.5000
|
||||
66 11133.3333 42885.8333
|
||||
67 11133.3333 42885.8333
|
||||
68 11133.3333 42885.8333
|
||||
69 11133.3333 42885.8333
|
||||
70 11133.3333 42885.8333
|
||||
71 11003.6111 42102.5000
|
||||
72 11770.2778 42651.9444
|
||||
73 11133.3333 42885.8333
|
||||
74 11690.5556 42686.6667
|
||||
75 11690.5556 42686.6667
|
||||
76 11751.1111 42814.4444
|
||||
77 12645.0000 42973.3333
|
||||
78 12421.6667 42895.5556
|
||||
79 12421.6667 42895.5556
|
||||
80 11485.5556 43187.2222
|
||||
81 11423.8889 43000.2778
|
||||
82 11423.8889 43000.2778
|
||||
83 11715.8333 41836.1111
|
||||
84 11297.5000 42853.3333
|
||||
85 11297.5000 42853.3333
|
||||
86 11583.3333 43150.0000
|
||||
87 11569.4444 43136.6667
|
||||
88 12286.9444 43355.5556
|
||||
89 12355.8333 43156.3889
|
||||
@@ -26,9 +26,9 @@ from gp.mutations import (
|
||||
ShrinkMutation,
|
||||
)
|
||||
from gp.ops import ADD, COS, DIV, EXP, MUL, POW, SIN, SQUARE, SUB
|
||||
from gp.population import ramped_initialization
|
||||
from gp.primitive import Var
|
||||
from gp.selection import tournament_selection
|
||||
from gp.utils import ramped_initialization
|
||||
|
||||
NUM_VARS = 8
|
||||
TEST_POINTS = 10000
|
||||
|
||||
3
lab5/README.md
Normal file
3
lab5/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Attention!
|
||||
|
||||
lab5 is fully AI generated slop.
|
||||
3
lab5/__init__.py
Normal file
3
lab5/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Evolution strategy toolkit for lab 5."""
|
||||
|
||||
__all__ = []
|
||||
327
lab5/csv_to_tex.py
Normal file
327
lab5/csv_to_tex.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
Скрипт для конвертации результатов экспериментов из CSV в LaTeX таблицы для lab5.
|
||||
|
||||
Адаптирован из lab2/csv_to_tex.py для работы с форматом эволюционных стратегий.
|
||||
Формат входных данных: "время±стд (поколения±стд) фитнес"
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Настройка цвета для выделения лучших результатов
|
||||
# None - только жирным, строка (например "magenta") - жирным и цветом
|
||||
HIGHLIGHT_COLOR = "magenta"
|
||||
|
||||
|
||||
def parse_csv_file(csv_path: str) -> tuple[str, list[list[str]]]:
|
||||
"""
|
||||
Парсит CSV файл с результатами эксперимента.
|
||||
|
||||
Args:
|
||||
csv_path: Путь к CSV файлу
|
||||
|
||||
Returns:
|
||||
Tuple с заголовком и данными таблицы
|
||||
"""
|
||||
with open(csv_path, "r", encoding="utf-8") as file:
|
||||
lines = file.readlines()
|
||||
|
||||
# Удаляем пустые строки и берём только строки с данными
|
||||
clean_lines = [line.strip() for line in lines if line.strip()]
|
||||
|
||||
# Первая строка - заголовки
|
||||
header = clean_lines[0]
|
||||
|
||||
# Остальные строки - данные
|
||||
data_lines = clean_lines[1:]
|
||||
|
||||
# Парсим данные
|
||||
data_rows = []
|
||||
for line in data_lines:
|
||||
parts = line.split(",")
|
||||
if len(parts) >= 2: # mu + хотя бы одно значение
|
||||
data_rows.append(parts)
|
||||
|
||||
return header, data_rows
|
||||
|
||||
|
||||
def extract_time_value(value: str) -> float | None:
|
||||
"""
|
||||
Извлекает значение времени из строки формата "X.Y±Z.W (...)".
|
||||
|
||||
Args:
|
||||
value: Строка с результатом
|
||||
|
||||
Returns:
|
||||
Время выполнения как float или None если значение пустое
|
||||
"""
|
||||
value = value.strip()
|
||||
if value == "—" or value == "" or value == "–":
|
||||
return None
|
||||
|
||||
# Ищем паттерн "число.число±число"
|
||||
match = re.match(r"(\d+\.?\d*)±", value)
|
||||
if match:
|
||||
return float(match.group(1))
|
||||
|
||||
# Если нет ±, пробуем просто число перед скобкой
|
||||
match = re.match(r"(\d+\.?\d*)\s*\(", value)
|
||||
if match:
|
||||
return float(match.group(1))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_generations_value(value: str) -> float | None:
|
||||
"""
|
||||
Извлекает среднее число поколений из строки формата "... (X±Y) ...".
|
||||
|
||||
Args:
|
||||
value: Строка с результатом
|
||||
|
||||
Returns:
|
||||
Среднее число поколений как float или None если значение пустое
|
||||
"""
|
||||
value = value.strip()
|
||||
if value == "—" or value == "" or value == "–":
|
||||
return None
|
||||
|
||||
# Ищем паттерн "(число±число)" и берём первое число
|
||||
match = re.search(r"\((\d+\.?\d*)±", value)
|
||||
if match:
|
||||
return float(match.group(1))
|
||||
|
||||
# Если нет ±, пробуем просто число в скобках
|
||||
match = re.search(r"\((\d+\.?\d*)\)", value)
|
||||
if match:
|
||||
return float(match.group(1))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_best_time(data_rows: list[list[str]]) -> float | None:
|
||||
"""
|
||||
Находит минимальное время выполнения среди всех значений в таблице.
|
||||
|
||||
Args:
|
||||
data_rows: Строки данных таблицы
|
||||
|
||||
Returns:
|
||||
Минимальное время или None если нет валидных значений
|
||||
"""
|
||||
min_time = None
|
||||
|
||||
for row in data_rows:
|
||||
for i in range(1, len(row)): # Пропускаем первую колонку (mu)
|
||||
time_value = extract_time_value(row[i])
|
||||
if time_value is not None:
|
||||
if min_time is None or time_value < min_time:
|
||||
min_time = time_value
|
||||
|
||||
return min_time
|
||||
|
||||
|
||||
def find_best_generations(data_rows: list[list[str]]) -> float | None:
|
||||
"""
|
||||
Находит минимальное число поколений среди всех значений в таблице.
|
||||
|
||||
Args:
|
||||
data_rows: Строки данных таблицы
|
||||
|
||||
Returns:
|
||||
Минимальное число поколений или None если нет валидных значений
|
||||
"""
|
||||
min_gens = None
|
||||
|
||||
for row in data_rows:
|
||||
for i in range(1, len(row)): # Пропускаем первую колонку (mu)
|
||||
gens_value = extract_generations_value(row[i])
|
||||
if gens_value is not None:
|
||||
if min_gens is None or gens_value < min_gens:
|
||||
min_gens = gens_value
|
||||
|
||||
return min_gens
|
||||
|
||||
|
||||
def format_value(
|
||||
value: str, best_time: float | None = None, best_gens: float | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Форматирует значение для LaTeX таблицы, выделяя лучшие результаты жирным.
|
||||
|
||||
Args:
|
||||
value: Строковое значение из CSV
|
||||
best_time: Лучшее время в таблице для сравнения
|
||||
best_gens: Лучшее число поколений для сравнения
|
||||
|
||||
Returns:
|
||||
Отформатированное значение для LaTeX
|
||||
"""
|
||||
value = value.strip()
|
||||
if value == "—" or value == "" or value == "–":
|
||||
return "—"
|
||||
|
||||
# Парсим значение: "время±стд (поколения±стд) фитнес"
|
||||
# Пример: "60.6±47.9 (37±29) 0.0000"
|
||||
pattern = r"(\d+\.?\d*)±(\d+\.?\d*)\s*\((\d+\.?\d*)±(\d+\.?\d*)\)\s+(\d+\.?\d+)"
|
||||
match = re.match(pattern, value)
|
||||
|
||||
if not match:
|
||||
# Если не удалось распарсить, возвращаем как есть
|
||||
return value
|
||||
|
||||
time_avg = float(match.group(1))
|
||||
time_std = float(match.group(2))
|
||||
gens_avg = float(match.group(3))
|
||||
gens_std = float(match.group(4))
|
||||
fitness = match.group(5)
|
||||
|
||||
# Формируем части БЕЗ стандартных отклонений
|
||||
time_part = f"{time_avg:.1f}"
|
||||
gens_part = f"{gens_avg:.0f}"
|
||||
|
||||
# Проверяем, является ли время лучшим
|
||||
is_best_time = best_time is not None and abs(time_avg - best_time) < 0.1
|
||||
is_best_gens = best_gens is not None and abs(gens_avg - best_gens) < 0.1
|
||||
|
||||
# Выделяем лучшее время
|
||||
if is_best_time:
|
||||
if HIGHLIGHT_COLOR is not None:
|
||||
time_part = f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{time_part}}}}}"
|
||||
else:
|
||||
time_part = f"\\textbf{{{time_part}}}"
|
||||
|
||||
# Выделяем лучшее число поколений
|
||||
if is_best_gens:
|
||||
if HIGHLIGHT_COLOR is not None:
|
||||
gens_part = f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{gens_part}}}}}"
|
||||
else:
|
||||
gens_part = f"\\textbf{{{gens_part}}}"
|
||||
|
||||
# Не показываем фитнес в таблице, т.к. он всегда близок к нулю
|
||||
return f"{time_part} ({gens_part})"
|
||||
|
||||
|
||||
def generate_latex_table(dimension: str, header: str, data_rows: list[list[str]]) -> str:
|
||||
"""
|
||||
Генерирует LaTeX код таблицы.
|
||||
|
||||
Args:
|
||||
dimension: Размерность задачи (2 или 3)
|
||||
header: Заголовок таблицы
|
||||
data_rows: Строки данных
|
||||
|
||||
Returns:
|
||||
LaTeX код таблицы
|
||||
"""
|
||||
# Находим лучшее время и лучшее число поколений в таблице
|
||||
best_time = find_best_time(data_rows)
|
||||
best_gens = find_best_generations(data_rows)
|
||||
|
||||
# Извлекаем заголовки колонок из header
|
||||
header_parts = header.split(",")
|
||||
p_mut_values = header_parts[1:] # Пропускаем "mu \ p_mut"
|
||||
|
||||
num_cols = len(p_mut_values)
|
||||
|
||||
latex_code = f""" \\begin{{table}}[h!]
|
||||
\\centering
|
||||
\\small
|
||||
\\caption{{Результаты для $n = {dimension}$. Формат: время в мс (число поколений)}}
|
||||
\\begin{{tabularx}}{{{0.95 if num_cols <= 5 else 1.0}\\linewidth}}{{l *{{{num_cols}}}{{Y}}}}
|
||||
\\toprule
|
||||
$\\mathbf{{\\mu \\;\\backslash\\; p_{{mut}}}}$"""
|
||||
|
||||
# Добавляем заголовки p_mut
|
||||
for p_mut in p_mut_values:
|
||||
latex_code += f" & \\textbf{{{p_mut.strip()}}}"
|
||||
|
||||
latex_code += " \\\\\n \\midrule\n"
|
||||
|
||||
# Добавляем строки данных
|
||||
for row in data_rows:
|
||||
mu_value = row[0].strip()
|
||||
latex_code += f" \\textbf{{{mu_value}}}"
|
||||
|
||||
# Добавляем значения для каждого p_mut
|
||||
for i in range(1, len(row)):
|
||||
value = format_value(row[i], best_time, best_gens)
|
||||
latex_code += f" & {value}"
|
||||
|
||||
# Заполняем недостающие колонки если их меньше чем в заголовке
|
||||
for i in range(len(row) - 1, num_cols):
|
||||
latex_code += " & —"
|
||||
|
||||
latex_code += " \\\\\n"
|
||||
|
||||
latex_code += f""" \\bottomrule
|
||||
\\end{{tabularx}}
|
||||
\\label{{tab:es_results_{dimension}}}
|
||||
\\end{{table}}"""
|
||||
|
||||
return latex_code
|
||||
|
||||
|
||||
def main():
|
||||
"""Основная функция скрипта."""
|
||||
experiments_path = Path("lab5_experiments")
|
||||
|
||||
if not experiments_path.exists():
|
||||
print("Папка lab5_experiments не найдена!")
|
||||
return
|
||||
|
||||
tables = []
|
||||
|
||||
# Обрабатываем файлы dimension_2.csv и dimension_3.csv
|
||||
for dimension in [2, 3]:
|
||||
csv_file = experiments_path / f"dimension_{dimension}.csv"
|
||||
|
||||
if csv_file.exists():
|
||||
print(f"Обрабатываем {csv_file}...")
|
||||
|
||||
try:
|
||||
header, data_rows = parse_csv_file(str(csv_file))
|
||||
best_time = find_best_time(data_rows)
|
||||
best_gens = find_best_generations(data_rows)
|
||||
latex_table = generate_latex_table(str(dimension), header, data_rows)
|
||||
tables.append(latex_table)
|
||||
print(
|
||||
f"[OK] Таблица для n={dimension} готова (лучшее время: {best_time:.1f} мс, лучшее число поколений: {best_gens:.0f})"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Ошибка при обработке {csv_file}: {e}")
|
||||
else:
|
||||
print(f"[ERROR] Файл {csv_file} не найден")
|
||||
|
||||
# Сохраняем все таблицы в файл
|
||||
if tables:
|
||||
output_file = experiments_path / "tables.tex"
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write("% Автоматически сгенерированные LaTeX таблицы\n")
|
||||
f.write(
|
||||
"% Лучший результат по времени и по числу поколений выделены жирным отдельно\n"
|
||||
)
|
||||
f.write("% Убедитесь, что подключен \\usepackage{tabularx}\n")
|
||||
if HIGHLIGHT_COLOR is not None:
|
||||
f.write(
|
||||
"% ВНИМАНИЕ: Убедитесь, что подключен \\usepackage{xcolor} для цветового выделения\n"
|
||||
)
|
||||
f.write(
|
||||
"% Используйте \\newcolumntype{Y}{>{\\centering\\arraybackslash}X} перед таблицами\n\n"
|
||||
)
|
||||
|
||||
for i, table in enumerate(tables):
|
||||
if i > 0:
|
||||
f.write("\n \n")
|
||||
f.write(table + "\n")
|
||||
|
||||
print(f"\n[OK] Все таблицы сохранены в файл '{output_file}'")
|
||||
print(f"Сгенерировано таблиц: {len(tables)}")
|
||||
else:
|
||||
print("Не найдено данных для генерации таблиц!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
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
704
lab5/report/report.tex
Normal file
704
lab5/report/report.tex
Normal file
@@ -0,0 +1,704 @@
|
||||
\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{Лабораторная работа №5}\\
|
||||
\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{Теоретические сведения}
|
||||
|
||||
\subsection{Общие сведения}
|
||||
|
||||
Эволюционные стратегии (ЭС), также как и генетические алгоритмы, основаны на эволюции популяции потенциальных решений, но, в отличие от них, здесь используются генетические операторы на уровне фенотипа, а не генотипа. Разница в том, что ГА работают в пространстве генотипа --- кодов решений, в то время как ЭС производят поиск в пространстве фенотипа --- векторном пространстве вещественных чисел.
|
||||
|
||||
В ЭС учитываются свойства хромосомы <<в целом>>, в отличие от ГА, где при поиске решений исследуются отдельные гены. В природе один ген может одновременно влиять на несколько свойств организма. С другой стороны, одно свойство особи может определяться несколькими генами. Естественная эволюция основана на исследовании совокупности генов, а не отдельного (изолированного) гена.
|
||||
|
||||
В эволюционных стратегиях целью является движение особей популяции по направлению к лучшей области ландшафта фитнесс-функции. ЭС изначально разработаны для решения многомерных оптимизационных задач, где пространство поиска --- многомерное пространство вещественных чисел.
|
||||
|
||||
Ранние эволюционные стратегии основывались на популяции, состоящей из одной особи, и в них использовался только один генетический оператор --- мутация. Здесь для представления особи (потенциального решения) была использована идея, которая заключается в следующем.
|
||||
|
||||
Особь представляется парой действительных векторов:
|
||||
$$v = (\mathbf{x}, \boldsymbol{\sigma}),$$
|
||||
где $\mathbf{x}$ --- точка в пространстве решений и $\boldsymbol{\sigma}$ --- вектор стандартных отклонений (вариабельность) от решения. В общем случае особь популяции определяется вектором потенциального решения и вектором <<стратегических параметров>> эволюции. Обычно это вектор стандартных отклонений (дисперсия), хотя допускаются и другие статистики.
|
||||
|
||||
Единственным генетическим оператором в классической ЭС является оператор мутации, который выполняется путём сложения координат вектора-родителя со случайными числами, подчиняющимися закону нормального распределения, следующим образом:
|
||||
$$\mathbf{x}^{(t+1)} = \mathbf{x}^{(t)} + \mathcal{N}(\mathbf{0}, \boldsymbol{\sigma}),$$
|
||||
где $\mathcal{N}(\mathbf{0}, \boldsymbol{\sigma})$ --- вектор независимых случайных чисел, генерируемых согласно распределению Гаусса с нулевым средним значением и стандартным отклонением $\boldsymbol{\sigma}$. Как видно из приведённой формулы, величина мутации управляется нетрадиционным способом. Иногда эволюционный процесс используется для изменения и самих стратегических параметров $\boldsymbol{\sigma}$, в этом случае величина мутации эволюционирует вместе с искомым потенциальным решением.
|
||||
|
||||
Интуитивно ясно, что увеличение отклонения подобно увеличению шага поиска на поверхности ландшафта. Высокая вариабельность способствует расширению пространства поиска и эффективна при нахождении потенциальных зон (суб)оптимальных решений и соответствует высоким значениям коэффициента мутации. В то же время малые значения вариабельности позволяют сфокусироваться на поиске решения в перспективной области. Стратегические параметры стохастически определяют величину шага поиска: большая вариабельность ведёт к большим шагам.
|
||||
|
||||
\subsection{Двукратная эволюционная (1+1)-стратегия}
|
||||
|
||||
Здесь потомок принимается в качестве нового члена популяции (он заменяет своего родителя), если значение фитнесс-функции (целевой функции) на нём лучше, чем у его родителя и выполняются все ограничения. Иначе (если значение фитнесс-функции на нём хуже, чем у родителя), потомок уничтожается и популяция остаётся неизменной.
|
||||
|
||||
Алгоритм процесса эволюции двукратной (1+1)-эволюционной стратегии можно сформулировать следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Выбрать множество параметров $\mathbf{X}$, необходимых для представления решения данной проблемы, и определить диапазон допустимых изменений каждого параметра: $\{x_1^{min}, x_1^{max}\}, \{x_2^{min}, x_2^{max}\}, \ldots, \{x_P^{min}, x_P^{max}\}$. Установить номер поколения $t=0$; задать стандартное отклонение $\sigma_i$ для каждого параметра, функцию $f$, для которой необходимо найти оптимум, и максимальное число поколений $k$.
|
||||
|
||||
\item Для каждого параметра случайным образом выбрать начальное значение из допустимого диапазона: множество этих значений составляет начальную популяцию (из одной особи) $\mathbf{X}^{(t)} = (x_1, x_2, \ldots, x_P)$.
|
||||
|
||||
\item Вычислить значение оптимизируемой функции $f$ для родительской особи $F_p = f(\mathbf{X}^{(t)})$.
|
||||
|
||||
\item Создать новую особь-потомка: $\mathbf{X}^* = \mathbf{X}^{(t)} + \mathcal{N}(\mathbf{0}, \boldsymbol{\sigma})$.
|
||||
|
||||
\item Вычислить значение $f$ для особи-потомка $F_o = f(\mathbf{X}^*)$.
|
||||
|
||||
\item Сравнить значения функций $f$ для родителя и потомка; если значение потомка $F_o$ лучше, чем у родительской особи, то заменить родителя на потомка $\mathbf{X}^{(t)} = \mathbf{X}^*$, иначе оставить в популяции родителя.
|
||||
|
||||
\item Увеличить номер поколения $t = t + 1$.
|
||||
|
||||
\item Если не достигнуто максимальное число поколений $t < k$, то переход на шаг 4, иначе выдать найденное решение $\mathbf{X}^{(t)}$.
|
||||
\end{enumerate}
|
||||
|
||||
Несмотря на то, что фактически здесь популяция состоит из одной особи, рассмотренная стратегия называется двукратной ЭС. Причина в том, что здесь фактически происходит конкуренция потомка и родителя.
|
||||
|
||||
\subsection{Правило успеха $1/5$}
|
||||
|
||||
Обычно вектор стандартных отклонений $\boldsymbol{\sigma}$ остаётся неизменным в течение всего процесса эволюции. Чтобы оптимизировать скорость сходимости этого процесса, И. Решенберг (основоположник ЭС) предложил правило успеха <<$1/5$>>.
|
||||
|
||||
Смысл его заключается в следующем --- правило применяется после каждых $k$ поколений процесса (где $k$ --- параметр этого метода):
|
||||
$$\sigma^{(t+1)}_i = \begin{cases}
|
||||
c_i \cdot \sigma^{(t)}_i, & \text{если } \varphi(k) > 1/5, \\
|
||||
\sigma^{(t)}_i, & \text{если } \varphi(k) = 1/5, \\
|
||||
c_d \cdot \sigma^{(t)}_i, & \text{если } \varphi(k) < 1/5,
|
||||
\end{cases}$$
|
||||
где $\varphi(k)$ --- отношение числа успешных мутаций к общему числу произведённых мутаций $k$ (число успехов, делённое на $k$), которое называется коэффициентом успеха для оператора мутации в течение $k$ последних поколений; величина $c_i > 1$, $c_d < 1$ --- регулирует увеличение/уменьшение отклонения мутации.
|
||||
|
||||
Обычно на практике оптимальные значения полагают равными следующим величинам: $c_d = 0.82$; $c_i = 1/0.82 = 1.22$. Смысл этого правила в следующем:
|
||||
\begin{itemize}
|
||||
\item если коэффициент успеха $\varphi(k) > 1/5$, то отклонение $\sigma^{(t+1)}$ увеличивается (мы идём более крупными шагами);
|
||||
\item если коэффициент успеха $\varphi(k) < 1/5$, то отклонение $\sigma^{(t+1)}$ уменьшается (шаг поиска уменьшается).
|
||||
\end{itemize}
|
||||
|
||||
Таким образом, алгоритм автоматически подстраивает шаг поиска под текущий рельеф функции.
|
||||
|
||||
\subsection{Многократная эволюционная стратегия}
|
||||
|
||||
По сравнению с двукратной многократная эволюция отличается не только размером популяции ($N > 2$), но и имеет некоторые дополнительные отличия:
|
||||
\begin{itemize}
|
||||
\item все особи в поколении имеют одинаковую вероятность выбора для мутации;
|
||||
\item имеется возможность введения оператора рекомбинации, где два случайно выбранных родителя производят потомка по следующей схеме:
|
||||
$$x_i^{\text{потомок}} = x_i^{q_i}, \quad i = 1, \ldots, n,$$
|
||||
где $q_i = 1$ или $q_i = 2$ (т.е. каждая компонента потомка копируется из первого или второго родителя).
|
||||
\end{itemize}
|
||||
|
||||
В современной литературе используются следующие обозначения:
|
||||
\begin{itemize}
|
||||
\item $(1+1)$-ЭС --- двукратная стратегия (1 родитель производит 1 потомка);
|
||||
\item $(\mu+1)$-ЭС --- многократная стратегия ($\mu$ родителей производят 1 потомка);
|
||||
\item $(\mu+\lambda)$-ЭС --- $\mu$ родителей производят $\lambda$ потомков и отбор $\mu$ лучших представителей производится среди объединённого множества ($\mu + \lambda$ особей) родителей и потомков;
|
||||
\item $(\mu, \lambda)$-ЭС --- $\mu$ особей родителей порождает $\lambda$ потомков, причём $\lambda > \mu$ и процесс выбора $\mu$ лучших производится только на множестве потомков.
|
||||
\end{itemize}
|
||||
|
||||
Следует подчеркнуть, что в обоих последних видах ЭС обычно число потомков существенно больше числа родителей $\lambda > \mu$ (иногда полагают $\lambda/\mu = 7$).
|
||||
|
||||
Многочисленные исследования доказывают, что ЭС не менее эффективно, а часто гораздо лучше справляются с задачами оптимизации в многомерных пространствах, при этом более просты в реализации из-за отсутствия процедур кодирования и декодирования хромосом.
|
||||
|
||||
\newpage
|
||||
\section{Особенности реализации}
|
||||
|
||||
\subsection{Структура модулей}
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Модуль \texttt{functions.py}}: содержит реализацию тестовой функции axis parallel hyper-ellipsoid и вспомогательные генераторы диапазонов.
|
||||
\item \textbf{Модуль \texttt{es.py}}: ядро эволюционной стратегии. Определены структуры конфигурации, представление особей и популяции, операторы рекомбинации и мутации.
|
||||
\item \textbf{Модуль \texttt{experiments.py}}: сценарии серийных экспериментов с переборами параметров и сохранением метрик.
|
||||
\item \textbf{Модуль \texttt{main.py}}: точка входа для интерактивных запусков с визуализацией.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Модуль functions.py}
|
||||
|
||||
Модуль содержит реализацию тестовой функции axis parallel hyper-ellipsoid:
|
||||
|
||||
\begin{lstlisting}[language=Python]
|
||||
def axis_parallel_hyperellipsoid(x: Array) -> float:
|
||||
"""Axis-parallel hyper-ellipsoid benchmark function.
|
||||
|
||||
Parameters:
|
||||
x: Point in R^n
|
||||
|
||||
Returns:
|
||||
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)))
|
||||
\end{lstlisting}
|
||||
|
||||
Функция принимает вектор NumPy произвольной размерности и возвращает скалярное значение фитнеса. Для двумерного случая формула принимает вид $f(x_1, x_2) = x_1^2 + 2x_2^2$, для трёхмерного $f(x_1, x_2, x_3) = x_1^2 + 2x_2^2 + 3x_3^2$.
|
||||
|
||||
Также определена вспомогательная функция для генерации симметричных границ:
|
||||
|
||||
\begin{lstlisting}[language=Python]
|
||||
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
|
||||
\end{lstlisting}
|
||||
|
||||
\subsection{Модуль es.py}
|
||||
|
||||
\subsubsection{Структуры данных}
|
||||
|
||||
Особь представлена классом \texttt{Individual}, содержащим координаты решения, стратегические параметры и фитнес:
|
||||
|
||||
\begin{lstlisting}[language=Python]
|
||||
@dataclass
|
||||
class Individual:
|
||||
"""Single individual of the evolution strategy population."""
|
||||
x: Array # Coordinates in solution space
|
||||
sigma: Array # Standard deviations for mutation
|
||||
fitness: float # Fitness value
|
||||
|
||||
def copy(self) -> "Individual":
|
||||
return Individual(self.x.copy(),
|
||||
self.sigma.copy(),
|
||||
float(self.fitness))
|
||||
\end{lstlisting}
|
||||
|
||||
Конфигурация эволюционной стратегии задаётся через \texttt{EvolutionStrategyConfig}:
|
||||
|
||||
\begin{lstlisting}[language=Python]
|
||||
@dataclass
|
||||
class EvolutionStrategyConfig:
|
||||
fitness_func: FitnessFn
|
||||
dimension: int
|
||||
x_min: Array
|
||||
x_max: Array
|
||||
mu: int # Number of parents
|
||||
lambda_: int # Number of offspring
|
||||
mutation_probability: float
|
||||
initial_sigma: Array | float
|
||||
max_generations: int
|
||||
selection: Literal["plus", "comma"] = "comma"
|
||||
recombination: Literal["intermediate", "discrete",
|
||||
"none"] = "intermediate"
|
||||
success_rule_window: int = 10
|
||||
success_rule_target: float = 0.2
|
||||
sigma_increase: float = 1.22
|
||||
sigma_decrease: float = 0.82
|
||||
# ... other parameters
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Рекомбинация}
|
||||
|
||||
Функция \texttt{recombine} реализует выбор родителей и создание базового вектора для потомка:
|
||||
|
||||
\begin{lstlisting}[language=Python]
|
||||
def recombine(parents: Sequence[Individual],
|
||||
config: EvolutionStrategyConfig) -> tuple[Array, Array, float]:
|
||||
"""Recombine parent individuals before mutation.
|
||||
|
||||
Returns:
|
||||
Base vector, sigma and the best parent fitness
|
||||
"""
|
||||
if config.recombination == "none":
|
||||
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)
|
||||
x = np.array([selected[mask[i]].x[i]
|
||||
for i in range(config.dimension)])
|
||||
sigma = np.array([selected[mask[i]].sigma[i]
|
||||
for i in range(config.dimension)])
|
||||
|
||||
parent_fitness = min(p.fitness for p in selected)
|
||||
return x, sigma, parent_fitness
|
||||
\end{lstlisting}
|
||||
|
||||
Промежуточная рекомбинация усредняет координаты родителей, дискретная копирует каждую координату из случайно выбранного родителя.
|
||||
|
||||
\subsubsection{Мутация}
|
||||
|
||||
Оператор мутации использует логнормальное распределение для адаптации стратегических параметров:
|
||||
|
||||
\begin{lstlisting}[language=Python]
|
||||
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)
|
||||
|
||||
# Adapt sigma using log-normal distribution
|
||||
sigma_new = sigma * np.exp(config.tau_prime * global_noise +
|
||||
config.tau * coordinate_noise)
|
||||
sigma_new = np.clip(sigma_new * sigma_scale,
|
||||
config.sigma_min, config.sigma_max)
|
||||
|
||||
# Apply mutation steps
|
||||
steps = np.random.normal(size=config.dimension) * sigma_new
|
||||
|
||||
# Optional per-coordinate mutation probability
|
||||
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
|
||||
\end{lstlisting}
|
||||
|
||||
Параметры $\tau$ и $\tau'$ вычисляются как $\tau = 1/\sqrt{2\sqrt{n}}$ и $\tau' = 1/\sqrt{2n}$, где $n$ --- размерность задачи.
|
||||
|
||||
\subsubsection{Создание потомков}
|
||||
|
||||
Функция \texttt{create\_offspring} генерирует $\lambda$ потомков и отслеживает успешные мутации:
|
||||
|
||||
\begin{lstlisting}[language=Python]
|
||||
def create_offspring(parents: Sequence[Individual],
|
||||
config: EvolutionStrategyConfig,
|
||||
sigma_scale: float) -> tuple[list[Individual],
|
||||
list[bool]]:
|
||||
"""Create offspring and track successful mutations."""
|
||||
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
|
||||
\end{lstlisting}
|
||||
|
||||
\subsubsection{Селекция}
|
||||
|
||||
Отбор следующего поколения производится согласно выбранной стратегии:
|
||||
|
||||
\begin{lstlisting}[language=Python]
|
||||
def select_next_generation(parents: list[Individual],
|
||||
offspring: list[Individual],
|
||||
config: EvolutionStrategyConfig) -> list[Individual]:
|
||||
"""Select next generation according to the strategy."""
|
||||
if config.selection == "plus":
|
||||
pool = parents + offspring # (mu + lambda)-strategy
|
||||
else:
|
||||
pool = offspring # (mu, lambda)-strategy
|
||||
|
||||
pool.sort(key=lambda ind: ind.fitness)
|
||||
next_generation = [ind.copy() for ind in pool[:config.mu]]
|
||||
return next_generation
|
||||
\end{lstlisting}
|
||||
|
||||
\subsection{Главная функция алгоритма}
|
||||
|
||||
Функция \texttt{run\_evolution\_strategy} реализует основной цикл эволюционной стратегии с адаптацией по правилу успеха $1/5$:
|
||||
|
||||
\begin{lstlisting}[language=Python]
|
||||
def run_evolution_strategy(config: EvolutionStrategyConfig) -> EvolutionStrategyResult:
|
||||
"""Main evolution strategy loop with 1/5 success rule."""
|
||||
# Initialize random seed
|
||||
if config.seed is not None:
|
||||
random.seed(config.seed)
|
||||
np.random.seed(config.seed)
|
||||
|
||||
# Initialize population
|
||||
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()
|
||||
|
||||
for generation_number in range(1, config.max_generations + 1):
|
||||
# Create offspring and track successes
|
||||
offspring, successes = create_offspring(parents, config,
|
||||
sigma_scale)
|
||||
success_ratio = sum(successes) / len(successes)
|
||||
success_window.append(success_ratio)
|
||||
|
||||
# Apply 1/5 success rule
|
||||
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()
|
||||
|
||||
# Select next generation
|
||||
parents = select_next_generation(parents, offspring, config)
|
||||
|
||||
# Check stopping criteria
|
||||
# ...
|
||||
|
||||
return EvolutionStrategyResult(...)
|
||||
\end{lstlisting}
|
||||
|
||||
Правило успеха $1/5$ применяется каждые $k$ поколений (по умолчанию $k=5$): если доля успешных мутаций выше $1/5$, масштаб $\sigma$ увеличивается в $1.22$ раза, если ниже --- уменьшается в $0.82$ раза.
|
||||
|
||||
\newpage
|
||||
\section{Результаты работы}
|
||||
|
||||
Для демонстрации работы алгоритма была выполнена визуализация процесса оптимизации двумерной функции ($n=2$) со следующими параметрами:
|
||||
|
||||
\begin{itemize}
|
||||
\item $\mu = 20$ -- размер популяции родителей.
|
||||
\item $\lambda = 80$ -- число потомков ($\lambda = 4\mu$).
|
||||
\item $p_{mut} = 0.7$ -- вероятность мутации каждой координаты.
|
||||
\item Промежуточная рекомбинация двух родителей.
|
||||
\item $(\mu, \lambda)$-селекция: родители полностью заменяются.
|
||||
\item Адаптивное масштабирование шага мутации по правилу успеха $1/5$.
|
||||
\item Начальное стандартное отклонение $\sigma_0 = 0.15 \cdot (x_{max} - x_{min})$.
|
||||
\end{itemize}
|
||||
|
||||
Визуализация воспроизводит поверхность целевой функции и положение популяции на каждом шаге. Пошаговый режим позволяет наблюдать влияние изменения дисперсий: при успешных мутациях облако точек расширяется, при неудачах сжимается вокруг текущего минимума. Популяция постепенно консолидируется вокруг глобального минимума в точке $(0, 0)$.
|
||||
|
||||
|
||||
\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_008.png}
|
||||
\caption{Поколение 8: стабилизация шага мутации}
|
||||
\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_017.png}
|
||||
\caption{Поколение 17: окончательная популяция}
|
||||
\end{figure}
|
||||
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Исследование параметров}
|
||||
|
||||
В рамках лабораторной работы было проведено исследование влияния размера популяции $\mu$ и вероятности мутации $p_{mut}$ на эффективность алгоритма. Для экспериментов использовалась $(\mu, \lambda)$-стратегия с $\lambda = 5\mu$, промежуточной рекомбинацией и адаптивным масштабированием шага мутации по правилу успеха $1/5$.
|
||||
|
||||
\subsection{Проведение измерений}
|
||||
|
||||
Для исследования были выбраны следующие значения параметров:
|
||||
\begin{itemize}
|
||||
\item $\mu = 5, 10, 20, 40$ -- размер популяции родителей.
|
||||
\item $p_{mut} = 0.3, 0.5, 0.7, 0.9, 1.0$ -- вероятность мутации каждой координаты.
|
||||
\item Количество независимых запусков для усреднения результатов: 5.
|
||||
\item Критерий остановки: достижение порога $f(\mathbf{x}) < 10^{-6}$ или исчерпание лимита 300 поколений.
|
||||
\end{itemize}
|
||||
|
||||
Результаты измерений представлены в таблицах~\ref{tab:es_results_2} и~\ref{tab:es_results_3}. В ячейках указано среднее время выполнения в миллисекундах и среднее число поколений до достижения критерия остановки. Лучшие результаты по времени выполнения и по числу поколений выделены жирным цветом.
|
||||
|
||||
\newcolumntype{Y}{>{\centering\arraybackslash}X}
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $n = 2$. Формат: время в мс (число поколений)}
|
||||
\begin{tabularx}{0.95\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{\mu \;\backslash\; p_{mut}}$ & \textbf{0.30} & \textbf{0.50} & \textbf{0.70} & \textbf{0.90} & \textbf{1.00} \\
|
||||
\midrule
|
||||
\textbf{5} & 60.6 (37) & 35.1 (23) & 37.9 (25) & 29.2 (20) & \textcolor{magenta}{\textbf{20.4}} (17) \\
|
||||
\textbf{10} & 69.5 (22) & 84.1 (28) & 61.1 (21) & 48.2 (17) & 38.1 (16) \\
|
||||
\textbf{20} & 109.6 (18) & 120.4 (20) & 107.0 (18) & 100.2 (17) & 69.4 (15) \\
|
||||
\textbf{40} & 239.8 (19) & 225.9 (19) & 199.9 (17) & 180.6 (16) & 121.4 (\textcolor{magenta}{\textbf{13}}) \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:es_results_2}
|
||||
\end{table}
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $n = 3$. Формат: время в мс (число поколений)}
|
||||
\begin{tabularx}{0.95\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{\mu \;\backslash\; p_{mut}}$ & \textbf{0.30} & \textbf{0.50} & \textbf{0.70} & \textbf{0.90} & \textbf{1.00} \\
|
||||
\midrule
|
||||
\textbf{5} & 146.0 (88) & 212.2 (126) & 93.7 (60) & 44.8 (29) & \textcolor{magenta}{\textbf{30.3}} (25) \\
|
||||
\textbf{10} & 155.9 (49) & 149.3 (48) & 88.7 (30) & 69.8 (24) & 55.7 (23) \\
|
||||
\textbf{20} & 235.5 (38) & 199.0 (32) & 157.7 (26) & 125.8 (21) & 105.9 (21) \\
|
||||
\textbf{40} & 670.3 (53) & 374.2 (31) & 311.8 (26) & 258.2 (22) & 194.0 (\textcolor{magenta}{\textbf{20}}) \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:es_results_3}
|
||||
\end{table}
|
||||
|
||||
\subsection{Анализ результатов}
|
||||
|
||||
Анализ экспериментальных данных выявляет следующие закономерности:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Влияние вероятности мутации:} Увеличение $p_{mut}$ от 0.3 до 1.0 последовательно улучшает результаты как по времени, так и по числу поколений. Это объясняется тем, что более частая мутация всех координат ускоряет исследование пространства и адаптацию популяции. Лучшие результаты достигаются при $p_{mut} = 1.0$ (мутация всех координат на каждом шаге).
|
||||
|
||||
\item \textbf{Влияние размера популяции:} При малых $\mu$ (5-10) алгоритм демонстрирует наименьшее время выполнения и умеренное число поколений. С ростом $\mu$ до 40 время увеличивается пропорционально размеру популяции, но число поколений снижается благодаря более широкому охвату пространства поиска. Для двумерной задачи оптимальным является $\mu=5$, $p_{mut}=1.0$ (20.4 мс, 17 поколений).
|
||||
|
||||
\item \textbf{Масштабирование на размерность:} При переходе от $n=2$ к $n=3$ время выполнения изменяется незначительно (30.3 мс против 20.4 мс для лучшей конфигурации), однако требуется больше поколений (25 против 17). Это связано с усложнением ландшафта целевой функции и необходимостью большего числа итераций для достижения порога $10^{-6}$.
|
||||
|
||||
\item \textbf{Эффективность адаптации:} Правило успеха $1/5$ обеспечивает автоматическую подстройку масштаба мутации, что позволяет алгоритму быстро сходиться без ручной настройки начального $\sigma$. Минимальное число поколений (13 и 20 для $n=2$ и $n=3$ соответственно) достигается при больших популяциях ($\mu=40$) и высокой вероятности мутации ($p_{mut}=1.0$).
|
||||
\end{itemize}
|
||||
|
||||
\newpage
|
||||
\section{Ответ на контрольный вопрос}
|
||||
|
||||
\textbf{Вопрос}: Что такое направленная мутация?
|
||||
|
||||
\textbf{Ответ}: Направленная мутация --- это тип мутации, при котором изменения вносятся не случайным образом, а с учётом информации о ландшафте фитнес-функции или направлении улучшения решения. В отличие от обычной (ненаправленной) мутации, которая добавляет случайный шум к параметрам, направленная мутация использует информацию о градиенте функции приспособленности, историю успешных мутаций или другие эвристики, чтобы изменять особь в направлении, с большей вероятностью ведущем к улучшению. Это позволяет ускорить сходимость алгоритма, особенно вблизи оптимума, комбинируя преимущества эволюционного поиска и методов локальной оптимизации.
|
||||
|
||||
\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}
|
||||
3
lab6/README.md
Normal file
3
lab6/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Attention!
|
||||
|
||||
lab6 is fully AI generated slop.
|
||||
160
lab6/aco.py
Normal file
160
lab6/aco.py
Normal file
@@ -0,0 +1,160 @@
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Sequence, Tuple
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
City = Tuple[float, float]
|
||||
Tour = List[int]
|
||||
|
||||
|
||||
def euclidean_distance(c1: City, c2: City) -> float:
|
||||
return math.hypot(c1[0] - c2[0], c1[1] - c2[1])
|
||||
|
||||
|
||||
def build_distance_matrix(cities: Sequence[City]) -> list[list[float]]:
|
||||
size = len(cities)
|
||||
matrix = [[0.0 for _ in range(size)] for _ in range(size)]
|
||||
for i in range(size):
|
||||
for j in range(i + 1, size):
|
||||
dist = euclidean_distance(cities[i], cities[j])
|
||||
matrix[i][j] = matrix[j][i] = dist
|
||||
return matrix
|
||||
|
||||
|
||||
def plot_tour(cities: Sequence[City], tour: Sequence[int], save_path: str) -> None:
|
||||
x = [cities[i][0] for i in tour]
|
||||
y = [cities[i][1] for i in tour]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 7))
|
||||
ax.plot(x + [x[0]], y + [y[0]], "k-", linewidth=1)
|
||||
ax.plot(x, y, "ro", markersize=4)
|
||||
|
||||
ax.axis("equal")
|
||||
fig.tight_layout()
|
||||
fig.savefig(save_path, dpi=220)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def plot_history(best_lengths: Sequence[float], save_path: str) -> None:
|
||||
if not best_lengths:
|
||||
return
|
||||
|
||||
iterations = list(range(len(best_lengths)))
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
ax.plot(iterations, best_lengths, linewidth=2, color="blue")
|
||||
|
||||
ax.set_xlabel("Итерация", fontsize=12)
|
||||
ax.set_ylabel("Длина лучшего тура", fontsize=12)
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ACOConfig:
|
||||
cities: Sequence[City]
|
||||
n_ants: int
|
||||
n_iterations: int
|
||||
alpha: float = 1.0
|
||||
beta: float = 5.0
|
||||
rho: float = 0.5
|
||||
q: float = 1.0
|
||||
seed: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ACOResult:
|
||||
best_tour: Tour
|
||||
best_length: float
|
||||
history: List[float]
|
||||
|
||||
|
||||
class AntColonyOptimizer:
|
||||
def __init__(self, config: ACOConfig):
|
||||
self.config = config
|
||||
if config.seed is not None:
|
||||
random.seed(config.seed)
|
||||
|
||||
self.cities = config.cities
|
||||
self.dist_matrix = build_distance_matrix(config.cities)
|
||||
n = len(config.cities)
|
||||
self.pheromone = [[1.0 if i != j else 0.0 for j in range(n)] for i in range(n)]
|
||||
|
||||
def _choose_next_city(self, current: int, unvisited: set[int]) -> int:
|
||||
candidates = list(unvisited)
|
||||
weights = []
|
||||
for nxt in candidates:
|
||||
tau = self.pheromone[current][nxt] ** self.config.alpha
|
||||
eta = (1.0 / (self.dist_matrix[current][nxt] + 1e-12)) ** self.config.beta
|
||||
weights.append(tau * eta)
|
||||
|
||||
total = sum(weights)
|
||||
probs = [w / total for w in weights]
|
||||
return random.choices(candidates, weights=probs, k=1)[0]
|
||||
|
||||
def _build_tour(self, start: int) -> Tour:
|
||||
n = len(self.cities)
|
||||
tour = [start]
|
||||
unvisited = set(range(n))
|
||||
unvisited.remove(start)
|
||||
|
||||
current = start
|
||||
while unvisited:
|
||||
nxt = self._choose_next_city(current, unvisited)
|
||||
tour.append(nxt)
|
||||
unvisited.remove(nxt)
|
||||
current = nxt
|
||||
|
||||
return tour
|
||||
|
||||
def _tour_length(self, tour: Sequence[int]) -> float:
|
||||
return sum(
|
||||
self.dist_matrix[tour[i]][tour[(i + 1) % len(tour)]]
|
||||
for i in range(len(tour))
|
||||
)
|
||||
|
||||
def run(self) -> ACOResult:
|
||||
best_tour: Tour = []
|
||||
best_length = float("inf")
|
||||
best_history: list[float] = []
|
||||
|
||||
for _ in range(self.config.n_iterations):
|
||||
tours: list[Tour] = []
|
||||
lengths: list[float] = []
|
||||
|
||||
for _ in range(self.config.n_ants):
|
||||
start_city = random.randrange(len(self.cities))
|
||||
tour = self._build_tour(start_city)
|
||||
length = self._tour_length(tour)
|
||||
tours.append(tour)
|
||||
lengths.append(length)
|
||||
|
||||
if length < best_length:
|
||||
best_length = length
|
||||
best_tour = tour
|
||||
|
||||
for i in range(len(self.pheromone)):
|
||||
for j in range(len(self.pheromone)):
|
||||
self.pheromone[i][j] *= 1 - self.config.rho
|
||||
|
||||
for tour, length in zip(tours, lengths):
|
||||
deposit = self.config.q / length
|
||||
for i in range(len(tour)):
|
||||
a, b = tour[i], tour[(i + 1) % len(tour)]
|
||||
self.pheromone[a][b] += deposit
|
||||
self.pheromone[b][a] += deposit
|
||||
|
||||
best_history.append(best_length)
|
||||
|
||||
return ACOResult(
|
||||
best_tour=best_tour, best_length=best_length, history=best_history
|
||||
)
|
||||
|
||||
|
||||
def run_aco(config: ACOConfig) -> ACOResult:
|
||||
optimizer = AntColonyOptimizer(config)
|
||||
return optimizer.run()
|
||||
38
lab6/main.py
Normal file
38
lab6/main.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
|
||||
from aco import ACOConfig, plot_history, plot_tour, run_aco
|
||||
|
||||
# В списке из 89 городов только 38 уникальных
|
||||
cities = set()
|
||||
with open(os.path.join(os.path.dirname(__file__), "../lab3/data.txt"), "r") as file:
|
||||
for line in file:
|
||||
# x и y поменяны местами в визуализациях в методичке
|
||||
_, y, x = line.split()
|
||||
cities.add((float(x), float(y)))
|
||||
cities = list(cities)
|
||||
|
||||
config = ACOConfig(
|
||||
cities=cities,
|
||||
n_ants=50,
|
||||
n_iterations=50,
|
||||
alpha=1.2,
|
||||
beta=5.0,
|
||||
rho=0.5,
|
||||
q=1.0,
|
||||
seed=7,
|
||||
)
|
||||
|
||||
result = run_aco(config)
|
||||
print(f"Лучшая длина: {result.best_length:.2f}")
|
||||
print(f"Лучший тур: {result.best_tour}")
|
||||
|
||||
results_dir = os.path.join(os.path.dirname(__file__), "report", "img")
|
||||
os.makedirs(results_dir, exist_ok=True)
|
||||
|
||||
plot_tour(
|
||||
config.cities, result.best_tour, os.path.join(results_dir, "aco_best_tour.png")
|
||||
)
|
||||
plot_history(result.history, os.path.join(results_dir, "aco_history.png"))
|
||||
|
||||
with open(os.path.join(results_dir, "aco_best_tour.txt"), "w", encoding="utf-8") as f:
|
||||
f.write(" ".join(map(str, result.best_tour)))
|
||||
5
lab6/report/.gitignore
vendored
Normal file
5
lab6/report/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
*
|
||||
!**/
|
||||
!.gitignore
|
||||
!report.tex
|
||||
!img/**/*.png
|
||||
BIN
lab6/report/img/aco_best_tour.png
Normal file
BIN
lab6/report/img/aco_best_tour.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
BIN
lab6/report/img/aco_history.png
Normal file
BIN
lab6/report/img/aco_history.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
lab6/report/img/best_lab3.png
Normal file
BIN
lab6/report/img/best_lab3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
lab6/report/img/optimal_tour.png
Normal file
BIN
lab6/report/img/optimal_tour.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
455
lab6/report/report.tex
Normal file
455
lab6/report/report.tex
Normal file
@@ -0,0 +1,455 @@
|
||||
\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{Лабораторная работа №6}\\
|
||||
\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 Сравнить найденное решение с представленным в условии задачи оптимальным решением и результатами, полученными в лабораторной работе №3.
|
||||
\end{itemize}
|
||||
|
||||
\textbf{Индивидуальное задание вариант 18:}
|
||||
|
||||
\textbf{Дано:} Эвклидовы координаты городов 38 городов в Джибути (см.~Приложение~А). Оптимальный тур представлен на Рис.~\ref{fig:optimal_tour}, его длина равна 6659.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{img/optimal_tour.png}
|
||||
\caption{Оптимальный тур для заданного набора данных}
|
||||
\label{fig:optimal_tour}
|
||||
\end{figure}
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Теоретические сведения}
|
||||
|
||||
\subsection{Общие сведения о муравьиных алгоритмах}
|
||||
|
||||
Муравьиные алгоритмы (МА) относятся к метаэвристическим методам оптимизации и предназначены преимущественно для решения задач комбинаторной оптимизации, в частности задачи поиска оптимальных путей на графах. Основная идея таких алгоритмов основана на моделировании коллективного поведения реальных муравьёв, использующих феромонные следы для обмена информацией.
|
||||
|
||||
Каждый агент, называемый \textit{искусственным муравьём}, поэтапно строит решение задачи, перемещаясь по графу и выбирая следующую вершину на основе вероятностного правила, учитывающего концентрацию феромона на дугах графа. Феромон отражает привлекательность соответствующих маршрутов: чем выше его концентрация на дуге, тем вероятнее выбор этой дуги муравьём.
|
||||
|
||||
\subsection{Простой муравьиный алгоритм (SACO)}
|
||||
|
||||
Для иллюстрации рассмотрим простой муравьиный алгоритм SACO (Simple Ant Colony Optimization). Пусть задан граф
|
||||
\[
|
||||
G = (V, E),
|
||||
\]
|
||||
где $V$ — множество вершин, $E$ — множество рёбер. Каждой дуге $(i,j)$ сопоставлена величина феромона $\tau_{ij}$.
|
||||
|
||||
В начальный момент концентрация феромона обычно принимается нулевой, однако для предотвращения зацикливания каждому ребру присваивается малое случайное начальное значение $\tau_{ij}^{(0)}$.
|
||||
|
||||
Каждый муравей $k=1,\ldots,n_k$ помещается в стартовую вершину и начинает построение пути. Если муравей находится в вершине $i$, он выбирает следующую вершину $j \in N_i^k$ на основе вероятностного правила
|
||||
\[
|
||||
p_{ij}^k(t) = \frac{\tau_{ij}^\alpha(t)}{\sum\limits_{l \in N_i^k} \tau_{il}^\alpha(t)},
|
||||
\]
|
||||
где $\alpha$ — параметр, определяющий степень влияния феромона.
|
||||
|
||||
При отсутствии допустимых переходов допускается возврат в предыдущую вершину, что приводит к появлению петель, которые впоследствии удаляются.
|
||||
|
||||
После завершения построения полного пути $x_k(t)$ выполняется его оценка. Длина пути обозначается как $L_k(t)$ и равна числу пройденных дуг.
|
||||
|
||||
\subsection{Обновление феромона}
|
||||
|
||||
Каждый муравей откладывает феромон на рёбрах своего пути согласно правилу
|
||||
\[
|
||||
\Delta \tau_{ij}^k(t) =
|
||||
\begin{cases}
|
||||
\frac{1}{L_k(t)}, &\text{если дуга } (i,j) \in x_k(t), \\
|
||||
0, &\text{иначе}.
|
||||
\end{cases}
|
||||
\]
|
||||
|
||||
Общее обновление феромона на дуге $(i,j)$:
|
||||
\[
|
||||
\tau_{ij}(t+1) = \tau_{ij}(t) + \sum_{k=1}^{n_k} \Delta\tau_{ij}^k(t).
|
||||
\]
|
||||
|
||||
Чем короче путь, тем больше феромона откладывается на его рёбрах, что повышает вероятность выбора коротких маршрутов в последующих итерациях.
|
||||
|
||||
\subsection{Испарение феромона}
|
||||
|
||||
Чтобы предотвратить преждевременную сходимость алгоритма к локальным минимумам, применяется механизм \textit{искусственного испарения феромона}. На каждом шаге выполняется:
|
||||
\[
|
||||
\tau_{ij}(t) = (1 - \rho)\,\tau_{ij}(t),
|
||||
\]
|
||||
где $\rho \in [0,1]$ — коэффициент испарения. Большие значения $\rho$ усиливают случайность поиска, малые — повышают устойчивость к изменениям.
|
||||
|
||||
\subsection{Критерии остановки алгоритма}
|
||||
|
||||
Муравьиные алгоритмы могут завершаться при выполнении одного из условий:
|
||||
\begin{itemize}
|
||||
\item достигнуто максимальное число итераций;
|
||||
\item найдено решение приемлемого качества $f(x_k(t)) \leq \varepsilon$;
|
||||
\item все муравьи начинают строить одинаковые маршруты, что говорит о стабилизации процесса.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Описание общего алгоритма}
|
||||
|
||||
Алгоритм SACO можно представить в следующем виде:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Инициализация феромона малыми случайными значениями $\tau_{ij}^{(0)}$.
|
||||
\item Размещение всех муравьёв в начальной вершине.
|
||||
\item Для каждой итерации:
|
||||
\begin{enumerate}
|
||||
\item Каждый муравей строит путь согласно вероятностному правилу выбора вершины.
|
||||
\item Выполняется удаление петель.
|
||||
\item Вычисляется длина пути $L_k(t)$.
|
||||
\end{enumerate}
|
||||
\item Выполняется испарение феромона.
|
||||
\item Каждый муравей откладывает феромон на рёбрах своего пути.
|
||||
\item Итерация продолжается до выполнения критерия остановки.
|
||||
\end{enumerate}
|
||||
|
||||
Муравьиные алгоритмы позволяют эффективно находить приближённые решения задач комбинаторной оптимизации, таких как задача коммивояжёра, что и является целью данной лабораторной работы.
|
||||
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Особенности реализации}
|
||||
|
||||
Код решения собран в модуле \texttt{lab6/aco.py}. Реализация использует объектно-ориентированный подход с явной типизацией через современные аннотации типов Python (PEP 604). Ниже приведены ключевые элементы реализации с сигнатурами функций и пояснениями.
|
||||
|
||||
\subsection{Структуры данных конфигурации и результата}
|
||||
Конфигурация алгоритма оформлена через \texttt{@dataclass} и включает все параметры, влияющие на поведение ACO:
|
||||
\begin{lstlisting}[language=Python]
|
||||
@dataclass
|
||||
class ACOConfig:
|
||||
cities: Sequence[City] # список координат городов
|
||||
n_ants: int # число муравьев
|
||||
n_iterations: int # число итераций
|
||||
alpha: float = 1.0 # влияние феромона
|
||||
beta: float = 5.0 # влияние эвристики (1/расстояние)
|
||||
rho: float = 0.5 # коэффициент испарения
|
||||
q: float = 1.0 # константа для отложения феромона
|
||||
seed: int | None = None # зерно ГСЧ (воспроизводимость)
|
||||
\end{lstlisting}
|
||||
|
||||
Результат работы алгоритма представлен структурой:
|
||||
\begin{lstlisting}[language=Python]
|
||||
@dataclass
|
||||
class ACOResult:
|
||||
best_tour: Tour # индексы городов в порядке обхода
|
||||
best_length: float # длина лучшего маршрута
|
||||
history: List[float] # история длин по итерациям
|
||||
\end{lstlisting}
|
||||
|
||||
\subsection{Класс AntColonyOptimizer и инициализация}
|
||||
Основная логика инкапсулирована в классе \texttt{AntColonyOptimizer}, который принимает конфигурацию при создании:
|
||||
\begin{lstlisting}[language=Python]
|
||||
class AntColonyOptimizer:
|
||||
def __init__(self, config: ACOConfig)
|
||||
\end{lstlisting}
|
||||
|
||||
В конструкторе выполняются следующие действия:
|
||||
\begin{itemize}
|
||||
\item инициализация генератора случайных чисел через \texttt{random.seed(config.seed)} для обеспечения воспроизводимости экспериментов;
|
||||
\item вычисление матрицы расстояний между всеми городами с помощью \texttt{build\_distance\_matrix};
|
||||
\item создание матрицы феромона размером $n \times n$, где все недиагональные элементы инициализируются единицами, а диагональные — нулями (для предотвращения самопереходов).
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Построение тура муравьём}
|
||||
Каждый муравей строит полный гамильтонов цикл, начиная со случайно выбранного стартового города. Ключевой метод выбора следующего города:
|
||||
\begin{lstlisting}[language=Python]
|
||||
def _choose_next_city(self, current: int,
|
||||
unvisited: set[int]) -> int
|
||||
\end{lstlisting}
|
||||
|
||||
Метод реализует вероятностный выбор на основе формулы:
|
||||
\[
|
||||
p_{ij} = \frac{[\tau_{ij}]^\alpha \cdot [\eta_{ij}]^\beta}{\sum_{k \in \text{unvisited}} [\tau_{ik}]^\alpha \cdot [\eta_{ik}]^\beta}
|
||||
\]
|
||||
где $\tau_{ij}$ — уровень феромона на ребре $(i,j)$, а $\eta_{ij} = 1/d_{ij}$ — эвристическая привлекательность (обратная величина расстояния). К расстоянию добавляется малая константа $10^{-12}$ для численной стабильности при делении. Финальный выбор осуществляется через \texttt{random.choices} с вычисленными вероятностями.
|
||||
|
||||
Построение полного тура выполняет метод:
|
||||
\begin{lstlisting}[language=Python]
|
||||
def _build_tour(self, start: int) -> Tour
|
||||
\end{lstlisting}
|
||||
Начиная со стартового города, муравей последовательно выбирает следующие непосещённые города до тех пор, пока множество \texttt{unvisited} не станет пустым.
|
||||
|
||||
Вычисление длины построенного тура:
|
||||
\begin{lstlisting}[language=Python]
|
||||
def _tour_length(self, tour: Sequence[int]) -> float
|
||||
\end{lstlisting}
|
||||
Метод суммирует расстояния между последовательными городами в туре, включая замыкающее ребро от последнего города к первому, используя предвычисленную матрицу расстояний.
|
||||
|
||||
\subsection{Основной цикл алгоритма}
|
||||
Главный метод запуска оптимизации:
|
||||
\begin{lstlisting}[language=Python]
|
||||
def run(self) -> ACOResult
|
||||
\end{lstlisting}
|
||||
|
||||
На каждой из \texttt{n\_iterations} итераций выполняются следующие шаги:
|
||||
\begin{enumerate}
|
||||
\item \textbf{Построение туров}: каждый из \texttt{n\_ants} муравьёв создаёт свой маршрут, начиная со случайного города. Вычисляется длина каждого маршрута, и глобально лучший тур обновляется при обнаружении более короткого.
|
||||
\item \textbf{Испарение феромона}: все элементы матрицы феромона умножаются на $(1 - \rho)$, моделируя естественное испарение. Это предотвращает неограниченный рост концентрации феромона и позволяет алгоритму «забывать» плохие решения.
|
||||
\item \textbf{Отложение феромона}: для каждого муравья вычисляется вклад $\Delta\tau = q/L$, где $L$ — длина его маршрута. Этот вклад добавляется симметрично на оба направления каждого ребра в туре. Таким образом, короткие маршруты откладывают больше феромона.
|
||||
\item \textbf{Запись истории}: лучшая на данный момент длина добавляется в список \texttt{history} для последующего анализа сходимости.
|
||||
\end{enumerate}
|
||||
|
||||
По завершении всех итераций метод возвращает \texttt{ACOResult} с лучшим найденным туром, его длиной и историей оптимизации.
|
||||
|
||||
\subsection{Точка входа}
|
||||
Для удобства использования предоставлена функция верхнего уровня:
|
||||
\begin{lstlisting}[language=Python]
|
||||
def run_aco(config: ACOConfig) -> ACOResult
|
||||
\end{lstlisting}
|
||||
Она создаёт экземпляр оптимизатора и запускает алгоритм, возвращая результат.
|
||||
|
||||
\subsection{Визуализация}
|
||||
Модуль включает две функции для визуализации результатов средствами \texttt{matplotlib}:
|
||||
|
||||
Функция построения графика маршрута:
|
||||
\begin{lstlisting}[language=Python]
|
||||
def plot_tour(cities: Sequence[City], tour: Sequence[int],
|
||||
save_path: str) -> None
|
||||
\end{lstlisting}
|
||||
Отображает города в виде точек и соединяет их ломаной линией в порядке обхода, включая возврат к начальной точке. Используется соотношение сторон \texttt{aspect="equal"} для сохранения геометрии, сетка для лучшей читаемости координат. Результат сохраняется в PNG с разрешением 220 DPI.
|
||||
|
||||
Функция построения графика сходимости:
|
||||
\begin{lstlisting}[language=Python]
|
||||
def plot_history(best_lengths: Sequence[float],
|
||||
save_path: str) -> None
|
||||
\end{lstlisting}
|
||||
Строит линейный график изменения длины лучшего найденного тура по итерациям. Позволяет визуально оценить скорость сходимости и стабильность алгоритма.
|
||||
|
||||
\newpage
|
||||
\section{Результаты работы}
|
||||
|
||||
Алгоритм был запущен со следующими параметрами: 50 муравьёв, 50 итераций, $\alpha = 1{,}2$, $\beta = 5$, $\rho = 0{,}5$, $q = 1$. Лучший найденный тур имеет длину $6662{,}35$, что на $0{,}05\%$ отличается от оптимального значения 6659.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\begin{minipage}{0.48\linewidth}
|
||||
\centering
|
||||
\includegraphics[width=0.95\linewidth]{img/optimal_tour.png}
|
||||
\caption{Оптимальный маршрут длиной 6659}
|
||||
\label{fig:optimal_result}
|
||||
\end{minipage}\hfill
|
||||
\begin{minipage}{0.48\linewidth}
|
||||
\centering
|
||||
\includegraphics[width=0.95\linewidth]{img/aco_best_tour.png}
|
||||
\caption{Лучший маршрут, найденный муравьиным алгоритмом (6662{,}35)}
|
||||
\label{fig:aco_tour}
|
||||
\end{minipage}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.9\linewidth]{img/aco_history.png}
|
||||
\caption{Сходимость длины лучшего тура по итерациям}
|
||||
\label{fig:aco_history}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Сравнение с результатами лабораторной работы~№3}
|
||||
|
||||
Для лабораторной работы №3 с генетическим алгоритмом лучший результат составил \textbf{6667{,}03} при популяции $N=500$, вероятностях $P_c=0{,}9$ и $P_m=0{,}5$. Муравьиный алгоритм показал более точное решение: длина тура \textbf{6662{,}35} против оптимального 6659. Разница с оптимумом составила 3{,}35 единицы (0{,}05\%), тогда как в лабораторной работе №3 отклонение было 8{,}03 (0{,}12\%).
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\begin{minipage}{0.48\linewidth}
|
||||
\centering
|
||||
\includegraphics[width=0.95\linewidth]{img/best_lab3.png}
|
||||
\caption{Лучший маршрут из лабораторной работы №3 (ГА): длина 6667{,}03}
|
||||
\label{fig:lab3_best}
|
||||
\end{minipage}\hfill
|
||||
\begin{minipage}{0.48\linewidth}
|
||||
\centering
|
||||
\includegraphics[width=0.95\linewidth]{img/aco_best_tour.png}
|
||||
\caption{Лучший маршрут лабораторной работы №6 (МА): длина 6662{,}35}
|
||||
\label{fig:lab6_best}
|
||||
\end{minipage}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.43\linewidth]{img/optimal_tour.png}
|
||||
\caption{Оптимальный маршрут длиной 6659}
|
||||
\label{fig:optimal_comparison}
|
||||
\end{figure}
|
||||
|
||||
\newpage
|
||||
\section{Ответ на контрольный вопрос}
|
||||
|
||||
\textbf{Вопрос}: Какие критерии окончания могут быть использованы в простом МА?
|
||||
|
||||
\textbf{Ответ}: В простом муравьином алгоритме могут использоваться следующие критерии завершения работы:
|
||||
|
||||
\begin{itemize}
|
||||
\item окончание при превышении заданного числа итераций;
|
||||
\item окончание по достижению приемлемого решения;
|
||||
\item окончание в случае, когда все муравьи начинают следовать одним и тем же путём.
|
||||
\end{itemize}
|
||||
|
||||
\newpage
|
||||
\section*{Заключение}
|
||||
\addcontentsline{toc}{section}{Заключение}
|
||||
|
||||
В ходе шестой лабораторной работы выполнена реализация простого муравьиного алгоритма для задачи коммивояжёра:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Разработан модуль \texttt{aco.py} с конфигурацией алгоритма, построением туров, обновлением феромона и визуализацией результатов с помощью \texttt{matplotlib}.
|
||||
\item Проведён численный эксперимент на данных из варианта 18 (38 городов Джибути); подобраны параметры $\alpha=1{,}2$, $\beta=5$, $\rho=0{,}5$, 50 муравьёв, 400 итераций.
|
||||
\item Получено приближённое решение длиной 6662{,}35, что всего на 0{,}05\% хуже известного оптимума 6659 и лучше результата, достигнутого генетическим алгоритмом из лабораторной работы №3.
|
||||
\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