Skip to content

Commit

Permalink
Merge branch 'main' into add-node-factory-to-graph
Browse files Browse the repository at this point in the history
  • Loading branch information
kasyanovse committed Dec 27, 2023
2 parents 3b4d4d9 + ee2e56c commit 7f761cc
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 149 deletions.
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,11 @@ GOLEM можно установить с помощью ``pip``:
.. |eng| image:: https://img.shields.io/badge/lang-en-red.svg
:target: /README_en.rst

.. |ITMO| image:: https://github.com/aimclub/open-source-ops/blob/add_badge/badges/ITMO_badge_rus.svg
.. |ITMO| image:: https://raw.githubusercontent.com/aimclub/open-source-ops/43bb283758b43d75ec1df0a6bb4ae3eb20066323/badges/ITMO_badge.svg
:alt: Acknowledgement to ITMO
:target: https://itmo.ru

.. |SAI| image:: https://github.com/aimclub/open-source-ops/blob/add_badge/badges/SAI_badge.svg
.. |SAI| image:: https://raw.githubusercontent.com/aimclub/open-source-ops/43bb283758b43d75ec1df0a6bb4ae3eb20066323/badges/SAI_badge.svg
:alt: Acknowledgement to SAI
:target: https://sai.itmo.ru/

Expand Down
4 changes: 2 additions & 2 deletions README_en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,11 @@ There are various cases solved with GOLEM's algorithms:
.. |rus| image:: https://img.shields.io/badge/lang-ru-yellow.svg
:target: /README.rst

.. |ITMO| image:: https://github.com/aimclub/open-source-ops/blob/add_badge/badges/ITMO_badge.svg
.. |ITMO| image:: https://raw.githubusercontent.com/aimclub/open-source-ops/43bb283758b43d75ec1df0a6bb4ae3eb20066323/badges/ITMO_badge.svg
:alt: Acknowledgement to ITMO
:target: https://en.itmo.ru/en/

.. |SAI| image:: https://github.com/aimclub/open-source-ops/blob/add_badge/badges/SAI_badge.svg
.. |SAI| image:: https://raw.githubusercontent.com/aimclub/open-source-ops/43bb283758b43d75ec1df0a6bb4ae3eb20066323/badges/SAI_badge.svg
:alt: Acknowledgement to SAI
:target: https://sai.itmo.ru/

Expand Down
183 changes: 104 additions & 79 deletions golem/core/optimisers/genetic/operators/base_mutations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from copy import deepcopy
from functools import partial
from random import choice, randint, random, sample
from random import choice, randint, random, sample, shuffle
from typing import TYPE_CHECKING, Optional

import numpy as np

from golem.core.adapter import register_native
from golem.core.dag.graph import ReconnectType
from golem.core.dag.graph_node import GraphNode
Expand Down Expand Up @@ -138,56 +141,67 @@ def nodes_not_cycling(source_node: OptNode, target_node: OptNode):

@register_native
def add_intermediate_node(graph: OptGraph,
node_to_mutate: OptNode,
node_factory: OptNodeFactory) -> OptGraph:
# add between node and parent
new_node = node_factory.get_parent_node(node_to_mutate, is_primary=False)
if not new_node:
return graph

# rewire old children to new parent
new_node.nodes_from = node_to_mutate.nodes_from
node_to_mutate.nodes_from = [new_node]

# add new node to graph
graph.add_node(new_node)
nodes_with_parents = [node for node in graph.nodes if node.nodes_from]
if len(nodes_with_parents) > 0:
shuffle(nodes_with_parents)
for node_to_mutate in nodes_with_parents:
# add between node and parent
new_node = node_factory.get_parent_node(node_to_mutate, is_primary=False)
if not new_node:
continue

# rewire old children to new parent
new_node.nodes_from = node_to_mutate.nodes_from
node_to_mutate.nodes_from = [new_node]

# add new node to graph
graph.add_node(new_node)
break
return graph


