another save
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,4 +2,5 @@
|
|||||||
|
|
||||||
!**/
|
!**/
|
||||||
!*.gitignore
|
!*.gitignore
|
||||||
!*.py
|
!*.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 .chromosome import Chromosome
|
||||||
from .operation import Operation
|
|
||||||
from .terminal import Terminal
|
|
||||||
|
|
||||||
__all__ = ["Chromosome", "Operation", "Terminal"]
|
__all__ = ["Chromosome"]
|
||||||
|
|||||||
@@ -1,159 +1,113 @@
|
|||||||
import random
|
import random
|
||||||
from typing import Callable, Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from .operation import Operation
|
from .node import Node
|
||||||
from .terminal import Terminal
|
from .primitive import Primitive
|
||||||
|
|
||||||
type InitFunc = Callable[["Chromosome"], "Chromosome.Node"]
|
|
||||||
|
|
||||||
|
|
||||||
class Chromosome:
|
class Chromosome:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
operations: Sequence[Operation],
|
terminals: Sequence[Primitive],
|
||||||
terminals: Sequence[Terminal],
|
operations: Sequence[Primitive],
|
||||||
init_func: InitFunc,
|
root: Node,
|
||||||
):
|
):
|
||||||
self.operations = operations
|
|
||||||
self.terminals = terminals
|
self.terminals = terminals
|
||||||
self.root = init_func(self)
|
self.operations = operations
|
||||||
|
self.root = root
|
||||||
|
|
||||||
def get_depth(self) -> int:
|
def copy(self) -> Chromosome:
|
||||||
"""Вычисляет глубину дерева. Дерево из одного только корня имеет глубину 1."""
|
return Chromosome(self.terminals, self.operations, self.root.copy_subtree())
|
||||||
return self.root.get_depth() if self.root is not None else 0
|
|
||||||
|
|
||||||
def clone(self) -> "Chromosome":
|
def prune(self, max_depth: int) -> None:
|
||||||
"""Создает копию хромосомы."""
|
self.root.prune(self.terminals, max_depth)
|
||||||
return Chromosome(
|
|
||||||
self.operations,
|
|
||||||
self.terminals,
|
|
||||||
lambda _: self.root.clone(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def eval(self, values: list[float]) -> float:
|
def shrink_mutation(self) -> None:
|
||||||
"""Вычисляет значение хромосомы для заданных значений терминалов."""
|
"""Усекающая мутация. Заменяет случайно выбранную операцию на случайный терминал."""
|
||||||
if self.root is None:
|
operation_nodes = [n for n in self.root.list_nodes() if n.value.arity > 0]
|
||||||
raise ValueError("Chromosome is not initialized")
|
|
||||||
|
|
||||||
# Мне это не нравится, но, возможно, это будет работать
|
if not operation_nodes:
|
||||||
for terminal, value in zip(self.terminals, values):
|
return
|
||||||
terminal._value = value
|
|
||||||
|
|
||||||
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:
|
def __str__(self) -> str:
|
||||||
"""Строковое представление хромосомы в виде формулы в инфиксной форме."""
|
"""Строковое представление хромосомы в виде формулы в инфиксной форме."""
|
||||||
return str(self.root)
|
return str(self.root)
|
||||||
|
|
||||||
def _tree_lines(
|
@classmethod
|
||||||
self, node: "Chromosome.Node", prefix: str = "", is_last: bool = True
|
def full_init(
|
||||||
) -> list[str]:
|
cls,
|
||||||
connector = "└── " if is_last else "├── "
|
terminals: Sequence[Primitive],
|
||||||
lines = [prefix + connector + node.value.name]
|
operations: Sequence[Primitive],
|
||||||
child_prefix = prefix + (" " if is_last else "│ ")
|
max_depth: int,
|
||||||
for i, child in enumerate(node.children):
|
) -> Chromosome:
|
||||||
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]
|
def build(level: int) -> Node:
|
||||||
for i, child in enumerate(self.root.children):
|
# Если достигнута максимальная глубина — выбираем терминал
|
||||||
last = i == len(self.root.children) - 1
|
if level == max_depth:
|
||||||
lines.extend(self._tree_lines(child, "", last))
|
return Node(random.choice(terminals))
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
class Node:
|
# Иначе выбираем операцию и создаём потомков
|
||||||
def __init__(
|
op = random.choice(operations)
|
||||||
self, value: Operation | Terminal, children: list["Chromosome.Node"]
|
node = Node(op)
|
||||||
):
|
for _ in range(op.arity):
|
||||||
self.value = value
|
node.add_child(build(level + 1))
|
||||||
self.children = children
|
return node
|
||||||
|
|
||||||
def clone(self) -> "Chromosome.Node":
|
return cls(terminals, operations, build(1))
|
||||||
"""Создает копию поддерева."""
|
|
||||||
return Chromosome.Node(
|
|
||||||
self.value, [child.clone() for child in self.children]
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_depth(self) -> int:
|
@classmethod
|
||||||
"""Вычисляет глубину поддерева."""
|
def grow_init(
|
||||||
return (
|
cls,
|
||||||
max(child.get_depth() for child in self.children) + 1
|
terminals: Sequence[Primitive],
|
||||||
if self.children
|
operations: Sequence[Primitive],
|
||||||
else 1
|
max_depth: int,
|
||||||
)
|
# min_depth: int, # ???
|
||||||
|
terminal_probability: float = 0.5,
|
||||||
|
) -> Chromosome:
|
||||||
|
"""Растущая инициализация.
|
||||||
|
|
||||||
def __str__(self) -> str:
|
В растущей инициализации генерируются нерегулярные деревья с различной глубиной
|
||||||
"""Рекурсивный перевод древовидного вида формулы в строку в инфиксной форме."""
|
листьев вследствие случайного на каждом шаге выбора функционального
|
||||||
if isinstance(self.value, Terminal):
|
или терминального символа. Здесь при выборе терминального символа рост дерева
|
||||||
return self.value.name
|
прекращается по текущей ветви и поэтому дерево имеет нерегулярную структуру.
|
||||||
|
"""
|
||||||
|
|
||||||
if self.value.arity == 2:
|
def build(level: int) -> Node:
|
||||||
return f"({self.children[0]} {self.value.name} {self.children[1]})"
|
# Если достигнута максимальная глубина, либо сыграла заданная вероятность
|
||||||
|
# — выбираем терминал
|
||||||
|
if level == max_depth or random.random() < terminal_probability:
|
||||||
|
return Node(random.choice(terminals))
|
||||||
|
|
||||||
return (
|
# Иначе выбираем случайную операцию и создаём потомков
|
||||||
f"{self.value.name}({', '.join(str(child) for child in self.children)})"
|
op = random.choice(operations)
|
||||||
)
|
node = Node(op)
|
||||||
|
for _ in range(op.arity):
|
||||||
|
node.add_child(build(level + 1))
|
||||||
|
return node
|
||||||
|
|
||||||
def _eval(self) -> float:
|
return cls(terminals, operations, build(1))
|
||||||
"""Рекурсивно вычисляет значение поддерева. Значения терминалов должны быть
|
|
||||||
заданы предварительно."""
|
|
||||||
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:
|
|
||||||
"""Полная инициализация.
|
|
||||||
|
|
||||||
В полном методе при генерации дерева, пока не достигнута максимальная глубина,
|
|
||||||
допускается выбор только функциональных символов, а на последнем уровне
|
|
||||||
(максимальной глубины) выбираются только терминальные символы.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build(level: int) -> Chromosome.Node:
|
|
||||||
# Если достигнута максимальная глубина — выбираем терминал
|
|
||||||
if level == max_depth:
|
|
||||||
return Chromosome.Node(_random_terminal(chromosome.terminals), [])
|
|
||||||
|
|
||||||
# Иначе выбираем операцию и создаём потомков
|
|
||||||
op = random.choice(chromosome.operations)
|
|
||||||
node = Chromosome.Node(op, [build(level + 1) for _ in range(op.arity)])
|
|
||||||
return node
|
|
||||||
|
|
||||||
return build(1)
|
|
||||||
|
|
||||||
|
|
||||||
def init_grow(
|
|
||||||
chromosome: Chromosome, max_depth: int, terminal_probability: float = 0.5
|
|
||||||
) -> Chromosome.Node:
|
|
||||||
"""Растущая инициализация.
|
|
||||||
|
|
||||||
В растущей инициализации генерируются нерегулярные деревья с различной глубиной
|
|
||||||
листьев вследствие случайного на каждом шаге выбора функционального
|
|
||||||
или терминального символа. Здесь при выборе терминального символа рост дерева
|
|
||||||
прекращается по текущей ветви и поэтому дерево имеет нерегулярную структуру.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build(level: int) -> Chromosome.Node:
|
|
||||||
# Если достигнута максимальная глубина, либо сыграла заданная вероятность
|
|
||||||
# — выбираем терминал
|
|
||||||
if level == max_depth or random.random() < terminal_probability:
|
|
||||||
return Chromosome.Node(_random_terminal(chromosome.terminals), [])
|
|
||||||
|
|
||||||
# Иначе выбираем случайную операцию и создаём потомков
|
|
||||||
op = random.choice(chromosome.operations)
|
|
||||||
children = [build(level + 1) for _ in range(op.arity)]
|
|
||||||
return Chromosome.Node(op, children)
|
|
||||||
|
|
||||||
return build(1)
|
|
||||||
|
|||||||
@@ -1,192 +1,28 @@
|
|||||||
import random
|
import random
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .chromosome import Chromosome
|
from .chromosome import Chromosome
|
||||||
from .operation import Operation
|
from .node import swap_subtrees
|
||||||
from .terminal import Terminal
|
|
||||||
|
|
||||||
|
|
||||||
def crossover_subtree(
|
def crossover_subtree(
|
||||||
p1: Chromosome, p2: Chromosome, max_depth: int | None = None
|
parent1: Chromosome, parent2: Chromosome, max_depth: int
|
||||||
) -> tuple[Chromosome, Chromosome]:
|
) -> tuple[Chromosome, Chromosome]:
|
||||||
|
"""Кроссовер поддеревьев.
|
||||||
|
|
||||||
|
Выбираются случайные узлы в каждом родителе, затем соответствующие им поддеревья
|
||||||
|
меняются местами. Если глубина результирующих хромосом превышает max_depth,
|
||||||
|
то их деревья обрезаются до max_depth.
|
||||||
"""
|
"""
|
||||||
Кроссовер поддеревьев: выбираются случайные узлы в каждом родителе,
|
child1 = parent1.copy()
|
||||||
затем соответствующие поддеревья обмениваются местами. Возвращаются два новых потомка.
|
child2 = parent2.copy()
|
||||||
|
|
||||||
Аргументы:
|
# Выбираем случайные узлы, не включая корень
|
||||||
p1 : первый родитель (не изменяется).
|
cut1 = random.choice(child1.root.list_nodes()[1:])
|
||||||
p2 : второй родитель (не изменяется).
|
cut2 = random.choice(child2.root.list_nodes()[1:])
|
||||||
max_depth : максимальная допустимая глубина потомков. Если None, ограничение не применяется.
|
|
||||||
|
|
||||||
Примечания:
|
swap_subtrees(cut1, cut2)
|
||||||
- Для «совместимости» узлов сперва пытаемся подобрать пары одного класса
|
|
||||||
(оба Terminal или оба Operation). Если подходящей пары не нашлось за
|
|
||||||
разумное число попыток — допускаем любой обмен.
|
|
||||||
- Если задан max_depth, проверяется глубина результирующих потомков,
|
|
||||||
и при превышении лимита выбор узлов повторяется.
|
|
||||||
- Обмен выполняется на КЛОНАХ родителей, чтобы не портить входные деревья.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# -------- Вспомогательные функции --------
|
child1.prune(max_depth)
|
||||||
|
child2.prune(max_depth)
|
||||||
|
|
||||||
def enumerate_nodes_with_meta(
|
return child1, child2
|
||||||
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)
|
|
||||||
|
|||||||
@@ -1,143 +1,3 @@
|
|||||||
import random
|
import random
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .chromosome import Chromosome
|
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
|
import math
|
||||||
|
|
||||||
from .operation import Operation
|
from .primitive import Operation
|
||||||
|
|
||||||
# Унарные операции
|
# Унарные операции
|
||||||
NEG = Operation("-", 1, lambda x: -x[0])
|
NEG = Operation("-", 1, lambda x: -x[0])
|
||||||
|
|||||||
@@ -1,42 +1,30 @@
|
|||||||
import random
|
from typing import Sequence
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from .chromosome import Chromosome, InitFunc, init_full, init_grow
|
from .chromosome import Chromosome
|
||||||
|
from .primitive import Primitive
|
||||||
|
|
||||||
type Population = list[Chromosome]
|
type Population = list[Chromosome]
|
||||||
|
|
||||||
|
|
||||||
def ramped_initialization(
|
def ramped_initialization(
|
||||||
population_size: int,
|
chromosomes_per_variation: int,
|
||||||
depths: list[int],
|
depths: list[int],
|
||||||
make_chromosome: Callable[[InitFunc], Chromosome],
|
terminals: Sequence[Primitive],
|
||||||
|
operations: Sequence[Primitive],
|
||||||
) -> Population:
|
) -> Population:
|
||||||
"""Комбинация методов grow и full инициализации хромосом для инициализации начальной
|
"""Комбинация методов grow и full инициализации хромосом для инициализации начальной
|
||||||
популяции.
|
популяции.
|
||||||
|
|
||||||
Начальная популяция генерируется так, чтобы в нее входили деревья с разной
|
|
||||||
максимальной длиной примерно поровну. Для каждой глубины первая половина деревьев
|
|
||||||
генерируется полным методом, а вторая – растущей инициализацией.
|
|
||||||
"""
|
"""
|
||||||
population: Population = []
|
population: Population = []
|
||||||
per_depth = population_size / len(depths)
|
|
||||||
|
|
||||||
for depth in depths:
|
for depth in depths:
|
||||||
n_full = int(per_depth / 2)
|
|
||||||
n_grow = int(per_depth / 2)
|
|
||||||
|
|
||||||
population.extend(
|
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(
|
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
|
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 import Chromosome, ops
|
||||||
from gp.chromosome import init_grow
|
|
||||||
from gp.population import ramped_initialization
|
from gp.population import ramped_initialization
|
||||||
|
from gp.primitive import Var
|
||||||
|
|
||||||
operations = ops.ALL
|
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.full_init(terminals, operations, max_depth=3)
|
||||||
|
print("Depth:", chrom.root.get_subtree_depth())
|
||||||
chrom = Chromosome(operations, terminals, init_func=lambda c: init_grow(c, 8))
|
|
||||||
print("Depth:", chrom.get_depth())
|
|
||||||
print("Formula:", chrom)
|
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]
|
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(
|
# population = ramped_initialization(
|
||||||
100,
|
# 5,
|
||||||
[3, 4, 5, 6, 7, 8],
|
# [3, 4, 5, 6, 7, 8],
|
||||||
lambda init_func: Chromosome(operations, terminals, init_func),
|
# terminals,
|
||||||
)
|
# operations,
|
||||||
print("Population size:", len(population))
|
# )
|
||||||
print("Population:")
|
# print("Population size:", len(population))
|
||||||
[print(str(chrom)) for chrom in 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