Skip to content

Commit e6cfcaa

Browse files
committed
chore(optimizers): Implements first draft of GSGP.
1 parent 4320039 commit e6cfcaa

File tree

6 files changed

+233
-15
lines changed

6 files changed

+233
-15
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from opytimizer.optimizers.evolutionary import GSGP
2+
3+
# One should declare a hyperparameters object based
4+
# on the desired algorithm that will be used
5+
params = {
6+
"p_reproduction": 0.25,
7+
"p_mutation": 0.1,
8+
"p_crossover": 0.2,
9+
"prunning_ratio": 0.0,
10+
}
11+
12+
# Creates a GSGP optimizer
13+
o = GSGP(params=params)

opytimizer/optimizers/evolutionary/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from opytimizer.optimizers.evolutionary.foa import FOA
1010
from opytimizer.optimizers.evolutionary.ga import GA
1111
from opytimizer.optimizers.evolutionary.gp import GP
12+
from opytimizer.optimizers.evolutionary.gsgp import GSGP
1213
from opytimizer.optimizers.evolutionary.hs import GHS, GOGHS, HS, IHS, NGHS, SGHS
1314
from opytimizer.optimizers.evolutionary.iwo import IWO
1415
from opytimizer.optimizers.evolutionary.rra import RRA

opytimizer/optimizers/evolutionary/gp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def _mutate(self, space: TreeSpace, tree: Node, max_nodes: int) -> Node:
177177
178178
Args:
179179
space: A TreeSpace object.
180-
trees: A Node instance to be mutated.
180+
tree: A Node instance to be mutated.
181181
max_nodes: Maximum number of nodes to be searched.
182182
183183
Returns:
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
"""Geometric Semantic Genetic Programming.
2+
"""
3+
4+
import copy
5+
from hashlib import sha1
6+
from typing import Any, Dict, Optional
7+
8+
import numpy as np
9+
10+
import opytimizer.math.general as g
11+
import opytimizer.math.random as r
12+
from opytimizer.core.node import Node
13+
from opytimizer.optimizers.evolutionary.gp import GP
14+
from opytimizer.spaces.tree import TreeSpace
15+
from opytimizer.utils import logging
16+
17+
logger = logging.get_logger(__name__)
18+
19+
20+
class GSGP(GP):
21+
"""A GSGP class, inherited from GP.
22+
23+
This is the designed class to define GSGP-related
24+
variables and methods.
25+
26+
References:
27+
A. Moraglio, K. Krawiec and C. G. Johnson.
28+
Geometric semantic genetic programming.
29+
Lecture Notes in Computer Science (2012).
30+
31+
"""
32+
33+
def __init__(self, params: Optional[Dict[str, Any]] = None) -> None:
34+
"""Initialization method.
35+
36+
Args:
37+
params: Contains key-value parameters to the meta-heuristics.
38+
39+
"""
40+
41+
logger.info("Overriding class: GP -> GSGP.")
42+
43+
super(GSGP, self).__init__(params)
44+
45+
logger.info("Class overrided.")
46+
47+
def _mutation(self, space: TreeSpace) -> None:
48+
"""Mutates a number of individuals pre-selected through a tournament procedure.
49+
50+
Args:
51+
space: A TreeSpace object.
52+
53+
"""
54+
55+
fitness = [agent.fit for agent in space.agents]
56+
57+
n_individuals = int(space.n_agents * self.p_mutation)
58+
if n_individuals % 2 != 0:
59+
n_individuals += 1
60+
61+
selected = g.tournament_selection(fitness, n_individuals)
62+
for s in selected:
63+
n_nodes = space.trees[s].n_nodes
64+
if n_nodes > 1:
65+
max_nodes = self._prune_nodes(n_nodes)
66+
space.trees[s] = self._mutate(
67+
space.trees[s], space.n_variables, max_nodes
68+
)
69+
70+
def _mutate(self, tree: Node, n_variables: int, max_nodes: int) -> Node:
71+
"""Actually performs the mutation on a single tree.
72+
73+
Args:
74+
tree: A Node instance to be mutated.
75+
n_variables: Number of variables.
76+
max_nodes: Maximum number of nodes to be searched.
77+
78+
Returns:
79+
(Node): A mutated tree.
80+
81+
"""
82+
83+
mutated_tree = copy.deepcopy(tree)
84+
mutation_point = int(r.generate_uniform_random_number(2, max_nodes))
85+
sub_tree, _ = mutated_tree.find_node(mutation_point)
86+
87+
# If the mutation point's parent is not a root (this may happen when the mutation point is a function),
88+
# and find_node() stops at a terminal node whose father is a root
89+
if sub_tree:
90+
position = r.generate_uniform_random_number(size=n_variables)
91+
position_hash = sha1(repr(position).encode("ascii")).hexdigest()[:4]
92+
93+
terminal = Node(position_hash, "TERMINAL", position)
94+
95+
operator_id = r.generate_integer_random_number(0, 3)
96+
if operator_id == 0:
97+
terminal.value = np.exp(terminal.value)
98+
elif operator_id == 1:
99+
terminal.value = np.fabs(np.sin(terminal.value))
100+
elif operator_id == 2:
101+
terminal.value = np.cos(np.sin(terminal.value))
102+
103+
if r.generate_uniform_random_number() <= 0.5:
104+
root = Node("SUM", "FUNCTION")
105+
else:
106+
root = Node("MUL", "FUNCTION")
107+
108+
root.parent = None
109+
root.left = sub_tree
110+
root.right = terminal
111+
112+
sub_tree.parent = root
113+
terminal.parent = root
114+
terminal.flag = False
115+
116+
return root
117+
118+
return mutated_tree
119+
120+
def _crossover(self, space: TreeSpace) -> None:
121+
"""Crossover a number of individuals pre-selected through a tournament procedure.
122+
123+
Args:
124+
space: A TreeSpace object.
125+
126+
"""
127+
128+
fitness = [agent.fit for agent in space.agents]
129+
130+
n_individuals = int(space.n_agents * self.p_crossover)
131+
if n_individuals % 2 != 0:
132+
n_individuals += 1
133+
134+
selected = g.tournament_selection(fitness, n_individuals)
135+
for s in g.n_wise(selected):
136+
father_nodes = space.trees[s[0]].n_nodes
137+
mother_nodes = space.trees[s[1]].n_nodes
138+
139+
if (father_nodes > 1) and (mother_nodes > 1):
140+
max_f_nodes = self._prune_nodes(father_nodes)
141+
max_m_nodes = self._prune_nodes(mother_nodes)
142+
143+
space.trees[s[0]] = self._cross(
144+
space.trees[s[0]],
145+
space.trees[s[1]],
146+
space.n_variables,
147+
max_f_nodes,
148+
max_m_nodes,
149+
)
150+
151+
def _cross(
152+
self,
153+
father: Node,
154+
mother: Node,
155+
n_variables: int,
156+
max_father: int,
157+
max_mother: int,
158+
) -> Node:
159+
"""Actually performs the crossover over a father and mother nodes.
160+
161+
Args:
162+
father: A father's node to be crossed.
163+
mother: A mother's node to be crossed.
164+
n_variables: Number of variables.
165+
max_father: Maximum of nodes from father to be used.
166+
max_mother: Maximum of nodes from mother to be used.
167+
168+
Returns:
169+
(Node): Single offspring based on the crossover operator.
170+
171+
"""
172+
173+
father_offspring = copy.deepcopy(father)
174+
father_point = int(r.generate_uniform_random_number(2, max_father))
175+
sub_father, _ = father_offspring.find_node(father_point)
176+
177+
mother_offspring = copy.deepcopy(mother)
178+
mother_point = int(r.generate_uniform_random_number(2, max_mother))
179+
sub_mother, _ = mother_offspring.find_node(mother_point)
180+
181+
if sub_father and sub_mother:
182+
position = r.generate_uniform_random_number(size=n_variables)
183+
position_hash = sha1(repr(position).encode("ascii")).hexdigest()[:4]
184+
185+
terminal = Node(position_hash, "TERMINAL", position)
186+
not_terminal = Node("~" + position_hash, "TERMINAL", 1 - terminal.value)
187+
188+
root = Node("SUM", "FUNCTION")
189+
left_node = Node("MUL", "FUNCTION")
190+
right_node = Node("MUL", "FUNCTION")
191+
192+
root.parent = None
193+
root.left = left_node
194+
root.right = right_node
195+
196+
sub_father.parent = left_node
197+
sub_mother.parent = right_node
198+
sub_mother.flag = False
199+
200+
left_node.parent = root
201+
left_node.left = sub_father
202+
left_node.right = terminal
203+
204+
not_terminal.parent = right_node
205+
terminal.parent = left_node
206+
terminal.flag = False
207+
208+
right_node.parent = root
209+
right_node.left = not_terminal
210+
right_node.right = sub_mother
211+
right_node.flag = False
212+
213+
return root
214+
215+
return father_offspring

