eckity.genetic_encodings.gp.tree.tree_individual
This module implements the tree class.
1""" 2This module implements the tree class. 3""" 4 5import numpy as np 6from numbers import Number 7from random import randint, uniform, random 8 9from eckity.base.utils import arity 10from eckity.genetic_encodings.gp.tree.utils import _generate_args 11 12from eckity.individual import Individual 13from eckity.genetic_encodings.gp.tree.functions import f_add, f_sub, f_mul, f_div 14 15 16class Tree(Individual): 17 """ 18 A tree optimized for genetic programming operations. 19 It is represented by a list of nodes in depth-first order. 20 There are two types of nodes: functions and terminals. 21 22 (tree is not meant as a stand-alone -- parameters are supplied through the call from the Tree Creators) 23 24 Parameters 25 ---------- 26 init_depth : (int, int) 27 Min and max depths of initial random trees. The default is None. 28 29 function_set : list 30 List of functions used as internal nodes in the GP tree. The default is None. 31 32 terminal_set : list 33 List of terminals used in the GP-tree leaves. The default is None. 34 35 erc_range : (float, float) 36 Range of values for ephemeral random constant (ERC). The default is None. 37 """ 38 def __init__(self, 39 fitness, 40 function_set=None, 41 terminal_set=None, 42 erc_range=None, 43 init_depth=(1, 2)): 44 super().__init__(fitness) 45 if function_set is None: 46 function_set = [f_add, f_sub, f_mul, f_div] 47 48 if terminal_set is None: 49 terminal_set = ['x', 'y', 'z', 0, 1, -1] 50 51 self.function_set = function_set 52 self.terminal_set = terminal_set 53 self.n_terminals = len(terminal_set) 54 self.arity = dict([(func, arity(func)) for func in self.function_set]) 55 self.vars = [t for t in terminal_set if not isinstance(t, Number)] 56 self.erc_range = erc_range 57 self.n_functions = len(self.function_set) 58 self.init_depth = init_depth 59 self.tree = [] 60 61 def size(self): 62 """ 63 Compute size of tree. 64 65 Returns 66 ------- 67 int 68 tree size (= number of nodes). 69 """ 70 return len(self.tree) 71 72 def add_tree(self, node): 73 self.tree.append(node) 74 75 def empty_tree(self): 76 self.tree = [] 77 78 def _depth(self, pos, depth): 79 """Recursively compute depth 80 (pos is a size-1 list so as to pass "by reference" on successive recursive calls).""" 81 82 node = self.tree[pos[0]] 83 84 depths = [] 85 if node in self.function_set: 86 for i in range(self.arity[node]): 87 pos[0] += 1 88 depths.append(1 + self._depth(pos, depth + 1)) 89 return max(depths) 90 else: 91 return 0 92 93 def depth(self): 94 """ 95 Compute depth of tree (maximal path length to a leaf). 96 97 Returns 98 ------- 99 int 100 tree depth. 101 """ 102 103 return self._depth([0], 0) 104 105 def random_function(self): 106 """select a random function""" 107 return self.function_set[randint(0, self.n_functions - 1)] 108 109 def random_terminal(self): 110 """Select a random terminal or create an ERC terminal""" 111 if self.erc_range is None: 112 node = self.terminal_set[randint(0, self.n_terminals - 1)] 113 else: 114 if random() > 0.5: 115 node = self.terminal_set[randint(0, self.n_terminals - 1)] 116 else: 117 node = round(uniform(*self.erc_range), 4) 118 return node 119 120 def _execute(self, pos, **kwargs): 121 """Recursively execute the tree by traversing it in a depth-first order 122 (pos is a size-1 list so as to pass "by reference" on successive recursive calls).""" 123 124 node = self.tree[pos[0]] 125 126 if node in self.function_set: 127 arglist = [] 128 for i in range(self.arity[node]): 129 pos[0] += 1 130 res = self._execute(pos, **kwargs) 131 arglist.append(res) 132 return node(*arglist) 133 else: # terminal 134 if isinstance(node, Number): # terminal is a constant 135 return node 136 else: # terminal is a variable, return its value 137 return kwargs[node] 138 139 def execute(self, *args, **kwargs): 140 """ 141 Execute the program (tree). 142 Input is a numpy array or keyword arguments (but not both). 143 144 Parameters 145 ---------- 146 args : arguments 147 A numpy array. 148 149 kwargs : keyword arguments 150 Input to program, including every variable in the terminal set as a keyword argument. 151 For example, if `terminal_set=['x', 'y', 'z', 0, 1, -1]` 152 then call `execute(x=..., y=..., z=...)`. 153 154 Returns 155 ------- 156 object 157 Result of tree execution. 158 """ 159 160 reshape = False 161 if args != (): # numpy array -- convert to kwargs 162 try: 163 X = args[0] 164 kwargs = _generate_args(X) 165 reshape = True 166 except Exception: 167 raise ValueError(f'Bad argument to tree.execute, must be numpy array or kwargs: {args}') 168 169 kw = list(kwargs.keys()) 170 171 bad_vars = [item for item in kw if item not in self.vars] 172 if len(bad_vars) > 0: 173 raise ValueError(f'tree.execute received variable arguments not in terminal set: {bad_vars}') 174 175 missing_vars = [item for item in self.vars if item not in kw] 176 if len(missing_vars) > 0: 177 raise ValueError( 178 f'Some variable terminals were not passed to tree.execute as keyword arguments: {missing_vars}') 179 180 res = self._execute([0], **kwargs) 181 if reshape and (isinstance(res, Number) or res.shape == np.shape(0)): 182 # sometimes a tree degenrates to a scalar value 183 res = np.full_like(X[:, 0], res) 184 return res 185 186 def random_subtree(self): 187 # select a random node index from this individual's tree 188 rnd_i = randint(0, self.size() - 1) 189 # find the subtree's end 190 end_i = self._find_subtree_end([rnd_i]) 191 # now we have a random subtree from this individual 192 return self.tree[rnd_i:end_i + 1] 193 194 def _find_subtree_end(self, pos): 195 """find index of final node of subtree that starts at `pos` 196 (pos is a size-1 list so as to pass "by reference" on successive recursive calls).""" 197 198 node = self.tree[pos[0]] 199 200 if node in self.function_set: 201 for i in range(self.arity[node]): 202 pos[0] += 1 203 self._find_subtree_end(pos) 204 205 return pos[0] 206 207 def replace_subtree(self, subtree): 208 """ 209 Replace the subtree starting at `index` with `subtree` 210 211 Parameters 212 ---------- 213 subtree - new subtree to replace the some existing subtree in this individual's tree 214 215 Returns 216 ------- 217 None 218 """ 219 220 index = randint(0, self.size() - 1) # select a random node (index) 221 end_i = self._find_subtree_end([index]) 222 if isinstance(self.tree[end_i], list): 223 print(self.tree[end_i], list) 224 left_part = self.tree[:index] 225 right_part = self.tree[(end_i + 1):] 226 self.tree = left_part + subtree + right_part 227 228 def _node_label(self, node): 229 """ 230 return a string label for the node 231 232 Parameters 233 ---------- 234 node - some node in the tree's function/terminal set 235 236 Returns 237 ------- 238 node name - either a terminal (x0, x1,...) or a function (f_add, f_or, ...) 239 """ 240 return node.__name__ if node in self.function_set else str(node) 241 242 def _show(self, prefix, pos): 243 """Recursively produce a simple textual printout of the tree 244 (pos is a size-1 list so as to pass "by reference" on successive recursive calls).""" 245 246 node = self.tree[pos[0]] 247 if node in self.function_set: 248 print(f'{prefix}{self._node_label(node)}') 249 for i in range(self.arity[node]): 250 pos[0] += 1 251 self._show(prefix + " ", pos) 252 else: # terminal 253 print(f'{prefix}{self._node_label(node)}') 254 255 def show(self): 256 """ 257 Print out a simple textual representation of the tree. 258 259 Returns 260 ------- 261 None. 262 """ 263 self._show("", [0]) 264 265# end class tree
17class Tree(Individual): 18 """ 19 A tree optimized for genetic programming operations. 20 It is represented by a list of nodes in depth-first order. 21 There are two types of nodes: functions and terminals. 22 23 (tree is not meant as a stand-alone -- parameters are supplied through the call from the Tree Creators) 24 25 Parameters 26 ---------- 27 init_depth : (int, int) 28 Min and max depths of initial random trees. The default is None. 29 30 function_set : list 31 List of functions used as internal nodes in the GP tree. The default is None. 32 33 terminal_set : list 34 List of terminals used in the GP-tree leaves. The default is None. 35 36 erc_range : (float, float) 37 Range of values for ephemeral random constant (ERC). The default is None. 38 """ 39 def __init__(self, 40 fitness, 41 function_set=None, 42 terminal_set=None, 43 erc_range=None, 44 init_depth=(1, 2)): 45 super().__init__(fitness) 46 if function_set is None: 47 function_set = [f_add, f_sub, f_mul, f_div] 48 49 if terminal_set is None: 50 terminal_set = ['x', 'y', 'z', 0, 1, -1] 51 52 self.function_set = function_set 53 self.terminal_set = terminal_set 54 self.n_terminals = len(terminal_set) 55 self.arity = dict([(func, arity(func)) for func in self.function_set]) 56 self.vars = [t for t in terminal_set if not isinstance(t, Number)] 57 self.erc_range = erc_range 58 self.n_functions = len(self.function_set) 59 self.init_depth = init_depth 60 self.tree = [] 61 62 def size(self): 63 """ 64 Compute size of tree. 65 66 Returns 67 ------- 68 int 69 tree size (= number of nodes). 70 """ 71 return len(self.tree) 72 73 def add_tree(self, node): 74 self.tree.append(node) 75 76 def empty_tree(self): 77 self.tree = [] 78 79 def _depth(self, pos, depth): 80 """Recursively compute depth 81 (pos is a size-1 list so as to pass "by reference" on successive recursive calls).""" 82 83 node = self.tree[pos[0]] 84 85 depths = [] 86 if node in self.function_set: 87 for i in range(self.arity[node]): 88 pos[0] += 1 89 depths.append(1 + self._depth(pos, depth + 1)) 90 return max(depths) 91 else: 92 return 0 93 94 def depth(self): 95 """ 96 Compute depth of tree (maximal path length to a leaf). 97 98 Returns 99 ------- 100 int 101 tree depth. 102 """ 103 104 return self._depth([0], 0) 105 106 def random_function(self): 107 """select a random function""" 108 return self.function_set[randint(0, self.n_functions - 1)] 109 110 def random_terminal(self): 111 """Select a random terminal or create an ERC terminal""" 112 if self.erc_range is None: 113 node = self.terminal_set[randint(0, self.n_terminals - 1)] 114 else: 115 if random() > 0.5: 116 node = self.terminal_set[randint(0, self.n_terminals - 1)] 117 else: 118 node = round(uniform(*self.erc_range), 4) 119 return node 120 121 def _execute(self, pos, **kwargs): 122 """Recursively execute the tree by traversing it in a depth-first order 123 (pos is a size-1 list so as to pass "by reference" on successive recursive calls).""" 124 125 node = self.tree[pos[0]] 126 127 if node in self.function_set: 128 arglist = [] 129 for i in range(self.arity[node]): 130 pos[0] += 1 131 res = self._execute(pos, **kwargs) 132 arglist.append(res) 133 return node(*arglist) 134 else: # terminal 135 if isinstance(node, Number): # terminal is a constant 136 return node 137 else: # terminal is a variable, return its value 138 return kwargs[node] 139 140 def execute(self, *args, **kwargs): 141 """ 142 Execute the program (tree). 143 Input is a numpy array or keyword arguments (but not both). 144 145 Parameters 146 ---------- 147 args : arguments 148 A numpy array. 149 150 kwargs : keyword arguments 151 Input to program, including every variable in the terminal set as a keyword argument. 152 For example, if `terminal_set=['x', 'y', 'z', 0, 1, -1]` 153 then call `execute(x=..., y=..., z=...)`. 154 155 Returns 156 ------- 157 object 158 Result of tree execution. 159 """ 160 161 reshape = False 162 if args != (): # numpy array -- convert to kwargs 163 try: 164 X = args[0] 165 kwargs = _generate_args(X) 166 reshape = True 167 except Exception: 168 raise ValueError(f'Bad argument to tree.execute, must be numpy array or kwargs: {args}') 169 170 kw = list(kwargs.keys()) 171 172 bad_vars = [item for item in kw if item not in self.vars] 173 if len(bad_vars) > 0: 174 raise ValueError(f'tree.execute received variable arguments not in terminal set: {bad_vars}') 175 176 missing_vars = [item for item in self.vars if item not in kw] 177 if len(missing_vars) > 0: 178 raise ValueError( 179 f'Some variable terminals were not passed to tree.execute as keyword arguments: {missing_vars}') 180 181 res = self._execute([0], **kwargs) 182 if reshape and (isinstance(res, Number) or res.shape == np.shape(0)): 183 # sometimes a tree degenrates to a scalar value 184 res = np.full_like(X[:, 0], res) 185 return res 186 187 def random_subtree(self): 188 # select a random node index from this individual's tree 189 rnd_i = randint(0, self.size() - 1) 190 # find the subtree's end 191 end_i = self._find_subtree_end([rnd_i]) 192 # now we have a random subtree from this individual 193 return self.tree[rnd_i:end_i + 1] 194 195 def _find_subtree_end(self, pos): 196 """find index of final node of subtree that starts at `pos` 197 (pos is a size-1 list so as to pass "by reference" on successive recursive calls).""" 198 199 node = self.tree[pos[0]] 200 201 if node in self.function_set: 202 for i in range(self.arity[node]): 203 pos[0] += 1 204 self._find_subtree_end(pos) 205 206 return pos[0] 207 208 def replace_subtree(self, subtree): 209 """ 210 Replace the subtree starting at `index` with `subtree` 211 212 Parameters 213 ---------- 214 subtree - new subtree to replace the some existing subtree in this individual's tree 215 216 Returns 217 ------- 218 None 219 """ 220 221 index = randint(0, self.size() - 1) # select a random node (index) 222 end_i = self._find_subtree_end([index]) 223 if isinstance(self.tree[end_i], list): 224 print(self.tree[end_i], list) 225 left_part = self.tree[:index] 226 right_part = self.tree[(end_i + 1):] 227 self.tree = left_part + subtree + right_part 228 229 def _node_label(self, node): 230 """ 231 return a string label for the node 232 233 Parameters 234 ---------- 235 node - some node in the tree's function/terminal set 236 237 Returns 238 ------- 239 node name - either a terminal (x0, x1,...) or a function (f_add, f_or, ...) 240 """ 241 return node.__name__ if node in self.function_set else str(node) 242 243 def _show(self, prefix, pos): 244 """Recursively produce a simple textual printout of the tree 245 (pos is a size-1 list so as to pass "by reference" on successive recursive calls).""" 246 247 node = self.tree[pos[0]] 248 if node in self.function_set: 249 print(f'{prefix}{self._node_label(node)}') 250 for i in range(self.arity[node]): 251 pos[0] += 1 252 self._show(prefix + " ", pos) 253 else: # terminal 254 print(f'{prefix}{self._node_label(node)}') 255 256 def show(self): 257 """ 258 Print out a simple textual representation of the tree. 259 260 Returns 261 ------- 262 None. 263 """ 264 self._show("", [0])
A tree optimized for genetic programming operations. It is represented by a list of nodes in depth-first order. There are two types of nodes: functions and terminals.
(tree is not meant as a stand-alone -- parameters are supplied through the call from the Tree Creators)
Parameters
- init_depth ((int, int)): Min and max depths of initial random trees. The default is None.
- function_set (list): List of functions used as internal nodes in the GP tree. The default is None.
- terminal_set (list): List of terminals used in the GP-tree leaves. The default is None.
- erc_range ((float, float)): Range of values for ephemeral random constant (ERC). The default is None.
Tree( fitness, function_set=None, terminal_set=None, erc_range=None, init_depth=(1, 2))
39 def __init__(self, 40 fitness, 41 function_set=None, 42 terminal_set=None, 43 erc_range=None, 44 init_depth=(1, 2)): 45 super().__init__(fitness) 46 if function_set is None: 47 function_set = [f_add, f_sub, f_mul, f_div] 48 49 if terminal_set is None: 50 terminal_set = ['x', 'y', 'z', 0, 1, -1] 51 52 self.function_set = function_set 53 self.terminal_set = terminal_set 54 self.n_terminals = len(terminal_set) 55 self.arity = dict([(func, arity(func)) for func in self.function_set]) 56 self.vars = [t for t in terminal_set if not isinstance(t, Number)] 57 self.erc_range = erc_range 58 self.n_functions = len(self.function_set) 59 self.init_depth = init_depth 60 self.tree = []
def
size(self):
62 def size(self): 63 """ 64 Compute size of tree. 65 66 Returns 67 ------- 68 int 69 tree size (= number of nodes). 70 """ 71 return len(self.tree)
Compute size of tree.
Returns
- int: tree size (= number of nodes).
def
depth(self):
94 def depth(self): 95 """ 96 Compute depth of tree (maximal path length to a leaf). 97 98 Returns 99 ------- 100 int 101 tree depth. 102 """ 103 104 return self._depth([0], 0)
Compute depth of tree (maximal path length to a leaf).
Returns
- int: tree depth.
def
random_function(self):
106 def random_function(self): 107 """select a random function""" 108 return self.function_set[randint(0, self.n_functions - 1)]
select a random function
def
random_terminal(self):
110 def random_terminal(self): 111 """Select a random terminal or create an ERC terminal""" 112 if self.erc_range is None: 113 node = self.terminal_set[randint(0, self.n_terminals - 1)] 114 else: 115 if random() > 0.5: 116 node = self.terminal_set[randint(0, self.n_terminals - 1)] 117 else: 118 node = round(uniform(*self.erc_range), 4) 119 return node
Select a random terminal or create an ERC terminal
def
execute(self, *args, **kwargs):
140 def execute(self, *args, **kwargs): 141 """ 142 Execute the program (tree). 143 Input is a numpy array or keyword arguments (but not both). 144 145 Parameters 146 ---------- 147 args : arguments 148 A numpy array. 149 150 kwargs : keyword arguments 151 Input to program, including every variable in the terminal set as a keyword argument. 152 For example, if `terminal_set=['x', 'y', 'z', 0, 1, -1]` 153 then call `execute(x=..., y=..., z=...)`. 154 155 Returns 156 ------- 157 object 158 Result of tree execution. 159 """ 160 161 reshape = False 162 if args != (): # numpy array -- convert to kwargs 163 try: 164 X = args[0] 165 kwargs = _generate_args(X) 166 reshape = True 167 except Exception: 168 raise ValueError(f'Bad argument to tree.execute, must be numpy array or kwargs: {args}') 169 170 kw = list(kwargs.keys()) 171 172 bad_vars = [item for item in kw if item not in self.vars] 173 if len(bad_vars) > 0: 174 raise ValueError(f'tree.execute received variable arguments not in terminal set: {bad_vars}') 175 176 missing_vars = [item for item in self.vars if item not in kw] 177 if len(missing_vars) > 0: 178 raise ValueError( 179 f'Some variable terminals were not passed to tree.execute as keyword arguments: {missing_vars}') 180 181 res = self._execute([0], **kwargs) 182 if reshape and (isinstance(res, Number) or res.shape == np.shape(0)): 183 # sometimes a tree degenrates to a scalar value 184 res = np.full_like(X[:, 0], res) 185 return res
Execute the program (tree). Input is a numpy array or keyword arguments (but not both).
Parameters
- args (arguments): A numpy array.
- kwargs (keyword arguments):
Input to program, including every variable in the terminal set as a keyword argument.
For example, if
terminal_set=['x', 'y', 'z', 0, 1, -1]
then callexecute(x=..., y=..., z=...)
.
Returns
- object: Result of tree execution.
def
replace_subtree(self, subtree):
208 def replace_subtree(self, subtree): 209 """ 210 Replace the subtree starting at `index` with `subtree` 211 212 Parameters 213 ---------- 214 subtree - new subtree to replace the some existing subtree in this individual's tree 215 216 Returns 217 ------- 218 None 219 """ 220 221 index = randint(0, self.size() - 1) # select a random node (index) 222 end_i = self._find_subtree_end([index]) 223 if isinstance(self.tree[end_i], list): 224 print(self.tree[end_i], list) 225 left_part = self.tree[:index] 226 right_part = self.tree[(end_i + 1):] 227 self.tree = left_part + subtree + right_part
Replace the subtree starting at index
with subtree
Parameters
- subtree - new subtree to replace the some existing subtree in this individual's tree
Returns
- None