Skip to content

Commit

Permalink
wip, start work with mutations are adapted for atomized models
Browse files Browse the repository at this point in the history
  • Loading branch information
kasyanovse committed Dec 15, 2023
1 parent 5d60648 commit 7c2e51a
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from fedot.core.repository.tasks import TaskTypesEnum, TsForecastingParams, Task


class AtomizedTimeSeriesDataSample(AtomizedModel):
class AtomizedTimeSeriesSampler(AtomizedModel):
""" Increase data for fitting for short time series """

def __init__(self, pipeline: Optional['Pipeline'] = None, mode='sparse'):
Expand Down
Empty file.
141 changes: 141 additions & 0 deletions fedot/core/optimisers/genetic_operators/mutation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from functools import partial
from random import choice, randint, random, sample
from typing import TYPE_CHECKING, Optional

from golem.core.adapter import register_native
from golem.core.dag.graph import ReconnectType
from golem.core.dag.graph_node import GraphNode
from golem.core.dag.graph_utils import distance_to_root_level, distance_to_primary_level, graph_has_cycle
from golem.core.optimisers.advisor import RemoveType
from golem.core.optimisers.genetic.operators.base_mutations import single_edge_mutation as golem_single_edge_mutation, \
add_as_child, add_separate_parent_node, add_intermediate_node
from golem.core.optimisers.graph import OptGraph, OptNode
from golem.core.optimisers.opt_node_factory import OptNodeFactory
from golem.core.optimisers.optimization_parameters import GraphRequirements
from golem.core.optimisers.optimizer import GraphGenerationParams, AlgorithmParameters
from golem.utilities.data_structures import ComparableEnum as Enum

if TYPE_CHECKING:
from golem.core.optimisers.genetic.gp_params import GPAlgorithmParameters

# @register_native
def fedot_single_edge_mutation(graph: OptGraph,
requirements: GraphRequirements,
graph_gen_params: GraphGenerationParams,
parameters: 'GPAlgorithmParameters'
) -> OptGraph:
"""
This mutation adds new edge between two random nodes in graph.
:param graph: graph to mutate
"""

def nodes_not_cycling(source_node: OptNode, target_node: OptNode):
parents = source_node.nodes_from
while parents:
if target_node not in parents:
grandparents = []
for parent in parents:
grandparents.extend(parent.nodes_from)
parents = grandparents
else:
return False
return True

for _ in range(parameters.max_num_of_operator_attempts):
if len(graph.nodes) < 2 or graph.depth > requirements.max_depth:
return graph

source_node, target_node = sample(graph.nodes, 2)
if source_node not in target_node.nodes_from:
if graph_has_cycle(graph):
graph.connect_nodes(source_node, target_node)
break
else:
if nodes_not_cycling(source_node, target_node):
graph.connect_nodes(source_node, target_node)
break
return graph


