Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tuning history to OptHistory #228

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
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: 2 additions & 2 deletions examples/molecule_search/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def visualize_results(molecules: Iterable[MolGraph],

# Plot pareto front (if multi-objective)
if objective.is_multi_objective:
visualise_pareto(history.archive_history[-1],
visualise_pareto(history.evolution_best_archive[-1],
objectives_names=objective.metric_names[:2],
folder=str(save_path))

Expand Down Expand Up @@ -193,7 +193,7 @@ def run_experiment(optimizer_setup: Callable,
result_dir = Path('results') / exp_name
result_dir.mkdir(parents=True, exist_ok=True)
history.save(result_dir / f'history_trial_{trial}.json')
trial_results.extend(history.final_choices)
trial_results.extend(history.evolution_results)
trial_histories.append(history)

# Compute mean & std for metrics of trials
Expand Down
2 changes: 1 addition & 1 deletion examples/synthetic_graph_evolution/experiment_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def run_experiments(optimizer_setup: Callable,
found_graph = found_graphs[0] if isinstance(found_graphs, Sequence) else found_graphs
history = optimizer.history

trial_results.extend(history.final_choices)
trial_results.extend(history.evolution_results)
found_nx_graph = BaseNetworkxAdapter().restore(found_graph)

duration = datetime.now() - start_time
Expand Down
10 changes: 6 additions & 4 deletions experiments/experiment_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def _analyze_convergence(history: OptHistory, metrics_num: int) -> List[float]:
# find best final metric for each objective
best_fitness_per_objective = [np.inf] * metrics_num
for i in range(metrics_num):
for ind in history.final_choices.data:
for ind in history.evolution_results.data:
if ind.fitness.values[i] < best_fitness_per_objective[i]:
best_fitness_per_objective[i] = ind.fitness.values[i]

Expand Down Expand Up @@ -231,7 +231,8 @@ def plot_convergence(self, path_to_save: str, with_confidence: bool = True,

def analyze_statistical_significance(self, data_to_analyze: Dict[str, Dict[str, List[float]]],
stat_tests: List[Callable], path_to_save: Optional[str] = None,
test_format: Optional[List[str]] = None) -> Dict[str, Dict[str, Dict[str, float]]]:
test_format: Optional[List[str]] = None
) -> Dict[str, Dict[str, Dict[str, float]]]:
""" Method to perform statistical analysis of data. Metric data obtained with 'analyze_metrics' and
convergence data obtained with 'analyze_convergence' can be simply analyzed, for example.
:param data_to_analyze: data to analyze.
Expand Down Expand Up @@ -291,7 +292,8 @@ def analyze_structural_complexity(self, path_to_save: str, dir_name: str, class_
os.makedirs(path_to_save, exist_ok=True)

for setup, dataset, path_to_launch in self._get_path_to_launch():
if not self._check_if_file_or_folder_present(path=os.path.join(path_to_launch, dir_name), is_raise=is_raise):
if not self._check_if_file_or_folder_present(path=os.path.join(path_to_launch, dir_name),
is_raise=is_raise):
continue

path_to_json = None
Expand Down Expand Up @@ -324,7 +326,7 @@ def analyze_structural_complexity(self, path_to_save: str, dir_name: str, class_
saved_results = [int(cur_name.split("_")[0]) for cur_name in os.listdir(cur_path_to_save)
if cur_name not in self._folders_to_ignore]
max_saved_num = max(saved_results) if saved_results else 0
cur_path_to_save = os.path.join(cur_path_to_save, f'{max_saved_num+1}_result.png')
cur_path_to_save = os.path.join(cur_path_to_save, f'{max_saved_num + 1}_result.png')
result.show(cur_path_to_save, title=title)
self._log.info(f"Resulting graph was saved to {cur_path_to_save}")

Expand Down
2 changes: 1 addition & 1 deletion experiments/experiment_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def launch(self, optimizer_setup: Callable, **kwargs):
found_graph = found_graphs[0] if isinstance(found_graphs, Sequence) else found_graphs
history = optimizer.history

trial_results.extend(history.final_choices)
trial_results.extend(history.evolution_results)
found_nx_graph = BaseNetworkxAdapter().restore(found_graph)

duration = datetime.now() - start_time
Expand Down
2 changes: 1 addition & 1 deletion golem/core/optimisers/adaptive/experience_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def unroll_trajectories(history: OptHistory) -> List[GraphTrajectory]:
"""Iterates through history and find continuous sequences of applied operator actions."""
trajectories = []
seen_uids = set()
for terminal_individual in history.final_choices:
for terminal_individual in history.evolution_results:
trajectory = []
next_ind = terminal_individual
while True:
Expand Down
11 changes: 6 additions & 5 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, Union, Any
from typing import Any, Sequence, Union

from golem.core.constants import MAX_GRAPH_GEN_ATTEMPTS
from golem.core.dag.graph import Graph
Expand All @@ -9,15 +9,16 @@
from golem.core.optimisers.genetic.operators.elitism import Elitism
from golem.core.optimisers.genetic.operators.inheritance import Inheritance
from golem.core.optimisers.genetic.operators.mutation import Mutation
from golem.core.optimisers.genetic.operators.operator import PopulationT, EvaluationOperator
from golem.core.optimisers.genetic.operators.operator import EvaluationOperator, PopulationT
from golem.core.optimisers.genetic.operators.regularization import Regularization
from golem.core.optimisers.genetic.operators.reproduction import ReproductionController
from golem.core.optimisers.genetic.operators.selection import Selection
from golem.core.optimisers.genetic.parameters.graph_depth import AdaptiveGraphDepth
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.genetic.parameters.population_size import PopulationSize, init_adaptive_pop_size
from golem.core.optimisers.objective.objective import Objective
from golem.core.optimisers.opt_history_objects.individual import Individual
from golem.core.optimisers.opt_history_objects.opt_history import OptHistoryLabels
from golem.core.optimisers.optimization_parameters import GraphRequirements
from golem.core.optimisers.optimizer import GraphGenerationParams
from golem.core.optimisers.populational_optimizer import PopulationalOptimizer
Expand Down Expand Up @@ -64,13 +65,13 @@ def __init__(self,
def _initial_population(self, evaluator: EvaluationOperator):
""" Initializes the initial population """
# Adding of initial assumptions to history as zero generation
self._update_population(evaluator(self.initial_individuals), 'initial_assumptions')
self._update_population(evaluator(self.initial_individuals), OptHistoryLabels.initial_assumptions)
pop_size = self.graph_optimizer_params.pop_size

if len(self.initial_individuals) < pop_size:
self.initial_individuals = self._extend_population(self.initial_individuals, pop_size)
# Adding of extended population to history
self._update_population(evaluator(self.initial_individuals), 'extended_initial_assumptions')
self._update_population(evaluator(self.initial_individuals), OptHistoryLabels.extended_initial_assumptions)

def _extend_population(self, pop: PopulationT, target_pop_size: int) -> PopulationT:
verifier = self.graph_generation_params.verifier
Expand Down
4 changes: 3 additions & 1 deletion golem/core/optimisers/meta/surrogate_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from golem.core.optimisers.meta.surrogate_evaluator import SurrogateDispatcher
from golem.core.optimisers.meta.surrogate_model import RandomValuesSurrogateModel
from golem.core.optimisers.objective import Objective, ObjectiveFunction
from golem.core.optimisers.opt_history_objects.opt_history import OptHistoryLabels
from golem.core.optimisers.optimization_parameters import GraphRequirements
from golem.core.optimisers.optimizer import GraphGenerationParams
from golem.core.optimisers.populational_optimizer import EvaluationAttemptsError, _try_unfit_graph
Expand All @@ -17,6 +18,7 @@ class SurrogateEachNgenOptimizer(EvoGraphOptimizer):

Additionally, we need to pass surrogate_model object
"""

def __init__(self,
objective: Objective,
initial_graphs: Sequence[OptGraph],
Expand Down Expand Up @@ -54,5 +56,5 @@ def optimise(self, objective: ObjectiveFunction) -> Sequence[OptGraph]:
break
# Adding of new population to history
self._update_population(new_population)
self._update_population(self.best_individuals, 'final_choices')
self._update_population(self.best_individuals, OptHistoryLabels.evolution_results)
return [ind.graph for ind in self.best_individuals]
56 changes: 33 additions & 23 deletions golem/core/optimisers/opt_history_objects/opt_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,29 @@
import itertools
import os
import shutil
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Union, TYPE_CHECKING
from typing import Any, Dict, List, Optional, Sequence, TYPE_CHECKING, Union

from golem.core.log import default_log
from golem.core.optimisers.objective.objective import ObjectiveInfo
from golem.core.optimisers.opt_history_objects.generation import Generation

from golem.core.paths import default_data_dir
from golem.serializers.serializer import default_load, default_save
from golem.visualisation.opt_viz import OptHistoryVisualizer

if TYPE_CHECKING:
from golem.core.dag.graph import Graph
from golem.core.optimisers.opt_history_objects.individual import Individual


class OptHistoryLabels(str, Enum):
initial_assumptions = 'initial_assumptions'
extended_initial_assumptions = 'extended_initial_assumptions'
evolution_results = 'evolution_results'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Аналогично, можно про эволюцию явно не писать.

tuning_start = 'tuning_start'
tuning_results = 'tuning_results'


class OptHistory:
"""
Contains optimization history, saves history to csv.
Expand All @@ -36,8 +43,7 @@ def __init__(self,
default_save_dir: Optional[os.PathLike] = None):
self._objective = objective or ObjectiveInfo()
self._generations: List[Generation] = []
self.archive_history: List[List[Individual]] = []
self._tuning_result: Optional[Graph] = None
self.evolution_best_archive: List[List[Individual]] = []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я не совсем понимаю зачем нужно такое переименование?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я пока не реализовал ведение archive_history для тюнинга, потому что для оптимизации его ведёт фронт Парето в составе оптимизатора. Поэтому название поля явно отражает, что оно относится к истории эволюции, но не тюнинга.

Честно, не знаю, как тут лучше поступить.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ну точно не нужна отсылка к эволюции, для большей абстрактности.
Мне кажется идея что archive_history это основная история, а для файн-тюнинга - отдельная, будет доступной пользователю.


# init default save directory
if default_save_dir:
Expand All @@ -61,8 +67,8 @@ def add_to_history(self, individuals: Sequence[Individual], generation_label: Op
generation = Generation(individuals, self.generations_count, generation_label, generation_metadata)
self.generations.append(generation)

def add_to_archive_history(self, individuals: Sequence[Individual]):
self.archive_history.append(list(individuals))
def add_to_evolution_best_archive(self, individuals: Sequence[Individual]):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Может сделать единый метод, просто передавать в параметрах нужную метку?

self.evolution_best_archive.append(list(individuals))

def to_csv(self, save_dir: Optional[os.PathLike] = None, file: os.PathLike = 'history.csv'):
save_dir = save_dir or self._default_save_dir
Expand Down Expand Up @@ -215,31 +221,36 @@ def initial_assumptions(self) -> Optional[Generation]:
if not self.generations:
return None
for gen in self.generations:
if gen.label == 'initial_assumptions':
if gen.label == OptHistoryLabels.initial_assumptions:
return gen

@property
def final_choices(self) -> Optional[Generation]:
def generations_count(self) -> int:
return len(self.generations)

@property
def evolution_results(self) -> Optional[Generation]:
if not self.generations:
return None
for gen in reversed(self.generations):
if gen.label == 'final_choices':
if gen.label == OptHistoryLabels.evolution_results:
return gen

@property
def generations_count(self) -> int:
return len(self.generations)
def tuning_start(self) -> Optional[Generation]:
if not self.generations:
return None
for gen in reversed(self.generations):
if gen.label == OptHistoryLabels.tuning_start:
return gen

@property
def tuning_result(self):
if hasattr(self, '_tuning_result'):
return self._tuning_result
else:
def tuning_result(self) -> Optional[Generation]:
if not self.generations:
return None

@tuning_result.setter
def tuning_result(self, val):
self._tuning_result = val
for gen in reversed(self.generations):
if gen.label == OptHistoryLabels.tuning_results:
return gen

@property
def generations(self):
Expand Down Expand Up @@ -270,8 +281,7 @@ def lighten_history(history: OptHistory) -> OptHistory:
without excessive memory usage. """
light_history = OptHistory()
light_history._generations = \
[Generation(iterable=gen, generation_num=i) for i, gen in enumerate(history.archive_history)]
light_history.archive_history = history.archive_history
[Generation(iterable=gen, generation_num=i) for i, gen in enumerate(history.evolution_best_archive)]
light_history.evolution_best_archive = history.evolution_best_archive
light_history._objective = history.objective
light_history._tuning_result = history.tuning_result
return light_history
11 changes: 6 additions & 5 deletions golem/core/optimisers/populational_optimizer.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from abc import abstractmethod
from random import choice
from typing import Any, Optional, Sequence, Dict
from typing import Any, Dict, Optional, Sequence

from golem.core.constants import MIN_POP_SIZE
from golem.core.dag.graph import Graph
from golem.core.optimisers.archive import GenerationKeeper
from golem.core.optimisers.genetic.evaluation import MultiprocessingDispatcher, SequentialDispatcher
from golem.core.optimisers.genetic.operators.operator import PopulationT, EvaluationOperator
from golem.core.optimisers.genetic.operators.operator import EvaluationOperator, PopulationT
from golem.core.optimisers.objective import GraphFunction, ObjectiveFunction
from golem.core.optimisers.objective.objective import Objective
from golem.core.optimisers.opt_history_objects.individual import Individual
from golem.core.optimisers.opt_history_objects.opt_history import OptHistoryLabels
from golem.core.optimisers.optimization_parameters import GraphRequirements
from golem.core.optimisers.optimizer import GraphGenerationParams, GraphOptimizer, AlgorithmParameters
from golem.core.optimisers.optimizer import AlgorithmParameters, GraphGenerationParams, GraphOptimizer
from golem.core.optimisers.timer import OptimisationTimer
from golem.core.utilities.grouped_condition import GroupedCondition

Expand Down Expand Up @@ -105,7 +106,7 @@ def optimise(self, objective: ObjectiveFunction) -> Sequence[Graph]:
# Adding of new population to history
self._update_population(new_population)
pbar.close()
self._update_population(self.best_individuals, 'final_choices')
self._update_population(self.best_individuals, OptHistoryLabels.evolution_results)
return [ind.graph for ind in self.best_individuals]

@property
Expand Down Expand Up @@ -146,7 +147,7 @@ def _update_population(self, next_population: PopulationT, label: Optional[str]
def _log_to_history(self, population: PopulationT, label: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None):
self.history.add_to_history(population, label, metadata)
self.history.add_to_archive_history(self.generations.best_individuals)
self.history.add_to_evolution_best_archive(self.generations.best_individuals)
if self.requirements.history_dir:
self.history.save_current_results(self.requirements.history_dir)

Expand Down
7 changes: 4 additions & 3 deletions golem/core/optimisers/random/random_mutation_optimizer.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from typing import Union, Optional, Sequence
from typing import Optional, Sequence, Union

from golem.core.dag.graph import Graph
from golem.core.optimisers.genetic.gp_params import GPAlgorithmParameters
from golem.core.optimisers.genetic.operators.mutation import Mutation
from golem.core.optimisers.genetic.operators.operator import EvaluationOperator, PopulationT
from golem.core.optimisers.objective import Objective
from golem.core.optimisers.opt_history_objects.individual import Individual
from golem.core.optimisers.opt_history_objects.opt_history import OptHistoryLabels
from golem.core.optimisers.optimization_parameters import GraphRequirements
from golem.core.optimisers.optimizer import GraphGenerationParams
from golem.core.optimisers.populational_optimizer import PopulationalOptimizer
Expand Down Expand Up @@ -38,13 +39,13 @@ def _evolve_population(self, evaluator: EvaluationOperator) -> PopulationT:
return new_population

def _initial_population(self, evaluator: EvaluationOperator):
self._update_population(evaluator(self.initial_individuals), 'initial_assumptions')
self._update_population(evaluator(self.initial_individuals), OptHistoryLabels.initial_assumptions)
pop_size = self.graph_optimizer_params.pop_size

if len(self.initial_individuals) < pop_size:
self.initial_individuals = self._extend_population(self.initial_individuals, pop_size)
# Adding of extended population to history
self._update_population(evaluator(self.initial_individuals), 'extended_initial_assumptions')
self._update_population(evaluator(self.initial_individuals), OptHistoryLabels.extended_initial_assumptions)


class RandomMutationOptimizer(RandomSearchOptimizer):
Expand Down
Loading