commit 010efa17e031f11ec686f9f03f636c5def31e793 Author: Arity-T Date: Wed Sep 10 15:45:02 2025 +0300 lab1 предварительная версия diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2ea21b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +* + +!**/ +!*.gitignore +!*.py \ No newline at end of file diff --git a/lab1/gen.py b/lab1/gen.py new file mode 100644 index 0000000..05b44ac --- /dev/null +++ b/lab1/gen.py @@ -0,0 +1,310 @@ +import math +import os +import random +import shutil +import time +from dataclasses import dataclass +from typing import Callable, List, Tuple + +import matplotlib.pyplot as plt +import numpy as np + + +def target_function(x: float) -> float: + """f(x) = sin(x)/x^2""" + return math.sin(x) / (x * x) + + +def bits_for_precision(x_min: float, x_max: float, digits_after_decimal: int) -> int: + """ + Подбор числа бит L так, чтобы шаг сетки был ≤ 10^{-digits_after_decimal}. + Идея как в методичке: 2^L >= (x_max - x_min)*10^{digits_after_decimal}. + """ + required_levels = int(math.ceil((x_max - x_min) * (10**digits_after_decimal))) + L = 1 + while (1 << L) < required_levels: + L += 1 + return L + + +def decode_bits_to_x(bits: List[int], x_min: float, x_max: float) -> float: + """Линейное отображение битовой строки в x ∈ [x_min, x_max].""" + v = 0 + for b in bits: + v = (v << 1) | int(b) + L = len(bits) + levels = (1 << L) - 1 + return x_min + (x_max - x_min) * (v / levels) + + +def random_bits(L: int) -> List[int]: + return [random.randint(0, 1) for _ in range(L)] + + +def eval_population( + population: List[List[int]], + x_min: float, + x_max: float, + fitness_func: Callable[[float], float], +) -> Tuple[List[float], List[float]]: + """Оценка популяции: преобразование в x значения и вычисление фитнес функции.""" + xs = [decode_bits_to_x(ch, x_min, x_max) for ch in population] + fits = [fitness_func(x) for x in xs] + return xs, fits + + +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 reproduction( + population: List[List[int]], fitnesses: List[float] +) -> List[List[int]]: + """Репродукция (селекция) методом рулетки.""" + # Чтобы работать с отрицательными f, сдвигаем значения фитнес функции на минимальное + # значение в популяции. Вычитаем min_fit, т. к. min_fit может быть отрицательным. + min_fit = min(fitnesses) + shifted_fitnesses = [f - min_fit + 1e-12 for f in fitnesses] + s = sum(shifted_fitnesses) + + probs = [sf / s for sf in shifted_fitnesses] + cum = np.cumsum(probs) + selected = [] + for _ in population: + r = random.random() + idx = int(np.searchsorted(cum, r, side="left")) + selected.append(population[idx][:]) + return selected + + +def crossover_pair( + p1: List[int], p2: List[int], pc: float +) -> Tuple[List[int], List[int]]: + """Кроссинговер между двумя хромосомами с вероятностью pc.""" + if random.random() <= pc: + k = random.randint(1, len(p1) - 1) + return p1[:k] + p2[k:], p2[:k] + p1[k:] + else: + return p1[:], p2[:] + + +def crossover(population: List[List[int]], pc: float) -> List[List[int]]: + """Оператор кроссинговера (скрещивания) выполняется с заданной вероятностью pc. + 1. Две хромосомы (родители) выбираются случайно из промежуточной популяции. + 2. Случайно выбирается точка скрещивания - число k из диапазона [1,2,…,n-1], + где n – длина хромосомы (число бит в двоичном коде). + 3. Две новых хромосомы A', B' (потомки) формируются из A и B путем обмена подстрок + после точки скрещивания с вероятностью pc. Иначе родители добавляются в новую + популяцию без изменений. + + Если популяция нечетного размера, то последняя хромосома скрещивается со случайной + другой хромосомой из популяции. В таком случае одна из хромосом может поучаствовать + в кроссовере дважды. + """ + # Создаем копию популяции и перемешиваем её для случайного выбора пар + shuffled_population = population[:] + 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] + c1, c2 = crossover_pair(p1, p2, pc) + next_population.append(c1) + next_population.append(c2) + + return next_population[:pop_size] + + +def mutation(chrom: List[int], pm: float) -> None: + """Мутация происходит с вероятностью pm. + 1. В хромосоме случайно выбирается k-ая позиция (бит) мутации. + 2. Производится инверсия значения гена в k-й позиции. + """ + if random.random() <= pm: + k = random.randint(0, len(chrom) - 1) + chrom[k] = 1 - chrom[k] + + +@dataclass +class GARunConfig: + x_min: float = 3.1 + x_max: float = 20.0 + precision_digits: int = 3 # точность сетки ~0.001 + pop_size: int = 100 # размер популяции + pc: float = 0.7 # вероятность кроссинговера + pm: float = 0.01 # вероятность мутации + max_generations: int = 200 # максимальное количество поколений + seed: int | None = None # seed для генератора случайных чисел + save_generations: list[int] | None = ( + None # индексы поколений для сохранения графиков + ) + results_dir: str = "results" # папка для сохранения графиков + + +@dataclass +class GARunResult: + best_x: float + best_f: float + generations: int + history_best_x: List[float] + history_best_f: List[float] + history_populations_x: List[List[float]] # история всех популяций (x значения) + history_populations_f: List[List[float]] # история всех популяций (f значения) + time_ms: float + L: int # число бит + + +def genetic_algorithm( + config: GARunConfig, + fitness_func: Callable[[float], float] = target_function, +) -> 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) + + L = bits_for_precision(config.x_min, config.x_max, config.precision_digits) + population = [random_bits(L) for _ in range(config.pop_size)] + + start = time.perf_counter() + history_best_x, history_best_f = [], [] + history_populations_x, history_populations_f = [], [] + best_x, best_f = 0, -float("inf") + + for generation in range(config.max_generations): + xs, fits = eval_population(population, config.x_min, config.x_max, fitness_func) + + # лучший в поколении + глобально лучший + gi = int(np.argmax(fits)) + gen_best_x, gen_best_f = xs[gi], fits[gi] + if gen_best_f > best_f: + best_x, best_f = gen_best_x, gen_best_f + + history_best_x.append(gen_best_x) + history_best_f.append(gen_best_f) + history_populations_x.append(xs[:]) + history_populations_f.append(fits[:]) + + # Сохранение графика для указанных поколений + if config.save_generations and generation in config.save_generations: + plot_generation_snapshot( + history_best_x, + history_best_f, + history_populations_x, + history_populations_f, + generation, + config.x_min, + config.x_max, + config.results_dir, + ) + + # селекция + parents = reproduction(population, fits) + + # кроссинговер попарно + next_population = crossover(parents, config.pc) + + # мутация + for ch in next_population: + mutation(ch, config.pm) + + population = next_population[: config.pop_size] + + end = time.perf_counter() + + return GARunResult( + best_x, + best_f, + config.max_generations, + history_best_x, + history_best_f, + history_populations_x, + history_populations_f, + (end - start) * 1000.0, + L, + ) + + +def plot_generation_snapshot( + history_best_x: List[float], + history_best_f: List[float], + history_populations_x: List[List[float]], + history_populations_f: List[List[float]], + generation: int, + x_min: float, + x_max: float, + results_dir: str, +) -> str: + """ + График для конкретного поколения с отображением всей популяции. + """ + os.makedirs(results_dir, exist_ok=True) + + xs = np.linspace(x_min, x_max, 1500) + ys = np.sin(xs) / (xs * xs) + + fig = plt.figure(figsize=(10, 6)) + plt.plot(xs, ys, label="f(x)=sin(x)/x^2", alpha=0.7, color="blue") + + # Отображаем всю популяцию текущего поколения + if generation < len(history_populations_x): + current_pop_x = history_populations_x[generation] + current_pop_f = history_populations_f[generation] + + # Вся популяция серыми точками + plt.scatter( + current_pop_x, + current_pop_f, + s=20, + alpha=0.9, + color="gray", + label=f"Популяция поколения {generation}", + ) + + # Лучшая особь красной точкой + best_idx = np.argmax(current_pop_f) + plt.scatter( + [current_pop_x[best_idx]], + [current_pop_f[best_idx]], + s=60, + color="red", + marker="o", + label=f"Лучшая особь поколения {generation}", + edgecolors="darkred", + linewidth=1, + ) + + # История лучших по поколениям (до текущего включительно) + if generation > 0: + history_x_until_now = history_best_x[:generation] + history_f_until_now = history_best_f[:generation] + plt.scatter( + history_x_until_now, + history_f_until_now, + s=15, + alpha=0.9, + color="orange", + label="Лучшие предыдущих поколений", + ) + + plt.xlabel("x") + plt.ylabel("f(x)") + plt.title(f"Популяция поколения {generation}") + plt.legend(loc="best") + plt.grid(True, alpha=0.3) + plt.tight_layout() + + filename = f"generation_{generation:03d}.png" + path_png = os.path.join(results_dir, filename) + plt.savefig(path_png, dpi=150, bbox_inches="tight") + plt.close(fig) + return path_png diff --git a/lab1/main.py b/lab1/main.py new file mode 100644 index 0000000..02e263e --- /dev/null +++ b/lab1/main.py @@ -0,0 +1,49 @@ +import os + +from gen import GARunConfig, genetic_algorithm + +# Запуск эксперимента с генетическим алгоритмом +config = GARunConfig( + x_min=3.1, + x_max=20.0, + precision_digits=3, + pop_size=5, + pc=0.7, + pm=0.01, + max_generations=200, + seed=17, + save_generations=[ + 0, + 1, + 2, + 3, + 10, + 25, + 49, + 99, + 150, + 199, + ], # поколения для сохранения графиков + results_dir="results", +) + +# Запускаем генетический алгоритм +result = genetic_algorithm(config) + +# Выводим результаты +print(f"Лучшее x: {result.best_x:.4f}") +print(f"Лучшее f(x): {result.best_f:.6f}") +print(f"Количество поколений: {result.generations}") +print(f"Время выполнения: {result.time_ms:.2f} мс") +print(f"Количество бит: {result.L}") + +# Выводим информацию о сохраненных графиках поколений +if config.save_generations: + print( + f"\nГрафики для поколений {config.save_generations} сохранены в папку '{config.results_dir}/'" + ) + for gen in config.save_generations: + if gen < result.generations: + filename = f"generation_{gen:03d}.png" + filepath = os.path.join(config.results_dir, filename) + print(f" - Поколение {gen}: {filepath}")