another save
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
||||
!**/
|
||||
!*.gitignore
|
||||
!*.py
|
||||
!lab4/*
|
||||
1
lab4/.python-version
Normal file
1
lab4/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.14
|
||||
@@ -1,5 +1,3 @@
|
||||
from .chromosome import Chromosome
|
||||
from .operation import Operation
|
||||
from .terminal import Terminal
|
||||
|
||||
__all__ = ["Chromosome", "Operation", "Terminal"]
|
||||
__all__ = ["Chromosome"]
|
||||
|
||||
@@ -1,119 +1,64 @@
|
||||
import random
|
||||
from typing import Callable, Sequence
|
||||
from typing import Sequence
|
||||
|
||||
from .operation import Operation
|
||||
from .terminal import Terminal
|
||||
|
||||
type InitFunc = Callable[["Chromosome"], "Chromosome.Node"]
|
||||
from .node import Node
|
||||
from .primitive import Primitive
|
||||
|
||||
|
||||
class Chromosome:
|
||||
def __init__(
|
||||
self,
|
||||
operations: Sequence[Operation],
|
||||
terminals: Sequence[Terminal],
|
||||
init_func: InitFunc,
|
||||
terminals: Sequence[Primitive],
|
||||
operations: Sequence[Primitive],
|
||||
root: Node,
|
||||
):
|
||||
self.operations = operations
|
||||
self.terminals = terminals
|
||||
self.root = init_func(self)
|
||||
self.operations = operations
|
||||
self.root = root
|
||||
|
||||
def get_depth(self) -> int:
|
||||
"""Вычисляет глубину дерева. Дерево из одного только корня имеет глубину 1."""
|
||||
return self.root.get_depth() if self.root is not None else 0
|
||||
def copy(self) -> Chromosome:
|
||||
return Chromosome(self.terminals, self.operations, self.root.copy_subtree())
|
||||
|
||||
def clone(self) -> "Chromosome":
|
||||
"""Создает копию хромосомы."""
|
||||
return Chromosome(
|
||||
self.operations,
|
||||
self.terminals,
|
||||
lambda _: self.root.clone(),
|
||||
)
|
||||
def prune(self, max_depth: int) -> None:
|
||||
self.root.prune(self.terminals, max_depth)
|
||||
|
||||
def eval(self, values: list[float]) -> float:
|
||||
"""Вычисляет значение хромосомы для заданных значений терминалов."""
|
||||
if self.root is None:
|
||||
raise ValueError("Chromosome is not initialized")
|
||||
def shrink_mutation(self) -> None:
|
||||
"""Усекающая мутация. Заменяет случайно выбранную операцию на случайный терминал."""
|
||||
operation_nodes = [n for n in self.root.list_nodes() if n.value.arity > 0]
|
||||
|
||||
# Мне это не нравится, но, возможно, это будет работать
|
||||
for terminal, value in zip(self.terminals, values):
|
||||
terminal._value = value
|
||||
if not operation_nodes:
|
||||
return
|
||||
|
||||
return self.root._eval()
|
||||
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)
|
||||
|
||||
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:
|
||||
"""Строковое представление древовидной структуры формулы."""
|
||||
if self.root is None:
|
||||
return ""
|
||||
|
||||
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 clone(self) -> "Chromosome.Node":
|
||||
"""Создает копию поддерева."""
|
||||
return Chromosome.Node(
|
||||
self.value, [child.clone() for child in self.children]
|
||||
)
|
||||
|
||||
def get_depth(self) -> int:
|
||||
"""Вычисляет глубину поддерева."""
|
||||
return (
|
||||
max(child.get_depth() for child in self.children) + 1
|
||||
if self.children
|
||||
else 1
|
||||
)
|
||||
|
||||
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])
|
||||
|
||||
|
||||
def _random_terminal(terminals: Sequence[Terminal]) -> Terminal:
|
||||
return random.choice(terminals)
|
||||
|
||||
|
||||
def init_full(chromosome: Chromosome, max_depth: int) -> Chromosome.Node:
|
||||
@classmethod
|
||||
def full_init(
|
||||
cls,
|
||||
terminals: Sequence[Primitive],
|
||||
operations: Sequence[Primitive],
|
||||
max_depth: int,
|
||||
) -> Chromosome:
|
||||
"""Полная инициализация.
|
||||
|
||||
В полном методе при генерации дерева, пока не достигнута максимальная глубина,
|
||||
@@ -121,22 +66,29 @@ def init_full(chromosome: Chromosome, max_depth: int) -> Chromosome.Node:
|
||||
(максимальной глубины) выбираются только терминальные символы.
|
||||
"""
|
||||
|
||||
def build(level: int) -> Chromosome.Node:
|
||||
def build(level: int) -> Node:
|
||||
# Если достигнута максимальная глубина — выбираем терминал
|
||||
if level == max_depth:
|
||||
return Chromosome.Node(_random_terminal(chromosome.terminals), [])
|
||||
return Node(random.choice(terminals))
|
||||
|
||||
# Иначе выбираем операцию и создаём потомков
|
||||
op = random.choice(chromosome.operations)
|
||||
node = Chromosome.Node(op, [build(level + 1) for _ in range(op.arity)])
|
||||
op = random.choice(operations)
|
||||
node = Node(op)
|
||||
for _ in range(op.arity):
|
||||
node.add_child(build(level + 1))
|
||||
return node
|
||||
|
||||
return build(1)
|
||||
return cls(terminals, operations, build(1))
|
||||
|
||||
|
||||
def init_grow(
|
||||
chromosome: Chromosome, max_depth: int, terminal_probability: float = 0.5
|
||||
) -> Chromosome.Node:
|
||||
@classmethod
|
||||
def grow_init(
|
||||
cls,
|
||||
terminals: Sequence[Primitive],
|
||||
operations: Sequence[Primitive],
|
||||
max_depth: int,
|
||||
# min_depth: int, # ???
|
||||
terminal_probability: float = 0.5,
|
||||
) -> Chromosome:
|
||||
"""Растущая инициализация.
|
||||
|
||||
В растущей инициализации генерируются нерегулярные деревья с различной глубиной
|
||||
@@ -145,15 +97,17 @@ def init_grow(
|
||||
прекращается по текущей ветви и поэтому дерево имеет нерегулярную структуру.
|
||||
"""
|
||||
|
||||
def build(level: int) -> Chromosome.Node:
|
||||
def build(level: int) -> Node:
|
||||
# Если достигнута максимальная глубина, либо сыграла заданная вероятность
|
||||
# — выбираем терминал
|
||||
if level == max_depth or random.random() < terminal_probability:
|
||||
return Chromosome.Node(_random_terminal(chromosome.terminals), [])
|
||||
return Node(random.choice(terminals))
|
||||
|
||||
# Иначе выбираем случайную операцию и создаём потомков
|
||||
op = random.choice(chromosome.operations)
|
||||
children = [build(level + 1) for _ in range(op.arity)]
|
||||
return Chromosome.Node(op, children)
|
||||
op = random.choice(operations)
|
||||
node = Node(op)
|
||||
for _ in range(op.arity):
|
||||
node.add_child(build(level + 1))
|
||||
return node
|
||||
|
||||
return build(1)
|
||||
return cls(terminals, operations, build(1))
|
||||
|
||||
@@ -1,192 +1,28 @@
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from .chromosome import Chromosome
|
||||
from .operation import Operation
|
||||
from .terminal import Terminal
|
||||
from .node import swap_subtrees
|
||||
|
||||
|
||||
def crossover_subtree(
|
||||
p1: Chromosome, p2: Chromosome, max_depth: int | None = None
|
||||
parent1: Chromosome, parent2: Chromosome, max_depth: int
|
||||
) -> tuple[Chromosome, Chromosome]:
|
||||
"""Кроссовер поддеревьев.
|
||||
|
||||
Выбираются случайные узлы в каждом родителе, затем соответствующие им поддеревья
|
||||
меняются местами. Если глубина результирующих хромосом превышает max_depth,
|
||||
то их деревья обрезаются до max_depth.
|
||||
"""
|
||||
Кроссовер поддеревьев: выбираются случайные узлы в каждом родителе,
|
||||
затем соответствующие поддеревья обмениваются местами. Возвращаются два новых потомка.
|
||||
child1 = parent1.copy()
|
||||
child2 = parent2.copy()
|
||||
|
||||
Аргументы:
|
||||
p1 : первый родитель (не изменяется).
|
||||
p2 : второй родитель (не изменяется).
|
||||
max_depth : максимальная допустимая глубина потомков. Если None, ограничение не применяется.
|
||||
# Выбираем случайные узлы, не включая корень
|
||||
cut1 = random.choice(child1.root.list_nodes()[1:])
|
||||
cut2 = random.choice(child2.root.list_nodes()[1:])
|
||||
|
||||
Примечания:
|
||||
- Для «совместимости» узлов сперва пытаемся подобрать пары одного класса
|
||||
(оба Terminal или оба Operation). Если подходящей пары не нашлось за
|
||||
разумное число попыток — допускаем любой обмен.
|
||||
- Если задан max_depth, проверяется глубина результирующих потомков,
|
||||
и при превышении лимита выбор узлов повторяется.
|
||||
- Обмен выполняется на КЛОНАХ родителей, чтобы не портить входные деревья.
|
||||
"""
|
||||
swap_subtrees(cut1, cut2)
|
||||
|
||||
# -------- Вспомогательные функции --------
|
||||
child1.prune(max_depth)
|
||||
child2.prune(max_depth)
|
||||
|
||||
def enumerate_nodes_with_meta(
|
||||
root: Chromosome.Node,
|
||||
) -> list[
|
||||
tuple[Chromosome.Node, Optional[Chromosome.Node], Optional[int], list[int], int]
|
||||
]:
|
||||
"""
|
||||
Возвращает список кортежей: (node, parent, index_in_parent, path_from_root, depth_from_root)
|
||||
path_from_root — список индексов детей от корня до узла.
|
||||
depth_from_root: 1 для корня, 2 для детей корня и т.д.
|
||||
"""
|
||||
out: list[
|
||||
tuple[
|
||||
Chromosome.Node,
|
||||
Optional[Chromosome.Node],
|
||||
Optional[int],
|
||||
list[int],
|
||||
int,
|
||||
]
|
||||
] = []
|
||||
|
||||
def dfs(
|
||||
node: Chromosome.Node,
|
||||
parent: Optional[Chromosome.Node],
|
||||
idx: Optional[int],
|
||||
path: list[int],
|
||||
depth: int,
|
||||
) -> None:
|
||||
out.append((node, parent, idx, path, depth))
|
||||
for i, ch in enumerate(node.children):
|
||||
dfs(ch, node, i, path + [i], depth + 1)
|
||||
|
||||
dfs(root, None, None, [], 1)
|
||||
return out
|
||||
|
||||
def get_parent_and_index_by_path(
|
||||
root: Chromosome.Node, path: list[int]
|
||||
) -> tuple[Optional[Chromosome.Node], Optional[int], Chromosome.Node]:
|
||||
"""
|
||||
По пути возвращает (parent, index_in_parent, node).
|
||||
Для корня parent/index равны None.
|
||||
"""
|
||||
if not path:
|
||||
return None, None, root
|
||||
|
||||
parent = root
|
||||
for i in path[:-1]:
|
||||
parent = parent.children[i]
|
||||
idx = path[-1]
|
||||
return parent, idx, parent.children[idx]
|
||||
|
||||
def is_op(node: Chromosome.Node) -> bool:
|
||||
return isinstance(node.value, Operation)
|
||||
|
||||
def is_term(node: Chromosome.Node) -> bool:
|
||||
return isinstance(node.value, Terminal)
|
||||
|
||||
def check_depth_after_swap(
|
||||
node_depth: int, new_subtree: Chromosome.Node, max_d: int
|
||||
) -> bool:
|
||||
"""
|
||||
Проверяет, не превысит ли глубина дерева max_d после замены узла на глубине node_depth
|
||||
на поддерево new_subtree.
|
||||
|
||||
node_depth: глубина узла, который заменяем (1 для корня)
|
||||
new_subtree: поддерево, которое вставляем
|
||||
max_d: максимальная допустимая глубина
|
||||
|
||||
Возвращает True, если глубина будет в пределах нормы.
|
||||
"""
|
||||
# Глубина нового поддерева
|
||||
subtree_depth = new_subtree.get_depth()
|
||||
# Итоговая глубина = глубина узла + глубина поддерева - 1
|
||||
# (т.к. node_depth уже включает этот узел)
|
||||
resulting_depth = node_depth + subtree_depth - 1
|
||||
return resulting_depth <= max_d
|
||||
|
||||
# -------- Выбор доминантного/рецессивного родителя (просто случайно) --------
|
||||
dom, rec = (p1, p2) if random.random() < 0.5 else (p2, p1)
|
||||
|
||||
# Собираем все узлы с метаданными
|
||||
dom_nodes = enumerate_nodes_with_meta(dom.root)
|
||||
rec_nodes = enumerate_nodes_with_meta(rec.root)
|
||||
|
||||
# Пытаемся выбрать совместимые узлы: оба термины ИЛИ оба операции.
|
||||
# Дадим несколько попыток, затем, если не повезло — возьмём любые.
|
||||
MAX_TRIES = 64
|
||||
chosen_dom = None
|
||||
chosen_rec = None
|
||||
|
||||
for _ in range(MAX_TRIES):
|
||||
nd = random.choice(dom_nodes)
|
||||
# Предпочтём узел того же «класса»
|
||||
if is_term(nd[0]):
|
||||
same_type_pool = [nr for nr in rec_nodes if is_term(nr[0])]
|
||||
elif is_op(nd[0]):
|
||||
same_type_pool = [nr for nr in rec_nodes if is_op(nr[0])]
|
||||
else:
|
||||
same_type_pool = rec_nodes # на всякий
|
||||
|
||||
if same_type_pool:
|
||||
nr = random.choice(same_type_pool)
|
||||
|
||||
# Если задан max_depth, проверяем, что обмен не приведёт к превышению глубины
|
||||
if max_depth is not None:
|
||||
nd_node, _, _, nd_path, nd_depth = nd
|
||||
nr_node, _, _, nr_path, nr_depth = nr
|
||||
|
||||
# Проверяем обе возможные замены
|
||||
dom_ok = check_depth_after_swap(nd_depth, nr_node, max_depth)
|
||||
rec_ok = check_depth_after_swap(nr_depth, nd_node, max_depth)
|
||||
|
||||
if dom_ok and rec_ok:
|
||||
chosen_dom, chosen_rec = nd, nr
|
||||
break
|
||||
# Иначе пробуем другую пару
|
||||
else:
|
||||
# Если ограничения нет, принимаем первую подходящую пару
|
||||
chosen_dom, chosen_rec = nd, nr
|
||||
break
|
||||
|
||||
# Если подобрать подходящую пару не удалось
|
||||
if chosen_dom is None or chosen_rec is None:
|
||||
# Возвращаем клоны родителей без изменений
|
||||
return (p1.clone(), p2.clone())
|
||||
|
||||
_, _, _, dom_path, _ = chosen_dom
|
||||
_, _, _, rec_path, _ = chosen_rec
|
||||
|
||||
# -------- Создаём клоны родителей --------
|
||||
c_dom = dom.clone()
|
||||
c_rec = rec.clone()
|
||||
|
||||
# Выцепляем соответствующие позиции на клонах по тем же путям
|
||||
c_dom_parent, c_dom_idx, c_dom_node = get_parent_and_index_by_path(
|
||||
c_dom.root, dom_path
|
||||
)
|
||||
c_rec_parent, c_rec_idx, c_rec_node = get_parent_and_index_by_path(
|
||||
c_rec.root, rec_path
|
||||
)
|
||||
|
||||
# Клонируем поддеревья, чтобы не смешивать ссылки между хромосомами
|
||||
subtree_dom = c_dom_node.clone()
|
||||
subtree_rec = c_rec_node.clone()
|
||||
|
||||
# Меняем местами
|
||||
if c_dom_parent is None:
|
||||
# Меняем корень
|
||||
c_dom.root = subtree_rec
|
||||
else:
|
||||
c_dom_parent.children[c_dom_idx] = subtree_rec # type: ignore[index]
|
||||
|
||||
if c_rec_parent is None:
|
||||
c_rec.root = subtree_dom
|
||||
else:
|
||||
c_rec_parent.children[c_rec_idx] = subtree_dom # type: ignore[index]
|
||||
|
||||
# Возвращаем потомков в том же порядке, что и вход (p1 -> first, p2 -> second)
|
||||
if dom is p1:
|
||||
return (c_dom, c_rec)
|
||||
else:
|
||||
return (c_rec, c_dom)
|
||||
return child1, child2
|
||||
|
||||
@@ -1,143 +1,3 @@
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from .chromosome import Chromosome
|
||||
from .operation import Operation
|
||||
from .terminal import Terminal
|
||||
|
||||
|
||||
def mutate_grow(parent: Chromosome, max_depth: int, max_tries: int = 64) -> Chromosome:
|
||||
"""
|
||||
Растущая мутация (subtree-growing mutation).
|
||||
|
||||
Аргументы:
|
||||
parent : исходная хромосома (не изменяется).
|
||||
max_depth : верхняя граница глубины мутанта.
|
||||
max_tries : ограничение попыток подбора узла/поддерева.
|
||||
|
||||
Возвращает:
|
||||
Новый экземпляр Chromosome (мутант).
|
||||
"""
|
||||
|
||||
# ---------- Вспомогательные ----------
|
||||
def enumerate_nodes_with_meta(
|
||||
root: Chromosome.Node,
|
||||
) -> list[
|
||||
tuple[Chromosome.Node, Optional[Chromosome.Node], Optional[int], list[int], int]
|
||||
]:
|
||||
"""
|
||||
(node, parent, index_in_parent, path_from_root, depth_from_root)
|
||||
depth_from_root: 1 для корня, 2 для детей корня и т.д.
|
||||
"""
|
||||
out: list[
|
||||
tuple[
|
||||
Chromosome.Node,
|
||||
Optional[Chromosome.Node],
|
||||
Optional[int],
|
||||
list[int],
|
||||
int,
|
||||
]
|
||||
] = []
|
||||
|
||||
def dfs(
|
||||
n: Chromosome.Node,
|
||||
p: Optional[Chromosome.Node],
|
||||
idx: Optional[int],
|
||||
path: list[int],
|
||||
depth: int,
|
||||
) -> None:
|
||||
out.append((n, p, idx, path, depth))
|
||||
for i, ch in enumerate(n.children):
|
||||
dfs(ch, n, i, path + [i], depth + 1)
|
||||
|
||||
dfs(root, None, None, [], 1)
|
||||
return out
|
||||
|
||||
def get_parent_and_index_by_path(
|
||||
root: Chromosome.Node, path: list[int]
|
||||
) -> tuple[Optional[Chromosome.Node], Optional[int], Chromosome.Node]:
|
||||
if not path:
|
||||
return None, None, root
|
||||
parent = root
|
||||
for i in path[:-1]:
|
||||
parent = parent.children[i]
|
||||
idx = path[-1]
|
||||
return parent, idx, parent.children[idx]
|
||||
|
||||
def build_depth_limited_subtree(
|
||||
chromo: Chromosome, max_depth_limit: int, current_depth: int = 1
|
||||
) -> Chromosome.Node:
|
||||
"""
|
||||
Строит случайное поддерево с ограничением по глубине.
|
||||
current_depth: текущая глубина узла (1 для корня поддерева).
|
||||
max_depth_limit: максимальная допустимая глубина поддерева.
|
||||
"""
|
||||
# Если достигли максимальной глубины — обязательно терминал
|
||||
if current_depth >= max_depth_limit:
|
||||
term = random.choice(chromo.terminals)
|
||||
return Chromosome.Node(term, [])
|
||||
|
||||
# Иначе случайно выбираем между операцией и терминалом
|
||||
# С большей вероятностью выбираем операцию, если глубина позволяет
|
||||
if random.random() < 0.7: # 70% вероятность операции
|
||||
op = random.choice(chromo.operations)
|
||||
children = [
|
||||
build_depth_limited_subtree(chromo, max_depth_limit, current_depth + 1)
|
||||
for _ in range(op.arity)
|
||||
]
|
||||
return Chromosome.Node(op, children)
|
||||
else:
|
||||
term = random.choice(chromo.terminals)
|
||||
return Chromosome.Node(term, [])
|
||||
|
||||
# ---------- Подготовка ----------
|
||||
# Если в дереве только терминал — мутация невозможна (нужен нетерминал)
|
||||
if isinstance(parent.root.value, Terminal):
|
||||
return parent.clone()
|
||||
|
||||
# Работаем на клоне
|
||||
child = parent.clone()
|
||||
|
||||
# Список нетерминальных узлов с путями и глубинами
|
||||
nodes = enumerate_nodes_with_meta(child.root)
|
||||
internal = [
|
||||
(n, p, i, path, depth)
|
||||
for (n, p, i, path, depth) in nodes
|
||||
if isinstance(n.value, Operation)
|
||||
]
|
||||
|
||||
if not internal:
|
||||
# На всякий случай: если всё терминалы
|
||||
return child
|
||||
|
||||
# ---------- Основной цикл подбора позиции ----------
|
||||
for _ in range(max_tries):
|
||||
node, _, _, path, node_depth = random.choice(internal)
|
||||
|
||||
# Вычисляем максимальную допустимую глубину для нового поддерева
|
||||
# max_depth - node_depth + 1 (так как node_depth начинается с 1)
|
||||
allowed_subtree_depth = max_depth - node_depth + 1
|
||||
|
||||
if allowed_subtree_depth < 1:
|
||||
# Этот узел слишком глубоко — попробуем другой
|
||||
continue
|
||||
|
||||
# Строим новое поддерево с ограничением по глубине
|
||||
new_sub = build_depth_limited_subtree(child, allowed_subtree_depth)
|
||||
|
||||
# Вставляем его на место узла мутации
|
||||
parent_node, idx, _ = get_parent_and_index_by_path(child.root, path)
|
||||
if parent_node is None:
|
||||
child.root = new_sub
|
||||
else:
|
||||
parent_node.children[idx] = new_sub # type: ignore[index]
|
||||
|
||||
# Проверяем, что не превысили максимальную глубину
|
||||
if child.get_depth() <= max_depth:
|
||||
return child
|
||||
else:
|
||||
# Откат: пересоздаём клон
|
||||
child = parent.clone()
|
||||
|
||||
# Если не удалось подобрать подходящее место/поддерево — вернём немутированного клона
|
||||
return parent.clone()
|
||||
|
||||
113
lab4/gp/node.py
Normal file
113
lab4/gp/node.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import random
|
||||
from typing import Sequence
|
||||
|
||||
from .primitive import Primitive
|
||||
from .types import Context, Value
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, value: Primitive):
|
||||
self.value = value
|
||||
self.parent: Node | None = None
|
||||
self.children: list[Node] = []
|
||||
|
||||
def add_child(self, child: Node) -> None:
|
||||
self.children.append(child)
|
||||
child.parent = self
|
||||
|
||||
def remove_child(self, child: Node) -> None:
|
||||
self.children.remove(child)
|
||||
child.parent = None
|
||||
|
||||
def replace_child(self, old_child: Node, new_child: Node) -> None:
|
||||
self.children[self.children.index(old_child)] = new_child
|
||||
old_child.parent = None
|
||||
new_child.parent = self
|
||||
|
||||
def remove_children(self) -> None:
|
||||
for child in self.children:
|
||||
child.parent = None
|
||||
self.children = []
|
||||
|
||||
def copy_subtree(self) -> Node:
|
||||
node = Node(self.value)
|
||||
for child in self.children:
|
||||
node.add_child(child.copy_subtree())
|
||||
return node
|
||||
|
||||
def list_nodes(self) -> list[Node]:
|
||||
nodes: list[Node] = [self]
|
||||
for child in self.children:
|
||||
nodes.extend(child.list_nodes())
|
||||
return nodes
|
||||
|
||||
def prune(self, terminals: Sequence[Primitive], max_depth: int) -> None:
|
||||
"""Усечение поддерева до заданной глубины.
|
||||
|
||||
Заменяет операции на глубине max_depth на случайные терминалы.
|
||||
"""
|
||||
|
||||
def prune_recursive(node: Node, current_depth: int) -> None:
|
||||
if node.value.arity == 0: # Терминалы остаются без изменений
|
||||
return
|
||||
|
||||
if current_depth >= max_depth:
|
||||
node.remove_children()
|
||||
node.value = random.choice(terminals)
|
||||
return
|
||||
|
||||
for child in node.children:
|
||||
prune_recursive(child, current_depth + 1)
|
||||
|
||||
prune_recursive(self, 1)
|
||||
|
||||
def get_subtree_depth(self) -> int:
|
||||
"""Вычисляет глубину поддерева, начиная с текущего узла."""
|
||||
return (
|
||||
max(child.get_subtree_depth() for child in self.children) + 1
|
||||
if self.children
|
||||
else 1
|
||||
)
|
||||
|
||||
def get_level(self) -> int:
|
||||
"""Вычисляет уровень узла в дереве (расстояние от корня). Корень имеет уровень 1."""
|
||||
return self.parent.get_level() + 1 if self.parent else 1
|
||||
|
||||
def eval(self, context: Context) -> Value:
|
||||
return self.value.eval(
|
||||
[child.eval(context) for child in self.children], context
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Рекурсивный перевод древовидного вида формулы в строку в инфиксной форме."""
|
||||
if self.value.arity == 0:
|
||||
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 to_str_tree(self, prefix="", is_last: bool = True) -> str:
|
||||
"""Строковое представление древовидной структуры."""
|
||||
lines = prefix + ("└── " if is_last else "├── ") + self.value.name + "\n"
|
||||
child_prefix = prefix + (" " if is_last else "│ ")
|
||||
for i, child in enumerate(self.children):
|
||||
is_child_last = i == len(self.children) - 1
|
||||
lines += child.to_str_tree(child_prefix, is_child_last)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def swap_subtrees(a: Node, b: Node) -> None:
|
||||
if a.parent is None or b.parent is None:
|
||||
raise ValueError("Нельзя обменять корни деревьев")
|
||||
|
||||
# Сохраняем ссылки на родителей
|
||||
a_parent = a.parent
|
||||
b_parent = b.parent
|
||||
|
||||
i = a_parent.children.index(a)
|
||||
j = b_parent.children.index(b)
|
||||
a_parent.children[i], b_parent.children[j] = b, a
|
||||
a.parent, b.parent = b_parent, a_parent
|
||||
@@ -1,11 +0,0 @@
|
||||
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)
|
||||
@@ -1,6 +1,6 @@
|
||||
import math
|
||||
|
||||
from .operation import Operation
|
||||
from .primitive import Operation
|
||||
|
||||
# Унарные операции
|
||||
NEG = Operation("-", 1, lambda x: -x[0])
|
||||
|
||||
@@ -1,42 +1,30 @@
|
||||
import random
|
||||
from typing import Callable
|
||||
from typing import Sequence
|
||||
|
||||
from .chromosome import Chromosome, InitFunc, init_full, init_grow
|
||||
from .chromosome import Chromosome
|
||||
from .primitive import Primitive
|
||||
|
||||
type Population = list[Chromosome]
|
||||
|
||||
|
||||
def ramped_initialization(
|
||||
population_size: int,
|
||||
chromosomes_per_variation: int,
|
||||
depths: list[int],
|
||||
make_chromosome: Callable[[InitFunc], Chromosome],
|
||||
terminals: Sequence[Primitive],
|
||||
operations: Sequence[Primitive],
|
||||
) -> 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(lambda c: init_full(c, depth)) for _ in range(n_full)
|
||||
Chromosome.full_init(terminals, operations, depth)
|
||||
for _ in range(chromosomes_per_variation)
|
||||
)
|
||||
population.extend(
|
||||
make_chromosome(lambda c: init_grow(c, depth)) for _ in range(n_grow)
|
||||
Chromosome.grow_init(terminals, operations, depth)
|
||||
for _ in range(chromosomes_per_variation)
|
||||
)
|
||||
|
||||
# Из-за округления хромосом может оказаться меньше заданного количества,
|
||||
# поэтому дозаполняем остаток популяции случайными хромосомами
|
||||
while len(population) < population_size:
|
||||
depth = random.choice(depths)
|
||||
init_func = init_full if random.random() < 0.5 else init_grow
|
||||
population.append(make_chromosome(lambda c: init_func(c, depth)))
|
||||
|
||||
return population
|
||||
|
||||
35
lab4/gp/primitive.py
Normal file
35
lab4/gp/primitive.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Sequence
|
||||
|
||||
from .types import Context, Value
|
||||
|
||||
type OperationFn = Callable[[Sequence[Value]], Value]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Primitive:
|
||||
name: str
|
||||
arity: int
|
||||
operation_fn: OperationFn | None
|
||||
|
||||
def eval(self, args: Sequence[Value], context: Context) -> Value:
|
||||
if self.operation_fn is None:
|
||||
return context[self]
|
||||
|
||||
return self.operation_fn(args)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.arity != 0 and self.operation_fn is None:
|
||||
raise ValueError("Operation is required for primitive with non-zero arity")
|
||||
|
||||
|
||||
def Var(name: str) -> Primitive:
|
||||
return Primitive(name=name, arity=0, operation_fn=None)
|
||||
|
||||
|
||||
def Const(name: str, val: Value) -> Primitive:
|
||||
return Primitive(name=name, arity=0, operation_fn=lambda _args: val)
|
||||
|
||||
|
||||
def Operation(name: str, arity: int, operation_fn: OperationFn) -> Primitive:
|
||||
return Primitive(name=name, arity=arity, operation_fn=operation_fn)
|
||||
@@ -1,7 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass()
|
||||
class Terminal:
|
||||
name: str
|
||||
_value: float | None = None
|
||||
13
lab4/gp/types.py
Normal file
13
lab4/gp/types.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from typing import TYPE_CHECKING, Callable, Protocol
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .chromosome import Chromosome
|
||||
from .node import Node
|
||||
from .primitive import Primitive
|
||||
|
||||
type InitFunc = Callable[["Chromosome"], "Node"]
|
||||
type Value = float
|
||||
|
||||
|
||||
class Context(Protocol):
|
||||
def __getitem__(self, key: "Primitive", /) -> Value: ...
|
||||
33
lab4/main.py
33
lab4/main.py
@@ -1,24 +1,25 @@
|
||||
from gp import Chromosome, Terminal, ops
|
||||
from gp.chromosome import init_grow
|
||||
from gp import Chromosome, ops
|
||||
from gp.population import ramped_initialization
|
||||
from gp.primitive import Var
|
||||
|
||||
operations = ops.ALL
|
||||
terminals = [Var(f"x{i}") for i in range(1, 9)]
|
||||
|
||||
terminals = [Terminal(f"x{i}") for i in range(1, 9)]
|
||||
|
||||
chrom = Chromosome(operations, terminals, init_func=lambda c: init_grow(c, 8))
|
||||
print("Depth:", chrom.get_depth())
|
||||
chrom = Chromosome.full_init(terminals, operations, max_depth=3)
|
||||
print("Depth:", chrom.root.get_subtree_depth())
|
||||
print("Formula:", chrom)
|
||||
print("Tree:\n", chrom.str_tree())
|
||||
print("Tree:\n", chrom.root.to_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))
|
||||
context = {var: value for var, value in zip(terminals, values)}
|
||||
print("Value for ", values, ":", chrom.root.eval(context))
|
||||
|
||||
population = ramped_initialization(
|
||||
100,
|
||||
[3, 4, 5, 6, 7, 8],
|
||||
lambda init_func: Chromosome(operations, terminals, init_func),
|
||||
)
|
||||
print("Population size:", len(population))
|
||||
print("Population:")
|
||||
[print(str(chrom)) for chrom in population]
|
||||
# population = ramped_initialization(
|
||||
# 5,
|
||||
# [3, 4, 5, 6, 7, 8],
|
||||
# terminals,
|
||||
# operations,
|
||||
# )
|
||||
# print("Population size:", len(population))
|
||||
# print("Population:")
|
||||
# [print(str(chrom)) for chrom in population]
|
||||
|
||||
8
lab4/pyproject.toml
Normal file
8
lab4/pyproject.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[project]
|
||||
name = "lab4"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = []
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py314"
|
||||
26
lab4/pytest.ini
Normal file
26
lab4/pytest.ini
Normal file
@@ -0,0 +1,26 @@
|
||||
[tool:pytest]
|
||||
# Пути для поиска тестов
|
||||
testpaths = tests
|
||||
|
||||
# Паттерны для имён файлов с тестами
|
||||
python_files = test_*.py
|
||||
|
||||
# Паттерны для имён классов с тестами
|
||||
python_classes = Test*
|
||||
|
||||
# Паттерны для имён функций-тестов
|
||||
python_functions = test_*
|
||||
|
||||
# Опции для более подробного вывода
|
||||
addopts =
|
||||
-v
|
||||
--strict-markers
|
||||
--tb=short
|
||||
--disable-warnings
|
||||
|
||||
# Маркеры для категоризации тестов
|
||||
markers =
|
||||
slow: marks tests as slow (deselect with '-m "not slow"')
|
||||
unit: unit tests
|
||||
integration: integration tests
|
||||
|
||||
Reference in New Issue
Block a user