# MIT License
#
# Copyright (c) 2024 DALabNOVA
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
Tree class implementation for representing tree structures in genetic programming.
"""
from slim_gsgp.algorithms.GP.representations.tree_utils import bound_value, flatten, tree_depth, _execute_tree
import torch
[docs]
class Tree:
"""
The Tree class representing the candidate solutions in genetic programming.
Attributes
----------
repr_ : tuple or str
Representation of the tree structure.
FUNCTIONS : dict
Dictionary of allowed functions in the tree representation.
TERMINALS : dict
Dictionary of terminal symbols allowed in the tree representation.
CONSTANTS : dict
Dictionary of constant values allowed in the tree representation.
depth : int
Depth of the tree.
fitness : float
Fitness value of the tree.
test_fitness : float
Test fitness value of the tree.
node_count : int
Number of nodes in the tree.
"""
TERMINALS = None
FUNCTIONS = None
CONSTANTS = None
def __init__(self, repr_):
"""
Initializes a Tree object.
Parameters
----------
repr_ : tuple
Representation of the tree structure.
"""
self.FUNCTIONS = Tree.FUNCTIONS
self.TERMINALS = Tree.TERMINALS
self.CONSTANTS = Tree.CONSTANTS
self.repr_ = repr_
self.depth = tree_depth(Tree.FUNCTIONS)(repr_)
self.fitness = None
self.test_fitness = None
self.node_count = len(list(flatten(self.repr_)))
[docs]
def apply_tree(self, inputs):
"""
Evaluates the tree on input vectors.
Parameters
----------
inputs : tuple
Input vectors.
Returns
-------
float
Output of the evaluated tree.
"""
return _execute_tree(
repr_=self.repr_,
X=inputs,
FUNCTIONS=self.FUNCTIONS,
TERMINALS=self.TERMINALS,
CONSTANTS=self.CONSTANTS
)
[docs]
def evaluate(self, ffunction, X, y, testing=False, new_data = False):
"""
Evaluates the tree given a fitness function, input data (X), and target data (y).
Parameters
----------
ffunction : function
Fitness function to evaluate the individual.
X : torch.Tensor
The input data (which can be training or testing).
y : torch.Tensor
The expected output (target) values.
testing : bool, optional
Flag indicating if the data is testing data. Default is False.
new_data : bool, optional
Flag indicating that the input data is new and the model is being used outside the training process.
Returns
-------
None
If the data is training or testing data, the fitness value is attributed to the individual.
float
If exposed to new data, the fitness value is returned.
"""
# getting the predictions (i.e., semantics) of the individual
preds = self.apply_tree(X)
# if new (testing data) is being used, return the fitness value for this new data
if new_data:
return float(ffunction(y, preds))
# if not, attribute the fitness value to the individual
else:
if testing:
self.test_fitness = ffunction(y, preds)
else:
self.fitness = ffunction(y, preds)
[docs]
def predict(self, X):
"""
Predict the tree semantics (output) for the given input data.
Parameters
----------
X : torch.Tensor
The input data to predict.
Returns
-------
torch.Tensor
The predicted output for the input data.
Notes
-----
This function delegates the actual prediction task to the `apply_tree` method,
which is assumed to be another method in the same class. The `apply_tree` method
should be defined to handle the specifics of how predictions are made based on
the tree structure used in this model.
"""
return self.apply_tree(X)
[docs]
def get_tree_representation(self, indent=""):
"""
Returns the tree representation as a string with indentation.
Parameters
----------
indent : str, optional
Indentation for tree structure representation. Default is an empty string.
Returns
-------
str
Returns the tree representation with the chosen indentation.
"""
representation = []
if isinstance(self.repr_, tuple): # If it's a function node
function_name = self.repr_[0]
representation.append(indent + f"{function_name}(\n")
# if the function has an arity of 2, process both left and right subtrees
if Tree.FUNCTIONS[function_name]["arity"] == 2:
left_subtree, right_subtree = self.repr_[1], self.repr_[2]
representation.append(Tree(left_subtree).get_tree_representation(indent + " "))
representation.append(Tree(right_subtree).get_tree_representation(indent + " "))
# if the function has an arity of 1, process the left subtree
else:
left_subtree = self.repr_[1]
representation.append(Tree(left_subtree).get_tree_representation(indent + " "))
representation.append(indent + ")\n")
else: # If it's a terminal node
representation.append(indent + f"{self.repr_}\n")
return "".join(representation)
[docs]
def print_tree_representation(self, indent=""):
"""
Prints the tree representation with indentation.
Parameters
----------
indent : str, optional
Indentation for tree structure representation. Default is an empty string.
Returns
-------
None
Prints the tree representation as a string with indentation.
"""
print(self.get_tree_representation(indent=indent))