diff --git a/docs/source/intro.rst b/docs/source/intro.rst index e670ba81..71911310 100644 --- a/docs/source/intro.rst +++ b/docs/source/intro.rst @@ -7,7 +7,7 @@ Project Background ------------------ mlrose was initially developed to support students of Georgia Tech's OMSCS/OMSA offering of CS 7641: Machine Learning. -It includes implementations of all randomized optimization algorithms taught in this course, as well as functionality to apply these algorithms to integer-string optimization problems, such as N-Queens and the Knapsack problem; continuous-valued optimization problems, such as the neural network weight problem; and tour optimization problems, such as the Travelling Salesperson problem. It also has the flexibility to solve user-defined optimization problems. +It includes implementations of all randomized optimization algorithms taught in this course, as well as functionality to apply these algorithms to integer-string optimization problems, such as N-Queens and the Knapsack problem; continuous-valued optimization problems, such as the neural network weight problem; and tour optimization problems, such as the Travelling Salesperson problem. It also has the flexibility to solve user-defined optimization problems. At the time of development, there did not exist a single Python package that collected all of this functionality together in the one location. diff --git a/docs/source/tutorial2.rst b/docs/source/tutorial2.rst index 8ad95d87..2a8aea24 100644 --- a/docs/source/tutorial2.rst +++ b/docs/source/tutorial2.rst @@ -159,6 +159,6 @@ This solution is illustrated below and can be shown to be the optimal solution t Summary ------- -In this tutorial we introduced the travelling salesperson problem, and discussed how mlrose can be used to efficiently solve this problem. This is an example of how mlrose caters to solving one very specific type of optimization problem. +In this tutorial we introduced the travelling salesperson problem, and discussed how mlrose can be used to efficiently solve this problem. This is an example of how mlrose caters to solving one very specific type of optimization problem. Another very specific type of optimization problem mlrose caters to solving is the machine learning weight optimization problem. That is, the problem of finding the optimal weights for machine learning models such as neural networks and regression models. We will discuss how mlrose can be used to solve this problem next, in our third and final tutorial. \ No newline at end of file diff --git a/mlrose_hiive/__init__.py b/mlrose_hiive/__init__.py index 907e5d10..84bc7b65 100644 --- a/mlrose_hiive/__init__.py +++ b/mlrose_hiive/__init__.py @@ -13,7 +13,7 @@ from .algorithms.crossovers import OnePointCrossover, UniformCrossover, TSPCrossover from .algorithms.mutators import SingleGeneMutator, DiscreteGeneMutator, GeneSwapMutator, SingleShiftMutator -from .fitness import OneMax, FlipFlop, FourPeaks, SixPeaks, ContinuousPeaks, Knapsack, TravellingSales, Queens, MaxKColor, CustomFitness +from .fitness import OneMax, FlipFlop, FourPeaks, SixPeaks, ContinuousPeaks, Knapsack, TravellingSalesperson, Queens, MaxKColor, CustomFitness from .neural import NeuralNetwork, LinearRegression, LogisticRegression, NNClassifier, nn_core from .neural.activation import identity, relu, leaky_relu, sigmoid, softmax, tanh diff --git a/mlrose_hiive/algorithms/crossovers/tsp_crossover.py b/mlrose_hiive/algorithms/crossovers/tsp_crossover.py index 770e7fc6..295b891f 100644 --- a/mlrose_hiive/algorithms/crossovers/tsp_crossover.py +++ b/mlrose_hiive/algorithms/crossovers/tsp_crossover.py @@ -16,7 +16,7 @@ class TSPCrossover(_CrossoverBase): """ - Crossover operation tailored for the Traveling Salesman Problem (TSP) in genetic algorithms. + Crossover operation tailored for the Travelling Salesperson Problem (TSP) in genetic algorithms. Implements specific crossover techniques that ensure valid TSP routes in the offspring. The crossover handles distinct city sequences without repetitions and uses specialized diff --git a/mlrose_hiive/algorithms/mimic.py b/mlrose_hiive/algorithms/mimic.py index fdf3aa4e..d1459c34 100644 --- a/mlrose_hiive/algorithms/mimic.py +++ b/mlrose_hiive/algorithms/mimic.py @@ -72,7 +72,7 @@ def mimic(problem: Any, De Bonet, J., C. Isbell, and P. Viola (1997). MIMIC: Finding Optima by Estimating Probability Densities. In *Advances in Neural Information Processing Systems* (NIPS) 9, pp. 424–430. """ - if problem.get_prob_type() == 'continuous': + if problem.get_problem_type() == 'continuous': raise ValueError("problem type must be discrete or tsp.") if not isinstance(pop_size, int) or pop_size < 0: raise ValueError(f"pop_size must be a positive integer. Got {pop_size}") diff --git a/mlrose_hiive/fitness/__init__.py b/mlrose_hiive/fitness/__init__.py index 4ba78d81..4263d3f2 100644 --- a/mlrose_hiive/fitness/__init__.py +++ b/mlrose_hiive/fitness/__init__.py @@ -1,12 +1,21 @@ +"""Classes for defining fitness functions (i.e., optimization problems) for optimization algorithms.""" + from .continuous_peaks import ContinuousPeaks + from .flip_flop import FlipFlop + from .four_peaks import FourPeaks -from .six_peaks import SixPeaks -from .continuous_peaks import ContinuousPeaks -from .one_max import OneMax -from .max_k_color import MaxKColor + from .knapsack import Knapsack + +from .max_k_color import MaxKColor + +from .one_max import OneMax + from .queens import Queens -from .travelling_sales import TravellingSales + +from .six_peaks import SixPeaks + +from .travelling_salesperson import TravellingSalesperson from .custom_fitness import CustomFitness diff --git a/mlrose_hiive/fitness/discrete_peaks_base.py b/mlrose_hiive/fitness/_discrete_peaks_base.py similarity index 89% rename from mlrose_hiive/fitness/discrete_peaks_base.py rename to mlrose_hiive/fitness/_discrete_peaks_base.py index e3b0b82a..7f604895 100644 --- a/mlrose_hiive/fitness/discrete_peaks_base.py +++ b/mlrose_hiive/fitness/_discrete_peaks_base.py @@ -1,10 +1,10 @@ -"""Classes for defining fitness functions.""" +"""Class defining the Discrete Peaks base fitness function for use with the Four Peaks, Six Peaks, and Custom fitness functions.""" # Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) # License: BSD 3 clause -class DiscretePeaksBase: +class _DiscretePeaksBase: @staticmethod def head(_b, _x): diff --git a/mlrose_hiive/fitness/continuous_peaks.py b/mlrose_hiive/fitness/continuous_peaks.py index 396b5d75..35cddbb5 100644 --- a/mlrose_hiive/fitness/continuous_peaks.py +++ b/mlrose_hiive/fitness/continuous_peaks.py @@ -1,4 +1,4 @@ -"""Classes for defining fitness functions.""" +"""Class defining the Continuous Peaks fitness function for use with optimization algorithms.""" # Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) # License: BSD 3 clause @@ -7,126 +7,125 @@ class ContinuousPeaks: - """Fitness function for Continuous Peaks optimization problem. Evaluates - the fitness of an n-dimensional state vector :math:`x`, given parameter T, - as: - - .. math:: - - Fitness(x, T) = \\max(max\\_run(0, x), max\\_run(1, x)) + R(x, T) - - where: - - * :math:`max\\_run(b, x)` is the length of the maximum run of b's - in :math:`x`; - * :math:`R(x, T) = n`, if (:math:`max\\_run(0, x) > T` and - :math:`max\\_run(1, x) > T`); and - * :math:`R(x, T) = 0`, otherwise. + """ + Fitness function for Continuous Peaks optimization problem. Evaluates the fitness + of an n-dimensional state vector `x`, given parameter T. Parameters ---------- - t_pct: float, default: 0.1 - Threshold parameter (T) for Continuous Peaks fitness function, - expressed as a percentage of the state space dimension, n (i.e. - :math:`T = t_{pct} \\times n`). + threshold_percentage : float, default=0.1 + Threshold parameter (T) for Continuous Peaks fitness function, expressed as a + percentage of the state space dimension, n (i.e., `T = threshold_percentage * n`). + + Attributes + ---------- + threshold_percentage : float + The threshold percentage for the fitness function. + problem_type : str + Specifies problem type as 'discrete'. Examples - ------- - >>> import mlrose_hiive + -------- >>> import numpy as np - >>> fitness = mlrose_hiive.ContinuousPeaks(t_pct=0.15) + >>> fitness = ContinuousPeaks(threshold_percentage=0.15) >>> state = np.array([0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1]) >>> fitness.evaluate(state) 17 Note ---- - The Continuous Peaks fitness function is suitable for use in bit-string - (discrete-state with :code:`max_val = 2`) optimization problems *only*. + The Continuous Peaks fitness function is suitable for use in bit-string (discrete-state + with `max_val = 2`) optimization problems only. """ - def __init__(self, t_pct=0.1): + def __init__(self, threshold_percentage: float = 0.1) -> None: + self.threshold_percentage: float = threshold_percentage + self.problem_type: str = 'discrete' - self.t_pct = t_pct - self.prob_type = 'discrete' + if not (0 <= self.threshold_percentage <= 1): + raise ValueError(f"threshold_percentage must be between 0 and 1, got {self.threshold_percentage} instead.") - if (self.t_pct < 0) or (self.t_pct > 1): - raise Exception("""t_pct must be between 0 and 1.""") - - def evaluate(self, state): - """Evaluate the fitness of a state vector. + def evaluate(self, state: np.ndarray) -> float: + """ + Evaluate the fitness of a state vector. Parameters ---------- - state: np.ndarray + state : np.ndarray State array for evaluation. Returns ------- - fitness: float - Value of fitness function. + float + Value of the fitness function. """ - _n = len(state) - _t = np.ceil(self.t_pct*_n) + num_elements = len(state) + threshold = int(np.ceil(self.threshold_percentage * num_elements)) - # Calculate length of maximum runs of 0's and 1's - max_0 = self.max_run(0, state) - max_1 = self.max_run(1, state) + max_zeros = self._max_run(0, state) + max_ones = self._max_run(1, state) - # Calculate R(X, T) - if max_0 > _t and max_1 > _t: - _r = _n - else: - _r = 0 - - # Evaluate function - fitness = max(max_0, max_1) + _r + reward = num_elements if max_zeros > threshold and max_ones > threshold else 0 + fitness = max(max_zeros, max_ones) + reward return fitness - def get_prob_type(self): - """ Return the problem type. + def get_problem_type(self) -> str: + """ + Return the problem type. Returns ------- - self.prob_type: string - Specifies problem type as 'discrete', 'continuous', 'tsp' - or 'either'. + str + Specifies problem type as 'discrete'. """ - return self.prob_type + return self.problem_type @staticmethod - def max_run(_b, _x): - """Determine the length of the maximum run of b's in vector x. + def _max_run(value: int, vector: np.ndarray) -> int: + """ + Determine the length of the maximum run of a given value in a vector. Parameters ---------- - _b: int - Integer for counting. - - _x: np.ndarray + value : int + Value to count. + vector : np.ndarray Vector of integers. Returns ------- - max: int - Length of maximum run of b's. + int + Length of the maximum run of the given value. """ - # Initialize counter - _max = 0 - run = 0 + # Create a boolean array where each element is True if it equals the given value + is_value = np.array(vector == value) + + # If the value does not exist in the vector, return 0 + if not np.any(is_value): + return 0 + + # Calculate the differences between consecutive elements in the boolean array + diffs = np.diff(is_value.astype(int)) + + # Find the indices where the value starts and ends + run_starts = np.where(diffs == 1)[0] + 1 + run_ends = np.where(diffs == -1)[0] + 1 + + # If the run starts at the beginning of the vector, include the first index + if is_value[0]: + run_starts = np.insert(run_starts, 0, 0) - # Iterate through values in vector - for i in _x: - if i == _b: - run += 1 - else: - if run > _max: - _max = run + # If the run ends at the end of the vector, include the last index + if is_value[-1]: + run_ends = np.append(run_ends, len(vector)) - run = 0 + # Ensure that run_ends has the same length as run_starts + if len(run_starts) > len(run_ends): + run_ends = np.append(run_ends, len(vector)) - if (_x[-1] == _b) and (run > _max): - _max = run + # Calculate the lengths of the runs + run_lengths = run_ends - run_starts - return _max + # Return the maximum run length, or 0 if no runs are found + return run_lengths.max() if run_lengths.size > 0 else 0 diff --git a/mlrose_hiive/fitness/custom_fitness.py b/mlrose_hiive/fitness/custom_fitness.py index c00a82bb..74afaadb 100644 --- a/mlrose_hiive/fitness/custom_fitness.py +++ b/mlrose_hiive/fitness/custom_fitness.py @@ -1,4 +1,4 @@ -"""Classes for defining fitness functions.""" +"""Class defining a customizable fitness function for use with optimization algorithms.""" # Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) # License: BSD 3 clause @@ -15,7 +15,7 @@ class CustomFitness: Function for calculating fitness of a state with the signature :code:`fitness_fn(state, **kwargs)`. - problem_type: string, default: 'either' + problem_type: str, default: 'either' Specifies problem type as 'discrete', 'continuous', 'tsp' or 'either' (denoting either discrete or continuous). @@ -58,12 +58,12 @@ def evaluate(self, state): fitness = self.fitness_fn(state, **self.kwargs) return fitness - def get_prob_type(self): + def get_problem_type(self): """ Return the problem type. Returns ------- - self.prob_type: string + self.prob_type: str Specifies problem type as 'discrete', 'continuous', 'tsp' or 'either'. """ diff --git a/mlrose_hiive/fitness/flip_flop.py b/mlrose_hiive/fitness/flip_flop.py index 14f6e254..cf77a942 100644 --- a/mlrose_hiive/fitness/flip_flop.py +++ b/mlrose_hiive/fitness/flip_flop.py @@ -1,4 +1,4 @@ -"""Classes for defining fitness functions.""" +"""Class defining the Flip Flop fitness function for use with optimization algorithms.""" # Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) # License: BSD 3 clause @@ -76,12 +76,12 @@ def evaluate_many(states): return fitness - def get_prob_type(self): + def get_problem_type(self): """ Return the problem type. Returns ------- - self.prob_type: string + self.prob_type: str Specifies problem type as 'discrete', 'continuous', 'tsp' or 'either'. """ diff --git a/mlrose_hiive/fitness/four_peaks.py b/mlrose_hiive/fitness/four_peaks.py index d57493c1..cf02d234 100644 --- a/mlrose_hiive/fitness/four_peaks.py +++ b/mlrose_hiive/fitness/four_peaks.py @@ -1,14 +1,14 @@ -"""Classes for defining fitness functions.""" +"""Class defining the Four Peaks fitness function for use with optimization algorithms.""" # Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) # License: BSD 3 clause import numpy as np -from mlrose_hiive.fitness.discrete_peaks_base import DiscretePeaksBase +from mlrose_hiive.fitness._discrete_peaks_base import _DiscretePeaksBase -class FourPeaks(DiscretePeaksBase): +class FourPeaks(_DiscretePeaksBase): """Fitness function for Four Peaks optimization problem. Evaluates the fitness of an n-dimensional state vector :math:`x`, given parameter T, as: @@ -91,12 +91,12 @@ def evaluate(self, state): return fitness - def get_prob_type(self): + def get_problem_type(self): """ Return the problem type. Returns ------- - self.prob_type: string + self.prob_type: str Specifies problem type as 'discrete', 'continuous', 'tsp' or 'either'. """ diff --git a/mlrose_hiive/fitness/knapsack.py b/mlrose_hiive/fitness/knapsack.py index b40afca3..1b165a91 100644 --- a/mlrose_hiive/fitness/knapsack.py +++ b/mlrose_hiive/fitness/knapsack.py @@ -1,4 +1,4 @@ -"""Classes for defining fitness functions.""" +"""Class defining the Knapsack fitness function for use with optimization algorithms.""" # Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) # License: BSD 3 clause @@ -107,12 +107,12 @@ def evaluate(self, state): return fitness - def get_prob_type(self): + def get_problem_type(self): """ Return the problem type. Returns ------- - self.prob_type: string + self.prob_type: str Specifies problem type as 'discrete', 'continuous', 'tsp' or 'either'. """ diff --git a/mlrose_hiive/fitness/max_k_color.py b/mlrose_hiive/fitness/max_k_color.py index a28157aa..e8b2ccb1 100644 --- a/mlrose_hiive/fitness/max_k_color.py +++ b/mlrose_hiive/fitness/max_k_color.py @@ -1,5 +1,4 @@ -"""Classes for defining fitness functions.""" - +"""Class defining the Max-K Color fitness function for use with optimization algorithms.""" # Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) # License: BSD 3 clause @@ -79,12 +78,12 @@ def evaluate(self, state): # Minimize the number of adjacent nodes of the same color. return sum(int(state[n1] == state[n2]) for (n1, n2) in edges) - def get_prob_type(self): + def get_problem_type(self): """ Return the problem type. Returns ------- - self.prob_type: string + self.prob_type: str Specifies problem type as 'discrete', 'continuous', 'tsp' or 'either'. """ diff --git a/mlrose_hiive/fitness/one_max.py b/mlrose_hiive/fitness/one_max.py index e38c7983..f1adac8d 100644 --- a/mlrose_hiive/fitness/one_max.py +++ b/mlrose_hiive/fitness/one_max.py @@ -1,4 +1,4 @@ -"""Classes for defining fitness functions.""" +"""Class defining the One Max fitness function for use with optimization algorithms.""" # Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) # License: BSD 3 clause @@ -52,12 +52,12 @@ def evaluate(state): fitness = np.sum(state) return fitness - def get_prob_type(self): + def get_problem_type(self): """ Return the problem type. Returns ------- - self.prob_type: string + self.prob_type: str Specifies problem type as 'discrete', 'continuous', 'tsp' or 'either'. """ diff --git a/mlrose_hiive/fitness/queens.py b/mlrose_hiive/fitness/queens.py index b367bb37..5a6a917a 100644 --- a/mlrose_hiive/fitness/queens.py +++ b/mlrose_hiive/fitness/queens.py @@ -1,4 +1,4 @@ -"""Classes for defining fitness functions.""" +"""Class defining the N-Queens fitness function for use with optimization algorithms.""" # Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) # License: BSD 3 clause @@ -86,12 +86,12 @@ def evaluate(self, state): fitness = self.get_max_size(ls) - fitness return fitness - def get_prob_type(self): + def get_problem_type(self): """ Return the problem type. Returns ------- - self.prob_type: string + self.prob_type: str Specifies problem type as 'discrete', 'continuous', 'tsp' or 'either'. """ diff --git a/mlrose_hiive/fitness/six_peaks.py b/mlrose_hiive/fitness/six_peaks.py index 2ec14d18..32432562 100644 --- a/mlrose_hiive/fitness/six_peaks.py +++ b/mlrose_hiive/fitness/six_peaks.py @@ -1,14 +1,14 @@ -"""Classes for defining fitness functions.""" +"""Class defining the Six Peaks fitness function for use with optimization algorithms.""" # Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) # License: BSD 3 clause import numpy as np -from mlrose_hiive.fitness.discrete_peaks_base import DiscretePeaksBase +from mlrose_hiive.fitness._discrete_peaks_base import _DiscretePeaksBase -class SixPeaks(DiscretePeaksBase): +class SixPeaks(_DiscretePeaksBase): """Fitness function for Six Peaks optimization problem. Evaluates the fitness of an n-dimensional state vector :math:`x`, given parameter T, as: @@ -91,12 +91,12 @@ def evaluate(self, state): # Evaluate function return _max_score + _r - def get_prob_type(self): + def get_problem_type(self): """ Return the problem type. Returns ------- - self.prob_type: string + self.prob_type: str Specifies problem type as 'discrete', 'continuous', 'tsp' or 'either'. """ diff --git a/mlrose_hiive/fitness/travelling_sales.py b/mlrose_hiive/fitness/travelling_sales.py deleted file mode 100644 index 305e530a..00000000 --- a/mlrose_hiive/fitness/travelling_sales.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Classes for defining fitness functions.""" - -# Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) -# License: BSD 3 clause - -import numpy as np -import pandas as pd - - -class TravellingSales: - """Fitness function for Travelling Salesman optimization problem. - Evaluates the fitness of a tour of n nodes, represented by state vector - :math:`x`, giving the order in which the nodes are visited, as the total - distance travelled on the tour (including the distance travelled between - the final node in the state vector and the first node in the state vector - during the return leg of the tour). Each node must be visited exactly - once for a tour to be considered valid. - - Parameters - ---------- - coords: list of pairs, default: None - Ordered list of the (x, y) coordinates of all nodes (where element i - gives the coordinates of node i). This assumes that travel between - all pairs of nodes is possible. If this is not the case, then use - :code:`distances` instead. - - distances: list of triples, default: None - List giving the distances, d, between all pairs of nodes, u and v, for - which travel is possible, with each list item in the form (u, v, d). - Order of the nodes does not matter, so (u, v, d) and (v, u, d) are - considered to be the same. If a pair is missing from the list, it is - assumed that travel between the two nodes is not possible. This - argument is ignored if coords is not :code:`None`. - - Examples - -------- - >>> import mlrose_hiive - >>> import numpy as np - >>> coords = [(0, 0), (3, 0), (3, 2), (2, 4), (1, 3)] - >>> dists = [(0, 1, 3), (0, 2, 5), (0, 3, 1), (0, 4, 7), (1, 3, 6), (4, 1, 9), (2, 3, 8), (2, 4, 2), (3, 2, 8), (3, 4, 4)] - >>> fitness_coords = mlrose_hiive.TravellingSales(coords=coords) - >>> state = np.array([0, 1, 4, 3, 2]) - >>> fitness_coords.evaluate(state) - 13.861384090800865 - - >>> fitness_dists = mlrose_hiive.TravellingSales(distances=dists) - >>> fitness_dists.evaluate(state) - 29 - - Note - ---- - 1. The TravellingSales fitness function is suitable for use in travelling - salesperson (tsp) optimization problems *only*. - 2. It is necessary to specify at least one of :code:`coords` and - :code:`distances` in initializing a TravellingSales fitness function - object. - """ - - def __init__(self, coords=None, distances=None): - - if coords is None and distances is None: - raise Exception("""At least one of coords and distances must be""" - + """ specified.""") - - elif coords is not None: - self.is_coords = True - path_list = [] - dist_list = [] - - else: - self.is_coords = False - - # Remove any duplicates from list - distances = list({tuple(sorted(dist[0:2]) + [dist[2]]) - for dist in distances}) - - # Split into separate lists - node1_list, node2_list, dist_list = zip(*distances) - - if min(dist_list) <= 0: - raise Exception("""The distance between each pair of nodes""" - + """ must be greater than 0.""") - if min(node1_list + node2_list) < 0: - raise Exception("""The minimum node value must be 0.""") - - if not max(node1_list + node2_list) == \ - (len(set(node1_list + node2_list)) - 1): - raise Exception("""All nodes must appear at least once in""" - + """ distances.""") - - path_list = list(zip(node1_list, node2_list)) - - self.coords = coords - self.distances = distances - self.path_list = path_list - self.dist_list = dist_list - self.prob_type = 'tsp' - if self.coords: - self.calculate_fitness = self.__calculate_fitness_by_coords - else: - self.df_path_list = pd.DataFrame([[ - self.path_list[i][0], - self.path_list[i][1], - self.dist_list[i]] for i in range(len(self.path_list))]) - self.calculate_fitness = self.__calculate_fitness_by_distance - - def evaluate(self, state): - """Evaluate the fitness of a state vector. - - Parameters - ---------- - state: np.ndarray - State array for evaluation. Each integer between 0 and - (len(state) - 1), inclusive must appear exactly once in the array. - - Returns - ------- - fitness: float - Value of fitness function. Returns :code:`np.inf` if travel between - two consecutive nodes on the tour is not possible. - """ - - if self.is_coords and len(state) != len(self.coords): - raise Exception("""state must have the same length as coords.""") - - if not len(state) == len(set(state)): - raise Exception("""Each node must appear exactly once in state.""") - - if min(state) < 0: - raise Exception("""All elements of state must be non-negative""" - + """ integers.""") - - if max(state) >= len(state): - raise Exception("""All elements of state must be less than""" - + """ len(state).""") - - return self.calculate_fitness(state) - - def __calculate_fitness_by_coords(self, state): - # Calculate length of journey - ls = len(state) - nodes = np.array([self.coords[state[i]] for i in range(ls)] + [self.coords[state[0]]]) - nodes.reshape((2, nodes.size // 2)) - fitness = np.linalg.norm(nodes[1:] - nodes[:-1], axis=1).sum() - - return fitness - - def __calculate_fitness_by_distance(self, state): - - ls = len(state) - - nodes = np.array([[state[i-1], state[i]] for i in range(1, ls)] + [[state[ls-1]] + [state[0]]]) - nodes.sort(axis=1) - df_nodes = pd.merge(self.df_path_list, pd.DataFrame(nodes)) - if df_nodes.shape[0] != nodes.shape[0]: - return np.inf - fitness = df_nodes.iloc[:, 2].sum() - return fitness - - def get_prob_type(self): - """ Return the problem type. - - Returns - ------- - self.prob_type: string - Specifies problem type as 'discrete', 'continuous', 'tsp' - or 'either'. - """ - return self.prob_type diff --git a/mlrose_hiive/fitness/travelling_salesperson.py b/mlrose_hiive/fitness/travelling_salesperson.py new file mode 100644 index 00000000..768696a5 --- /dev/null +++ b/mlrose_hiive/fitness/travelling_salesperson.py @@ -0,0 +1,183 @@ +"""Class defining the Travelling Salesperson fitness function for use with optimization algorithms.""" + +# Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura) +# License: BSD 3 clause + +import numpy as np +from typing import Callable + +class TravellingSalesperson: + """ + Fitness function for the Travelling Salesperson optimization problem. + + Evaluates the fitness of a tour of n nodes, represented by state vector x, giving the order in which + the nodes are visited, as the total distance travelled on the tour (including the distance travelled + between the final node in the state vector and the first node in the state vector during the return + leg of the tour). Each node must be visited exactly once for a tour to be considered valid. + + Parameters + ---------- + coords : list of tuple[float, float] | None, optional + Ordered list of the (x, y) coordinates of all nodes (where element i gives the coordinates of node i). + This assumes that travel between all pairs of nodes is possible. If this is not the case, then use + distances instead. + + distances : list of tuple[int, int, float] | None, optional + List giving the distances, d, between all pairs of nodes, u and v, for which travel is possible, with each + list item in the form (u, v, d). Order of the nodes does not matter, so (u, v, d) and (v, u, d) are + considered to be the same. If a pair is missing from the list, it is assumed that travel between the two nodes + is not possible. This argument is ignored if coords is not None. + + Examples + -------- + >>> import mlrose_hiive + >>> import numpy as np + >>> coords = [(0, 0), (3, 0), (3, 2), (2, 4), (1, 3)] + >>> dists = [(0, 1, 3), (0, 2, 5), (0, 3, 1), (0, 4, 7), (1, 3, 6), (4, 1, 9), (2, 3, 8), (2, 4, 2), (3, 2, 8), (3, 4, 4)] + >>> fitness_coords = mlrose_hiive.TravellingSalesperson(coords=coords) + >>> state = np.array([0, 1, 4, 3, 2]) + >>> fitness_coords.evaluate(state) + 13.861384090800865 + + >>> fitness_dists = mlrose_hiive.TravellingSalesperson(distances=dists) + >>> fitness_dists.evaluate(state) + 29 + + Note + ---- + 1. The TravellingSalesperson fitness function is suitable for use in travelling salesperson (tsp) optimization + problems only. + 2. It is necessary to specify at least one of coords and distances in initializing a TravellingSalesperson + fitness function object. + """ + + def __init__(self, coords: list[tuple[float, float]] = None, distances: list[tuple[int, int, float]] = None) -> None: + # Ensure that at least one of coords or distances is provided + if coords is None and distances is None: + raise ValueError("At least one of coords and distances must be specified.") + + # Initialize class variables + self.prob_type: str = 'tsp' + self.coords: list = coords + self.distances: list = distances + self.is_coords: bool = coords is not None + + # Determine which fitness calculation method to use + self.calculate_fitness: Callable = self._calculate_fitness_by_coords if self.is_coords else self._calculate_fitness_by_distance + + if self.is_coords: + # Precompute the coordinates array for faster access + self.coords_array: np.ndarray = np.array(coords) + else: + # Remove duplicates and sort distances + self.distances = list({tuple(sorted((u, v)) + [d]) for u, v, d in distances}) + + # Unpack node lists and distances + node1_list, node2_list, self.dist_list = zip(*self.distances) + + # Validation checks on distances + if min(self.dist_list) <= 0: + raise ValueError("The distance between each pair of nodes must be greater than 0.") + if min(node1_list + node2_list) < 0: + raise ValueError("The minimum node value must be 0.") + if not max(node1_list + node2_list) == (len(set(node1_list + node2_list)) - 1): + raise ValueError("All nodes must appear at least once in distances.") + + # Create a distance matrix for quick lookup of distances between nodes + num_nodes = max(max(node1_list), max(node2_list)) + 1 + self.distance_matrix = np.full((num_nodes, num_nodes), np.inf) + for u, v, d in self.distances: + self.distance_matrix[u, v] = d + self.distance_matrix[v, u] = d + + def evaluate(self, state: np.ndarray) -> float: + """ + Evaluate the fitness of a state vector. + + Parameters + ---------- + state : np.ndarray + State array for evaluation. Each integer between 0 and (len(state) - 1), inclusive must appear exactly once in the array. + + Returns + ------- + fitness : float + Value of fitness function. Returns np.inf if travel between two consecutive nodes on the tour is not possible. + """ + # Validation checks on the state array + if self.is_coords and len(state) != len(self.coords): + raise ValueError("state must have the same length as coords.") + if not len(state) == len(set(state)): + raise ValueError("Each node must appear exactly once in state.") + if min(state) < 0: + raise ValueError("All elements of state must be non-negative integers.") + if max(state) >= len(state): + raise ValueError("All elements of state must be less than len(state).") + + # Calculate and return the fitness of the state + return self.calculate_fitness(state) + + def _calculate_fitness_by_coords(self, state: np.ndarray) -> float: + """ + Calculate fitness based on coordinates. + + Parameters + ---------- + state : np.ndarray + State array for evaluation. + + Returns + ------- + fitness : float + Calculated fitness value. + """ + # Map state indices to coordinates + nodes = self.coords_array[state] + + # Calculate total journey distance using Euclidean distance + journey = np.linalg.norm(nodes[1:] - nodes[:-1], axis=1).sum() + journey += np.linalg.norm(nodes[0] - nodes[-1]) + + return journey + + def _calculate_fitness_by_distance(self, state: np.ndarray) -> float: + """ + Calculate fitness based on distances. + + Parameters + ---------- + state : np.ndarray + State array for evaluation. + + Returns + ------- + fitness : float + Calculated fitness value. Returns np.inf if any segment of the tour is not possible. + """ + total_distance = 0.0 + num_nodes = len(state) + + # Iterate over each node in the state + for i in range(num_nodes): + start = state[i] + end = state[(i + 1) % num_nodes] + distance = self.distance_matrix[start, end] + + # Check if the segment is possible + if np.isinf(distance): + return np.inf + + total_distance += distance + + return total_distance + + def get_problem_type(self) -> str: + """ + Return the problem type. + + Returns + ------- + prob_type : str + Specifies problem type as 'tsp'. + """ + return self.prob_type diff --git a/mlrose_hiive/generators/continuous_peaks_generator.py b/mlrose_hiive/generators/continuous_peaks_generator.py index 264f5e25..fbf8cc40 100644 --- a/mlrose_hiive/generators/continuous_peaks_generator.py +++ b/mlrose_hiive/generators/continuous_peaks_generator.py @@ -12,6 +12,6 @@ class ContinuousPeaksGenerator: @staticmethod def generate(seed, size=20, t_pct=0.1): np.random.seed(seed) - fitness = ContinuousPeaks(t_pct=t_pct) + fitness = ContinuousPeaks(threshold_percentage=t_pct) problem = DiscreteOpt(length=size, fitness_fn=fitness) return problem diff --git a/mlrose_hiive/neural/fitness/network_weights.py b/mlrose_hiive/neural/fitness/network_weights.py index cbea550b..9227a28d 100644 --- a/mlrose_hiive/neural/fitness/network_weights.py +++ b/mlrose_hiive/neural/fitness/network_weights.py @@ -158,12 +158,12 @@ def get_output_activation(self): """ return self.output_activation - def get_prob_type(self): + def get_problem_type(self): """ Return the problem type. Returns ------- - self.prob_type: string + self.prob_type: str Specifies problem type as 'discrete', 'continuous', 'tsp', or 'either'. """ diff --git a/mlrose_hiive/neural/linear_regression.py b/mlrose_hiive/neural/linear_regression.py index a5a0ce5d..8d4c9fcf 100644 --- a/mlrose_hiive/neural/linear_regression.py +++ b/mlrose_hiive/neural/linear_regression.py @@ -17,7 +17,7 @@ class LinearRegression(_NNCore, RegressorMixin): Parameters ---------- - algorithm: string, default: 'random_hill_climb' + algorithm: str, default: 'random_hill_climb' Algorithm used to find optimal network weights. Must be one of:'random_hill_climb', 'simulated_annealing', 'genetic_alg' or 'gradient_descent'. diff --git a/mlrose_hiive/neural/logistic_regression.py b/mlrose_hiive/neural/logistic_regression.py index 7ede13b9..777802ad 100644 --- a/mlrose_hiive/neural/logistic_regression.py +++ b/mlrose_hiive/neural/logistic_regression.py @@ -17,7 +17,7 @@ class LogisticRegression(_NNCore, ClassifierMixin): Parameters ---------- - algorithm: string, default: 'random_hill_climb' + algorithm: str, default: 'random_hill_climb' Algorithm used to find optimal network weights. Must be one of:'random_hill_climb', 'simulated_annealing', 'genetic_alg' or 'gradient_descent'. diff --git a/mlrose_hiive/neural/neural_network.py b/mlrose_hiive/neural/neural_network.py index cb7158c3..468c6850 100644 --- a/mlrose_hiive/neural/neural_network.py +++ b/mlrose_hiive/neural/neural_network.py @@ -18,11 +18,11 @@ class NeuralNetwork(_NNCore, ClassifierMixin): hidden_nodes: list of ints List giving the number of nodes in each hidden layer. - activation: string, default: 'relu' + activation: str, default: 'relu' Activation function for each of the hidden layers. Must be one of: 'identity', 'relu', 'sigmoid' or 'tanh'. - algorithm: string, default: 'random_hill_climb' + algorithm: str, default: 'random_hill_climb' Algorithm used to find optimal network weights. Must be one of:'random_hill_climb', 'simulated_annealing', 'genetic_alg' or 'gradient_descent'. diff --git a/mlrose_hiive/opt_probs/continuous_opt.py b/mlrose_hiive/opt_probs/continuous_opt.py index 3ac8216b..c7634474 100644 --- a/mlrose_hiive/opt_probs/continuous_opt.py +++ b/mlrose_hiive/opt_probs/continuous_opt.py @@ -38,8 +38,8 @@ def __init__(self, length, fitness_fn, maximize=True, min_val=0, OptProb.__init__(self, length, fitness_fn, maximize=maximize) - if (self.fitness_fn.get_prob_type() != 'continuous') \ - and (self.fitness_fn.get_prob_type() != 'either'): + if (self.fitness_fn.get_problem_type() != 'continuous') \ + and (self.fitness_fn.get_problem_type() != 'either'): raise Exception("fitness_fn must have problem type 'continuous'" + """ or 'either'. Define problem as""" + """ DiscreteOpt problem or use alternative""" @@ -92,12 +92,12 @@ def find_neighbors(self): if not np.array_equal(np.array(neighbor), self.state): self.neighbors.append(neighbor) - def get_prob_type(self): + def get_problem_type(self): """ Return the problem type. Returns ------- - self.prob_type: string + self.prob_type: str Returns problem type. """ return self.prob_type diff --git a/mlrose_hiive/opt_probs/discrete_opt.py b/mlrose_hiive/opt_probs/discrete_opt.py index d50e1e53..267141f4 100644 --- a/mlrose_hiive/opt_probs/discrete_opt.py +++ b/mlrose_hiive/opt_probs/discrete_opt.py @@ -40,7 +40,7 @@ def __init__(self, length, fitness_fn, maximize=True, max_val=2, OptProb.__init__(self, length, fitness_fn, maximize) - if self.fitness_fn.get_prob_type() == 'continuous': + if self.fitness_fn.get_problem_type() == 'continuous': raise Exception("""fitness_fn must have problem type 'discrete',""" + """ 'either' or 'tsp'. Define problem as""" + """ ContinuousOpt problem or use alternative""" @@ -288,12 +288,12 @@ def get_keep_sample(self): """ return self.keep_sample - def get_prob_type(self): + def get_problem_type(self): """ Return the problem type. Returns ------- - self.prob_type: string + self.prob_type: str Returns problem type. """ return self.prob_type diff --git a/mlrose_hiive/opt_probs/tsp_opt.py b/mlrose_hiive/opt_probs/tsp_opt.py index 1493cd36..1d1496f4 100644 --- a/mlrose_hiive/opt_probs/tsp_opt.py +++ b/mlrose_hiive/opt_probs/tsp_opt.py @@ -7,7 +7,7 @@ from mlrose_hiive.algorithms.crossovers import TSPCrossover from mlrose_hiive.algorithms.mutators import GeneSwapMutator -from mlrose_hiive.fitness import TravellingSales +from mlrose_hiive.fitness import TravellingSalesperson from mlrose_hiive.opt_probs.discrete_opt import DiscreteOpt @@ -50,7 +50,7 @@ def __init__(self, length=None, fitness_fn=None, maximize=False, coords=None, raise Exception("""At least one of fitness_fn, coords and""" + """ distances must be specified.""") elif fitness_fn is None: - fitness_fn = TravellingSales(coords=coords, distances=distances) + fitness_fn = TravellingSalesperson(coords=coords, distances=distances) self.distances = distances self.coords = coords if length is None: @@ -62,7 +62,7 @@ def __init__(self, length=None, fitness_fn=None, maximize=False, coords=None, DiscreteOpt.__init__(self, length, fitness_fn, maximize, max_val=length, crossover=TSPCrossover(self), mutator=GeneSwapMutator(self)) - if self.fitness_fn.get_prob_type() != 'tsp': + if self.fitness_fn.get_problem_type() != 'tsp': raise Exception("""fitness_fn must have problem type 'tsp'.""") self.source_graph = source_graph diff --git a/tests/test_fitness.py b/tests/test_fitness.py index 8b606f76..0ac7cb66 100644 --- a/tests/test_fitness.py +++ b/tests/test_fitness.py @@ -12,8 +12,8 @@ sys.path.append("..") from mlrose_hiive import (OneMax, FlipFlop, FourPeaks, SixPeaks, ContinuousPeaks, - Knapsack, TravellingSales, Queens, MaxKColor, CustomFitness) -from mlrose_hiive.fitness.discrete_peaks_base import DiscretePeaksBase + Knapsack, TravellingSalesperson, Queens, MaxKColor, CustomFitness) +from mlrose_hiive.fitness._discrete_peaks_base import _DiscretePeaksBase def test_onemax(): @@ -31,31 +31,31 @@ def test_flipflop(): def test_head(): """Test head function""" state = np.array([1, 1, 1, 1, 0, 1, 0, 2, 1, 1, 1, 1, 1, 4, 6, 1, 1]) - assert DiscretePeaksBase.head(1, state) == 4 + assert _DiscretePeaksBase.head(1, state) == 4 def test_tail(): """Test tail function""" state = np.array([1, 1, 1, 1, 0, 1, 0, 2, 1, 1, 1, 1, 1, 4, 6, 1, 1]) - assert DiscretePeaksBase.tail(1, state) == 2 + assert _DiscretePeaksBase.tail(1, state) == 2 def test_max_run_middle(): """Test max_run function for case where run is in the middle of the state""" state = np.array([1, 1, 1, 1, 0, 1, 0, 2, 1, 1, 1, 1, 1, 4, 6, 1, 1]) - assert ContinuousPeaks.max_run(1, state) == 5 + assert ContinuousPeaks._max_run(1, state) == 5 def test_max_run_start(): """Test max_run function for case where run is at the start of the state""" state = np.array([1, 1, 1, 1, 1, 1, 0, 2, 1, 1, 1, 1, 1, 4, 6, 1, 1]) - assert ContinuousPeaks.max_run(1, state) == 6 + assert ContinuousPeaks._max_run(1, state) == 6 def test_max_run_end(): """Test max_run function for case where run is at the end of the state""" state = np.array([1, 1, 1, 1, 0, 1, 0, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1]) - assert ContinuousPeaks.max_run(1, state) == 9 + assert ContinuousPeaks._max_run(1, state) == 9 def test_fourpeaks_r0(): @@ -103,13 +103,13 @@ def test_sixpeaks_r_gt0_max0(): def test_continuouspeaks_r0(): """Test ContinuousPeaks fitness function for case when R = 0.""" state = np.array([0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1]) - assert ContinuousPeaks(t_pct=0.30).evaluate(state) == 5 + assert ContinuousPeaks(threshold_percentage=0.30).evaluate(state) == 5 def test_continuouspeaks_r_gt(): """Test ContinuousPeaks fitness function for case when R > 0.""" state = np.array([0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1]) - assert ContinuousPeaks(t_pct=0.15).evaluate(state) == 17 + assert ContinuousPeaks(threshold_percentage=0.15).evaluate(state) == 17 def test_knapsack_weight_lt_max(): @@ -135,21 +135,21 @@ def test_travelling_sales_coords(): """Test TravellingSales fitness function for case where city nodes coords are specified.""" coords = [(0, 0), (3, 0), (3, 2), (2, 4), (1, 3)] state = np.array([0, 1, 4, 3, 2]) - assert round(TravellingSales(coords=coords).evaluate(state), 4) == 13.8614 + assert round(TravellingSalesperson(coords=coords).evaluate(state), 4) == 13.8614 def test_travelling_sales_dists(): """Test TravellingSales fitness function for case where distances between node pairs are specified.""" dists = [(0, 1, 3), (0, 2, 5), (0, 3, 1), (0, 4, 7), (1, 3, 6), (4, 1, 9), (2, 3, 8), (2, 4, 2), (3, 2, 8), (3, 4, 4)] state = np.array([0, 1, 4, 3, 2]) - assert TravellingSales(distances=dists).evaluate(state) == 29 + assert TravellingSalesperson(distances=dists).evaluate(state) == 29 def test_travelling_sales_invalid(): """Test TravellingSales fitness function for invalid tour""" dists = [(0, 1, 3), (0, 2, 5), (0, 3, 1), (0, 4, 7), (1, 3, 6), (4, 1, 9), (2, 3, 8), (2, 4, 2), (3, 2, 8), (3, 4, 4)] state = np.array([0, 1, 2, 3, 4]) - assert TravellingSales(distances=dists).evaluate(state) == np.inf + assert TravellingSalesperson(distances=dists).evaluate(state) == np.inf def test_queens():