From be6a67d7e288e718eff0c917201985149974d28b Mon Sep 17 00:00:00 2001 From: root Date: Thu, 11 Jul 2024 18:32:08 +0800 Subject: [PATCH] Add CustomFuncFit into problem; Add related examples --- Pipeline 20240711012327.pkl | 0 examples/func_fit/custom_func_fit.py | 56 +++ examples/tmp2.py | 45 +- network.svg | 415 ------------------ tensorneat/algorithm/hyperneat/hyperneat.py | 2 +- .../algorithm/hyperneat/substrate/base.py | 2 +- .../algorithm/hyperneat/substrate/default.py | 11 +- tensorneat/common/__init__.py | 1 + tensorneat/common/activation/act_jnp.py | 4 + tensorneat/common/activation/act_sympy.py | 6 + tensorneat/genome/gene/conn/default.py | 8 +- tensorneat/genome/gene/node/bias.py | 4 +- tensorneat/genome/gene/node/default.py | 4 +- tensorneat/problem/func_fit/__init__.py | 1 + tensorneat/problem/func_fit/custom.py | 119 +++++ 15 files changed, 241 insertions(+), 437 deletions(-) delete mode 100644 Pipeline 20240711012327.pkl create mode 100644 examples/func_fit/custom_func_fit.py delete mode 100644 network.svg create mode 100644 tensorneat/problem/func_fit/custom.py diff --git a/Pipeline 20240711012327.pkl b/Pipeline 20240711012327.pkl deleted file mode 100644 index e69de29..0000000 diff --git a/examples/func_fit/custom_func_fit.py b/examples/func_fit/custom_func_fit.py new file mode 100644 index 0000000..1015fc3 --- /dev/null +++ b/examples/func_fit/custom_func_fit.py @@ -0,0 +1,56 @@ +import jax.numpy as jnp + +from tensorneat.pipeline import Pipeline +from tensorneat.algorithm.neat import NEAT +from tensorneat.genome import DefaultGenome, DefaultNode, DefaultMutation, BiasNode +from tensorneat.problem.func_fit import CustomFuncFit +from tensorneat.common import Act, Agg + + +def pagie_polynomial(inputs): + x, y = inputs + res = 1 / (1 + jnp.pow(x, -4)) + 1 / (1 + jnp.pow(y, -4)) + + # important! returns an array, NOT a scalar + return jnp.array([res]) + + +if __name__ == "__main__": + + custom_problem = CustomFuncFit( + func=pagie_polynomial, + low_bounds=[-1, -1], + upper_bounds=[1, 1], + method="sample", + num_samples=1000, + ) + + pipeline = Pipeline( + algorithm=NEAT( + pop_size=10000, + species_size=20, + survival_threshold=0.01, + genome=DefaultGenome( + num_inputs=2, + num_outputs=1, + init_hidden_layers=(), + node_gene=BiasNode( + activation_options=[Act.identity, Act.inv, Act.square], + aggregation_options=[Agg.sum, Agg.product], + ), + output_transform=Act.identity, + ), + ), + problem=custom_problem, + generation_limit=100, + fitness_target=-1e-4, + seed=42, + ) + + # initialize state + state = pipeline.setup() + # run until terminate + state, best = pipeline.auto_run(state) + # show result + # pipeline.show(state, best) + print(pipeline.algorithm.genome.repr(state, *best)) diff --git a/examples/tmp2.py b/examples/tmp2.py index 26d752e..626f593 100644 --- a/examples/tmp2.py +++ b/examples/tmp2.py @@ -1,16 +1,39 @@ import jax, jax.numpy as jnp -arr = jnp.ones((10, 10)) -a = jnp.array([ - [1, 2, 3], - [4, 5, 6] -]) +from tensorneat.pipeline import Pipeline +from tensorneat.algorithm.neat import NEAT +from tensorneat.genome import DefaultGenome, DefaultNode, DefaultMutation, BiasNode +from tensorneat.problem.func_fit import CustomFuncFit +from tensorneat.common import Act, Agg -def attach_with_inf(arr, idx): - target_dim = arr.ndim + idx.ndim - 1 - expand_idx = jnp.expand_dims(idx, axis=tuple(range(idx.ndim, target_dim))) - return jnp.where(expand_idx == 1, jnp.nan, arr[idx]) +def pagie_polynomial(inputs): + x, y = inputs + return x + y + + +if __name__ == "__main__": + genome=DefaultGenome( + num_inputs=2, + num_outputs=1, + max_nodes=3, + max_conns=2, + init_hidden_layers=(), + node_gene=BiasNode( + activation_options=[Act.identity], + aggregation_options=[Agg.sum], + ), + output_transform=Act.identity, + mutation=DefaultMutation( + node_add=0, + node_delete=0, + conn_add=0.0, + conn_delete=0.0, + ) + ) + randkey = jax.random.PRNGKey(42) + state = genome.setup() + nodes, conns = genome.initialize(state, randkey) + print(genome) + -b = attach_with_inf(arr, a) -print(b) \ No newline at end of file diff --git a/network.svg b/network.svg deleted file mode 100644 index b673fe3..0000000 --- a/network.svg +++ /dev/null @@ -1,415 +0,0 @@ - - - - - - - - 2024-07-10T19:47:34.359228 - image/svg+xml - - - Matplotlib v3.9.0, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tensorneat/algorithm/hyperneat/hyperneat.py b/tensorneat/algorithm/hyperneat/hyperneat.py index e58eb21..93021f8 100644 --- a/tensorneat/algorithm/hyperneat/hyperneat.py +++ b/tensorneat/algorithm/hyperneat/hyperneat.py @@ -76,7 +76,7 @@ class HyperNEAT(BaseAlgorithm): h_nodes, h_conns = self.substrate.make_nodes( query_res - ), self.substrate.make_conn(query_res) + ), self.substrate.make_conns(query_res) return self.hyper_genome.transform(state, h_nodes, h_conns) diff --git a/tensorneat/algorithm/hyperneat/substrate/base.py b/tensorneat/algorithm/hyperneat/substrate/base.py index 4f2a074..768ab20 100644 --- a/tensorneat/algorithm/hyperneat/substrate/base.py +++ b/tensorneat/algorithm/hyperneat/substrate/base.py @@ -6,7 +6,7 @@ class BaseSubstrate(StatefulBaseClass): def make_nodes(self, query_res): raise NotImplementedError - def make_conn(self, query_res): + def make_conns(self, query_res): raise NotImplementedError @property diff --git a/tensorneat/algorithm/hyperneat/substrate/default.py b/tensorneat/algorithm/hyperneat/substrate/default.py index 42601ee..40c691c 100644 --- a/tensorneat/algorithm/hyperneat/substrate/default.py +++ b/tensorneat/algorithm/hyperneat/substrate/default.py @@ -1,5 +1,7 @@ -import jax.numpy as jnp -from . import BaseSubstrate +from jax import vmap, numpy as jnp + +from .base import BaseSubstrate +from tensorneat.genome.utils import set_conn_attrs class DefaultSubstrate(BaseSubstrate): @@ -13,8 +15,9 @@ class DefaultSubstrate(BaseSubstrate): def make_nodes(self, query_res): return self.nodes - def make_conn(self, query_res): - return self.conns.at[:, 2:].set(query_res) # change weight + def make_conns(self, query_res): + # change weight of conns + return vmap(set_conn_attrs)(self.conns, query_res) @property def query_coors(self): diff --git a/tensorneat/common/__init__.py b/tensorneat/common/__init__.py index 7e64bb7..979bc66 100644 --- a/tensorneat/common/__init__.py +++ b/tensorneat/common/__init__.py @@ -31,6 +31,7 @@ name2sympy = { "maxabs": SympyMaxabs, "mean": SympyMean, "clip": SympyClip, + "square": SympySquare, } diff --git a/tensorneat/common/activation/act_jnp.py b/tensorneat/common/activation/act_jnp.py index d554bc1..790152e 100644 --- a/tensorneat/common/activation/act_jnp.py +++ b/tensorneat/common/activation/act_jnp.py @@ -69,6 +69,10 @@ class Act: z = jnp.clip(z, -10, 10) return jnp.exp(z) + @staticmethod + def square(z): + return jnp.pow(z, 2) + @staticmethod def abs(z): z = jnp.clip(z, -1, 1) diff --git a/tensorneat/common/activation/act_sympy.py b/tensorneat/common/activation/act_sympy.py index 6c43194..c7055a7 100644 --- a/tensorneat/common/activation/act_sympy.py +++ b/tensorneat/common/activation/act_sympy.py @@ -184,6 +184,12 @@ class SympyExp(sp.Function): return rf"\mathrm{{exp}}\left({sp.latex(self.args[0])}\right)" +class SympySquare(sp.Function): + @classmethod + def eval(cls, z): + return sp.Pow(z, 2) + + class SympyAbs(sp.Function): @classmethod def eval(cls, z): diff --git a/tensorneat/genome/gene/conn/default.py b/tensorneat/genome/gene/conn/default.py index 6b0017a..48dbbc4 100644 --- a/tensorneat/genome/gene/conn/default.py +++ b/tensorneat/genome/gene/conn/default.py @@ -17,6 +17,8 @@ class DefaultConn(BaseConn): weight_mutate_power: float = 0.15, weight_mutate_rate: float = 0.2, weight_replace_rate: float = 0.015, + weight_lower_bound: float = -5.0, + weight_upper_bound: float = 5.0, ): super().__init__() self.weight_init_mean = weight_init_mean @@ -24,6 +26,9 @@ class DefaultConn(BaseConn): self.weight_mutate_power = weight_mutate_power self.weight_mutate_rate = weight_mutate_rate self.weight_replace_rate = weight_replace_rate + self.weight_lower_bound = weight_lower_bound + self.weight_upper_bound = weight_upper_bound + def new_zero_attrs(self, state): return jnp.array([0.0]) # weight = 0 @@ -36,6 +41,7 @@ class DefaultConn(BaseConn): jax.random.normal(randkey, ()) * self.weight_init_std + self.weight_init_mean ) + weight = jnp.clip(weight, self.weight_lower_bound, self.weight_upper_bound) return jnp.array([weight]) def mutate(self, state, randkey, attrs): @@ -49,7 +55,7 @@ class DefaultConn(BaseConn): self.weight_mutate_rate, self.weight_replace_rate, ) - + weight = jnp.clip(weight, self.weight_lower_bound, self.weight_upper_bound) return jnp.array([weight]) def distance(self, state, attrs1, attrs2): diff --git a/tensorneat/genome/gene/node/bias.py b/tensorneat/genome/gene/node/bias.py index f86c440..91c20ae 100644 --- a/tensorneat/genome/gene/node/bias.py +++ b/tensorneat/genome/gene/node/bias.py @@ -47,9 +47,9 @@ class BiasNode(BaseNode): if isinstance(activation_options, Callable): activation_options = [activation_options] - if len(aggregation_options) == 1 and aggregation_default is None: + if aggregation_default is None: aggregation_default = aggregation_options[0] - if len(activation_options) == 1 and activation_default is None: + if activation_default is None: activation_default = activation_options[0] self.bias_init_mean = bias_init_mean diff --git a/tensorneat/genome/gene/node/default.py b/tensorneat/genome/gene/node/default.py index 96700e2..aa36e19 100644 --- a/tensorneat/genome/gene/node/default.py +++ b/tensorneat/genome/gene/node/default.py @@ -52,9 +52,9 @@ class DefaultNode(BaseNode): if isinstance(activation_options, Callable): activation_options = [activation_options] - if len(aggregation_options) == 1 and aggregation_default is None: + if aggregation_default is None: aggregation_default = aggregation_options[0] - if len(activation_options) == 1 and activation_default is None: + if activation_default is None: activation_default = activation_options[0] self.bias_init_mean = bias_init_mean diff --git a/tensorneat/problem/func_fit/__init__.py b/tensorneat/problem/func_fit/__init__.py index ecad1e1..bf633e0 100644 --- a/tensorneat/problem/func_fit/__init__.py +++ b/tensorneat/problem/func_fit/__init__.py @@ -1,3 +1,4 @@ from .func_fit import FuncFit from .xor import XOR from .xor3d import XOR3d +from .custom import CustomFuncFit \ No newline at end of file diff --git a/tensorneat/problem/func_fit/custom.py b/tensorneat/problem/func_fit/custom.py new file mode 100644 index 0000000..3a21365 --- /dev/null +++ b/tensorneat/problem/func_fit/custom.py @@ -0,0 +1,119 @@ +from typing import Callable, Union, List, Tuple, Sequence + +import jax +from jax import vmap, Array, numpy as jnp +import numpy as np + +from .func_fit import FuncFit + + +class CustomFuncFit(FuncFit): + + def __init__( + self, + func: Callable, + low_bounds: Union[List, Tuple, Array], + upper_bounds: Union[List, Tuple, Array], + method: str = "sample", + num_samples: int = 100, + step_size: Array = None, + *args, + **kwargs, + ): + + if isinstance(low_bounds, list) or isinstance(low_bounds, tuple): + low_bounds = np.array(low_bounds, dtype=np.float32) + if isinstance(upper_bounds, list) or isinstance(upper_bounds, tuple): + upper_bounds = np.array(upper_bounds, dtype=np.float32) + + try: + out = func(low_bounds) + except Exception as e: + raise ValueError(f"func(low_bounds) raise an exception: {e}") + assert low_bounds.shape == upper_bounds.shape + + assert method in {"sample", "grid"} + + self.func = func + self.low_bounds = low_bounds + self.upper_bounds = upper_bounds + + self.method = method + self.num_samples = num_samples + self.step_size = step_size + + self.generate_dataset() + + super().__init__(*args, **kwargs) + + def generate_dataset(self): + + if self.method == "sample": + assert ( + self.num_samples > 0 + ), f"num_samples must be positive, got {self.num_samples}" + + inputs = np.zeros( + (self.num_samples, self.low_bounds.shape[0]), dtype=np.float32 + ) + for i in range(self.low_bounds.shape[0]): + inputs[:, i] = np.random.uniform( + low=self.low_bounds[i], + high=self.upper_bounds[i], + size=(self.num_samples,), + ) + elif self.method == "grid": + assert ( + self.step_size is not None + ), "step_size must be provided when method is 'grid'" + assert ( + self.step_size.shape == self.low_bounds.shape + ), "step_size must have the same shape as low_bounds" + assert np.all(self.step_size > 0), "step_size must be positive" + + inputs = np.zeros((1, 1)) + for i in range(self.low_bounds.shape[0]): + new_col = np.arange( + self.low_bounds[i], self.upper_bounds[i], self.step_size[i] + ) + inputs = cartesian_product(inputs, new_col[:, None]) + inputs = inputs[:, 1:] + else: + raise ValueError(f"Unknown method: {self.method}") + + outputs = vmap(self.func)(inputs) + + self.data_inputs = jnp.array(inputs) + self.data_outputs = jnp.array(outputs) + + @property + def inputs(self): + return self.data_inputs + + @property + def targets(self): + return self.data_outputs + + @property + def input_shape(self): + return self.data_inputs.shape + + @property + def output_shape(self): + return self.data_outputs.shape + + +def cartesian_product(arr1, arr2): + assert ( + arr1.ndim == arr2.ndim + ), "arr1 and arr2 must have the same number of dimensions" + assert arr1.ndim <= 2, "arr1 and arr2 must have at most 2 dimensions" + + len1 = arr1.shape[0] + len2 = arr2.shape[0] + + repeated_arr1 = np.repeat(arr1, len2, axis=0) + tiled_arr2 = np.tile(arr2, (len1, 1)) + + new_arr = np.concatenate((repeated_arr1, tiled_arr2), axis=1) + return new_arr