From 6bb353d774288a0da6d73416a58119f9b2789d28 Mon Sep 17 00:00:00 2001 From: fk03983 Date: Tue, 7 Jan 2020 19:22:41 -0600 Subject: [PATCH 1/4] added hill climbing algorithm --- optimization/hill_climbing.py | 213 ++++++++++++++++++++++++++++++++++ optimization/requirements.txt | 1 + 2 files changed, 214 insertions(+) create mode 100644 optimization/hill_climbing.py create mode 100644 optimization/requirements.txt diff --git a/optimization/hill_climbing.py b/optimization/hill_climbing.py new file mode 100644 index 000000000000..a9346e539558 --- /dev/null +++ b/optimization/hill_climbing.py @@ -0,0 +1,213 @@ +import math +import matplotlib.pyplot as plt + + +class SearchProblem: + """ + A interface to define search problems. The interface will be illustrated using + the example of mathematical function. + """ + + def __init__( + self, x_coordinate: int, y_coordinate: int, step: int, function_to_optimize + ): + """ + The constructor of the search problem. + x_coordinate: an integer representing the current x co-ordinate of the current search state. + y_coordinate: an integer representing the current y co-ordinate of the current search state. + step: an integer representing the size of the step to take when looking for neighbors. + function_to_optimize: a function to optimize having the signature f(x,y). + """ + self.x = x_coordinate + self.y = y_coordinate + self.step_size = step + self.function = function_to_optimize + + def score(self) -> int: + """ + Returns the function output at the current x and y co-ordinates. + >>> def test_function(x,y): return x + y + >>> SearchProblem(0, 0, 1, test_function).score() #should return 0 as 0 + 0 = 0 + 0 + + >>> SearchProblem(5, 7, 1, test_function).score() #should 12 as 5 + 7 = 12 + 12 + """ + return self.function(self.x, self.y) + + def get_neighbors(self): + """ + Returns a list of neighbors. Neighbors are co_ordinates adjacent to the current co_ordinates. + """ + current_x = self.x + current_y = self.y + neighbors = [None for i in range(8)] # creating an empty list of size 8. + neighbors[0] = SearchProblem( + current_x + self.step_size, current_y, self.step_size, self.function + ) + neighbors[1] = SearchProblem( + current_x + self.step_size, + current_y + self.step_size, + self.step_size, + self.function, + ) + neighbors[2] = SearchProblem( + current_x, current_y + self.step_size, self.step_size, self.function + ) + neighbors[3] = SearchProblem( + current_x - self.step_size, + current_y + self.step_size, + self.step_size, + self.function, + ) + neighbors[4] = SearchProblem( + current_x - self.step_size, current_y, self.step_size, self.function + ) + neighbors[5] = SearchProblem( + current_x - self.step_size, + current_y - self.step_size, + self.step_size, + self.function, + ) + neighbors[6] = SearchProblem( + current_x, current_y - self.step_size, self.step_size, self.function + ) + neighbors[7] = SearchProblem( + current_x + self.step_size, + current_y - self.step_size, + self.step_size, + self.function, + ) + return neighbors + + def __hash__(self): + """ + utility function to hash the current search state. + """ + return hash( + str(self) + ) # hashing the string represetation of the current search state. + + def __str__(self): + """ + utility function for the string representation of the current search state. + >>> str(SearchProblem(0, 0, 1, None)) + 'x: 0 y: 0' + + >>> str(SearchProblem(2, 5, 1, None)) + 'x: 2 y: 5' + """ + return "x: " + str(self.x) + " y: " + str(self.y) + + +def hill_climbing( + search_prob, + find_max: bool = True, + max_x: float = math.inf, + min_x: float = -math.inf, + max_y: float = math.inf, + min_y: float = -math.inf, + visualization: bool = False, + max_iter: int = 10000, +) -> SearchProblem: + """ + implementation of the hill climbling algorithm. We start with a given state, find all its neighbors, move towards + the neighbor which provides the maximum (or minimum) change. We keep doing this untill we are at a state where + we do not have any neighbors which can improve the solution. + Args: + search_prob: The search state at the start. + find_max: Whether to find the maximum or not. If False, the algorithm finds the minimum. + max_x, min_x, max_y, min_y: integers representing the maximum and minimum bounds of x and y. + visualization: a flag to switch visualization off. If True, provides a matplotlib graph at the end of execution. + max_iter: number of times to run the iteration. + Returns a search state having the maximum (or minimum) score. + """ + current_state = search_prob + scores = [] # list to store the current score at each iteration + iterations = 0 + solution_found = False + visited = set() + while not solution_found and iterations < max_iter: + visited.add(current_state) + iterations += 1 + current_score = current_state.score() + scores.append(current_score) + neighbors = current_state.get_neighbors() + max_change = -math.inf + min_change = math.inf + next_state = None # to hold the next best neighbor + for neighbor in neighbors: + if neighbor in visited: + continue # do not want to visit the same state again + if ( + neighbor.x > max_x + or neighbor.x < min_x + or neighbor.y > max_y + or neighbor.y < min_y + ): + continue # neighbor outside our bounds + change = neighbor.score() - current_score + if find_max: # finding max + if ( + change > max_change and change > 0 + ): # going to direction with greatest ascent + max_change = change + next_state = neighbor + else: # finding min + if ( + change < min_change and change < 0 + ): # to direction with greatest descent + min_change = change + next_state = neighbor + if ( + next_state is not None + ): # we found atleast one neighbor which improved the current state + current_state = next_state + else: + solution_found = True # since we have no neighbor that improves the solution we stop the search + + if visualization: + plt.plot(range(iterations), scores) + plt.xlabel("Iterations") + plt.ylabel("Funcation values") + plt.show() + + return current_state + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + def test_f1(x, y): + return (x ** 2) + (y ** 2) + + prob = SearchProblem( + x_coordinate=3, y_coordinate=4, step=1, function_to_optimize=test_f1 + ) # starting the problem with initial co_ordinates (3,4) + local_min = hill_climbing(prob, find_max=False) + print( + f"The minimum score for f(x,y) = x^2 + y^2 found via hill climbing: {local_min.score()}" + ) + + prob = SearchProblem( + x_coordinate=12, y_coordinate=47, step=1, function_to_optimize=test_f1 + ) # starting the problem with initial co_ordinates (3,4) + local_min = hill_climbing( + prob, find_max=False, max_x=100, min_x=5, max_y=50, min_y=-5, visualization=True + ) + print( + f"The minimum score for f(x,y) = x^2 + y^2 with the domain 100 > x > 5 and 50 > y > - 5 found via hill climbing: {local_min.score()}" + ) + + def test_f2(x, y): + return (3 * x ** 2) - (6 * y) + + prob = SearchProblem( + x_coordinate=3, y_coordinate=4, step=1, function_to_optimize=test_f1 + ) + local_min = hill_climbing(prob, find_max=True) + print( + f"The maximum score for f(x,y) = x^2 + y^2 found via hill climbing: {local_min.score()}" + ) diff --git a/optimization/requirements.txt b/optimization/requirements.txt new file mode 100644 index 000000000000..4b43f7e68658 --- /dev/null +++ b/optimization/requirements.txt @@ -0,0 +1 @@ +matplotlib \ No newline at end of file From 1ca5de22e432e0debd43cc9f4453e3e2c2335cb0 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 8 Jan 2020 05:52:09 +0100 Subject: [PATCH 2/4] Shorten long lines, streamline get_neighbors() --- optimization/hill_climbing.py | 159 ++++++++++++++-------------------- 1 file changed, 66 insertions(+), 93 deletions(-) diff --git a/optimization/hill_climbing.py b/optimization/hill_climbing.py index a9346e539558..1f48e8990d43 100644 --- a/optimization/hill_climbing.py +++ b/optimization/hill_climbing.py @@ -1,5 +1,4 @@ import math -import matplotlib.pyplot as plt class SearchProblem: @@ -8,96 +7,70 @@ class SearchProblem: the example of mathematical function. """ - def __init__( - self, x_coordinate: int, y_coordinate: int, step: int, function_to_optimize - ): + def __init__(self, x: int, y: int, step_size: int, function_to_optimize): """ The constructor of the search problem. - x_coordinate: an integer representing the current x co-ordinate of the current search state. - y_coordinate: an integer representing the current y co-ordinate of the current search state. - step: an integer representing the size of the step to take when looking for neighbors. - function_to_optimize: a function to optimize having the signature f(x,y). + x: the x coordinate of the current search state. + y: the y coordinate of the current search state. + step_size: size of the step to take when looking for neighbors. + function_to_optimize: a function to optimize having the signature f(x, y). """ - self.x = x_coordinate - self.y = y_coordinate - self.step_size = step + self.x = x + self.y = y + self.step_size = step_size self.function = function_to_optimize def score(self) -> int: """ - Returns the function output at the current x and y co-ordinates. - >>> def test_function(x,y): return x + y - >>> SearchProblem(0, 0, 1, test_function).score() #should return 0 as 0 + 0 = 0 + Returns the output for the function called with current x and y coordinates. + >>> def test_function(x, y): + ... return x + y + >>> SearchProblem(0, 0, 1, test_function).score() # 0 + 0 = 0 0 - - >>> SearchProblem(5, 7, 1, test_function).score() #should 12 as 5 + 7 = 12 + >>> SearchProblem(5, 7, 1, test_function).score() # 5 + 7 = 12 12 """ return self.function(self.x, self.y) def get_neighbors(self): """ - Returns a list of neighbors. Neighbors are co_ordinates adjacent to the current co_ordinates. + Returns a list of coordinates of neighbors adjacent to the current coordinates. + + Neighbors: + | 0 | 1 | 2 | + | 3 | _ | 4 | + | 5 | 6 | 7 | """ - current_x = self.x - current_y = self.y - neighbors = [None for i in range(8)] # creating an empty list of size 8. - neighbors[0] = SearchProblem( - current_x + self.step_size, current_y, self.step_size, self.function - ) - neighbors[1] = SearchProblem( - current_x + self.step_size, - current_y + self.step_size, - self.step_size, - self.function, - ) - neighbors[2] = SearchProblem( - current_x, current_y + self.step_size, self.step_size, self.function - ) - neighbors[3] = SearchProblem( - current_x - self.step_size, - current_y + self.step_size, - self.step_size, - self.function, - ) - neighbors[4] = SearchProblem( - current_x - self.step_size, current_y, self.step_size, self.function - ) - neighbors[5] = SearchProblem( - current_x - self.step_size, - current_y - self.step_size, - self.step_size, - self.function, - ) - neighbors[6] = SearchProblem( - current_x, current_y - self.step_size, self.step_size, self.function - ) - neighbors[7] = SearchProblem( - current_x + self.step_size, - current_y - self.step_size, - self.step_size, - self.function, - ) - return neighbors + step_size = self.step_size + return [ + SearchProblem(x, y, step_size, self.function) + for x, y in ( + (self.x - step_size, self.y - step_size), + (self.x - step_size, self.y), + (self.x - step_size, self.y + step_size), + (self.x, self.y - step_size), + (self.x, self.y + step_size), + (self.x + step_size, self.y - step_size), + (self.x + step_size, self.y), + (self.x + step_size, self.y + step_size), + ) + ] def __hash__(self): """ - utility function to hash the current search state. + hash the string represetation of the current search state. """ - return hash( - str(self) - ) # hashing the string represetation of the current search state. + return hash(str(self)) def __str__(self): """ - utility function for the string representation of the current search state. + string representation of the current search state. >>> str(SearchProblem(0, 0, 1, None)) 'x: 0 y: 0' - >>> str(SearchProblem(2, 5, 1, None)) 'x: 2 y: 5' """ - return "x: " + str(self.x) + " y: " + str(self.y) + return f"x: {self.x} y: {self.y}" def hill_climbing( @@ -111,14 +84,15 @@ def hill_climbing( max_iter: int = 10000, ) -> SearchProblem: """ - implementation of the hill climbling algorithm. We start with a given state, find all its neighbors, move towards - the neighbor which provides the maximum (or minimum) change. We keep doing this untill we are at a state where - we do not have any neighbors which can improve the solution. + implementation of the hill climbling algorithm. We start with a given state, find + all its neighbors, move towards the neighbor which provides the maximum (or + minimum) change. We keep doing this untill we are at a state where we do not + have any neighbors which can improve the solution. Args: search_prob: The search state at the start. - find_max: Whether to find the maximum or not. If False, the algorithm finds the minimum. - max_x, min_x, max_y, min_y: integers representing the maximum and minimum bounds of x and y. - visualization: a flag to switch visualization off. If True, provides a matplotlib graph at the end of execution. + find_max: If True, the algorithm should find the minimum else the minimum. + max_x, min_x, max_y, min_y: the maximum and minimum bounds of x and y. + visualization: If True, a matplotlib graph is displayed. max_iter: number of times to run the iteration. Returns a search state having the maximum (or minimum) score. """ @@ -148,28 +122,28 @@ def hill_climbing( continue # neighbor outside our bounds change = neighbor.score() - current_score if find_max: # finding max - if ( - change > max_change and change > 0 - ): # going to direction with greatest ascent + # going to direction with greatest ascent + if change > max_change and change > 0: max_change = change next_state = neighbor else: # finding min - if ( - change < min_change and change < 0 - ): # to direction with greatest descent + # to direction with greatest descent + if change < min_change and change < 0: min_change = change next_state = neighbor - if ( - next_state is not None - ): # we found atleast one neighbor which improved the current state + if next_state is not None: + # we found at least one neighbor which improved the current state current_state = next_state else: - solution_found = True # since we have no neighbor that improves the solution we stop the search + # since we have no neighbor that improves the solution we stop the search + solution_found = True if visualization: + import matplotlib.pyplot as plt + plt.plot(range(iterations), scores) plt.xlabel("Iterations") - plt.ylabel("Funcation values") + plt.ylabel("Function values") plt.show() return current_state @@ -183,31 +157,30 @@ def hill_climbing( def test_f1(x, y): return (x ** 2) + (y ** 2) - prob = SearchProblem( - x_coordinate=3, y_coordinate=4, step=1, function_to_optimize=test_f1 - ) # starting the problem with initial co_ordinates (3,4) + # starting the problem with initial co_ordinates (3, 4) + prob = SearchProblem(x=3, y=4, step_size=1, function_to_optimize=test_f1) local_min = hill_climbing(prob, find_max=False) print( - f"The minimum score for f(x,y) = x^2 + y^2 found via hill climbing: {local_min.score()}" + "The minimum score for f(x, y) = x^2 + y^2 found via hill climbing: " + f"{local_min.score()}" ) - prob = SearchProblem( - x_coordinate=12, y_coordinate=47, step=1, function_to_optimize=test_f1 - ) # starting the problem with initial co_ordinates (3,4) + # starting the problem with initial co_ordinates (12, 47) + prob = SearchProblem(x=12, y=47, step_size=1, function_to_optimize=test_f1) local_min = hill_climbing( prob, find_max=False, max_x=100, min_x=5, max_y=50, min_y=-5, visualization=True ) print( - f"The minimum score for f(x,y) = x^2 + y^2 with the domain 100 > x > 5 and 50 > y > - 5 found via hill climbing: {local_min.score()}" + "The minimum score for f(x, y) = x^2 + y^2 with the domain 100 > x > 5 " + f"and 50 > y > - 5 found via hill climbing: {local_min.score()}" ) def test_f2(x, y): return (3 * x ** 2) - (6 * y) - prob = SearchProblem( - x_coordinate=3, y_coordinate=4, step=1, function_to_optimize=test_f1 - ) + prob = SearchProblem(x=3, y=4, step_size=1, function_to_optimize=test_f1) local_min = hill_climbing(prob, find_max=True) print( - f"The maximum score for f(x,y) = x^2 + y^2 found via hill climbing: {local_min.score()}" + "The maximum score for f(x, y) = x^2 + y^2 found via hill climbing: " + f"{local_min.score()}" ) From 565bc6edc3d718d3d6b2dc5c57b59108938910cc Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 8 Jan 2020 05:54:19 +0100 Subject: [PATCH 3/4] Update hill_climbing.py --- optimization/hill_climbing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optimization/hill_climbing.py b/optimization/hill_climbing.py index 1f48e8990d43..2f8dce54af7f 100644 --- a/optimization/hill_climbing.py +++ b/optimization/hill_climbing.py @@ -157,7 +157,7 @@ def hill_climbing( def test_f1(x, y): return (x ** 2) + (y ** 2) - # starting the problem with initial co_ordinates (3, 4) + # starting the problem with initial coordinates (3, 4) prob = SearchProblem(x=3, y=4, step_size=1, function_to_optimize=test_f1) local_min = hill_climbing(prob, find_max=False) print( @@ -165,7 +165,7 @@ def test_f1(x, y): f"{local_min.score()}" ) - # starting the problem with initial co_ordinates (12, 47) + # starting the problem with initial coordinates (12, 47) prob = SearchProblem(x=12, y=47, step_size=1, function_to_optimize=test_f1) local_min = hill_climbing( prob, find_max=False, max_x=100, min_x=5, max_y=50, min_y=-5, visualization=True From 679117976a63d10852f3610093b9d54ea2016b03 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 8 Jan 2020 06:00:38 +0100 Subject: [PATCH 4/4] Update and rename optimization/hill_climbing.py to searches/hill_climbing.py --- {optimization => searches}/hill_climbing.py | 1 + 1 file changed, 1 insertion(+) rename {optimization => searches}/hill_climbing.py (99%) diff --git a/optimization/hill_climbing.py b/searches/hill_climbing.py similarity index 99% rename from optimization/hill_climbing.py rename to searches/hill_climbing.py index 2f8dce54af7f..7c4ba3fb84ab 100644 --- a/optimization/hill_climbing.py +++ b/searches/hill_climbing.py @@ -1,3 +1,4 @@ +# https://en.wikipedia.org/wiki/Hill_climbing import math