# @register_native
# def single_add_mutation(graph: OptGraph,
# requirements: GraphRequirements,
# graph_gen_params: GraphGenerationParams,
# parameters: AlgorithmParameters
# ) -> OptGraph:
# """
# Add new node between two sequential existing modes
#
# :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)
#
# result = strategy(graph, node_to_mutate, graph_gen_params.node_factory)
# return result
#
#
# @register_native
# def single_change_mutation(graph: OptGraph,
# requirements: GraphRequirements,
# graph_gen_params: GraphGenerationParams,
# parameters: AlgorithmParameters
# ) -> OptGraph:
# """
# Change node between two sequential existing modes.
#
# :param graph: graph to mutate
# """
# node = choice(graph.nodes)
# new_node = graph_gen_params.node_factory.exchange_node(node)
# if not new_node:
# return graph
# graph.update_node(node, new_node)
# return graph
#
#
# @register_native
# def single_drop_mutation(graph: OptGraph,
# requirements: GraphRequirements,
# graph_gen_params: GraphGenerationParams,
# parameters: AlgorithmParameters
# ) -> OptGraph:
# """
# Drop single node from graph.
#
# :param graph: graph to mutate
# """
# if len(graph.nodes) < 2:
# return graph
# node_to_del = choice(graph.nodes)
# node_name = node_to_del.name
# removal_type = graph_gen_params.advisor.can_be_removed(node_to_del)
# if removal_type == RemoveType.with_direct_children:
# # TODO refactor workaround with data_source
# graph.delete_node(node_to_del)
# nodes_to_delete = \
# [n for n in graph.nodes
# if n.descriptive_id.count('data_source') == 1 and node_name in n.descriptive_id]
# for child_node in nodes_to_delete:
# graph.delete_node(child_node, reconnect=ReconnectType.all)
# elif removal_type == RemoveType.with_parents:
# graph.delete_subtree(node_to_del)
# elif removal_type == RemoveType.node_rewire:
# graph.delete_node(node_to_del, reconnect=ReconnectType.all)
# elif removal_type == RemoveType.node_only:
# graph.delete_node(node_to_del, reconnect=ReconnectType.none)
# elif removal_type == RemoveType.forbidden:
# pass
# else:
# raise ValueError("Unknown advice (RemoveType) returned by Advisor ")
# return graph
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
import json
import os
from functools import reduce

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pytest
from sklearn.metrics import mean_squared_error
from typing import Type

from fedot.core.composer.metrics import RMSE
from fedot.core.data.data import InputData
from fedot.core.data.data_split import train_test_data_setup
from fedot.core.operations.atomized_model.atomized_model import AtomizedModel
from fedot.core.operations.atomized_model.atomized_ts_differ import AtomizedTimeSeriesDiffer
from fedot.core.operations.atomized_model.atomized_ts_sampler import AtomizedTimeSeriesDataSample
from fedot.core.operations.atomized_model.atomized_ts_sampler import AtomizedTimeSeriesSampler
from fedot.core.operations.atomized_model.atomized_ts_scaler import AtomizedTimeSeriesScaler
from fedot.core.pipelines.node import PipelineNode
from fedot.core.pipelines.pipeline import Pipeline
from fedot.core.repository.dataset_types import DataTypesEnum
from fedot.core.repository.tasks import TaskTypesEnum, Task, TsForecastingParams
from fedot.core.utils import fedot_project_root
from test.integration.utilities.test_pipeline_import_export import create_correct_path, create_func_delete_files
from fedot.core.pipelines.ts_wrappers import in_sample_ts_forecast


Expand Down Expand Up @@ -58,7 +49,7 @@ def predict(pipeline: Pipeline, train: InputData, test: InputData):