@register_native
def add_separate_parent_node(graph: OptGraph,
node_to_mutate: OptNode,
node_factory: OptNodeFactory) -> OptGraph:
# add as separate parent
new_node = node_factory.get_parent_node(node_to_mutate, is_primary=True)
if not new_node:
# there is no possible operators
return graph
if node_to_mutate.nodes_from:
node_to_mutate.nodes_from.append(new_node)
else:
node_to_mutate.nodes_from = [new_node]
graph.nodes.append(new_node)
node_idx = np.arange(len(graph.nodes))
shuffle(node_idx)
for idx in node_idx:
node_to_mutate = graph.nodes[idx]
# add as separate parent
new_node = node_factory.get_parent_node(node_to_mutate, is_primary=True)
if not new_node:
# there is no possible operators
continue
if node_to_mutate.nodes_from:
node_to_mutate.nodes_from.append(new_node)
else:
node_to_mutate.nodes_from = [new_node]
graph.nodes.append(new_node)
break
return graph


@register_native
def add_as_child(graph: OptGraph,
node_to_mutate: OptNode,
node_factory: OptNodeFactory) -> OptGraph:
# add as child
old_node_children = graph.node_children(node_to_mutate)
new_node_child = choice(old_node_children) if old_node_children else None
new_node = node_factory.get_node(is_primary=False)
if not new_node:
return graph
graph.add_node(new_node)
graph.connect_nodes(node_parent=node_to_mutate, node_child=new_node)
if new_node_child:
graph.connect_nodes(node_parent=new_node, node_child=new_node_child)
graph.disconnect_nodes(node_parent=node_to_mutate, node_child=new_node_child,
clean_up_leftovers=True)

node_idx = np.arange(len(graph.nodes))
shuffle(node_idx)
for idx in node_idx:
node_to_mutate = graph.nodes[idx]
# add as child
old_node_children = graph.node_children(node_to_mutate)
new_node_child = choice(old_node_children) if old_node_children else None
new_node = node_factory.get_node(is_primary=False)
if not new_node:
continue
graph.add_node(new_node)
graph.connect_nodes(node_parent=node_to_mutate, node_child=new_node)
if new_node_child:
graph.connect_nodes(node_parent=new_node, node_child=new_node_child)
graph.disconnect_nodes(node_parent=node_to_mutate, node_child=new_node_child,
clean_up_leftovers=True)
break
return graph


