Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4fcf6562e | |||
|
|
93ab829cff | ||
|
|
9f591dadda | ||
| 7394e5b9fb | |||
| 1f80f2f7dc | |||
| b6c19c5240 | |||
| cf9fc98376 | |||
| 6400996fcf | |||
| 957de42e16 | |||
|
|
f213bc3fb5 | ||
| ca1095671e | |||
| 7ec38a3385 | |||
| 4b2398ae05 | |||
| bacfa20061 | |||
| 74e02df205 | |||
| cfae423f11 | |||
| cb2b031e9c | |||
| cc180dc700 | |||
| e6765c9254 | |||
| 26bd6da1b4 | |||
| 8e8e0abd0d | |||
| 83be98e923 | |||
| afd7a700ca | |||
| 268c4cf4a1 | |||
| 740a7be984 | |||
| 2cf0693070 |
6
.gitignore
vendored
@@ -3,3 +3,9 @@
|
||||
!**/
|
||||
!*.gitignore
|
||||
!*.py
|
||||
!.gitkeep
|
||||
!lab3/data.txt
|
||||
!lab4/*
|
||||
!lab5/report/report.tex
|
||||
!lab5/README.md
|
||||
!lab6/README.md
|
||||
|
||||
@@ -16,7 +16,7 @@ from numpy.typing import NDArray
|
||||
type Chromosome = NDArray[np.float64]
|
||||
type Population = list[Chromosome]
|
||||
type Fitnesses = NDArray[np.float64]
|
||||
type FitnessFn = Callable[[Chromosome], Fitnesses]
|
||||
type FitnessFn = Callable[[Chromosome], np.float64]
|
||||
type CrossoverFn = Callable[[Chromosome, Chromosome], tuple[Chromosome, Chromosome]]
|
||||
type MutationFn = Callable[[Chromosome], Chromosome]
|
||||
|
||||
@@ -185,7 +185,7 @@ def plot_fitness_surface(
|
||||
x_max: Chromosome,
|
||||
ax: Axes3D,
|
||||
num_points: int = 100,
|
||||
) -> None:
|
||||
):
|
||||
"""Рисует поверхность функции фитнеса в 3D."""
|
||||
assert (
|
||||
x_min.shape == x_max.shape == (2,)
|
||||
|
||||
@@ -3,7 +3,7 @@ import numpy as np
|
||||
from gen import GARunConfig, genetic_algorithm
|
||||
|
||||
|
||||
def fitness_function(chromosome: np.ndarray) -> np.ndarray:
|
||||
def fitness_function(chromosome: np.ndarray) -> np.float64:
|
||||
return chromosome[0] ** 2 + 2 * chromosome[1] ** 2
|
||||
|
||||
|
||||
|
||||
340
lab3/csv_to_tex.py
Normal file
@@ -0,0 +1,340 @@
|
||||
"""
|
||||
Скрипт для конвертации результатов экспериментов из CSV в LaTeX таблицы.
|
||||
|
||||
Этот скрипт автоматически сканирует папку experiments/, находит все подпапки
|
||||
с файлами results.csv, парсит данные экспериментов и генерирует LaTeX код
|
||||
таблиц в формате, готовом для вставки в отчёт.
|
||||
|
||||
Структура входных данных:
|
||||
- experiments/N/results.csv, где N - размер популяции
|
||||
- CSV содержит результаты экспериментов с различными параметрами Pc и Pm
|
||||
- Значения в формате "X.Y (Z)" где X.Y - время выполнения, Z - количество итераций
|
||||
- "—" для отсутствующих данных
|
||||
|
||||
Выходной файл: tables.tex с готовым LaTeX кодом всех таблиц.
|
||||
Лучшие результаты по времени и фитнесу выделяются жирным (и цветом, если задан HIGHLIGHT_COLOR).
|
||||
"""
|
||||
|
||||
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: # Pc + как минимум одно значение Pm
|
||||
data_rows.append(parts)
|
||||
|
||||
return header, data_rows
|
||||
|
||||
|
||||
def extract_time_value(value: str) -> float | None:
|
||||
"""
|
||||
Извлекает значение времени из строки формата "X.Y (Z)" или "X.Y (Z) W.V".
|
||||
|
||||
Args:
|
||||
value: Строка с результатом
|
||||
|
||||
Returns:
|
||||
Время выполнения как float или None если значение пустое
|
||||
"""
|
||||
value = value.strip()
|
||||
if value == "—" or value == "" or value == "–":
|
||||
return None
|
||||
|
||||
# Ищем паттерн "число.число (число)"
|
||||
match = re.match(r"(\d+\.?\d*)\s*\(", value)
|
||||
if match:
|
||||
return float(match.group(1))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_fitness_value(value: str) -> float | None:
|
||||
"""
|
||||
Извлекает значение фитнеса из строки формата "X.Y (Z) W.V".
|
||||
|
||||
Args:
|
||||
value: Строка с результатом
|
||||
|
||||
Returns:
|
||||
Значение фитнеса как float или None если значение пустое
|
||||
"""
|
||||
value = value.strip()
|
||||
if value == "—" or value == "" or value == "–":
|
||||
return None
|
||||
|
||||
# Ищем паттерн "число.число (число) число.число"
|
||||
# Фитнес - это последнее число в строке
|
||||
match = re.search(r"\)\s+(\d+\.?\d*)\s*$", 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)): # Пропускаем первую колонку (Pc)
|
||||
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_fitness(data_rows: list[list[str]]) -> float | None:
|
||||
"""
|
||||
Находит минимальное значение фитнеса среди всех значений в таблице.
|
||||
|
||||
Args:
|
||||
data_rows: Строки данных таблицы
|
||||
|
||||
Returns:
|
||||
Минимальное значение фитнеса или None если нет валидных значений
|
||||
"""
|
||||
min_fitness = None
|
||||
|
||||
for row in data_rows:
|
||||
for i in range(1, len(row)): # Пропускаем первую колонку (Pc)
|
||||
fitness_value = extract_fitness_value(row[i])
|
||||
if fitness_value is not None:
|
||||
if min_fitness is None or fitness_value < min_fitness:
|
||||
min_fitness = fitness_value
|
||||
|
||||
return min_fitness
|
||||
|
||||
|
||||
def format_value(
|
||||
value: str, best_time: float | None = None, best_fitness: float | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Форматирует значение для LaTeX таблицы, выделяя лучшие результаты жирным.
|
||||
|
||||
Args:
|
||||
value: Строковое значение из CSV
|
||||
best_time: Лучшее время в таблице для сравнения
|
||||
best_fitness: Лучший фитнес в таблице для сравнения
|
||||
|
||||
Returns:
|
||||
Отформатированное значение для LaTeX
|
||||
"""
|
||||
value = value.strip()
|
||||
if value == "—" or value == "" or value == "–":
|
||||
return "—"
|
||||
|
||||
# Проверяем есть ли фитнес в строке
|
||||
fitness_match = re.search(r"(\d+\.?\d*)\s*\((\d+)\)\s+(\d+\.?\d*)\s*$", value)
|
||||
|
||||
if fitness_match:
|
||||
# Есть фитнес: "время (поколения) фитнес"
|
||||
time_str = fitness_match.group(1)
|
||||
generations_str = fitness_match.group(2)
|
||||
fitness_str = fitness_match.group(3)
|
||||
|
||||
current_time = float(time_str)
|
||||
current_fitness = float(fitness_str)
|
||||
|
||||
# Проверяем, является ли время лучшим
|
||||
time_part = f"{time_str} ({generations_str})"
|
||||
if best_time is not None and abs(current_time - best_time) < 0.001:
|
||||
if HIGHLIGHT_COLOR is not None:
|
||||
time_part = (
|
||||
f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{time_part}}}}}"
|
||||
)
|
||||
else:
|
||||
time_part = f"\\textbf{{{time_part}}}"
|
||||
|
||||
# Проверяем, является ли фитнес лучшим
|
||||
fitness_part = fitness_str
|
||||
if best_fitness is not None and abs(current_fitness - best_fitness) < 0.00001:
|
||||
if HIGHLIGHT_COLOR is not None:
|
||||
fitness_part = (
|
||||
f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{fitness_part}}}}}"
|
||||
)
|
||||
else:
|
||||
fitness_part = f"\\textbf{{{fitness_part}}}"
|
||||
|
||||
return f"{time_part} {fitness_part}"
|
||||
|
||||
else:
|
||||
# Нет фитнеса: только "время (поколения)"
|
||||
time_match = re.match(r"(\d+\.?\d*)\s*\((\d+)\)", value)
|
||||
if time_match:
|
||||
current_time = float(time_match.group(1))
|
||||
if best_time is not None and abs(current_time - best_time) < 0.001:
|
||||
if HIGHLIGHT_COLOR is not None:
|
||||
return f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{value}}}}}"
|
||||
else:
|
||||
return f"\\textbf{{{value}}}"
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def generate_latex_table(n: str, header: str, data_rows: list[list[str]]) -> str:
|
||||
"""
|
||||
Генерирует LaTeX код таблицы.
|
||||
|
||||
Args:
|
||||
n: Размер популяции
|
||||
header: Заголовок таблицы
|
||||
data_rows: Строки данных
|
||||
|
||||
Returns:
|
||||
LaTeX код таблицы
|
||||
"""
|
||||
# Находим лучшее время и лучший фитнес в таблице
|
||||
best_time = find_best_time(data_rows)
|
||||
best_fitness = find_best_fitness(data_rows)
|
||||
|
||||
# Извлекаем заголовки колонок из header
|
||||
header_parts = header.split(",")
|
||||
pm_values = header_parts[1:] # Пропускаем "Pc \ Pm"
|
||||
num_pm_columns = len(pm_values) # Динамически определяем количество колонок
|
||||
|
||||
latex_code = f""" \\begin{{table}}[h!]
|
||||
\\centering
|
||||
\\small
|
||||
\\caption{{Результаты для $N = {n}$}}
|
||||
\\begin{{tabularx}}{{\\linewidth}}{{l *{{{num_pm_columns}}}{{Y}}}}
|
||||
\\toprule
|
||||
$\\mathbf{{P_c \\;\\backslash\\; P_m}}$"""
|
||||
|
||||
# Добавляем заголовки Pm
|
||||
for pm in pm_values:
|
||||
latex_code += f" & \\textbf{{{pm.strip()}}}"
|
||||
|
||||
latex_code += " \\\\\n \\midrule\n"
|
||||
|
||||
# Добавляем строки данных
|
||||
for row in data_rows:
|
||||
pc_value = row[0].strip()
|
||||
latex_code += f" \\textbf{{{pc_value}}}"
|
||||
|
||||
# Добавляем значения для каждого Pm
|
||||
for i in range(1, min(num_pm_columns + 1, len(row))):
|
||||
value = format_value(row[i], best_time, best_fitness)
|
||||
latex_code += f" & {value}"
|
||||
|
||||
# Заполняем недостающие колонки если их меньше чем num_pm_columns
|
||||
for i in range(len(row) - 1, num_pm_columns):
|
||||
latex_code += " & —"
|
||||
|
||||
latex_code += " \\\\\n"
|
||||
|
||||
latex_code += f""" \\bottomrule
|
||||
\\end{{tabularx}}
|
||||
\\label{{tab:pc_pm_results_{n}}}
|
||||
\\end{{table}}"""
|
||||
|
||||
return latex_code
|
||||
|
||||
|
||||
def main():
|
||||
"""Основная функция скрипта."""
|
||||
experiments_path = Path("experiments")
|
||||
|
||||
if not experiments_path.exists():
|
||||
print("Папка experiments не найдена!")
|
||||
return
|
||||
|
||||
tables = []
|
||||
|
||||
# Сканируем все подпапки в experiments, сортируем по числовому значению N
|
||||
subdirs = [
|
||||
subdir
|
||||
for subdir in experiments_path.iterdir()
|
||||
if subdir.is_dir() and subdir.name.isdigit()
|
||||
]
|
||||
subdirs.sort(key=lambda x: int(x.name))
|
||||
|
||||
for subdir in subdirs:
|
||||
n = subdir.name
|
||||
csv_file = subdir / "results.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_fitness = find_best_fitness(data_rows)
|
||||
latex_table = generate_latex_table(n, header, data_rows)
|
||||
tables.append(latex_table)
|
||||
print(
|
||||
f"✓ Таблица для N={n} готова (лучшее время: {best_time}, лучший фитнес: {best_fitness})"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Ошибка при обработке {csv_file}: {e}")
|
||||
else:
|
||||
print(f"✗ Файл {csv_file} не найден")
|
||||
|
||||
# Сохраняем все таблицы в файл
|
||||
if tables:
|
||||
with open("tables.tex", "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✓ Все таблицы сохранены в файл 'tables.tex'")
|
||||
print(f"Сгенерировано таблиц: {len(tables)}")
|
||||
else:
|
||||
print("Не найдено данных для генерации таблиц!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
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
|
||||
208
lab3/expirements.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
import statistics
|
||||
|
||||
import numpy as np
|
||||
from gen import (
|
||||
Chromosome,
|
||||
GARunConfig,
|
||||
genetic_algorithm,
|
||||
initialize_random_population,
|
||||
inversion_mutation_fn,
|
||||
partially_mapped_crossover_fn,
|
||||
)
|
||||
from prettytable import PrettyTable
|
||||
|
||||
# В списке из 89 городов только 38 уникальных
|
||||
cities = set()
|
||||
with open("data.txt", "r") as file:
|
||||
for line in file:
|
||||
# x и y поменяны местами в визуализациях в методичке
|
||||
_, y, x = line.split()
|
||||
cities.add((float(x), float(y)))
|
||||
cities = list(cities)
|
||||
|
||||
|
||||
def euclidean_distance(city1, city2):
|
||||
return math.sqrt((city1[0] - city2[0]) ** 2 + (city1[1] - city2[1]) ** 2)
|
||||
|
||||
|
||||
def build_fitness_function(cities):
|
||||
def fitness_function(chromosome: Chromosome) -> float:
|
||||
return sum(
|
||||
euclidean_distance(cities[chromosome[i]], cities[chromosome[i + 1]])
|
||||
for i in range(len(chromosome) - 1)
|
||||
) + euclidean_distance(cities[chromosome[0]], cities[chromosome[-1]])
|
||||
|
||||
return fitness_function
|
||||
|
||||
|
||||
# Базовая папка для экспериментов
|
||||
BASE_DIR = "experiments"
|
||||
|
||||
# Параметры для экспериментов
|
||||
POPULATION_SIZES = [10, 25, 50, 100]
|
||||
PC_VALUES = [0.5, 0.6, 0.7, 0.8, 0.9] # вероятности кроссинговера
|
||||
PM_VALUES = [0.2, 0.3, 0.4, 0.5, 0.8] # вероятности мутации
|
||||
SAVE_AVG_BEST_FITNESS = True
|
||||
|
||||
# Количество запусков для усреднения результатов
|
||||
NUM_RUNS = 3
|
||||
|
||||
# Базовые параметры (как в main.py)
|
||||
BASE_CONFIG = {
|
||||
"fitness_func": build_fitness_function(cities),
|
||||
"max_generations": 2500,
|
||||
"elitism": 2,
|
||||
"cities": cities,
|
||||
"initialize_population_fn": initialize_random_population,
|
||||
"crossover_fn": partially_mapped_crossover_fn,
|
||||
"mutation_fn": inversion_mutation_fn,
|
||||
"seed": None, # None для случайности, т. к. всё усредняем
|
||||
"minimize": True,
|
||||
# "fitness_avg_threshold": 0.05, # критерий остановки
|
||||
# "max_best_repetitions": 10,
|
||||
"best_value_threshold": 7000,
|
||||
# при включенном сохранении графиков на время смотреть бессмысленно
|
||||
# "save_generations": [1, 50, 199],
|
||||
}
|
||||
|
||||
|
||||
def run_single_experiment(
|
||||
pop_size: int, pc: float, pm: float
|
||||
) -> tuple[float, float, float, float, float, float]:
|
||||
"""
|
||||
Запускает несколько экспериментов с заданными параметрами и усредняет результаты.
|
||||
Возвращает (среднее_время_в_мс, стд_отклонение_времени, среднее_поколений,
|
||||
стд_отклонение_поколений, среднее_лучшее_значение_фитнеса, стд_отклонение_лучшего_значения_фитнеса).
|
||||
"""
|
||||
times = []
|
||||
generations = []
|
||||
best_fitnesses = []
|
||||
|
||||
for run_num in range(NUM_RUNS):
|
||||
config = GARunConfig(
|
||||
**BASE_CONFIG,
|
||||
pop_size=pop_size,
|
||||
pc=pc,
|
||||
pm=pm,
|
||||
results_dir=os.path.join(
|
||||
BASE_DIR,
|
||||
str(pop_size),
|
||||
f"pc_{pc:.3f}",
|
||||
f"pm_{pm:.3f}",
|
||||
f"run_{run_num}",
|
||||
),
|
||||
)
|
||||
|
||||
result = genetic_algorithm(config)
|
||||
times.append(result.time_ms)
|
||||
generations.append(result.generations_count)
|
||||
best_fitnesses.append(result.best_generation.best_fitness)
|
||||
|
||||
# Вычисляем средние значения и стандартные отклонения
|
||||
avg_time = statistics.mean(times)
|
||||
std_time = statistics.stdev(times) if len(times) > 1 else 0.0
|
||||
avg_generations = statistics.mean(generations)
|
||||
std_generations = statistics.stdev(generations) if len(generations) > 1 else 0.0
|
||||
avg_best_fitness = statistics.mean(best_fitnesses)
|
||||
std_best_fitness = (
|
||||
statistics.stdev(best_fitnesses) if len(best_fitnesses) > 1 else 0.0
|
||||
)
|
||||
|
||||
return (
|
||||
avg_time,
|
||||
std_time,
|
||||
avg_generations,
|
||||
std_generations,
|
||||
avg_best_fitness,
|
||||
std_best_fitness,
|
||||
)
|
||||
|
||||
|
||||
def run_experiments_for_population(pop_size: int) -> PrettyTable:
|
||||
"""
|
||||
Запускает эксперименты для одного размера популяции.
|
||||
Возвращает таблицу результатов.
|
||||
"""
|
||||
print(f"\nЗапуск экспериментов для популяции размером {pop_size}...")
|
||||
print(f"Количество запусков для усреднения: {NUM_RUNS}")
|
||||
|
||||
# Создаем таблицу
|
||||
table = PrettyTable()
|
||||
table.field_names = ["Pc \\ Pm"] + [f"{pm:.3f}" for pm in PM_VALUES]
|
||||
|
||||
# Запускаем эксперименты для всех комбинаций Pc и Pm
|
||||
for pc in PC_VALUES:
|
||||
row = [f"{pc:.1f}"]
|
||||
for pm in PM_VALUES:
|
||||
print(f" Эксперимент: pop_size={pop_size}, Pc={pc:.1f}, Pm={pm:.3f}")
|
||||
(
|
||||
avg_time,
|
||||
std_time,
|
||||
avg_generations,
|
||||
std_generations,
|
||||
avg_best_fitness,
|
||||
std_best_fitness,
|
||||
) = run_single_experiment(pop_size, pc, pm)
|
||||
|
||||
# Форматируем результат: среднее_время±стд_отклонение (среднее_поколения±стд_отклонение)
|
||||
# cell_value = f"{avg_time:.1f}±{std_time:.1f} ({avg_generations:.1f}±{std_generations:.1f})"
|
||||
cell_value = f"{avg_time:.0f} ({avg_generations:.0f})"
|
||||
|
||||
if SAVE_AVG_BEST_FITNESS:
|
||||
cell_value += f" {avg_best_fitness:.0f}"
|
||||
|
||||
if avg_generations == BASE_CONFIG["max_generations"]:
|
||||
cell_value = "—"
|
||||
|
||||
row.append(cell_value)
|
||||
table.add_row(row)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def main():
|
||||
"""Основная функция для запуска всех экспериментов."""
|
||||
print("=" * 60)
|
||||
print("ЗАПУСК ЭКСПЕРИМЕНТОВ ПО ПАРАМЕТРАМ ГЕНЕТИЧЕСКОГО АЛГОРИТМА")
|
||||
print("=" * 60)
|
||||
print(f"Размеры популяции: {POPULATION_SIZES}")
|
||||
print(f"Значения Pc: {PC_VALUES}")
|
||||
print(f"Значения Pm: {PM_VALUES}")
|
||||
print(f"Количество запусков для усреднения: {NUM_RUNS}")
|
||||
print("=" * 60)
|
||||
|
||||
# Создаем базовую папку
|
||||
if os.path.exists(BASE_DIR):
|
||||
shutil.rmtree(BASE_DIR)
|
||||
os.makedirs(BASE_DIR)
|
||||
|
||||
# Запускаем эксперименты для каждого размера популяции
|
||||
for pop_size in POPULATION_SIZES:
|
||||
table = run_experiments_for_population(pop_size)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"РЕЗУЛЬТАТЫ ДЛЯ ПОПУЛЯЦИИ РАЗМЕРОМ {pop_size}")
|
||||
print(f"{'='*60}")
|
||||
print(
|
||||
f"Формат: среднее_время±стд_отклонение_мс (среднее_поколения±стд_отклонение)"
|
||||
)
|
||||
print(f"Усреднено по {NUM_RUNS} запускам")
|
||||
print(table)
|
||||
|
||||
pop_exp_dir = os.path.join(BASE_DIR, str(pop_size))
|
||||
os.makedirs(pop_exp_dir, exist_ok=True)
|
||||
with open(os.path.join(pop_exp_dir, "results.csv"), "w", encoding="utf-8") as f:
|
||||
f.write(table.get_csv_string())
|
||||
print(f"Результаты сохранены в папке: {pop_exp_dir}")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("ВСЕ ЭКСПЕРИМЕНТЫ ЗАВЕРШЕНЫ!")
|
||||
print(f"Результаты сохранены в {BASE_DIR}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
531
lab3/gen.py
Normal file
@@ -0,0 +1,531 @@
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
import plotly.graph_objects as go
|
||||
from matplotlib import pyplot as plt
|
||||
from matplotlib.axes import Axes
|
||||
from mpl_toolkits.mplot3d import Axes3D
|
||||
from numpy.typing import NDArray
|
||||
|
||||
type Cites = list[tuple[float, float]]
|
||||
type InitializePopulationFn = Callable[[int, Cites], Population]
|
||||
type Chromosome = list[int]
|
||||
type Population = list[Chromosome]
|
||||
type Fitnesses = NDArray[np.float64]
|
||||
type FitnessFn = Callable[[Chromosome], float]
|
||||
type CrossoverFn = Callable[[Chromosome, Chromosome], tuple[Chromosome, Chromosome]]
|
||||
type MutationFn = Callable[[Chromosome], Chromosome]
|
||||
|
||||
|
||||
@dataclass
|
||||
class GARunConfig:
|
||||
fitness_func: FitnessFn
|
||||
cities: Cites
|
||||
initialize_population_fn: InitializePopulationFn
|
||||
crossover_fn: CrossoverFn
|
||||
mutation_fn: MutationFn
|
||||
pop_size: int # размер популяции
|
||||
pc: float # вероятность кроссинговера
|
||||
pm: float # вероятность мутации
|
||||
max_generations: int # максимальное количество поколений
|
||||
elitism: int = (
|
||||
0 # сколько лучших особей перенести без изменения в следующее поколение
|
||||
)
|
||||
max_best_repetitions: int | None = (
|
||||
None # остановка при повторении лучшего результата
|
||||
)
|
||||
seed: int | None = None # seed для генератора случайных чисел
|
||||
minimize: bool = False # если True, ищем минимум вместо максимума
|
||||
save_generations: list[int] | None = (
|
||||
None # индексы поколений для сохранения графиков
|
||||
)
|
||||
results_dir: str = "results" # папка для сохранения графиков
|
||||
fitness_avg_threshold: float | None = (
|
||||
None # порог среднего значения фитнес функции для остановки
|
||||
)
|
||||
best_value_threshold: float | None = (
|
||||
None # остановка при достижении значения фитнеса лучше заданного
|
||||
)
|
||||
log_every_generation: bool = False # логировать каждое поколение
|
||||
|
||||
def save(self, filename: str = "GARunConfig.txt"):
|
||||
"""Сохраняет конфиг в results_dir."""
|
||||
os.makedirs(self.results_dir, exist_ok=True)
|
||||
path = os.path.join(self.results_dir, filename)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
for k, v in asdict(self).items():
|
||||
f.write(f"{k}: {v}\n")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Generation:
|
||||
number: int
|
||||
best: Chromosome
|
||||
best_fitness: float
|
||||
population: Population
|
||||
fitnesses: Fitnesses
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GARunResult:
|
||||
generations_count: int
|
||||
best_generation: Generation
|
||||
history: list[Generation]
|
||||
time_ms: float
|
||||
|
||||
def save(self, path: str, filename: str = "GARunResult.txt"):
|
||||
"""Сохраняет конфиг в results_dir."""
|
||||
os.makedirs(path, exist_ok=True)
|
||||
path = os.path.join(path, filename)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
for k, v in asdict(self).items():
|
||||
if k == "history":
|
||||
continue
|
||||
if k == "best_generation":
|
||||
f.write(
|
||||
f"{k}: Number: {v['number']}, Best Fitness: {v['best_fitness']}, Best: {v['best']}\n"
|
||||
)
|
||||
else:
|
||||
f.write(f"{k}: {v}\n")
|
||||
|
||||
|
||||
def initialize_random_population(pop_size: int, cities: Cites) -> Population:
|
||||
"""Инициализирует популяцию случайными маршрутами без повторений городов."""
|
||||
return [random.sample(range(len(cities)), len(cities)) for _ in range(pop_size)]
|
||||
|
||||
|
||||
def reproduction(population: Population, fitnesses: Fitnesses) -> Population:
|
||||
"""Репродукция (селекция) методом рулетки.
|
||||
|
||||
Чем больше значение фитнеса, тем больше вероятность выбора особи. Для минимизации
|
||||
значения фитнеса нужно предварительно инвертировать.
|
||||
"""
|
||||
# Чтобы работать с отрицательными f, сдвигаем значения фитнес функции на минимальное
|
||||
# значение в популяции. Вычитаем min_fit, т. к. min_fit может быть отрицательным.
|
||||
min_fit = np.min(fitnesses)
|
||||
shifted_fitnesses = fitnesses - min_fit + 1e-12
|
||||
|
||||
# Получаем вероятности для каждой особи
|
||||
probs = shifted_fitnesses / np.sum(shifted_fitnesses)
|
||||
cum = np.cumsum(probs)
|
||||
|
||||
# Выбираем особей методом рулетки
|
||||
selected = []
|
||||
for _ in population:
|
||||
r = np.random.random()
|
||||
idx = int(np.searchsorted(cum, r, side="left"))
|
||||
selected.append(population[idx])
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
def partially_mapped_crossover_fn(
|
||||
p1: Chromosome,
|
||||
p2: Chromosome,
|
||||
cut1: int | None = None,
|
||||
cut2: int | None = None,
|
||||
) -> tuple[Chromosome, Chromosome]:
|
||||
n = len(p1)
|
||||
# если разрезы не заданы — выберем случайные
|
||||
if cut1 is None or cut2 is None:
|
||||
cut1 = random.randint(1, n - 2) # [1, n-2]
|
||||
cut2 = random.randint(cut1 + 1, n - 1) # (cut1, n-1]
|
||||
|
||||
# отображения внутри среднего сегмента
|
||||
mapping12 = {p1[i]: p2[i] for i in range(cut1, cut2)}
|
||||
mapping21 = {p2[i]: p1[i] for i in range(cut1, cut2)}
|
||||
|
||||
# будущие потомки
|
||||
o1 = p2[:cut1] + p1[cut1:cut2] + p2[cut2:]
|
||||
o2 = p1[:cut1] + p2[cut1:cut2] + p1[cut2:]
|
||||
|
||||
# разрешаем конфликты по цепочке
|
||||
def resolve(x: int, mapping: dict[int, int]) -> int:
|
||||
while x in mapping:
|
||||
x = mapping[x]
|
||||
return x
|
||||
|
||||
# исправляем только вне среднего сегмента
|
||||
for i in (*range(0, cut1), *range(cut2, n)):
|
||||
o1[i] = resolve(o1[i], mapping12)
|
||||
o2[i] = resolve(o2[i], mapping21)
|
||||
|
||||
return o1, o2
|
||||
|
||||
|
||||
def ordered_crossover_fn(
|
||||
p1: Chromosome,
|
||||
p2: Chromosome,
|
||||
cut1: int | None = None,
|
||||
cut2: int | None = None,
|
||||
) -> tuple[Chromosome, Chromosome]:
|
||||
n = len(p1)
|
||||
|
||||
# если разрезы не заданы — выберем случайные корректно
|
||||
if cut1 is None or cut2 is None:
|
||||
cut1 = random.randint(1, n - 2) # [1, n-2]
|
||||
cut2 = random.randint(cut1 + 1, n - 1) # [cut1+1, n-1]
|
||||
|
||||
# --- o1: сегмент от p1, остальное — порядок из p2
|
||||
o1: Chromosome = [None] * n # type: ignore
|
||||
o1[cut1:cut2] = p1[cut1:cut2]
|
||||
segment1 = set(p1[cut1:cut2])
|
||||
|
||||
fill_idx = cut2 % n
|
||||
for x in (p2[i % n] for i in range(cut2, cut2 + n)):
|
||||
if x not in segment1:
|
||||
# прокручиваем fill_idx до ближайшей пустой ячейки
|
||||
while o1[fill_idx] is not None:
|
||||
fill_idx = (fill_idx + 1) % n
|
||||
o1[fill_idx] = x
|
||||
fill_idx = (fill_idx + 1) % n
|
||||
|
||||
# --- o2: сегмент от p2, остальное — порядок из p1
|
||||
o2: Chromosome = [None] * n # type: ignore
|
||||
o2[cut1:cut2] = p2[cut1:cut2]
|
||||
segment2 = set(p2[cut1:cut2])
|
||||
|
||||
fill_idx = cut2 % n
|
||||
for x in (p1[i % n] for i in range(cut2, cut2 + n)):
|
||||
if x not in segment2:
|
||||
while o2[fill_idx] is not None:
|
||||
fill_idx = (fill_idx + 1) % n
|
||||
o2[fill_idx] = x
|
||||
fill_idx = (fill_idx + 1) % n
|
||||
|
||||
return o1, o2
|
||||
|
||||
|
||||
def cycle_crossover_fn(p1: Chromosome, p2: Chromosome) -> tuple[Chromosome, Chromosome]:
|
||||
n = len(p1)
|
||||
o1 = [None] * n
|
||||
o2 = [None] * n
|
||||
|
||||
# быстрый поиск позиций элементов p1
|
||||
pos_in_p1 = {val: i for i, val in enumerate(p1)}
|
||||
|
||||
used = [False] * n
|
||||
cycle_index = 0
|
||||
|
||||
for start in range(n):
|
||||
if used[start]:
|
||||
continue
|
||||
# строим цикл индексов
|
||||
idx = start
|
||||
cycle = []
|
||||
while not used[idx]:
|
||||
used[idx] = True
|
||||
cycle.append(idx)
|
||||
# переход: idx -> элемент p2[idx] -> его позиция в p1
|
||||
val = p2[idx]
|
||||
idx = pos_in_p1[val]
|
||||
|
||||
# нечётные циклы: из p1 в o1, из p2 в o2
|
||||
# чётные циклы: наоборот
|
||||
if cycle_index % 2 == 0:
|
||||
for i in cycle:
|
||||
o1[i] = p1[i]
|
||||
o2[i] = p2[i]
|
||||
else:
|
||||
for i in cycle:
|
||||
o1[i] = p2[i]
|
||||
o2[i] = p1[i]
|
||||
cycle_index += 1
|
||||
|
||||
return o1, o2 # type: ignore
|
||||
|
||||
|
||||
def crossover(
|
||||
population: Population,
|
||||
pc: float,
|
||||
crossover_fn: CrossoverFn,
|
||||
) -> Population:
|
||||
"""Оператор кроссинговера (скрещивания) выполняется с заданной вероятностью pc.
|
||||
|
||||
Две хромосомы (родители) выбираются случайно из промежуточной популяции.
|
||||
|
||||
Если популяция нечетного размера, то последняя хромосома скрещивается со случайной
|
||||
другой хромосомой из популяции. В таком случае одна из хромосом может поучаствовать
|
||||
в кроссовере дважды.
|
||||
"""
|
||||
# Создаем копию популяции и перемешиваем её для случайного выбора пар
|
||||
shuffled_population = population.copy()
|
||||
np.random.shuffle(shuffled_population)
|
||||
|
||||
next_population = []
|
||||
pop_size = len(shuffled_population)
|
||||
|
||||
for i in range(0, pop_size, 2):
|
||||
p1 = shuffled_population[i]
|
||||
p2 = shuffled_population[(i + 1) % pop_size]
|
||||
if np.random.random() <= pc:
|
||||
p1, p2 = crossover_fn(p1, p2)
|
||||
next_population.append(p1)
|
||||
next_population.append(p2)
|
||||
|
||||
return next_population[:pop_size]
|
||||
|
||||
|
||||
def swap_mutation_fn(chrom: Chromosome) -> Chromosome:
|
||||
"""Меняем два случайных города в маршруте местами."""
|
||||
chrom = chrom.copy()
|
||||
a, b = random.sample(range(len(chrom)), 2)
|
||||
chrom[a], chrom[b] = chrom[b], chrom[a]
|
||||
return chrom
|
||||
|
||||
|
||||
def inversion_mutation_fn(chrom: Chromosome) -> Chromosome:
|
||||
"""Инвертируем случайный сегмент маршрута."""
|
||||
chrom = chrom.copy()
|
||||
a, b = sorted(random.sample(range(len(chrom)), 2))
|
||||
chrom[a:b] = reversed(chrom[a:b])
|
||||
return chrom
|
||||
|
||||
|
||||
def insertion_mutation_fn(chrom: Chromosome) -> Chromosome:
|
||||
"""Вырезаем случайный город и вставляем его в случайное место маршрута."""
|
||||
chrom = chrom.copy()
|
||||
a, b = random.sample(range(len(chrom)), 2)
|
||||
city = chrom.pop(a)
|
||||
chrom.insert(b, city)
|
||||
return chrom
|
||||
|
||||
|
||||
def mutation(population: Population, pm: float, mutation_fn: MutationFn) -> Population:
|
||||
"""Мутация происходит с вероятностью pm."""
|
||||
next_population = []
|
||||
for chrom in population:
|
||||
next_population.append(
|
||||
mutation_fn(chrom) if np.random.random() <= pm else chrom
|
||||
)
|
||||
return next_population
|
||||
|
||||
|
||||
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 eval_population(population: Population, fitness_func: FitnessFn) -> Fitnesses:
|
||||
return np.array([fitness_func(chrom) for chrom in population])
|
||||
|
||||
|
||||
def plot_tour(cities: list[tuple[float, float]], tour: list[int], ax: Axes):
|
||||
"""Рисует маршрут обхода городов."""
|
||||
|
||||
x = [cities[i][0] for i in tour]
|
||||
y = [cities[i][1] for i in tour]
|
||||
|
||||
ax.plot(x + [x[0]], y + [y[0]], "k-", linewidth=1)
|
||||
ax.plot(x, y, "ro", markersize=4)
|
||||
|
||||
# for i, (cx, cy) in enumerate(cities):
|
||||
# plt.text(cx, cy, str(i), fontsize=7, ha="right", va="bottom")
|
||||
|
||||
ax.axis("equal")
|
||||
|
||||
|
||||
def save_generation(
|
||||
generation: Generation, history: list[Generation], config: GARunConfig
|
||||
) -> None:
|
||||
os.makedirs(config.results_dir, exist_ok=True)
|
||||
|
||||
fig = plt.figure(figsize=(7, 7))
|
||||
fig.suptitle(
|
||||
f"Поколение #{generation.number}. "
|
||||
f"Лучшая особь: {generation.best_fitness:.0f}. "
|
||||
f"Среднее значение: {np.mean(generation.fitnesses):.0f}",
|
||||
fontsize=14,
|
||||
y=0.95,
|
||||
)
|
||||
|
||||
# Рисуем лучший маршрут в поколении
|
||||
ax = fig.add_subplot(1, 1, 1)
|
||||
plot_tour(config.cities, generation.best, ax)
|
||||
|
||||
filename = f"generation_{generation.number:03d}.png"
|
||||
path_png = os.path.join(config.results_dir, filename)
|
||||
fig.savefig(path_png, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
||||
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)
|
||||
|
||||
population = config.initialize_population_fn(config.pop_size, config.cities)
|
||||
|
||||
start = time.perf_counter()
|
||||
history: list[Generation] = []
|
||||
best: Generation | None = None
|
||||
|
||||
generation_number = 1
|
||||
best_repetitions = 0
|
||||
|
||||
while True:
|
||||
# Вычисляем фитнес для всех особей в популяции
|
||||
fitnesses = eval_population(population, config.fitness_func)
|
||||
|
||||
# Сохраняем лучших особей для переноса в следующее поколение
|
||||
elites: list[Chromosome] = []
|
||||
if config.elitism:
|
||||
elites = deepcopy(
|
||||
[
|
||||
population[i]
|
||||
for i in sorted(
|
||||
range(len(fitnesses)),
|
||||
key=lambda i: fitnesses[i],
|
||||
reverse=not config.minimize,
|
||||
)
|
||||
][: config.elitism]
|
||||
)
|
||||
|
||||
# Находим лучшую особь в поколении
|
||||
best_index = (
|
||||
int(np.argmin(fitnesses)) if config.minimize else int(np.argmax(fitnesses))
|
||||
)
|
||||
|
||||
# Добавляем эпоху в историю
|
||||
current = Generation(
|
||||
number=generation_number,
|
||||
best=population[best_index],
|
||||
best_fitness=fitnesses[best_index],
|
||||
population=deepcopy(population),
|
||||
fitnesses=deepcopy(fitnesses),
|
||||
)
|
||||
history.append(current)
|
||||
|
||||
if config.log_every_generation:
|
||||
print(
|
||||
f"Generation #{generation_number} best: {current.best_fitness},"
|
||||
f" avg: {np.mean(current.fitnesses)}"
|
||||
)
|
||||
|
||||
# Обновляем лучшую эпоху
|
||||
if (
|
||||
best is None
|
||||
or (config.minimize and current.best_fitness < best.best_fitness)
|
||||
or (not config.minimize and current.best_fitness > best.best_fitness)
|
||||
):
|
||||
best = current
|
||||
|
||||
# Проверка критериев остановки
|
||||
stop_algorithm = False
|
||||
|
||||
if generation_number >= config.max_generations:
|
||||
stop_algorithm = True
|
||||
|
||||
if config.max_best_repetitions is not None and generation_number > 1:
|
||||
if history[-2].best_fitness == current.best_fitness:
|
||||
best_repetitions += 1
|
||||
|
||||
if best_repetitions == config.max_best_repetitions:
|
||||
stop_algorithm = True
|
||||
else:
|
||||
best_repetitions = 0
|
||||
|
||||
# if config.variance_threshold is not None:
|
||||
# fitness_variance = np.var(fitnesses)
|
||||
# if fitness_variance < config.variance_threshold:
|
||||
# stop_algorithm = True
|
||||
|
||||
if config.best_value_threshold is not None:
|
||||
if (
|
||||
config.minimize and current.best_fitness < config.best_value_threshold
|
||||
) or (
|
||||
not config.minimize
|
||||
and current.best_fitness > config.best_value_threshold
|
||||
):
|
||||
stop_algorithm = True
|
||||
|
||||
if config.fitness_avg_threshold is not None:
|
||||
mean_fitness = np.mean(fitnesses)
|
||||
if (config.minimize and mean_fitness < config.fitness_avg_threshold) or (
|
||||
not config.minimize and mean_fitness > config.fitness_avg_threshold
|
||||
):
|
||||
stop_algorithm = True
|
||||
|
||||
# Сохраняем указанные поколения и последнее поколение
|
||||
if config.save_generations and (
|
||||
stop_algorithm or generation_number in config.save_generations
|
||||
):
|
||||
save_generation(current, history, config)
|
||||
|
||||
if stop_algorithm:
|
||||
break
|
||||
|
||||
# селекция (для минимума инвертируем знак)
|
||||
parents = reproduction(
|
||||
population, fitnesses if not config.minimize else -fitnesses
|
||||
)
|
||||
|
||||
# кроссинговер попарно
|
||||
next_population = crossover(parents, config.pc, config.crossover_fn)
|
||||
|
||||
# мутация
|
||||
next_population = mutation(
|
||||
next_population,
|
||||
config.pm,
|
||||
config.mutation_fn,
|
||||
)
|
||||
|
||||
# Вставляем элиту в новую популяцию
|
||||
population = next_population[: config.pop_size - config.elitism] + elites
|
||||
|
||||
generation_number += 1
|
||||
|
||||
end = time.perf_counter()
|
||||
|
||||
assert best is not None, "Best was never set"
|
||||
return GARunResult(
|
||||
len(history),
|
||||
best,
|
||||
history,
|
||||
(end - start) * 1000.0,
|
||||
)
|
||||
|
||||
|
||||
def plot_fitness_history(result: GARunResult, save_path: str | None = None) -> None:
|
||||
"""Рисует график изменения лучших и средних значений фитнеса по поколениям."""
|
||||
generations = [gen.number for gen in result.history]
|
||||
best_fitnesses = [gen.best_fitness for gen in result.history]
|
||||
avg_fitnesses = [np.mean(gen.fitnesses) for gen in result.history]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
ax.plot(
|
||||
generations, best_fitnesses, label="Лучшее значение", linewidth=2, color="blue"
|
||||
)
|
||||
ax.plot(
|
||||
generations,
|
||||
avg_fitnesses,
|
||||
label="Среднее значение",
|
||||
linewidth=2,
|
||||
color="orange",
|
||||
)
|
||||
|
||||
ax.set_xlabel("Поколение", fontsize=12)
|
||||
ax.set_ylabel("Значение фитнес-функции", fontsize=12)
|
||||
ax.legend(fontsize=11)
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
if save_path:
|
||||
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
||||
print(f"График сохранен в {save_path}")
|
||||
else:
|
||||
plt.show()
|
||||
plt.close(fig)
|
||||
111
lab3/main.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import math
|
||||
import os
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from gen import (
|
||||
Chromosome,
|
||||
GARunConfig,
|
||||
genetic_algorithm,
|
||||
initialize_random_population,
|
||||
inversion_mutation_fn,
|
||||
partially_mapped_crossover_fn,
|
||||
plot_fitness_history,
|
||||
plot_tour,
|
||||
swap_mutation_fn,
|
||||
)
|
||||
|
||||
# В списке из 89 городов только 38 уникальных
|
||||
cities = set()
|
||||
with open("data.txt", "r") as file:
|
||||
for line in file:
|
||||
# x и y поменяны местами в визуализациях в методичке
|
||||
_, y, x = line.split()
|
||||
cities.add((float(x), float(y)))
|
||||
cities = list(cities)
|
||||
|
||||
|
||||
def euclidean_distance(city1, city2):
|
||||
return math.sqrt((city1[0] - city2[0]) ** 2 + (city1[1] - city2[1]) ** 2)
|
||||
|
||||
|
||||
def build_fitness_function(cities):
|
||||
def fitness_function(chromosome: Chromosome) -> float:
|
||||
return sum(
|
||||
euclidean_distance(cities[chromosome[i]], cities[chromosome[i + 1]])
|
||||
for i in range(len(chromosome) - 1)
|
||||
) + euclidean_distance(cities[chromosome[0]], cities[chromosome[-1]])
|
||||
|
||||
return fitness_function
|
||||
|
||||
|
||||
config = GARunConfig(
|
||||
fitness_func=build_fitness_function(cities),
|
||||
initialize_population_fn=initialize_random_population,
|
||||
cities=cities,
|
||||
crossover_fn=partially_mapped_crossover_fn,
|
||||
# mutation_fn=swap_mutation_fn,
|
||||
mutation_fn=inversion_mutation_fn,
|
||||
pop_size=500,
|
||||
elitism=3,
|
||||
pc=0.9,
|
||||
pm=0.3,
|
||||
max_generations=2500,
|
||||
# max_best_repetitions=10,
|
||||
minimize=False,
|
||||
seed=17,
|
||||
save_generations=[
|
||||
1,
|
||||
5,
|
||||
20,
|
||||
50,
|
||||
100,
|
||||
300,
|
||||
500,
|
||||
700,
|
||||
900,
|
||||
1500,
|
||||
2000,
|
||||
2500,
|
||||
3000,
|
||||
3500,
|
||||
4000,
|
||||
4500,
|
||||
],
|
||||
log_every_generation=True,
|
||||
)
|
||||
|
||||
result = genetic_algorithm(config)
|
||||
|
||||
# Сохраняем конфиг и результаты в файлы
|
||||
config.save()
|
||||
result.save(config.results_dir)
|
||||
|
||||
# Выводим результаты
|
||||
print(f"Лучшая особь: {result.best_generation.best}")
|
||||
print(f"Лучшее значение фитнеса: {result.best_generation.best_fitness:.6f}")
|
||||
print(f"Количество поколений: {result.generations_count}")
|
||||
print(f"Время выполнения: {result.time_ms:.2f} мс")
|
||||
|
||||
# Сохраняем лучшую особь за всё время
|
||||
fig = plt.figure(figsize=(7, 7))
|
||||
fig.suptitle(
|
||||
f"Поколение #{result.best_generation.number}. "
|
||||
f"Лучшая особь: {result.best_generation.best_fitness:.4f}. "
|
||||
f"Среднее значение: {np.mean(result.best_generation.fitnesses):.4f}",
|
||||
fontsize=14,
|
||||
y=0.95,
|
||||
)
|
||||
|
||||
# Рисуем лучший маршрут в поколении
|
||||
ax = fig.add_subplot(1, 1, 1)
|
||||
plot_tour(config.cities, result.best_generation.best, ax)
|
||||
filename = f"best_generation_{result.best_generation.number:03d}.png"
|
||||
path_png = os.path.join(config.results_dir, filename)
|
||||
fig.savefig(path_png, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
# Рисуем график прогресса по поколениям
|
||||
plot_fitness_history(
|
||||
result, save_path=os.path.join(config.results_dir, "fitness_history.png")
|
||||
)
|
||||
99
lab3/plot_best.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import math
|
||||
import os
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from gen import (
|
||||
Chromosome,
|
||||
GARunConfig,
|
||||
genetic_algorithm,
|
||||
initialize_random_population,
|
||||
inversion_mutation_fn,
|
||||
partially_mapped_crossover_fn,
|
||||
plot_fitness_history,
|
||||
plot_tour,
|
||||
swap_mutation_fn,
|
||||
)
|
||||
|
||||
best = [
|
||||
0,
|
||||
29,
|
||||
9,
|
||||
27,
|
||||
18,
|
||||
14,
|
||||
5,
|
||||
17,
|
||||
13,
|
||||
30,
|
||||
20,
|
||||
34,
|
||||
15,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
26,
|
||||
33,
|
||||
32,
|
||||
7,
|
||||
12,
|
||||
37,
|
||||
11,
|
||||
2,
|
||||
6,
|
||||
16,
|
||||
35,
|
||||
1,
|
||||
36,
|
||||
3,
|
||||
28,
|
||||
21,
|
||||
8,
|
||||
31,
|
||||
4,
|
||||
10,
|
||||
25,
|
||||
19,
|
||||
]
|
||||
|
||||
|
||||
cities = set()
|
||||
with open("data.txt", "r") as file:
|
||||
for line in file:
|
||||
# x и y поменяны местами в визуализациях в методичке
|
||||
_, y, x = line.split()
|
||||
cities.add((float(x), float(y)))
|
||||
cities = list(cities)
|
||||
|
||||
|
||||
def euclidean_distance(city1, city2):
|
||||
return math.sqrt((city1[0] - city2[0]) ** 2 + (city1[1] - city2[1]) ** 2)
|
||||
|
||||
|
||||
def build_fitness_function(cities):
|
||||
def fitness_function(chromosome: Chromosome) -> float:
|
||||
return sum(
|
||||
euclidean_distance(cities[chromosome[i]], cities[chromosome[i + 1]])
|
||||
for i in range(len(chromosome) - 1)
|
||||
) + euclidean_distance(cities[chromosome[0]], cities[chromosome[-1]])
|
||||
|
||||
return fitness_function
|
||||
|
||||
|
||||
fitness_function = build_fitness_function(cities)
|
||||
|
||||
# Сохраняем лучшую особь за всё время
|
||||
fig = plt.figure(figsize=(7, 7))
|
||||
fig.suptitle(
|
||||
f"Лучший возможный маршрут. " f"Длина: {fitness_function(best):.4f}",
|
||||
fontsize=14,
|
||||
y=0.95,
|
||||
)
|
||||
|
||||
# Рисуем лучший маршрут в поколении
|
||||
ax = fig.add_subplot(1, 1, 1)
|
||||
plot_tour(cities, best, ax)
|
||||
filename = f"best_possible.png"
|
||||
path_png = os.path.join("", filename)
|
||||
fig.savefig(path_png, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
6
lab3/report/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
*
|
||||
|
||||
!**/
|
||||
!.gitignore
|
||||
!report.tex
|
||||
!img/**/*.png
|
||||
BIN
lab3/report/img/alg.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
lab3/report/img/optimal_tour.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
lab3/report/img/results/best_generation_1896.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
lab3/report/img/results/fitness_history.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
lab3/report/img/results/generation_001.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
lab3/report/img/results/generation_005.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
lab3/report/img/results/generation_050.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
lab3/report/img/results/generation_100.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
lab3/report/img/results/generation_300.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
lab3/report/img/results/generation_500.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
lab3/report/img/results/generation_900.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
678
lab3/report/report.tex
Normal file
@@ -0,0 +1,678 @@
|
||||
\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{Лабораторная работа №3}\\
|
||||
\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{Дано:} Эвклидовы координаты городов 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}
|
||||
|
||||
\vspace{0.3cm}
|
||||
\textbf{Требуется:}
|
||||
|
||||
\begin{enumerate}
|
||||
\item Реализовать с использованием генетических алгоритмов решение задачи коммивояжера.
|
||||
\item Для туров использовать путевое представление.
|
||||
\end{enumerate}
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Теоретические сведения}
|
||||
|
||||
Генетические алгоритмы (ГА) используют принципы и терминологию, заимствованные у биологической науки – генетики. В ГА каждая особь представляет потенциальное решение некоторой
|
||||
проблемы. В классическом ГА особь кодируется строкой двоичных символов – хромосомой. Однако представление хромосомы зависит от постановки задачи: для непрерывных задач удобны векторы вещественных чисел (real-coded), тогда как для комбинаторных задач, таких как задача коммивояжера (ЗК), естественно представлять тур как перестановку городов. Длина хромосомы совпадает с числом элементов задачи; двоичное кодирование ЗК, как правило, неэффективно из‑за необходимости «ремонта» решений после применения операторов.
|
||||
|
||||
Множество особей – потенциальных решений составляет популяцию. Поиск (суб)оптимального решения проблемы выполняется в процессе эволюции популяции - последовательного преобразования одного конечного множества решений в другое с помощью генетических операторов репродукции, кроссинговера и мутации.
|
||||
|
||||
Предварительно простой ГА случайным образом генерирует начальную популяцию стрингов
|
||||
(хромосом). Затем алгоритм генерирует следующее поколение (популяцию), с помощью трех основных генетических операторов:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Оператор репродукции (ОР);
|
||||
\item Оператор скрещивания (кроссинговера, ОК);
|
||||
\item Оператор мутации (ОМ).
|
||||
\end{enumerate}
|
||||
|
||||
ГА работает до тех пор, пока не будет выполнено заданное количество поколений (итераций)
|
||||
процесса эволюции или на некоторой генерации будет получено заданное качество или вследствие
|
||||
преждевременной сходимости при попадании в некоторый локальный оптимум. На Рис.~\ref{fig:alg} представлен простой генетический алгоритм.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.9\linewidth]{img/alg.png}
|
||||
\caption{Простой генетический алгоритм}
|
||||
\label{fig:alg}
|
||||
\end{figure}
|
||||
|
||||
\newpage
|
||||
\subsection{Основная терминология в генетических алгоритмах}
|
||||
|
||||
\textbf{Ген} -- элементарный код в хромосоме $s_i$, называемый также знаком или детектором
|
||||
(в классическом ГА $s_i = 0, 1$).
|
||||
|
||||
\textbf{Хромосома} -- упорядоченная последовательность генов в виде закодированной структуры
|
||||
данных $S = (s_1, s_2, \ldots, s_n)$, определяющая решение. Представление зависит от типа задачи: для непрерывных задач — вектор вещественных чисел; для ЗК — перестановка городов (см. раздел о представлениях: соседское, порядковое и путевое).
|
||||
|
||||
\textbf{Локус} -- местоположение (позиция, номер бита) данного гена в хромосоме.
|
||||
|
||||
\textbf{Аллель} -- значение, которое принимает данный ген (например, 0 или 1).
|
||||
|
||||
\textbf{Особь} -- одно потенциальное решение задачи (представляемое хромосомой).
|
||||
|
||||
\textbf{Популяция} -- множество особей (хромосом), представляющих потенциальные решения.
|
||||
|
||||
\textbf{Поколение} -- текущая популяция ГА на данной итерации алгоритма.
|
||||
|
||||
\textbf{Генотип} -- набор хромосом данной особи. В популяции могут использоваться как отдельные
|
||||
хромосомы, так и целые генотипы.
|
||||
|
||||
\textbf{Генофонд} -- множество всех возможных генотипов.
|
||||
|
||||
\textbf{Фенотип} -- набор значений, соответствующий данному генотипу. Это декодированное множество
|
||||
параметров задачи (например, десятичное значение $x$, соответствующее двоичному коду).
|
||||
|
||||
\textbf{Размер популяции $N$} -- число особей в популяции.
|
||||
|
||||
\textbf{Число поколений} -- количество итераций, в течение которых производится поиск.
|
||||
|
||||
\textbf{Селекция} -- совокупность правил, определяющих выживание особей на основе значений целевой функции.
|
||||
|
||||
\textbf{Эволюция популяции} -- чередование поколений, в которых хромосомы изменяют свои признаки,
|
||||
чтобы каждая новая популяция лучше приспосабливалась к среде.
|
||||
|
||||
\textbf{Фитнесс-функция} -- функция полезности, определяющая меру приспособленности особи.
|
||||
В задачах оптимизации она совпадает с целевой функцией или описывает близость к оптимальному решению.
|
||||
|
||||
\subsection{Представления хромосом для задачи коммивояжера}
|
||||
|
||||
Задача коммивояжера (ЗК) формулируется так: требуется посетить каждый из $N$ городов ровно один раз и вернуться в исходную точку, минимизируя суммарную стоимость (или длину) тура. Естественным является представление тура как перестановки городов. На практике используются три основных представления, каждое со своими операторами рекомбинации:
|
||||
|
||||
\subsubsection{Представление соседства}
|
||||
|
||||
Тур задаётся списком из $N$ городов, где в позиции $i$ указан город $j$, означающий переход из города $i$ в город $j$. Например, вектор $(2\;4\;8\;3\;9\;7\;1\;5\;6)$ соответствует туру $1\!\to\!2\!\to\!4\!\to\!3\!\to\!8\!\to\!5\!\to\!9\!\to\!6\!\to\!7$. У каждого корректного тура есть единственное соседское представление, однако не всякая строка в этом представлении корректна (возможны преждевременные циклы, например $1\!\to\!2\!\to\!4\!\to\!1\ldots$).
|
||||
|
||||
\subsubsection{Порядковое представление}
|
||||
|
||||
Тур представляется списком из $N$ позиций; $i$-й элемент равен индексу города в текущем упорядоченном списке доступных городов. Например, при опорном списке $C=(1\;2\;3\;4\;5\;6\;7\;8\;9)$ тур $1\!\to\!2\!\to\!4\!\to\!3\!\to\!8\!\to\!5\!\to\!9\!\to\!6\!\to\!7$ кодируется как $l=(1\;1\;2\;1\;4\;1\;3\;1\;1)$, последовательно «выбирая» элементы из $C$.
|
||||
|
||||
\subsubsection{Путевое представление}
|
||||
|
||||
Наиболее интуитивное представление: тур записывается как последовательность городов, например $5\!\to\!1\!\to\!7\!\to\!8\!\to\!9\!\to\!4\!\to\!6\!\to\!2\!\to\!3$ кодируется как $(5\;1\;7\;8\;9\;4\;6\;2\;3)$. Это представление сохраняет относительный порядок городов и широко применяется на практике.
|
||||
|
||||
\subsection{Кроссинговеры для представлений ЗК}
|
||||
|
||||
Операторы рекомбинации должны сохранять допустимость туров (перестановочную природу решения). Для разных представлений используются различные кроссинговеры.
|
||||
|
||||
\subsubsection{Кроссинговеры для представления соседства}
|
||||
|
||||
\textbf{Alternating Edges (обмен рёбрами):} потомок строится, поочерёдно выбирая ребра у родителей: одно ребро у первого родителя, следующее — у второго, затем снова у первого и т.д. Если выбранное ребро замыкает цикл преждевременно, выбирается другое ещё не использованное ребро того же родителя, не образующее цикл.
|
||||
|
||||
\textbf{Subtour Chunks (обмен подтурами):} потомок формируется конкатенацией кусочков (подтуров), поочерёдно взятых у родителей. При образовании преждевременного цикла производится «ремонт» аналогично предыдущему оператору.
|
||||
|
||||
\textbf{Heuristic Crossover (эвристический):} стартуя из случайного города, на каждом шаге сравниваются два инцидентных ребра, предлагаемых родителями, и выбирается более короткое; если возникает цикл или ребро уже использовано, выбирается случайный ещё не посещённый город. Оператор нацелен на сохранение коротких рёбер, но может иметь нестабильную производительность.
|
||||
|
||||
\subsubsection{Кроссинговеры для порядкового представления}
|
||||
|
||||
Для порядкового представления корректность потомков обеспечивает классический одноточечный кроссовер: любые два родителя, разрезанные в одной позиции и склеенные, порождают допустимых потомков (поскольку выбор «по индексу» в оставшемся списке городов остаётся корректным).
|
||||
|
||||
\subsubsection{Кроссинговеры для путевого представления}
|
||||
|
||||
Для путевого представления широко применяются три оператора, гарантирующие корректную перестановку у потомков.
|
||||
|
||||
\paragraph{PMX (Partially Mapped Crossover).}
|
||||
Идея: обменять подпоследовательности между родителями и построить отображение соответствий, которым затем разрешать конфликты (дубликаты).
|
||||
|
||||
\textit{Пример.} Пусть точки разреза задают сегмент позиций $4\dots7$:
|
||||
$$
|
||||
p_1=(1\;2\;3\;|\;4\;5\;6\;7\;|\;8\;9),\quad
|
||||
p_2=(4\;5\;2\;|\;1\;8\;7\;6\;|\;9\;3).
|
||||
$$
|
||||
1) Копируем сегмент второго родителя в потомка $o_1$ и формируем отображение $\{4\leftrightarrow1,\;5\leftrightarrow8,\;6\leftrightarrow7,\;7\leftrightarrow6\}$:
|
||||
$$o_1=(\_\;\_\;\_\;|\;1\;8\;7\;6\;|\;\_\;\_).$$
|
||||
2) Заполняем прочие позиции по порядку из $p_1$, применяя отображение при конфликтах: $1\mapsto4$, $8\mapsto5$.
|
||||
$$o_1=(4\;2\;3\;|\;1\;8\;7\;6\;|\;5\;9).$$
|
||||
Аналогично для $o_2$ (копируем сегмент из $p_1$, заполняем остальное из $p_2$):
|
||||
$$o_2=(1\;8\;2\;|\;4\;5\;6\;7\;|\;9\;3).$$
|
||||
PMX сохраняет как позиции части элементов, так и относительный порядок/соответствия на остальной части хромосомы.
|
||||
|
||||
\paragraph{OX (Order Crossover).}
|
||||
Идея: скопировать сегмент одного родителя и дозаполнить оставшиеся позиции элементами второго родителя в их порядке появления (пропуская уже скопированные).
|
||||
|
||||
\textit{Пример.} С теми же родителями и разрезами $4\dots7$:
|
||||
$$
|
||||
p_1=(1\;2\;3\;|\;4\;5\;6\;7\;|\;8\;9),\quad
|
||||
p_2=(4\;5\;2\;|\;1\;8\;7\;6\;|\;9\;3).
|
||||
$$
|
||||
1) Копируем сегмент $p_1$ в $o_1$:
|
||||
$$o_1=(\_\;\_\;\_\;|\;4\;5\;6\;7\;|\;\_\;\_).$$
|
||||
2) Обходя $p_2$ с позиции после правого разреза, дозаполняем: получаем
|
||||
$$o_1=(2\;1\;8\;|\;4\;5\;6\;7\;|\;9\;3).$$
|
||||
Симметрично для $o_2$ (копируем сегмент из $p_2$ и дозаполняем порядком из $p_1$):
|
||||
$$o_2=(3\;4\;5\;|\;1\;8\;7\;6\;|\;9\;2).$$
|
||||
Оператор OX сохраняет относительный порядок городов; циклический сдвиг тура несущественен.
|
||||
|
||||
\paragraph{CX (Cycle Crossover).}
|
||||
Идея: находить циклы позиций, индуцированные взаимным расположением значений у родителей, и наследовать циклы по очереди из разных родителей.
|
||||
|
||||
\textit{Пример.} Возьмём
|
||||
$$
|
||||
p_1=(1\;2\;3\;4\;5\;6\;7\;8\;9),\quad
|
||||
p_2=(4\;5\;2\;1\;8\;7\;6\;9\;3).
|
||||
$$
|
||||
Построив циклы позиций, получим допустимых потомков, например:
|
||||
$$o_1=(1\;2\;3\;4\;7\;6\;9\;8\;5),\quad o_2=(4\;1\;2\;8\;5\;6\;7\;3\;9).$$
|
||||
CX сохраняет абсолютные позиции части элементов и способствует передаче «циклами» взаимных расположений.
|
||||
|
||||
Отметим, что путевое представление акцентирует порядок городов (а не стартовый город), поэтому туры, отличающиеся циклическим сдвигом, эквивалентны.
|
||||
|
||||
\subsection{Мутации для путевого представления}
|
||||
|
||||
Операторы мутации в ГА для задачи коммивояжёра должны сохранять допустимость решения (перестановочную структуру). Для путевого представления применяются специализированные операторы, которые модифицируют порядок городов, не нарушая корректности тура.
|
||||
|
||||
\paragraph{Swap (обмен двух элементов).}
|
||||
|
||||
Идея: выбрать случайным образом две позиции в маршруте и обменять находящиеся на них города местами.
|
||||
|
||||
\textit{Пример.} Пусть исходный тур:
|
||||
$$
|
||||
t=(1\;2\;3\;4\;5\;6\;7\;8\;9).
|
||||
$$
|
||||
Выбираем позиции $i=2$ и $j=6$ (элементы $3$ и $7$). После обмена получаем:
|
||||
$$
|
||||
t'=(1\;2\;7\;4\;5\;6\;3\;8\;9).
|
||||
$$
|
||||
Оператор swap обеспечивает локальную модификацию тура, изменяя положение только двух городов.
|
||||
|
||||
\paragraph{Inversion (инверсия сегмента).}
|
||||
|
||||
Идея: выбрать случайный сегмент маршрута и обратить порядок городов внутри него.
|
||||
|
||||
\textit{Пример.} Для того же тура выбираем позиции разреза $i=3$ и $j=7$ (сегмент $4\;5\;6\;7$):
|
||||
$$
|
||||
t=(1\;2\;3\;|\;4\;5\;6\;7\;|\;8\;9).
|
||||
$$
|
||||
Инвертируем выделенный сегмент:
|
||||
$$
|
||||
t'=(1\;2\;3\;|\;7\;6\;5\;4\;|\;8\;9).
|
||||
$$
|
||||
Инверсия сохраняет связность частей маршрута, меняя направление обхода в подтуре. Этот оператор особенно эффективен при наличии пересечений рёбер, так как инверсия может «распутать» некоторые из них и улучшить длину маршрута.
|
||||
|
||||
\paragraph{Insertion (вырезка и вставка).}
|
||||
|
||||
Идея: выбрать случайный город, удалить его из текущей позиции и вставить в другую случайную позицию маршрута.
|
||||
|
||||
\textit{Пример.} Пусть исходный тур:
|
||||
$$
|
||||
t=(1\;2\;3\;4\;5\;6\;7\;8\;9).
|
||||
$$
|
||||
Выбираем город на позиции $i=3$ (элемент $4$) и целевую позицию $j=7$. Удаляем элемент $4$:
|
||||
$$
|
||||
t_{\text{tmp}}=(1\;2\;3\;5\;6\;7\;8\;9).
|
||||
$$
|
||||
Вставляем $4$ на позицию $7$:
|
||||
$$
|
||||
t'=(1\;2\;3\;5\;6\;7\;4\;8\;9).
|
||||
$$
|
||||
insertion изменяет расположение одного города относительно других, смещая соседей.
|
||||
|
||||
Все три оператора гарантируют сохранение корректной перестановки: каждый город остаётся в туре ровно один раз.
|
||||
|
||||
\newpage
|
||||
\section{Особенности реализации}
|
||||
В рамках работы создана мини-библиотека \texttt{gen.py} для решения задачи коммивояжёра (TSP) генетическим алгоритмом с путевым представлением хромосом. Второй модуль
|
||||
\texttt{expirements.py} организует серийные эксперименты (перебор параметров,
|
||||
форматирование и сохранение результатов).
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Кодирование особей}: каждая хромосома представлена как перестановка городов (\texttt{Chromosome = list[int]}), где каждый элемент -- индекс города. Популяция -- список хромосом (\texttt{Population = list[Chromosome]}). Инициализация случайными перестановками без повторений:
|
||||
\begin{itemize}
|
||||
\item \texttt{initialize\_random\_population(pop\_size: int, cities: Cites) -> Population}
|
||||
\end{itemize}
|
||||
\item \textbf{Фитнесс-функция}: целевая функция принимает хромосому (маршрут) и возвращает скалярное значение фитнесса (длину пути). Для режима минимизации используется внутреннее преобразование при селекции (сдвиг и инверсия знака), что позволяет применять рулетку:
|
||||
\begin{itemize}
|
||||
\item \texttt{eval\_population(population: Population, fitness\_func: FitnessFn) -> Fitnesses}
|
||||
\item Логика режима минимизации в \texttt{genetic\_algorithm(config: GARunConfig) -> GARunResult}
|
||||
\end{itemize}
|
||||
\item \textbf{Селекция (рулетка)}: вероятности нормируются после сдвига на минимальное значение в поколении (устойчиво к отрицательным фитнессам). Функция:
|
||||
\texttt{reproduction(population: Population, fitnesses: Fitnesses) -> Population}.
|
||||
\item \textbf{Кроссинговер}: реализованы специализированные операторы для перестановок: PMX (Partially Mapped Crossover), OX (Ordered Crossover) и CX (Cycle Crossover). Кроссинговер выполняется попарно по перемешанной популяции с вероятностью $p_c$. Функции:
|
||||
\begin{itemize}
|
||||
\item \texttt{partially\_mapped\_crossover\_fn(p1: Chromosome, p2: Chromosome) -> tuple[Chromosome, Chromosome]}
|
||||
\item \texttt{ordered\_crossover\_fn(p1: Chromosome, p2: Chromosome) -> tuple[Chromosome, Chromosome]}
|
||||
\item \texttt{cycle\_crossover\_fn(p1: Chromosome, p2: Chromosome) -> tuple[Chromosome, Chromosome]}
|
||||
\item \texttt{crossover(population: Population, pc: float, crossover\_fn: CrossoverFn) -> Population}
|
||||
\end{itemize}
|
||||
\item \textbf{Мутация}: реализованы три типа мутаций для перестановок: обмен двух городов (swap), инверсия сегмента (inversion), вырезка и вставка города (insertion). Мутация применяется с вероятностью $p_m$. Функции:
|
||||
\begin{itemize}
|
||||
\item \texttt{swap\_mutation\_fn(chrom: Chromosome) -> Chromosome}
|
||||
\item \texttt{inversion\_mutation\_fn(chrom: Chromosome) -> Chromosome}
|
||||
\item \texttt{insertion\_mutation\_fn(chrom: Chromosome) -> Chromosome}
|
||||
\item \texttt{mutation(population: Population, pm: float, mutation\_fn: MutationFn) -> Population}
|
||||
\end{itemize}
|
||||
|
||||
\item \textbf{Критерий остановки}: поддерживаются критерии по максимальному количеству поколений, повторению лучшего результата, достижению порогового значения фитнесса. Хранится история всех поколений. Проверка выполняется в функции:
|
||||
|
||||
\texttt{genetic\_algorithm(config: GARunConfig) -> GARunResult}.
|
||||
\item \textbf{Визуализация}: реализована отрисовка маршрутов обхода городов на плоскости с отображением лучшей особи поколения. Функции:
|
||||
\begin{itemize}
|
||||
\item \texttt{plot\_tour(cities: list[tuple[float, float]], tour: list[int], ax: Axes)}
|
||||
\item \texttt{save\_generation(generation: Generation, history: list[Generation], config: GARunConfig)}
|
||||
\item \texttt{plot\_fitness\_history(result: GARunResult, save\_path: str | None) -> None}
|
||||
\end{itemize}
|
||||
\item \textbf{Элитизм}: поддерживается перенос лучших особей без изменения в следующее поколение (\texttt{elitism} параметр).
|
||||
\item \textbf{Измерение времени}: длительность вычислений возвращается в миллисекундах как часть \texttt{GARunResult.time\_ms}.
|
||||
\item \textbf{Файловая организация}: результаты экспериментов сохраняются в структуре \texttt{experiments/N/} с таблицами результатов. Задействованные функции:
|
||||
\begin{itemize}
|
||||
\item \texttt{clear\_results\_directory(results\_dir: str) -> None}
|
||||
\item Функции для проведения экспериментов в модуле \texttt{expirements.py}
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
|
||||
В модуле \texttt{expirements.py} задаются координаты городов и параметры экспериментов.
|
||||
Серийные запуски и сохранение результатов реализованы для исследования влияния параметров ГА на качество решения задачи коммивояжёра.
|
||||
|
||||
\newpage
|
||||
\section{Результаты работы}
|
||||
|
||||
На Рис.~\ref{fig:gen1}--\ref{fig:lastgen} представлены результаты работы генетического алгоритма со следующими параметрами:
|
||||
\begin{itemize}
|
||||
\item $N = 500$ -- размер популяции.
|
||||
\item $p_c = 0.9$ -- вероятность кроссинговера.
|
||||
\item $p_m = 0.3$ -- вероятность мутации.
|
||||
\item $2500$ -- максимальное количество поколений.
|
||||
\item $3$ -- количество "элитных" особей, переносимых без изменения в следующее поколение.
|
||||
\item Partially mapped crossover - кроссовер.
|
||||
\item Inversion mutation - мутация
|
||||
\end{itemize}
|
||||
|
||||
На Рис.~\ref{fig:fitness_history} показан график изменения фитнесса по поколениям. Видно, что алгоритм постепенно сходится к минимально возможному значению фитнеса. Лучший маршрут был найден на поколнении №1896 (см. Рис.~\ref{fig:lastgen}).
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/fitness_history.png}
|
||||
\caption{График изменения фитнесса по поколениям}
|
||||
\label{fig:fitness_history}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/results/generation_001.png}
|
||||
\caption{Лучший маршрут поколения №1}
|
||||
\label{fig:gen1}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/results/generation_005.png}
|
||||
\caption{Лучший маршрут поколения №5}
|
||||
\label{fig:gen5}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/results/generation_050.png}
|
||||
\caption{Лучший маршрут поколения №50}
|
||||
\label{fig:gen50}
|
||||
\end{figure}
|
||||
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/results/generation_100.png}
|
||||
\caption{Лучший маршрут поколения №100}
|
||||
\label{fig:gen100}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/results/generation_300.png}
|
||||
\caption{Лучший маршрут поколения №300}
|
||||
\label{fig:gen300}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/results/generation_500.png}
|
||||
\caption{Лучший маршрут поколения №500}
|
||||
\label{fig:gen500}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/results/generation_900.png}
|
||||
\caption{Лучший маршрут поколения №900}
|
||||
\label{fig:gen900}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/results/best_generation_1896.png}
|
||||
\caption{Лучший маршрут поколения №1896}
|
||||
\label{fig:lastgen}
|
||||
\end{figure}
|
||||
|
||||
|
||||
\newpage
|
||||
\phantom{text}
|
||||
|
||||
\newpage
|
||||
\section{Исследование реализации}
|
||||
\subsection{Проведение измерений}
|
||||
В рамках лабораторной работы необходимо было исследовать зависимость времени выполнения задачи и количества поколений от популяции и вероятностей кроссинговера и мутации хромосомы
|
||||
|
||||
Для исследования были выбраны следующие значения параметров:
|
||||
\begin{itemize}
|
||||
\item $N = 10, 50, 100, 500$ -- размер популяции.
|
||||
\item $p_c = 0.5, 0.6, 0.7, 0.8, 0.9$ -- вероятность кроссинговера.
|
||||
\item $p_m = 0.05, 0.2, 0.3, 0.4, 0.5, 0.8$ -- вероятность мутации.
|
||||
\item $3$ -- количество "элитных" особей, переносимых без изменения в следующее поколение.
|
||||
\item Partially mapped crossover - кроссовер.
|
||||
\item Inversion mutation - мутация
|
||||
\item 7000 - пороговое значение фитнеса для остановки алгоритма.
|
||||
\end{itemize}
|
||||
|
||||
Результаты измерений представлены в таблицах \ref{tab:pc_pm_results_10}--\ref{tab:pc_pm_results_500}. В ячейках указано время в миллисекундах нахождения минимума функции. В скобках указано количество поколений, за которое было найдено решение. Во второй строке указано усреднённое по всем запускам лучшее значение фитнеса. Если в ячейке стоит прочерк, то это означает, что решение не было найдено за 2500 поколений. Лучшее значение по времени выполнения и по значению фитнеса для каждого размера популяции выделено цветом и жирным шрифтом.
|
||||
|
||||
\newcolumntype{Y}{>{\centering\arraybackslash}X}
|
||||
% Автоматически сгенерированные LaTeX таблицы
|
||||
% Лучший результат по времени и по фитнесу выделены жирным отдельно
|
||||
% Убедитесь, что подключен \usepackage{tabularx}
|
||||
% ВНИМАНИЕ: Убедитесь, что подключен \usepackage{xcolor} для цветового выделения
|
||||
% Используйте \newcolumntype{Y}{>{\centering\arraybackslash}X} перед таблицами
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 10$}
|
||||
\begin{tabularx}{\linewidth}{l *{6}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.050} & \textbf{0.200} & \textbf{0.300} & \textbf{0.400} & \textbf{0.500} & \textbf{0.800} \\
|
||||
\midrule
|
||||
\textbf{0.5} & — & — & 674.4 (1783) 6943.28027 & 715.1 (1856) 6925.47290 & — & 225.5 (567) 6984.75016 \\
|
||||
\textbf{0.6} & — & — & 550.6 (1427) 6899.82219 & 649.4 (1653) 6897.01699 & — & — \\
|
||||
\textbf{0.7} & — & — & 476.7 (1216) 6796.98342 & 287.4 (724) 6977.43028 & \textcolor{magenta}{\textbf{201.0 (503)}} 6794.32839 & — \\
|
||||
\textbf{0.8} & — & — & — & 767.2 (1852) 6810.96744 & 253.3 (623) 6905.36866 & — \\
|
||||
\textbf{0.9} & — & — & — & — & — & — \\
|
||||
\textbf{1.0} & — & 750.9 (1847) 6988.52746 & 415.7 (1016) 6897.99266 & 465.7 (1126) \textcolor{magenta}{\textbf{6762.96572}} & 275.9 (662) 6997.70453 & — \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_10}
|
||||
\end{table}
|
||||
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 50$}
|
||||
\begin{tabularx}{\linewidth}{l *{6}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.050} & \textbf{0.200} & \textbf{0.300} & \textbf{0.400} & \textbf{0.500} & \textbf{0.800} \\
|
||||
\midrule
|
||||
\textbf{0.5} & 1711.4 (1083) 6927.73356 & — & 1642.7 (1015) 6894.10066 & 1355.6 (809) 6938.12550 & — & 936.3 (544) 6925.57274 \\
|
||||
\textbf{0.6} & 1338.4 (828) 6952.02461 & 889.1 (552) 6951.40489 & 1142.5 (687) 6963.17379 & 1446.9 (864) 6992.95281 & — & 2646.2 (1509) 6932.85788 \\
|
||||
\textbf{0.7} & 1860.8 (1146) 6996.63686 & — & 2387.8 (1378) 6999.00110 & — & \textcolor{magenta}{\textbf{809.9 (474)}} 6965.83938 & 1614.7 (918) 6990.50067 \\
|
||||
\textbf{0.8} & — & — & 1244.4 (713) \textcolor{magenta}{\textbf{6704.60011}} & 1500.5 (859) 6970.42362 & 1013.5 (581) 6998.68282 & — \\
|
||||
\textbf{0.9} & — & — & — & — & — & — \\
|
||||
\textbf{1.0} & — & 891.6 (503) 6952.80522 & — & — & 1489.6 (824) 6735.40661 & 3685.9 (1978) 6989.21247 \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_50}
|
||||
\end{table}
|
||||
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 100$}
|
||||
\begin{tabularx}{\linewidth}{l *{6}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.050} & \textbf{0.200} & \textbf{0.300} & \textbf{0.400} & \textbf{0.500} & \textbf{0.800} \\
|
||||
\midrule
|
||||
\textbf{0.5} & 1342.3 (441) 6988.26353 & 1467.7 (459) 6958.81642 & 4041.3 (1269) \textcolor{magenta}{\textbf{6839.94363}} & — & — & 3635.1 (1046) 6966.14098 \\
|
||||
\textbf{0.6} & 2460.6 (763) 6872.20321 & \textcolor{magenta}{\textbf{1316.5 (409)}} 6861.65860 & — & 2310.7 (691) 6912.50054 & 2220.9 (663) 6907.57533 & — \\
|
||||
\textbf{0.7} & — & 1934.1 (591) 6933.87982 & — & 1966.0 (587) 6943.09435 & 2872.9 (840) 6998.39699 & — \\
|
||||
\textbf{0.8} & 3227.9 (969) 6990.28735 & 1754.4 (523) 6996.67018 & — & 2152.8 (621) 6988.30495 & 8057.2 (2236) 6899.21400 & — \\
|
||||
\textbf{0.9} & — & — & 3794.4 (1079) 6963.79199 & 2549.3 (721) 6975.22091 & 4469.6 (1249) 6945.46938 & 8919.4 (2375) 6858.03529 \\
|
||||
\textbf{1.0} & 4164.4 (1215) 6927.53288 & — & — & — & 3618.7 (1019) 6898.56773 & — \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_100}
|
||||
\end{table}
|
||||
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 500$}
|
||||
\begin{tabularx}{\linewidth}{l *{6}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.050} & \textbf{0.200} & \textbf{0.300} & \textbf{0.400} & \textbf{0.500} & \textbf{0.800} \\
|
||||
\midrule
|
||||
\textbf{0.5} & 11709.8 (782) 6994.15844 & \textcolor{magenta}{\textbf{5232.3 (341)}} 6957.38204 & 10676.9 (674) 6980.66167 & 6849.7 (430) 6782.99526 & — & 13051.6 (775) 6880.72481 \\
|
||||
\textbf{0.6} & 7193.3 (461) 6960.64487 & — & — & 14856.5 (866) 6941.77959 & 12944.9 (776) 6958.57319 & 19051.6 (1102) 6951.30787 \\
|
||||
\textbf{0.7} & 18611.7 (1150) 6810.96744 & 23286.9 (1413) 6895.65139 & — & 14141.6 (830) 6976.37927 & — & — \\
|
||||
\textbf{0.8} & 25456.0 (1556) 6962.40902 & — & 20592.3 (1223) 6998.71555 & — & — & 38979.2 (2097) 6842.54074 \\
|
||||
\textbf{0.9} & 14260.1 (825) 6967.60134 & 26692.6 (1551) 6922.32909 & — & — & 29235.4 (1644) \textcolor{magenta}{\textbf{6667.02991}} & 41352.0 (2252) 6765.87009 \\
|
||||
\textbf{1.0} & 34026.1 (1996) 6953.24255 & — & — & — & — & — \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_500}
|
||||
\end{table}
|
||||
|
||||
|
||||
|
||||
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\subsection{Анализ результатов}
|
||||
|
||||
Наилучшее найденное решение составило \textbf{6667.03} при параметрах $N=500$, $P_c=0.9$, $P_m=0.5$ за 1644 поколения. Это всего на \textbf{0.12\%} хуже оптимального значения 6659, что демонстрирует высокую эффективность алгоритма. Наихудшие результаты показала конфигурация с $N=10$, $P_c=0.7$, $P_m=0.3$ (лучший фитнес 6796.98), что на 2.07\% хуже оптимума. Малый размер популяции в 10 особей оказался недостаточным для стабильного поиска качественных решений — более половины конфигураций при $N=10$ вообще не нашли решение за 2500 поколений.
|
||||
|
||||
Наиболее быстрая конфигурация — $N=10$, $P_c=0.7$, $P_m=0.5$ — нашла решение за \textbf{201 мс} (503 поколения). Однако качество решения при таких параметрах нестабильно. Среди конфигураций с большой популяцией лучшее время показала $N=500$, $P_c=0.5$, $P_m=0.2$ — \textbf{5232 мс} (341 поколение), что является оптимальным балансом скорости и качества для больших популяций.
|
||||
|
||||
С ростом размера популяции наблюдается явное улучшение качества решений: при $N=10$ лучший результат 6762.97, при $N=500$ — 6667.03. Одновременно количество необходимых поколений снижается (с 503 до 341), но общее время выполнения растет линейно из-за увеличения числа особей в каждом поколении. Этот эффект объясняется тем, что большая популяция обеспечивает большее генетическое разнообразие, позволяя алгоритму быстрее находить оптимальные решения.
|
||||
|
||||
Что касается вероятности кроссовера, средние значения $P_c=0.6$--$0.8$ показывают стабильные результаты для всех размеров популяций. Экстремальные значения ($P_c=0.9$ или $1.0$) работают хорошо только при больших популяциях ($N \geq 100$), при малых — часто приводят к преждевременной сходимости (наблюдается много прочерков в таблицах). Это связано с тем, что высокая вероятность кроссовера при малой популяции быстро приводит к гомогенизации генофонда.
|
||||
|
||||
Анализ влияния вероятности мутации показал, что низкие значения $P_m=0.05$ неэффективны для малых популяций — недостаточно разнообразия для выхода из локальных минимумов. Умеренные значения $P_m=0.2$--$0.5$ демонстрируют лучшие результаты, обеспечивая баланс между эксплуатацией найденных решений и исследованием нового пространства поиска. Высокое значение $P_m=0.8$ часто приводит к расхождению алгоритма, так как слишком сильные изменения разрушают хорошие решения быстрее, чем алгоритм успевает их найти (многие конфигурации не нашли решение за отведенное время).
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Ответ на контрольный вопрос}
|
||||
|
||||
\textbf{Вопрос}: Тур в порядковом представлении, используемые кроссинговеры.
|
||||
|
||||
\textbf{Ответ}: Тур представляется списком из $N$ позиций; $i$-й элемент равен индексу города в текущем упорядоченном списке доступных городов. Например, при опорном списке $C=(1\;2\;3\;4\;5\;6\;7\;8\;9)$ тур $1\!\to\!2\!\to\!4\!\to\!3\!\to\!8\!\to\!5\!\to\!9\!\to\!6\!\to\!7$ кодируется как $l=(1\;1\;2\;1\;4\;1\;3\;1\;1)$, последовательно «выбирая» элементы из $C$.
|
||||
|
||||
Для порядкового представления корректность потомков обеспечивает классический одноточечный кроссовер: любые два родителя, разрезанные в одной позиции и склеенные, порождают допустимых потомков (поскольку выбор «по индексу» в оставшемся списке городов остаётся корректным).
|
||||
|
||||
|
||||
\newpage
|
||||
\section*{Заключение}
|
||||
\addcontentsline{toc}{section}{Заключение}
|
||||
|
||||
В ходе третьей лабораторной работы была успешно решена задача коммивояжера с использованием генетических алгоритмов для 38 городов Джибути:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Изучен теоретический материал о представлениях туров (соседское, порядковое, путевое) и специализированных операторах кроссинговера и мутации для задачи коммивояжера;
|
||||
\item Создана программная библиотека на языке Python с реализацией путевого представления хромосом, операторов PMX, OX и CX для кроссинговера, операторов swap, inversion и insertion для мутации, а также селекции методом рулетки с поддержкой элитизма;
|
||||
\item Проведено исследование влияния параметров генетического алгоритма на качество и скорость нахождения решения для популяций размером 10, 50, 100 и 500 особей с различными значениями вероятностей кроссинговера и мутации;
|
||||
\item Получено решение с длиной маршрута 6667.03, отклоняющееся от оптимального значения 6659 всего на 0.12\%.
|
||||
\end{enumerate}
|
||||
|
||||
|
||||
\newpage
|
||||
\section*{Список литературы}
|
||||
\addcontentsline{toc}{section}{Список литературы}
|
||||
|
||||
\vspace{-1.5cm}
|
||||
\begin{thebibliography}{0}
|
||||
\bibitem{vostrov}
|
||||
Методические указания по выполнению лабораторных работ к курсу «Генетические алгоритмы», 119 стр.
|
||||
\end{thebibliography}
|
||||
|
||||
\end{document}
|
||||
1
lab4/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.14
|
||||
50
lab4/draw_tree.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from graphviz import Digraph
|
||||
|
||||
|
||||
def make_pow2_sum_tree(n=8):
|
||||
dot = Digraph("FullTree")
|
||||
dot.attr(rankdir="TB") # направление сверху вниз
|
||||
dot.attr("node", shape="circle", style="filled", fillcolor="lightgray")
|
||||
|
||||
node_count = 0
|
||||
|
||||
def new_node(label):
|
||||
nonlocal node_count
|
||||
node_id = f"n{node_count}"
|
||||
node_count += 1
|
||||
dot.node(node_id, label)
|
||||
return node_id
|
||||
|
||||
def pow2_node(xi):
|
||||
n1 = new_node("pow2")
|
||||
n2 = new_node(xi)
|
||||
dot.edge(n1, n2)
|
||||
return n1
|
||||
|
||||
def plus(a, b):
|
||||
n = new_node("+")
|
||||
dot.edge(n, a)
|
||||
dot.edge(n, b)
|
||||
return n
|
||||
|
||||
all_terms = []
|
||||
for i in range(1, n + 1):
|
||||
terms = [pow2_node(f"x{j}") for j in range(1, i + 1)]
|
||||
s = terms[0]
|
||||
for t in terms[1:]:
|
||||
s = plus(s, t)
|
||||
all_terms.append(s)
|
||||
|
||||
root = all_terms[0]
|
||||
for t in all_terms[1:]:
|
||||
root = plus(root, t)
|
||||
|
||||
dot.node("root", "f(x)")
|
||||
dot.edge("root", root)
|
||||
|
||||
return dot
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
g = make_pow2_sum_tree(8)
|
||||
g.render("original_tree", format="png", cleanup=True)
|
||||
0
lab4/gp/__init__.py
Normal file
87
lab4/gp/chromosome.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import random
|
||||
from typing import Sequence
|
||||
|
||||
from .node import Node
|
||||
from .primitive import Primitive
|
||||
|
||||
|
||||
class Chromosome:
|
||||
def __init__(
|
||||
self,
|
||||
terminals: Sequence[Primitive],
|
||||
operations: Sequence[Primitive],
|
||||
root: Node,
|
||||
):
|
||||
self.terminals = terminals
|
||||
self.operations = operations
|
||||
self.root = root
|
||||
|
||||
def copy(self) -> Chromosome:
|
||||
return Chromosome(self.terminals, self.operations, self.root.copy_subtree())
|
||||
|
||||
def prune(self, max_depth: int) -> None:
|
||||
self.root.prune(self.terminals, max_depth)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Строковое представление хромосомы в виде формулы в инфиксной форме."""
|
||||
return str(self.root)
|
||||
|
||||
@classmethod
|
||||
def full_init(
|
||||
cls,
|
||||
terminals: Sequence[Primitive],
|
||||
operations: Sequence[Primitive],
|
||||
max_depth: int,
|
||||
) -> Chromosome:
|
||||
"""Полная инициализация.
|
||||
|
||||
В полном методе при генерации дерева, пока не достигнута максимальная глубина,
|
||||
допускается выбор только функциональных символов, а на последнем уровне
|
||||
(максимальной глубины) выбираются только терминальные символы.
|
||||
"""
|
||||
|
||||
def build(level: int) -> Node:
|
||||
# Если достигнута максимальная глубина — выбираем терминал
|
||||
if level == max_depth:
|
||||
return Node(random.choice(terminals))
|
||||
|
||||
# Иначе выбираем операцию и создаём потомков
|
||||
op = random.choice(operations)
|
||||
node = Node(op)
|
||||
for _ in range(op.arity):
|
||||
node.add_child(build(level + 1))
|
||||
return node
|
||||
|
||||
return cls(terminals, operations, build(1))
|
||||
|
||||
@classmethod
|
||||
def grow_init(
|
||||
cls,
|
||||
terminals: Sequence[Primitive],
|
||||
operations: Sequence[Primitive],
|
||||
max_depth: int,
|
||||
# min_depth: int, # ???
|
||||
terminal_probability: float = 0.5,
|
||||
) -> Chromosome:
|
||||
"""Растущая инициализация.
|
||||
|
||||
В растущей инициализации генерируются нерегулярные деревья с различной глубиной
|
||||
листьев вследствие случайного на каждом шаге выбора функционального
|
||||
или терминального символа. Здесь при выборе терминального символа рост дерева
|
||||
прекращается по текущей ветви и поэтому дерево имеет нерегулярную структуру.
|
||||
"""
|
||||
|
||||
def build(level: int) -> Node:
|
||||
# Если достигнута максимальная глубина, либо сыграла заданная вероятность
|
||||
# — выбираем терминал
|
||||
if level == max_depth or random.random() < terminal_probability:
|
||||
return Node(random.choice(terminals))
|
||||
|
||||
# Иначе выбираем случайную операцию и создаём потомков
|
||||
op = random.choice(operations)
|
||||
node = Node(op)
|
||||
for _ in range(op.arity):
|
||||
node.add_child(build(level + 1))
|
||||
return node
|
||||
|
||||
return cls(terminals, operations, build(1))
|
||||
31
lab4/gp/crossovers.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import random
|
||||
|
||||
from .chromosome import Chromosome
|
||||
from .node import swap_subtrees
|
||||
|
||||
|
||||
def crossover_subtree(
|
||||
parent1: Chromosome, parent2: Chromosome, max_depth: int
|
||||
) -> tuple[Chromosome, Chromosome]:
|
||||
"""Кроссовер поддеревьев.
|
||||
|
||||
Выбираются случайные узлы в каждом родителе, затем соответствующие им поддеревья
|
||||
меняются местами. Если глубина результирующих хромосом превышает max_depth,
|
||||
то их деревья обрезаются до max_depth.
|
||||
"""
|
||||
child1 = parent1.copy()
|
||||
child2 = parent2.copy()
|
||||
|
||||
# Выбираем случайные узлы, не включая корень
|
||||
if child1.root.get_depth() <= 1 or child2.root.get_depth() <= 1:
|
||||
return child1, child2
|
||||
|
||||
cut1 = random.choice(child1.root.list_nodes()[1:])
|
||||
cut2 = random.choice(child2.root.list_nodes()[1:])
|
||||
|
||||
swap_subtrees(cut1, cut2)
|
||||
|
||||
child1.prune(max_depth)
|
||||
child2.prune(max_depth)
|
||||
|
||||
return child1, child2
|
||||
133
lab4/gp/fitness.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from .chromosome import Chromosome
|
||||
|
||||
type FitnessFn = Callable[
|
||||
[
|
||||
Chromosome,
|
||||
NDArray[np.float64],
|
||||
Callable[[NDArray[np.float64]], NDArray[np.float64]],
|
||||
],
|
||||
float,
|
||||
]
|
||||
type TargetFunction = Callable[[NDArray[np.float64]], NDArray[np.float64]]
|
||||
type TestPointsFn = Callable[[], NDArray[np.float64]]
|
||||
|
||||
|
||||
class BaseFitness(ABC):
|
||||
def __init__(self, target_fn: TargetFunction, test_points_fn: TestPointsFn):
|
||||
self.target_function = target_fn
|
||||
self.test_points_fn = test_points_fn
|
||||
|
||||
@abstractmethod
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float: ...
|
||||
|
||||
def __call__(self, chromosome: Chromosome) -> float:
|
||||
test_points = self.test_points_fn()
|
||||
|
||||
context = {t: test_points[:, i] for i, t in enumerate(chromosome.terminals)}
|
||||
predicted = chromosome.root.eval(context)
|
||||
true_values = self.target_function(test_points)
|
||||
|
||||
return self.fitness_fn(chromosome, predicted, true_values)
|
||||
|
||||
|
||||
class MSEFitness(BaseFitness):
|
||||
"""Среднеквадратичная ошибка"""
|
||||
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float:
|
||||
return float(np.mean((predicted - true_values) ** 2))
|
||||
|
||||
|
||||
class RMSEFitness(BaseFitness):
|
||||
"""Корень из среднеквадратичной ошибки"""
|
||||
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float:
|
||||
return float(np.sqrt(np.mean((predicted - true_values) ** 2)))
|
||||
|
||||
|
||||
class MAEFitness(BaseFitness):
|
||||
"""Средняя абсолютная ошибка"""
|
||||
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float:
|
||||
return float(np.mean(np.abs(predicted - true_values)))
|
||||
|
||||
|
||||
class NRMSEFitness(BaseFitness):
|
||||
"""Нормализованный RMSE (масштаб-инвариантен)"""
|
||||
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float:
|
||||
denom = np.std(true_values)
|
||||
if denom == 0:
|
||||
return 1e6
|
||||
return float(np.sqrt(np.mean((predicted - true_values) ** 2)) / denom)
|
||||
|
||||
|
||||
class PenalizedFitness(BaseFitness):
|
||||
"""Фитнес со штрафом за размер и глубину дерева: ошибка + λ * (размер + depth_weight * глубина)"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target_fn: TargetFunction,
|
||||
test_points_fn: TestPointsFn,
|
||||
base_fitness: BaseFitness,
|
||||
lambda_: float = 0.001,
|
||||
depth_weight: float = 0.2,
|
||||
scale_penalty: bool | None = None,
|
||||
):
|
||||
super().__init__(target_fn, test_points_fn)
|
||||
self.base_fitness = base_fitness
|
||||
self.lambda_ = lambda_
|
||||
self.depth_weight = depth_weight
|
||||
|
||||
# Масштабировать штраф необязательно, если функция фитнеса нормализована
|
||||
if scale_penalty is None:
|
||||
scale_penalty = not isinstance(base_fitness, NRMSEFitness)
|
||||
self.scale_penalty = scale_penalty
|
||||
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float:
|
||||
base = self.base_fitness.fitness_fn(chromosome, predicted, true_values)
|
||||
|
||||
size = chromosome.root.get_size()
|
||||
depth = chromosome.root.get_depth()
|
||||
|
||||
penalty = self.lambda_ * (size + self.depth_weight * depth)
|
||||
|
||||
if self.scale_penalty:
|
||||
penalty *= base
|
||||
|
||||
return float(base + penalty)
|
||||
382
lab4/gp/ga.py
Normal file
@@ -0,0 +1,382 @@
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Callable
|
||||
|
||||
import graphviz
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
from .chromosome import Chromosome
|
||||
from .node import Node
|
||||
from .types import Fitnesses, Population
|
||||
|
||||
type FitnessFn = Callable[[Chromosome], float]
|
||||
|
||||
type InitializePopulationFn = Callable[[int], Population]
|
||||
type CrossoverFn = Callable[[Chromosome, Chromosome], tuple[Chromosome, Chromosome]]
|
||||
type MutationFn = Callable[[Chromosome], Chromosome]
|
||||
type SelectionFn = Callable[[Population, Fitnesses], Population]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GARunConfig:
|
||||
fitness_func: FitnessFn
|
||||
crossover_fn: CrossoverFn
|
||||
mutation_fn: MutationFn
|
||||
selection_fn: SelectionFn
|
||||
init_population: Population
|
||||
pc: float # вероятность кроссинговера
|
||||
pm: float # вероятность мутации
|
||||
max_generations: int # максимальное количество поколений
|
||||
elitism: int = (
|
||||
0 # сколько лучших особей перенести без изменения в следующее поколение
|
||||
)
|
||||
max_best_repetitions: int | None = (
|
||||
None # остановка при повторении лучшего результата
|
||||
)
|
||||
seed: int | None = None # seed для генератора случайных чисел
|
||||
minimize: bool = True # если True, ищем минимум вместо максимума
|
||||
save_generations: list[int] | None = (
|
||||
None # индексы поколений для сохранения графиков
|
||||
)
|
||||
results_dir: str = "results" # папка для сохранения графиков
|
||||
fitness_avg_threshold: float | None = (
|
||||
None # порог среднего значения фитнес функции для остановки
|
||||
)
|
||||
best_value_threshold: float | None = (
|
||||
None # остановка при достижении значения фитнеса лучше заданного
|
||||
)
|
||||
log_every_generation: bool = False # логировать каждое поколение
|
||||
|
||||
def save(self, filename: str = "GARunConfig.txt"):
|
||||
"""Сохраняет конфиг в results_dir."""
|
||||
os.makedirs(self.results_dir, exist_ok=True)
|
||||
path = os.path.join(self.results_dir, filename)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
for k, v in asdict(self).items():
|
||||
f.write(f"{k}: {v}\n")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Generation:
|
||||
number: int
|
||||
best: Chromosome
|
||||
best_fitness: float
|
||||
avg_fitness: float
|
||||
population: Population
|
||||
fitnesses: Fitnesses
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GARunResult:
|
||||
generations_count: int
|
||||
best_generation: Generation
|
||||
history: list[Generation]
|
||||
time_ms: float
|
||||
|
||||
def save(self, path: str, filename: str = "GARunResult.txt"):
|
||||
"""Сохраняет конфиг в results_dir."""
|
||||
os.makedirs(path, exist_ok=True)
|
||||
path = os.path.join(path, filename)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
for k, v in asdict(self).items():
|
||||
if k == "history":
|
||||
continue
|
||||
if k == "best_generation":
|
||||
f.write(
|
||||
f"{k}: Number: {v['number']}, Best Fitness: {v['best_fitness']}, Best: {v['best']}\n"
|
||||
)
|
||||
else:
|
||||
f.write(f"{k}: {v}\n")
|
||||
|
||||
|
||||
def crossover(
|
||||
population: Population,
|
||||
pc: float,
|
||||
crossover_fn: CrossoverFn,
|
||||
) -> Population:
|
||||
"""Оператор кроссинговера (скрещивания) выполняется с заданной вероятностью pc.
|
||||
|
||||
Две хромосомы (родители) выбираются случайно из промежуточной популяции.
|
||||
|
||||
Если популяция нечетного размера, то последняя хромосома скрещивается со случайной
|
||||
другой хромосомой из популяции. В таком случае одна из хромосом может поучаствовать
|
||||
в кроссовере дважды.
|
||||
"""
|
||||
# Создаем копию популяции и перемешиваем её для случайного выбора пар
|
||||
shuffled_population = population.copy()
|
||||
random.shuffle(shuffled_population)
|
||||
|
||||
next_population = []
|
||||
pop_size = len(shuffled_population)
|
||||
|
||||
for i in range(0, pop_size, 2):
|
||||
p1 = shuffled_population[i]
|
||||
p2 = shuffled_population[(i + 1) % pop_size]
|
||||
if np.random.random() <= pc:
|
||||
p1, p2 = crossover_fn(p1, p2)
|
||||
next_population.append(p1)
|
||||
next_population.append(p2)
|
||||
|
||||
return next_population[:pop_size]
|
||||
|
||||
|
||||
def mutation(
|
||||
population: Population, pm: float, gen_num: int, mutation_fn: MutationFn
|
||||
) -> Population:
|
||||
"""Мутация происходит с вероятностью pm."""
|
||||
next_population = []
|
||||
for chrom in population:
|
||||
next_population.append(
|
||||
mutation_fn(chrom) if np.random.random() <= pm else chrom
|
||||
)
|
||||
return next_population
|
||||
|
||||
|
||||
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 eval_population(population: Population, fitness_func: FitnessFn) -> Fitnesses:
|
||||
return np.array([fitness_func(chrom) for chrom in population])
|
||||
|
||||
|
||||
def render_tree_to_graphviz(
|
||||
node: Node, graph: graphviz.Digraph, node_id: str = "0"
|
||||
) -> None:
|
||||
"""Рекурсивно добавляет узлы дерева в graphviz граф."""
|
||||
graph.node(node_id, label=node.value.name)
|
||||
|
||||
for i, child in enumerate(node.children):
|
||||
child_id = f"{node_id}_{i}"
|
||||
render_tree_to_graphviz(child, graph, child_id)
|
||||
graph.edge(node_id, child_id)
|
||||
|
||||
|
||||
def save_generation(
|
||||
generation: Generation, history: list[Generation], config: GARunConfig
|
||||
) -> None:
|
||||
"""Сохраняет визуализацию лучшей хромосомы поколения в виде дерева."""
|
||||
os.makedirs(config.results_dir, exist_ok=True)
|
||||
|
||||
# Создаем граф для визуализации дерева
|
||||
dot = graphviz.Digraph(comment=f"Generation {generation.number}")
|
||||
dot.attr(rankdir="TB") # Top to Bottom direction
|
||||
dot.attr("node", shape="circle", style="filled", fillcolor="lightblue")
|
||||
|
||||
# Добавляем заголовок
|
||||
depth = generation.best.root.get_depth()
|
||||
title = (
|
||||
f"Поколение #{generation.number}\\n"
|
||||
f"Лучшая особь: {generation.best_fitness:.4f}\\n"
|
||||
f"Глубина дерева: {depth}"
|
||||
)
|
||||
dot.attr(label=title, labelloc="t", fontsize="14")
|
||||
|
||||
# Рендерим дерево
|
||||
render_tree_to_graphviz(generation.best.root, dot)
|
||||
|
||||
# Сохраняем
|
||||
filename = f"generation_{generation.number:03d}"
|
||||
filepath = os.path.join(config.results_dir, filename)
|
||||
dot.render(filepath, format="png", cleanup=True)
|
||||
|
||||
|
||||
def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
||||
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)
|
||||
|
||||
population = config.init_population
|
||||
|
||||
start = time.perf_counter()
|
||||
history: list[Generation] = []
|
||||
best: Generation | None = None
|
||||
|
||||
generation_number = 1
|
||||
best_repetitions = 0
|
||||
|
||||
while True:
|
||||
# Вычисляем фитнес для всех особей в популяции
|
||||
fitnesses = eval_population(population, config.fitness_func)
|
||||
|
||||
# Сохраняем лучших особей для переноса в следующее поколение
|
||||
elites: list[Chromosome] = []
|
||||
if config.elitism:
|
||||
elites = deepcopy(
|
||||
[
|
||||
population[i]
|
||||
for i in sorted(
|
||||
range(len(fitnesses)),
|
||||
key=lambda i: fitnesses[i],
|
||||
reverse=not config.minimize,
|
||||
)
|
||||
][: config.elitism]
|
||||
)
|
||||
|
||||
# Находим лучшую особь в поколении
|
||||
best_index = (
|
||||
int(np.argmin(fitnesses)) if config.minimize else int(np.argmax(fitnesses))
|
||||
)
|
||||
|
||||
# Добавляем эпоху в историю
|
||||
current = Generation(
|
||||
number=generation_number,
|
||||
best=population[best_index],
|
||||
best_fitness=fitnesses[best_index],
|
||||
avg_fitness=float(np.mean(fitnesses)),
|
||||
# population=deepcopy(population),
|
||||
population=[],
|
||||
# fitnesses=deepcopy(fitnesses),
|
||||
fitnesses=np.array([]),
|
||||
)
|
||||
history.append(current)
|
||||
|
||||
if config.log_every_generation:
|
||||
print(
|
||||
f"Generation #{generation_number} best: {current.best_fitness},"
|
||||
f" avg: {np.mean(fitnesses)}"
|
||||
)
|
||||
|
||||
# Обновляем лучшую эпоху
|
||||
if (
|
||||
best is None
|
||||
or (config.minimize and current.best_fitness < best.best_fitness)
|
||||
or (not config.minimize and current.best_fitness > best.best_fitness)
|
||||
):
|
||||
best = current
|
||||
|
||||
# Проверка критериев остановки
|
||||
stop_algorithm = False
|
||||
|
||||
if generation_number >= config.max_generations:
|
||||
stop_algorithm = True
|
||||
|
||||
if config.max_best_repetitions is not None and generation_number > 1:
|
||||
if history[-2].best_fitness == current.best_fitness:
|
||||
best_repetitions += 1
|
||||
|
||||
if best_repetitions == config.max_best_repetitions:
|
||||
stop_algorithm = True
|
||||
else:
|
||||
best_repetitions = 0
|
||||
|
||||
if config.best_value_threshold is not None:
|
||||
if (
|
||||
config.minimize and current.best_fitness < config.best_value_threshold
|
||||
) or (
|
||||
not config.minimize
|
||||
and current.best_fitness > config.best_value_threshold
|
||||
):
|
||||
stop_algorithm = True
|
||||
|
||||
if config.fitness_avg_threshold is not None:
|
||||
mean_fitness = np.mean(fitnesses)
|
||||
if (config.minimize and mean_fitness < config.fitness_avg_threshold) or (
|
||||
not config.minimize and mean_fitness > config.fitness_avg_threshold
|
||||
):
|
||||
stop_algorithm = True
|
||||
|
||||
# Сохраняем указанные поколения и последнее поколение
|
||||
if config.save_generations and (
|
||||
stop_algorithm or generation_number in config.save_generations
|
||||
):
|
||||
save_generation(current, history, config)
|
||||
|
||||
if stop_algorithm:
|
||||
break
|
||||
|
||||
# селекция (для минимума инвертируем знак)
|
||||
parents = config.selection_fn(
|
||||
population, fitnesses if not config.minimize else -fitnesses
|
||||
)
|
||||
|
||||
# кроссинговер попарно
|
||||
next_population = crossover(parents, config.pc, config.crossover_fn)
|
||||
|
||||
# мутация
|
||||
next_population = mutation(
|
||||
next_population,
|
||||
config.pm,
|
||||
generation_number,
|
||||
config.mutation_fn,
|
||||
)
|
||||
|
||||
# Вставляем элиту в новую популяцию
|
||||
population = next_population[: len(population) - config.elitism] + elites
|
||||
|
||||
generation_number += 1
|
||||
|
||||
end = time.perf_counter()
|
||||
|
||||
assert best is not None, "Best was never set"
|
||||
result = GARunResult(
|
||||
len(history),
|
||||
best,
|
||||
history,
|
||||
(end - start) * 1000.0,
|
||||
)
|
||||
|
||||
# Автоматически строим графики истории фитнеса
|
||||
if config.save_generations:
|
||||
plot_fitness_history(result, save_dir=config.results_dir)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def plot_fitness_history(result: GARunResult, save_dir: str | None = None) -> None:
|
||||
"""Рисует графики изменения лучших и средних значений фитнеса по поколениям.
|
||||
|
||||
Создает два отдельных графика:
|
||||
- fitness_best.png - график лучших значений
|
||||
- fitness_avg.png - график средних значений
|
||||
"""
|
||||
generations = [gen.number for gen in result.history]
|
||||
best_fitnesses = [gen.best_fitness for gen in result.history]
|
||||
avg_fitnesses = [gen.avg_fitness for gen in result.history]
|
||||
|
||||
# График лучших значений
|
||||
fig_best, ax_best = plt.subplots(figsize=(10, 6))
|
||||
ax_best.plot(generations, best_fitnesses, linewidth=2, color="blue")
|
||||
ax_best.set_xlabel("Поколение", fontsize=12)
|
||||
ax_best.set_ylabel("Лучшее значение фитнес-функции", fontsize=12)
|
||||
ax_best.set_title("Лучшее значение фитнеса по поколениям", fontsize=14)
|
||||
ax_best.grid(True, alpha=0.3)
|
||||
|
||||
if save_dir:
|
||||
best_path = os.path.join(save_dir, "fitness_best.png")
|
||||
fig_best.savefig(best_path, dpi=150, bbox_inches="tight")
|
||||
print(f"График лучших значений сохранен в {best_path}")
|
||||
else:
|
||||
plt.show()
|
||||
|
||||
plt.close(fig_best)
|
||||
|
||||
# График средних значений
|
||||
fig_avg, ax_avg = plt.subplots(figsize=(10, 6))
|
||||
ax_avg.plot(generations, avg_fitnesses, linewidth=2, color="orange")
|
||||
ax_avg.set_xlabel("Поколение", fontsize=12)
|
||||
ax_avg.set_ylabel("Среднее значение фитнес-функции", fontsize=12)
|
||||
ax_avg.set_title("Среднее значение фитнеса по поколениям", fontsize=14)
|
||||
ax_avg.grid(True, alpha=0.3)
|
||||
|
||||
if save_dir:
|
||||
avg_path = os.path.join(save_dir, "fitness_avg.png")
|
||||
fig_avg.savefig(avg_path, dpi=150, bbox_inches="tight")
|
||||
print(f"График средних значений сохранен в {avg_path}")
|
||||
else:
|
||||
plt.show()
|
||||
|
||||
plt.close(fig_avg)
|
||||
131
lab4/gp/mutations.py
Normal file
@@ -0,0 +1,131 @@
|
||||
import random
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Sequence
|
||||
|
||||
from .chromosome import Chromosome
|
||||
|
||||
|
||||
class BaseMutation(ABC):
|
||||
@abstractmethod
|
||||
def mutate(self, chromosome: Chromosome) -> Chromosome: ...
|
||||
def __call__(self, chromosome: Chromosome) -> Chromosome:
|
||||
chromosome = chromosome.copy()
|
||||
return self.mutate(chromosome)
|
||||
|
||||
|
||||
class ShrinkMutation(BaseMutation):
|
||||
"""Усекающая мутация. Заменяет случайно выбранную операцию на случайный терминал."""
|
||||
|
||||
def mutate(self, chromosome: Chromosome) -> Chromosome:
|
||||
operation_nodes = [n for n in chromosome.root.list_nodes() if n.value.arity > 0]
|
||||
|
||||
if not operation_nodes:
|
||||
return chromosome
|
||||
|
||||
target_node = random.choice(operation_nodes)
|
||||
|
||||
target_node.prune(chromosome.terminals, max_depth=1)
|
||||
|
||||
return chromosome
|
||||
|
||||
|
||||
class GrowMutation(BaseMutation):
|
||||
"""Растущая мутация. Заменяет случайно выбранный узел на случайное поддерево."""
|
||||
|
||||
def __init__(self, max_depth: int):
|
||||
self.max_depth = max_depth
|
||||
|
||||
def mutate(self, chromosome: Chromosome) -> Chromosome:
|
||||
target_node = random.choice(chromosome.root.list_nodes())
|
||||
|
||||
max_subtree_depth = self.max_depth - target_node.get_level() + 1
|
||||
|
||||
subtree = Chromosome.grow_init(
|
||||
chromosome.terminals, chromosome.operations, max_subtree_depth
|
||||
).root
|
||||
|
||||
if target_node.parent:
|
||||
target_node.parent.replace_child(target_node, subtree)
|
||||
else:
|
||||
chromosome.root = subtree
|
||||
|
||||
return chromosome
|
||||
|
||||
|
||||
class NodeReplacementMutation(BaseMutation):
|
||||
"""Мутация замены операции (Node Replacement Mutation).
|
||||
|
||||
Выбирает случайный узел и заменяет его
|
||||
на случайную другую операцию той же арности или терминал, сохраняя поддеревья.
|
||||
|
||||
Если подходящей альтернативы нет — возвращает копию без изменений.
|
||||
"""
|
||||
|
||||
def mutate(self, chromosome: Chromosome) -> Chromosome:
|
||||
target_node = random.choice(chromosome.root.list_nodes())
|
||||
current_arity = target_node.value.arity
|
||||
|
||||
same_arity = [
|
||||
op
|
||||
for op in list(chromosome.operations) + list(chromosome.terminals)
|
||||
if op.arity == current_arity and op != target_node.value
|
||||
]
|
||||
if not same_arity:
|
||||
return chromosome
|
||||
|
||||
new_operation = random.choice(same_arity)
|
||||
|
||||
target_node.value = new_operation
|
||||
|
||||
return chromosome
|
||||
|
||||
|
||||
class HoistMutation(BaseMutation):
|
||||
def mutate(self, chromosome: Chromosome) -> Chromosome:
|
||||
"""Hoist-мутация (анти-bloat).
|
||||
|
||||
Выбирает случайное поддерево, затем внутри него — случайное поддерево меньшей
|
||||
глубины, и заменяет исходное поддерево на это внутреннее.
|
||||
|
||||
В результате дерево становится короче, сохраняя часть структуры.
|
||||
"""
|
||||
operation_nodes = [n for n in chromosome.root.list_nodes() if n.value.arity > 0]
|
||||
if not operation_nodes:
|
||||
return chromosome
|
||||
|
||||
outer_subtree = random.choice(operation_nodes)
|
||||
outer_nodes = outer_subtree.list_nodes()[1:] # исключаем корень
|
||||
|
||||
inner_subtree = random.choice(outer_nodes).copy_subtree()
|
||||
|
||||
if outer_subtree.parent:
|
||||
outer_subtree.parent.replace_child(outer_subtree, inner_subtree)
|
||||
else:
|
||||
chromosome.root = inner_subtree
|
||||
|
||||
return chromosome
|
||||
|
||||
|
||||
class CombinedMutation(BaseMutation):
|
||||
"""Комбинированная мутация.
|
||||
|
||||
Принимает список (или словарь) мутаций и случайно выбирает одну из них
|
||||
для применения. Можно задать веса вероятностей.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, mutations: Sequence[BaseMutation], probs: Sequence[float] | None = None
|
||||
):
|
||||
if probs is not None:
|
||||
assert abs(sum(probs) - 1.0) < 1e-8, (
|
||||
"Сумма вероятностей должна быть равна 1"
|
||||
)
|
||||
assert len(probs) == len(mutations), (
|
||||
"Число вероятностей должно совпадать с числом мутаций"
|
||||
)
|
||||
self.mutations = mutations
|
||||
self.probs = probs
|
||||
|
||||
def mutate(self, chromosome: Chromosome) -> Chromosome:
|
||||
mutation = random.choices(self.mutations, weights=self.probs, k=1)[0]
|
||||
return mutation(chromosome)
|
||||
118
lab4/gp/node.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import random
|
||||
from typing import Sequence
|
||||
|
||||
from .primitive import Primitive
|
||||
from .types import Context, Value
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, value: Primitive):
|
||||
self.value = value
|
||||
self.parent: Node | None = None
|
||||
self.children: list[Node] = []
|
||||
|
||||
def add_child(self, child: Node) -> None:
|
||||
self.children.append(child)
|
||||
child.parent = self
|
||||
|
||||
def remove_child(self, child: Node) -> None:
|
||||
self.children.remove(child)
|
||||
child.parent = None
|
||||
|
||||
def replace_child(self, old_child: Node, new_child: Node) -> None:
|
||||
self.children[self.children.index(old_child)] = new_child
|
||||
old_child.parent = None
|
||||
new_child.parent = self
|
||||
|
||||
def remove_children(self) -> None:
|
||||
for child in self.children:
|
||||
child.parent = None
|
||||
self.children = []
|
||||
|
||||
def copy_subtree(self) -> Node:
|
||||
node = Node(self.value)
|
||||
for child in self.children:
|
||||
node.add_child(child.copy_subtree())
|
||||
return node
|
||||
|
||||
def list_nodes(self) -> list[Node]:
|
||||
"""Список всех узлов поддерева, начиная с текущего (aka depth-first-search)."""
|
||||
nodes: list[Node] = [self]
|
||||
for child in self.children:
|
||||
nodes.extend(child.list_nodes())
|
||||
return nodes
|
||||
|
||||
def prune(self, terminals: Sequence[Primitive], max_depth: int) -> None:
|
||||
"""Усечение поддерева до заданной глубины.
|
||||
|
||||
Заменяет операции на глубине max_depth на случайные терминалы.
|
||||
"""
|
||||
|
||||
def prune_recursive(node: Node, current_depth: int) -> None:
|
||||
if node.value.arity == 0: # Терминалы остаются без изменений
|
||||
return
|
||||
|
||||
if current_depth >= max_depth:
|
||||
node.remove_children()
|
||||
node.value = random.choice(terminals)
|
||||
return
|
||||
|
||||
for child in node.children:
|
||||
prune_recursive(child, current_depth + 1)
|
||||
|
||||
prune_recursive(self, 1)
|
||||
|
||||
def get_depth(self) -> int:
|
||||
"""Вычисляет глубину поддерева, начиная с текущего узла."""
|
||||
return (
|
||||
max(child.get_depth() for child in self.children) + 1
|
||||
if self.children
|
||||
else 1
|
||||
)
|
||||
|
||||
def get_size(self) -> int:
|
||||
"""Вычисляет размер поддерева, начиная с текущего узла."""
|
||||
return sum(child.get_size() for child in self.children) + 1
|
||||
|
||||
def get_level(self) -> int:
|
||||
"""Вычисляет уровень узла в дереве (расстояние от корня). Корень имеет уровень 1."""
|
||||
return self.parent.get_level() + 1 if self.parent else 1
|
||||
|
||||
def eval(self, context: Context) -> Value:
|
||||
return self.value.eval(
|
||||
[child.eval(context) for child in self.children], context
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Рекурсивный перевод древовидного вида формулы в строку в инфиксной форме."""
|
||||
if self.value.arity == 0:
|
||||
return self.value.name
|
||||
|
||||
if self.value.arity == 2:
|
||||
return f"({self.children[0]} {self.value.name} {self.children[1]})"
|
||||
|
||||
return f"{self.value.name}({', '.join(str(child) for child in self.children)})"
|
||||
|
||||
def to_str_tree(self, prefix="", is_last: bool = True) -> str:
|
||||
"""Строковое представление древовидной структуры."""
|
||||
lines = prefix + ("└── " if is_last else "├── ") + self.value.name + "\n"
|
||||
child_prefix = prefix + (" " if is_last else "│ ")
|
||||
for i, child in enumerate(self.children):
|
||||
is_child_last = i == len(self.children) - 1
|
||||
lines += child.to_str_tree(child_prefix, is_child_last)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def swap_subtrees(a: Node, b: Node) -> None:
|
||||
if a.parent is None or b.parent is None:
|
||||
raise ValueError("Нельзя обменять корни деревьев")
|
||||
|
||||
# Сохраняем ссылки на родителей
|
||||
a_parent = a.parent
|
||||
b_parent = b.parent
|
||||
|
||||
i = a_parent.children.index(a)
|
||||
j = b_parent.children.index(b)
|
||||
a_parent.children[i], b_parent.children[j] = b, a
|
||||
a.parent, b.parent = b_parent, a_parent
|
||||
63
lab4/gp/ops.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from typing import Callable, Sequence
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from .primitive import Operation
|
||||
|
||||
type Value = NDArray[np.float64]
|
||||
|
||||
|
||||
def make_safe(
|
||||
fn: Callable[[Sequence[Value]], Value],
|
||||
) -> Callable[[Sequence[Value]], Value]:
|
||||
"""Обёртка для стабилизации результатов векторных операций."""
|
||||
|
||||
def wrapped(args: Sequence[Value]) -> Value:
|
||||
with np.errstate(
|
||||
over="ignore", invalid="ignore", divide="ignore", under="ignore"
|
||||
):
|
||||
res = fn(args)
|
||||
|
||||
# гарантируем, что на выходе всегда NDArray[np.float64]
|
||||
if not isinstance(res, np.ndarray):
|
||||
res = np.array(res, dtype=np.float64)
|
||||
|
||||
res = np.nan_to_num(res, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
res = np.clip(res, -1e6, 1e6)
|
||||
|
||||
return res
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
# Унарные операции
|
||||
NEG = Operation("-", 1, make_safe(lambda x: -x[0]))
|
||||
SIN = Operation("sin", 1, make_safe(lambda x: np.sin(x[0])))
|
||||
COS = Operation("cos", 1, make_safe(lambda x: np.cos(x[0])))
|
||||
|
||||
|
||||
SQUARE = Operation("pow2", 1, make_safe(lambda x: np.clip(x[0], -1e3, 1e3) ** 2))
|
||||
|
||||
|
||||
EXP = Operation("exp", 1, make_safe(lambda x: np.exp(np.clip(x[0], -10, 10))))
|
||||
|
||||
|
||||
# Бинарные операции
|
||||
ADD = Operation("+", 2, lambda x: x[0] + x[1])
|
||||
SUB = Operation("-", 2, lambda x: x[0] - x[1])
|
||||
MUL = Operation("*", 2, lambda x: x[0] * x[1])
|
||||
|
||||
ADD = Operation("+", 2, make_safe(lambda x: x[0] + x[1]))
|
||||
SUB = Operation("-", 2, make_safe(lambda x: x[0] - x[1]))
|
||||
MUL = Operation("*", 2, make_safe(lambda x: x[0] * x[1]))
|
||||
DIV = Operation(
|
||||
"/",
|
||||
2,
|
||||
make_safe(lambda x: np.divide(x[0], np.where(np.abs(x[1]) < 1e-10, 1e-10, x[1]))),
|
||||
)
|
||||
POW = Operation(
|
||||
"^",
|
||||
2,
|
||||
make_safe(lambda x: np.power(np.clip(x[0], -1e3, 1e3), np.clip(x[1], -3, 3))),
|
||||
)
|
||||
35
lab4/gp/primitive.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Sequence
|
||||
|
||||
from .types import Context, Value
|
||||
|
||||
type OperationFn = Callable[[Sequence[Value]], Value]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Primitive:
|
||||
name: str
|
||||
arity: int
|
||||
operation_fn: OperationFn | None
|
||||
|
||||
def eval(self, args: Sequence[Value], context: Context) -> Value:
|
||||
if self.operation_fn is None:
|
||||
return context[self]
|
||||
|
||||
return self.operation_fn(args)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.arity != 0 and self.operation_fn is None:
|
||||
raise ValueError("Operation is required for primitive with non-zero arity")
|
||||
|
||||
|
||||
def Var(name: str) -> Primitive:
|
||||
return Primitive(name=name, arity=0, operation_fn=None)
|
||||
|
||||
|
||||
def Const(name: str, val: Value) -> Primitive:
|
||||
return Primitive(name=name, arity=0, operation_fn=lambda _args: val)
|
||||
|
||||
|
||||
def Operation(name: str, arity: int, operation_fn: OperationFn) -> Primitive:
|
||||
return Primitive(name=name, arity=arity, operation_fn=operation_fn)
|
||||
88
lab4/gp/selection.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import numpy as np
|
||||
|
||||
from .types import Fitnesses, Population
|
||||
|
||||
|
||||
def roulette_selection(population: Population, fitnesses: Fitnesses) -> Population:
|
||||
"""Селекция методом рулетки.
|
||||
|
||||
Чем больше значение фитнеса, тем больше вероятность выбора особи. Для минимизации
|
||||
значения фитнеса нужно предварительно инвертировать.
|
||||
"""
|
||||
# Чтобы работать с отрицательными f, сдвигаем значения фитнес функции на минимальное
|
||||
# значение в популяции. Вычитаем min_fit, т. к. min_fit может быть отрицательным.
|
||||
min_fit = np.min(fitnesses)
|
||||
shifted_fitnesses = fitnesses - min_fit + 1e-12
|
||||
|
||||
# Получаем вероятности для каждой особи
|
||||
probs = shifted_fitnesses / np.sum(shifted_fitnesses)
|
||||
cum = np.cumsum(probs)
|
||||
|
||||
# Выбираем особей методом рулетки
|
||||
selected = []
|
||||
for _ in population:
|
||||
r = np.random.random()
|
||||
idx = int(np.searchsorted(cum, r, side="left"))
|
||||
selected.append(population[idx])
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
def tournament_selection(
|
||||
population: Population,
|
||||
fitnesses: Fitnesses,
|
||||
k: int = 3,
|
||||
) -> Population:
|
||||
"""Турнирная селекция.
|
||||
|
||||
В каждом турнире случайно выбирается k особей, и побеждает та,
|
||||
у которой лучшее (наибольшее) значение фитнеса. Для минимизации
|
||||
значения фитнеса нужно предварительно инвертировать.
|
||||
|
||||
Args:
|
||||
population: список особей (Population)
|
||||
fitnesses: список или массив фитнесов (Fitnesses)
|
||||
k: размер турнира
|
||||
|
||||
Returns:
|
||||
Новая популяция того же размера
|
||||
"""
|
||||
size = len(population)
|
||||
selected = []
|
||||
for _ in range(size):
|
||||
idxs = np.random.choice(size, size=k, replace=False)
|
||||
|
||||
fits = fitnesses[idxs]
|
||||
|
||||
winner_idx = idxs[np.argmax(fits)]
|
||||
selected.append(population[winner_idx])
|
||||
return selected
|
||||
|
||||
|
||||
def stochastic_tournament_selection(
|
||||
population: Population,
|
||||
fitnesses: Fitnesses,
|
||||
k: int = 3,
|
||||
p_best: float = 0.75,
|
||||
) -> Population:
|
||||
"""Стохастическая турнирная селекция.
|
||||
|
||||
Побеждает лучший в турнире с вероятностью p_best, иначе выбирается
|
||||
случайный участник турнира.
|
||||
"""
|
||||
size = len(population)
|
||||
selected = []
|
||||
|
||||
for _ in range(size):
|
||||
idxs = np.random.choice(size, size=k, replace=False)
|
||||
fits = fitnesses[idxs]
|
||||
order = np.argsort(-fits)
|
||||
|
||||
if np.random.random() < p_best:
|
||||
winner_idx = idxs[order[0]]
|
||||
else:
|
||||
winner_idx = np.random.choice(idxs[1:]) if k > 1 else idxs[0]
|
||||
|
||||
selected.append(population[winner_idx])
|
||||
|
||||
return selected
|
||||
18
lab4/gp/types.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import TYPE_CHECKING, Callable, Protocol
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .chromosome import Chromosome
|
||||
from .node import Node
|
||||
from .primitive import Primitive
|
||||
|
||||
type Population = list[Chromosome]
|
||||
type Fitnesses = NDArray[np.float64]
|
||||
type InitFunc = Callable[[Chromosome], Node]
|
||||
type Value = NDArray[np.float64]
|
||||
|
||||
|
||||
class Context(Protocol):
|
||||
def __getitem__(self, key: Primitive, /) -> Value: ...
|
||||
29
lab4/gp/utils.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from typing import Sequence
|
||||
|
||||
from .chromosome import Chromosome
|
||||
from .primitive import Primitive
|
||||
from .types import Population
|
||||
|
||||
|
||||
def ramped_initialization(
|
||||
chromosomes_per_variation: int,
|
||||
depths: list[int],
|
||||
terminals: Sequence[Primitive],
|
||||
operations: Sequence[Primitive],
|
||||
) -> Population:
|
||||
"""Комбинация методов grow и full инициализации хромосом для инициализации начальной
|
||||
популяции.
|
||||
"""
|
||||
population: Population = []
|
||||
|
||||
for depth in depths:
|
||||
population.extend(
|
||||
Chromosome.full_init(terminals, operations, depth)
|
||||
for _ in range(chromosomes_per_variation)
|
||||
)
|
||||
population.extend(
|
||||
Chromosome.grow_init(terminals, operations, depth)
|
||||
for _ in range(chromosomes_per_variation)
|
||||
)
|
||||
|
||||
return population
|
||||
108
lab4/main.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
graphviz должен быть доступен в PATH (недостаточно просто установить через pip)
|
||||
|
||||
Можно проверить командой
|
||||
dot -V
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from gp.crossovers import crossover_subtree
|
||||
from gp.fitness import (
|
||||
MAEFitness,
|
||||
MSEFitness,
|
||||
NRMSEFitness,
|
||||
RMSEFitness,
|
||||
)
|
||||
from gp.ga import GARunConfig, genetic_algorithm
|
||||
from gp.mutations import (
|
||||
CombinedMutation,
|
||||
GrowMutation,
|
||||
HoistMutation,
|
||||
NodeReplacementMutation,
|
||||
ShrinkMutation,
|
||||
)
|
||||
from gp.ops import ADD, COS, DIV, EXP, MUL, POW, SIN, SQUARE, SUB
|
||||
from gp.primitive import Var
|
||||
from gp.selection import tournament_selection
|
||||
from gp.utils import ramped_initialization
|
||||
|
||||
NUM_VARS = 8
|
||||
TEST_POINTS = 10000
|
||||
MAX_DEPTH = 10
|
||||
MAX_GENERATIONS = 200
|
||||
SEED = 17
|
||||
np.random.seed(SEED)
|
||||
random.seed(SEED)
|
||||
X = np.random.uniform(-5.536, 5.536, size=(TEST_POINTS, NUM_VARS))
|
||||
operations = [SQUARE, SIN, COS, EXP, ADD, SUB, MUL, DIV, POW]
|
||||
terminals = [Var(f"x{i}") for i in range(1, NUM_VARS + 1)]
|
||||
|
||||
|
||||
def target_function(x: NDArray[np.float64]) -> NDArray[np.float64]:
|
||||
"""
|
||||
Векторизованная версия функции:
|
||||
f(x) = sum_{i=1}^n sum_{j=1}^i x_j^2
|
||||
x имеет форму (n_samples, n_vars)
|
||||
"""
|
||||
# Префиксные суммы квадратов по оси переменных
|
||||
x_sq = x**2
|
||||
prefix_sums = np.cumsum(x_sq, axis=1)
|
||||
# Суммируем по i (ось 1)
|
||||
return np.sum(prefix_sums, axis=1)
|
||||
|
||||
|
||||
fitness_function = RMSEFitness(target_function, lambda: X)
|
||||
combined_mutation = CombinedMutation(
|
||||
mutations=[
|
||||
GrowMutation(max_depth=MAX_DEPTH),
|
||||
NodeReplacementMutation(),
|
||||
HoistMutation(),
|
||||
ShrinkMutation(),
|
||||
],
|
||||
probs=[0.4, 0.3, 0.15, 0.15],
|
||||
)
|
||||
|
||||
init_population = ramped_initialization(
|
||||
20, [i for i in range(MAX_DEPTH - 9, MAX_DEPTH + 1)], terminals, operations
|
||||
)
|
||||
|
||||
print("Population size:", len(init_population))
|
||||
|
||||
config = GARunConfig(
|
||||
fitness_func=fitness_function,
|
||||
crossover_fn=lambda p1, p2: crossover_subtree(p1, p2, max_depth=MAX_DEPTH),
|
||||
mutation_fn=combined_mutation,
|
||||
selection_fn=lambda p, f: tournament_selection(p, f, k=3),
|
||||
init_population=init_population,
|
||||
seed=SEED,
|
||||
pc=0.85,
|
||||
pm=0.15,
|
||||
elitism=15,
|
||||
max_generations=MAX_GENERATIONS,
|
||||
log_every_generation=True,
|
||||
save_generations=[1, 10, 20, 30, 40, 50, 100, 150, 200],
|
||||
)
|
||||
|
||||
result = genetic_algorithm(config)
|
||||
|
||||
|
||||
# Выводим результаты
|
||||
print(f"Лучшая особь: {result.best_generation.best}")
|
||||
print(result.best_generation.best.root.to_str_tree())
|
||||
print(f"Лучшее значение фитнеса: {result.best_generation.best_fitness:.6f}")
|
||||
print(f"Количество поколений: {result.generations_count}")
|
||||
print(f"Время выполнения: {result.time_ms:.2f} мс")
|
||||
print("Population size:", len(init_population))
|
||||
|
||||
mse_fitness = MSEFitness(target_function, lambda: X)
|
||||
print(f"MSE: {mse_fitness(result.best_generation.best):.6f}")
|
||||
rmse_fitness = RMSEFitness(target_function, lambda: X)
|
||||
print(f"RMSE: {rmse_fitness(result.best_generation.best):.6f}")
|
||||
mae_fitness = MAEFitness(target_function, lambda: X)
|
||||
print(f"MAE: {mae_fitness(result.best_generation.best):.6f}")
|
||||
nrmse_fitness = NRMSEFitness(target_function, lambda: X)
|
||||
print(f"NRMSE: {nrmse_fitness(result.best_generation.best):.6f}")
|
||||
BIN
lab4/original_tree.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
12
lab4/pyproject.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[project]
|
||||
name = "lab4"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"graphviz>=0.21",
|
||||
"matplotlib>=3.10.7",
|
||||
"numpy>=2.3.4",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py314"
|
||||
26
lab4/pytest.ini
Normal file
@@ -0,0 +1,26 @@
|
||||
[tool:pytest]
|
||||
# Пути для поиска тестов
|
||||
testpaths = tests
|
||||
|
||||
# Паттерны для имён файлов с тестами
|
||||
python_files = test_*.py
|
||||
|
||||
# Паттерны для имён классов с тестами
|
||||
python_classes = Test*
|
||||
|
||||
# Паттерны для имён функций-тестов
|
||||
python_functions = test_*
|
||||
|
||||
# Опции для более подробного вывода
|
||||
addopts =
|
||||
-v
|
||||
--strict-markers
|
||||
--tb=short
|
||||
--disable-warnings
|
||||
|
||||
# Маркеры для категоризации тестов
|
||||
markers =
|
||||
slow: marks tests as slow (deselect with '-m "not slow"')
|
||||
unit: unit tests
|
||||
integration: integration tests
|
||||
|
||||
6
lab4/report/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
*
|
||||
|
||||
!**/
|
||||
!.gitignore
|
||||
!report.tex
|
||||
!img/**/*.png
|
||||
BIN
lab4/report/img/best_tree.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
lab4/report/img/original_tree.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
lab4/report/img/results/fitness_avg.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
lab4/report/img/results/fitness_best.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
lab4/report/img/results/generation_001.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
lab4/report/img/results/generation_010.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
lab4/report/img/results/generation_020.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
lab4/report/img/results/generation_030.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
lab4/report/img/results/generation_040.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
lab4/report/img/results/generation_050.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
lab4/report/img/results/generation_100.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
lab4/report/img/results/generation_150.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
lab4/report/img/results/generation_200.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
705
lab4/report/report.tex
Normal file
@@ -0,0 +1,705 @@
|
||||
\documentclass[a4paper, final]{article}
|
||||
%\usepackage{literat} % Нормальные шрифты
|
||||
\usepackage[14pt]{extsizes} % для того чтобы задать нестандартный 14-ый размер шрифта
|
||||
\usepackage{tabularx}
|
||||
\usepackage{booktabs}
|
||||
\usepackage[T2A]{fontenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[russian]{babel}
|
||||
\usepackage{amsmath}
|
||||
\usepackage[left=25mm, top=20mm, right=20mm, bottom=20mm, footskip=10mm]{geometry}
|
||||
\usepackage{ragged2e} %для растягивания по ширине
|
||||
\usepackage{setspace} %для межстрочно го интервала
|
||||
\usepackage{moreverb} %для работы с листингами
|
||||
\usepackage{indentfirst} % для абзацного отступа
|
||||
\usepackage{moreverb} %для печати в листинге исходного кода программ
|
||||
\usepackage{pdfpages} %для вставки других pdf файлов
|
||||
\usepackage{tikz}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{afterpage}
|
||||
\usepackage{longtable}
|
||||
\usepackage{float}
|
||||
\usepackage{xcolor}
|
||||
|
||||
|
||||
|
||||
|
||||
% \usepackage[paper=A4,DIV=12]{typearea}
|
||||
\usepackage{pdflscape}
|
||||
% \usepackage{lscape}
|
||||
|
||||
\usepackage{array}
|
||||
\usepackage{multirow}
|
||||
|
||||
\renewcommand\verbatimtabsize{4\relax}
|
||||
\renewcommand\listingoffset{0.2em} %отступ от номеров строк в листинге
|
||||
\renewcommand{\arraystretch}{1.4} % изменяю высоту строки в таблице
|
||||
\usepackage[font=small, singlelinecheck=false, justification=centering, format=plain, labelsep=period]{caption} %для настройки заголовка таблицы
|
||||
\usepackage{listings} %листинги
|
||||
\usepackage{xcolor} % цвета
|
||||
\usepackage{hyperref}% для гиперссылок
|
||||
\usepackage{enumitem} %для перечислений
|
||||
|
||||
\newcommand{\specialcell}[2][l]{\begin{tabular}[#1]{@{}l@{}}#2\end{tabular}}
|
||||
|
||||
|
||||
\setlist[enumerate,itemize]{leftmargin=1.2cm} %отступ в перечислениях
|
||||
|
||||
\hypersetup{colorlinks,
|
||||
allcolors=[RGB]{010 090 200}} %красивые гиперссылки (не красные)
|
||||
|
||||
% подгружаемые языки — подробнее в документации listings (это всё для листингов)
|
||||
% включаем кириллицу и добавляем кое−какие опции
|
||||
\lstset{tabsize=2,
|
||||
breaklines,
|
||||
basicstyle=\footnotesize,
|
||||
columns=fullflexible,
|
||||
flexiblecolumns,
|
||||
numbers=left,
|
||||
numberstyle={\footnotesize},
|
||||
keywordstyle=\color{blue},
|
||||
inputencoding=cp1251,
|
||||
extendedchars=true
|
||||
}
|
||||
|
||||
\textheight=24cm % высота текста
|
||||
\textwidth=16cm % ширина текста
|
||||
\oddsidemargin=0pt % отступ от левого края
|
||||
\topmargin=-1.5cm % отступ от верхнего края
|
||||
\parindent=24pt % абзацный отступ
|
||||
\parskip=5pt % интервал между абзацами
|
||||
\tolerance=2000 % терпимость к "жидким" строкам
|
||||
\flushbottom % выравнивание высоты страниц
|
||||
|
||||
|
||||
% Настройка листингов
|
||||
\lstset{
|
||||
language=python,
|
||||
extendedchars=\true,
|
||||
inputencoding=utf8,
|
||||
keepspaces=true,
|
||||
% captionpos=b, % подписи листингов снизу
|
||||
}
|
||||
|
||||
\begin{document} % начало документа
|
||||
|
||||
|
||||
|
||||
% НАЧАЛО ТИТУЛЬНОГО ЛИСТА
|
||||
\begin{center}
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\normalsize{МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ\\
|
||||
федеральное государственное автономное образовательное учреждение высшего образования «Санкт-Петербургский политехнический университет Петра Великого»\\[10pt]}
|
||||
\normalsize{Институт компьютерных наук и кибербезопасности}\\[10pt]
|
||||
\normalsize{Высшая школа технологий искусственного интеллекта}\\[10pt]
|
||||
\normalsize{Направление: 02.03.01 <<Математика и компьютерные науки>>}\\
|
||||
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\large{Лабораторная работа №4}\\
|
||||
\large{по дисциплине}\\
|
||||
\large{<<Генетические алгоритмы>>}\\
|
||||
\large{Вариант 18}\\
|
||||
|
||||
% \hfill \break
|
||||
\hfill \break
|
||||
\end{center}
|
||||
|
||||
\small{
|
||||
\begin{tabular}{lrrl}
|
||||
\!\!\!Студент, & \hspace{2cm} & & \\
|
||||
\!\!\!группы 5130201/20101 & \hspace{2cm} & \underline{\hspace{3cm}} &Тищенко А. А. \\\\
|
||||
\!\!\!Преподаватель & \hspace{2cm} & \underline{\hspace{3cm}} & Большаков А. А. \\\\
|
||||
&&\hspace{4cm}
|
||||
\end{tabular}
|
||||
\begin{flushright}
|
||||
<<\underline{\hspace{1cm}}>>\underline{\hspace{2.5cm}} 2025г.
|
||||
\end{flushright}
|
||||
}
|
||||
|
||||
\hfill \break
|
||||
% \hfill \break
|
||||
\begin{center} \small{Санкт-Петербург, 2025} \end{center}
|
||||
\thispagestyle{empty} % выключаем отображение номера для этой страницы
|
||||
|
||||
% КОНЕЦ ТИТУЛЬНОГО ЛИСТА
|
||||
\newpage
|
||||
|
||||
\tableofcontents
|
||||
|
||||
\newpage
|
||||
\section {Постановка задачи}
|
||||
В данной работе были поставлены следующие задачи:
|
||||
|
||||
\begin{itemize}
|
||||
\item Разработать эволюционный алгоритм, реализующий ГП для нахождения заданной по варианту функции.
|
||||
\begin{itemize}
|
||||
\item Структура для представления программы – древовидное представление.
|
||||
\item Терминальное множество: переменные $x_1, x_2, x_3, \ldots, x_n$, и константы в соответствии с заданием по варианту.
|
||||
\item Функциональное множество: $+$, $-$, $*$, $/$, $abs()$, $sin()$, $cos()$, $exp()$, возведение в степень.
|
||||
\item Фитнесс-функция – мера близости между реальными значениями выхода и требуемыми.
|
||||
\end{itemize}
|
||||
\item Представить графически найденное решение на каждой итерации.
|
||||
\item Сравнить найденное решение с представленным в условии задачи.
|
||||
\end{itemize}
|
||||
|
||||
\textbf{Индивидуальное задание вариант 18:}
|
||||
|
||||
\textbf{Дано:} Функция
|
||||
|
||||
$$f(x) = \sum_{i=1}^{n} \sum_{j=1}^{i} x_j^2, \text{ где } x_j \in [-5.536, 5.536] \text{ для всех } j = 1, \ldots, n, \text{ а } n = 8.$$
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Теоретические сведения}
|
||||
|
||||
\subsection{Генетическое программирование}
|
||||
|
||||
\textbf{Генетическое программирование} (ГП) — разновидность эволюционных алгоритмов, в которых особь представляет собой программу, автоматически создаваемую для решения задачи. В отличие от генетических алгоритмов с фиксированной структурой хромосом, в ГП особи имеют переменную длину, что требует специальных методов кодирования, инициализации и генетических операторов. Ключевая идея ГП — представление программы на высоком уровне абстракции с учётом структуры компьютерных программ.
|
||||
|
||||
Оценка программ выполняется с помощью фитнесс-функции, отражающей степень соответствия решения требованиям задачи. Обычно используются метрики ошибки: среднеквадратичная ошибка, абсолютная ошибка или другие функции рассогласования между вычисленным и ожидаемым значением. Чем ниже ошибка, тем выше приспособленность особи.
|
||||
|
||||
\subsection{Терминальное и функциональное множества}
|
||||
|
||||
Программы формируются из \textbf{переменных}, \textbf{констант} и \textbf{функций}, связанных синтаксическими правилами. Для их описания необходимо определить два базовых множества:
|
||||
\begin{itemize}
|
||||
\item \textbf{Терминальное множество}, включающее константы и переменные.
|
||||
\item \textbf{Функциональное множество}, состоящее из операторов и элементарных функций, таких как \( \exp(x) \), \( \sin(x) \) и других.
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{Терминальное множество}
|
||||
Терминальное множество включает:
|
||||
\begin{enumerate}
|
||||
\item Внешние входы программы.
|
||||
\item Константы, используемые в программе.
|
||||
\item Функции без аргументов.
|
||||
\end{enumerate}
|
||||
|
||||
Термин «терминал» используется потому, что эти элементы соответствуют концевым (висячим) узлам в древовидных структурах и терминалам формальных грамматик. Терминал предоставляет численное значение, не требуя входных аргументов, то есть имеет нулевую арность. В классическом ГП на основе деревьев множество числовых констант выбирается для всей популяции и остается неизменным.
|
||||
|
||||
\subsubsection{Функциональное множество}
|
||||
|
||||
Функциональное множество состоит из операторов и различных функций. Оно может быть очень широким и включать типичные конструкции языков программирования, такие как:
|
||||
|
||||
\begin{itemize}
|
||||
\item Логические функции: AND, OR, NOT;
|
||||
\item Арифметические операции: $+$, $-$, $\times$, $\div$;
|
||||
\item Трансцендентные функции: $\sin$, $\cos$, $\tan$, $\log$;
|
||||
\item Операции присваивания: $a := 2$;
|
||||
\item Условные операторы: if-then-else, switch/case;
|
||||
\item Операторы переходов: go to, jump, call;
|
||||
\item Операторы циклов: while, repeat-until, for;
|
||||
\item Подпрограммы и пользовательские функции.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Виды представления программ. Древовидное представление}
|
||||
|
||||
Среди наиболее распространённых структур для представления особей (потенциальных решений) в современном генетическом программировании можно выделить:
|
||||
|
||||
\begin{enumerate}
|
||||
\item \textbf{Древовидное представление} — классический подход, где программы представляются в виде деревьев с операторами в узлах и терминалами в листьях
|
||||
|
||||
\item \textbf{Линейная структура} — программы записываются как последовательности инструкций, аналогично ассемблерному коду
|
||||
|
||||
\item \textbf{Графоподобная структура} — расширенное представление, допускающее множественные связи и переиспользование компонентов
|
||||
\end{enumerate}
|
||||
|
||||
Древовидная форма представления является классической для ГП. Программа представляется в виде дерева, где внутренние узлы — это функции из функционального множества, а листья (терминальные узлы) — это переменные и константы из терминального
|
||||
множества. Такая структура позволяет гибко работать с выражениями различной длины
|
||||
и сложности
|
||||
|
||||
\subsection{Инициализация древовидных структур}
|
||||
|
||||
Сложность древовидных структур оценивается через максимальную глубину дерева $D_m$ или общее количество узлов. Процесс инициализации древовидных структур основан на случайном выборе функциональных и терминальных символов при заданном ограничении максимальной глубины. Рассмотрим пример с терминальным множеством:
|
||||
|
||||
Существуют два основных метода инициализации:
|
||||
|
||||
\subsubsection*{Полный метод (full)}
|
||||
|
||||
На всех уровнях, кроме последнего, выбираются только функциональные символы. Терминальные символы размещаются исключительно на уровне максимальной глубины $D_m$. Это гарантирует создание сбалансированных деревьев регулярной структуры.
|
||||
|
||||
\subsubsection*{Растущий метод (grow)}
|
||||
На каждом шаге случайным образом выбирается либо функциональный, либо терминальный символ. Выбор терминала прекращает рост ветви, что приводит к формированию нерегулярных деревьев с различной глубиной листьев.
|
||||
|
||||
\subsection{Оператор кроссинговера на древовидных структурах}
|
||||
|
||||
Для древовидной формы представления программ в генетическом программировании применяются три основных типа операторов кроссинговера:
|
||||
|
||||
\begin{enumerate}[label=\alph*)]
|
||||
\item Узловой ОК
|
||||
\item Кроссинговер поддеревьев
|
||||
\item Смешанный
|
||||
\end{enumerate}
|
||||
|
||||
\subsubsection{Узловой оператор кроссинговера}
|
||||
|
||||
В узловом операторе кроссинговера выбираются два родителя (два дерева) и внутри них — узлы. Первый родитель называется доминантом, второй — рецессивом. Узлы могут различаться по типу, поэтому сначала необходимо проверить, что выбранные узлы взаимозаменяемы. Если типы не совпадают, выбирается другой узел во втором родителе, и проверка повторяется. После этого осуществляется обмен выбранных узлов между деревьями.
|
||||
|
||||
\subsubsection{Кроссинговер поддеревьев}
|
||||
|
||||
В кроссинговере поддеревьев не происходит обмен отдельными узлами, а определяется обмен поддеревьями. Он осуществляется следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Выбираются два родителя (\textit{один — доминантный, другой — рецессивный}). Необходимо убедиться, что выбранные узлы взаимозаменяемы, то есть принадлежат одному типу. В противном случае выбирается другой узел в рецессивном дереве.
|
||||
\item Производится обмен соответствующими поддеревьями.
|
||||
\item Далее вычисляется предполагаемый размер потомков. Если он не превышает установленный порог, то обмен ветвями запоминается.
|
||||
\end{enumerate}
|
||||
|
||||
При смешанном операторе кроссинговера для некоторых узлов выполняется узловой ОК, а для других - кроссинговер поддеревьев. В целом ОК выполняется следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Выбор точек скрещивания \( P_1, P_2 \) в обоих родителях
|
||||
\item Выбор типа кроссинговера с заданной вероятностью:
|
||||
\begin{itemize}
|
||||
\item Первый тип (обмен подграфами) с вероятностью \( P_G \)
|
||||
\item Второй тип (линейный обмен) с вероятностью \( 1 - P_G \)
|
||||
\end{itemize}
|
||||
\item Если выбран первый тип и размер потомка не превышает порог, выполняется кроссинговер подграфами
|
||||
\item Если выбран второй тип и размер потомка не превышает порог, выполняется линейный кроссинговер
|
||||
\end{enumerate}
|
||||
|
||||
\subsection{Мутационные операторы для древовидных структур}
|
||||
|
||||
В контексте древовидного представления программ применяются следующие мутационные операторы:
|
||||
|
||||
\begin{enumerate}[label=\alph*)]
|
||||
\item Мутация узлов (узловая)
|
||||
\item Мутация с усечением (усекающая)
|
||||
\item Мутация с ростом (растущая)
|
||||
\item Hoist-мутация
|
||||
\end{enumerate}
|
||||
|
||||
\textbf{Процедура узловой мутации} включает следующие шаги:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Случайный выбор целевого узла в дереве программы и идентификация его типа
|
||||
\item Случайный выбор заменяющего узла того же типа из соответствующего множества (функционального или терминального)
|
||||
\item Замена исходного узла на выбранный вариант
|
||||
\end{enumerate}
|
||||
|
||||
\textbf{Алгоритм усекающей мутации} реализуется следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Выбор узла, который будет подвергнут мутации
|
||||
\item Случайный выбор терминального символа из допустимого множества
|
||||
\item Удаление поддерева, корнем которого является выбранный узел
|
||||
\item Замена удаленного поддерева терминальным символом
|
||||
\end{enumerate}
|
||||
|
||||
\textbf{Алгоритм растущей мутации} реализуется следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Определение узла, подвергаемого мутации
|
||||
\item Если узел является терминальным, выбирается другой узел; для нетерминального узла производится удаление всех исходящих ветвей
|
||||
\item Вычисление размера и сложности оставшейся части дерева
|
||||
\item Генерация нового случайного поддерева, размер которого не превышает заданного порогового значения, и его размещение вместо удалённой части
|
||||
\end{enumerate}
|
||||
|
||||
\textbf{Алгоритм Hoist-мутации} предназначен для борьбы с избыточным ростом деревьев (bloat) и реализуется следующим образом:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Случайный выбор поддерева с функциональным узлом в корне
|
||||
\item Выбор случайного узла внутри этого поддерева (исключая корень выбранного поддерева)
|
||||
\item Замена исходного поддерева на поддерево, начинающееся с выбранного внутреннего узла
|
||||
\item В результате дерево становится короче, сохраняя при этом часть исходной структуры
|
||||
\end{enumerate}
|
||||
|
||||
Данная мутация всегда уменьшает размер дерева, что помогает контролировать сложность программ и предотвращает неконтролируемый рост деревьев в процессе эволюции.
|
||||
|
||||
\textbf{Комбинированная мутация.} В реализованном алгоритме используется стратегия комбинированной мутации, которая на каждом шаге случайно выбирает один из четырёх описанных операторов с заданными вероятностями:
|
||||
|
||||
\begin{itemize}
|
||||
\item Растущая мутация: $p = 0.40$
|
||||
\item Узловая мутация: $p = 0.30$
|
||||
\item Hoist-мутация: $p = 0.15$
|
||||
\item Усекающая мутация: $p = 0.15$
|
||||
\end{itemize}
|
||||
|
||||
Такой подход обеспечивает баланс между увеличением разнообразия популяции (растущая мутация), локальными изменениями (узловая мутация) и контролем размера деревьев (Hoist-мутация и усекающая мутация).
|
||||
|
||||
\subsection{Фитнес-функции в генетическом программировании}
|
||||
|
||||
В отличие от генетических алгоритмов, где фитнес-функция часто совпадает с исходной целевой функцией, в генетическом программировании фитнес-функция обычно измеряет степень соответствия между фактическими выходными значениями $y_i$ и целевыми значениями $d_i$. В качестве фитнес-функций часто используются метрики ошибок, такие как абсолютное отклонение или среднеквадратичная ошибка.
|
||||
|
||||
\newpage
|
||||
\section{Особенности реализации}
|
||||
|
||||
В рамках работы создана библиотека \texttt{gp} для генетического программирования с древовидным представлением программ. Реализация выполнена на языке Python с использованием NumPy для векторизованных вычислений.
|
||||
|
||||
\subsection{Примитивы и операции (primitive.py, ops.py)}
|
||||
|
||||
Базовый класс \texttt{Primitive} представляет атомарные элементы дерева программы:
|
||||
|
||||
\begin{lstlisting}
|
||||
@dataclass(frozen=True)
|
||||
class Primitive:
|
||||
name: str
|
||||
arity: int # арность: 0 для терминалов, >0 для операций
|
||||
operation_fn: OperationFn | None
|
||||
\end{lstlisting}
|
||||
|
||||
Реализованы конструкторы для создания терминалов и операций: \texttt{Var(name: str)}, \texttt{Const(name: str, val: Value)}, \texttt{Operation(name: str, arity: int, fn)}.
|
||||
|
||||
Модуль \texttt{ops.py} содержит набор безопасных векторизованных операций. Функция \texttt{make\_safe} оборачивает операции для обработки некорректных значений:
|
||||
|
||||
\begin{lstlisting}
|
||||
def make_safe(fn: Callable) -> Callable:
|
||||
def wrapped(args: Sequence[Value]) -> Value:
|
||||
with np.errstate(over="ignore", invalid="ignore",
|
||||
divide="ignore", under="ignore"):
|
||||
res = fn(args)
|
||||
res = np.nan_to_num(res, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
return np.clip(res, -1e6, 1e6)
|
||||
return wrapped
|
||||
\end{lstlisting}
|
||||
|
||||
Реализованы унарные операции (\texttt{NEG, SIN, COS, SQUARE, EXP}) и бинарные (\texttt{ADD, SUB, MUL, DIV, POW}). Для деления используется защита от деления на ноль, для возведения в степень -- ограничение показателя.
|
||||
|
||||
\subsection{Узлы дерева (node.py)}
|
||||
|
||||
Класс \texttt{Node} представляет узел дерева программы:
|
||||
|
||||
\begin{lstlisting}
|
||||
class Node:
|
||||
value: Primitive
|
||||
parent: Node | None
|
||||
children: list[Node]
|
||||
\end{lstlisting}
|
||||
|
||||
Реализованы методы для манипуляций с деревом: \texttt{add\_child}, \texttt{replace\_child}, \texttt{copy\_subtree}. Метод \texttt{list\_nodes} возвращает список всех узлов поддерева (обход в глубину). Для контроля размера реализован метод \texttt{prune}, который усекает дерево до заданной глубины, заменяя операции на случайные терминалы.
|
||||
|
||||
Вычисление программы выполняется методом \texttt{eval}, который рекурсивно вычисляет значения поддеревьев и применяет операцию узла:
|
||||
|
||||
\begin{lstlisting}
|
||||
def eval(self, context: Context) -> Value:
|
||||
return self.value.eval(
|
||||
[child.eval(context) for child in self.children],
|
||||
context
|
||||
)
|
||||
\end{lstlisting}
|
||||
|
||||
Для кроссовера реализована функция \texttt{swap\_subtrees(a: Node, b: Node)}, которая обменивает два поддерева, корректно обновляя ссылки на родителей.
|
||||
|
||||
\subsection{Хромосомы (chromosome.py)}
|
||||
|
||||
Класс \texttt{Chromosome} инкапсулирует дерево программы вместе с множествами терминалов и операций:
|
||||
|
||||
\begin{lstlisting}
|
||||
class Chromosome:
|
||||
terminals: Sequence[Primitive]
|
||||
operations: Sequence[Primitive]
|
||||
root: Node
|
||||
\end{lstlisting}
|
||||
|
||||
Реализованы два метода инициализации случайных деревьев:
|
||||
|
||||
\begin{itemize}
|
||||
\item \texttt{full\_init(terminals, operations, max\_depth)} -- полная инициализация, где на каждом уровне до максимальной глубины выбираются только операции, а на последнем -- только терминалы.
|
||||
\item \texttt{grow\_init(terminals, operations, max\_depth, terminal\_probability)} -- растущая инициализация с вероятностным выбором терминалов на каждом уровне, что создаёт деревья различной формы.
|
||||
\end{itemize}
|
||||
|
||||
Комбинация этих методов (\textit{ramped half-and-half}) реализована в функции \texttt{ramped\_initialization}, которая создаёт начальную популяцию из деревьев различных глубин, используя оба метода поровну.
|
||||
|
||||
\subsection{Кроссовер (crossovers.py)}
|
||||
|
||||
Реализован оператор кроссовера поддеревьев:
|
||||
|
||||
\begin{lstlisting}
|
||||
def crossover_subtree(parent1: Chromosome, parent2: Chromosome,
|
||||
max_depth: int) -> tuple[Chromosome, Chromosome]:
|
||||
\end{lstlisting}
|
||||
|
||||
Алгоритм выбирает случайные узлы в каждом родителе (кроме корня) и обменивает соответствующие поддеревья. Если глубина потомков превышает \texttt{max\_depth}, деревья усекаются методом \texttt{prune}.
|
||||
|
||||
\subsection{Мутации (mutations.py)}
|
||||
|
||||
Все мутации наследуются от базового класса \texttt{BaseMutation} с методом \texttt{mutate}. Реализованы четыре типа мутаций:
|
||||
|
||||
\begin{itemize}
|
||||
\item \texttt{NodeReplacementMutation} -- заменяет узел на другой той же арности
|
||||
\item \texttt{ShrinkMutation} -- заменяет случайную операцию на терминал (усечение)
|
||||
\item \texttt{GrowMutation} -- заменяет узел на случайное поддерево с контролем глубины
|
||||
\item \texttt{HoistMutation} -- заменяет поддерево на его случайную внутреннюю часть (уменьшает размер)
|
||||
\end{itemize}
|
||||
|
||||
Класс \texttt{CombinedMutation} позволяет комбинировать мутации с заданными вероятностями, случайно выбирая одну из них на каждом шаге.
|
||||
|
||||
\subsection{Фитнес-функции (fitness.py)}
|
||||
|
||||
Базовый класс \texttt{BaseFitness} определяет интерфейс для вычисления ошибки:
|
||||
|
||||
\begin{lstlisting}
|
||||
class BaseFitness(ABC):
|
||||
def __call__(self, chromosome: Chromosome) -> float:
|
||||
test_points = self.test_points_fn()
|
||||
context = {t: test_points[:, i]
|
||||
for i, t in enumerate(chromosome.terminals)}
|
||||
predicted = chromosome.root.eval(context)
|
||||
true_values = self.target_function(test_points)
|
||||
return self.fitness_fn(chromosome, predicted, true_values)
|
||||
\end{lstlisting}
|
||||
|
||||
Реализованы метрики ошибок: \texttt{MSEFitness} (среднеквадратичная), \texttt{RMSEFitness} (корень из MSE), \texttt{MAEFitness} (средняя абсолютная), \texttt{NRMSEFitness} (нормализованная RMSE). Класс \texttt{PenalizedFitness} добавляет штраф за размер и глубину дерева для борьбы с bloat.
|
||||
|
||||
\subsection{Селекция (selection.py)}
|
||||
|
||||
Реализованы три метода селекции:
|
||||
|
||||
\begin{itemize}
|
||||
\item \texttt{roulette\_selection} -- селекция рулеткой со сдвигом для обработки отрицательных значений
|
||||
\item \texttt{tournament\_selection(k)} -- турнирная селекция размера $k$
|
||||
\item \texttt{stochastic\_tournament\_selection(k, p\_best)} -- стохастическая турнирная с вероятностью выбора лучшего
|
||||
\end{itemize}
|
||||
|
||||
Для минимизации фитнес-функции используется инверсия знака при передаче фитнесов в селекцию.
|
||||
|
||||
\subsection{Генетический алгоритм (ga.py)}
|
||||
|
||||
Основная функция \texttt{genetic\_algorithm(config: GARunConfig)} реализует классический цикл ГА:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Вычисление фитнеса: \texttt{eval\_population(population, fitness\_func)}
|
||||
\item Сохранение элиты (если \texttt{config.elitism > 0})
|
||||
\item Селекция родителей: \texttt{config.selection\_fn(population, fitnesses)}
|
||||
\item Кроссовер с вероятностью $p_c$: попарный обмен поддеревьями
|
||||
\item Мутация с вероятностью $p_m$
|
||||
\item Замещение популяции с восстановлением элиты
|
||||
\end{enumerate}
|
||||
|
||||
Поддерживаются критерии остановки: по числу поколений, повторению лучшего результата, достижению порогового значения. История поколений сохраняется в виде списка объектов \texttt{Generation}.
|
||||
|
||||
Функция \texttt{save\_generation} использует библиотеку Graphviz для визуализации лучшего дерева поколения. Функция \texttt{plot\_fitness\_history} строит графики динамики лучших и средних значений фитнеса по поколениям и сохраняет их отдельно в \texttt{fitness\_best.png} и \texttt{fitness\_avg.png}.
|
||||
|
||||
\newpage
|
||||
\section{Результаты работы}
|
||||
|
||||
На Рис.~\ref{fig:gen1}--\ref{fig:lastgen} представлены результаты работы генетического алгоритма со следующими параметрами:
|
||||
\begin{itemize}
|
||||
\item $N = 400$ -- размер популяции.
|
||||
\item $10$ -- максимальная глубина дерева.
|
||||
\item $p_c = 0.85$ -- вероятность кроссинговера поддеревьев.
|
||||
\item $p_m = 0.15$ -- вероятность мутации, при этом использовалась комбинация различных вариантов:
|
||||
\begin{itemize}
|
||||
\item Растущая мутация: $p = 0.40$
|
||||
\item Узловая мутация: $p = 0.30$
|
||||
\item Hoist-мутация: $p = 0.15$
|
||||
\item Усекающая мутация: $p = 0.15$
|
||||
\end{itemize}
|
||||
\item $200$ -- максимальное количество поколений.
|
||||
\item $15$ -- количество "элитных" особей, переносимых без изменения в следующее поколение.
|
||||
\item $3$ -- размер турнира для селекции.
|
||||
\end{itemize}
|
||||
|
||||
На Рис.~\ref{fig:fitness_avg} и Рис.~\ref{fig:fitness_best} показаны графики изменения среднего и лучшего значения фитнеса по поколениям.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.95\linewidth]{img/results/fitness_avg.png}
|
||||
\caption{График среднего значения фитнеса по поколениям}
|
||||
\label{fig:fitness_avg}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.95\linewidth]{img/results/fitness_best.png}
|
||||
\caption{График лучшего значения фитнеса по поколениям}
|
||||
\label{fig:fitness_best}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.25\linewidth]{img/results/generation_001.png}
|
||||
\caption{Лучшая особь поколения №1}
|
||||
\label{fig:gen1}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.25\linewidth]{img/results/generation_010.png}
|
||||
\caption{Лучшая особь поколения №10}
|
||||
\label{fig:gen10}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.25\linewidth]{img/results/generation_020.png}
|
||||
\caption{Лучшая особь поколения №20}
|
||||
\label{fig:gen20}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.25\linewidth]{img/results/generation_030.png}
|
||||
\caption{Лучшая особь поколения №30}
|
||||
\label{fig:gen30}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{img/results/generation_040.png}
|
||||
\caption{Лучшая особь поколения №40}
|
||||
\label{fig:gen40}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_050.png}
|
||||
\caption{Лучшая особь поколения №50}
|
||||
\label{fig:gen50}
|
||||
\end{figure}
|
||||
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_100.png}
|
||||
\caption{Лучшая особь поколения №100}
|
||||
\label{fig:gen100}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_150.png}
|
||||
\caption{Лучшая особь поколения №150}
|
||||
\label{fig:gen300}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_200.png}
|
||||
\caption{Лучшая особь поколения №200}
|
||||
\label{fig:lastgen}
|
||||
\end{figure}
|
||||
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\subsection{Анализ результатов}
|
||||
|
||||
\subsubsection*{Сравнение полученных деревьев}
|
||||
|
||||
На Рис.~\ref{fig:original_tree} представлено исходное дерево, на Рис.~\ref{fig:best_tree} представлено лучшее дерево, найденное алгоритмом.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.9\linewidth]{img/original_tree.png}
|
||||
\caption{Дерево целевой функции}
|
||||
\label{fig:original_tree}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.9\linewidth]{img/best_tree.png}
|
||||
\caption{Лучшая особь, найденная алгоритмом}
|
||||
\label{fig:best_tree}
|
||||
\end{figure}
|
||||
|
||||
\subsubsection*{Сравнение полученных формул}
|
||||
|
||||
Перед сравнением, упростим исходную формулу, раскрыв знаки суммирования и перегруппировав слагаемые.
|
||||
|
||||
$$f(x) = \sum_{i=1}^{n} \sum_{j=1}^{i} x_j^2, \text{ для всех } j = 1, \ldots, n, \text{ при этом n }= 8.$$
|
||||
|
||||
$$
|
||||
f(x) = \underbrace{(x_1^2)
|
||||
+ (x_1^2 + x_2^2)
|
||||
+ \ldots
|
||||
+ (x_1^2 + x_2^2 + x_3^2 + x_4^2 + x_5^2 + x_6^2 + x_7^2 + x_8^2)}_{\text{ всего } n = 8 \text{ слагаемых}}
|
||||
$$
|
||||
|
||||
$$
|
||||
f(x) = 8 x_1^2 + 7 x_2^2 + 6 x_3^2 + 5 x_4^2 + 4 x_5^2 + 3 x_6^2 + 2 x_7^2 + x_8^2
|
||||
$$
|
||||
|
||||
В программе реализован метод преобразования особи (дереве) в строковую формулу. Вывод программы для лучшей особи представлен ниже:
|
||||
|
||||
\begin{lstlisting}[label={lst:}]
|
||||
(((((pow2(x3) + ((pow2(x1) + pow2(x2)) + pow2(x1))) + pow2(x6)) +
|
||||
((pow2(x2) + pow2(x2)) + ((sin(((x6 + x2) + sin(x6))) + ((pow2(x4) +
|
||||
pow2(x2)) + pow2(x4))) + (((pow2(x3) + pow2(x4)) + pow2(x7)) + (pow2(x6) +
|
||||
pow2(x4)))))) + (((pow2(x2) + ((pow2(x8) + pow2((x5 + x5))) + pow2(x3))) +
|
||||
pow2(x1)) + (pow2(x6) + pow2(x4)))) + (((((pow2(x3) + pow2(x3))
|
||||
+ ((pow2(x7) + pow2(x2)) + pow2(x1))) + pow2(x1)) + (pow2(x2) + ((pow2(x3) +
|
||||
pow2(x1)) + pow2(x1)))) + (sin(x2) + pow2(x1))))
|
||||
\end{lstlisting}
|
||||
|
||||
Программный метод автоматически обрамляет функции и переменные в скобки, чтобы правильно расставить приоритеты операций. Однако в данном случае они избыточны, поэтому их можно убрать:
|
||||
|
||||
\begin{lstlisting}[label={lst:}]
|
||||
pow2(x3) + pow2(x1) + pow2(x2) + pow2(x1) + pow2(x6) + pow2(x2) + pow2(x2) +
|
||||
sin(x6 + x2) + sin(x6) + pow2(x4) + pow2(x2) + pow2(x4) + pow2(x3) + pow2(x4) +
|
||||
pow2(x7) + pow2(x6) + pow2(x4) + pow2(x2) + pow2(x8) + pow2(x5 + x5) + pow2(x3) +
|
||||
pow2(x1) + pow2(x6) + pow2(x4) + pow2(x3) + pow2(x3) + pow2(x7) + pow2(x2) + pow2(x1) +
|
||||
pow2(x1) + pow2(x2) + pow2(x3) + pow2(x1) + pow2(x1) + sin(x2) + pow2(x1)
|
||||
\end{lstlisting}
|
||||
|
||||
Переставим слагаемые:
|
||||
|
||||
\begin{lstlisting}[label={lst:}]
|
||||
pow2(x1) + pow2(x1) + pow2(x1) + pow2(x1) + pow2(x1) + pow2(x1) + pow2(x1) + pow2(x1) +
|
||||
pow2(x2) + pow2(x2) + pow2(x2) + pow2(x2) + pow2(x2) + pow2(x2) + pow2(x2) +
|
||||
pow2(x3) + pow2(x3) + pow2(x3) + pow2(x3) + pow2(x3) + pow2(x3) +
|
||||
pow2(x4) + pow2(x4) + pow2(x4) + pow2(x4) + pow2(x4) +
|
||||
pow2(x5 + x5) +
|
||||
pow2(x6) + pow2(x6) + pow2(x6) +
|
||||
pow2(x7) + pow2(x7) +
|
||||
pow2(x8) +
|
||||
sin(x6 + x2) + sin(x6) + sin(x2)
|
||||
\end{lstlisting}
|
||||
|
||||
Заметим, что $(x_5 + x_5)^2 = (2x_5)^2 = 4x_5^2$, а также сгруппируем слагаемые, чтобы получить финальный вид формулы, найденной алгоритмом:
|
||||
|
||||
$$
|
||||
\hat{f}(x) = \textcolor{green!70!black}{8x_1^2 + 7x_2^2 + 6x_3^2 + 5x_4^2 + 4x_5^2 + 3x_6^2 + 2x_7^2 + x_8^2} + \textcolor{red!90!black}{sin(x_6 + x_2) + sin(x_6) + sin(x_2)}
|
||||
$$
|
||||
|
||||
Найденная формула полностью включает в себя целевую и содержит лишь несколько лишних слагаемых.
|
||||
|
||||
\newpage
|
||||
\section{Ответ на контрольный вопрос}
|
||||
|
||||
\textbf{Вопрос}: Опишите древовидное представление.
|
||||
|
||||
\textbf{Ответ}:
|
||||
|
||||
Древовидное представление — классический подход в генетическом программировании, где программы представляются в виде синтаксических деревьев. Внутренние узлы содержат функции из функционального множества (арифметические операции, математические функции), а листья — терминалы из терминального множества (переменные и константы). Вычисление происходит рекурсивно от листьев к корню. Сложность дерева оценивается через максимальную глубину $D_m$ (расстояние от корня до самого дальнего листа) или общее количество узлов.
|
||||
|
||||
Основные преимущества: естественное отображение синтаксической структуры математических выражений, гибкость в работе с выражениями различной длины и сложности, простота реализации генетических операторов (кроссовер поддеревьев, узловая мутация, растущая и усекающая мутации), автоматическое соблюдение синтаксической корректности при генерации и модификации программ. Инициализация выполняется полным методом (full) или растущим методом (grow), либо их комбинацией (ramped half-and-half).
|
||||
|
||||
|
||||
\newpage
|
||||
\section*{Заключение}
|
||||
\addcontentsline{toc}{section}{Заключение}
|
||||
|
||||
В ходе четвёртой лабораторной работы была успешно решена задача нахождения формулы целевой функции вида $f(x) = \sum_{i=1}^{n} \sum_{j=1}^{i} x_j^2$ с использованием генетического программирования:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Изучен теоретический материал о представлениях программ в генетическом программировании (древовидное, линейное, графовое) и специализированных операторах кроссинговера и мутации для древовидных структур;
|
||||
\item Создана программная библиотека \texttt{gp} на языке Python с реализацией древовидного представления хромосом, кроссовера поддеревьев, четырёх типов мутаций (узловая, усекающая, растущая, Hoist-мутация), турнирной селекции и безопасных векторизованных операций;
|
||||
\item Реализованы методы инициализации популяции (full, grow, ramped half-and-half), фитнес-функции на основе метрик ошибок (MSE, RMSE, MAE, NRMSE), механизм элитизма и визуализация деревьев с помощью Graphviz;
|
||||
\item Проведён эксперимент с популяцией из 400 особей на 10000 тестовых точках для 8 переменных. За 200 поколений (~5.9 минут) получено решение с MSE = 0.412 и RMSE = 0.642, полностью включающее целевую функцию с небольшими дополнительными слагаемыми.
|
||||
\end{enumerate}
|
||||
|
||||
|
||||
\newpage
|
||||
\section*{Список литературы}
|
||||
\addcontentsline{toc}{section}{Список литературы}
|
||||
|
||||
\vspace{-1.5cm}
|
||||
\begin{thebibliography}{0}
|
||||
\bibitem{vostrov}
|
||||
Методические указания по выполнению лабораторных работ к курсу «Генетические алгоритмы», 119 стр.
|
||||
\end{thebibliography}
|
||||
|
||||
\end{document}
|
||||
265
lab4/uv.lock
generated
Normal file
@@ -0,0 +1,265 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "contourpy"
|
||||
version = "1.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cycler"
|
||||
version = "0.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.60.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphviz"
|
||||
version = "0.21"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kiwisolver"
|
||||
version = "1.4.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lab4"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "graphviz" },
|
||||
{ name = "matplotlib" },
|
||||
{ name = "numpy" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "graphviz", specifier = ">=0.21" },
|
||||
{ name = "matplotlib", specifier = ">=3.10.7" },
|
||||
{ name = "numpy", specifier = ">=2.3.4" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib"
|
||||
version = "3.10.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "contourpy" },
|
||||
{ name = "cycler" },
|
||||
{ name = "fonttools" },
|
||||
{ name = "kiwisolver" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pyparsing" },
|
||||
{ name = "python-dateutil" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787, upload-time = "2025-10-09T00:27:23.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348, upload-time = "2025-10-09T00:27:24.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949, upload-time = "2025-10-09T00:27:26.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247, upload-time = "2025-10-09T00:27:28.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497, upload-time = "2025-10-09T00:27:30.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732, upload-time = "2025-10-09T00:27:32.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240, upload-time = "2025-10-09T00:27:33.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938, upload-time = "2025-10-09T00:27:35.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245, upload-time = "2025-10-09T00:27:37.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411, upload-time = "2025-10-09T00:27:39.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664, upload-time = "2025-10-09T00:27:41.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.2.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
3
lab5/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Attention!
|
||||
|
||||
lab5 is fully AI generated slop.
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
@@ -0,0 +1,3 @@
|
||||
# Attention!
|
||||
|
||||
lab6 is fully AI generated slop.
|
||||
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
@@ -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
@@ -0,0 +1,5 @@
|
||||
*
|
||||
!**/
|
||||
!.gitignore
|
||||
!report.tex
|
||||
!img/**/*.png
|
||||
BIN
lab6/report/img/aco_best_tour.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
lab6/report/img/aco_history.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
lab6/report/img/best_lab3.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
lab6/report/img/optimal_tour.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
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}
|
||||