{ "cells": [ { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "# Solving problems by Searching\n", "\n", "This notebook serves as supporting material for topics covered in **Chapter 3 - Solving Problems by Searching** and **Chapter 4 - Beyond Classical Search** from the book *Artificial Intelligence: A Modern Approach.* This notebook uses implementations from [search.py](https://github.com/aimacode/aima-python/blob/master/search.py) module. Let's start by importing everything from search module." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "scrolled": true }, "outputs": [], "source": [ "from search import *\n", "from notebook import psource\n", "\n", "# Needed to hide warnings in the matplotlib sections\n", "import warnings\n", "warnings.filterwarnings(\"ignore\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## CONTENTS\n", "\n", "* Overview\n", "* Problem\n", "* Search Algorithms Visualization\n", "* Breadth-First Tree Search\n", "* Breadth-First Search\n", "* Best First Search\n", "* Uniform Cost Search\n", "* Greedy Best First Search\n", "* A\\* Search\n", "* Genetic Algorithm" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## OVERVIEW\n", "\n", "Here, we learn about problem solving. Building goal-based agents that can plan ahead to solve problems, in particular, navigation problem/route finding problem. First, we will start the problem solving by precisely defining **problems** and their **solutions**. We will look at several general-purpose search algorithms. Broadly, search algorithms are classified into two types:\n", "\n", "* **Uninformed search algorithms**: Search algorithms which explore the search space without having any information about the problem other than its definition.\n", "* Examples:\n", " 1. Breadth First Search\n", " 2. Depth First Search\n", " 3. Depth Limited Search\n", " 4. Iterative Deepening Search\n", "\n", "\n", "* **Informed search algorithms**: These type of algorithms leverage any information (heuristics, path cost) on the problem to search through the search space to find the solution efficiently.\n", "* Examples:\n", " 1. Best First Search\n", " 2. Uniform Cost Search\n", " 3. A\\* Search\n", " 4. Recursive Best First Search\n", "\n", "*Don't miss the visualisations of these algorithms solving the route-finding problem defined on Romania map at the end of this notebook.*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## PROBLEM\n", "\n", "Let's see how we define a Problem. Run the next cell to see how abstract class `Problem` is defined in the search module." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "%psource Problem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `Problem` class has six methods.\n", "\n", "* `__init__(self, initial, goal)` : This is what is called a `constructor` and is the first method called when you create an instance of the class. `initial` specifies the initial state of our search problem. It represents the start state from where our agent begins its task of exploration to find the goal state(s) which is given in the `goal` parameter.\n", "\n", "\n", "* `actions(self, state)` : This method returns all the possible actions agent can execute in the given state `state`.\n", "\n", "\n", "* `result(self, state, action)` : This returns the resulting state if action `action` is taken in the state `state`. This `Problem` class only deals with deterministic outcomes. So we know for sure what every action in a state would result to.\n", "\n", "\n", "* `goal_test(self, state)` : Given a graph state, it checks if it is a terminal state. If the state is indeed a goal state, value of `True` is returned. Else, of course, `False` is returned.\n", "\n", "\n", "* `path_cost(self, c, state1, action, state2)` : Return the cost of the path that arrives at `state2` as a result of taking `action` from `state1`, assuming total cost of `c` to get up to `state1`.\n", "\n", "\n", "* `value(self, state)` : This acts as a bit of extra information in problems where we try to optimise a value when we cannot do a goal test." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We will use the abstract class `Problem` to define our real **problem** named `GraphProblem`. You can see how we define `GraphProblem` by running the next cell." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "%psource GraphProblem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now it's time to define our problem. We will define it by passing `initial`, `goal`, `graph` to `GraphProblem`. So, our problem is to find the goal state starting from the given initial state on the provided graph. Have a look at our romania_map, which is an Undirected Graph containing a dict of nodes as keys and neighbours as values." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "romania_map = UndirectedGraph(dict(\n", " Arad=dict(Zerind=75, Sibiu=140, Timisoara=118),\n", " Bucharest=dict(Urziceni=85, Pitesti=101, Giurgiu=90, Fagaras=211),\n", " Craiova=dict(Drobeta=120, Rimnicu=146, Pitesti=138),\n", " Drobeta=dict(Mehadia=75),\n", " Eforie=dict(Hirsova=86),\n", " Fagaras=dict(Sibiu=99),\n", " Hirsova=dict(Urziceni=98),\n", " Iasi=dict(Vaslui=92, Neamt=87),\n", " Lugoj=dict(Timisoara=111, Mehadia=70),\n", " Oradea=dict(Zerind=71, Sibiu=151),\n", " Pitesti=dict(Rimnicu=97),\n", " Rimnicu=dict(Sibiu=80),\n", " Urziceni=dict(Vaslui=142)))\n", "\n", "romania_map.locations = dict(\n", " Arad=(91, 492), Bucharest=(400, 327), Craiova=(253, 288),\n", " Drobeta=(165, 299), Eforie=(562, 293), Fagaras=(305, 449),\n", " Giurgiu=(375, 270), Hirsova=(534, 350), Iasi=(473, 506),\n", " Lugoj=(165, 379), Mehadia=(168, 339), Neamt=(406, 537),\n", " Oradea=(131, 571), Pitesti=(320, 368), Rimnicu=(233, 410),\n", " Sibiu=(207, 457), Timisoara=(94, 410), Urziceni=(456, 350),\n", " Vaslui=(509, 444), Zerind=(108, 531))" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "It is pretty straightforward to understand this `romania_map`. The first node **Arad** has three neighbours named **Zerind**, **Sibiu**, **Timisoara**. Each of these nodes are 75, 140, 118 units apart from **Arad** respectively. And the same goes with other nodes.\n", "\n", "And `romania_map.locations` contains the positions of each of the nodes. We will use the straight line distance (which is different from the one provided in `romania_map`) between two cities in algorithms like A\\*-search and Recursive Best First Search.\n", "\n", "**Define a problem:**\n", "Hmm... say we want to start exploring from **Arad** and try to find **Bucharest** in our romania_map. So, this is how we do it." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Romania Map Visualisation\n", "\n", "Let's have a visualisation of Romania map [Figure 3.2] from the book and see how different searching algorithms perform / how frontier expands in each search algorithm for a simple problem named `romania_problem`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Have a look at `romania_locations`. It is a dictionary defined in search module. We will use these location values to draw the romania graph using **networkx**." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'Arad': (91, 492), 'Bucharest': (400, 327), 'Craiova': (253, 288), 'Drobeta': (165, 299), 'Eforie': (562, 293), 'Fagaras': (305, 449), 'Giurgiu': (375, 270), 'Hirsova': (534, 350), 'Iasi': (473, 506), 'Lugoj': (165, 379), 'Mehadia': (168, 339), 'Neamt': (406, 537), 'Oradea': (131, 571), 'Pitesti': (320, 368), 'Rimnicu': (233, 410), 'Sibiu': (207, 457), 'Timisoara': (94, 410), 'Urziceni': (456, 350), 'Vaslui': (509, 444), 'Zerind': (108, 531)}\n" ] } ], "source": [ "romania_locations = romania_map.locations\n", "print(romania_locations)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's start the visualisations by importing necessary modules. We use networkx and matplotlib to show the map in the notebook and we use ipywidgets to interact with the map to see how the searching algorithm works." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", "import networkx as nx\n", "import matplotlib.pyplot as plt\n", "from matplotlib import lines\n", "\n", "from ipywidgets import interact\n", "import ipywidgets as widgets\n", "from IPython.display import display\n", "import time" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's get started by initializing an empty graph. We will add nodes, place the nodes in their location as shown in the book, add edges to the graph." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "# initialise a graph\n", "G = nx.Graph()\n", "\n", "# use this while labeling nodes in the map\n", "node_labels = dict()\n", "# use this to modify colors of nodes while exploring the graph.\n", "# This is the only dict we send to `show_map(node_colors)` while drawing the map\n", "node_colors = dict()\n", "\n", "for n, p in romania_locations.items():\n", " # add nodes from romania_locations\n", " G.add_node(n)\n", " # add nodes to node_labels\n", " node_labels[n] = n\n", " # node_colors to color nodes while exploring romania map\n", " node_colors[n] = \"white\"\n", "\n", "# we'll save the initial node colors to a dict to use later\n", "initial_node_colors = dict(node_colors)\n", " \n", "# positions for node labels\n", "node_label_pos = { k:[v[0],v[1]-10] for k,v in romania_locations.items() }\n", "\n", "# use this while labeling edges\n", "edge_labels = dict()\n", "\n", "# add edges between cities in romania map - UndirectedGraph defined in search.py\n", "for node in romania_map.nodes():\n", " connections = romania_map.get(node)\n", " for connection in connections.keys():\n", " distance = connections[connection]\n", "\n", " # add edges to the graph\n", " G.add_edge(node, connection)\n", " # add distances to edge_labels\n", " edge_labels[(node, connection)] = distance" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "# initialise a graph\n", "G = nx.Graph()\n", "\n", "# use this while labeling nodes in the map\n", "node_labels = dict()\n", "# use this to modify colors of nodes while exploring the graph.\n", "# This is the only dict we send to `show_map(node_colors)` while drawing the map\n", "node_colors = dict()\n", "\n", "for n, p in romania_locations.items():\n", " # add nodes from romania_locations\n", " G.add_node(n)\n", " # add nodes to node_labels\n", " node_labels[n] = n\n", " # node_colors to color nodes while exploring romania map\n", " node_colors[n] = \"white\"\n", "\n", "# we'll save the initial node colors to a dict to use later\n", "initial_node_colors = dict(node_colors)\n", " \n", "# positions for node labels\n", "node_label_pos = { k:[v[0],v[1]-10] for k,v in romania_locations.items() }\n", "\n", "# use this while labeling edges\n", "edge_labels = dict()\n", "\n", "# add edges between cities in romania map - UndirectedGraph defined in search.py\n", "for node in romania_map.nodes():\n", " connections = romania_map.get(node)\n", " for connection in connections.keys():\n", " distance = connections[connection]\n", "\n", " # add edges to the graph\n", " G.add_edge(node, connection)\n", " # add distances to edge_labels\n", " edge_labels[(node, connection)] = distance" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We have completed building our graph based on romania_map and its locations. It's time to display it here in the notebook. This function `show_map(node_colors)` helps us do that. We will be calling this function later on to display the map at each and every interval step while searching, using variety of algorithms from the book." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def show_map(node_colors):\n", " # set the size of the plot\n", " plt.figure(figsize=(18,13))\n", " # draw the graph (both nodes and edges) with locations from romania_locations\n", " nx.draw(G, pos = romania_locations, node_color = [node_colors[node] for node in G.nodes()])\n", "\n", " # draw labels for nodes\n", " node_label_handles = nx.draw_networkx_labels(G, pos = node_label_pos, labels = node_labels, font_size = 14)\n", " # add a white bounding box behind the node labels\n", " [label.set_bbox(dict(facecolor='white', edgecolor='none')) for label in node_label_handles.values()]\n", "\n", " # add edge lables to the graph\n", " nx.draw_networkx_edge_labels(G, pos = romania_locations, edge_labels=edge_labels, font_size = 14)\n", " \n", " # add a legend\n", " white_circle = lines.Line2D([], [], color=\"white\", marker='o', markersize=15, markerfacecolor=\"white\")\n", " orange_circle = lines.Line2D([], [], color=\"orange\", marker='o', markersize=15, markerfacecolor=\"orange\")\n", " red_circle = lines.Line2D([], [], color=\"red\", marker='o', markersize=15, markerfacecolor=\"red\")\n", " gray_circle = lines.Line2D([], [], color=\"gray\", marker='o', markersize=15, markerfacecolor=\"gray\")\n", " green_circle = lines.Line2D([], [], color=\"green\", marker='o', markersize=15, markerfacecolor=\"green\")\n", " plt.legend((white_circle, orange_circle, red_circle, gray_circle, green_circle),\n", " ('Un-explored', 'Frontier', 'Currently Exploring', 'Explored', 'Final Solution'),\n", " numpoints=1,prop={'size':16}, loc=(.8,.75))\n", " \n", " # show the plot. No need to use in notebooks. nx.draw will show the graph itself.\n", " plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can simply call the function with node_colors dictionary object to display it." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true, "scrolled": true }, "outputs": [], "source": [ "show_map(node_colors)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Voila! You see, the romania map as shown in the Figure[3.2] in the book. Now, see how different searching algorithms perform with our problem statements." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## SIMPLE PROBLEM SOLVING AGENT PROGRAM\n", "\n", "Let us now define a Simple Problem Solving Agent Program. Run the next cell to see how the abstract class `SimpleProblemSolvingAgentProgram` is defined in the search module." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "%psource SimpleProblemSolvingAgentProgram" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The SimpleProblemSolvingAgentProgram class has six methods: \n", "\n", "* `__init__(self, intial_state=None)`: This is the `contructor` of the class and is the first method to be called when the class is instantiated. It takes in a keyword argument, `initial_state` which is initially `None`. The argument `intial_state` represents the state from which the agent starts.\n", "\n", "* `__call__(self, percept)`: This method updates the `state` of the agent based on its `percept` using the `update_state` method. It then formulates a `goal` with the help of `formulate_goal` method and a `problem` using the `formulate_problem` method and returns a sequence of actions to solve it (using the `search` method).\n", "\n", "* `update_state(self, percept)`: This method updates the `state` of the agent based on its `percept`.\n", "\n", "* `formulate_goal(self, state)`: Given a `state` of the agent, this method formulates the `goal` for it.\n", "\n", "* `formulate_problem(self, state, goal)`: It is used in problem formulation given a `state` and a `goal` for the `agent`.\n", "\n", "* `search(self, problem)`: This method is used to search a sequence of `actions` to solve a `problem`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## SEARCHING ALGORITHMS VISUALIZATION\n", "\n", "In this section, we have visualizations of the following searching algorithms:\n", "\n", "1. Breadth First Tree Search - Implemented\n", "2. Depth First Tree Search - Implemented\n", "3. Depth First Graph Search - Implemented\n", "4. Breadth First Search - Implemented\n", "5. Best First Graph Search - Implemented\n", "6. Uniform Cost Search - Implemented\n", "7. Depth Limited Search\n", "8. Iterative Deepening Search\n", "9. A\\*-Search - Implemented\n", "10. Recursive Best First Search\n", "\n", "We add the colors to the nodes to have a nice visualisation when displaying. So, these are the different colors we are using in these visuals:\n", "* Un-explored nodes - white\n", "* Frontier nodes - orange\n", "* Currently exploring node - red\n", "* Already explored nodes - gray\n", "\n", "Now, we will define some helper methods to display interactive buttons and sliders when visualising search algorithms." ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def final_path_colors(problem, solution):\n", " \"returns a node_colors dict of the final path provided the problem and solution\"\n", " \n", " # get initial node colors\n", " final_colors = dict(initial_node_colors)\n", " # color all the nodes in solution and starting node to green\n", " final_colors[problem.initial] = \"green\"\n", " for node in solution:\n", " final_colors[node] = \"green\" \n", " return final_colors\n", "\n", "\n", "def display_visual(user_input, algorithm=None, problem=None):\n", " if user_input == False:\n", " def slider_callback(iteration):\n", " # don't show graph for the first time running the cell calling this function\n", " try:\n", " show_map(all_node_colors[iteration])\n", " except:\n", " pass\n", " def visualize_callback(Visualize):\n", " if Visualize is True:\n", " button.value = False\n", " \n", " global all_node_colors\n", " \n", " iterations, all_node_colors, node = algorithm(problem)\n", " solution = node.solution()\n", " all_node_colors.append(final_path_colors(problem, solution))\n", " \n", " slider.max = len(all_node_colors) - 1\n", " \n", " for i in range(slider.max + 1):\n", " slider.value = i\n", " #time.sleep(.5)\n", " \n", " slider = widgets.IntSlider(min=0, max=1, step=1, value=0)\n", " slider_visual = widgets.interactive(slider_callback, iteration = slider)\n", " display(slider_visual)\n", "\n", " button = widgets.ToggleButton(value = False)\n", " button_visual = widgets.interactive(visualize_callback, Visualize = button)\n", " display(button_visual)\n", " \n", " if user_input == True:\n", " node_colors = dict(initial_node_colors)\n", " if algorithm == None:\n", " algorithms = {\"Breadth First Tree Search\": breadth_first_tree_search,\n", " \"Depth First Tree Search\": depth_first_tree_search,\n", " \"Breadth First Search\": breadth_first_search,\n", " \"Depth First Graph Search\": depth_first_graph_search,\n", " \"Uniform Cost Search\": uniform_cost_search,\n", " \"A-star Search\": astar_search}\n", " algo_dropdown = widgets.Dropdown(description = \"Search algorithm: \",\n", " options = sorted(list(algorithms.keys())),\n", " value = \"Breadth First Tree Search\")\n", " display(algo_dropdown)\n", " \n", " def slider_callback(iteration):\n", " # don't show graph for the first time running the cell calling this function\n", " try:\n", " show_map(all_node_colors[iteration])\n", " except:\n", " pass\n", " \n", " def visualize_callback(Visualize):\n", " if Visualize is True:\n", " button.value = False\n", " \n", " problem = GraphProblem(start_dropdown.value, end_dropdown.value, romania_map)\n", " global all_node_colors\n", " \n", " if algorithm == None:\n", " user_algorithm = algorithms[algo_dropdown.value]\n", " \n", "# print(user_algorithm)\n", "# print(problem)\n", " \n", " iterations, all_node_colors, node = user_algorithm(problem)\n", " solution = node.solution()\n", " all_node_colors.append(final_path_colors(problem, solution))\n", "\n", " slider.max = len(all_node_colors) - 1\n", " \n", " for i in range(slider.max + 1):\n", " slider.value = i\n", "# time.sleep(.5)\n", " \n", " start_dropdown = widgets.Dropdown(description = \"Start city: \",\n", " options = sorted(list(node_colors.keys())), value = \"Arad\")\n", " display(start_dropdown)\n", "\n", " end_dropdown = widgets.Dropdown(description = \"Goal city: \",\n", " options = sorted(list(node_colors.keys())), value = \"Fagaras\")\n", " display(end_dropdown)\n", " \n", " button = widgets.ToggleButton(value = False)\n", " button_visual = widgets.interactive(visualize_callback, Visualize = button)\n", " display(button_visual)\n", " \n", " slider = widgets.IntSlider(min=0, max=1, step=1, value=0)\n", " slider_visual = widgets.interactive(slider_callback, iteration = slider)\n", " display(slider_visual)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## BREADTH-FIRST TREE SEARCH\n", "\n", "We have a working implementation in search module. But as we want to interact with the graph while it is searching, we need to modify the implementation. Here's the modified breadth first tree search." ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def tree_search(problem, frontier):\n", " \"\"\"Search through the successors of a problem to find a goal.\n", " The argument frontier should be an empty queue.\n", " Don't worry about repeated paths to a state. [Figure 3.7]\"\"\"\n", " \n", " # we use these two variables at the time of visualisations\n", " iterations = 0\n", " all_node_colors = []\n", " node_colors = dict(initial_node_colors)\n", " \n", " #Adding first node to the queue\n", " frontier.append(Node(problem.initial))\n", " \n", " node_colors[Node(problem.initial).state] = \"orange\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " while frontier:\n", " #Popping first node of queue\n", " node = frontier.pop()\n", " \n", " # modify the currently searching node to red\n", " node_colors[node.state] = \"red\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " if problem.goal_test(node.state):\n", " # modify goal node to green after reaching the goal\n", " node_colors[node.state] = \"green\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " return(iterations, all_node_colors, node)\n", " \n", " frontier.extend(node.expand(problem))\n", " \n", " for n in node.expand(problem):\n", " node_colors[n.state] = \"orange\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", "\n", " # modify the color of explored nodes to gray\n", " node_colors[node.state] = \"gray\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " return None\n", "\n", "def breadth_first_tree_search(problem):\n", " \"Search the shallowest nodes in the search tree first.\"\n", " iterations, all_node_colors, node = tree_search(problem, FIFOQueue())\n", " return(iterations, all_node_colors, node)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we use `ipywidgets` to display a slider, a button and our romania map. By sliding the slider we can have a look at all the intermediate steps of a particular search algorithm. By pressing the button **Visualize**, you can see all the steps without interacting with the slider. These two helper functions are the callback functions which are called when we interact with the slider and the button." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "d55324f7343a4c71a9a2d4da6d037037" } }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "b07a3813dd724c51a9b37f646cf2be25" } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Fagaras', romania_map)\n", "display_visual(user_input = False, algorithm = breadth_first_tree_search, problem = romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Depth-First Tree Search:\n", "Now let's discuss another searching algorithm, Depth-First Tree Search." ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def depth_first_tree_search(problem):\n", " \"Search the deepest nodes in the search tree first.\"\n", " # This algorithm might not work in case of repeated paths\n", " # and may run into an infinite while loop.\n", " iterations, all_node_colors, node = tree_search(problem, Stack())\n", " return(iterations, all_node_colors, node)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "523b10cf84e54798a044ee714b864b52" } }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "aecea953f6a448c192ac8e173cf46e35" } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Oradea', romania_map)\n", "display_visual(user_input = False, algorithm = depth_first_tree_search, problem = romania_problem)" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "## BREADTH-FIRST SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def breadth_first_search(problem):\n", " \"[Figure 3.11]\"\n", " \n", " # we use these two variables at the time of visualisations\n", " iterations = 0\n", " all_node_colors = []\n", " node_colors = dict(initial_node_colors)\n", " \n", " node = Node(problem.initial)\n", " \n", " node_colors[node.state] = \"red\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " if problem.goal_test(node.state):\n", " node_colors[node.state] = \"green\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " return(iterations, all_node_colors, node)\n", " \n", " frontier = FIFOQueue()\n", " frontier.append(node)\n", " \n", " # modify the color of frontier nodes to blue\n", " node_colors[node.state] = \"orange\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " explored = set()\n", " while frontier:\n", " node = frontier.pop()\n", " node_colors[node.state] = \"red\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " explored.add(node.state) \n", " \n", " for child in node.expand(problem):\n", " if child.state not in explored and child not in frontier:\n", " if problem.goal_test(child.state):\n", " node_colors[child.state] = \"green\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " return(iterations, all_node_colors, child)\n", " frontier.append(child)\n", "\n", " node_colors[child.state] = \"orange\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " node_colors[node.state] = \"gray\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " return None" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "735a3dea191a42b6bd97fdfd337ea3e7" } }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "ef445770d70a4b7c9d1544b98a55ca4d" } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", "display_visual(user_input = False, algorithm = breadth_first_search, problem = romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Depth-First Graph Search: \n", "Although we have a working implementation in search module, we have to make a few changes in the algorithm to make it suitable for visualization." ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def graph_search(problem, frontier):\n", " \"\"\"Search through the successors of a problem to find a goal.\n", " The argument frontier should be an empty queue.\n", " If two paths reach a state, only use the first one. [Figure 3.7]\"\"\"\n", " # we use these two variables at the time of visualisations\n", " iterations = 0\n", " all_node_colors = []\n", " node_colors = dict(initial_node_colors)\n", " \n", " frontier.append(Node(problem.initial))\n", " explored = set()\n", " \n", " # modify the color of frontier nodes to orange\n", " node_colors[Node(problem.initial).state] = \"orange\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " while frontier:\n", " # Popping first node of queue\n", " node = frontier.pop()\n", " \n", " # modify the currently searching node to red\n", " node_colors[node.state] = \"red\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " if problem.goal_test(node.state):\n", " # modify goal node to green after reaching the goal\n", " node_colors[node.state] = \"green\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " return(iterations, all_node_colors, node)\n", " \n", " explored.add(node.state)\n", " frontier.extend(child for child in node.expand(problem)\n", " if child.state not in explored and\n", " child not in frontier)\n", " \n", " for n in frontier:\n", " # modify the color of frontier nodes to orange\n", " node_colors[n.state] = \"orange\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", "\n", " # modify the color of explored nodes to gray\n", " node_colors[node.state] = \"gray\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " return None\n", "\n", "\n", "def depth_first_graph_search(problem):\n", " \"\"\"Search the deepest nodes in the search tree first.\"\"\"\n", " iterations, all_node_colors, node = graph_search(problem, Stack())\n", " return(iterations, all_node_colors, node)" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "61149ffbc02846af97170f8975d4f11d" } }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "90b1f8f77fdb4207a3570fbe88a0bdf6" } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", "display_visual(user_input = False, algorithm = depth_first_graph_search, problem = romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## BEST FIRST SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def best_first_graph_search(problem, f):\n", " \"\"\"Search the nodes with the lowest f scores first.\n", " You specify the function f(node) that you want to minimize; for example,\n", " if f is a heuristic estimate to the goal, then we have greedy best\n", " first search; if f is node.depth then we have breadth-first search.\n", " There is a subtlety: the line \"f = memoize(f, 'f')\" means that the f\n", " values will be cached on the nodes as they are computed. So after doing\n", " a best first search you can examine the f values of the path returned.\"\"\"\n", " \n", " # we use these two variables at the time of visualisations\n", " iterations = 0\n", " all_node_colors = []\n", " node_colors = dict(initial_node_colors)\n", " \n", " f = memoize(f, 'f')\n", " node = Node(problem.initial)\n", " \n", " node_colors[node.state] = \"red\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " if problem.goal_test(node.state):\n", " node_colors[node.state] = \"green\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " return(iterations, all_node_colors, node)\n", " \n", " frontier = PriorityQueue(min, f)\n", " frontier.append(node)\n", " \n", " node_colors[node.state] = \"orange\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " explored = set()\n", " while frontier:\n", " node = frontier.pop()\n", " \n", " node_colors[node.state] = \"red\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " \n", " if problem.goal_test(node.state):\n", " node_colors[node.state] = \"green\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " return(iterations, all_node_colors, node)\n", " \n", " explored.add(node.state)\n", " for child in node.expand(problem):\n", " if child.state not in explored and child not in frontier:\n", " frontier.append(child)\n", " node_colors[child.state] = \"orange\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " elif child in frontier:\n", " incumbent = frontier[child]\n", " if f(child) < f(incumbent):\n", " del frontier[incumbent]\n", " frontier.append(child)\n", " node_colors[child.state] = \"orange\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", "\n", " node_colors[node.state] = \"gray\"\n", " iterations += 1\n", " all_node_colors.append(dict(node_colors))\n", " return None" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## UNIFORM COST SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def uniform_cost_search(problem):\n", " \"[Figure 3.14]\"\n", " #Uniform Cost Search uses Best First Search algorithm with f(n) = g(n)\n", " iterations, all_node_colors, node = best_first_graph_search(problem, lambda node: node.path_cost)\n", " return(iterations, all_node_colors, node)" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "46b8200b4a8f47e7b18145234a8469da" } }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "ca9b2d01bbd5458bb037585c719d73fc" } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", "display_visual(user_input = False, algorithm = uniform_cost_search, problem = romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## GREEDY BEST FIRST SEARCH\n", "Let's change all the node_colors to starting position and define a different problem statement." ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def greedy_best_first_search(problem, h=None):\n", " \"\"\"Greedy Best-first graph search is an informative searching algorithm with f(n) = h(n).\n", " You need to specify the h function when you call best_first_search, or\n", " else in your Problem subclass.\"\"\"\n", " h = memoize(h or problem.h, 'h')\n", " iterations, all_node_colors, node = best_first_graph_search(problem, lambda n: h(n))\n", " return(iterations, all_node_colors, node)" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "e3ddd0260d7d4a8aa62d610976b9568a" } }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "dae485b1f4224c34a88de42d252da76c" } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", "display_visual(user_input = False, algorithm = greedy_best_first_search, problem = romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A\\* SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def astar_search(problem, h=None):\n", " \"\"\"A* search is best-first graph search with f(n) = g(n)+h(n).\n", " You need to specify the h function when you call astar_search, or\n", " else in your Problem subclass.\"\"\"\n", " h = memoize(h or problem.h, 'h')\n", " iterations, all_node_colors, node = best_first_graph_search(problem, lambda n: n.path_cost + h(n))\n", " return(iterations, all_node_colors, node)" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "15a78d815f0c4ea589cdd5ad40bc8794" } }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "10450687dd574be2a380e9e40403fa83" } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", "display_visual(user_input = False, algorithm = astar_search, problem = romania_problem)" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "scrolled": false }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "9019790cf8324d73966373bb3f5373a8" } }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "b8a3195598da472d996e4e8b81595cb7" } }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "aabe167a0d6440f0a020df8a85a9206c" } }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "25d146d187004f4f9db6a7dccdbc7e93" } }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "68d532810a9e46309415fd353c474a4d" } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "all_node_colors = []\n", "# display_visual(user_input = True, algorithm = breadth_first_tree_search)\n", "display_visual(user_input = True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A* Heuristics\n", "\n", "Different heuristics provide different efficiency in solving A* problems which are generally defined by the number of explored nodes as well as the branching factor. With the classic 8 puzzle we can show the efficiency of different heuristics through the number of explored nodes.\n", "\n", "### 8 Puzzle Problem\n", "\n", "The *8 Puzzle Problem* consists of a 3x3 tray in which the goal is to get the initial configuration to the goal state by shifting the numbered tiles into the blank space.\n", "\n", "example:- \n", "\n", " Initial State Goal State\n", " | 7 | 2 | 4 | | 0 | 1 | 2 |\n", " | 5 | 0 | 6 | | 3 | 4 | 5 |\n", " | 8 | 3 | 1 | | 6 | 7 | 8 |\n", " \n", "We have a total of 9 blank tiles giving us a total of 9! initial configuration but not all of these are solvable. The solvability of a configuration can be checked by calculating the Inversion Permutation. If the total Inversion Permutation is even then the initial configuration is solvable else the initial configuration is not solvable which means that only 9!/2 initial states lead to a solution.\n", "\n", "#### Heuristics :-\n", "\n", "1) Manhattan Distance:- For the 8 puzzle problem Manhattan distance is defined as the distance of a tile from its goal state( for the tile numbered '1' in the initial configuration Manhattan distance is 4 \"2 for left and 2 for upward displacement\").\n", "\n", "2) No. of Misplaced Tiles:- The heuristic calculates the number of misplaced tiles between the current state and goal state.\n", "\n", "3) Sqrt of Manhattan Distance:- It calculates the square root of Manhattan distance.\n", "\n", "4) Max Heuristic:- It assign the score as the maximum between \"Manhattan Distance\" and \"No. of Misplaced Tiles\". " ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "# Heuristics for 8 Puzzle Problem\n", "\n", "def linear(state,goal):\n", " return sum([1 if state[i] != goal[i] else 0 for i in range(8)])\n", "\n", "def manhanttan(state,goal):\n", " index_goal = {0:[2,2], 1:[0,0], 2:[0,1], 3:[0,2], 4:[1,0], 5:[1,1], 6:[1,2], 7:[2,0], 8:[2,1]}\n", " index_state = {}\n", " index = [[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]]\n", " x, y = 0, 0\n", " \n", " for i in range(len(state)):\n", " index_state[state[i]] = index[i]\n", " \n", " mhd = 0\n", " \n", " for i in range(8):\n", " for j in range(2):\n", " mhd = abs(index_goal[i][j] - index_state[i][j]) + mhd\n", " \n", " return mhd\n", "\n", "def sqrt_manhanttan(state,goal):\n", " index_goal = {0:[2,2], 1:[0,0], 2:[0,1], 3:[0,2], 4:[1,0], 5:[1,1], 6:[1,2], 7:[2,0], 8:[2,1]}\n", " index_state = {}\n", " index = [[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]]\n", " x, y = 0, 0\n", " \n", " for i in range(len(state)):\n", " index_state[state[i]] = index[i]\n", " \n", " mhd = 0\n", " \n", " for i in range(8):\n", " for j in range(2):\n", " mhd = (index_goal[i][j] - index_state[i][j])**2 + mhd\n", " \n", " return math.sqrt(mhd)\n", "\n", "def max_heuristic(state,goal):\n", " score1 = manhanttan(state, goal)\n", " score2 = linear(state, goal)\n", " return max(score1, score2)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "True\n", "Number of explored nodes by the following heuristic are: 145\n", "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", "[2, 4, 3, 1, 5, 6, 7, 0, 8]\n", "[2, 4, 3, 1, 0, 6, 7, 5, 8]\n", "[2, 0, 3, 1, 4, 6, 7, 5, 8]\n", "[0, 2, 3, 1, 4, 6, 7, 5, 8]\n", "[1, 2, 3, 0, 4, 6, 7, 5, 8]\n", "[1, 2, 3, 4, 0, 6, 7, 5, 8]\n", "[1, 2, 3, 4, 5, 6, 7, 0, 8]\n", "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n", "Number of explored nodes by the following heuristic are: 153\n", "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", "[2, 4, 3, 1, 5, 6, 7, 0, 8]\n", "[2, 4, 3, 1, 0, 6, 7, 5, 8]\n", "[2, 0, 3, 1, 4, 6, 7, 5, 8]\n", "[0, 2, 3, 1, 4, 6, 7, 5, 8]\n", "[1, 2, 3, 0, 4, 6, 7, 5, 8]\n", "[1, 2, 3, 4, 0, 6, 7, 5, 8]\n", "[1, 2, 3, 4, 5, 6, 7, 0, 8]\n", "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n", "Number of explored nodes by the following heuristic are: 145\n", "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", "[2, 4, 3, 1, 5, 6, 7, 0, 8]\n", "[2, 4, 3, 1, 0, 6, 7, 5, 8]\n", "[2, 0, 3, 1, 4, 6, 7, 5, 8]\n", "[0, 2, 3, 1, 4, 6, 7, 5, 8]\n", "[1, 2, 3, 0, 4, 6, 7, 5, 8]\n", "[1, 2, 3, 4, 0, 6, 7, 5, 8]\n", "[1, 2, 3, 4, 5, 6, 7, 0, 8]\n", "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n", "Number of explored nodes by the following heuristic are: 169\n", "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", "[2, 4, 3, 1, 5, 6, 7, 0, 8]\n", "[2, 4, 3, 1, 0, 6, 7, 5, 8]\n", "[2, 0, 3, 1, 4, 6, 7, 5, 8]\n", "[0, 2, 3, 1, 4, 6, 7, 5, 8]\n", "[1, 2, 3, 0, 4, 6, 7, 5, 8]\n", "[1, 2, 3, 4, 0, 6, 7, 5, 8]\n", "[1, 2, 3, 4, 5, 6, 7, 0, 8]\n", "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n" ] } ], "source": [ "# Solving the puzzle \n", "puzzle = EightPuzzle()\n", "puzzle.checkSolvability([2,4,3,1,5,6,7,8,0]) # checks whether the initialized configuration is solvable or not\n", "puzzle.solve([2,4,3,1,5,6,7,8,0], [1,2,3,4,5,6,7,8,0],max_heuristic) # Max_heuristic\n", "puzzle.solve([2,4,3,1,5,6,7,8,0], [1,2,3,4,5,6,7,8,0],linear) # Linear\n", "puzzle.solve([2,4,3,1,5,6,7,8,0], [1,2,3,4,5,6,7,8,0],manhanttan) # Manhattan\n", "puzzle.solve([2,4,3,1,5,6,7,8,0], [1,2,3,4,5,6,7,8,0],sqrt_manhanttan) # Sqrt_manhattan" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## GENETIC ALGORITHM\n", "\n", "Genetic algorithms (or GA) are inspired by natural evolution and are particularly useful in optimization and search problems with large state spaces.\n", "\n", "Given a problem, algorithms in the domain make use of a *population* of solutions (also called *states*), where each solution/state represents a feasible solution. At each iteration (often called *generation*), the population gets updated using methods inspired by biology and evolution, like *crossover*, *mutation* and *natural selection*." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Overview\n", "\n", "A genetic algorithm works in the following way:\n", "\n", "1) Initialize random population.\n", "\n", "2) Calculate population fitness.\n", "\n", "3) Select individuals for mating.\n", "\n", "4) Mate selected individuals to produce new population.\n", "\n", " * Random chance to mutate individuals.\n", "\n", "5) Repeat from step 2) until an individual is fit enough or the maximum number of iterations was reached." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Glossary\n", "\n", "Before we continue, we will lay the basic terminology of the algorithm.\n", "\n", "* Individual/State: A list of elements (called *genes*) that represent possible solutions.\n", "\n", "* Population: The list of all the individuals/states.\n", "\n", "* Gene pool: The alphabet of possible values for an individual's genes.\n", "\n", "* Generation/Iteration: The number of times the population will be updated.\n", "\n", "* Fitness: An individual's score, calculated by a function specific to the problem." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Crossover\n", "\n", "Two individuals/states can \"mate\" and produce one child. This offspring bears characteristics from both of its parents. There are many ways we can implement this crossover. Here we will take a look at the most common ones. Most other methods are variations of those below.\n", "\n", "* Point Crossover: The crossover occurs around one (or more) point. The parents get \"split\" at the chosen point or points and then get merged. In the example below we see two parents get split and merged at the 3rd digit, producing the following offspring after the crossover.\n", "\n", "\n", "\n", "* Uniform Crossover: This type of crossover chooses randomly the genes to get merged. Here the genes 1, 2 and 5 were chosen from the first parent, so the genes 3, 4 were added by the second parent.\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Mutation\n", "\n", "When an offspring is produced, there is a chance it will mutate, having one (or more, depending on the implementation) of its genes altered.\n", "\n", "For example, let's say the new individual to undergo mutation is \"abcde\". Randomly we pick to change its third gene to 'z'. The individual now becomes \"abzde\" and is added to the population." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Selection\n", "\n", "At each iteration, the fittest individuals are picked randomly to mate and produce offsprings. We measure an individual's fitness with a *fitness function*. That function depends on the given problem and it is used to score an individual. Usually the higher the better.\n", "\n", "The selection process is this:\n", "\n", "1) Individuals are scored by the fitness function.\n", "\n", "2) Individuals are picked randomly, according to their score (higher score means higher chance to get picked). Usually the formula to calculate the chance to pick an individual is the following (for population *P* and individual *i*):\n", "\n", "$$ chance(i) = \\dfrac{fitness(i)}{\\sum_{k \\, in \\, P}{fitness(k)}} $$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Implementation\n", "\n", "Below we look over the implementation of the algorithm in the `search` module.\n", "\n", "First the implementation of the main core of the algorithm:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "def genetic_algorithm(population, fitness_fn, gene_pool=[0, 1], f_thres=None, ngen=1000, pmut=0.1):\n",
" """[Figure 4.8]"""\n",
" for i in range(ngen):\n",
" population = [mutate(recombine(*select(2, population, fitness_fn)), gene_pool, pmut)\n",
" for i in range(len(population))]\n",
"\n",
" fittest_individual = fitness_threshold(fitness_fn, f_thres, population)\n",
" if fittest_individual:\n",
" return fittest_individual\n",
"\n",
"\n",
" return argmax(population, key=fitness_fn)\n",
"
def recombine(x, y):\n",
" n = len(x)\n",
" c = random.randrange(0, n)\n",
" return x[:c] + y[c:]\n",
"
def mutate(x, gene_pool, pmut):\n",
" if random.uniform(0, 1) >= pmut:\n",
" return x\n",
"\n",
" n = len(x)\n",
" g = len(gene_pool)\n",
" c = random.randrange(0, n)\n",
" r = random.randrange(0, g)\n",
"\n",
" new_gene = gene_pool[r]\n",
" return x[:c] + [new_gene] + x[c+1:]\n",
"
def init_population(pop_number, gene_pool, state_length):\n",
" """Initializes population for genetic algorithm\n",
" pop_number : Number of individuals in population\n",
" gene_pool : List of possible values for individuals\n",
" state_length: The length of each individual"""\n",
" g = len(gene_pool)\n",
" population = []\n",
" for i in range(pop_number):\n",
" new_individual = [gene_pool[random.randrange(0, g)] for j in range(state_length)]\n",
" population.append(new_individual)\n",
"\n",
" return population\n",
"
def genetic_algorithm(population, fitness_fn, gene_pool=[0, 1], f_thres=None, ngen=1000, pmut=0.1):\n",
" """[Figure 4.8]"""\n",
" for i in range(ngen):\n",
" population = [mutate(recombine(*select(2, population, fitness_fn)), gene_pool, pmut)\n",
" for i in range(len(population))]\n",
"\n",
" fittest_individual = fitness_threshold(fitness_fn, f_thres, population)\n",
" if fittest_individual:\n",
" return fittest_individual\n",
"\n",
"\n",
" return argmax(population, key=fitness_fn)\n",
"