Expand All @@ -202,21 +216,21 @@ def single_add_mutation(graph: OptGraph,
:param graph: graph to mutate
"""

if graph.depth >= requirements.max_depth:
# add mutation is not possible
return graph

node_to_mutate = choice(graph.nodes)

single_add_strategies = [add_as_child, add_separate_parent_node]
if node_to_mutate.nodes_from:
single_add_strategies.append(add_intermediate_node)
strategy = choice(single_add_strategies)

node_factory = graph.node_factory or graph_gen_params.node_factory
result = strategy(graph, node_to_mutate, node_factory)
return result
new_graph = deepcopy(graph)
single_add_strategies = [add_as_child, add_separate_parent_node, add_intermediate_node]
shuffle(single_add_strategies)
for strategy in single_add_strategies:
new_graph = strategy(new_graph, node_factory)
# maximum three equality check
if new_graph == graph:
continue
break
return new_graph


@register_native
Expand All @@ -230,12 +244,16 @@ def single_change_mutation(graph: OptGraph,
:param graph: graph to mutate
"""
node = choice(graph.nodes)
node_factory = graph.node_factory or graph_gen_params.node_factory
new_node = node_factory.exchange_node(node)
if not new_node:
return graph
graph.update_node(node, new_node)
node_idx = np.arange(len(graph.nodes))
shuffle(node_idx)
for idx in node_idx:
node = graph.nodes[idx]
new_node = node_factory.exchange_node(node)
if not new_node:
continue
graph.update_node(node, new_node)
break
return graph


Expand Down Expand Up @@ -293,22 +311,27 @@ def tree_growth(graph: OptGraph,
"""
node_factory = graph.node_factory or graph_gen_params.node_factory
random_graph_factory = graph.random_graph_factory or graph_gen_params.random_graph_factory
node_from_graph = choice(graph.nodes)
if local_growth:
max_depth = distance_to_primary_level(node_from_graph)
is_primary_node_selected = (not node_from_graph.nodes_from) or (node_from_graph != graph.root_node and
randint(0, 1))
else:
max_depth = requirements.max_depth - distance_to_root_level(graph, node_from_graph)
is_primary_node_selected = \
distance_to_root_level(graph, node_from_graph) >= requirements.max_depth and randint(0, 1)
if is_primary_node_selected:
new_subtree = node_factory.get_node(is_primary=True)
if not new_subtree:
return graph
else:
new_subtree = random_graph_factory(requirements, max_depth).root_node
graph.update_subtree(node_from_graph, new_subtree)
node_idx = np.arange(len(graph.nodes))
shuffle(node_idx)
for idx in node_idx:
node_from_graph = graph.nodes[idx]
if local_growth:
max_depth = distance_to_primary_level(node_from_graph)
is_primary_node_selected = (not node_from_graph.nodes_from) or (node_from_graph != graph.root_node and
randint(0, 1))
else:
max_depth = requirements.max_depth - distance_to_root_level(graph, node_from_graph)
is_primary_node_selected = \
distance_to_root_level(graph, node_from_graph) >= requirements.max_depth and randint(0, 1)
if is_primary_node_selected:
new_subtree = node_factory.get_node(is_primary=True)
if not new_subtree:
continue
else:
new_subtree = random_graph_factory(requirements, max_depth).root_node

graph.update_subtree(node_from_graph, new_subtree)
break
return graph


Expand Down Expand Up @@ -354,16 +377,18 @@ def reduce_mutation(graph: OptGraph,

node_factory = graph.node_factory or graph_gen_params.node_factory
nodes = [node for node in graph.nodes if node is not graph.root_node]
node_to_del = choice(nodes)
children = graph.node_children(node_to_del)
is_possible_to_delete = all([len(child.nodes_from) - 1 >= requirements.min_arity for child in children])
if is_possible_to_delete:
graph.delete_subtree(node_to_del)
else:
primary_node = node_factory.get_node(is_primary=True)
if not primary_node:
return graph
graph.update_subtree(node_to_del, primary_node)
shuffle(nodes)
for node_to_del in nodes:
children = graph.node_children(node_to_del)
is_possible_to_delete = all([len(child.nodes_from) - 1 >= requirements.min_arity for child in children])
if is_possible_to_delete:
graph.delete_subtree(node_to_del)
else:
primary_node = node_factory.get_node(is_primary=True)
if not primary_node:
continue
graph.update_subtree(node_to_del, primary_node)
break
return graph


Expand Down
77 changes: 33 additions & 44 deletions golem/core/optimisers/genetic/operators/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,74 +81,63 @@ def __call__(self, population: Union[Individual, PopulationT]) -> Union[Individu
if isinstance(population, Individual):
population = [population]

final_population, mutations_applied, application_attempts = tuple(zip(*map(self._mutation, population)))
final_population, application_attempts = tuple(zip(*map(self._mutation, population)))

# drop individuals to which mutations could not be applied
final_population = [ind for ind, init_ind, attempt in zip(final_population, population, application_attempts)
if not attempt or ind.graph != init_ind.graph]
if not(attempt and ind.graph == init_ind.graph)]

if len(population) == 1:
return final_population[0] if final_population else final_population

return final_population

def _mutation(self, individual: Individual) -> Tuple[Individual, Optional[MutationIdType], bool]:
def _mutation(self, individual: Individual) -> Tuple[Individual, bool]:
""" Function applies mutation operator to graph """
application_attempt = False
mutation_applied = None
for _ in range(self.parameters.max_num_of_operator_attempts):
new_graph = deepcopy(individual.graph)

new_graph, mutation_applied = self._apply_mutations(new_graph)
if mutation_applied is None:
continue
application_attempt = True
is_correct_graph = self.graph_generation_params.verifier(new_graph)
if is_correct_graph:
parent_operator = ParentOperator(type_='mutation',
operators=mutation_applied,
parent_individuals=individual)
individual = Individual(new_graph, parent_operator,
metadata=self.requirements.static_individual_metadata)
break
mutation_type = self._operator_agent.choose_action(individual.graph)
is_applied = self._will_mutation_be_applied(mutation_type)
if is_applied:
for _ in range(self.parameters.max_num_of_operator_attempts):
new_graph = deepcopy(individual.graph)

new_graph = self._apply_mutations(new_graph, mutation_type)
is_correct_graph = self.graph_generation_params.verifier(new_graph)
if is_correct_graph:
if isinstance(mutation_type, MutationTypesEnum):
mutation_name = mutation_type.name
else:
mutation_name = mutation_type.__name__
parent_operator = ParentOperator(type_='mutation',
operators=mutation_name,
parent_individuals=individual)
individual = Individual(new_graph, parent_operator,
metadata=self.requirements.static_individual_metadata)
break
else:
# Collect invalid actions
self.agent_experience.collect_experience(individual, mutation_applied, reward=-1.0)
else:
self.log.debug('Number of mutation attempts exceeded. '
'Please check optimization parameters for correctness.')
return individual, mutation_applied, application_attempt
self.agent_experience.collect_experience(individual, mutation_type, reward=-1.0)

self.log.debug(f'Number of attempts for {mutation_type} mutation application exceeded. '
'Please check optimization parameters for correctness.')
return individual, is_applied

def _sample_num_of_mutations(self) -> int:
def _sample_num_of_mutations(self, mutation_type: Union[MutationTypesEnum, Callable]) -> int:
# most of the time returns 1 or rarely several mutations
if self.parameters.variable_mutation_num:
is_custom_mutation = isinstance(mutation_type, Callable)
if self.parameters.variable_mutation_num and not is_custom_mutation:
num_mut = max(int(round(np.random.lognormal(0, sigma=0.5))), 1)
else:
num_mut = 1
return num_mut

def _apply_mutations(self, new_graph: Graph) -> Tuple[Graph, Optional[MutationIdType]]:
def _apply_mutations(self, new_graph: Graph, mutation_type: Union[MutationTypesEnum, Callable]) -> Graph:
"""Apply mutation 1 or few times iteratively"""
mutation_type = self._operator_agent.choose_action(new_graph)
mutation_applied = None
for _ in range(self._sample_num_of_mutations()):
new_graph, applied = self._adapt_and_apply_mutation(new_graph, mutation_type)
if applied:
mutation_applied = mutation_type
is_custom_mutation = isinstance(mutation_type, Callable)
if is_custom_mutation: # custom mutation occurs once
break
return new_graph, mutation_applied

def _adapt_and_apply_mutation(self, new_graph: Graph, mutation_type) -> Tuple[Graph, bool]:
applied = self._will_mutation_be_applied(mutation_type)
if applied:
# get the mutation function and adapt it
for _ in range(self._sample_num_of_mutations(mutation_type)):
mutation_func = self._get_mutation_func(mutation_type)
new_graph = mutation_func(new_graph, requirements=self.requirements,
graph_gen_params=self.graph_generation_params,
parameters=self.parameters)
return new_graph, applied
return new_graph

def _will_mutation_be_applied(self, mutation_type: Union[MutationTypesEnum, Callable]) -> bool:
return random() <= self.parameters.mutation_prob and mutation_type is not MutationTypesEnum.none
Expand Down
2 changes: 1 addition & 1 deletion golem/core/optimisers/genetic/parameters/mutation_prob.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class AdaptiveMutationProb(AdaptiveParameter[float]):
def __init__(self, default_prob: float = 0.5):
self._current_std = 0.
self._max_std = 0.
self._min_proba = 0.05
self._min_proba = 0.1
self._default_prob = default_prob

@property
Expand Down
2 changes: 1 addition & 1 deletion golem/core/tuning/optuna_tuner.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def _tune(self, graph: DomainGraphForTune, show_progress: bool = True) -> \
n_trials=self.iterations,
n_jobs=self.n_jobs,
timeout=remaining_time,
callbacks=[self.early_stopping_callback],
callbacks=[self.early_stopping_callback] if not is_multi_objective else None,
show_progress_bar=show_progress)

if not is_multi_objective:
Expand Down
Loading

0 comments on commit 7f761cc

Please sign in to comment.