From afd7a700cafd7eb67880d132597884662e990995 Mon Sep 17 00:00:00 2001 From: Arity-T Date: Tue, 21 Oct 2025 12:26:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A5=D1=80=D0=BE=D0=BC=D0=BE=D1=81=D0=BE?= =?UTF-8?q?=D0=BC=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=BB=D0=B0=D0=B14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lab4/gp/__init__.py | 5 ++ lab4/gp/chromosome.py | 138 ++++++++++++++++++++++++++++++++++++++++++ lab4/gp/operation.py | 11 ++++ lab4/gp/ops.py | 51 ++++++++++++++++ lab4/gp/population.py | 38 ++++++++++++ lab4/gp/terminal.py | 7 +++ lab4/main.py | 23 +++++++ 7 files changed, 273 insertions(+) create mode 100644 lab4/gp/__init__.py create mode 100644 lab4/gp/chromosome.py create mode 100644 lab4/gp/operation.py create mode 100644 lab4/gp/ops.py create mode 100644 lab4/gp/population.py create mode 100644 lab4/gp/terminal.py create mode 100644 lab4/main.py diff --git a/lab4/gp/__init__.py b/lab4/gp/__init__.py new file mode 100644 index 0000000..3bcfe12 --- /dev/null +++ b/lab4/gp/__init__.py @@ -0,0 +1,5 @@ +from .chromosome import Chromosome +from .operation import Operation +from .terminal import Terminal + +__all__ = ["Chromosome", "Operation", "Terminal"] diff --git a/lab4/gp/chromosome.py b/lab4/gp/chromosome.py new file mode 100644 index 0000000..c52bcdf --- /dev/null +++ b/lab4/gp/chromosome.py @@ -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]) diff --git a/lab4/gp/operation.py b/lab4/gp/operation.py new file mode 100644 index 0000000..c0ba875 --- /dev/null +++ b/lab4/gp/operation.py @@ -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) diff --git a/lab4/gp/ops.py b/lab4/gp/ops.py new file mode 100644 index 0000000..d4930ed --- /dev/null +++ b/lab4/gp/ops.py @@ -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) diff --git a/lab4/gp/population.py b/lab4/gp/population.py new file mode 100644 index 0000000..1906019 --- /dev/null +++ b/lab4/gp/population.py @@ -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 diff --git a/lab4/gp/terminal.py b/lab4/gp/terminal.py new file mode 100644 index 0000000..180c061 --- /dev/null +++ b/lab4/gp/terminal.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass() +class Terminal: + name: str + _value: float | None = None diff --git a/lab4/main.py b/lab4/main.py new file mode 100644 index 0000000..44c2743 --- /dev/null +++ b/lab4/main.py @@ -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]