Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 268c4cf4a1 | |||
| 740a7be984 | |||
| 2cf0693070 | |||
| 3436f94b61 | |||
| f79a6abf1b | |||
| ee79d6ad41 | |||
| 745cfea282 | |||
| b7f2234bff | |||
| 1e9e52341a | |||
| 7c54d08b71 | |||
| c15867f027 | |||
| 12276dc54c | |||
| 42387c70cb | |||
| 27f4a8f2d8 | |||
| 9c2e13e90b |
@@ -448,6 +448,7 @@
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 10$}
|
||||
\begin{tabularx}{\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
@@ -460,13 +461,13 @@
|
||||
\textbf{0.8} & 8.7 (126) & 8.3 (119) & 3.9 (57) & 7.9 (113)& 4.4 (61) \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\caption{Результаты для $N = 10$}
|
||||
\label{tab:pc_pm_results}
|
||||
\end{table}
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 25$}
|
||||
\begin{tabularx}{\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
@@ -479,13 +480,13 @@
|
||||
\textbf{0.8} & 9.2 (64) & 5.8 (41) & 2.5 (18) & 3.0 (22) & 11.2 (78) \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\caption{Результаты для $N = 25$}
|
||||
\label{tab:pc_pm_results2}
|
||||
\end{table}
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 50$}
|
||||
\begin{tabularx}{\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
@@ -498,12 +499,12 @@
|
||||
\textbf{0.8} & 10.8 (44) & 5.0 (21) & 6.1 (24) & 13.9 (56) & 36.5 (145) \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\caption{Результаты для $N = 50$}
|
||||
\label{tab:pc_pm_results3}
|
||||
\end{table}
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 100$}
|
||||
\begin{tabularx}{\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
@@ -516,7 +517,6 @@
|
||||
\textbf{0.8} & 16.4 (34) & 12.1 (25) & 31.4 (64) & 54.9 (114) & -- \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\caption{Результаты для $N = 100$}
|
||||
\label{tab:pc_pm_results4}
|
||||
\end{table}
|
||||
|
||||
|
||||
339
lab2/csv_to_tex.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
Скрипт для конвертации результатов экспериментов из 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) >= 6: # Pc + 5 значений 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, min(6, 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, min(6, 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"
|
||||
|
||||
latex_code = f""" \\begin{{table}}[h!]
|
||||
\\centering
|
||||
\\small
|
||||
\\caption{{Результаты для $N = {n}$}}
|
||||
\\begin{{tabularx}}{{\\linewidth}}{{l *{{5}}{{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(6, len(row))): # Максимум 5 колонок Pm
|
||||
value = format_value(row[i], best_time, best_fitness)
|
||||
latex_code += f" & {value}"
|
||||
|
||||
# Заполняем недостающие колонки если их меньше 5
|
||||
for i in range(len(row) - 1, 5):
|
||||
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()
|
||||
179
lab2/expirements.py
Normal file
@@ -0,0 +1,179 @@
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
import statistics
|
||||
|
||||
import numpy as np
|
||||
from gen import GARunConfig, genetic_algorithm
|
||||
from prettytable import PrettyTable
|
||||
|
||||
|
||||
def fitness_function(chromosome: np.ndarray) -> np.ndarray:
|
||||
return chromosome[0] ** 2 + 2 * chromosome[1] ** 2
|
||||
|
||||
|
||||
# Базовая папка для экспериментов
|
||||
BASE_DIR = "experiments"
|
||||
|
||||
# Параметры для экспериментов
|
||||
POPULATION_SIZES = [10, 25, 50, 100]
|
||||
PC_VALUES = [0.3, 0.4, 0.5, 0.6, 0.7, 0.8] # вероятности кроссинговера
|
||||
PM_VALUES = [0.001, 0.01, 0.05, 0.1, 0.2] # вероятности мутации
|
||||
SAVE_AVG_BEST_FITNESS = True
|
||||
|
||||
# Количество запусков для усреднения результатов
|
||||
NUM_RUNS = 1
|
||||
|
||||
# Базовые параметры (как в main.py)
|
||||
BASE_CONFIG = {
|
||||
"x_min": np.array([-5.12, -5.12]),
|
||||
"x_max": np.array([5.12, 5.12]),
|
||||
"fitness_func": fitness_function,
|
||||
"max_generations": 200,
|
||||
"seed": None, # None для случайности, т. к. всё усредняем
|
||||
"minimize": True,
|
||||
# "fitness_avg_threshold": 0.05, # критерий остановки
|
||||
# "max_best_repetitions": 10,
|
||||
"best_value_threshold": 0.005,
|
||||
# при включенном сохранении графиков на время смотреть бессмысленно
|
||||
# "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:.1f} ({avg_generations:.0f})"
|
||||
|
||||
if SAVE_AVG_BEST_FITNESS:
|
||||
cell_value += f" {avg_best_fitness:.5f}"
|
||||
|
||||
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()
|
||||
454
lab2/gen.py
Normal file
@@ -0,0 +1,454 @@
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from dataclasses import 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 Chromosome = NDArray[np.float64]
|
||||
type Population = list[Chromosome]
|
||||
type Fitnesses = NDArray[np.float64]
|
||||
type FitnessFn = Callable[[Chromosome], np.float64]
|
||||
type CrossoverFn = Callable[[Chromosome, Chromosome], tuple[Chromosome, Chromosome]]
|
||||
type MutationFn = Callable[[Chromosome], Chromosome]
|
||||
|
||||
|
||||
@dataclass
|
||||
class GARunConfig:
|
||||
x_min: Chromosome
|
||||
x_max: Chromosome
|
||||
fitness_func: FitnessFn
|
||||
pop_size: int # размер популяции
|
||||
pc: float # вероятность кроссинговера
|
||||
pm: float # вероятность мутации
|
||||
max_generations: int # максимальное количество поколений
|
||||
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 # логировать каждое поколение
|
||||
|
||||
|
||||
@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 initialize_population(
|
||||
pop_size: int, x_min: Chromosome, x_max: Chromosome
|
||||
) -> Population:
|
||||
"""Инициализирует популяцию случайными векторами из заданного диапазона."""
|
||||
return [np.random.uniform(x_min, x_max, x_min.shape) 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 arithmetical_crossover_fn(
|
||||
p1: Chromosome, p2: Chromosome, w: float = 0.5
|
||||
) -> tuple[Chromosome, Chromosome]:
|
||||
"""Арифметический кроссинговер."""
|
||||
h1 = w * p1 + (1 - w) * p2
|
||||
h2 = (1 - w) * p1 + w * p2
|
||||
return h1, h2
|
||||
|
||||
|
||||
def geometrical_crossover_fn(
|
||||
p1: Chromosome, p2: Chromosome, w: float = 0.5
|
||||
) -> tuple[Chromosome, Chromosome]:
|
||||
"""Геометрический кроссинговер."""
|
||||
h1 = np.power(p1, w) * np.power(p2, 1 - w)
|
||||
h2 = np.power(p2, w) * np.power(p1, 1 - w)
|
||||
return h1, h2
|
||||
|
||||
|
||||
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 build_random_mutation_fn(x_min: Chromosome, x_max: Chromosome) -> MutationFn:
|
||||
"""Создаёт функцию случайной мутации."""
|
||||
|
||||
def mutation_fn(chrom: Chromosome) -> Chromosome:
|
||||
chrom_new = chrom.copy()
|
||||
k = np.random.randint(0, chrom_new.shape[0])
|
||||
chrom_new[k] = np.random.uniform(x_min[k], x_max[k])
|
||||
return chrom_new
|
||||
|
||||
return mutation_fn
|
||||
|
||||
|
||||
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_fitness_surface(
|
||||
fitness_func: FitnessFn,
|
||||
x_min: Chromosome,
|
||||
x_max: Chromosome,
|
||||
ax: Axes3D,
|
||||
num_points: int = 100,
|
||||
):
|
||||
"""Рисует поверхность функции фитнеса в 3D."""
|
||||
assert (
|
||||
x_min.shape == x_max.shape == (2,)
|
||||
), "Рисовать графики можно только для функции от двух переменных"
|
||||
X = np.linspace(x_min[0], x_max[0], num_points)
|
||||
Y = np.linspace(x_min[1], x_max[1], num_points)
|
||||
X, Y = np.meshgrid(X, Y)
|
||||
|
||||
vectorized_fitness = np.vectorize(lambda x, y: fitness_func(np.array([x, y])))
|
||||
Z = vectorized_fitness(X, Y)
|
||||
|
||||
return ax.plot_surface(
|
||||
X, Y, Z, cmap="viridis", edgecolor="none", alpha=0.7, shade=False
|
||||
)
|
||||
|
||||
|
||||
def plot_fitness_contour(
|
||||
fitness_func: FitnessFn,
|
||||
x_min: Chromosome,
|
||||
x_max: Chromosome,
|
||||
ax: Axes,
|
||||
num_points: int = 100,
|
||||
) -> None:
|
||||
"""Рисует контурный график функции фитнеса в 2D."""
|
||||
X = np.linspace(x_min[0], x_max[0], num_points)
|
||||
Y = np.linspace(x_min[1], x_max[1], num_points)
|
||||
X, Y = np.meshgrid(X, Y)
|
||||
|
||||
vectorized_fitness = np.vectorize(lambda x, y: fitness_func(np.array([x, y])))
|
||||
Z = vectorized_fitness(X, Y)
|
||||
|
||||
# Рисуем контуры
|
||||
# X и Y поменяны местами для единообразия с 3D графиками, там ось Y изображена
|
||||
# горизонтально из-за особенностей функции в моём варианте
|
||||
contourf = ax.contourf(Y, X, Z, levels=20, cmap="viridis", alpha=0.7)
|
||||
ax.set_xlabel("Y")
|
||||
ax.set_ylabel("X")
|
||||
|
||||
# Добавляем цветовую шкалу
|
||||
plt.colorbar(contourf, ax=ax, shrink=0.5)
|
||||
|
||||
# По умолчанию matplotlib пытается растянуть график по оси Y, тут мы это отключаем
|
||||
ax.set_aspect("equal")
|
||||
|
||||
|
||||
def save_generation(
|
||||
generation: Generation, history: list[Generation], config: GARunConfig
|
||||
) -> None:
|
||||
"""Сохраняем графики поколения.
|
||||
|
||||
Функция не самая универсальная, тут есть хардкод, однако для большинства вариантов
|
||||
должна работать и так.
|
||||
"""
|
||||
assert (
|
||||
config.x_min.shape == config.x_max.shape == (2,)
|
||||
), "Рисовать графики можно только для функции от двух переменных"
|
||||
os.makedirs(config.results_dir, exist_ok=True)
|
||||
|
||||
fig = plt.figure(figsize=(21, 7))
|
||||
fig.suptitle(
|
||||
f"Поколение #{generation.number}. "
|
||||
f"Лучшая особь: {generation.best_fitness:.4f}. "
|
||||
f"Среднее значение: {np.mean(generation.fitnesses):.4f}",
|
||||
fontsize=14,
|
||||
y=0.85,
|
||||
)
|
||||
|
||||
# Контурный график (как вид сверху)
|
||||
ax1 = fig.add_subplot(1, 3, 1)
|
||||
plot_fitness_contour(config.fitness_func, config.x_min, config.x_max, ax1)
|
||||
|
||||
# Популяция на контурном графике
|
||||
arr = np.array(generation.population)
|
||||
# Координаты специально поменяны местами (см. plot_fitness_contour)
|
||||
ax1.scatter(
|
||||
arr[:, 1],
|
||||
arr[:, 0],
|
||||
c="red",
|
||||
marker="o",
|
||||
alpha=0.9,
|
||||
s=20,
|
||||
)
|
||||
|
||||
# Подпись под первым графиком
|
||||
ax1.text(
|
||||
0.5,
|
||||
-0.3,
|
||||
"(a)",
|
||||
transform=ax1.transAxes,
|
||||
ha="center",
|
||||
fontsize=16,
|
||||
)
|
||||
|
||||
# 3D графики с разных ракурсов
|
||||
views_3d = [
|
||||
# (elev, azim)
|
||||
(50, 0),
|
||||
(50, 15),
|
||||
]
|
||||
|
||||
for i, (elev, azim) in enumerate(views_3d):
|
||||
ax = fig.add_subplot(1, 3, i + 2, projection="3d", computed_zorder=False)
|
||||
|
||||
plot_fitness_surface(config.fitness_func, config.x_min, config.x_max, ax)
|
||||
|
||||
ax.set_xlabel("X")
|
||||
ax.set_ylabel("Y")
|
||||
ax.set_zlabel("f(X, Y)")
|
||||
|
||||
ax.scatter(
|
||||
arr[:, 0],
|
||||
arr[:, 1],
|
||||
generation.fitnesses + 1, # type: ignore
|
||||
c="red",
|
||||
s=10,
|
||||
marker="o",
|
||||
alpha=0.9,
|
||||
)
|
||||
|
||||
# Устанавливаем угол обзора
|
||||
ax.view_init(elev=elev, azim=azim)
|
||||
|
||||
# Подпись под 3D графиками
|
||||
label = chr(ord("b") + i) # 'b' для i=0, 'c' для i=1
|
||||
ax.text2D(
|
||||
0.5,
|
||||
-0.15,
|
||||
f"({label})",
|
||||
transform=ax.transAxes,
|
||||
ha="center",
|
||||
fontsize=16,
|
||||
)
|
||||
|
||||
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")
|
||||
# Можно раскомментировать, чтобы подобрать более удачные ракурсы
|
||||
# в интерактивном режиме
|
||||
# fig.show()
|
||||
# plt.pause(1000)
|
||||
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 = initialize_population(config.pop_size, config.x_min, config.x_max)
|
||||
|
||||
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)
|
||||
|
||||
# Находим лучшую особь в поколении
|
||||
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)
|
||||
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, arithmetical_crossover_fn)
|
||||
|
||||
# мутация
|
||||
next_population = mutation(
|
||||
next_population,
|
||||
config.pm,
|
||||
build_random_mutation_fn(config.x_min, config.x_max),
|
||||
)
|
||||
|
||||
population = next_population[: config.pop_size]
|
||||
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,
|
||||
)
|
||||
31
lab2/main.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from gen import GARunConfig, genetic_algorithm
|
||||
|
||||
|
||||
def fitness_function(chromosome: np.ndarray) -> np.float64:
|
||||
return chromosome[0] ** 2 + 2 * chromosome[1] ** 2
|
||||
|
||||
|
||||
config = GARunConfig(
|
||||
x_min=np.array([-5.12, -5.12]),
|
||||
x_max=np.array([5.12, 5.12]),
|
||||
fitness_func=fitness_function,
|
||||
pop_size=25,
|
||||
pc=0.5,
|
||||
pm=0.01,
|
||||
max_generations=200,
|
||||
max_best_repetitions=10,
|
||||
minimize=True,
|
||||
seed=17,
|
||||
save_generations=[1, 2, 3, 5, 7, 9, 10, 15, 19],
|
||||
log_every_generation=True,
|
||||
)
|
||||
|
||||
result = genetic_algorithm(config)
|
||||
|
||||
# Выводим результаты
|
||||
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} мс")
|
||||
6
lab2/report/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
*
|
||||
|
||||
!**/
|
||||
!.gitignore
|
||||
!report.tex
|
||||
!img/**/*.png
|
||||
BIN
lab2/report/img/alg.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
lab2/report/img/arithmetic_crossover.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
lab2/report/img/blx_crossover.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
lab2/report/img/geometric_crossover.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
lab2/report/img/results/generation_001.png
Normal file
|
After Width: | Height: | Size: 560 KiB |
BIN
lab2/report/img/results/generation_002.png
Normal file
|
After Width: | Height: | Size: 554 KiB |
BIN
lab2/report/img/results/generation_003.png
Normal file
|
After Width: | Height: | Size: 552 KiB |
BIN
lab2/report/img/results/generation_005.png
Normal file
|
After Width: | Height: | Size: 550 KiB |
BIN
lab2/report/img/results/generation_007.png
Normal file
|
After Width: | Height: | Size: 550 KiB |
BIN
lab2/report/img/results/generation_009.png
Normal file
|
After Width: | Height: | Size: 545 KiB |
BIN
lab2/report/img/results/generation_010.png
Normal file
|
After Width: | Height: | Size: 544 KiB |
BIN
lab2/report/img/results/generation_015.png
Normal file
|
After Width: | Height: | Size: 542 KiB |
BIN
lab2/report/img/results/generation_019.png
Normal file
|
After Width: | Height: | Size: 542 KiB |
BIN
lab2/report/img/sbx_crossover.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
747
lab2/report/report.tex
Normal file
@@ -0,0 +1,747 @@
|
||||
\documentclass[a4paper, final]{article}
|
||||
%\usepackage{literat} % Нормальные шрифты
|
||||
\usepackage[14pt]{extsizes} % для того чтобы задать нестандартный 14-ый размер шрифта
|
||||
\usepackage{tabularx}
|
||||
\usepackage{booktabs}
|
||||
\usepackage[T2A]{fontenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[russian]{babel}
|
||||
\usepackage{amsmath}
|
||||
\usepackage[left=25mm, top=20mm, right=20mm, bottom=20mm, footskip=10mm]{geometry}
|
||||
\usepackage{ragged2e} %для растягивания по ширине
|
||||
\usepackage{setspace} %для межстрочно го интервала
|
||||
\usepackage{moreverb} %для работы с листингами
|
||||
\usepackage{indentfirst} % для абзацного отступа
|
||||
\usepackage{moreverb} %для печати в листинге исходного кода программ
|
||||
\usepackage{pdfpages} %для вставки других pdf файлов
|
||||
\usepackage{tikz}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{afterpage}
|
||||
\usepackage{longtable}
|
||||
\usepackage{float}
|
||||
\usepackage{xcolor}
|
||||
|
||||
|
||||
|
||||
|
||||
% \usepackage[paper=A4,DIV=12]{typearea}
|
||||
\usepackage{pdflscape}
|
||||
% \usepackage{lscape}
|
||||
|
||||
\usepackage{array}
|
||||
\usepackage{multirow}
|
||||
|
||||
\renewcommand\verbatimtabsize{4\relax}
|
||||
\renewcommand\listingoffset{0.2em} %отступ от номеров строк в листинге
|
||||
\renewcommand{\arraystretch}{1.4} % изменяю высоту строки в таблице
|
||||
\usepackage[font=small, singlelinecheck=false, justification=centering, format=plain, labelsep=period]{caption} %для настройки заголовка таблицы
|
||||
\usepackage{listings} %листинги
|
||||
\usepackage{xcolor} % цвета
|
||||
\usepackage{hyperref}% для гиперссылок
|
||||
\usepackage{enumitem} %для перечислений
|
||||
|
||||
\newcommand{\specialcell}[2][l]{\begin{tabular}[#1]{@{}l@{}}#2\end{tabular}}
|
||||
|
||||
|
||||
\setlist[enumerate,itemize]{leftmargin=1.2cm} %отступ в перечислениях
|
||||
|
||||
\hypersetup{colorlinks,
|
||||
allcolors=[RGB]{010 090 200}} %красивые гиперссылки (не красные)
|
||||
|
||||
% подгружаемые языки — подробнее в документации listings (это всё для листингов)
|
||||
\lstloadlanguages{ SQL}
|
||||
% включаем кириллицу и добавляем кое−какие опции
|
||||
\lstset{tabsize=2,
|
||||
breaklines,
|
||||
basicstyle=\footnotesize,
|
||||
columns=fullflexible,
|
||||
flexiblecolumns,
|
||||
numbers=left,
|
||||
numberstyle={\footnotesize},
|
||||
keywordstyle=\color{blue},
|
||||
inputencoding=cp1251,
|
||||
extendedchars=true
|
||||
}
|
||||
\lstdefinelanguage{MyC}{
|
||||
language=SQL,
|
||||
% ndkeywordstyle=\color{darkgray}\bfseries,
|
||||
% identifierstyle=\color{black},
|
||||
% morecomment=[n]{/**}{*/},
|
||||
% commentstyle=\color{blue}\ttfamily,
|
||||
% stringstyle=\color{red}\ttfamily,
|
||||
% morestring=[b]",
|
||||
% showstringspaces=false,
|
||||
% morecomment=[l][\color{gray}]{//},
|
||||
keepspaces=true,
|
||||
escapechar=\%,
|
||||
texcl=true
|
||||
}
|
||||
|
||||
\textheight=24cm % высота текста
|
||||
\textwidth=16cm % ширина текста
|
||||
\oddsidemargin=0pt % отступ от левого края
|
||||
\topmargin=-1.5cm % отступ от верхнего края
|
||||
\parindent=24pt % абзацный отступ
|
||||
\parskip=5pt % интервал между абзацами
|
||||
\tolerance=2000 % терпимость к "жидким" строкам
|
||||
\flushbottom % выравнивание высоты страниц
|
||||
|
||||
|
||||
% Настройка листингов
|
||||
\lstset{
|
||||
language=python,
|
||||
extendedchars=\true,
|
||||
inputencoding=utf8,
|
||||
keepspaces=true,
|
||||
% captionpos=b, % подписи листингов снизу
|
||||
}
|
||||
|
||||
\begin{document} % начало документа
|
||||
|
||||
|
||||
|
||||
% НАЧАЛО ТИТУЛЬНОГО ЛИСТА
|
||||
\begin{center}
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\normalsize{МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ\\
|
||||
федеральное государственное автономное образовательное учреждение высшего образования «Санкт-Петербургский политехнический университет Петра Великого»\\[10pt]}
|
||||
\normalsize{Институт компьютерных наук и кибербезопасности}\\[10pt]
|
||||
\normalsize{Высшая школа технологий искусственного интеллекта}\\[10pt]
|
||||
\normalsize{Направление: 02.03.01 <<Математика и компьютерные науки>>}\\
|
||||
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\large{Лабораторная работа №2}\\
|
||||
\large{по дисциплине}\\
|
||||
\large{<<Генетические алгоритмы>>}\\
|
||||
\large{Вариант 18}\\
|
||||
|
||||
% \hfill \break
|
||||
\hfill \break
|
||||
\end{center}
|
||||
|
||||
\small{
|
||||
\begin{tabular}{lrrl}
|
||||
\!\!\!Студент, & \hspace{2cm} & & \\
|
||||
\!\!\!группы 5130201/20101 & \hspace{2cm} & \underline{\hspace{3cm}} &Тищенко А. А. \\\\
|
||||
\!\!\!Преподаватель & \hspace{2cm} & \underline{\hspace{3cm}} & Большаков А. А. \\\\
|
||||
&&\hspace{4cm}
|
||||
\end{tabular}
|
||||
\begin{flushright}
|
||||
<<\underline{\hspace{1cm}}>>\underline{\hspace{2.5cm}} 2025г.
|
||||
\end{flushright}
|
||||
}
|
||||
|
||||
\hfill \break
|
||||
% \hfill \break
|
||||
\begin{center} \small{Санкт-Петербург, 2025} \end{center}
|
||||
\thispagestyle{empty} % выключаем отображение номера для этой страницы
|
||||
|
||||
% КОНЕЦ ТИТУЛЬНОГО ЛИСТА
|
||||
\newpage
|
||||
|
||||
\tableofcontents
|
||||
|
||||
\newpage
|
||||
\section {Постановка задачи}
|
||||
В данной работе были поставлены следующие задачи:
|
||||
|
||||
\begin{itemize}
|
||||
\item Изучить теоретический материал;
|
||||
\item Ознакомиться с вариантами кодирования хромосомы;
|
||||
\item Рассмотреть способы выполнения операторов репродукции,
|
||||
кроссинговера и мутации;
|
||||
\item Выполнить индивидуальное задание на любом языке высокого
|
||||
уровня
|
||||
\end{itemize}
|
||||
|
||||
\textbf{Индивидуальное задание вариант 18:}
|
||||
|
||||
\textbf{Дано:} Функция Axis parallel hyper-ellipsoid function.
|
||||
|
||||
Общая формула для n-мерного случая:
|
||||
$$f(\mathbf{x}) = \sum_{i=1}^{n} i \cdot x_i^2$$
|
||||
где $\mathbf{x} = (x_1, x_2, \ldots, x_n)$, область определения $x_i \in [-5.12, 5.12]$ для всех $i = 1, \ldots, n$.
|
||||
|
||||
Для двумерного случая (n=2):
|
||||
$$f(x, y) = 1 \cdot x^2 + 2 \cdot y^2 = x^2 + 2y^2$$
|
||||
область нахождения решения $x \in [-5.12, 5.12], y \in [-5.12, 5.12]$.
|
||||
|
||||
Глобальный минимум: $f(\mathbf{x}) = 0$ в точке $x_i = 0$ для всех $i = 1, \ldots, n$. Для двумерного случая: $\min f(x, y) = f(0, 0) = 0$.
|
||||
|
||||
\vspace{0.3cm}
|
||||
\textbf{Требуется:}
|
||||
|
||||
\begin{enumerate}
|
||||
\item Создать программу, использующую генетический алгоритм для нахождения минимума данной функции;
|
||||
\item Для n=2 вывести на экран график функции с указанием найденного экстремума и точек популяции. Предусмотреть возможность пошагового просмотра процесса поиска решения;
|
||||
\item Исследовать зависимость времени поиска, числа поколений (генераций), точности нахождения решения от основных параметров генетического алгоритма: числа особей в популяции, вероятности кроссинговера и мутации;
|
||||
\item Повторить процесс поиска решения для n=3, сравнить результаты и скорость работы программы.
|
||||
\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)$, определяющая решение. Может быть представлена как двоичная
|
||||
последовательность (где $s_i = 0, 1$) или как вектор вещественных чисел (real-coded представление).
|
||||
|
||||
\textbf{Локус} -- местоположение (позиция, номер бита) данного гена в хромосоме.
|
||||
|
||||
\textbf{Аллель} -- значение, которое принимает данный ген (например, 0 или 1).
|
||||
|
||||
\textbf{Особь} -- одно потенциальное решение задачи (представляемое хромосомой).
|
||||
|
||||
\textbf{Популяция} -- множество особей (хромосом), представляющих потенциальные решения.
|
||||
|
||||
\textbf{Поколение} -- текущая популяция ГА на данной итерации алгоритма.
|
||||
|
||||
\textbf{Генотип} -- набор хромосом данной особи. В популяции могут использоваться как отдельные
|
||||
хромосомы, так и целые генотипы.
|
||||
|
||||
\textbf{Генофонд} -- множество всех возможных генотипов.
|
||||
|
||||
\textbf{Фенотип} -- набор значений, соответствующий данному генотипу. Это декодированное множество
|
||||
параметров задачи (например, десятичное значение $x$, соответствующее двоичному коду).
|
||||
|
||||
\textbf{Размер популяции $N$} -- число особей в популяции.
|
||||
|
||||
\textbf{Число поколений} -- количество итераций, в течение которых производится поиск.
|
||||
|
||||
\textbf{Селекция} -- совокупность правил, определяющих выживание особей на основе значений целевой функции.
|
||||
|
||||
\textbf{Эволюция популяции} -- чередование поколений, в которых хромосомы изменяют свои признаки,
|
||||
чтобы каждая новая популяция лучше приспосабливалась к среде.
|
||||
|
||||
\textbf{Фитнесс-функция} -- функция полезности, определяющая меру приспособленности особи.
|
||||
В задачах оптимизации она совпадает с целевой функцией или описывает близость к оптимальному решению.
|
||||
|
||||
\subsection{Генетические операторы}
|
||||
|
||||
\subsubsection{Оператор репродукции}
|
||||
|
||||
Репродукция -- процесс копирования хромосом в промежуточную популяцию для дальнейшего
|
||||
``размножения'' в соответствии со значениями фитнесс-функции. В данной работе рассматривается метод колеса рулетки. Каждой хромосоме соответствует сектор, пропорциональный значению фитнесс-функции.
|
||||
Хромосомы с большим значением имеют больше шансов попасть в следующее поколение.
|
||||
|
||||
\subsubsection{Операторы кроссинговера для real-coded алгоритмов}
|
||||
|
||||
Оператор скрещивания непрерывного ГА (кроссовер) порождает одного или нескольких потомков от двух хромосом. Требуется из двух векторов вещественных чисел получить новые векторы по определённым законам. Большинство real-coded алгоритмов генерируют новые векторы в окрестности родительских пар.
|
||||
|
||||
Пусть $C_1=(c_{11},c_{21},\ldots,c_{n1})$ и $C_2=(c_{12},c_{22},\ldots,c_{n2})$ -- две хромосомы, выбранные оператором селекции для скрещивания.
|
||||
|
||||
\textbf{Арифметический кроссовер (arithmetical crossover):} создаются два потомка $H_1=(h_{11},\ldots,h_{n1})$, $H_2=(h_{12},\ldots,h_{n2})$, где:
|
||||
$$h_{k1}=w \cdot c_{k1}+(1-w) \cdot c_{k2}$$
|
||||
$$h_{k2}=w \cdot c_{k2}+(1-w) \cdot c_{k1}$$
|
||||
где $k=1,\ldots,n$, $w$ -- весовой коэффициент из интервала $[0;1]$.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{img/arithmetic_crossover.png}
|
||||
\caption{Арифметический кроссовер}
|
||||
\label{fig:arithmetic_crossover}
|
||||
\end{figure}
|
||||
|
||||
\textbf{Геометрический кроссовер (geometrical crossover):} создаются два потомка $H_1=(h_{11},\ldots,h_{n1})$, $H_2=(h_{12},\ldots,h_{n2})$, где:
|
||||
$$h_{k1}=(c_{k1})^w \cdot (c_{k2})^{(1-w)}$$
|
||||
$$h_{k2}=(c_{k2})^w \cdot (c_{k1})^{(1-w)}$$
|
||||
где $w$ -- случайное число из интервала $[0;1]$.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{img/geometric_crossover.png}
|
||||
\caption{Геометрический кроссовер}
|
||||
\label{fig:geometric_crossover}
|
||||
\end{figure}
|
||||
|
||||
\textbf{Смешанный кроссовер (BLX-alpha crossover):} генерируется один потомок $H=(h_1,\ldots,h_k,\ldots,h_n)$, где $h_k$ -- случайное число из интервала $[c_{min}-I \cdot \alpha, c_{max}+I \cdot \alpha]$, $c_{min}=\min(c_{k1},c_{k2})$, $c_{max}=\max(c_{k1},c_{k2})$, $I=c_{max}-c_{min}$.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{img/blx_crossover.png}
|
||||
\caption{Смешанный кроссовер}
|
||||
\label{fig:blx_crossover}
|
||||
\end{figure}
|
||||
|
||||
\textbf{SBX кроссовер (Simulated Binary Crossover):} кроссовер, имитирующий двоичный, разработанный в 1995 году исследовательской группой под руководством K. Deb'а. Моделирует принципы работы двоичного оператора скрещивания, сохраняя важное свойство -- среднее значение функции приспособленности остаётся неизменным у родителей и их потомков.
|
||||
|
||||
Создаются два потомка $H_k=(h_{1k}, \ldots, h_{jk}, \ldots, h_{nk})$, $k=1,2$, где:
|
||||
$$h_{j1} = 0.5[(1+\beta_k)c_{j1} + (1-\beta_k)c_{j2}]$$
|
||||
$$h_{j2} = 0.5[(1-\beta_k)c_{j1} + (1+\beta_k)c_{j2}]$$
|
||||
|
||||
где $\beta_k \geq 0$ -- число, полученное по формуле:
|
||||
$$\beta_k = \begin{cases}
|
||||
(2u)^{\frac{1}{n+1}}, & \text{при } u \leq 0.5 \\
|
||||
\left(\frac{1}{2(1-u)}\right)^{\frac{1}{n+1}}, & \text{при } u > 0.5
|
||||
\end{cases}$$
|
||||
|
||||
где $u \in (0,1)$ -- случайное число, распределённое по равномерному закону, $n \in [2,5]$ -- параметр кроссовера. Увеличение $n$ повышает вероятность появления потомка в окрестности родителей.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/sbx_crossover.png}
|
||||
\caption{SBX кроссовер}
|
||||
\label{fig:sbx_crossover}
|
||||
\end{figure}
|
||||
|
||||
\subsubsection{Операторы мутации для real-coded алгоритмов}
|
||||
|
||||
В качестве оператора мутации наибольшее распространение получили: случайная и неравномерная мутация.
|
||||
|
||||
\textbf{Случайная мутация (random mutation):} ген, подлежащий изменению, принимает случайное значение из интервала своего изменения.
|
||||
|
||||
\textbf{Неравномерная мутация (non-uniform mutation):} из особи случайно выбирается точка $c_k$ с разрешёнными пределами изменения $[c_{kl}, c_{kr}]$. Точка меняется на:
|
||||
$$c_k' = \begin{cases}
|
||||
c_k + \Delta(t, c_{kr} - c_k), & \text{при } a = 1 \\
|
||||
c_k - \Delta(t, c_k - c_{kl}), & \text{при } a = 0
|
||||
\end{cases}$$
|
||||
|
||||
где $a$ -- случайно выбранное направление изменения, $\Delta(t, y)$ -- функция, возвращающая случайную величину в пределах $[0, y]$ таким образом, что при увеличении $t$ среднее возвращаемое значение уменьшается:
|
||||
$$\Delta(t, y) = y \cdot r \cdot \left(1 - \frac{t}{T}\right)^b$$
|
||||
|
||||
где $r$ -- случайная величина на интервале $[0, 1]$, $t$ -- текущая эпоха работы генетического алгоритма, $T$ -- общее разрешённое число эпох алгоритма, $b$ -- задаваемый пользователем параметр, определяющий степень зависимости от числа эпох.
|
||||
|
||||
\newpage
|
||||
\section{Особенности реализации}
|
||||
В рамках работы создана мини-библиотека \texttt{gen.py} для экспериментов с real-coded
|
||||
генетическим алгоритмом для многомерных функций. Второй модуль
|
||||
\texttt{expirements.py} организует серийные эксперименты (перебор параметров,
|
||||
форматирование и сохранение результатов).
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Кодирование особей}: каждая хромосома представлена как \texttt{np.ndarray} вещественных чисел (\texttt{Chromosome = NDArray[np.float64]}). Длина хромосомы соответствует размерности задачи оптимизации. Популяция -- список хромосом (\texttt{Population = list[Chromosome]}). Инициализация случайными векторами в заданном диапазоне:
|
||||
\begin{itemize}
|
||||
\item \texttt{initialize\_population(pop\_size: int, x\_min: Chromosome, x\_max:}\\ \texttt{Chromosome) -> 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{Кроссинговер}: реализованы арифметический и геометрический кроссоверы для real-coded алгоритмов. Кроссинговер выполняется попарно по перемешанной популяции с вероятностью $p_c$. Функции:
|
||||
\begin{itemize}
|
||||
\item \texttt{arithmetical\_crossover\_fn(p1: Chromosome, p2: Chromosome, w: float) -> tuple[Chromosome, Chromosome]}
|
||||
\item \texttt{geometrical\_crossover\_fn(p1: Chromosome, p2: Chromosome, w: float) -> tuple[Chromosome, Chromosome]}
|
||||
\item \texttt{crossover(population: Population, pc: float, crossover\_fn: CrossoverFn) -> Population}
|
||||
\end{itemize}
|
||||
\item \textbf{Мутация}: случайная мутация -- с вероятностью $p_m$ на хромосому изменяется один случайно выбранный ген на случайное значение из допустимого диапазона. Функции:
|
||||
\begin{itemize}
|
||||
\item \texttt{build\_random\_mutation\_fn(x\_min: Chromosome, x\_max: Chromosome) -> MutationFn}
|
||||
\item \texttt{mutation(population: Population, pm: float, mutation\_fn: MutationFn) -> Population}
|
||||
\end{itemize}
|
||||
|
||||
\item \textbf{Критерий остановки}: поддерживается критерий по среднему значению фитнесс-функции в популяции и максимальному количеству поколений. Хранится история всех поколений. Проверка выполняется в функции:
|
||||
|
||||
\texttt{genetic\_algorithm(config: GARunConfig) -> GARunResult}.
|
||||
\item \textbf{Визуализация}: для двумерных функций реализованы 3D-графики поверхности и 2D-контурные графики с отображением популяций. Функции:
|
||||
\begin{itemize}
|
||||
\item \texttt{plot\_fitness\_surface(fitness\_func: FitnessFn, x\_min: Chromosome, x\_max: Chromosome, ax: Axes3D)}
|
||||
\item \texttt{plot\_fitness\_contour(fitness\_func: FitnessFn, x\_min: Chromosome, x\_max: Chromosome, ax: Axes)}
|
||||
\item \texttt{save\_generation(generation: Generation, history: list[Generation], config: GARunConfig)}
|
||||
\end{itemize}
|
||||
\item \textbf{Измерение времени}: длительность вычислений возвращается в миллисекундах как часть \texttt{GARunResult.time\_ms}.
|
||||
\item \textbf{Файловая организация}: результаты экспериментов сохраняются иерархически в структуре \texttt{experiments/N/} с таблицами результатов в формате CSV. Задействованные функции:
|
||||
\begin{itemize}
|
||||
\item \texttt{clear\_results\_directory(results\_dir: str) -> None}
|
||||
\item \texttt{run\_single\_experiment(pop\_size: int, pc: float, pm: float) -> tuple[float, float, float, float]}
|
||||
\item \texttt{run\_experiments\_for\_population(pop\_size: int) -> PrettyTable}
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
|
||||
В модуле \texttt{expirements.py} задаётся целевая функция axis parallel hyper-ellipsoid: $f(x, y) = x^2 + 2y^2$ и параметры экспериментов.
|
||||
Серийные запуски и сохранение результатов реализованы в функциях \texttt{run\_single\_experiment}, \texttt{run\_experiments\_for\_population} и \texttt{main}.
|
||||
|
||||
\newpage
|
||||
\section{Результаты работы}
|
||||
На Рис.~\ref{fig:gen1}--\ref{fig:lastgen} представлены результаты работы генетического алгоритма со следующими параметрами:
|
||||
\begin{itemize}
|
||||
\item $N = 25$ -- размер популяции.
|
||||
\item $p_c = 0.5$ -- вероятность кроссинговера.
|
||||
\item $p_m = 0.01$ -- вероятность мутации.
|
||||
\item Алгоритм останавливался, если лучшее значение фитнеса не изменялось $10$ поколений подряд.
|
||||
\item Использован арифметический кроссовер для real-coded хромосом.
|
||||
\end{itemize}
|
||||
|
||||
Популяция постепенно консолидируется вокруг глобального минимума в точке $(0, 0)$. Лучшая особь была найдена на поколнении №9 (см. Рис.~\ref{fig:gen9}), но судя по всему она подверглась мутации или кроссинговеру, поэтому алгоритм не остановился. На поколении №19 (см. Рис.~\ref{fig:lastgen}) было получено значение фитнеса $0.0201$, которое затем повторялось в следующих 10 поколениях. Алгоритм остановился на поколлении №29. На графиках показаны 2D-контурный график (a) и 3D-поверхность целевой функции с точками популяции текущего поколения (b) и (c).
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_001.png}
|
||||
\caption{График целевой функции и популяции поколения №1}
|
||||
\label{fig:gen1}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_002.png}
|
||||
\caption{График целевой функции и популяции поколения №2}
|
||||
\label{fig:gen2}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_003.png}
|
||||
\caption{График целевой функции и популяции поколения №3}
|
||||
\label{fig:gen3}
|
||||
\end{figure}
|
||||
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_005.png}
|
||||
\caption{График целевой функции и популяции поколения №5}
|
||||
\label{fig:gen5}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_007.png}
|
||||
\caption{График целевой функции и популяции поколения №7}
|
||||
\label{fig:gen7}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_009.png}
|
||||
\caption{График целевой функции и популяции поколения №9}
|
||||
\label{fig:gen9}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_010.png}
|
||||
\caption{График целевой функции и популяции поколения №10}
|
||||
\label{fig:gen10}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_015.png}
|
||||
\caption{График целевой функции и популяции поколения №15}
|
||||
\label{fig:gen15}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_019.png}
|
||||
\caption{График целевой функции и популяции поколения №19}
|
||||
\label{fig:lastgen}
|
||||
\end{figure}
|
||||
|
||||
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
|
||||
\newpage
|
||||
\section{Исследование реализации}
|
||||
\subsection{Проведение измерений}
|
||||
В рамках лабораторной работы необходимо было исследовать зависимость времени выполнения задачи и количества поколений от популяции и вероятностей кроссинговера и мутации хромосомы
|
||||
|
||||
Для исследования были выбраны следующие значения параметров:
|
||||
\begin{itemize}
|
||||
\item $N = 10, 25, 50, 100$ -- размер популяции.
|
||||
\item $p_c = 0.3, 0.4, 0.5, 0.6, 0.7, 0.8$ -- вероятность кроссинговера.
|
||||
\item $p_m = 0.001, 0.01, 0.05, 0.1, 0.2$ -- вероятность мутации.
|
||||
\end{itemize}
|
||||
|
||||
Измерения были проведены для двух критериев остановки:
|
||||
\begin{itemize}
|
||||
\item Лучшее значение фитнеса не изменялось 10 поколений.
|
||||
\item Лучшее значение фитнеса достигло заданного значения $0.005$.
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection*{Результаты для первого критерия остановки}
|
||||
|
||||
Результаты измерений представлены в таблицах \ref{tab:pc_pm_results_10}--\ref{tab:pc_pm_results_100}. В ячейках указано время в миллисекундах нахождения минимума функции. В скобках указано количество поколений, за которое было найдено решение. Во второй строке указано усреднённое по всем запускам лучшее значение фитнеса. Если в ячейке стоит прочерк, то это означает, что решение не было найдено за 200 поколений. Лучшее значение по времени выполнения и по значению фитнеса для каждого размера популяции выделено цветом и жирным шрифтом.
|
||||
|
||||
\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 *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
\midrule
|
||||
\textbf{0.3} & 1.3 (13) 0.36281 & 1.7 (18) 7.55685 & 1.2 (13) 1.55537 & \textcolor{magenta}{\textbf{1.0 (11)}} 1.78411 & 9.4 (87) 0.04271 \\
|
||||
\textbf{0.4} & 1.3 (14) 0.03913 & 1.6 (17) 0.02868 & 1.3 (13) 0.36232 & 2.1 (20) 0.10641 & — \\
|
||||
\textbf{0.5} & 1.4 (15) 0.87081 & 1.7 (18) 1.71634 & 2.3 (21) 0.10401 & 3.4 (25) 0.00461 & — \\
|
||||
\textbf{0.6} & 2.8 (19) 0.06375 & 1.8 (13) 0.72202 & 2.9 (22) 0.01473 & 3.4 (25) 0.01162 & 29.4 (184) \textcolor{magenta}{\textbf{0.00033}} \\
|
||||
\textbf{0.7} & 1.5 (15) 1.25409 & 2.3 (22) 8.67464 & 1.9 (18) 0.13319 & 8.6 (66) 0.00078 & 8.9 (48) 0.11136 \\
|
||||
\textbf{0.8} & 1.9 (15) 3.10415 & 1.4 (13) 1.09275 & 2.1 (19) 0.43094 & 6.4 (54) 0.00191 & — \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_10}
|
||||
\end{table}
|
||||
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 25$}
|
||||
\begin{tabularx}{\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
\midrule
|
||||
\textbf{0.3} & 3.0 (18) 0.16836 & \textcolor{magenta}{\textbf{2.2 (13)}} 0.04190 & 4.7 (27) 0.00544 & — & — \\
|
||||
\textbf{0.4} & 4.1 (24) 0.00808 & 4.6 (26) 0.01101 & 5.8 (31) 0.02330 & 3.8 (19) 0.05414 & — \\
|
||||
\textbf{0.5} & 3.1 (17) 0.05259 & 5.0 (26) 0.47018 & 27.8 (138) \textcolor{magenta}{\textbf{0.00024}} & 14.5 (67) 0.00312 & — \\
|
||||
\textbf{0.6} & 6.1 (31) 0.01033 & 6.8 (34) 0.00148 & — & — & — \\
|
||||
\textbf{0.7} & 4.1 (21) 0.00107 & 3.2 (16) 0.32522 & — & — & — \\
|
||||
\textbf{0.8} & 23.9 (109) 0.00352 & 15.8 (72) 0.11662 & 28.3 (123) 0.00038 & — & — \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_25}
|
||||
\end{table}
|
||||
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 50$}
|
||||
\begin{tabularx}{\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
\midrule
|
||||
\textbf{0.3} & 14.9 (51) 0.05874 & 19.3 (59) \textcolor{magenta}{\textbf{0.00003}} & 36.7 (113) 0.00190 & — & — \\
|
||||
\textbf{0.4} & 12.5 (40) 0.01955 & \textcolor{magenta}{\textbf{5.6 (18)}} 0.00022 & — & — & — \\
|
||||
\textbf{0.5} & 65.0 (195) 0.04790 & 26.4 (78) 0.01673 & — & — & — \\
|
||||
\textbf{0.6} & 16.4 (47) 0.00329 & 18.5 (50) 0.00065 & — & — & — \\
|
||||
\textbf{0.7} & 51.0 (137) 0.00120 & 59.3 (158) 0.00010 & — & — & — \\
|
||||
\textbf{0.8} & 48.8 (126) 0.01393 & 67.6 (172) 0.00650 & — & — & — \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_50}
|
||||
\end{table}
|
||||
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 100$}
|
||||
\begin{tabularx}{\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
\midrule
|
||||
\textbf{0.3} & 24.2 (44) 0.00110 & 17.9 (32) 0.00113 & \textcolor{magenta}{\textbf{17.6 (29)}} 0.00193 & — & — \\
|
||||
\textbf{0.4} & 30.7 (51) 0.00173 & — & — & — & — \\
|
||||
\textbf{0.5} & 27.4 (43) 0.00016 & — & — & — & — \\
|
||||
\textbf{0.6} & 20.4 (31) 0.00115 & 129.8 (186) 0.00025 & — & — & — \\
|
||||
\textbf{0.7} & 115.4 (162) 0.00002 & — & — & — & — \\
|
||||
\textbf{0.8} & 106.5 (143) \textcolor{magenta}{\textbf{0.00001}} & — & — & — & — \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_100}
|
||||
\end{table}
|
||||
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\subsubsection*{Результаты для второго критерия остановки}
|
||||
|
||||
Результаты измерений представлены в таблицах \ref{tab:1_pc_pm_results_10}--\ref{tab:1_pc_pm_results_100}.
|
||||
|
||||
|
||||
% Автоматически сгенерированные LaTeX таблицы
|
||||
% Лучший результат по времени и по фитнесу выделены жирным отдельно
|
||||
% Убедитесь, что подключен \usepackage{tabularx}
|
||||
% ВНИМАНИЕ: Убедитесь, что подключен \usepackage{xcolor} для цветового выделения
|
||||
% Используйте \newcolumntype{Y}{>{\centering\arraybackslash}X} перед таблицами
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 10$}
|
||||
\begin{tabularx}{\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
\midrule
|
||||
\textbf{0.3} & — & — & — & 15.6 (155) 0.00063 & 7.8 (69) 0.00409 \\
|
||||
\textbf{0.4} & — & — & — & 8.9 (81) \textcolor{magenta}{\textbf{0.00038}} & \textcolor{magenta}{\textbf{4.6 (40)}} 0.00317 \\
|
||||
\textbf{0.5} & — & — & 8.7 (85) 0.00199 & — & 16.5 (140) 0.00453 \\
|
||||
\textbf{0.6} & — & — & — & 8.9 (77) 0.00310 & 14.3 (117) 0.00082 \\
|
||||
\textbf{0.7} & — & — & 8.2 (70) 0.00089 & 5.6 (49) 0.00431 & 7.1 (58) 0.00047 \\
|
||||
\textbf{0.8} & — & 19.7 (180) 0.00397 & — & 5.0 (42) 0.00494 & 5.5 (44) 0.00357 \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:1_pc_pm_results_10}
|
||||
\end{table}
|
||||
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 25$}
|
||||
\begin{tabularx}{\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
\midrule
|
||||
\textbf{0.3} & 1.1 (7) 0.00277 & 30.0 (173) \textcolor{magenta}{\textbf{0.00059}} & — & 2.2 (12) 0.00191 & 30.2 (139) 0.00200 \\
|
||||
\textbf{0.4} & 1.8 (10) 0.00384 & — & 12.2 (63) 0.00164 & 6.6 (33) 0.00354 & 18.5 (82) 0.00224 \\
|
||||
\textbf{0.5} & — & — & 12.5 (58) 0.00233 & 2.3 (11) 0.00196 & 17.1 (73) 0.00116 \\
|
||||
\textbf{0.6} & — & 30.9 (151) 0.00265 & 36.7 (175) 0.00146 & 10.0 (46) 0.00449 & 5.7 (23) 0.00281 \\
|
||||
\textbf{0.7} & 1.1 (6) 0.00472 & — & 0.8 (4) 0.00233 & 3.9 (17) 0.00112 & \textcolor{magenta}{\textbf{0.3 (2)}} 0.00371 \\
|
||||
\textbf{0.8} & — & — & 10.3 (43) 0.00137 & 7.7 (32) 0.00379 & 10.5 (41) 0.00155 \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:1_pc_pm_results_25}
|
||||
\end{table}
|
||||
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 50$}
|
||||
\begin{tabularx}{\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
\midrule
|
||||
\textbf{0.3} & 3.7 (12) 0.00354 & 3.4 (9) 0.00075 & 23.7 (73) 0.00467 & 4.9 (14) 0.00043 & 2.1 (6) 0.00029 \\
|
||||
\textbf{0.4} & 3.6 (12) 0.00270 & 4.2 (13) 0.00061 & 9.2 (25) 0.00251 & 18.2 (51) 0.00490 & 6.6 (16) 0.00063 \\
|
||||
\textbf{0.5} & 4.0 (10) 0.00099 & 48.8 (141) 0.00324 & 3.8 (11) 0.00087 & 14.7 (39) \textcolor{magenta}{\textbf{0.00017}} & 1.2 (3) 0.00115 \\
|
||||
\textbf{0.6} & 1.6 (5) 0.00070 & 51.6 (139) 0.00217 & 4.7 (13) 0.00294 & 2.6 (7) 0.00397 & 11.5 (27) 0.00053 \\
|
||||
\textbf{0.7} & — & — & 2.6 (7) 0.00144 & 3.5 (9) 0.00182 & \textcolor{magenta}{\textbf{1.1 (3)}} 0.00072 \\
|
||||
\textbf{0.8} & 4.1 (11) 0.00240 & 3.5 (8) 0.00380 & 2.5 (6) 0.00422 & 2.7 (7) 0.00126 & 4.3 (10) 0.00060 \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:1_pc_pm_results_50}
|
||||
\end{table}
|
||||
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\small
|
||||
\caption{Результаты для $N = 100$}
|
||||
\begin{tabularx}{\linewidth}{l *{5}{Y}}
|
||||
\toprule
|
||||
$\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\
|
||||
\midrule
|
||||
\textbf{0.3} & 9.3 (17) 0.00451 & 6.0 (11) 0.00344 & 10.0 (17) 0.00343 & 5.3 (8) 0.00046 & 9.8 (14) 0.00412 \\
|
||||
\textbf{0.4} & 5.7 (9) \textcolor{magenta}{\textbf{0.00005}} & 8.4 (14) 0.00108 & 3.5 (6) 0.00254 & 4.0 (6) 0.00186 & 6.5 (9) 0.00283 \\
|
||||
\textbf{0.5} & 3.8 (6) 0.00019 & 4.9 (8) 0.00103 & 3.6 (6) 0.00260 & 11.1 (16) 0.00204 & 7.5 (10) 0.00374 \\
|
||||
\textbf{0.6} & — & 6.5 (10) 0.00107 & 3.6 (5) 0.00079 & \textcolor{magenta}{\textbf{0.9 (2)}} 0.00324 & 10.1 (13) 0.00044 \\
|
||||
\textbf{0.7} & 1.7 (3) 0.00106 & 6.6 (10) 0.00489 & 4.1 (6) 0.00031 & 12.4 (16) 0.00240 & 4.8 (6) 0.00276 \\
|
||||
\textbf{0.8} & 5.0 (7) 0.00387 & 58.4 (77) 0.00453 & 7.8 (10) 0.00259 & 11.2 (13) 0.00210 & 6.1 (7) 0.00493 \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:1_pc_pm_results_100}
|
||||
\end{table}
|
||||
|
||||
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\subsection{Анализ результатов}
|
||||
|
||||
\subsubsection*{Обоснование применения двух критериев остановки}
|
||||
|
||||
В исследовании использовались два различных критерия остановки алгоритма, поскольку критерий по количеству поколений (отсутствие улучшения в течение 10 поколений) не всегда обеспечивал достижение достаточно хороших значений фитнеса, особенно для малых популяций. Это делало некорректным сравнение эффективности различных комбинаций параметров только по времени выполнения. Введение второго критерия (достижение фитнеса 0.005) позволило получить более объективную оценку скорости нахождении качественных решений.
|
||||
|
||||
\subsubsection*{Первый критерий остановки (отсутствие улучшения в течение 10 поколений)}
|
||||
|
||||
При использовании первого критерия остановки наблюдаются следующие закономерности:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Малые популяции ($N=10$):} Оптимальный баланс достигается при умеренных значениях параметров. Лучший результат по времени показывает комбинация $p_c=0.3$, $p_m=0.1$ (1.0 мс, 11 поколений), однако лучшее значение фитнеса достигается при $p_c=0.6$, $p_m=0.2$ (0.00033). Качество решений существенно варьируется.
|
||||
|
||||
\item \textbf{Средние популяции ($N=25$):} Демонстрируют высокую эффективность при низких значениях мутации. Минимальное время выполнения достигается при $p_c=0.3$, $p_m=0.01$ (2.2 мс, 13 поколений), а наилучший фитнес — при $p_c=0.5$, $p_m=0.05$ (0.00024).
|
||||
|
||||
\item \textbf{Большие популяции ($N=50, 100$):} Характеризуются критической чувствительностью к высоким значениям мутации и демонстрируют заметное улучшение качества фитнеса. Для $N=50$ лучшие результаты при $p_c=0.4$, $p_m=0.01$ (5.6 мс по времени) и $p_c=0.3$, $p_m=0.01$ (фитнес 0.00003). Для $N=100$ работают только комбинации с очень низкой мутацией, но обеспечивают отличное качество (фитнес до 0.00001).
|
||||
|
||||
\item \textbf{Проблема сходимости:} С увеличением размера популяции значительно возрастает количество комбинаций параметров, не обеспечивающих сходимость за 200 поколений, особенно при $p_m \geq 0.05$.
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection*{Второй критерий остановки (достижение фитнеса 0.005)}
|
||||
|
||||
Использование фиксированного порога фитнеса демонстрирует принципиально иную картину и подтверждает правильность введения альтернативного критерия:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Инверсия требований к мутации:} В отличие от первого критерия, здесь малые популяции требуют более высоких значений мутации для достижения целевого фитнеса. Для $N=10$ большинство комбинаций с $p_m \leq 0.01$ вообще не достигают порога, что подтверждает проблему качества при первом критерии.
|
||||
|
||||
\item \textbf{Лучшие результаты больших популяций:} Популяции $N=50$ и $N=100$ показывают отличные результаты — достижение высокого качества за минимальное время: $N=50$ при $p_c=0.7$, $p_m=0.2$ (1.1 мс, 3 поколения) и $N=100$ при $p_c=0.6$, $p_m=0.1$ (0.9 мс, 2 поколения).
|
||||
\end{itemize}
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Ответ на контрольный вопрос}
|
||||
|
||||
\textbf{Вопрос}: Опишите понятие «оптимизационная задача».
|
||||
|
||||
\textbf{Ответ}: Оптимизационная задача — это математическая задача, в которой требуется найти такие значения переменных, при которых некоторая функция, называемая целевой, принимает наибольшее или наименьшее значение. При этом искомые значения должны удовлетворять определённым условиям или ограничениям, задающим допустимую область решений. Цель оптимизации заключается в выборе наилучшего варианта среди множества возможных с точки зрения заданного критерия эффективности.
|
||||
|
||||
Такие задачи широко применяются в науке, технике, экономике и управлении для рационального распределения ресурсов, минимизации затрат или максимизации прибыли. В зависимости от формы целевой функции и ограничений оптимизационные задачи могут быть линейными, нелинейными, дискретными или непрерывными. Их решение позволяет принимать обоснованные решения и повышать эффективность различных процессов и систем.
|
||||
|
||||
|
||||
\newpage
|
||||
\section*{Заключение}
|
||||
\addcontentsline{toc}{section}{Заключение}
|
||||
|
||||
В ходе второй лабораторной работы была успешно решена задача оптимизации функции Axis parallel hyper-ellipsoid function с использованием генетических алгоритмов:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Изучен теоретический материал о real-coded генетических алгоритмах и различных операторах кроссинговера и мутации;
|
||||
\item Создана программная библиотека на языке Python с реализацией арифметического и геометрического кроссоверов, случайной мутации и селекции методом рулетки;
|
||||
\item Проведено исследование влияния параметров ГА на эффективность поиска для популяций размером 10, 25, 50 и 100 особей;
|
||||
\end{enumerate}
|
||||
|
||||
\newpage
|
||||
\section*{Список литературы}
|
||||
\addcontentsline{toc}{section}{Список литературы}
|
||||
|
||||
\vspace{-1.5cm}
|
||||
\begin{thebibliography}{0}
|
||||
\bibitem{vostrov}
|
||||
Методические указания по выполнению лабораторных работ к курсу «Генетические алгоритмы», 119 стр.
|
||||
\end{thebibliography}
|
||||
|
||||
\end{document}
|
||||
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()
|
||||
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}
|
||||
6
presentation/report/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
*
|
||||
|
||||
!.gitignore
|
||||
!**/
|
||||
!img/*
|
||||
!*.tex
|
||||
BIN
presentation/report/img/angle.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
presentation/report/img/angle_chromosome.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
presentation/report/img/angle_experiment1.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
presentation/report/img/angle_experiment2.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
presentation/report/img/angle_experiment3.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
presentation/report/img/binary.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
presentation/report/img/classification.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
presentation/report/img/crossover.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
presentation/report/img/decimal.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
presentation/report/img/experiment1.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
presentation/report/img/experiment2.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
presentation/report/img/genalg.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
presentation/report/img/grid.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
presentation/report/img/obstacle1.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
presentation/report/img/obstacle2.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
presentation/report/img/ordered.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
presentation/report/img/static.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
presentation/report/img/table.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
presentation/report/img/table1.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
presentation/report/img/table2.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
presentation/report/img/task.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
580
presentation/report/ГА. Реферат. Тищенко.tex
Normal file
@@ -0,0 +1,580 @@
|
||||
\documentclass[a4paper, final]{article}
|
||||
%\usepackage{literat} % Нормальные шрифты
|
||||
\usepackage[14pt]{extsizes} % для того чтобы задать нестандартный 14-ый размер шрифта
|
||||
\usepackage{tabularx}
|
||||
\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[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{Реферат по дисциплине}\\
|
||||
\large{<<Генетические алгоритмы>>}\\
|
||||
\large{<<Планирование пути робота>>}\\
|
||||
|
||||
\hfill \break
|
||||
\hfill \break
|
||||
\end{center}
|
||||
|
||||
\small{
|
||||
\begin{tabular}{lrrl}
|
||||
\!\!\!Студент, & \hspace{2cm} & & \\
|
||||
\!\!\!группы 5130201/20102 & \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*{Введение}
|
||||
\addcontentsline{toc}{section}{Введение}
|
||||
Планирование пути мобильного робота — это задача построения коллизие-свободной траектории из начальной точки в целевую в присутствии препятствий и ограничений среды. Актуальность задачи обусловлена широким спектром приложений: от автономной логистики и сельского хозяйства до роботизированной инспекции и сервисной робототехники. Традиционные графовые методы (Дейкстра, A*) обеспечивают точный поиск на известных картах, однако их эффективность снижается при росте размерности, наличии динамики и сложных ограничений. Эволюционные подходы, в частности генетические алгоритмы (ГА), предоставляют гибкую эвристическую альтернативу, позволяя естественно учитывать несколько критериев качества и штрафы за нарушения ограничений.
|
||||
|
||||
Цель работы — исследовать применение ГА для планирования пути на дискретном представлении среды (grid) и в параметризации траекторий через направления движения (угловое кодирование), а также сравнить различные способы кодирования хромосом и их влияние на качество маршрутов и вычислительные затраты.
|
||||
|
||||
В работе:
|
||||
\begin{itemize}
|
||||
\item кратко рассмотрена классификация алгоритмов планирования пути по характеру среды (статическая/динамическая), принципу (глобальные/локальные) и полноте (точные/эвристические);
|
||||
\item формализована фитнесс-функция с учётом длины пути и штрафов за пересечение препятствий, а для углового кодирования — с дополнительными слагаемыми за число поворотов;
|
||||
\item изучены три варианта grid-кодирования узлов пути (двоичное, десятичное, упорядоченное) и угловое кодирование с 4 и 8 направлениями движения;
|
||||
\item проведены эксперименты в статических сценах и сценарии с поэтапным появлением новых препятствий (offline/online-перепланирование);
|
||||
\item сопоставлены метрики: длина маршрута, число поколений и время решения.
|
||||
\end{itemize}
|
||||
|
||||
Полученные результаты демонстрируют применимость ГА к задачам планирования как в статических, так и в динамически меняющихся средах, а также выявляют влияние выбора представления пути на качество и стоимость поиска.
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Постановка задачи}
|
||||
Планирование пути робота -- вычисление пути, свободного от столкновений, от начальной позиции до конечной
|
||||
среди множества препятствий
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.8\linewidth]{img/task.png}
|
||||
\caption{Задача планирования пути}
|
||||
\label{fig:task}
|
||||
\end{figure}
|
||||
|
||||
\newpage
|
||||
\section{Классификация алгоритмов планирования пути}
|
||||
На схеме \ref{fig:classification} показана классификация алгоритмов планирования пути.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/classification.png}
|
||||
\caption{Классификация алгоритмов планирования пути}
|
||||
\label{fig:classification}
|
||||
\end{figure}
|
||||
|
||||
Алгоритмы планирования пути можно классифицировать по трём главным признакам:
|
||||
|
||||
\begin{itemize}
|
||||
|
||||
\item По характеру среды (based on environment)
|
||||
|
||||
\begin{itemize}
|
||||
|
||||
\item \textbf{Статические (Static)}
|
||||
Используются, когда карта среды известна заранее и не меняется. Все препятствия заранее заданы, и алгоритм строит маршрут до старта движения.
|
||||
Пример: классические алгоритмы поиска пути на графе (A*, Дейкстра).
|
||||
|
||||
\item \textbf{Динамические (Dynamic)}
|
||||
Применяются в условиях, где препятствия могут появляться или перемещаться во время движения робота. Алгоритм должен уметь перестраивать путь «на лету» (онлайн-планирование).
|
||||
Пример: D* Lite, Anytime Repairing A*, алгоритмы на основе предсказаний и реактивного управления.
|
||||
\end{itemize}
|
||||
|
||||
|
||||
\item По принципу алгоритма (based on algorithm)
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Глобальные (Global)}
|
||||
Предполагают знание полной карты окружающей среды. Алгоритм ищет оптимальный путь целиком от старта до цели.
|
||||
Пример: A*, Дейкстра, волновой алгоритм (Wavefront).
|
||||
|
||||
\item \textbf{Локальные (Local)}
|
||||
Робот строит путь только на основе данных с датчиков «здесь и сейчас». Такие методы хорошо работают в неизвестных или частично известных средах.
|
||||
Пример: Bug Algorithms, Potential Fields, Dynamic Window Approach.
|
||||
|
||||
\end{itemize}
|
||||
|
||||
\item По полноте поиска (based on completeness)
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Точные (Exact)}
|
||||
Гарантируют нахождение решения (если оно существует), а также могут обеспечить оптимальность. Но часто требуют много вычислительных ресурсов.
|
||||
Пример: алгоритм Дейкстры, точный A*.
|
||||
|
||||
\item \textbf{Эвристические (Heuristic)}
|
||||
Используют приближённые методы и эвристики для ускорения поиска. Решение находится быстрее, но не всегда оптимальное.
|
||||
Пример: эвристический A*, эволюционные методы (генетические алгоритмы, рой частиц, муравьиные алгоритмы).
|
||||
|
||||
\end{itemize}
|
||||
|
||||
\end{itemize}
|
||||
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Генетические алгоритмы для построения пути}
|
||||
Для решения произвольной задачи с помощью ГА необходимо определить:
|
||||
|
||||
\begin{itemize}
|
||||
\item особь и популяцию;
|
||||
\item генетические операторы;
|
||||
\item фитнесс функцию;
|
||||
\item параметры ГА.
|
||||
\end{itemize}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{img/genalg.png}
|
||||
\caption{Составляющие ГА}
|
||||
\label{fig:genalg}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Grid-кодирование пути}
|
||||
|
||||
\subsubsection*{Кодирование хромосом}
|
||||
Рассмотрим часто используемое на практике представление окружения робота в виде «решетки» -- сетки
|
||||
(grid).
|
||||
Для представления окружения используется:
|
||||
\begin{itemize}
|
||||
\item двоичное;
|
||||
\item десятичное;
|
||||
\item упорядоченное кодирование;
|
||||
\end{itemize}
|
||||
показанное на рисунке \ref{fig:grid} (белые клетки--свободное пространство, серые -- препятствия).
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{img/grid.png}
|
||||
\caption{Grid-кодирование пути}
|
||||
\label{fig:grid}
|
||||
\end{figure}
|
||||
|
||||
Тогда хромосома (особь популяции) представляет потенциальное решение -
|
||||
Путь от начальной точки S (левый верхний угол) до конечной точки- цели T (правый нижний угол)
|
||||
Хромосома (путь робота) содержит начальную и конечную вершины графа, представляющего решетку, а
|
||||
также вершины, пересекаемые роботом
|
||||
Эти вершины (или шаги) называются генами хромосомы.
|
||||
Различные методы кодирования используются для хромосомы, в зависимости от формы представления
|
||||
окружения:
|
||||
|
||||
\newpage
|
||||
\begin{enumerate}
|
||||
\item Двоичное кодирование – содержит двоичные коды номеров строк и столбцов (координаты x,y)
|
||||
«узловых» вершин пути.
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{img/binary.png}
|
||||
\caption{Двоичное кодирование}
|
||||
\label{fig:binary}
|
||||
\end{figure}
|
||||
\item Десятичное кодирование – содержит десятичные номера строк и столбцов (координаты x,y)
|
||||
«узловых» вершин пути.
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.45\linewidth]{img/decimal.png}
|
||||
\caption{Десятичное кодирование}
|
||||
\label{fig:decimal}
|
||||
\end{figure}
|
||||
\item Упорядоченное кодирование - содержит номера(в общем порядке ячеек - вершин) проходимых
|
||||
«узловых» вершин пути.
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.4\linewidth]{img/ordered.png}
|
||||
\caption{Упорядоченное кодирование}
|
||||
\label{fig:ordered}
|
||||
\end{figure}
|
||||
\end{enumerate}
|
||||
|
||||
|
||||
\subsubsection*{Инициализация популяции}
|
||||
Выполняется случайно
|
||||
При этом некоторые построенные хромосомы соответствуют нереализуемым путям, которые пересекают
|
||||
препятствия
|
||||
В таблице представлены экспериментальные данные по генерации начальной популяции
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.5\linewidth]{img/table.png}
|
||||
\caption{Таблица экспериментальных данных}
|
||||
\label{fig:table}
|
||||
\end{figure}
|
||||
|
||||
Далее находятся оптимальные или близкие к оптимальным решения (пути роботов), даже если начальная
|
||||
популяция содержит нереализуемые решения
|
||||
|
||||
\subsubsection*{Фитнесс функция}
|
||||
Целью планирования пути является построение пути робота между начальной и конечной точками.
|
||||
Оптимальный путь должен иметь минимальную длину, время и минимальные энергозатраты.
|
||||
Прежде всего, минимизируется длина пути.
|
||||
Поэтому все эволюционные методы построения пути включают в фитнесс-функцию длину пути робота,
|
||||
которая может быть определена следующим образом.
|
||||
|
||||
Здесь $P_i$
|
||||
- i-й ген хромосомы $P$ , $d$ - расстояние между 2-мя узлами.
|
||||
Фитнесс-функция для реализуемых путей определяется как сумма расстояний между узлами пути.
|
||||
Заметим, что для нереализуемых путей (имеющих пересечения с препятствия) фитнесс-функция имеет
|
||||
дополнительную штрафную функцию. Если между узлами встречается препятствие, то в фитнессфункцию добавляется штраф.
|
||||
Для поиска пути робота используется классический ГА.
|
||||
На последнем шаге итерации (поколения) в соответствии с вычисленными значениями фитнесс-функции
|
||||
отбираются хромосомы для производства потомков с помощью генетических операторов кроссинговера
|
||||
и мутации.
|
||||
Для выбора родителей применяется ранговый отбор
|
||||
|
||||
\begin{equation}
|
||||
f =
|
||||
\begin{cases}
|
||||
\displaystyle \sum_{i=1}^{n-1} d(p_i, p_{i+1}), & \text{для допустимых путей (без столкновений)}, \\[1.5ex]
|
||||
\displaystyle \sum_{i=1}^{n-1} d(p_i, p_{i+1}) + \text{штраф}, & \text{для недопустимых путей}.
|
||||
\end{cases}
|
||||
\end{equation}
|
||||
|
||||
\begin{equation}
|
||||
d(p_i, p_{i+1}) = \sqrt{(x_{(i+1)} - x_i)^2 + (y_{(i+1)} - y_i)^2},
|
||||
\end{equation}
|
||||
где $p_i = (x_i, y_i)$ — координаты $i$-й вершины пути.
|
||||
|
||||
|
||||
\subsubsection*{Генетические операторы}
|
||||
Для генерации потомков используется обычный 1-точечный кроссинговер с заданной вероятностью $P_c$.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/crossover.png}
|
||||
\caption{Одноточечный кроссинговер}
|
||||
\label{fig:crossover}
|
||||
\end{figure}
|
||||
|
||||
Пример на Рис.~\ref{fig:crossover} показывает кроссинговер для упорядоченного кодирования хромосом.
|
||||
|
||||
Далее к потомкам (каждому гену) с малой вероятностью $P_m$ применяется оператор мутации, который вносит
|
||||
небольшое изменение в ген в зависимости от способа кодирования.
|
||||
Мутация расширяет пространство поиска и препятствует преждевременной сходимости в локальном
|
||||
экстремуме
|
||||
|
||||
\subsubsection*{Эксперименты}
|
||||
|
||||
\textbf{Эксперимент 1.} Построение пути проводилось в следующем окружении (сетка 16х16) с 8
|
||||
препятствиями с использованием указанных выше методов кодирования хромосом.
|
||||
|
||||
Параметры ГА:
|
||||
\begin{itemize}
|
||||
\item Мощность популяции $N=60$;
|
||||
\item Вероятности: кроссинговера $P_c=1.0$, мутации $P_m=0.1$
|
||||
\end{itemize}
|
||||
После 10 запусков ГА для каждого метода получены средние значения расстояния, число поколений и
|
||||
время решения, которые представлены на
|
||||
Рис.~\ref{fig:table1}.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.45\linewidth]{img/experiment1.png}
|
||||
\caption{Эксперимент 1}
|
||||
\label{fig:experiment1}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.8\linewidth]{img/table1.png}
|
||||
\caption{Таблица эксперимента 1}
|
||||
\label{fig:table1}
|
||||
\end{figure}
|
||||
|
||||
\newpage
|
||||
\textbf{Эксперимент 2.} Эксперимент 2 для окружения с 7 препятствиями. Параметры ГА те же.
|
||||
|
||||
Данные для различных методов кодирования представлены на рисунке \ref{fig:table3}. Видно, что десятичное и упорядоченное кодирование показывают лучшие результаты.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.45\linewidth]{img/experiment2.png}
|
||||
\caption{Эксперимент 2}
|
||||
\label{fig:experiment2}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.8\linewidth]{img/table2.png}
|
||||
\caption{Таблица эксперимента 2}
|
||||
\label{fig:table2}
|
||||
\end{figure}
|
||||
|
||||
|
||||
|
||||
|
||||
\subsection{Угловое кодирование пути}
|
||||
|
||||
\subsubsection*{Кодирование хромосом}
|
||||
|
||||
Для кодирования пути робота эволюционные методы используют различные методы.
|
||||
Альтернативным рассмотренному ранее кодированию (узлов) вершин проходимого пути,
|
||||
является кодирование на основе углов направлений движения робота.
|
||||
Это можно сделать по-разному (с различной точностью).
|
||||
На Рис.~\ref{fig:angle} представлены 2 варианта кодирования направлений движения:
|
||||
|
||||
а) 4 направления - робот может передвигаться по горизонталям и вертикалям.
|
||||
|
||||
б) 8 направлений- робот может передвигаться также по диагоналям.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/angle.png}
|
||||
\caption{Для кодирования направлений используются целые числа:\\ а) (0,1,2,3), либо
|
||||
б) (0,1,2,3,4,5,6,7)}
|
||||
\label{fig:angle}
|
||||
\end{figure}
|
||||
|
||||
|
||||
Тогда каждый ген в хромосоме представляет направление движения робота на следующем
|
||||
шаге.
|
||||
|
||||
В этом случае хромосома (см. Рис.~\ref{fig:angle_chromosome}) представляет «коллекцию» направлений движения робота в каждой
|
||||
точке, начиная от старта, и кончая финалом.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.9\linewidth]{img/angle_chromosome.png}
|
||||
\caption{Хромосома с угловым кодированием пути. D – целое число, определяющее направление движения на следующем шаге.}
|
||||
\label{fig:angle_chromosome}
|
||||
\end{figure}
|
||||
|
||||
\subsubsection*{Фитнесс-функция}
|
||||
|
||||
Фитнесс-функция в задаче планирования пути робота определяется следующим образом:
|
||||
|
||||
\begin{equation}
|
||||
\text{Cost}(P) = w_1 D + w_2 O + w_3 C,
|
||||
\end{equation}
|
||||
|
||||
где:
|
||||
\begin{itemize}
|
||||
\item $w_1, w_2, w_3$ -- коэффициенты (константы), задающие вклад каждой компоненты;
|
||||
\item $D$ -- расстояние (длина) пути;
|
||||
\item $O$ -- число пересекаемых ячеек препятствий;
|
||||
\item $C$ -- число изменений направлений движения.
|
||||
\end{itemize}
|
||||
|
||||
Особь (маршрут) с минимальным значением фитнесс-функции соответствует оптимальному пути.
|
||||
Очевидно, что увеличение числа изменений направления приводит к увеличению времени движения,
|
||||
а каждое пересечение препятствия сопровождается значительным штрафом.
|
||||
|
||||
|
||||
\subsubsection*{Эксперименты}
|
||||
|
||||
\textbf{Эксперимент 1}. Метод апробирован на различного типа препятствиях с разным числом направлений движений (4 или 8).
|
||||
Начальная точка -- (0,0). Конечная точка -- (15, 9).
|
||||
В 1-м эксперименте использовались только статические препятствия.
|
||||
|
||||
На Рис.~\ref{fig:angle_experiment1}-\ref{fig:angle_experiment2} представлены результаты для 2- вариантов направлений движений(4 или 8), которые показывают способность ГА решать данную проблему.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/angle_experiment1.png}
|
||||
\caption{Загроможденные препятствия. \\ (a) 4 направления, (b) 8 направлений}
|
||||
\label{fig:angle_experiment1}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/angle_experiment2.png}
|
||||
\caption{Региональные препятствия. \\ (c) 4 направления, (d) 8 направлений}
|
||||
\label{fig:angle_experiment2}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/angle_experiment3.png}
|
||||
\caption{Препятствия окружают конечную точку. \\ (e) 4 направления, (f) 8 направлений}
|
||||
\label{fig:angle_experiment3}
|
||||
\end{figure}
|
||||
|
||||
|
||||
\newpage
|
||||
\textbf{Эксперимент 2.} Динамическую систему поиска пути можно разделить на две компоненты.
|
||||
|
||||
\begin{itemize}
|
||||
\item Ищем маршрут по заранее заданной карте ещё до запуска робота (offline компонента).
|
||||
\item Если робот встретил новые препятствия, которых не было в исходной карте, то запускается новый алгоритм поиска с актуальной точки (online компонента).
|
||||
\end{itemize}
|
||||
|
||||
Сначал генетический алгоритм был запущен для статической среды (см.~Рис.~\ref{fig:static}).
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/static.png}
|
||||
\caption{Запуск для статической среды (offline компонента)}
|
||||
\label{fig:static}
|
||||
\end{figure}
|
||||
|
||||
\newpage
|
||||
И далее добавлялись (случайно) 1 препятствие (см. Рис.~\ref{fig:obstacle1}).
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/obstacle1.png}
|
||||
\caption{Обновлённый путь после добавления ещё одного препятствия.}
|
||||
\label{fig:obstacle1}
|
||||
\end{figure}
|
||||
|
||||
И ещё одно препятствие (см. Рис.~\ref{fig:obstacle2}).
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=0.7\linewidth]{img/obstacle2.png}
|
||||
\caption{Обновлённый путь после добавления второго препятствия.}
|
||||
\label{fig:obstacle2}
|
||||
\end{figure}
|
||||
|
||||
|
||||
|
||||
\newpage
|
||||
% ---------- ЗАКЛЮЧЕНИЕ ----------
|
||||
\section*{Заключение}
|
||||
\addcontentsline{toc}{section}{Заключение}
|
||||
|
||||
В работе рассмотрено применение генетических алгоритмов к задаче планирования пути робота на дискретной карте и в форме углового (направленного) кодирования. Показано, что:
|
||||
\begin{enumerate}
|
||||
\item Для grid-представления десятичное и упорядоченное кодирование обеспечивают в среднем более короткие пути и/или меньшее время поиска по сравнению с двоичным кодированием при сопоставимых параметрах ГА (\(N=60\), \(P_c=1.0\), \(P_m=0.1\)).
|
||||
\item Угловое кодирование с 8 направлениями, как правило, даёт более гибкие и короткие траектории, чем с 4 направлениями, однако требует контроля числа поворотов (штраф \(C\)) для предотвращения «ломаных» маршрутов.
|
||||
\item В динамическом сценарии поэтапное появление препятствий эффективно обрабатывается за счёт online-перепланирования: повторный запуск ГА из текущего положения восстанавливает коллизие-свободный путь без полной переразметки карты.
|
||||
\end{enumerate}
|
||||
|
||||
Ограничения подхода включают чувствительность к настройкам операторов и вероятностей, риск преждевременной сходимости и вычислительные затраты при частых проверках коллизий.
|
||||
|
||||
ГА представляют собой практичный и переносимый инструмент планирования пути в условиях неопределённости и динамики, при этом выбор кодирования и архитектуры фитнесс-функции критически влияет на компромисс между качеством траектории и затратами вычислений.
|
||||
|
||||
|
||||
|
||||
\newpage
|
||||
\section*{Список литературы}
|
||||
\addcontentsline{toc}{section}{Список литературы}
|
||||
|
||||
\vspace{-1.5cm}
|
||||
\begin{thebibliography}{0}
|
||||
\bibitem{skobtsov} М.В. Прохоров, Ю.А. Скобцов. Многокритериальный генетический алгоритм построения пути робота в сложной среде. — Санкт-Петербург: СПбПУ, 2016. — 36с.
|
||||
|
||||
\bibitem{matveeva} Матвеева А.В. Оптимизация построения маршрута с помощью генетического алгоритма // Финансовый университет при Правительстве Российской Федерации, 2022. — С. 10–12.
|
||||
\end{thebibliography}
|
||||
|
||||
\end{document}
|
||||