From 6400996fcf886c53cc20fc44a30dcd23c3f49893 Mon Sep 17 00:00:00 2001 From: Arity-T Date: Wed, 12 Nov 2025 15:46:37 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A2=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lab5/csv_to_tex.py | 327 +++++++++++++++++++++++++++++++++++++++++ lab5/report/report.tex | 109 +++++++++----- 2 files changed, 401 insertions(+), 35 deletions(-) create mode 100644 lab5/csv_to_tex.py diff --git a/lab5/csv_to_tex.py b/lab5/csv_to_tex.py new file mode 100644 index 0000000..734ba67 --- /dev/null +++ b/lab5/csv_to_tex.py @@ -0,0 +1,327 @@ +""" +Скрипт для конвертации результатов экспериментов из CSV в LaTeX таблицы для lab5. + +Адаптирован из lab2/csv_to_tex.py для работы с форматом эволюционных стратегий. +Формат входных данных: "время±стд (поколения±стд) фитнес" +""" + +import re +from pathlib import Path + +# Настройка цвета для выделения лучших результатов +# None - только жирным, строка (например "magenta") - жирным и цветом +HIGHLIGHT_COLOR = "magenta" + + +def parse_csv_file(csv_path: str) -> tuple[str, list[list[str]]]: + """ + Парсит CSV файл с результатами эксперимента. + + Args: + csv_path: Путь к CSV файлу + + Returns: + Tuple с заголовком и данными таблицы + """ + with open(csv_path, "r", encoding="utf-8") as file: + lines = file.readlines() + + # Удаляем пустые строки и берём только строки с данными + clean_lines = [line.strip() for line in lines if line.strip()] + + # Первая строка - заголовки + header = clean_lines[0] + + # Остальные строки - данные + data_lines = clean_lines[1:] + + # Парсим данные + data_rows = [] + for line in data_lines: + parts = line.split(",") + if len(parts) >= 2: # mu + хотя бы одно значение + data_rows.append(parts) + + return header, data_rows + + +def extract_time_value(value: str) -> float | None: + """ + Извлекает значение времени из строки формата "X.Y±Z.W (...)". + + Args: + value: Строка с результатом + + Returns: + Время выполнения как float или None если значение пустое + """ + value = value.strip() + if value == "—" or value == "" or value == "–": + return None + + # Ищем паттерн "число.число±число" + match = re.match(r"(\d+\.?\d*)±", value) + if match: + return float(match.group(1)) + + # Если нет ±, пробуем просто число перед скобкой + match = re.match(r"(\d+\.?\d*)\s*\(", value) + if match: + return float(match.group(1)) + + return None + + +def extract_generations_value(value: str) -> float | None: + """ + Извлекает среднее число поколений из строки формата "... (X±Y) ...". + + Args: + value: Строка с результатом + + Returns: + Среднее число поколений как float или None если значение пустое + """ + value = value.strip() + if value == "—" or value == "" or value == "–": + return None + + # Ищем паттерн "(число±число)" и берём первое число + match = re.search(r"\((\d+\.?\d*)±", value) + if match: + return float(match.group(1)) + + # Если нет ±, пробуем просто число в скобках + match = re.search(r"\((\d+\.?\d*)\)", value) + if match: + return float(match.group(1)) + + return None + + +def find_best_time(data_rows: list[list[str]]) -> float | None: + """ + Находит минимальное время выполнения среди всех значений в таблице. + + Args: + data_rows: Строки данных таблицы + + Returns: + Минимальное время или None если нет валидных значений + """ + min_time = None + + for row in data_rows: + for i in range(1, len(row)): # Пропускаем первую колонку (mu) + time_value = extract_time_value(row[i]) + if time_value is not None: + if min_time is None or time_value < min_time: + min_time = time_value + + return min_time + + +def find_best_generations(data_rows: list[list[str]]) -> float | None: + """ + Находит минимальное число поколений среди всех значений в таблице. + + Args: + data_rows: Строки данных таблицы + + Returns: + Минимальное число поколений или None если нет валидных значений + """ + min_gens = None + + for row in data_rows: + for i in range(1, len(row)): # Пропускаем первую колонку (mu) + gens_value = extract_generations_value(row[i]) + if gens_value is not None: + if min_gens is None or gens_value < min_gens: + min_gens = gens_value + + return min_gens + + +def format_value( + value: str, best_time: float | None = None, best_gens: float | None = None +) -> str: + """ + Форматирует значение для LaTeX таблицы, выделяя лучшие результаты жирным. + + Args: + value: Строковое значение из CSV + best_time: Лучшее время в таблице для сравнения + best_gens: Лучшее число поколений для сравнения + + Returns: + Отформатированное значение для LaTeX + """ + value = value.strip() + if value == "—" or value == "" or value == "–": + return "—" + + # Парсим значение: "время±стд (поколения±стд) фитнес" + # Пример: "60.6±47.9 (37±29) 0.0000" + pattern = r"(\d+\.?\d*)±(\d+\.?\d*)\s*\((\d+\.?\d*)±(\d+\.?\d*)\)\s+(\d+\.?\d+)" + match = re.match(pattern, value) + + if not match: + # Если не удалось распарсить, возвращаем как есть + return value + + time_avg = float(match.group(1)) + time_std = float(match.group(2)) + gens_avg = float(match.group(3)) + gens_std = float(match.group(4)) + fitness = match.group(5) + + # Формируем части БЕЗ стандартных отклонений + time_part = f"{time_avg:.1f}" + gens_part = f"{gens_avg:.0f}" + + # Проверяем, является ли время лучшим + is_best_time = best_time is not None and abs(time_avg - best_time) < 0.1 + is_best_gens = best_gens is not None and abs(gens_avg - best_gens) < 0.1 + + # Выделяем лучшее время + if is_best_time: + if HIGHLIGHT_COLOR is not None: + time_part = f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{time_part}}}}}" + else: + time_part = f"\\textbf{{{time_part}}}" + + # Выделяем лучшее число поколений + if is_best_gens: + if HIGHLIGHT_COLOR is not None: + gens_part = f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{gens_part}}}}}" + else: + gens_part = f"\\textbf{{{gens_part}}}" + + # Не показываем фитнес в таблице, т.к. он всегда близок к нулю + return f"{time_part} ({gens_part})" + + +def generate_latex_table(dimension: str, header: str, data_rows: list[list[str]]) -> str: + """ + Генерирует LaTeX код таблицы. + + Args: + dimension: Размерность задачи (2 или 3) + header: Заголовок таблицы + data_rows: Строки данных + + Returns: + LaTeX код таблицы + """ + # Находим лучшее время и лучшее число поколений в таблице + best_time = find_best_time(data_rows) + best_gens = find_best_generations(data_rows) + + # Извлекаем заголовки колонок из header + header_parts = header.split(",") + p_mut_values = header_parts[1:] # Пропускаем "mu \ p_mut" + + num_cols = len(p_mut_values) + + latex_code = f""" \\begin{{table}}[h!] + \\centering + \\small + \\caption{{Результаты для $n = {dimension}$. Формат: время в мс (число поколений)}} + \\begin{{tabularx}}{{{0.95 if num_cols <= 5 else 1.0}\\linewidth}}{{l *{{{num_cols}}}{{Y}}}} + \\toprule + $\\mathbf{{\\mu \\;\\backslash\\; p_{{mut}}}}$""" + + # Добавляем заголовки p_mut + for p_mut in p_mut_values: + latex_code += f" & \\textbf{{{p_mut.strip()}}}" + + latex_code += " \\\\\n \\midrule\n" + + # Добавляем строки данных + for row in data_rows: + mu_value = row[0].strip() + latex_code += f" \\textbf{{{mu_value}}}" + + # Добавляем значения для каждого p_mut + for i in range(1, len(row)): + value = format_value(row[i], best_time, best_gens) + latex_code += f" & {value}" + + # Заполняем недостающие колонки если их меньше чем в заголовке + for i in range(len(row) - 1, num_cols): + latex_code += " & —" + + latex_code += " \\\\\n" + + latex_code += f""" \\bottomrule + \\end{{tabularx}} + \\label{{tab:es_results_{dimension}}} + \\end{{table}}""" + + return latex_code + + +def main(): + """Основная функция скрипта.""" + experiments_path = Path("lab5_experiments") + + if not experiments_path.exists(): + print("Папка lab5_experiments не найдена!") + return + + tables = [] + + # Обрабатываем файлы dimension_2.csv и dimension_3.csv + for dimension in [2, 3]: + csv_file = experiments_path / f"dimension_{dimension}.csv" + + if csv_file.exists(): + print(f"Обрабатываем {csv_file}...") + + try: + header, data_rows = parse_csv_file(str(csv_file)) + best_time = find_best_time(data_rows) + best_gens = find_best_generations(data_rows) + latex_table = generate_latex_table(str(dimension), header, data_rows) + tables.append(latex_table) + print( + f"[OK] Таблица для n={dimension} готова (лучшее время: {best_time:.1f} мс, лучшее число поколений: {best_gens:.0f})" + ) + + except Exception as e: + print(f"[ERROR] Ошибка при обработке {csv_file}: {e}") + else: + print(f"[ERROR] Файл {csv_file} не найден") + + # Сохраняем все таблицы в файл + if tables: + output_file = experiments_path / "tables.tex" + with open(output_file, "w", encoding="utf-8") as f: + f.write("% Автоматически сгенерированные LaTeX таблицы\n") + f.write( + "% Лучший результат по времени и по числу поколений выделены жирным отдельно\n" + ) + f.write("% Убедитесь, что подключен \\usepackage{tabularx}\n") + if HIGHLIGHT_COLOR is not None: + f.write( + "% ВНИМАНИЕ: Убедитесь, что подключен \\usepackage{xcolor} для цветового выделения\n" + ) + f.write( + "% Используйте \\newcolumntype{Y}{>{\\centering\\arraybackslash}X} перед таблицами\n\n" + ) + + for i, table in enumerate(tables): + if i > 0: + f.write("\n \n") + f.write(table + "\n") + + print(f"\n[OK] Все таблицы сохранены в файл '{output_file}'") + print(f"Сгенерировано таблиц: {len(tables)}") + else: + print("Не найдено данных для генерации таблиц!") + + +if __name__ == "__main__": + main() + diff --git a/lab5/report/report.tex b/lab5/report/report.tex index fec47ce..a0c8037 100644 --- a/lab5/report/report.tex +++ b/lab5/report/report.tex @@ -228,38 +228,20 @@ \newpage \section{Результаты работы} - Для анализа параметров стратегии подготовлен набор серийных экспериментов. В таблице~\ref{tab:configs} представлены базовые комбинации, используемые для минимизации функции при $n=2$ и $n=3$. + Для демонстрации работы алгоритма была выполнена визуализация процесса оптимизации двумерной функции ($n=2$) со следующими параметрами: - \newcolumntype{Y}{>{\centering\arraybackslash}X} - \begin{table}[h!] - \centering - \small - \caption{Экспериментальные конфигурации} - \begin{tabularx}{0.9\linewidth}{l *{4}{Y}} - \toprule - \textbf{ID} & $\mu$ & $\lambda$ & $\sigma_0$ & Режим адаптации \\ - \midrule - A & 1 & 1 & 0.5 & правило успеха $1/5$ \\ - B & 5 & 25 & 0.3 & логнормальная самоадаптация \\ - C & 10 & 70 & 0.2 & фиксированное $\sigma$ \\ - D & 15 & 105 & 0.2 & смешанный: рекомбинация $+$ правило $1/5$ \\ - \bottomrule - \end{tabularx} - \label{tab:configs} - \end{table} + \begin{itemize} + \item $\mu = 20$ -- размер популяции родителей. + \item $\lambda = 80$ -- число потомков ($\lambda = 4\mu$). + \item $p_{mut} = 0.7$ -- вероятность мутации каждой координаты. + \item Промежуточная рекомбинация двух родителей. + \item $(\mu, \lambda)$-селекция: родители полностью заменяются. + \item Адаптивное масштабирование шага мутации по правилу успеха $1/5$. + \item Начальное стандартное отклонение $\sigma_0 = 0.15 \cdot (x_{max} - x_{min})$. + \end{itemize} - Визуализация для двумерного случая воспроизводит поверхность целевой функции и положение популяции на каждом шаге. Пошаговый режим позволяет наблюдать влияние изменения дисперсий: при успешных мутациях облако точек расширяется, при неудачах сжимается вокруг текущего минимума. Для трёхмерного случая графически отображается последовательность лучших точек и динамика величины функции во времени. + Визуализация воспроизводит поверхность целевой функции и положение популяции на каждом шаге. Пошаговый режим позволяет наблюдать влияние изменения дисперсий: при успешных мутациях облако точек расширяется, при неудачах сжимается вокруг текущего минимума. Популяция постепенно консолидируется вокруг глобального минимума в точке $(0, 0)$. - \subsection{Пошаговая визуализация процесса оптимизации} - - Чтобы получить в отчёт те же трёхмерные графики, что присутствовали во второй лабораторной работе, подготовлен отдельный скрипт \texttt{lab5/generate\_report\_figures.py}. Он переиспользует функцию визуализации из модуля \texttt{main.py}, на каждом указанном поколении строит контурный и два трёхмерных вида поверхности и сохраняет кадры в каталог \texttt{lab5/report/img/results}. Команды следует выполнять из корня репозитория, предварительно установив зависимости: - - \begin{verbatim} - pip install numpy matplotlib - python lab5/generate_report_figures.py - \end{verbatim} - - После выполнения команды изображения автоматически появятся в каталоге отчёта и будут подхвачены при компиляции \LaTeX-документа. \begin{figure}[H] \centering @@ -310,16 +292,73 @@ \end{figure} - При запуске экспериментов собираются следующие показатели: + \newpage + \section{Исследование параметров} + + В рамках лабораторной работы было проведено исследование влияния размера популяции $\mu$ и вероятности мутации $p_{mut}$ на эффективность алгоритма. Для экспериментов использовалась $(\mu, \lambda)$-стратегия с $\lambda = 5\mu$, промежуточной рекомбинацией и адаптивным масштабированием шага мутации по правилу успеха $1/5$. + + \subsection{Проведение измерений} + + Для исследования были выбраны следующие значения параметров: \begin{itemize} - \item число поколений до достижения целевого порога $f(\mathbf{x}) < 10^{-6}$ либо исчерпания лимита поколений; - \item итоговая точность (значение функции и евклидово расстояние до нулевого вектора); - \item суммарное процессорное время на серию запусков (возвращается в миллисекундах); - \item статистика успехов для правила $1/5$ и распределение актуальных $\sigma_i$. + \item $\mu = 5, 10, 20, 40$ -- размер популяции родителей. + \item $p_{mut} = 0.3, 0.5, 0.7, 0.9, 1.0$ -- вероятность мутации каждой координаты. + \item Количество независимых запусков для усреднения результатов: 5. + \item Критерий остановки: достижение порога $f(\mathbf{x}) < 10^{-6}$ или исчерпание лимита 300 поколений. \end{itemize} - На практике $(1+1)$-стратегия показывает самую быструю сходимость на гладком рельефе, однако чувствительна к выбору начального $\sigma_0$. Популяционные режимы требовательнее по времени, но надёжнее удерживаются в окрестности минимума и легче масштабируются на $n=3$. + Результаты измерений представлены в таблицах~\ref{tab:es_results_2} и~\ref{tab:es_results_3}. В ячейках указано среднее время выполнения в миллисекундах и среднее число поколений до достижения критерия остановки. Лучшие результаты по времени выполнения и по числу поколений выделены жирным цветом. + + \newcolumntype{Y}{>{\centering\arraybackslash}X} + + \begin{table}[h!] + \centering + \small + \caption{Результаты для $n = 2$. Формат: время в мс (число поколений)} + \begin{tabularx}{0.95\linewidth}{l *{5}{Y}} + \toprule + $\mathbf{\mu \;\backslash\; p_{mut}}$ & \textbf{0.30} & \textbf{0.50} & \textbf{0.70} & \textbf{0.90} & \textbf{1.00} \\ + \midrule + \textbf{5} & 60.6 (37) & 35.1 (23) & 37.9 (25) & 29.2 (20) & \textcolor{magenta}{\textbf{20.4}} (17) \\ + \textbf{10} & 69.5 (22) & 84.1 (28) & 61.1 (21) & 48.2 (17) & 38.1 (16) \\ + \textbf{20} & 109.6 (18) & 120.4 (20) & 107.0 (18) & 100.2 (17) & 69.4 (15) \\ + \textbf{40} & 239.8 (19) & 225.9 (19) & 199.9 (17) & 180.6 (16) & 121.4 (\textcolor{magenta}{\textbf{13}}) \\ + \bottomrule + \end{tabularx} + \label{tab:es_results_2} + \end{table} + + \begin{table}[h!] + \centering + \small + \caption{Результаты для $n = 3$. Формат: время в мс (число поколений)} + \begin{tabularx}{0.95\linewidth}{l *{5}{Y}} + \toprule + $\mathbf{\mu \;\backslash\; p_{mut}}$ & \textbf{0.30} & \textbf{0.50} & \textbf{0.70} & \textbf{0.90} & \textbf{1.00} \\ + \midrule + \textbf{5} & 146.0 (88) & 212.2 (126) & 93.7 (60) & 44.8 (29) & \textcolor{magenta}{\textbf{30.3}} (25) \\ + \textbf{10} & 155.9 (49) & 149.3 (48) & 88.7 (30) & 69.8 (24) & 55.7 (23) \\ + \textbf{20} & 235.5 (38) & 199.0 (32) & 157.7 (26) & 125.8 (21) & 105.9 (21) \\ + \textbf{40} & 670.3 (53) & 374.2 (31) & 311.8 (26) & 258.2 (22) & 194.0 (\textcolor{magenta}{\textbf{20}}) \\ + \bottomrule + \end{tabularx} + \label{tab:es_results_3} + \end{table} + + \subsection{Анализ результатов} + + Анализ экспериментальных данных выявляет следующие закономерности: + + \begin{itemize} + \item \textbf{Влияние вероятности мутации:} Увеличение $p_{mut}$ от 0.3 до 1.0 последовательно улучшает результаты как по времени, так и по числу поколений. Это объясняется тем, что более частая мутация всех координат ускоряет исследование пространства и адаптацию популяции. Лучшие результаты достигаются при $p_{mut} = 1.0$ (мутация всех координат на каждом шаге). + + \item \textbf{Влияние размера популяции:} При малых $\mu$ (5-10) алгоритм демонстрирует наименьшее время выполнения и умеренное число поколений. С ростом $\mu$ до 40 время увеличивается пропорционально размеру популяции, но число поколений снижается благодаря более широкому охвату пространства поиска. Для двумерной задачи оптимальным является $\mu=5$, $p_{mut}=1.0$ (20.4 мс, 17 поколений). + + \item \textbf{Масштабирование на размерность:} При переходе от $n=2$ к $n=3$ время выполнения изменяется незначительно (30.3 мс против 20.4 мс для лучшей конфигурации), однако требуется больше поколений (25 против 17). Это связано с усложнением ландшафта целевой функции и необходимостью большего числа итераций для достижения порога $10^{-6}$. + + \item \textbf{Эффективность адаптации:} Правило успеха $1/5$ обеспечивает автоматическую подстройку масштаба мутации, что позволяет алгоритму быстро сходиться без ручной настройки начального $\sigma$. Минимальное число поколений (13 и 20 для $n=2$ и $n=3$ соответственно) достигается при больших популяциях ($\mu=40$) и высокой вероятности мутации ($p_{mut}=1.0$). + \end{itemize} \newpage \section{Ответ на контрольный вопрос}