Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/structural_analysis/opt_graph_optimization.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import os
import random
from functools import partial
Expand All @@ -10,6 +11,7 @@
from golem.core.optimisers.graph import OptGraph, OptNode
from golem.core.optimisers.objective import Objective
from golem.core.optimisers.opt_node_factory import DefaultOptNodeFactory
from golem.core.optimisers.timer import OptimisationTimer
from golem.core.paths import project_root
from golem.metrics.graph_metrics import size_diff
from golem.structural_analysis.graph_sa.graph_structural_analysis import GraphStructuralAnalysis
Expand Down Expand Up @@ -75,7 +77,7 @@ def complexity_metric(graph: OptGraph, adapter: BaseNetworkxAdapter, metric: Cal
path_to_save=path_to_save,
is_visualize_per_iteration=False)

graph, results = sa.optimize(graph=opt_graph, n_jobs=1, max_iter=3)
graph, results = sa.optimize(graph_=opt_graph, n_jobs=1, max_iter=3, timer=timer)

# to show SA results on each iteration
optimized_graph = GraphStructuralAnalysis.visualize_on_graph(graph=get_opt_graph(), analysis_result=results,
Expand Down
27 changes: 26 additions & 1 deletion golem/structural_analysis/graph_sa/edge_sa_approaches.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from golem.core.optimisers.objective import Objective
from golem.core.optimisers.timer import OptimisationTimer
from golem.core.paths import default_data_dir
from golem.structural_analysis.base_sa_approaches import BaseAnalyzeApproach
from golem.structural_analysis.graph_sa.base_sa_approaches import BaseAnalyzeApproach
from golem.structural_analysis.graph_sa.entities.edge import Edge
from golem.structural_analysis.graph_sa.results.deletion_sa_approach_result import \
DeletionSAApproachResult
Expand Down Expand Up @@ -64,6 +64,10 @@ def analyze(self, graph: Graph, edge: Edge,

for approach in self.approaches:
if timer is not None and timer.is_time_limit_reached():
# to fill uncalculated approaches with default results
approaches_left = self.approaches[self.approaches.index(approach):]
for approach_left in approaches_left:
results.add_result(approach_left.get_default_results(len_obj=len(objective.metrics)))
break

results.add_result(approach(graph=graph,
Expand All @@ -73,6 +77,15 @@ def analyze(self, graph: Graph, edge: Edge,

return results

@staticmethod
def _get_default_results(approach: Type['EdgeAnalyzeApproach']):
if isinstance(approach, EdgeDeletionAnalyze):
res_approach = DeletionSAApproachResult()
else:
res_approach = ReplaceSAApproachResult()
res_approach.add_results([-2])
return res_approach


class EdgeAnalyzeApproach(BaseAnalyzeApproach, ABC):
"""
Expand Down Expand Up @@ -152,6 +165,12 @@ def sample(self, edge: Edge) -> Optional[Graph]:

return graph_sample

@staticmethod
def get_default_results(len_obj: int):
res = DeletionSAApproachResult()
res.add_results(metrics_values=len_obj*[-2.0])
return res


class EdgeReplaceOperationAnalyze(EdgeAnalyzeApproach):
"""
Expand Down Expand Up @@ -316,3 +335,9 @@ def _edge_generation(self, edge: Edge, number_of_operations: int = 1) -> List[Di

edges_for_replacement = random.sample(available_edges_idx, min(number_of_operations, len(available_edges_idx)))
return edges_for_replacement

@staticmethod
def get_default_results(len_obj: int):
res = ReplaceSAApproachResult()
res.add_results(entity_to_replace_to='none', metrics_values=len_obj*[-2.0])
return res
106 changes: 59 additions & 47 deletions golem/structural_analysis/graph_sa/graph_structural_analysis.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import os
import pandas as pd
from copy import deepcopy
from typing import List, Optional, Tuple
from typing import List, Optional, Tuple, Dict
import multiprocessing

from golem.core.log import default_log
from golem.core.dag.graph import Graph, GraphNode
from golem.core.optimisers.objective import Objective
from golem.core.optimisers.objective import ObjectiveFunction
from golem.core.optimisers.opt_node_factory import OptNodeFactory
from golem.core.optimisers.timer import OptimisationTimer
from golem.structural_analysis.graph_sa.edge_sa_approaches import EdgeAnalyzeApproach, EdgeDeletionAnalyze, \
Expand Down Expand Up @@ -33,17 +34,17 @@ class GraphStructuralAnalysis:
:param approaches: methods applied to graph. Default: None
:param requirements: extra requirements to define specific details for different approaches.\
See StructuralAnalysisRequirements class documentation.
:param path_to_save: path to save results to. Default: ~home/Fedot/structural/
:param path_to_save: path to save results to. Default: ~home/Golem/structural/
Default: False
"""

def __init__(self, objective: Objective,
def __init__(self, objective: ObjectiveFunction,
node_factory: OptNodeFactory,
is_preproc: bool = True,
approaches: List = None,
requirements: StructuralAnalysisRequirements = StructuralAnalysisRequirements(),
path_to_save: str = None,
is_visualize_per_iteration: bool = False):
is_visualize_per_iteration: bool = True):

self.is_preproc = is_preproc
self._log = default_log(self)
Expand Down Expand Up @@ -113,12 +114,12 @@ def analyze(self, graph: Graph,

return result

def optimize(self, graph: Graph,
n_jobs: int = 1, timer: OptimisationTimer = None,
def optimize(self, graph_: Graph,
n_jobs: int = 1, timer: OptimisationTimer = (),
max_iter: int = 10) -> Tuple[Graph, SAAnalysisResults]:
""" Optimizes graph by applying 'analyze' method and deleting/replacing parts
of graph iteratively
:param graph: graph object to analyze.
:param graph_: graph object to analyze.
:param n_jobs: num of ``n_jobs`` for parallelization (``-1`` for use all cpu's).
Tip: if specified graph isn't huge (as NN, for example) than set n_jobs to default value.
:param timer: timer with timeout left for optimization.
Expand All @@ -130,49 +131,60 @@ def optimize(self, graph: Graph,

# what actions were applied on the graph and how many
actions_applied = dict.fromkeys(approaches_names, 0)

graph = deepcopy(graph_)
result = SAAnalysisResults()

analysis_result = self.analyze(graph=graph, result=result, timer=timer, n_jobs=n_jobs)
converged = False
iter = 0

if analysis_result.is_empty:
self._log.message(f'{iter} actions were taken during SA')
return graph, analysis_result

while not converged:
iter += 1
worst_result = analysis_result.get_info_about_worst_result(
metric_idx_to_optimize_by=self.main_metric_idx)
if self.is_visualize_per_iteration:
self.visualize_on_graph(graph=deepcopy(graph), analysis_result=analysis_result,
metric_idx_to_optimize_by=self.main_metric_idx,
mode='final',
font_size_scale=0.6)
if worst_result['value'] > 1:
# apply the worst approach
postproc_method = approaches_repo.postproc_method_by_name(worst_result['approach_name'])
graph = postproc_method(graph=graph, worst_result=worst_result)
actions_applied[f'{worst_result["approach_name"]}'] += 1

if timer is not None and timer.is_time_limit_reached():
break

if max_iter and iter >= max_iter:
break

analysis_result = self.analyze(graph=graph,
result=result,
n_jobs=n_jobs,
timer=timer)
else:
converged = True
with timer:
analysis_result = self.analyze(graph=graph, result=result, timer=timer, n_jobs=n_jobs)
converged = False
iter = 0

if analysis_result.is_empty:
self._log.message(f'{iter} actions were taken during SA')
return graph, analysis_result

while not converged:
iter += 1
worst_result = analysis_result.get_info_about_worst_result(
metric_idx_to_optimize_by=self.main_metric_idx)
if self.is_visualize_per_iteration:
base_name = os.path.basename(os.path.normpath(self.path_to_save))
if '.png' not in base_name:
i = 0
while f'{base_name}_{i}.png' in os.listdir(self.path_to_save):
i += 1
path_to_save = os.path.join(self.path_to_save, f'{base_name}_{i}.png')
self.visualize_on_graph(graph=deepcopy(graph), analysis_result=analysis_result,
metric_idx_to_optimize_by=self.main_metric_idx,
mode='final',
font_size_scale=0.6,
save_path=path_to_save)
if worst_result['value'] > 1:
# apply the worst approach
postproc_method = approaches_repo.postproc_method_by_name(worst_result['approach_name'])
graph = postproc_method(graph=graph, worst_result=worst_result)
actions_applied[f'{worst_result["approach_name"]}'] += 1

if timer is not None and timer.is_time_limit_reached():
break

if max_iter and iter >= max_iter:
break

analysis_result = self.analyze(graph=graph,
result=result,
n_jobs=n_jobs,
timer=timer)
else:
converged = True

self._log.message(f'{iter} iterations passed during SA')
self._log.message(f'The following actions were applied during SA: {actions_applied}')

if self.path_to_save:
# to save actions applied
for key in actions_applied.keys():
actions_applied[key] = [actions_applied[key]]
pd.DataFrame(actions_applied).to_csv(os.path.join(self.path_to_save, 'actions_applied.csv'))
if not os.path.exists(self.path_to_save):
os.makedirs(self.path_to_save)
analysis_result.save(path=self.path_to_save)
Expand Down Expand Up @@ -226,7 +238,7 @@ def visualize_on_graph(graph: Graph, analysis_result: SAAnalysisResults,
"""

def get_nodes_and_edges_labels(analysis_result: SAAnalysisResults, iter: int) \
-> tuple[dict[int, str], dict[int, str]]:
-> Tuple[Dict[int, str], Dict[int, str]]:
""" Get nodes and edges labels in dictionary form. """

def get_str_labels(result: ObjectSAResult) -> str:
Expand All @@ -239,7 +251,7 @@ def get_str_labels(result: ObjectSAResult) -> str:
short_approach_name = 'D'
else:
short_approach_name = 'R'
cur_label += f'{short_approach_name}: {approach.get_rounded_metrics(idx=2)}\n'
cur_label += f'{short_approach_name}: {approach.get_rounded_metrics(idx=4)}\n'
return cur_label

nodes_labels = {}
Expand Down
31 changes: 29 additions & 2 deletions golem/structural_analysis/graph_sa/node_sa_approaches.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from golem.core.optimisers.opt_node_factory import OptNodeFactory
from golem.core.optimisers.timer import OptimisationTimer
from golem.core.paths import default_data_dir
from golem.structural_analysis.base_sa_approaches import BaseAnalyzeApproach
from golem.structural_analysis.graph_sa.base_sa_approaches import BaseAnalyzeApproach
from golem.structural_analysis.graph_sa.results.deletion_sa_approach_result import \
DeletionSAApproachResult
from golem.structural_analysis.graph_sa.results.object_sa_result import ObjectSAResult
Expand All @@ -24,9 +24,14 @@

class NodeAnalysis:
"""
Class designated for Structural Analysis on one node level.

:param node_factory: node factory to advise changes from available operations and models
:param approaches: methods applied to nodes to modify the graph or analyze certain operations.\
Default: [NodeDeletionAnalyze, NodeTuneAnalyze, NodeReplaceOperationAnalyze]
:param path_to_save: path to save results to. Default: ~home/Fedot/structural
:param approaches_requirements: extra requirements to define specific details for different approaches.\
See StructuralAnalysisRequirements class documentation.
:param path_to_save: path to save results to. Default: ~home/Golem/structural
"""

def __init__(self, node_factory: Any,
Expand Down Expand Up @@ -64,6 +69,10 @@ def analyze(self, graph: Graph, node: GraphNode,

for approach in self.approaches:
if timer is not None and timer.is_time_limit_reached():
# to fill uncalculated approaches with default results
approaches_left = self.approaches[self.approaches.index(approach):]
for approach_left in approaches_left:
results.add_result(approach_left.get_default_results(len_obj=len(objective.metrics)))
break

results.add_result(approach(graph=graph,
Expand Down Expand Up @@ -156,6 +165,12 @@ def sample(self, node: GraphNode):

return graph_sample

@staticmethod
def get_default_results(len_obj: int):
res = DeletionSAApproachResult()
res.add_results(metrics_values=len_obj*[-2.0])
return res


class NodeReplaceOperationAnalyze(NodeAnalyzeApproach):
"""
Expand Down Expand Up @@ -259,6 +274,12 @@ def _node_generation(node: GraphNode,

return random_nodes

@staticmethod
def get_default_results(len_obj: int):
res = ReplaceSAApproachResult()
res.add_results(entity_to_replace_to='none', metrics_values=len_obj*[-2.0])
return res


class SubtreeDeletionAnalyze(NodeAnalyzeApproach):
"""
Expand Down Expand Up @@ -324,3 +345,9 @@ def sample(self, node: GraphNode):
return None

return graph_sample

@staticmethod
def get_default_results(len_obj: int):
res = DeletionSAApproachResult()
res.add_results(metrics_values=len_obj*[-2.0])
return res
9 changes: 4 additions & 5 deletions golem/structural_analysis/graph_sa/nodes_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@

class NodesAnalysis:
"""
This class is for nodes structural analysis within a Graph .
It takes nodes and approaches to be applied to chosen nodes.
To define which nodes to analyze pass them to nodes_to_analyze filed
or all nodes will be analyzed.
This class is for nodes structural analysis within a Graph. It takes nodes and
approaches to be applied to chosen nodes.
To define which nodes to analyze pass them to nodes_to_analyze filed or all nodes will be analyzed.

:param objective: objective functions for computing metric values
:param node_factory: node factory to advise changes from available operations and models
:param approaches: methods applied to nodes to modify the graph or analyze certain operations.\
Default: [NodeDeletionAnalyze, NodeReplaceOperationAnalyze]
:param path_to_save: path to save results to. Default: ~home/Fedot/structural
:param path_to_save: path to save results to. Default: ~home/Golem/structural
"""

def __init__(self, objective: Objective,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def get_dict_results(self) -> List[float]:
""" Returns all calculated results. """
return self.metrics

def get_rounded_metrics(self, idx: int = 2) -> list:
def get_rounded_metrics(self, idx: int = 4) -> list:
return [round(metric, idx) for metric in self.metrics]

def __str__(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def get_dict_results(self) -> Dict[int, List[float]]:
""" Returns dict representation of results. """
return self.metrics

def get_rounded_metrics(self, idx: int = 2) -> dict:
def get_rounded_metrics(self, idx: int = 4) -> dict:
rounded = {}
for metric in self.metrics:
rounded[metric] = [round(metric, idx) for metric in self.metrics[metric]]
Expand Down
Loading