Skip to content

Commit

Permalink
Add Quick Start example to readme with several usability fixes (#86)
Browse files Browse the repository at this point in the history
* Change default params for optimization

* minor upd in tree_search.py

* Simpler objective.py init for single metric

* minor: fix return type in optimise()

* WIP add minimal_run.py

* Minor enhancement in optimizer: aut adapt initial_graphs

* Fix OptGraph to Graph in adapter.py

* WIP minimal_run

* Rename minimal_run.py

* Add Quick Start example to README (ru & en)

* Fix pep8 issues

* Fix DirectAdapter

* Fix typo

* Make quick start more concise

* Add links to readme files

* fixup links
  • Loading branch information
gkirgizov authored Apr 24, 2023
1 parent 577dd6f commit c7c910c
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 47 deletions.
32 changes: 31 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ GOLEM потенциально применим к любой структуре
- `Поиск архитектуры нейронных сетей <https://github.com/ITMO-NSS-team/nas-fedot>`_

Поскольку GOLEM - это фреймворк общего назначения, легко представить его потенциальное применение, например,
поиск конечных автоматов для управления в робототехнике или изучение молекулярных графов для разработки лекарств и
поиск конечных автоматов для алгоритмов контроля в робототехнике или оптимизация молекулярных графов для разработки лекарств и
многое другое.


Expand All @@ -77,6 +77,36 @@ GOLEM можно установить с помощью ``pip``:
$ pip install thegolem
Быстрый старт
=============

Следующий пример показывает поиск графа по графу-эталону с помощью метрики расстояния редактирования (Edit Distance). Оптимизатор настраивается с минимальным набором параметров и простыми одноточечными мутациями. Более подробные примеры можно найти в файлах `simple_run.py <https://github.com/aimclub/GOLEM/blob/main/examples/synthetic_graph_evolution/simple_run.py>`_, `graph_search.py <https://github.com/aimclub/GOLEM/blob/main/examples/synthetic_graph_evolution/graph_search.py>`_ и `tree_search.py <https://github.com/aimclub/GOLEM/blob/main/examples/synthetic_graph_evolution/tree_search.py>`_ в директории `examples/synthetic_graph_evolution <https://github.com/aimclub/GOLEM/tree/main/examples/synthetic_graph_evolution>`_.

.. code-block::
def run_graph_search(size=16, timeout=8):
# Генерируем целевой граф и целевую функцию в виде edit distance
node_types = ('a', 'b') # Available node types that can appear in graphs
target_graph = generate_labeled_graph('tree', size, node_types)
objective = Objective(partial(tree_edit_dist, target_graph))
initial_population = [generate_labeled_graph('tree', 5, node_types) for _ in range(10)]
# Укажем параметры оптимизации
requirements = GraphRequirements(timeout=timedelta(minutes=timeout))
gen_params = GraphGenerationParams(adapter=BaseNetworkxAdapter(), available_node_types=node_types)
algo_params = GPAlgorithmParameters(pop_size=30)
# Инициализируем оптимизатор и запустим оптимизацию
optimiser = EvoGraphOptimizer(objective, initial_population, requirements, gen_params, algo_params)
found_graphs = optimiser.optimise(objective)
# Визуализируем итоговый граф и график сходимости
found_graph = gen_params.adapter.restore(found_graphs[0]) # Transform back to NetworkX graph
draw_graphs_subplots(target_graph, found_graph, titles=['Target Graph', 'Found Graph'])
optimiser.history.show.fitness_line()
return found_graph
Структура проекта
=================

Expand Down
30 changes: 30 additions & 0 deletions README_en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,36 @@ GOLEM can be installed with ``pip``:
$ pip install thegolem
Quick Start Example
===================

Following example demonstrates graph search using reference graph & edit distance metric. Optimizer is set up with a minimal set of parameters and simple single-point mutations. For more details see examples `simple_run.py <https://github.com/aimclub/GOLEM/blob/main/examples/synthetic_graph_evolution/simple_run.py>`_, `graph_search.py <https://github.com/aimclub/GOLEM/blob/main/examples/synthetic_graph_evolution/graph_search.py>`_ and `tree_search.py <https://github.com/aimclub/GOLEM/blob/main/examples/synthetic_graph_evolution/tree_search.py>`_ in directory `examples/synthetic_graph_evolution <https://github.com/aimclub/GOLEM/tree/main/examples/synthetic_graph_evolution>`_.

.. code-block::
def run_graph_search(size=16, timeout=8):
# Generate target graph sought by optimizer using edit distance objective
node_types = ('a', 'b') # Available node types that can appear in graphs
target_graph = generate_labeled_graph('tree', size, node_types)
objective = Objective(partial(tree_edit_dist, target_graph))
initial_population = [generate_labeled_graph('tree', 5, node_types) for _ in range(10)]
# Setup optimization parameters
requirements = GraphRequirements(timeout=timedelta(minutes=timeout))
gen_params = GraphGenerationParams(adapter=BaseNetworkxAdapter(), available_node_types=node_types)
algo_params = GPAlgorithmParameters(pop_size=30)
# Build and run the optimizer
optimiser = EvoGraphOptimizer(objective, initial_population, requirements, gen_params, algo_params)
found_graphs = optimiser.optimise(objective)
# Visualize results
found_graph = gen_params.adapter.restore(found_graphs[0]) # Transform back to NetworkX graph
draw_graphs_subplots(target_graph, found_graph, titles=['Target Graph', 'Found Graph'])
optimiser.history.show.fitness_line()
return found_graph
Project Structure
=================

Expand Down
6 changes: 2 additions & 4 deletions examples/synthetic_graph_evolution/graph_search.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import timedelta
from functools import partial
from typing import Type, Optional, Sequence
from typing import Type, Optional

from examples.synthetic_graph_evolution.experiment import run_experiments
from examples.synthetic_graph_evolution.generators import generate_labeled_graph
Expand Down Expand Up @@ -64,10 +64,8 @@ def graph_search_setup(target_graph: nx.DiGraph,
)

# Generate simple initial population with line graphs
initial_graphs = [generate_labeled_graph('line', k+3)
initial_graphs = [generate_labeled_graph('line', k+3, node_types)
for k in range(gp_params.pop_size)]
initial_graphs = graph_gen_params.adapter.adapt(initial_graphs)

# Build the optimizer
optimiser = optimizer_cls(objective, initial_graphs, requirements, graph_gen_params, gp_params)
return optimiser, objective
Expand Down
67 changes: 67 additions & 0 deletions examples/synthetic_graph_evolution/simple_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from datetime import timedelta
from functools import partial

from examples.synthetic_graph_evolution.generators import generate_labeled_graph
from examples.synthetic_graph_evolution.utils import draw_graphs_subplots
from golem.core.adapter.nx_adapter import BaseNetworkxAdapter
from golem.core.dag.verification_rules import DEFAULT_DAG_RULES
from golem.core.optimisers.genetic.gp_optimizer import EvoGraphOptimizer
from golem.core.optimisers.genetic.gp_params import GPAlgorithmParameters
from golem.core.optimisers.genetic.operators.base_mutations import MutationTypesEnum
from golem.core.optimisers.genetic.operators.crossover import CrossoverTypesEnum
from golem.core.optimisers.genetic.operators.inheritance import GeneticSchemeTypesEnum
from golem.core.optimisers.objective import Objective
from golem.core.optimisers.optimization_parameters import GraphRequirements
from golem.core.optimisers.optimizer import GraphGenerationParams
from golem.metrics.edit_distance import tree_edit_dist


def run_graph_search(size=16, timeout=8, visualize=True):
# Generate target graph that will be sought by optimizer
node_types = ('a', 'b')
target_graph = generate_labeled_graph('tree', size, node_labels=node_types)

# Generate initial population with small tree graphs
initial_graphs = [generate_labeled_graph('tree', 5, node_types) for _ in range(10)]
# Setup objective: edit distance to target graph
objective = Objective(partial(tree_edit_dist, target_graph))

# Setup optimization parameters
requirements = GraphRequirements(
early_stopping_iterations=100,
timeout=timedelta(minutes=timeout),
n_jobs=-1,
)
gp_params = GPAlgorithmParameters(
genetic_scheme_type=GeneticSchemeTypesEnum.parameter_free,
max_pop_size=50,
mutation_types=[MutationTypesEnum.single_add,
MutationTypesEnum.single_drop,
MutationTypesEnum.single_change],
crossover_types=[CrossoverTypesEnum.subtree]
)
graph_gen_params = GraphGenerationParams(
adapter=BaseNetworkxAdapter(), # Example works with NetworkX graphs
rules_for_constraint=DEFAULT_DAG_RULES, # We don't want cycles in the graph
available_node_types=node_types # Node types that can appear in graphs
)
all_parameters = (requirements, graph_gen_params, gp_params)

# Build and run the optimizer
optimiser = EvoGraphOptimizer(objective, initial_graphs, *all_parameters)
found_graphs = optimiser.optimise(objective)
if visualize:
# Restore the NetworkX graph back from internal Graph representation
found_graph = graph_gen_params.adapter.restore(found_graphs[0])
draw_graphs_subplots(target_graph, found_graph, titles=['Target Graph', 'Found Graph'])
optimiser.history.show.fitness_line()
return found_graphs


if __name__ == '__main__':
"""
In this example Optimizer is expected to find the target graph
using Tree Edit Distance metric and a random tree (nx.random_tree) as target.
The convergence can be seen from achieved metrics and visually from graph plots.
"""
run_graph_search(visualize=True)
7 changes: 2 additions & 5 deletions examples/synthetic_graph_evolution/tree_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ def tree_search_setup(target_graph: nx.DiGraph,
mutation_types=[
MutationTypesEnum.single_add,
MutationTypesEnum.single_drop,
MutationTypesEnum.single_edge,
],
crossover_types=[CrossoverTypesEnum.subtree]
)
Expand All @@ -63,10 +62,8 @@ def tree_search_setup(target_graph: nx.DiGraph,
)

# Generate simple initial population with small tree graphs
initial_graphs = [generate_labeled_graph('tree', 5)
for k in range(gp_params.pop_size)]
initial_graphs = graph_gen_params.adapter.adapt(initial_graphs)

initial_graphs = [generate_labeled_graph('tree', 5, node_types)
for _ in range(gp_params.pop_size)]
# Build the optimizer
optimiser = optimizer_cls(objective, initial_graphs, requirements, graph_gen_params, gp_params)
return optimiser, objective
Expand Down
31 changes: 15 additions & 16 deletions golem/core/adapter/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __init__(self, base_graph_class: Type[DomainStructureType] = Graph):
def restore_func(self, fun: Callable) -> Callable:
"""Wraps native function so that it could accept domain graphs as arguments.
Behavior: ``restore( f(OptGraph)->OptGraph ) => f'(DomainGraph)->DomainGraph``
Behavior: ``restore( f(Graph)->Graph ) => f'(DomainGraph)->DomainGraph``
Implementation details.
The method wraps callable into a function that transforms its args & return value.
Expand All @@ -45,7 +45,7 @@ def adapt_func(self, fun: Callable) -> Callable:
as arguments. If the function was registered as native, it is returned as-is.
``AdaptRegistry`` is responsible for function registration.
Behavior: ``adapt( f(DomainGraph)->DomainGraph ) => f'(OptGraph)->OptGraph``
Behavior: ``adapt( f(DomainGraph)->DomainGraph ) => f'(Graph)->Graph``
Implementation details.
The method wraps callable into a function that transforms its args & return value.
Expand All @@ -64,15 +64,15 @@ def adapt_func(self, fun: Callable) -> Callable:
return _transform(fun, f_args=self.restore, f_ret=self.adapt)

def adapt(self, item: Union[DomainStructureType, Sequence[DomainStructureType]]) \
-> Union[OptGraph, Sequence[OptGraph]]:
-> Union[Graph, Sequence[Graph]]:
"""Maps domain graphs to internal graph representation used by optimizer.
Performs mapping only if argument has a type of domain graph.
Args:
item: a domain graph or sequence of them
Returns:
OptGraph | Sequence: mapped internal graph or sequence of them
Graph | Sequence: mapped internal graph or sequence of them
"""
if type(item) is self.domain_graph_class:
return self._adapt(item)
Expand All @@ -81,7 +81,7 @@ def adapt(self, item: Union[DomainStructureType, Sequence[DomainStructureType]])
else:
return item

def restore(self, item: Union[OptGraph, Individual, PopulationT]) \
def restore(self, item: Union[Graph, Individual, PopulationT]) \
-> Union[DomainStructureType, Sequence[DomainStructureType]]:
"""Maps graphs from internal representation to domain graphs.
Performs mapping only if argument has a type of internal representation.
Expand All @@ -90,7 +90,7 @@ def restore(self, item: Union[OptGraph, Individual, PopulationT]) \
item: an internal graph representation or sequence of them
Returns:
OptGraph | Sequence: mapped domain graph or sequence of them
Graph | Sequence: mapped domain graph or sequence of them
"""
if type(item) is self.opt_graph_class:
return self._restore(item)
Expand All @@ -102,23 +102,23 @@ def restore(self, item: Union[OptGraph, Individual, PopulationT]) \
return item

@abstractmethod
def _adapt(self, adaptee: DomainStructureType) -> OptGraph:
def _adapt(self, adaptee: DomainStructureType) -> Graph:
"""Implementation of ``adapt`` for single graph."""
raise NotImplementedError()

@abstractmethod
def _restore(self, opt_graph: OptGraph, metadata: Optional[Dict[str, Any]] = None) -> DomainStructureType:
def _restore(self, opt_graph: Graph, metadata: Optional[Dict[str, Any]] = None) -> DomainStructureType:
"""Implementation of ``restore`` for single graph."""
raise NotImplementedError()


class IdentityAdapter(BaseOptimizationAdapter[DomainStructureType]):
"""Identity adapter that performs no transformation, returning same graphs."""

def _adapt(self, adaptee: DomainStructureType) -> OptGraph:
def _adapt(self, adaptee: DomainStructureType) -> Graph:
return adaptee

def _restore(self, opt_graph: OptGraph, metadata: Optional[Dict[str, Any]] = None) -> DomainStructureType:
def _restore(self, opt_graph: Graph, metadata: Optional[Dict[str, Any]] = None) -> DomainStructureType:
return opt_graph


Expand All @@ -129,21 +129,20 @@ def __init__(self,
base_graph_class: Type[DomainStructureType] = OptGraph,
base_node_class: Type = OptNode):
super().__init__(base_graph_class)
self._base_node_class = base_node_class
self.domain_node_class = base_node_class

def _adapt(self, adaptee: DomainStructureType) -> OptGraph:
def _adapt(self, adaptee: DomainStructureType) -> Graph:
opt_graph = deepcopy(adaptee)
opt_graph.__class__ = OptGraph

opt_graph.__class__ = self.opt_graph_class
for node in opt_graph.nodes:
node.__class__ = OptNode
return opt_graph

def _restore(self, opt_graph: OptGraph, metadata: Optional[Dict[str, Any]] = None) -> DomainStructureType:
def _restore(self, opt_graph: Graph, metadata: Optional[Dict[str, Any]] = None) -> DomainStructureType:
obj = deepcopy(opt_graph)
obj.__class__ = self.domain_graph_class
for node in obj.nodes:
node.__class__ = self._base_node_class
node.__class__ = self.domain_node_class
return obj


Expand Down
7 changes: 3 additions & 4 deletions golem/core/optimisers/genetic/gp_optimizer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from copy import deepcopy
from random import choice
from typing import Sequence, Callable
from typing import Sequence, Callable, Union, Any

from golem.core.constants import MAX_GRAPH_GEN_ATTEMPTS
from golem.core.dag.graph import Graph
Expand All @@ -16,7 +16,6 @@
from golem.core.optimisers.genetic.parameters.operators_prob import init_adaptive_operators_prob
from golem.core.optimisers.genetic.parameters.population_size import init_adaptive_pop_size, PopulationSize
from golem.core.optimisers.optimization_parameters import GraphRequirements
from golem.core.optimisers.graph import OptGraph
from golem.core.optimisers.objective.objective import Objective
from golem.core.optimisers.opt_history_objects.individual import Individual
from golem.core.optimisers.optimizer import GraphGenerationParams
Expand All @@ -33,7 +32,7 @@ class EvoGraphOptimizer(PopulationalOptimizer):

def __init__(self,
objective: Objective,
initial_graphs: Sequence[OptGraph],
initial_graphs: Sequence[Union[Graph, Any]],
requirements: GraphRequirements,
graph_generation_params: GraphGenerationParams,
graph_optimizer_params: GPAlgorithmParameters):
Expand Down Expand Up @@ -61,7 +60,7 @@ def __init__(self,
self.requirements.max_depth = self._graph_depth.initial
self.graph_optimizer_params.pop_size = self._pop_size.initial
self.initial_individuals = [Individual(graph, metadata=requirements.static_individual_metadata)
for graph in initial_graphs]
for graph in self.initial_graphs]

def _initial_population(self, evaluator: EvaluationOperator):
""" Initializes the initial population """
Expand Down
7 changes: 3 additions & 4 deletions golem/core/optimisers/genetic/gp_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Sequence, Union, Any

from golem.core.optimisers.genetic.operators.base_mutations import MutationStrengthEnum, MutationTypesEnum, \
rich_mutation_set
simple_mutation_set
from golem.core.optimisers.optimizer import AlgorithmParameters
from golem.core.optimisers.genetic.operators.crossover import CrossoverTypesEnum
from golem.core.optimisers.genetic.operators.elitism import ElitismTypesEnum
Expand Down Expand Up @@ -60,9 +60,8 @@ class GPAlgorithmParameters(AlgorithmParameters):
selection_types: Sequence[SelectionTypesEnum] = \
(SelectionTypesEnum.tournament,)
crossover_types: Sequence[Union[CrossoverTypesEnum, Any]] = \
(CrossoverTypesEnum.subtree,
CrossoverTypesEnum.one_point)
mutation_types: Sequence[Union[MutationTypesEnum, Any]] = rich_mutation_set
(CrossoverTypesEnum.one_point,)
mutation_types: Sequence[Union[MutationTypesEnum, Any]] = simple_mutation_set
elitism_type: ElitismTypesEnum = ElitismTypesEnum.keep_n_best
regularization_type: RegularizationTypesEnum = RegularizationTypesEnum.none
genetic_scheme_type: GeneticSchemeTypesEnum = GeneticSchemeTypesEnum.generational
Expand Down
8 changes: 5 additions & 3 deletions golem/core/optimisers/objective/objective.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import itertools
from dataclasses import dataclass
from numbers import Real
from typing import Any, Optional, Iterable, Callable, Sequence, TypeVar, Dict, Tuple, Union
from typing import Any, Optional, Callable, Sequence, TypeVar, Dict, Tuple, Union

from golem.core.dag.graph import Graph
from golem.core.log import default_log
from golem.core.optimisers.fitness import *
from golem.core.optimisers.fitness import Fitness, SingleObjFitness, null_fitness, MultiObjFitness

G = TypeVar('G', bound=Graph, covariant=True)
R = TypeVar('R', contravariant=True)
Expand Down Expand Up @@ -35,11 +35,13 @@ class Objective(ObjectiveInfo, ObjectiveFunction):
on Graphs and keeps information about metrics used."""

def __init__(self,
quality_metrics: Dict[Any, Callable],
quality_metrics: Union[Callable, Dict[Any, Callable]],
complexity_metrics: Optional[Dict[Any, Callable]] = None,
is_multi_objective: bool = False,
):
self._log = default_log(self)
if isinstance(quality_metrics, Callable):
quality_metrics = {'metric': quality_metrics}
self.quality_metrics = quality_metrics
self.complexity_metrics = complexity_metrics or {}
metric_names = [str(metric_id) for metric_id, _ in self.metrics]
Expand Down
Loading

0 comments on commit c7c910c

Please sign in to comment.