fitnesses
This commit is contained in:
@@ -22,32 +22,6 @@ class Chromosome:
|
||||
def prune(self, max_depth: int) -> None:
|
||||
self.root.prune(self.terminals, max_depth)
|
||||
|
||||
def shrink_mutation(self) -> None:
|
||||
"""Усекающая мутация. Заменяет случайно выбранную операцию на случайный терминал."""
|
||||
operation_nodes = [n for n in self.root.list_nodes() if n.value.arity > 0]
|
||||
|
||||
if not operation_nodes:
|
||||
return
|
||||
|
||||
target_node = random.choice(operation_nodes)
|
||||
|
||||
target_node.prune(self.terminals, max_depth=1)
|
||||
|
||||
def grow_mutation(self, max_depth: int) -> None:
|
||||
"""Растущая мутация. Заменяет случайно выбранный узел на случайное поддерево."""
|
||||
target_node = random.choice(self.root.list_nodes())
|
||||
|
||||
max_subtree_depth = max_depth - target_node.get_level() + 1
|
||||
|
||||
subtree = Chromosome.grow_init(
|
||||
self.terminals, self.operations, max_subtree_depth
|
||||
).root
|
||||
|
||||
if target_node.parent:
|
||||
target_node.parent.replace_child(target_node, subtree)
|
||||
else:
|
||||
self.root = subtree
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Строковое представление хромосомы в виде формулы в инфиксной форме."""
|
||||
return str(self.root)
|
||||
|
||||
@@ -17,6 +17,9 @@ def crossover_subtree(
|
||||
child2 = parent2.copy()
|
||||
|
||||
# Выбираем случайные узлы, не включая корень
|
||||
if child1.root.get_depth() <= 1 or child2.root.get_depth() <= 1:
|
||||
return child1, child2
|
||||
|
||||
cut1 = random.choice(child1.root.list_nodes()[1:])
|
||||
cut2 = random.choice(child2.root.list_nodes()[1:])
|
||||
|
||||
|
||||
149
lab4/gp/fitness.py
Normal file
149
lab4/gp/fitness.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from .chromosome import Chromosome
|
||||
|
||||
type FitnessFn = Callable[
|
||||
[
|
||||
Chromosome,
|
||||
NDArray[np.float64],
|
||||
Callable[[NDArray[np.float64]], NDArray[np.float64]],
|
||||
],
|
||||
float,
|
||||
]
|
||||
type TargetFunction = Callable[[NDArray[np.float64]], NDArray[np.float64]]
|
||||
type TestPointsFn = Callable[[], NDArray[np.float64]]
|
||||
|
||||
|
||||
class BaseFitness(ABC):
|
||||
def __init__(self, target_fn: TargetFunction, test_points_fn: TestPointsFn):
|
||||
self.target_function = target_fn
|
||||
self.test_points_fn = test_points_fn
|
||||
|
||||
@abstractmethod
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float: ...
|
||||
|
||||
def __call__(self, chromosome: Chromosome) -> float:
|
||||
test_points = self.test_points_fn()
|
||||
|
||||
context = {t: test_points[:, i] for i, t in enumerate(chromosome.terminals)}
|
||||
predicted = chromosome.root.eval(context)
|
||||
true_values = self.target_function(test_points)
|
||||
|
||||
return self.fitness_fn(chromosome, predicted, true_values)
|
||||
|
||||
|
||||
class MSEFitness(BaseFitness):
|
||||
"""Среднеквадратичная ошибка"""
|
||||
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float:
|
||||
return float(np.mean((predicted - true_values) ** 2))
|
||||
|
||||
|
||||
class RMSEFitness(BaseFitness):
|
||||
"""Корень из среднеквадратичной ошибки"""
|
||||
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float:
|
||||
return float(np.sqrt(np.mean((predicted - true_values) ** 2)))
|
||||
|
||||
|
||||
class MAEFitness(BaseFitness):
|
||||
"""Средняя абсолютная ошибка"""
|
||||
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float:
|
||||
return float(np.mean(np.abs(predicted - true_values)))
|
||||
|
||||
|
||||
class HuberFitness(BaseFitness):
|
||||
"""Huber Loss (компромисс между MSE и MAE)"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target_fn: TargetFunction,
|
||||
test_points_fn: TestPointsFn,
|
||||
delta: float = 1.0,
|
||||
):
|
||||
super().__init__(target_fn, test_points_fn)
|
||||
self.delta = delta
|
||||
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float:
|
||||
error = predicted - true_values
|
||||
mask = np.abs(error) <= self.delta
|
||||
squared = 0.5 * (error[mask] ** 2)
|
||||
linear = self.delta * (np.abs(error[~mask]) - 0.5 * self.delta)
|
||||
huber = np.concatenate([squared, linear])
|
||||
return float(np.mean(huber))
|
||||
|
||||
|
||||
class NRMSEFitness(BaseFitness):
|
||||
"""Нормализованный RMSE (масштаб-инвариантен)"""
|
||||
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float:
|
||||
denom = np.std(true_values)
|
||||
if denom == 0:
|
||||
return 1e6
|
||||
return float(np.sqrt(np.mean((predicted - true_values) ** 2)) / denom)
|
||||
|
||||
|
||||
class PenalizedFitness(BaseFitness):
|
||||
"""Фитнес со штрафом за размер и глубину дерева: ошибка + λ * (размер + depth_weight * глубина)"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target_fn: TargetFunction,
|
||||
test_points_fn: TestPointsFn,
|
||||
base_fitness: BaseFitness,
|
||||
lambda_: float = 0.001,
|
||||
depth_weight: float = 0.2,
|
||||
):
|
||||
super().__init__(target_fn, test_points_fn)
|
||||
self.base_fitness = base_fitness
|
||||
self.lambda_ = lambda_
|
||||
self.depth_weight = depth_weight
|
||||
|
||||
def fitness_fn(
|
||||
self,
|
||||
chromosome: Chromosome,
|
||||
predicted: NDArray[np.float64],
|
||||
true_values: NDArray[np.float64],
|
||||
) -> float:
|
||||
base = self.base_fitness.fitness_fn(chromosome, predicted, true_values)
|
||||
|
||||
size = chromosome.root.get_size()
|
||||
depth = chromosome.root.get_depth()
|
||||
|
||||
penalty = self.lambda_ * (size + self.depth_weight * depth)
|
||||
return float(base + penalty)
|
||||
341
lab4/gp/ga.py
Normal file
341
lab4/gp/ga.py
Normal file
@@ -0,0 +1,341 @@
|
||||
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
|
||||
from matplotlib import pyplot as plt
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from .chromosome import Chromosome
|
||||
from .types import Fitnesses, Population
|
||||
|
||||
type FitnessFn = Callable[[Chromosome], float]
|
||||
|
||||
type InitializePopulationFn = Callable[[int], Population]
|
||||
type CrossoverFn = Callable[[Chromosome, Chromosome], tuple[Chromosome, Chromosome]]
|
||||
type MutationFn = Callable[[Chromosome, int], Chromosome]
|
||||
type SelectionFn = Callable[[Population, Fitnesses], Population]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GARunConfig:
|
||||
fitness_func: FitnessFn
|
||||
crossover_fn: CrossoverFn
|
||||
mutation_fn: MutationFn
|
||||
selection_fn: SelectionFn
|
||||
init_population: Population
|
||||
pc: float # вероятность кроссинговера
|
||||
pm: float # вероятность мутации
|
||||
max_generations: int # максимальное количество поколений
|
||||
elitism: int = (
|
||||
0 # сколько лучших особей перенести без изменения в следующее поколение
|
||||
)
|
||||
max_best_repetitions: int | None = (
|
||||
None # остановка при повторении лучшего результата
|
||||
)
|
||||
seed: int | None = None # seed для генератора случайных чисел
|
||||
minimize: bool = True # если 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 crossover(
|
||||
population: Population,
|
||||
pc: float,
|
||||
crossover_fn: CrossoverFn,
|
||||
) -> Population:
|
||||
"""Оператор кроссинговера (скрещивания) выполняется с заданной вероятностью pc.
|
||||
|
||||
Две хромосомы (родители) выбираются случайно из промежуточной популяции.
|
||||
|
||||
Если популяция нечетного размера, то последняя хромосома скрещивается со случайной
|
||||
другой хромосомой из популяции. В таком случае одна из хромосом может поучаствовать
|
||||
в кроссовере дважды.
|
||||
"""
|
||||
# Создаем копию популяции и перемешиваем её для случайного выбора пар
|
||||
shuffled_population = population.copy()
|
||||
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 mutation(
|
||||
population: Population, pm: float, gen_num: int, mutation_fn: MutationFn
|
||||
) -> Population:
|
||||
"""Мутация происходит с вероятностью pm."""
|
||||
next_population = []
|
||||
for chrom in population:
|
||||
next_population.append(
|
||||
mutation_fn(chrom, gen_num) 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 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,
|
||||
)
|
||||
|
||||
# Рисуем
|
||||
...
|
||||
|
||||
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.init_population
|
||||
|
||||
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),
|
||||
population=[],
|
||||
# fitnesses=deepcopy(fitnesses),
|
||||
fitnesses=np.array([]),
|
||||
)
|
||||
history.append(current)
|
||||
|
||||
if config.log_every_generation:
|
||||
print(
|
||||
f"Generation #{generation_number} best: {current.best_fitness},"
|
||||
f" avg: {np.mean(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.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 = config.selection_fn(
|
||||
population, fitnesses if not config.minimize else -fitnesses
|
||||
)
|
||||
|
||||
# кроссинговер попарно
|
||||
next_population = crossover(parents, config.pc, config.crossover_fn)
|
||||
|
||||
# мутация
|
||||
next_population = mutation(
|
||||
next_population,
|
||||
config.pm,
|
||||
generation_number,
|
||||
config.mutation_fn,
|
||||
)
|
||||
|
||||
# Вставляем элиту в новую популяцию
|
||||
population = next_population[: len(population) - 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)
|
||||
@@ -1,3 +1,39 @@
|
||||
import random
|
||||
|
||||
from .chromosome import Chromosome
|
||||
|
||||
|
||||
def shrink_mutation(chromosome: Chromosome) -> Chromosome:
|
||||
"""Усекающая мутация. Заменяет случайно выбранную операцию на случайный терминал."""
|
||||
chromosome = chromosome.copy()
|
||||
|
||||
operation_nodes = [n for n in chromosome.root.list_nodes() if n.value.arity > 0]
|
||||
|
||||
if not operation_nodes:
|
||||
return chromosome
|
||||
|
||||
target_node = random.choice(operation_nodes)
|
||||
|
||||
target_node.prune(chromosome.terminals, max_depth=1)
|
||||
|
||||
return chromosome
|
||||
|
||||
|
||||
def grow_mutation(chromosome: Chromosome, max_depth: int) -> Chromosome:
|
||||
"""Растущая мутация. Заменяет случайно выбранный узел на случайное поддерево."""
|
||||
chromosome = chromosome.copy()
|
||||
|
||||
target_node = random.choice(chromosome.root.list_nodes())
|
||||
|
||||
max_subtree_depth = max_depth - target_node.get_level() + 1
|
||||
|
||||
subtree = Chromosome.grow_init(
|
||||
chromosome.terminals, chromosome.operations, max_subtree_depth
|
||||
).root
|
||||
|
||||
if target_node.parent:
|
||||
target_node.parent.replace_child(target_node, subtree)
|
||||
else:
|
||||
chromosome.root = subtree
|
||||
|
||||
return chromosome
|
||||
|
||||
@@ -61,14 +61,18 @@ class Node:
|
||||
|
||||
prune_recursive(self, 1)
|
||||
|
||||
def get_subtree_depth(self) -> int:
|
||||
def get_depth(self) -> int:
|
||||
"""Вычисляет глубину поддерева, начиная с текущего узла."""
|
||||
return (
|
||||
max(child.get_subtree_depth() for child in self.children) + 1
|
||||
max(child.get_depth() for child in self.children) + 1
|
||||
if self.children
|
||||
else 1
|
||||
)
|
||||
|
||||
def get_size(self) -> int:
|
||||
"""Вычисляет размер поддерева, начиная с текущего узла."""
|
||||
return sum(child.get_size() for child in self.children) + 1
|
||||
|
||||
def get_level(self) -> int:
|
||||
"""Вычисляет уровень узла в дереве (расстояние от корня). Корень имеет уровень 1."""
|
||||
return self.parent.get_level() + 1 if self.parent else 1
|
||||
|
||||
@@ -7,6 +7,7 @@ type Value = NDArray[np.float64]
|
||||
|
||||
# Унарные операции
|
||||
NEG = Operation("-", 1, lambda x: -x[0])
|
||||
SQUARE = Operation("pow2", 1, lambda x: x[0] ** 2)
|
||||
SIN = Operation("sin", 1, lambda x: np.sin(x[0]))
|
||||
COS = Operation("cos", 1, lambda x: np.cos(x[0]))
|
||||
|
||||
@@ -53,6 +54,3 @@ def _safe_pow(a: Value, b: Value) -> Value:
|
||||
|
||||
|
||||
POW = Operation("^", 2, lambda x: _safe_pow(x[0], x[1]))
|
||||
|
||||
# Все операции в либе
|
||||
ALL = (NEG, SIN, COS, EXP, ADD, SUB, MUL, DIV, POW)
|
||||
|
||||
28
lab4/gp/selection.py
Normal file
28
lab4/gp/selection.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import numpy as np
|
||||
|
||||
from .types import Fitnesses, Population
|
||||
|
||||
|
||||
def roulette_selection(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
|
||||
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
|
||||
from .primitive import Primitive
|
||||
|
||||
type Population = list[Chromosome]
|
||||
type Fitnesses = NDArray[np.float64]
|
||||
type InitFunc = Callable[[Chromosome], Node]
|
||||
type Value = NDArray[np.float64]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user