diff --git a/lab3/csv_to_tex.py b/lab3/csv_to_tex.py new file mode 100644 index 0000000..8d19df8 --- /dev/null +++ b/lab3/csv_to_tex.py @@ -0,0 +1,339 @@ +""" +Скрипт для конвертации результатов экспериментов из CSV в LaTeX таблицы. + +Этот скрипт автоматически сканирует папку experiments/, находит все подпапки +с файлами results.csv, парсит данные экспериментов и генерирует LaTeX код +таблиц в формате, готовом для вставки в отчёт. + +Структура входных данных: +- experiments/N/results.csv, где N - размер популяции +- CSV содержит результаты экспериментов с различными параметрами Pc и Pm +- Значения в формате "X.Y (Z)" где X.Y - время выполнения, Z - количество итераций +- "—" для отсутствующих данных + +Выходной файл: tables.tex с готовым LaTeX кодом всех таблиц. +Лучшие результаты по времени и фитнесу выделяются жирным (и цветом, если задан HIGHLIGHT_COLOR). +""" + +import re +from pathlib import Path + +# Настройка цвета для выделения лучших результатов +# None - только жирным, строка (например "magenta") - жирным и цветом +HIGHLIGHT_COLOR = "magenta" + + +def parse_csv_file(csv_path: str) -> tuple[str, list[list[str]]]: + """ + Парсит CSV файл с результатами эксперимента. + + Args: + csv_path: Путь к CSV файлу + + Returns: + Tuple с заголовком и данными таблицы + """ + with open(csv_path, "r", encoding="utf-8") as file: + lines = file.readlines() + + # Удаляем пустые строки и берём только строки с данными + clean_lines = [line.strip() for line in lines if line.strip()] + + # Первая строка - заголовки + header = clean_lines[0] + + # Остальные строки - данные + data_lines = clean_lines[1:] + + # Парсим данные + data_rows = [] + for line in data_lines: + parts = line.split(",") + if len(parts) >= 6: # Pc + 5 значений Pm + data_rows.append(parts) + + return header, data_rows + + +def extract_time_value(value: str) -> float | None: + """ + Извлекает значение времени из строки формата "X.Y (Z)" или "X.Y (Z) W.V". + + Args: + value: Строка с результатом + + Returns: + Время выполнения как float или None если значение пустое + """ + value = value.strip() + if value == "—" or value == "" or value == "–": + return None + + # Ищем паттерн "число.число (число)" + match = re.match(r"(\d+\.?\d*)\s*\(", value) + if match: + return float(match.group(1)) + + return None + + +def extract_fitness_value(value: str) -> float | None: + """ + Извлекает значение фитнеса из строки формата "X.Y (Z) W.V". + + Args: + value: Строка с результатом + + Returns: + Значение фитнеса как float или None если значение пустое + """ + value = value.strip() + if value == "—" or value == "" or value == "–": + return None + + # Ищем паттерн "число.число (число) число.число" + # Фитнес - это последнее число в строке + match = re.search(r"\)\s+(\d+\.?\d*)\s*$", value) + if match: + return float(match.group(1)) + + return None + + +def find_best_time(data_rows: list[list[str]]) -> float | None: + """ + Находит минимальное время выполнения среди всех значений в таблице. + + Args: + data_rows: Строки данных таблицы + + Returns: + Минимальное время или None если нет валидных значений + """ + min_time = None + + for row in data_rows: + for i in range(1, min(6, len(row))): # Пропускаем первую колонку (Pc) + time_value = extract_time_value(row[i]) + if time_value is not None: + if min_time is None or time_value < min_time: + min_time = time_value + + return min_time + + +def find_best_fitness(data_rows: list[list[str]]) -> float | None: + """ + Находит минимальное значение фитнеса среди всех значений в таблице. + + Args: + data_rows: Строки данных таблицы + + Returns: + Минимальное значение фитнеса или None если нет валидных значений + """ + min_fitness = None + + for row in data_rows: + for i in range(1, min(6, len(row))): # Пропускаем первую колонку (Pc) + fitness_value = extract_fitness_value(row[i]) + if fitness_value is not None: + if min_fitness is None or fitness_value < min_fitness: + min_fitness = fitness_value + + return min_fitness + + +def format_value( + value: str, best_time: float | None = None, best_fitness: float | None = None +) -> str: + """ + Форматирует значение для LaTeX таблицы, выделяя лучшие результаты жирным. + + Args: + value: Строковое значение из CSV + best_time: Лучшее время в таблице для сравнения + best_fitness: Лучший фитнес в таблице для сравнения + + Returns: + Отформатированное значение для LaTeX + """ + value = value.strip() + if value == "—" or value == "" or value == "–": + return "—" + + # Проверяем есть ли фитнес в строке + fitness_match = re.search(r"(\d+\.?\d*)\s*\((\d+)\)\s+(\d+\.?\d*)\s*$", value) + + if fitness_match: + # Есть фитнес: "время (поколения) фитнес" + time_str = fitness_match.group(1) + generations_str = fitness_match.group(2) + fitness_str = fitness_match.group(3) + + current_time = float(time_str) + current_fitness = float(fitness_str) + + # Проверяем, является ли время лучшим + time_part = f"{time_str} ({generations_str})" + if best_time is not None and abs(current_time - best_time) < 0.001: + if HIGHLIGHT_COLOR is not None: + time_part = ( + f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{time_part}}}}}" + ) + else: + time_part = f"\\textbf{{{time_part}}}" + + # Проверяем, является ли фитнес лучшим + fitness_part = fitness_str + if best_fitness is not None and abs(current_fitness - best_fitness) < 0.00001: + if HIGHLIGHT_COLOR is not None: + fitness_part = ( + f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{fitness_part}}}}}" + ) + else: + fitness_part = f"\\textbf{{{fitness_part}}}" + + return f"{time_part} {fitness_part}" + + else: + # Нет фитнеса: только "время (поколения)" + time_match = re.match(r"(\d+\.?\d*)\s*\((\d+)\)", value) + if time_match: + current_time = float(time_match.group(1)) + if best_time is not None and abs(current_time - best_time) < 0.001: + if HIGHLIGHT_COLOR is not None: + return f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{value}}}}}" + else: + return f"\\textbf{{{value}}}" + + return value + + +def generate_latex_table(n: str, header: str, data_rows: list[list[str]]) -> str: + """ + Генерирует LaTeX код таблицы. + + Args: + n: Размер популяции + header: Заголовок таблицы + data_rows: Строки данных + + Returns: + LaTeX код таблицы + """ + # Находим лучшее время и лучший фитнес в таблице + best_time = find_best_time(data_rows) + best_fitness = find_best_fitness(data_rows) + + # Извлекаем заголовки колонок из header + header_parts = header.split(",") + pm_values = header_parts[1:] # Пропускаем "Pc \ Pm" + + latex_code = f""" \\begin{{table}}[h!] + \\centering + \\small + \\caption{{Результаты для $N = {n}$}} + \\begin{{tabularx}}{{\\linewidth}}{{l *{{5}}{{Y}}}} + \\toprule + $\\mathbf{{P_c \\;\\backslash\\; P_m}}$""" + + # Добавляем заголовки Pm + for pm in pm_values: + latex_code += f" & \\textbf{{{pm.strip()}}}" + + latex_code += " \\\\\n \\midrule\n" + + # Добавляем строки данных + for row in data_rows: + pc_value = row[0].strip() + latex_code += f" \\textbf{{{pc_value}}}" + + # Добавляем значения для каждого Pm + for i in range(1, min(6, len(row))): # Максимум 5 колонок Pm + value = format_value(row[i], best_time, best_fitness) + latex_code += f" & {value}" + + # Заполняем недостающие колонки если их меньше 5 + for i in range(len(row) - 1, 5): + latex_code += " & —" + + latex_code += " \\\\\n" + + latex_code += f""" \\bottomrule + \\end{{tabularx}} + \\label{{tab:pc_pm_results_{n}}} + \\end{{table}}""" + + return latex_code + + +def main(): + """Основная функция скрипта.""" + experiments_path = Path("experiments") + + if not experiments_path.exists(): + print("Папка experiments не найдена!") + return + + tables = [] + + # Сканируем все подпапки в experiments, сортируем по числовому значению N + subdirs = [ + subdir + for subdir in experiments_path.iterdir() + if subdir.is_dir() and subdir.name.isdigit() + ] + subdirs.sort(key=lambda x: int(x.name)) + + for subdir in subdirs: + n = subdir.name + csv_file = subdir / "results.csv" + + if csv_file.exists(): + print(f"Обрабатываем {csv_file}...") + + try: + header, data_rows = parse_csv_file(str(csv_file)) + best_time = find_best_time(data_rows) + best_fitness = find_best_fitness(data_rows) + latex_table = generate_latex_table(n, header, data_rows) + tables.append(latex_table) + print( + f"✓ Таблица для N={n} готова (лучшее время: {best_time}, лучший фитнес: {best_fitness})" + ) + + except Exception as e: + print(f"✗ Ошибка при обработке {csv_file}: {e}") + else: + print(f"✗ Файл {csv_file} не найден") + + # Сохраняем все таблицы в файл + if tables: + with open("tables.tex", "w", encoding="utf-8") as f: + f.write("% Автоматически сгенерированные LaTeX таблицы\n") + f.write( + "% Лучший результат по времени и по фитнесу выделены жирным отдельно\n" + ) + f.write("% Убедитесь, что подключен \\usepackage{tabularx}\n") + if HIGHLIGHT_COLOR is not None: + f.write( + "% ВНИМАНИЕ: Убедитесь, что подключен \\usepackage{xcolor} для цветового выделения\n" + ) + f.write( + "% Используйте \\newcolumntype{Y}{>{\\centering\\arraybackslash}X} перед таблицами\n\n" + ) + + for i, table in enumerate(tables): + if i > 0: + f.write("\n \n") + f.write(table + "\n") + + print(f"\n✓ Все таблицы сохранены в файл 'tables.tex'") + print(f"Сгенерировано таблиц: {len(tables)}") + else: + print("Не найдено данных для генерации таблиц!") + + +if __name__ == "__main__": + main() diff --git a/lab3/expirements.py b/lab3/expirements.py new file mode 100644 index 0000000..7824636 --- /dev/null +++ b/lab3/expirements.py @@ -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, 50, 100, 500] +PC_VALUES = [0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 1.0] # вероятности кроссинговера +PM_VALUES = [0.05, 0.2, 0.3, 0.4, 0.5, 0.8] # вероятности мутации +SAVE_AVG_BEST_FITNESS = True + +# Количество запусков для усреднения результатов +NUM_RUNS = 1 + +# Базовые параметры (как в 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:.1f} ({avg_generations:.0f})" + + if SAVE_AVG_BEST_FITNESS: + cell_value += f" {avg_best_fitness:.5f}" + + if avg_generations == BASE_CONFIG["max_generations"]: + cell_value = "—" + + row.append(cell_value) + table.add_row(row) + + return table + + +def main(): + """Основная функция для запуска всех экспериментов.""" + print("=" * 60) + print("ЗАПУСК ЭКСПЕРИМЕНТОВ ПО ПАРАМЕТРАМ ГЕНЕТИЧЕСКОГО АЛГОРИТМА") + print("=" * 60) + print(f"Размеры популяции: {POPULATION_SIZES}") + print(f"Значения Pc: {PC_VALUES}") + print(f"Значения Pm: {PM_VALUES}") + print(f"Количество запусков для усреднения: {NUM_RUNS}") + print("=" * 60) + + # Создаем базовую папку + if os.path.exists(BASE_DIR): + shutil.rmtree(BASE_DIR) + os.makedirs(BASE_DIR) + + # Запускаем эксперименты для каждого размера популяции + for pop_size in POPULATION_SIZES: + table = run_experiments_for_population(pop_size) + + print(f"\n{'='*60}") + print(f"РЕЗУЛЬТАТЫ ДЛЯ ПОПУЛЯЦИИ РАЗМЕРОМ {pop_size}") + print(f"{'='*60}") + print( + f"Формат: среднее_время±стд_отклонение_мс (среднее_поколения±стд_отклонение)" + ) + print(f"Усреднено по {NUM_RUNS} запускам") + print(table) + + pop_exp_dir = os.path.join(BASE_DIR, str(pop_size)) + os.makedirs(pop_exp_dir, exist_ok=True) + with open(os.path.join(pop_exp_dir, "results.csv"), "w", encoding="utf-8") as f: + f.write(table.get_csv_string()) + print(f"Результаты сохранены в папке: {pop_exp_dir}") + + print(f"\n{'='*60}") + print("ВСЕ ЭКСПЕРИМЕНТЫ ЗАВЕРШЕНЫ!") + print(f"Результаты сохранены в {BASE_DIR}") + print(f"{'='*60}") + + +if __name__ == "__main__": + main() diff --git a/lab3/gen.py b/lab3/gen.py new file mode 100644 index 0000000..ad9ceda --- /dev/null +++ b/lab3/gen.py @@ -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) diff --git a/lab3/main.py b/lab3/main.py new file mode 100644 index 0000000..709887f --- /dev/null +++ b/lab3/main.py @@ -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") +) diff --git a/lab3/plot_best.py b/lab3/plot_best.py new file mode 100644 index 0000000..d0599b7 --- /dev/null +++ b/lab3/plot_best.py @@ -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) diff --git a/lab3/report/.gitignore b/lab3/report/.gitignore new file mode 100644 index 0000000..38887f8 --- /dev/null +++ b/lab3/report/.gitignore @@ -0,0 +1,6 @@ +* + +!**/ +!.gitignore +!report.tex +!img/**/*.png \ No newline at end of file diff --git a/lab3/report/img/alg.png b/lab3/report/img/alg.png new file mode 100644 index 0000000..f55b76f Binary files /dev/null and b/lab3/report/img/alg.png differ diff --git a/lab3/report/img/optimal_tour.png b/lab3/report/img/optimal_tour.png new file mode 100644 index 0000000..15e5e95 Binary files /dev/null and b/lab3/report/img/optimal_tour.png differ diff --git a/lab3/report/img/results/best_generation_1896.png b/lab3/report/img/results/best_generation_1896.png new file mode 100644 index 0000000..2b2dbac Binary files /dev/null and b/lab3/report/img/results/best_generation_1896.png differ diff --git a/lab3/report/img/results/fitness_history.png b/lab3/report/img/results/fitness_history.png new file mode 100644 index 0000000..d7904e8 Binary files /dev/null and b/lab3/report/img/results/fitness_history.png differ diff --git a/lab3/report/img/results/generation_001.png b/lab3/report/img/results/generation_001.png new file mode 100644 index 0000000..62dddea Binary files /dev/null and b/lab3/report/img/results/generation_001.png differ diff --git a/lab3/report/img/results/generation_005.png b/lab3/report/img/results/generation_005.png new file mode 100644 index 0000000..cf0c2fd Binary files /dev/null and b/lab3/report/img/results/generation_005.png differ diff --git a/lab3/report/img/results/generation_050.png b/lab3/report/img/results/generation_050.png new file mode 100644 index 0000000..c25d2e7 Binary files /dev/null and b/lab3/report/img/results/generation_050.png differ diff --git a/lab3/report/img/results/generation_100.png b/lab3/report/img/results/generation_100.png new file mode 100644 index 0000000..c395d1e Binary files /dev/null and b/lab3/report/img/results/generation_100.png differ diff --git a/lab3/report/img/results/generation_300.png b/lab3/report/img/results/generation_300.png new file mode 100644 index 0000000..db5e327 Binary files /dev/null and b/lab3/report/img/results/generation_300.png differ diff --git a/lab3/report/img/results/generation_500.png b/lab3/report/img/results/generation_500.png new file mode 100644 index 0000000..9fe6d4d Binary files /dev/null and b/lab3/report/img/results/generation_500.png differ diff --git a/lab3/report/img/results/generation_900.png b/lab3/report/img/results/generation_900.png new file mode 100644 index 0000000..b8f7222 Binary files /dev/null and b/lab3/report/img/results/generation_900.png differ diff --git a/lab3/report/report.tex b/lab3/report/report.tex new file mode 100644 index 0000000..30b2c39 --- /dev/null +++ b/lab3/report/report.tex @@ -0,0 +1,793 @@ +\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 + \phantom{text} + \newpage + \phantom{text} + + \newpage + \section{Исследование реализации} + \subsection{Проведение измерений} + В рамках лабораторной работы необходимо было исследовать зависимость времени выполнения задачи и количества поколений от популяции и вероятностей кроссинговера и мутации хромосомы + + Для исследования были выбраны следующие значения параметров: + \begin{itemize} + \item $N = 10, 25, 50, 100$ -- размер популяции. + \item $p_c = 0.3, 0.4, 0.5, 0.6, 0.7, 0.8$ -- вероятность кроссинговера. + \item $p_m = 0.001, 0.01, 0.05, 0.1, 0.2$ -- вероятность мутации. + \end{itemize} + + Измерения были проведены для двух критериев остановки: + \begin{itemize} + \item Лучшее значение фитнеса не изменялось 10 поколений. + \item Лучшее значение фитнеса достигло заданного значения $0.005$. + \end{itemize} + + \subsubsection*{Результаты для первого критерия остановки} + + Результаты измерений представлены в таблицах \ref{tab:pc_pm_results_10}--\ref{tab:pc_pm_results_100}. В ячейках указано время в миллисекундах нахождения минимума функции. В скобках указано количество поколений, за которое было найдено решение. Во второй строке указано усреднённое по всем запускам лучшее значение фитнеса. Если в ячейке стоит прочерк, то это означает, что решение не было найдено за 200 поколений. Лучшее значение по времени выполнения и по значению фитнеса для каждого размера популяции выделено цветом и жирным шрифтом. + + \newcolumntype{Y}{>{\centering\arraybackslash}X} + % Автоматически сгенерированные LaTeX таблицы +% Лучший результат по времени и по фитнесу выделены жирным отдельно +% Убедитесь, что подключен \usepackage{tabularx} +% ВНИМАНИЕ: Убедитесь, что подключен \usepackage{xcolor} для цветового выделения +% Используйте \newcolumntype{Y}{>{\centering\arraybackslash}X} перед таблицами + +\begin{table}[h!] + \centering + \small + \caption{Результаты для $N = 10$} + \begin{tabularx}{\linewidth}{l *{5}{Y}} + \toprule + $\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\ + \midrule + \textbf{0.3} & 1.3 (13) 0.36281 & 1.7 (18) 7.55685 & 1.2 (13) 1.55537 & \textcolor{magenta}{\textbf{1.0 (11)}} 1.78411 & 9.4 (87) 0.04271 \\ + \textbf{0.4} & 1.3 (14) 0.03913 & 1.6 (17) 0.02868 & 1.3 (13) 0.36232 & 2.1 (20) 0.10641 & — \\ + \textbf{0.5} & 1.4 (15) 0.87081 & 1.7 (18) 1.71634 & 2.3 (21) 0.10401 & 3.4 (25) 0.00461 & — \\ + \textbf{0.6} & 2.8 (19) 0.06375 & 1.8 (13) 0.72202 & 2.9 (22) 0.01473 & 3.4 (25) 0.01162 & 29.4 (184) \textcolor{magenta}{\textbf{0.00033}} \\ + \textbf{0.7} & 1.5 (15) 1.25409 & 2.3 (22) 8.67464 & 1.9 (18) 0.13319 & 8.6 (66) 0.00078 & 8.9 (48) 0.11136 \\ + \textbf{0.8} & 1.9 (15) 3.10415 & 1.4 (13) 1.09275 & 2.1 (19) 0.43094 & 6.4 (54) 0.00191 & — \\ + \bottomrule + \end{tabularx} + \label{tab:pc_pm_results_10} +\end{table} + + +\begin{table}[h!] + \centering + \small + \caption{Результаты для $N = 25$} + \begin{tabularx}{\linewidth}{l *{5}{Y}} + \toprule + $\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\ + \midrule + \textbf{0.3} & 3.0 (18) 0.16836 & \textcolor{magenta}{\textbf{2.2 (13)}} 0.04190 & 4.7 (27) 0.00544 & — & — \\ + \textbf{0.4} & 4.1 (24) 0.00808 & 4.6 (26) 0.01101 & 5.8 (31) 0.02330 & 3.8 (19) 0.05414 & — \\ + \textbf{0.5} & 3.1 (17) 0.05259 & 5.0 (26) 0.47018 & 27.8 (138) \textcolor{magenta}{\textbf{0.00024}} & 14.5 (67) 0.00312 & — \\ + \textbf{0.6} & 6.1 (31) 0.01033 & 6.8 (34) 0.00148 & — & — & — \\ + \textbf{0.7} & 4.1 (21) 0.00107 & 3.2 (16) 0.32522 & — & — & — \\ + \textbf{0.8} & 23.9 (109) 0.00352 & 15.8 (72) 0.11662 & 28.3 (123) 0.00038 & — & — \\ + \bottomrule + \end{tabularx} + \label{tab:pc_pm_results_25} +\end{table} + + +\begin{table}[h!] + \centering + \small + \caption{Результаты для $N = 50$} + \begin{tabularx}{\linewidth}{l *{5}{Y}} + \toprule + $\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\ + \midrule + \textbf{0.3} & 14.9 (51) 0.05874 & 19.3 (59) \textcolor{magenta}{\textbf{0.00003}} & 36.7 (113) 0.00190 & — & — \\ + \textbf{0.4} & 12.5 (40) 0.01955 & \textcolor{magenta}{\textbf{5.6 (18)}} 0.00022 & — & — & — \\ + \textbf{0.5} & 65.0 (195) 0.04790 & 26.4 (78) 0.01673 & — & — & — \\ + \textbf{0.6} & 16.4 (47) 0.00329 & 18.5 (50) 0.00065 & — & — & — \\ + \textbf{0.7} & 51.0 (137) 0.00120 & 59.3 (158) 0.00010 & — & — & — \\ + \textbf{0.8} & 48.8 (126) 0.01393 & 67.6 (172) 0.00650 & — & — & — \\ + \bottomrule + \end{tabularx} + \label{tab:pc_pm_results_50} +\end{table} + + +\begin{table}[h!] + \centering + \small + \caption{Результаты для $N = 100$} + \begin{tabularx}{\linewidth}{l *{5}{Y}} + \toprule + $\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\ + \midrule + \textbf{0.3} & 24.2 (44) 0.00110 & 17.9 (32) 0.00113 & \textcolor{magenta}{\textbf{17.6 (29)}} 0.00193 & — & — \\ + \textbf{0.4} & 30.7 (51) 0.00173 & — & — & — & — \\ + \textbf{0.5} & 27.4 (43) 0.00016 & — & — & — & — \\ + \textbf{0.6} & 20.4 (31) 0.00115 & 129.8 (186) 0.00025 & — & — & — \\ + \textbf{0.7} & 115.4 (162) 0.00002 & — & — & — & — \\ + \textbf{0.8} & 106.5 (143) \textcolor{magenta}{\textbf{0.00001}} & — & — & — & — \\ + \bottomrule + \end{tabularx} + \label{tab:pc_pm_results_100} +\end{table} + + \newpage + \phantom{text} + \newpage + \subsubsection*{Результаты для второго критерия остановки} + +Результаты измерений представлены в таблицах \ref{tab:1_pc_pm_results_10}--\ref{tab:1_pc_pm_results_100}. + + +% Автоматически сгенерированные LaTeX таблицы +% Лучший результат по времени и по фитнесу выделены жирным отдельно +% Убедитесь, что подключен \usepackage{tabularx} +% ВНИМАНИЕ: Убедитесь, что подключен \usepackage{xcolor} для цветового выделения +% Используйте \newcolumntype{Y}{>{\centering\arraybackslash}X} перед таблицами + +\begin{table}[h!] + \centering + \small + \caption{Результаты для $N = 10$} + \begin{tabularx}{\linewidth}{l *{5}{Y}} + \toprule + $\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\ + \midrule + \textbf{0.3} & — & — & — & 15.6 (155) 0.00063 & 7.8 (69) 0.00409 \\ + \textbf{0.4} & — & — & — & 8.9 (81) \textcolor{magenta}{\textbf{0.00038}} & \textcolor{magenta}{\textbf{4.6 (40)}} 0.00317 \\ + \textbf{0.5} & — & — & 8.7 (85) 0.00199 & — & 16.5 (140) 0.00453 \\ + \textbf{0.6} & — & — & — & 8.9 (77) 0.00310 & 14.3 (117) 0.00082 \\ + \textbf{0.7} & — & — & 8.2 (70) 0.00089 & 5.6 (49) 0.00431 & 7.1 (58) 0.00047 \\ + \textbf{0.8} & — & 19.7 (180) 0.00397 & — & 5.0 (42) 0.00494 & 5.5 (44) 0.00357 \\ + \bottomrule + \end{tabularx} + \label{tab:1_pc_pm_results_10} +\end{table} + + +\begin{table}[h!] + \centering + \small + \caption{Результаты для $N = 25$} + \begin{tabularx}{\linewidth}{l *{5}{Y}} + \toprule + $\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\ + \midrule + \textbf{0.3} & 1.1 (7) 0.00277 & 30.0 (173) \textcolor{magenta}{\textbf{0.00059}} & — & 2.2 (12) 0.00191 & 30.2 (139) 0.00200 \\ + \textbf{0.4} & 1.8 (10) 0.00384 & — & 12.2 (63) 0.00164 & 6.6 (33) 0.00354 & 18.5 (82) 0.00224 \\ + \textbf{0.5} & — & — & 12.5 (58) 0.00233 & 2.3 (11) 0.00196 & 17.1 (73) 0.00116 \\ + \textbf{0.6} & — & 30.9 (151) 0.00265 & 36.7 (175) 0.00146 & 10.0 (46) 0.00449 & 5.7 (23) 0.00281 \\ + \textbf{0.7} & 1.1 (6) 0.00472 & — & 0.8 (4) 0.00233 & 3.9 (17) 0.00112 & \textcolor{magenta}{\textbf{0.3 (2)}} 0.00371 \\ + \textbf{0.8} & — & — & 10.3 (43) 0.00137 & 7.7 (32) 0.00379 & 10.5 (41) 0.00155 \\ + \bottomrule + \end{tabularx} + \label{tab:1_pc_pm_results_25} +\end{table} + + +\begin{table}[h!] + \centering + \small + \caption{Результаты для $N = 50$} + \begin{tabularx}{\linewidth}{l *{5}{Y}} + \toprule + $\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\ + \midrule + \textbf{0.3} & 3.7 (12) 0.00354 & 3.4 (9) 0.00075 & 23.7 (73) 0.00467 & 4.9 (14) 0.00043 & 2.1 (6) 0.00029 \\ + \textbf{0.4} & 3.6 (12) 0.00270 & 4.2 (13) 0.00061 & 9.2 (25) 0.00251 & 18.2 (51) 0.00490 & 6.6 (16) 0.00063 \\ + \textbf{0.5} & 4.0 (10) 0.00099 & 48.8 (141) 0.00324 & 3.8 (11) 0.00087 & 14.7 (39) \textcolor{magenta}{\textbf{0.00017}} & 1.2 (3) 0.00115 \\ + \textbf{0.6} & 1.6 (5) 0.00070 & 51.6 (139) 0.00217 & 4.7 (13) 0.00294 & 2.6 (7) 0.00397 & 11.5 (27) 0.00053 \\ + \textbf{0.7} & — & — & 2.6 (7) 0.00144 & 3.5 (9) 0.00182 & \textcolor{magenta}{\textbf{1.1 (3)}} 0.00072 \\ + \textbf{0.8} & 4.1 (11) 0.00240 & 3.5 (8) 0.00380 & 2.5 (6) 0.00422 & 2.7 (7) 0.00126 & 4.3 (10) 0.00060 \\ + \bottomrule + \end{tabularx} + \label{tab:1_pc_pm_results_50} +\end{table} + + +\begin{table}[h!] + \centering + \small + \caption{Результаты для $N = 100$} + \begin{tabularx}{\linewidth}{l *{5}{Y}} + \toprule + $\mathbf{P_c \;\backslash\; P_m}$ & \textbf{0.001} & \textbf{0.010} & \textbf{0.050} & \textbf{0.100} & \textbf{0.200} \\ + \midrule + \textbf{0.3} & 9.3 (17) 0.00451 & 6.0 (11) 0.00344 & 10.0 (17) 0.00343 & 5.3 (8) 0.00046 & 9.8 (14) 0.00412 \\ + \textbf{0.4} & 5.7 (9) \textcolor{magenta}{\textbf{0.00005}} & 8.4 (14) 0.00108 & 3.5 (6) 0.00254 & 4.0 (6) 0.00186 & 6.5 (9) 0.00283 \\ + \textbf{0.5} & 3.8 (6) 0.00019 & 4.9 (8) 0.00103 & 3.6 (6) 0.00260 & 11.1 (16) 0.00204 & 7.5 (10) 0.00374 \\ + \textbf{0.6} & — & 6.5 (10) 0.00107 & 3.6 (5) 0.00079 & \textcolor{magenta}{\textbf{0.9 (2)}} 0.00324 & 10.1 (13) 0.00044 \\ + \textbf{0.7} & 1.7 (3) 0.00106 & 6.6 (10) 0.00489 & 4.1 (6) 0.00031 & 12.4 (16) 0.00240 & 4.8 (6) 0.00276 \\ + \textbf{0.8} & 5.0 (7) 0.00387 & 58.4 (77) 0.00453 & 7.8 (10) 0.00259 & 11.2 (13) 0.00210 & 6.1 (7) 0.00493 \\ + \bottomrule + \end{tabularx} + \label{tab:1_pc_pm_results_100} +\end{table} + + + \newpage + \phantom{text} + \newpage + \phantom{text} + \newpage + \phantom{text} + \subsection{Анализ результатов} + + \subsubsection*{Обоснование применения двух критериев остановки} + + В исследовании использовались два различных критерия остановки алгоритма, поскольку критерий по количеству поколений (отсутствие улучшения в течение 10 поколений) не всегда обеспечивал достижение достаточно хороших значений фитнеса, особенно для малых популяций. Это делало некорректным сравнение эффективности различных комбинаций параметров только по времени выполнения. Введение второго критерия (достижение фитнеса 0.005) позволило получить более объективную оценку скорости нахождении качественных решений. + + \subsubsection*{Первый критерий остановки (отсутствие улучшения в течение 10 поколений)} + + При использовании первого критерия остановки наблюдаются следующие закономерности: + + \begin{itemize} + \item \textbf{Малые популяции ($N=10$):} Оптимальный баланс достигается при умеренных значениях параметров. Лучший результат по времени показывает комбинация $p_c=0.3$, $p_m=0.1$ (1.0 мс, 11 поколений), однако лучшее значение фитнеса достигается при $p_c=0.6$, $p_m=0.2$ (0.00033). Качество решений существенно варьируется. + + \item \textbf{Средние популяции ($N=25$):} Демонстрируют высокую эффективность при низких значениях мутации. Минимальное время выполнения достигается при $p_c=0.3$, $p_m=0.01$ (2.2 мс, 13 поколений), а наилучший фитнес — при $p_c=0.5$, $p_m=0.05$ (0.00024). + + \item \textbf{Большие популяции ($N=50, 100$):} Характеризуются критической чувствительностью к высоким значениям мутации и демонстрируют заметное улучшение качества фитнеса. Для $N=50$ лучшие результаты при $p_c=0.4$, $p_m=0.01$ (5.6 мс по времени) и $p_c=0.3$, $p_m=0.01$ (фитнес 0.00003). Для $N=100$ работают только комбинации с очень низкой мутацией, но обеспечивают отличное качество (фитнес до 0.00001). + + \item \textbf{Проблема сходимости:} С увеличением размера популяции значительно возрастает количество комбинаций параметров, не обеспечивающих сходимость за 200 поколений, особенно при $p_m \geq 0.05$. + \end{itemize} + + \subsubsection*{Второй критерий остановки (достижение фитнеса 0.005)} + + Использование фиксированного порога фитнеса демонстрирует принципиально иную картину и подтверждает правильность введения альтернативного критерия: + + \begin{itemize} + \item \textbf{Инверсия требований к мутации:} В отличие от первого критерия, здесь малые популяции требуют более высоких значений мутации для достижения целевого фитнеса. Для $N=10$ большинство комбинаций с $p_m \leq 0.01$ вообще не достигают порога, что подтверждает проблему качества при первом критерии. + + \item \textbf{Лучшие результаты больших популяций:} Популяции $N=50$ и $N=100$ показывают отличные результаты — достижение высокого качества за минимальное время: $N=50$ при $p_c=0.7$, $p_m=0.2$ (1.1 мс, 3 поколения) и $N=100$ при $p_c=0.6$, $p_m=0.1$ (0.9 мс, 2 поколения). + \end{itemize} + + + \newpage + \section{Ответ на контрольный вопрос} + + \textbf{Вопрос}: Опишите понятие «оптимизационная задача». + + \textbf{Ответ}: Оптимизационная задача — это математическая задача, в которой требуется найти такие значения переменных, при которых некоторая функция, называемая целевой, принимает наибольшее или наименьшее значение. При этом искомые значения должны удовлетворять определённым условиям или ограничениям, задающим допустимую область решений. Цель оптимизации заключается в выборе наилучшего варианта среди множества возможных с точки зрения заданного критерия эффективности. + + Такие задачи широко применяются в науке, технике, экономике и управлении для рационального распределения ресурсов, минимизации затрат или максимизации прибыли. В зависимости от формы целевой функции и ограничений оптимизационные задачи могут быть линейными, нелинейными, дискретными или непрерывными. Их решение позволяет принимать обоснованные решения и повышать эффективность различных процессов и систем. + + + \newpage + \section*{Заключение} + \addcontentsline{toc}{section}{Заключение} + + В ходе второй лабораторной работы была успешно решена задача оптимизации функции Axis parallel hyper-ellipsoid function с использованием генетических алгоритмов: + + \begin{enumerate} + \item Изучен теоретический материал о real-coded генетических алгоритмах и различных операторах кроссинговера и мутации; + \item Создана программная библиотека на языке Python с реализацией арифметического и геометрического кроссоверов, случайной мутации и селекции методом рулетки; + \item Проведено исследование влияния параметров ГА на эффективность поиска для популяций размером 10, 25, 50 и 100 особей; + \end{enumerate} + +\newpage +\section*{Список литературы} +\addcontentsline{toc}{section}{Список литературы} + +\vspace{-1.5cm} +\begin{thebibliography}{0} + \bibitem{vostrov} + Методические указания по выполнению лабораторных работ к курсу «Генетические алгоритмы», 119 стр. +\end{thebibliography} + +\end{document} \ No newline at end of file