Compare commits
8 Commits
1e9e52341a
...
lab3
| Author | SHA1 | Date | |
|---|---|---|---|
| 268c4cf4a1 | |||
| 740a7be984 | |||
| 2cf0693070 | |||
| 3436f94b61 | |||
| f79a6abf1b | |||
| ee79d6ad41 | |||
| 745cfea282 | |||
| b7f2234bff |
@@ -12,12 +12,16 @@
|
||||
- "—" для отсутствующих данных
|
||||
|
||||
Выходной файл: 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]]]:
|
||||
"""
|
||||
@@ -53,7 +57,7 @@ def parse_csv_file(csv_path: str) -> tuple[str, list[list[str]]]:
|
||||
|
||||
def extract_time_value(value: str) -> float | None:
|
||||
"""
|
||||
Извлекает значение времени из строки формата "X.Y (Z)".
|
||||
Извлекает значение времени из строки формата "X.Y (Z)" или "X.Y (Z) W.V".
|
||||
|
||||
Args:
|
||||
value: Строка с результатом
|
||||
@@ -73,6 +77,29 @@ def extract_time_value(value: str) -> float | None:
|
||||
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:
|
||||
"""
|
||||
Находит минимальное время выполнения среди всех значений в таблице.
|
||||
@@ -95,13 +122,38 @@ def find_best_time(data_rows: list[list[str]]) -> float | None:
|
||||
return min_time
|
||||
|
||||
|
||||
def format_value(value: str, best_time: float | None = None) -> str:
|
||||
def find_best_fitness(data_rows: list[list[str]]) -> float | None:
|
||||
"""
|
||||
Форматирует значение для LaTeX таблицы, выделяя лучший результат жирным.
|
||||
Находит минимальное значение фитнеса среди всех значений в таблице.
|
||||
|
||||
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
|
||||
@@ -110,16 +162,52 @@ def format_value(value: str, best_time: float | None = None) -> str:
|
||||
if value == "—" or value == "" or value == "–":
|
||||
return "—"
|
||||
|
||||
# Проверяем, является ли это лучшим результатом
|
||||
current_time = extract_time_value(value)
|
||||
if (
|
||||
current_time is not None
|
||||
and best_time is not None
|
||||
and abs(current_time - best_time) < 0.001
|
||||
):
|
||||
return f"\\textbf{{{value}}}"
|
||||
# Проверяем есть ли фитнес в строке
|
||||
fitness_match = re.search(r"(\d+\.?\d*)\s*\((\d+)\)\s+(\d+\.?\d*)\s*$", value)
|
||||
|
||||
return 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:
|
||||
@@ -134,8 +222,9 @@ def generate_latex_table(n: str, header: str, data_rows: list[list[str]]) -> str
|
||||
Returns:
|
||||
LaTeX код таблицы
|
||||
"""
|
||||
# Находим лучшее время в таблице
|
||||
# Находим лучшее время и лучший фитнес в таблице
|
||||
best_time = find_best_time(data_rows)
|
||||
best_fitness = find_best_fitness(data_rows)
|
||||
|
||||
# Извлекаем заголовки колонок из header
|
||||
header_parts = header.split(",")
|
||||
@@ -162,7 +251,7 @@ def generate_latex_table(n: str, header: str, data_rows: list[list[str]]) -> str
|
||||
|
||||
# Добавляем значения для каждого Pm
|
||||
for i in range(1, min(6, len(row))): # Максимум 5 колонок Pm
|
||||
value = format_value(row[i], best_time)
|
||||
value = format_value(row[i], best_time, best_fitness)
|
||||
latex_code += f" & {value}"
|
||||
|
||||
# Заполняем недостающие колонки если их меньше 5
|
||||
@@ -207,9 +296,12 @@ def main():
|
||||
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})")
|
||||
print(
|
||||
f"✓ Таблица для N={n} готова (лучшее время: {best_time}, лучший фитнес: {best_fitness})"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Ошибка при обработке {csv_file}: {e}")
|
||||
@@ -221,9 +313,13 @@ def main():
|
||||
with open("tables.tex", "w", encoding="utf-8") as f:
|
||||
f.write("% Автоматически сгенерированные LaTeX таблицы\n")
|
||||
f.write(
|
||||
"% Лучший результат по времени выполнения в каждой таблице выделен жирным\n"
|
||||
"% Лучший результат по времени и по фитнесу выделены жирным отдельно\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"
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ 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
|
||||
@@ -31,7 +32,9 @@ BASE_CONFIG = {
|
||||
"max_generations": 200,
|
||||
"seed": None, # None для случайности, т. к. всё усредняем
|
||||
"minimize": True,
|
||||
"fitness_avg_threshold": 0.05, # критерий остановки
|
||||
# "fitness_avg_threshold": 0.05, # критерий остановки
|
||||
# "max_best_repetitions": 10,
|
||||
"best_value_threshold": 0.005,
|
||||
# при включенном сохранении графиков на время смотреть бессмысленно
|
||||
# "save_generations": [1, 50, 199],
|
||||
}
|
||||
@@ -39,13 +42,15 @@ BASE_CONFIG = {
|
||||
|
||||
def run_single_experiment(
|
||||
pop_size: int, pc: float, pm: float
|
||||
) -> tuple[float, float, float, float]:
|
||||
) -> tuple[float, float, float, float, float, float]:
|
||||
"""
|
||||
Запускает несколько экспериментов с заданными параметрами и усредняет результаты.
|
||||
Возвращает (среднее_время_в_мс, стд_отклонение_времени, среднее_поколений, стд_отклонение_поколений).
|
||||
Возвращает (среднее_время_в_мс, стд_отклонение_времени, среднее_поколений,
|
||||
стд_отклонение_поколений, среднее_лучшее_значение_фитнеса, стд_отклонение_лучшего_значения_фитнеса).
|
||||
"""
|
||||
times = []
|
||||
generations = []
|
||||
best_fitnesses = []
|
||||
|
||||
for run_num in range(NUM_RUNS):
|
||||
config = GARunConfig(
|
||||
@@ -65,14 +70,26 @@ def run_single_experiment(
|
||||
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
|
||||
return (
|
||||
avg_time,
|
||||
std_time,
|
||||
avg_generations,
|
||||
std_generations,
|
||||
avg_best_fitness,
|
||||
std_best_fitness,
|
||||
)
|
||||
|
||||
|
||||
def run_experiments_for_population(pop_size: int) -> PrettyTable:
|
||||
@@ -92,14 +109,22 @@ def run_experiments_for_population(pop_size: int) -> PrettyTable:
|
||||
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 = (
|
||||
run_single_experiment(pop_size, pc, pm)
|
||||
)
|
||||
(
|
||||
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 = "—"
|
||||
|
||||
@@ -118,9 +143,6 @@ def main():
|
||||
print(f"Значения Pc: {PC_VALUES}")
|
||||
print(f"Значения Pm: {PM_VALUES}")
|
||||
print(f"Количество запусков для усреднения: {NUM_RUNS}")
|
||||
print(
|
||||
f"Критерий остановки: среднее значение > {BASE_CONFIG['fitness_avg_threshold']}"
|
||||
)
|
||||
print("=" * 60)
|
||||
|
||||
# Создаем базовую папку
|
||||
|
||||
36
lab2/gen.py
@@ -16,7 +16,7 @@ from numpy.typing import NDArray
|
||||
type Chromosome = NDArray[np.float64]
|
||||
type Population = list[Chromosome]
|
||||
type Fitnesses = NDArray[np.float64]
|
||||
type FitnessFn = Callable[[Chromosome], Fitnesses]
|
||||
type FitnessFn = Callable[[Chromosome], np.float64]
|
||||
type CrossoverFn = Callable[[Chromosome, Chromosome], tuple[Chromosome, Chromosome]]
|
||||
type MutationFn = Callable[[Chromosome], Chromosome]
|
||||
|
||||
@@ -30,6 +30,9 @@ class GARunConfig:
|
||||
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 = (
|
||||
@@ -39,6 +42,10 @@ class GARunConfig:
|
||||
fitness_avg_threshold: float | None = (
|
||||
None # порог среднего значения фитнес функции для остановки
|
||||
)
|
||||
best_value_threshold: float | None = (
|
||||
None # остановка при достижении значения фитнеса лучше заданного
|
||||
)
|
||||
log_every_generation: bool = False # логировать каждое поколение
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -178,7 +185,7 @@ def plot_fitness_surface(
|
||||
x_max: Chromosome,
|
||||
ax: Axes3D,
|
||||
num_points: int = 100,
|
||||
) -> None:
|
||||
):
|
||||
"""Рисует поверхность функции фитнеса в 3D."""
|
||||
assert (
|
||||
x_min.shape == x_max.shape == (2,)
|
||||
@@ -337,6 +344,7 @@ def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
||||
best: Generation | None = None
|
||||
|
||||
generation_number = 1
|
||||
best_repetitions = 0
|
||||
|
||||
while True:
|
||||
# Вычисляем фитнес для всех особей в популяции
|
||||
@@ -357,6 +365,12 @@ def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
||||
)
|
||||
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
|
||||
@@ -371,11 +385,29 @@ def genetic_algorithm(config: GARunConfig) -> GARunResult:
|
||||
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 (
|
||||
|
||||
@@ -3,7 +3,7 @@ import numpy as np
|
||||
from gen import GARunConfig, genetic_algorithm
|
||||
|
||||
|
||||
def fitness_function(chromosome: np.ndarray) -> np.ndarray:
|
||||
def fitness_function(chromosome: np.ndarray) -> np.float64:
|
||||
return chromosome[0] ** 2 + 2 * chromosome[1] ** 2
|
||||
|
||||
|
||||
@@ -11,14 +11,15 @@ config = GARunConfig(
|
||||
x_min=np.array([-5.12, -5.12]),
|
||||
x_max=np.array([5.12, 5.12]),
|
||||
fitness_func=fitness_function,
|
||||
max_generations=200,
|
||||
pop_size=25,
|
||||
pc=0.5,
|
||||
pm=0.01,
|
||||
max_generations=200,
|
||||
max_best_repetitions=10,
|
||||
minimize=True,
|
||||
seed=17,
|
||||
fitness_avg_threshold=0.05,
|
||||
save_generations=[1, 2, 3, 5, 7, 10, 15],
|
||||
save_generations=[1, 2, 3, 5, 7, 9, 10, 15, 19],
|
||||
log_every_generation=True,
|
||||
)
|
||||
|
||||
result = genetic_algorithm(config)
|
||||
|
||||
BIN
lab2/report/img/results/generation_009.png
Normal file
|
After Width: | Height: | Size: 545 KiB |
|
Before Width: | Height: | Size: 543 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 |
@@ -19,6 +19,8 @@
|
||||
\usepackage{afterpage}
|
||||
\usepackage{longtable}
|
||||
\usepackage{float}
|
||||
\usepackage{xcolor}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -153,7 +155,7 @@
|
||||
\item Рассмотреть способы выполнения операторов репродукции,
|
||||
кроссинговера и мутации;
|
||||
\item Выполнить индивидуальное задание на любом языке высокого
|
||||
уровня с необходимыми комментариями и выводами
|
||||
уровня
|
||||
\end{itemize}
|
||||
|
||||
\textbf{Индивидуальное задание вариант 18:}
|
||||
@@ -393,11 +395,11 @@
|
||||
\item $N = 25$ -- размер популяции.
|
||||
\item $p_c = 0.5$ -- вероятность кроссинговера.
|
||||
\item $p_m = 0.01$ -- вероятность мутации.
|
||||
\item $0.05$ -- минимальное среднее значение фитнесс функции по популяции для остановки алгоритма. Глобальный минимум функции равен $f(0, 0) = 0$.
|
||||
\item Алгоритм останавливался, если лучшее значение фитнеса не изменялось $10$ поколений подряд.
|
||||
\item Использован арифметический кроссовер для real-coded хромосом.
|
||||
\end{itemize}
|
||||
|
||||
С каждым поколением точность найденного минимума становится выше. Популяция постепенно сходится к глобальному минимуму в точке $(0, 0)$. На графиках показаны 2D-контурный график (a) и 3D-поверхность целевой функции с точками популяции текущего поколения (b) и (c).
|
||||
Популяция постепенно консолидируется вокруг глобального минимума в точке $(0, 0)$. Лучшая особь была найдена на поколнении №9 (см. Рис.~\ref{fig:gen9}), но судя по всему она подверглась мутации или кроссинговеру, поэтому алгоритм не остановился. На поколении №19 (см. Рис.~\ref{fig:lastgen}) было получено значение фитнеса $0.0201$, которое затем повторялось в следующих 10 поколениях. Алгоритм остановился на поколлении №29. На графиках показаны 2D-контурный график (a) и 3D-поверхность целевой функции с точками популяции текущего поколения (b) и (c).
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
@@ -435,6 +437,13 @@
|
||||
\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}
|
||||
@@ -444,8 +453,15 @@
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
\includegraphics[width=1\linewidth]{img/results/generation_013.png}
|
||||
\caption{График целевой функции и популяции поколения №13}
|
||||
\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}
|
||||
|
||||
@@ -454,6 +470,8 @@
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
\newpage
|
||||
\phantom{text}
|
||||
|
||||
\newpage
|
||||
\section{Исследование реализации}
|
||||
@@ -467,132 +485,253 @@
|
||||
\item $p_m = 0.001, 0.01, 0.05, 0.1, 0.2$ -- вероятность мутации.
|
||||
\end{itemize}
|
||||
|
||||
Результаты измерений представлены в таблицах \ref{tab:pc_pm_results_10}--\ref{tab:pc_pm_results_100}. В ячейках указано время в миллисекундах нахождения минимума функции. В скобках указано количество поколений, за которое было найдено решение. Если в ячейке стоит прочерк, то это означает, что решение не было найдено за 200 поколений. Лучшее значение по времени выполнения для каждого размера популяции выделено жирным шрифтом.
|
||||
Измерения были проведены для двух критериев остановки:
|
||||
\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} & — & — & 8.9 (87) & 5.3 (46) & — \\
|
||||
\textbf{0.4} & — & — & 19.1 (127) & 14.2 (111) & 2.9 (24) \\
|
||||
\textbf{0.5} & — & — & 13.3 (117) & 13.7 (123) & 10.1 (74) \\
|
||||
\textbf{0.6} & — & — & 7.8 (68) & 14.4 (100) & 7.5 (57) \\
|
||||
\textbf{0.7} & — & 6.9 (59) & — & \textbf{1.1 (9)} & — \\
|
||||
\textbf{0.8} & — & — & — & 5.4 (41) & — \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_10}
|
||||
\end{table}
|
||||
\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.2 (17) & 11.8 (55) & — & — \\
|
||||
\textbf{0.4} & — & 2.6 (11) & 4.8 (22) & 17.7 (85) & — \\
|
||||
\textbf{0.5} & \textbf{1.9 (10)} & — & 29.0 (137) & — & — \\
|
||||
\textbf{0.6} & — & 2.7 (13) & 17.6 (81) & 35.7 (157) & — \\
|
||||
\textbf{0.7} & — & 2.6 (13) & 9.1 (38) & 28.3 (119) & — \\
|
||||
\textbf{0.8} & — & 17.6 (76) & 13.7 (57) & 23.4 (95) & — \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_25}
|
||||
\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} & 5.6 (19) & 4.7 (15) & — & — & — \\
|
||||
\textbf{0.4} & \textbf{3.3 (11)} & 48.7 (148) & — & — & — \\
|
||||
\textbf{0.5} & 4.0 (12) & 8.0 (24) & 56.5 (151) & — & — \\
|
||||
\textbf{0.6} & 3.6 (10) & 4.9 (14) & 29.3 (77) & — & — \\
|
||||
\textbf{0.7} & 3.9 (11) & 36.5 (87) & 44.2 (107) & — & — \\
|
||||
\textbf{0.8} & — & 76.4 (189) & 17.3 (41) & — & — \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_50}
|
||||
\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} & 7.8 (14) & 12.6 (22) & — & — & — \\
|
||||
\textbf{0.4} & — & 14.9 (25) & — & — & — \\
|
||||
\textbf{0.5} & 7.3 (12) & 10.9 (17) & — & — & — \\
|
||||
\textbf{0.6} & 8.4 (13) & 12.4 (16) & — & — & — \\
|
||||
\textbf{0.7} & 9.9 (14) & 11.1 (15) & — & — & — \\
|
||||
\textbf{0.8} & \textbf{7.0 (10)} & 28.4 (38) & — & — & — \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\label{tab:pc_pm_results_100}
|
||||
\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{Анализ результатов}
|
||||
|
||||
Ключевые наблюдения:
|
||||
\begin{itemize}
|
||||
\item При небольших популяциях ($N=10$) лучший результат достигается при $p_c=0.7$, $p_m=0.1$ (1.1 мс, 9 пок.). Многие комбинации с низкой мутацией ($p_m \leq 0.01$) и высокой мутацией ($p_m=0.2$) не сходятся за 200 поколений.
|
||||
\item Для $N=25$ оптимальные параметры: $p_c=0.5$, $p_m=0.001$ (1.9 мс, 10 пок.) — лучший результат среди всех экспериментов. Большинство комбинаций с $p_m \geq 0.05$ показывают плохую сходимость.
|
||||
\item Для $N=50$ минимальное время при $p_c=0.4$, $p_m=0.001$ (3.3 мс, 11 пок.). Почти все комбинации с $p_m \geq 0.05$ не сходятся, что указывает на чувствительность к избыточной мутации.
|
||||
\item Для $N=100$ лучший результат при $p_c=0.8$, $p_m=0.001$ (7.0 мс, 10 пок.). Только комбинации с очень низкой мутацией обеспечивают сходимость.
|
||||
\item С ростом размера популяции диапазон работающих параметров сужается: для больших $N$ критична минимальная мутация ($p_m=0.001$).
|
||||
\end{itemize}
|
||||
\subsubsection*{Обоснование применения двух критериев остановки}
|
||||
|
||||
Практические выводы:
|
||||
В исследовании использовались два различных критерия остановки алгоритма, поскольку критерий по количеству поколений (отсутствие улучшения в течение 10 поколений) не всегда обеспечивал достижение достаточно хороших значений фитнеса, особенно для малых популяций. Это делало некорректным сравнение эффективности различных комбинаций параметров только по времени выполнения. Введение второго критерия (достижение фитнеса 0.005) позволило получить более объективную оценку скорости нахождении качественных решений.
|
||||
|
||||
\subsubsection*{Первый критерий остановки (отсутствие улучшения в течение 10 поколений)}
|
||||
|
||||
При использовании первого критерия остановки наблюдаются следующие закономерности:
|
||||
|
||||
\begin{itemize}
|
||||
\item Для данной задачи axis parallel hyper-ellipsoid function оптимальная стратегия — использование очень низких значений мутации ($p_m=0.001$) для популяций $N \geq 25$.
|
||||
\item Малые популяции ($N=10$) требуют умеренной мутации ($p_m=0.1$) для обеспечения достаточного разнообразия.
|
||||
\item Функция показывает высокую чувствительность к параметрам: большинство неоптимальных комбинаций приводят к отсутствию сходимости за 200 поколений.
|
||||
\item Лучшее соотношение скорости и надёжности показывает $N=25$ с минимальной мутацией — компромисс между вычислительными затратами и качеством решения.
|
||||
\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{Вопрос}: Опишите понятие «оптимизационная задача».
|
||||
|
||||
\textbf{Ответ}: Оператор репродукции (ОР) в ГА играет роль селекции. Он выбирает наиболее приспособленных особей для дальнейшего участия в скрещивании и мутации. Это позволяет сохранить наиболее приспособленные особи и постепенно улучшить популяцию.
|
||||
\textbf{Ответ}: Оптимизационная задача — это математическая задача, в которой требуется найти такие значения переменных, при которых некоторая функция, называемая целевой, принимает наибольшее или наименьшее значение. При этом искомые значения должны удовлетворять определённым условиям или ограничениям, задающим допустимую область решений. Цель оптимизации заключается в выборе наилучшего варианта среди множества возможных с точки зрения заданного критерия эффективности.
|
||||
|
||||
Такие задачи широко применяются в науке, технике, экономике и управлении для рационального распределения ресурсов, минимизации затрат или максимизации прибыли. В зависимости от формы целевой функции и ограничений оптимизационные задачи могут быть линейными, нелинейными, дискретными или непрерывными. Их решение позволяет принимать обоснованные решения и повышать эффективность различных процессов и систем.
|
||||
|
||||
|
||||
\newpage
|
||||
\section*{Заключение}
|
||||
\addcontentsline{toc}{section}{Заключение}
|
||||
|
||||
В ходе второй лабораторной работы:
|
||||
В ходе второй лабораторной работы была успешно решена задача оптимизации функции Axis parallel hyper-ellipsoid function с использованием генетических алгоритмов:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Был изучен теоретический материал, основная терминология ГА, генетические операторы,
|
||||
использующиеся в простых ГА;
|
||||
\item Реализована программа на языке Python для нахождения минимума заданной функции;
|
||||
\item Проведено исследование зависимости времени выполнения программы и поколения от мощности популяции и коэффициентов кроссинговера и мутации.
|
||||
\item Изучен теоретический материал о real-coded генетических алгоритмах и различных операторах кроссинговера и мутации;
|
||||
\item Создана программная библиотека на языке Python с реализацией арифметического и геометрического кроссоверов, случайной мутации и селекции методом рулетки;
|
||||
\item Проведено исследование влияния параметров ГА на эффективность поиска для популяций размером 10, 25, 50 и 100 особей;
|
||||
\end{enumerate}
|
||||
|
||||
\newpage
|
||||
|
||||
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}
|
||||