Хромосомы для лаб4
This commit is contained in:
5
lab4/gp/__init__.py
Normal file
5
lab4/gp/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .chromosome import Chromosome
|
||||
from .operation import Operation
|
||||
from .terminal import Terminal
|
||||
|
||||
__all__ = ["Chromosome", "Operation", "Terminal"]
|
||||
138
lab4/gp/chromosome.py
Normal file
138
lab4/gp/chromosome.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import random
|
||||
from typing import Literal, Sequence
|
||||
|
||||
from .operation import Operation
|
||||
from .terminal import Terminal
|
||||
|
||||
type InitMethod = Literal["full", "grow"]
|
||||
|
||||
|
||||
class Chromosome:
|
||||
def __init__(
|
||||
self,
|
||||
operations: Sequence[Operation],
|
||||
terminals: Sequence[Terminal],
|
||||
max_depth: int,
|
||||
init_method: InitMethod,
|
||||
terminal_probability: float = 0.5,
|
||||
):
|
||||
self.operations = operations
|
||||
self.terminals = terminals
|
||||
self.root = (
|
||||
self._init_full(max_depth)
|
||||
if init_method == "full"
|
||||
else self._init_grow(max_depth, terminal_probability)
|
||||
)
|
||||
self.depth = self._compute_depth(self.root)
|
||||
|
||||
def _compute_depth(self, node: "Chromosome.Node") -> int:
|
||||
"""Вычисляет глубину дерева. Дерево из одного только корня имеет глубину 1."""
|
||||
return (
|
||||
max(self._compute_depth(child) for child in node.children) + 1
|
||||
if node.children
|
||||
else 1
|
||||
)
|
||||
|
||||
def _random_terminal(self) -> Terminal:
|
||||
return random.choice(self.terminals)
|
||||
|
||||
def _init_full(self, max_depth: int) -> "Chromosome.Node":
|
||||
"""Полная инициализация.
|
||||
|
||||
В полном методе при генерации дерева, пока не достигнута максимальная глубина,
|
||||
допускается выбор только функциональных символов, а на последнем уровне
|
||||
(максимальной глубины) выбираются только терминальные символы.
|
||||
"""
|
||||
|
||||
def build(level: int) -> Chromosome.Node:
|
||||
# Если достигнута максимальная глубина — выбираем терминал
|
||||
if level == max_depth:
|
||||
return Chromosome.Node(self._random_terminal(), [])
|
||||
|
||||
# Иначе выбираем операцию и создаём потомков
|
||||
op = random.choice(self.operations)
|
||||
node = Chromosome.Node(op, [build(level + 1) for _ in range(op.arity)])
|
||||
return node
|
||||
|
||||
return build(1)
|
||||
|
||||
def _init_grow(
|
||||
self, max_depth: int, terminal_probability: float
|
||||
) -> "Chromosome.Node":
|
||||
"""Растущая инициализация.
|
||||
|
||||
В растущей инициализации генерируются нерегулярные деревья с различной глубиной
|
||||
листьев вследствие случайного на каждом шаге выбора функционального
|
||||
или терминального символа. Здесь при выборе терминального символа рост дерева
|
||||
прекращается по текущей ветви и поэтому дерево имеет нерегулярную структуру.
|
||||
"""
|
||||
|
||||
def build(level: int) -> Chromosome.Node:
|
||||
# Если достигнута максимальная глубина, либо сыграла заданная вероятность
|
||||
# — выбираем терминал
|
||||
if level == max_depth or random.random() < terminal_probability:
|
||||
return Chromosome.Node(self._random_terminal(), [])
|
||||
|
||||
# Иначе выбираем случайную операцию и создаём потомков
|
||||
op = random.choice(self.operations)
|
||||
children = [build(level + 1) for _ in range(op.arity)]
|
||||
return Chromosome.Node(op, children)
|
||||
|
||||
return build(1)
|
||||
|
||||
def eval(self, values: list[float]) -> float:
|
||||
"""Вычисляет значение хромосомы для заданных значений терминалов."""
|
||||
for terminal, value in zip(self.terminals, values):
|
||||
terminal._value = value
|
||||
|
||||
return self.root._eval()
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Строковое представление хромосомы в виде формулы в инфиксной форме."""
|
||||
return str(self.root)
|
||||
|
||||
def _tree_lines(
|
||||
self, node: "Chromosome.Node", prefix: str = "", is_last: bool = True
|
||||
) -> list[str]:
|
||||
connector = "└── " if is_last else "├── "
|
||||
lines = [prefix + connector + node.value.name]
|
||||
child_prefix = prefix + (" " if is_last else "│ ")
|
||||
for i, child in enumerate(node.children):
|
||||
last = i == len(node.children) - 1
|
||||
lines.extend(self._tree_lines(child, child_prefix, last))
|
||||
return lines
|
||||
|
||||
def str_tree(self) -> str:
|
||||
"""Строковое представление древовидной структуры формулы."""
|
||||
lines = [self.root.value.name]
|
||||
for i, child in enumerate(self.root.children):
|
||||
last = i == len(self.root.children) - 1
|
||||
lines.extend(self._tree_lines(child, "", last))
|
||||
return "\n".join(lines)
|
||||
|
||||
class Node:
|
||||
def __init__(
|
||||
self, value: Operation | Terminal, children: list["Chromosome.Node"]
|
||||
):
|
||||
self.value = value
|
||||
self.children = children
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Рекурсивный перевод древовидного вида формулы в строку в инфиксной форме."""
|
||||
if isinstance(self.value, Terminal):
|
||||
return self.value.name
|
||||
|
||||
if self.value.arity == 2:
|
||||
return f"({self.children[0]} {self.value.name} {self.children[1]})"
|
||||
|
||||
return (
|
||||
f"{self.value.name}({', '.join(str(child) for child in self.children)})"
|
||||
)
|
||||
|
||||
def _eval(self) -> float:
|
||||
"""Рекурсивно вычисляет значение поддерева. Значения терминалов должны быть
|
||||
заданы предварительно."""
|
||||
if isinstance(self.value, Terminal):
|
||||
return self.value._value # type: ignore
|
||||
|
||||
return self.value._eval([child._eval() for child in self.children])
|
||||
11
lab4/gp/operation.py
Normal file
11
lab4/gp/operation.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class Operation:
|
||||
def __init__(self, name: str, arity: int, eval_fn: Callable[[list[float]], float]):
|
||||
self.name = name
|
||||
self.arity = arity
|
||||
self.eval_fn = eval_fn
|
||||
|
||||
def _eval(self, args: list[float]) -> float:
|
||||
return self.eval_fn(args)
|
||||
51
lab4/gp/ops.py
Normal file
51
lab4/gp/ops.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import math
|
||||
|
||||
from .operation import Operation
|
||||
|
||||
# Унарные операции
|
||||
NEG = Operation("-", 1, lambda x: -x[0])
|
||||
SIN = Operation("sin", 1, lambda x: math.sin(x[0]))
|
||||
COS = Operation("cos", 1, lambda x: math.cos(x[0]))
|
||||
|
||||
|
||||
def _safe_exp(a: float) -> float:
|
||||
if a < 700.0:
|
||||
if a > -700.0:
|
||||
return math.exp(a)
|
||||
return 0.0
|
||||
else:
|
||||
return float("inf")
|
||||
|
||||
|
||||
EXP = Operation("exp", 1, lambda x: _safe_exp(x[0]))
|
||||
|
||||
|
||||
# Бинарные операции
|
||||
ADD = Operation("+", 2, lambda x: x[0] + x[1])
|
||||
SUB = Operation("-", 2, lambda x: x[0] - x[1])
|
||||
MUL = Operation("*", 2, lambda x: x[0] * x[1])
|
||||
DIV = Operation("/", 2, lambda x: x[0] / x[1] if x[1] != 0 else float("inf"))
|
||||
|
||||
|
||||
def safe_pow(a, b):
|
||||
# 0 в отрицательной степени
|
||||
if abs(a) <= 1e-12 and b < 0:
|
||||
return float("inf")
|
||||
# отрицательное основание при нецелой степени
|
||||
if a < 0 and abs(b - round(b)) > 1e-12:
|
||||
return float("inf")
|
||||
# грубое насыщение (настрой пороги под задачу)
|
||||
if abs(a) > 1 and b > 20:
|
||||
return float("inf")
|
||||
if abs(a) < 1 and b < -20:
|
||||
return float("inf")
|
||||
try:
|
||||
return a**b
|
||||
except OverflowError, ValueError:
|
||||
return float("inf")
|
||||
|
||||
|
||||
POW = Operation("^", 2, lambda x: safe_pow(x[0], x[1]))
|
||||
|
||||
# Все операции в либе
|
||||
ALL = (NEG, SIN, COS, EXP, ADD, SUB, MUL, DIV, POW)
|
||||
38
lab4/gp/population.py
Normal file
38
lab4/gp/population.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import random
|
||||
from typing import Callable
|
||||
|
||||
from .chromosome import Chromosome, InitMethod
|
||||
|
||||
type Population = list[Chromosome]
|
||||
|
||||
|
||||
def ramped_initialization(
|
||||
population_size: int,
|
||||
depths: list[int],
|
||||
make_chromosome: Callable[[int, InitMethod], Chromosome], # (max_depth, method)
|
||||
) -> Population:
|
||||
"""Комбинация методов grow и full инициализации хромосом для инициализации начальной
|
||||
популяции.
|
||||
|
||||
Начальная популяция генерируется так, чтобы в нее входили деревья с разной
|
||||
максимальной длиной примерно поровну. Для каждой глубины первая половина деревьев
|
||||
генерируется полным методом, а вторая – растущей инициализацией.
|
||||
"""
|
||||
population: Population = []
|
||||
per_depth = population_size / len(depths)
|
||||
|
||||
for depth in depths:
|
||||
n_full = int(per_depth / 2)
|
||||
n_grow = int(per_depth / 2)
|
||||
|
||||
population.extend(make_chromosome(depth, "full") for _ in range(n_full))
|
||||
population.extend(make_chromosome(depth, "grow") for _ in range(n_grow))
|
||||
|
||||
# Из-за округления хромосом может оказаться меньше заданного количества,
|
||||
# поэтому дозаполняем остаток популяции случайными хромосомами
|
||||
while len(population) < population_size:
|
||||
depth = random.choice(depths)
|
||||
method = "full" if random.random() < 0.5 else "grow"
|
||||
population.append(make_chromosome(depth, method))
|
||||
|
||||
return population
|
||||
7
lab4/gp/terminal.py
Normal file
7
lab4/gp/terminal.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass()
|
||||
class Terminal:
|
||||
name: str
|
||||
_value: float | None = None
|
||||
23
lab4/main.py
Normal file
23
lab4/main.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from gp import Chromosome, Terminal, ops
|
||||
from gp.population import ramped_initialization
|
||||
|
||||
operations = ops.ALL
|
||||
|
||||
terminals = [Terminal(f"x{i}") for i in range(1, 9)]
|
||||
|
||||
chrom = Chromosome(operations, terminals, 8, "grow")
|
||||
print("Depth:", chrom.depth)
|
||||
print("Formula:", chrom)
|
||||
print("Tree:\n", chrom.str_tree())
|
||||
|
||||
values = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
|
||||
print("Value for ", values, ":", chrom.eval(values))
|
||||
|
||||
population = ramped_initialization(
|
||||
100,
|
||||
[3, 4, 5, 6, 7, 8],
|
||||
lambda depth, method: Chromosome(operations, terminals, depth, method),
|
||||
)
|
||||
print("Population size:", len(population))
|
||||
print("Population:")
|
||||
[print(str(chrom)) for chrom in population]
|
||||
Reference in New Issue
Block a user