opytimizer/spaces/tree.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ def _create_terminals(self) -> None:
178178
for _ in range(self.n_terminals)
179179
]
180180

181+
for terminal in self.terminals:
182+
terminal.fill_with_uniform()
183+
181184
def _create_trees(self) -> None:
182185
"""Creates a list of trees based on the GROW algorithm."""
183186

@@ -203,12 +206,6 @@ def _initialize_agents(self) -> None:
203206

204207
self.best_agent = copy.deepcopy(self.agents[0])
205208

206-
def _initialize_terminals(self) -> None:
207-
"""Initializes terminals with their positions."""
208-
209-
for terminal in self.terminals:
210-
terminal.fill_with_uniform()
211-
212209
def grow(self, min_depth: Optional[int] = 1, max_depth: Optional[int] = 3) -> Node:
213210
"""Creates a random tree based on the GROW algorithm.
214211
@@ -225,8 +222,6 @@ def grow(self, min_depth: Optional[int] = 1, max_depth: Optional[int] = 3) -> No
225222
226223
"""
227224

228-
self._initialize_terminals()
229-
230225
if min_depth == max_depth:
231226
terminal_id = r.generate_integer_random_number(0, self.n_terminals)
232227

tests/opytimizer/spaces/test_tree.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,6 @@ def test_tree_space_initialize_agents():
150150
assert new_tree_space.agents[0].position[0] != 0
151151

152152

153-
def test_tree_space_initialize_terminals():
154-
new_tree_space = tree.TreeSpace(1, 1, 0, 1)
155-
156-
assert new_tree_space.terminals[0].position[0] != 0
157-
158-
159153
def test_tree_space_grow():
160154
new_tree_space = tree.TreeSpace(
161155
1, 1, 0, 1, min_depth=1, max_depth=5, functions=["SUM", "SUB", "MUL", "DIV"]

0 commit comments

Comments
 (0)