@pytest.mark.parametrize(('data_length', 'atomized_class'),
[(100, AtomizedTimeSeriesDataSample),
[(100, AtomizedTimeSeriesSampler),
(1000, AtomizedTimeSeriesScaler),
(1000, AtomizedTimeSeriesDiffer),
])
Expand Down
66 changes: 63 additions & 3 deletions test/unit/optimizer/gp_operators/test_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from pathlib import Path

import pytest
from typing import Any, List, Optional, Type, Callable

from fedot.core.operations.atomized_model.atomized_model import AtomizedModel
from golem.core.dag.graph_node import GraphNode
from golem.core.dag.verification_rules import DEFAULT_DAG_RULES
from golem.core.optimisers.genetic.gp_params import GPAlgorithmParameters
Expand All @@ -20,6 +23,7 @@
from fedot.core.pipelines.pipeline_graph_generation_params import get_pipeline_generation_params
from fedot.core.repository.operation_types_repository import get_operations_for_task
from fedot.core.repository.tasks import Task, TaskTypesEnum
from fedot.core.optimisers.genetic_operators.mutation import fedot_single_edge_mutation
from test.integration.composer.test_composer import to_categorical_codes
from test.unit.dag.test_graph_utils import find_first
from test.unit.tasks.test_forecasting import get_ts_data
Expand All @@ -39,7 +43,7 @@ def file_data():
return input_data


def get_mutation_obj() -> Mutation:
def get_mutation_obj(mutation_types: Optional[List[Any]] = None) -> Mutation:
"""
Function for initializing mutation interface
"""
Expand All @@ -51,8 +55,11 @@ def get_mutation_obj() -> Mutation:
graph_params = get_pipeline_generation_params(requirements=requirements,
rules_for_constraint=DEFAULT_DAG_RULES,
task=task)
parameters = GPAlgorithmParameters(mutation_strength=MutationStrengthEnum.strong,
mutation_prob=1)
kwargs = dict(mutation_strength=MutationStrengthEnum.strong,
mutation_prob=1)
if mutation_types is not None:
kwargs = {'mutation_types': mutation_types, **kwargs}
parameters = GPAlgorithmParameters(**kwargs)

mutation = Mutation(parameters, requirements, graph_params)
return mutation
Expand Down Expand Up @@ -104,6 +111,32 @@ def get_ts_forecasting_graph_with_boosting() -> Pipeline:
return pipeline


def get_graph_with_two_nested_atomized_models(atomized_model):
simple_pipeline = (PipelineBuilder()
.add_node('scaling')
.add_branch('linear', 'poly_features')
.grow_branches('rf', 'catboost')
.join_branches('ridge')
.build())

node1 = PipelineNode('a')
node2 = PipelineNode('b', nodes_from=[node1])
node3 = PipelineNode(atomized_model(simple_pipeline), nodes_from=[node1])
node4 = PipelineNode('c', nodes_from=[node1, node3])
node5 = PipelineNode('d', nodes_from=[node2, node4])
node6 = PipelineNode('e', nodes_from=[node2, node5])
pipeline_with_atomized = Pipeline(node6)

node1 = PipelineNode('1')
node2 = PipelineNode('2', nodes_from=[node1])
node3 = PipelineNode(atomized_model(pipeline_with_atomized), nodes_from=[node1])
node4 = PipelineNode('3', nodes_from=[node1, node3])
node5 = PipelineNode('4', nodes_from=[node2, node4])
node6 = PipelineNode('5', nodes_from=[node2, node5])
pipeline_with_atomized = Pipeline(node6)
return PipelineAdapter().adapt(pipeline_with_atomized)


def test_boosting_mutation_for_linear_graph():
"""
Tests boosting mutation can add correct boosting cascade
Expand Down Expand Up @@ -170,3 +203,30 @@ def test_no_opt_or_graph_nodes_after_mutation():
new_pipeline = adapter.restore(graph)

assert not find_first(new_pipeline, lambda n: type(n) in (GraphNode, OptNode))


@pytest.mark.parametrize('atomized_model',
(AtomizedModel, ))
@pytest.mark.parametrize('mutation_type',
(fedot_single_edge_mutation, ))
def test_fedot_mutation_with_atomized_models(atomized_model: Type[AtomizedModel],
mutation_type: Callable[[OptGraph], OptGraph]):
mutation = get_mutation_obj(mutation_types=[mutation_type])
# check that mutation_type has been set correctly
assert len(mutation.parameters.mutation_types) == 1
assert mutation.parameters.mutation_types[0] is mutation_type

# make mutation some times
mut = mutation.parameters.mutation_types[0]
origin_graph = get_graph_with_two_nested_atomized_models(atomized_model)
origin_descriptive_id = origin_graph.descriptive_id

atomized_1_mutation_count = 0
atomized_2_mutation_count = 0

for _ in range(20):
graph, _ = mutation._adapt_and_apply_mutation(new_graph=deepcopy(origin_graph), mutation_type=mut)

# check that mutation was made
assert graph.descriptive_id != origin_descriptive_id

0 comments on commit 7c2e51a

Please sign in to comment.