From 34997e48547a57939d568f170c60e442c2d54db5 Mon Sep 17 00:00:00 2001 From: Devesh Sawant Date: Tue, 13 Mar 2018 00:11:41 +0530 Subject: [PATCH 1/9] test case for zebra problem --- tests/test_csp.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_csp.py b/tests/test_csp.py index f63e657aa..b1e3ab3ad 100644 --- a/tests/test_csp.py +++ b/tests/test_csp.py @@ -382,6 +382,8 @@ def test_tree_csp_solver(): assert (tcs['NT'] == 'R' and tcs['WA'] == 'B' and tcs['Q'] == 'B' and tcs['NSW'] == 'R' and tcs['V'] == 'B') or \ (tcs['NT'] == 'B' and tcs['WA'] == 'R' and tcs['Q'] == 'R' and tcs['NSW'] == 'B' and tcs['V'] == 'R') - +def test_zebra_problem(): + zebra_solution = solve_zebra() + assert(zebra_solution[0] == 5 and zebra_solution[1] == 1) if __name__ == "__main__": pytest.main() From 1da06b8beda9c08025456385e932f2e486ab0ca7 Mon Sep 17 00:00:00 2001 From: Devesh Sawant Date: Wed, 14 Mar 2018 19:14:06 +0530 Subject: [PATCH 2/9] Revert "Merge remote-tracking branch 'upstream/master'" This reverts commit 5ceab1a27ff73b410200c2f84d22e0a1d79b6fbe, reversing changes made to 34997e48547a57939d568f170c60e442c2d54db5. --- .travis.yml | 2 - README.md | 2 +- notebook.py | 156 --- search.ipynb | 2280 +++++++++++++++++++++++++++++++++++++----- search.py | 56 +- tests/test_agents.py | 35 +- 6 files changed, 2028 insertions(+), 503 deletions(-) diff --git a/.travis.yml b/.travis.yml index 600d6bd00..e0932e6b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,8 +12,6 @@ install: - pip install flake8 - pip install ipython - pip install matplotlib - - pip install networkx - - pip install ipywidgets script: - py.test diff --git a/README.md b/README.md index 968632477..a793deb30 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 2.3 | Table-Driven-Vacuum-Agent | `TableDrivenVacuumAgent` | [`agents.py`][agents] | Done | Included | | 2.7 | Table-Driven-Agent | `TableDrivenAgent` | [`agents.py`][agents] | Done | Included | | 2.8 | Reflex-Vacuum-Agent | `ReflexVacuumAgent` | [`agents.py`][agents] | Done | Included | -| 2.10 | Simple-Reflex-Agent | `SimpleReflexAgent` | [`agents.py`][agents] | Done | Included | +| 2.10 | Simple-Reflex-Agent | `SimpleReflexAgent` | [`agents.py`][agents] | | Included | | 2.12 | Model-Based-Reflex-Agent | `ReflexAgentWithState` | [`agents.py`][agents] | | Included | | 3 | Problem | `Problem` | [`search.py`][search] | Done | Included | | 3 | Node | `Node` | [`search.py`][search] | Done | Included | diff --git a/notebook.py b/notebook.py index ae0976900..6e1a0fbfc 100644 --- a/notebook.py +++ b/notebook.py @@ -886,159 +886,3 @@ def draw_table(self): self.fill(0, 0, 0) self.text_n(self.table[self.context[0]][self.context[1]] if self.context else "Click for text", 0.025, 0.975) self.update() - -############################################################################################################ - -##################### Functions to assist plotting in search.ipynb #################### - -############################################################################################################ -import networkx as nx -import matplotlib.pyplot as plt -from matplotlib import lines - -from ipywidgets import interact -import ipywidgets as widgets -from IPython.display import display -import time -from search import GraphProblem, romania_map - -def show_map(graph_data, node_colors = None): - G = nx.Graph(graph_data['graph_dict']) - node_colors = node_colors or graph_data['node_colors'] - node_positions = graph_data['node_positions'] - node_label_pos = graph_data['node_label_positions'] - edge_weights= graph_data['edge_weights'] - - # set the size of the plot - plt.figure(figsize=(18,13)) - # draw the graph (both nodes and edges) with locations from romania_locations - nx.draw(G, pos = {k : node_positions[k] for k in G.nodes()}, - node_color = [node_colors[node] for node in G.nodes()], linewidths = 0.3, edgecolors = 'k') - - # draw labels for nodes - node_label_handles = nx.draw_networkx_labels(G, pos = node_label_pos, font_size = 14) - - # add a white bounding box behind the node labels - [label.set_bbox(dict(facecolor='white', edgecolor='none')) for label in node_label_handles.values()] - - # add edge lables to the graph - nx.draw_networkx_edge_labels(G, pos = node_positions, edge_labels = edge_weights, font_size = 14) - - # add a legend - white_circle = lines.Line2D([], [], color="white", marker='o', markersize=15, markerfacecolor="white") - orange_circle = lines.Line2D([], [], color="orange", marker='o', markersize=15, markerfacecolor="orange") - red_circle = lines.Line2D([], [], color="red", marker='o', markersize=15, markerfacecolor="red") - gray_circle = lines.Line2D([], [], color="gray", marker='o', markersize=15, markerfacecolor="gray") - green_circle = lines.Line2D([], [], color="green", marker='o', markersize=15, markerfacecolor="green") - plt.legend((white_circle, orange_circle, red_circle, gray_circle, green_circle), - ('Un-explored', 'Frontier', 'Currently Exploring', 'Explored', 'Final Solution'), - numpoints=1,prop={'size':16}, loc=(.8,.75)) - - # show the plot. No need to use in notebooks. nx.draw will show the graph itself. - plt.show() - -## helper functions for visualisations - -def final_path_colors(initial_node_colors, problem, solution): - "returns a node_colors dict of the final path provided the problem and solution" - - # get initial node colors - final_colors = dict(initial_node_colors) - # color all the nodes in solution and starting node to green - final_colors[problem.initial] = "green" - for node in solution: - final_colors[node] = "green" - return final_colors - -def display_visual(graph_data, user_input, algorithm=None, problem=None): - initial_node_colors = graph_data['node_colors'] - if user_input == False: - def slider_callback(iteration): - # don't show graph for the first time running the cell calling this function - try: - show_map(graph_data, node_colors = all_node_colors[iteration]) - except: - pass - def visualize_callback(Visualize): - if Visualize is True: - button.value = False - - global all_node_colors - - iterations, all_node_colors, node = algorithm(problem) - solution = node.solution() - all_node_colors.append(final_path_colors(all_node_colors[0], problem, solution)) - - slider.max = len(all_node_colors) - 1 - - for i in range(slider.max + 1): - slider.value = i - #time.sleep(.5) - - slider = widgets.IntSlider(min=0, max=1, step=1, value=0) - slider_visual = widgets.interactive(slider_callback, iteration = slider) - display(slider_visual) - - button = widgets.ToggleButton(value = False) - button_visual = widgets.interactive(visualize_callback, Visualize = button) - display(button_visual) - - if user_input == True: - node_colors = dict(initial_node_colors) - if isinstance(algorithm, dict): - assert set(algorithm.keys()).issubset(set(["Breadth First Tree Search", - "Depth First Tree Search", - "Breadth First Search", - "Depth First Graph Search", - "Uniform Cost Search", - "A-star Search"])) - - algo_dropdown = widgets.Dropdown(description = "Search algorithm: ", - options = sorted(list(algorithm.keys())), - value = "Breadth First Tree Search") - display(algo_dropdown) - elif algorithm is None: - print("No algorithm to run.") - return 0 - - def slider_callback(iteration): - # don't show graph for the first time running the cell calling this function - try: - show_map(graph_data, node_colors = all_node_colors[iteration]) - except: - pass - - def visualize_callback(Visualize): - if Visualize is True: - button.value = False - - problem = GraphProblem(start_dropdown.value, end_dropdown.value, romania_map) - global all_node_colors - - user_algorithm = algorithm[algo_dropdown.value] - - iterations, all_node_colors, node = user_algorithm(problem) - solution = node.solution() - all_node_colors.append(final_path_colors(all_node_colors[0], problem, solution)) - - slider.max = len(all_node_colors) - 1 - - for i in range(slider.max + 1): - slider.value = i - #time.sleep(.5) - - start_dropdown = widgets.Dropdown(description = "Start city: ", - options = sorted(list(node_colors.keys())), value = "Arad") - display(start_dropdown) - - end_dropdown = widgets.Dropdown(description = "Goal city: ", - options = sorted(list(node_colors.keys())), value = "Fagaras") - display(end_dropdown) - - button = widgets.ToggleButton(value = False) - button_visual = widgets.interactive(visualize_callback, Visualize = button) - display(button_visual) - - slider = widgets.IntSlider(min=0, max=1, step=1, value=0) - slider_visual = widgets.interactive(slider_callback, iteration = slider) - display(slider_visual) \ No newline at end of file diff --git a/search.ipynb b/search.ipynb index 1ac4b075a..edcdf592f 100644 --- a/search.ipynb +++ b/search.ipynb @@ -13,15 +13,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 134, "metadata": { - "collapsed": true, "scrolled": true }, "outputs": [], "source": [ "from search import *\n", - "from notebook import psource, show_map, final_path_colors, display_visual\n", + "from notebook import psource\n", "\n", "# Needed to hide warnings in the matplotlib sections\n", "import warnings\n", @@ -74,32 +73,6 @@ "*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": [ - "For visualisations, 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. These are imported as required in `notebook.py`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "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": {}, @@ -111,9 +84,159 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 135, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
class Problem(object):\n",
+       "\n",
+       "    """The abstract class for a formal problem. You should subclass\n",
+       "    this and implement the methods actions and result, and possibly\n",
+       "    __init__, goal_test, and path_cost. Then you will create instances\n",
+       "    of your subclass and solve them with the various search functions."""\n",
+       "\n",
+       "    def __init__(self, initial, goal=None):\n",
+       "        """The constructor specifies the initial state, and possibly a goal\n",
+       "        state, if there is a unique goal. Your subclass's constructor can add\n",
+       "        other arguments."""\n",
+       "        self.initial = initial\n",
+       "        self.goal = goal\n",
+       "\n",
+       "    def actions(self, state):\n",
+       "        """Return the actions that can be executed in the given\n",
+       "        state. The result would typically be a list, but if there are\n",
+       "        many actions, consider yielding them one at a time in an\n",
+       "        iterator, rather than building them all at once."""\n",
+       "        raise NotImplementedError\n",
+       "\n",
+       "    def result(self, state, action):\n",
+       "        """Return the state that results from executing the given\n",
+       "        action in the given state. The action must be one of\n",
+       "        self.actions(state)."""\n",
+       "        raise NotImplementedError\n",
+       "\n",
+       "    def goal_test(self, state):\n",
+       "        """Return True if the state is a goal. The default method compares the\n",
+       "        state to self.goal or checks for state in self.goal if it is a\n",
+       "        list, as specified in the constructor. Override this method if\n",
+       "        checking against a single self.goal is not enough."""\n",
+       "        if isinstance(self.goal, list):\n",
+       "            return is_in(state, self.goal)\n",
+       "        else:\n",
+       "            return state == self.goal\n",
+       "\n",
+       "    def path_cost(self, c, state1, action, state2):\n",
+       "        """Return the cost of a solution path that arrives at state2 from\n",
+       "        state1 via action, assuming cost c to get up to state1. If the problem\n",
+       "        is such that the path doesn't matter, this function will only look at\n",
+       "        state2.  If the path does matter, it will consider c and maybe state1\n",
+       "        and action. The default method costs 1 for every step in the path."""\n",
+       "        return c + 1\n",
+       "\n",
+       "    def value(self, state):\n",
+       "        """For optimization problems, each state has a value.  Hill-climbing\n",
+       "        and related algorithms try to maximize this value."""\n",
+       "        raise NotImplementedError\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(Problem)" ] @@ -153,9 +276,171 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 136, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
class Node:\n",
+       "\n",
+       "    """A node in a search tree. Contains a pointer to the parent (the node\n",
+       "    that this is a successor of) and to the actual state for this node. Note\n",
+       "    that if a state is arrived at by two paths, then there are two nodes with\n",
+       "    the same state.  Also includes the action that got us to this state, and\n",
+       "    the total path_cost (also known as g) to reach the node.  Other functions\n",
+       "    may add an f and h value; see best_first_graph_search and astar_search for\n",
+       "    an explanation of how the f and h values are handled. You will not need to\n",
+       "    subclass this class."""\n",
+       "\n",
+       "    def __init__(self, state, parent=None, action=None, path_cost=0):\n",
+       "        """Create a search tree Node, derived from a parent by an action."""\n",
+       "        self.state = state\n",
+       "        self.parent = parent\n",
+       "        self.action = action\n",
+       "        self.path_cost = path_cost\n",
+       "        self.depth = 0\n",
+       "        if parent:\n",
+       "            self.depth = parent.depth + 1\n",
+       "\n",
+       "    def __repr__(self):\n",
+       "        return "<Node {}>".format(self.state)\n",
+       "\n",
+       "    def __lt__(self, node):\n",
+       "        return self.state < node.state\n",
+       "\n",
+       "    def expand(self, problem):\n",
+       "        """List the nodes reachable in one step from this node."""\n",
+       "        return [self.child_node(problem, action)\n",
+       "                for action in problem.actions(self.state)]\n",
+       "\n",
+       "    def child_node(self, problem, action):\n",
+       "        """[Figure 3.10]"""\n",
+       "        next = problem.result(self.state, action)\n",
+       "        return Node(next, self, action,\n",
+       "                    problem.path_cost(self.path_cost, self.state,\n",
+       "                                      action, next))\n",
+       "\n",
+       "    def solution(self):\n",
+       "        """Return the sequence of actions to go from the root to this node."""\n",
+       "        return [node.action for node in self.path()[1:]]\n",
+       "\n",
+       "    def path(self):\n",
+       "        """Return a list of nodes forming the path from the root to this node."""\n",
+       "        node, path_back = self, []\n",
+       "        while node:\n",
+       "            path_back.append(node)\n",
+       "            node = node.parent\n",
+       "        return list(reversed(path_back))\n",
+       "\n",
+       "    # We want for a queue of nodes in breadth_first_search or\n",
+       "    # astar_search to have no duplicated states, so we treat nodes\n",
+       "    # with the same state as equal. [Problem: this may not be what you\n",
+       "    # want in other contexts.]\n",
+       "\n",
+       "    def __eq__(self, other):\n",
+       "        return isinstance(other, Node) and self.state == other.state\n",
+       "\n",
+       "    def __hash__(self):\n",
+       "        return hash(self.state)\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(Node)" ] @@ -194,9 +479,148 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 137, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
class GraphProblem(Problem):\n",
+       "\n",
+       "    """The problem of searching a graph from one node to another."""\n",
+       "\n",
+       "    def __init__(self, initial, goal, graph):\n",
+       "        Problem.__init__(self, initial, goal)\n",
+       "        self.graph = graph\n",
+       "\n",
+       "    def actions(self, A):\n",
+       "        """The actions at a graph node are just its neighbors."""\n",
+       "        return list(self.graph.get(A).keys())\n",
+       "\n",
+       "    def result(self, state, action):\n",
+       "        """The result of going to a neighbor is just that neighbor."""\n",
+       "        return action\n",
+       "\n",
+       "    def path_cost(self, cost_so_far, A, action, B):\n",
+       "        return cost_so_far + (self.graph.get(A, B) or infinity)\n",
+       "\n",
+       "    def find_min_edge(self):\n",
+       "        """Find minimum value of edges."""\n",
+       "        m = infinity\n",
+       "        for d in self.graph.dict.values():\n",
+       "            local_min = min(d.values())\n",
+       "            m = min(m, local_min)\n",
+       "\n",
+       "        return m\n",
+       "\n",
+       "    def h(self, node):\n",
+       "        """h function is straight-line distance from a node's state to goal."""\n",
+       "        locs = getattr(self.graph, 'locations', None)\n",
+       "        if locs:\n",
+       "            if type(node) is str:\n",
+       "                return int(distance(locs[node], locs[self.goal]))\n",
+       "\n",
+       "            return int(distance(locs[node.state], locs[self.goal]))\n",
+       "        else:\n",
+       "            return infinity\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(GraphProblem)" ] @@ -210,10 +634,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 138, + "metadata": {}, "outputs": [], "source": [ "romania_map = UndirectedGraph(dict(\n", @@ -257,10 +679,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 139, + "metadata": {}, "outputs": [], "source": [ "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)" @@ -284,14 +704,46 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 140, "metadata": {}, - "outputs": [], + "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": 141, + "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": {}, @@ -301,24 +753,46 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 142, + "metadata": {}, "outputs": [], "source": [ - "# node colors, node positions and node label positions\n", - "node_colors = {node: 'white' for node in romania_map.locations.keys()}\n", - "node_positions = romania_map.locations\n", - "node_label_pos = { k:[v[0],v[1]-10] for k,v in romania_map.locations.items() }\n", - "edge_weights = {(k, k2) : v2 for k, v in romania_map.graph_dict.items() for k2, v2 in v.items()}\n", + "# 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", - "romania_graph_data = { 'graph_dict' : romania_map.graph_dict,\n", - " 'node_colors': node_colors,\n", - " 'node_positions': node_positions,\n", - " 'node_label_positions': node_label_pos,\n", - " 'edge_weights': edge_weights\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" ] }, { @@ -328,6 +802,40 @@ "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": 143, + "metadata": {}, + "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": {}, @@ -337,13 +845,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 144, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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Ll68qNmzZysoKChTeZmYmJixVP3mkvf4+Hi5urpmmSV685rExESVKVNGBQrwz0tOud+ZnQkJCerXr5+WL1+usmXL5mAyAMhb0tPTtXDhQn333XdmRwEAALmIT6MA4IROnDihdu3a6fr164qLi5Ovr6/mzJmj1q1bq0mTJoqIiNDUqVPVokULvf/++ypWrJikv/fvNAxDxYoV09WrV1W0aFFJkqurqyRp3759cnd3zzit/d+zOoOCgnT48GFVqVIly8nxVqtVPj4+KliwYO79IJyQv7+/4uLilJaWdtdSOT09Xb1799aQIUPUunXrXEoIAHnDhg0b5OXlpdq1a5sdBQAA5CJOYwcAJ2QYhvbv3689e/YoISFBu3bt0q5du9SgQQPNmDFDdevW1fnz5xUUFKRGjRqpWrVqCggIUO3ateXm5iYXFxf16dNHR48e1YoVK1SpUiVJUsOGDdWgQQNNnTo1oyD9t5SUFP31118ZJ8b/8/T4EydOyMvLK0sJarVa5evryzLDe1S1alVt3rxZ/v7+d7wuLCxMP/30k77//vuMwhoA8ovnnntO7du310svvWR2FAAAkIsoOwEgHzp8+LCioqK0detW7d+/X7GxsTp69KimT5+uQYMGycXFRXv27FGvXr3UsWNHdejQQZ9++qk2btyoH3/8UXXr1n2gcVNTU3X06NEsJWh0dLTi4+NVoUKFWxahfn5+cnd3t/NPwXE98cQTeuONNxQUFHTba3788Uf16tVLu3fvVsWKFXMxHQCYLzExUdWqVVN8fPxt/zgHAACcE2UnACCDzWbLdIDNV199pUmTJik2NlaNGzfW+PHj1ahRoxwZOy0tTfHx8VlK0OjoaP31118qW7ZslhLUarXK399fHh4eOZIprxoyZIiqV6+u4cOH3/L506dPq0GDBlqwYIHatWuXy+kAwHxTpkzRH3/8oYULF5odBQAA5DLKTgDZFhwcrLNnz2rt2rVmR0EOMvPk9fT0dB07dixLCRodHa3Y2FiVKFEiSwl688vT09OUzDkpOjpaRYoUydhe4J9sNps6duyoevXq6f333zchHQCYyzAM1ahRQ/PmzdMjjzxidhwAAJDLOKAIyAeCg4P12WefSZIKFCigkiVLqmbNmnr++ef10ksv5YkDY24eorNz584cmzmI7DGr6JT+PiDJx8dHPj4+atu2babnbDab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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "show_map(romania_graph_data)" + "show_map(node_colors)" ] }, { @@ -364,9 +883,144 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 145, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
class SimpleProblemSolvingAgentProgram:\n",
+       "\n",
+       "    """Abstract framework for a problem-solving agent. [Figure 3.1]"""\n",
+       "\n",
+       "    def __init__(self, initial_state=None):\n",
+       "        """State is an abstract representation of the state\n",
+       "        of the world, and seq is the list of actions required\n",
+       "        to get to a particular state from the initial state(root)."""\n",
+       "        self.state = initial_state\n",
+       "        self.seq = []\n",
+       "\n",
+       "    def __call__(self, percept):\n",
+       "        """[Figure 3.1] Formulate a goal and problem, then\n",
+       "        search for a sequence of actions to solve it."""\n",
+       "        self.state = self.update_state(self.state, percept)\n",
+       "        if not self.seq:\n",
+       "            goal = self.formulate_goal(self.state)\n",
+       "            problem = self.formulate_problem(self.state, goal)\n",
+       "            self.seq = self.search(problem)\n",
+       "            if not self.seq:\n",
+       "                return None\n",
+       "        return self.seq.pop(0)\n",
+       "\n",
+       "    def update_state(self, percept):\n",
+       "        raise NotImplementedError\n",
+       "\n",
+       "    def formulate_goal(self, state):\n",
+       "        raise NotImplementedError\n",
+       "\n",
+       "    def formulate_problem(self, state, goal):\n",
+       "        raise NotImplementedError\n",
+       "\n",
+       "    def search(self, problem):\n",
+       "        raise NotImplementedError\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(SimpleProblemSolvingAgentProgram)" ] @@ -401,10 +1055,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, + "execution_count": 146, + "metadata": {}, "outputs": [], "source": [ "class vacuumAgent(SimpleProblemSolvingAgentProgram):\n", @@ -444,24 +1096,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Left\n", + "Suck\n", + "Right\n" + ] + } + ], "source": [ - "state1 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", - "state2 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", - "state3 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", - "state4 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", - "state5 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", - "state6 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", - "state7 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", - "state8 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", + " state1 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", + " state2 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", + " state3 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", + " state4 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", + " state5 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", + " state6 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", + " state7 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", + " state8 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", "\n", - "a = vacuumAgent(state1)\n", + " a = vacuumAgent(state1)\n", "\n", - "print(a(state6)) \n", - "print(a(state1))\n", - "print(a(state3))" + " print(a(state6)) \n", + " print(a(state1))\n", + " print(a(state3))" ] }, { @@ -472,42 +1134,157 @@ "\n", "In this section, we have visualizations of the following searching algorithms:\n", "\n", - "1. Breadth First Tree Search\n", - "2. Depth First Tree Search\n", - "3. Breadth First Search\n", - "4. Depth First Graph Search\n", - "5. Best First Graph Search\n", - "6. Uniform Cost Search\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\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" + "* 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": [ - "## 1. BREADTH-FIRST TREE SEARCH\n", + "## 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": null, + "execution_count": 13, "metadata": { "collapsed": true }, "outputs": [], "source": [ - "def tree_search_for_vis(problem, frontier):\n", + "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", @@ -515,7 +1292,7 @@ " # we use these two variables at the time of visualisations\n", " iterations = 0\n", " all_node_colors = []\n", - " node_colors = {k : 'white' for k in problem.graph.nodes()}\n", + " node_colors = dict(initial_node_colors)\n", " \n", " #Adding first node to the queue\n", " frontier.append(Node(problem.initial))\n", @@ -556,7 +1333,7 @@ "\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_for_vis(problem, FIFOQueue())\n", + " iterations, all_node_colors, node = tree_search(problem, FIFOQueue())\n", " return(iterations, all_node_colors, node)" ] }, @@ -569,29 +1346,45 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, - "outputs": [], + "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", - "a, b, c = breadth_first_tree_search(romania_problem)\n", - "display_visual(romania_graph_data, user_input=False, \n", - " algorithm=breadth_first_tree_search, \n", - " problem=romania_problem)" + "display_visual(user_input = False, algorithm = breadth_first_tree_search, problem = romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 2. Depth-First Tree Search:\n", + "## Depth-First Tree Search:\n", "Now let's discuss another searching algorithm, Depth-First Tree Search." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": { "collapsed": true }, @@ -601,21 +1394,38 @@ " \"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_for_vis(problem, Stack())\n", + " iterations, all_node_colors, node = tree_search(problem, Stack())\n", " return(iterations, all_node_colors, node)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, - "outputs": [], + "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(romania_graph_data, user_input=False, \n", - " algorithm=depth_first_tree_search, \n", - " problem=romania_problem)" + "display_visual(user_input = False, algorithm = depth_first_tree_search, problem = romania_problem)" ] }, { @@ -624,14 +1434,14 @@ "collapsed": true }, "source": [ - "## 3. BREADTH-FIRST GRAPH SEARCH\n", + "## 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": null, + "execution_count": 17, "metadata": { "collapsed": true }, @@ -643,7 +1453,7 @@ " # we use these two variables at the time of visualisations\n", " iterations = 0\n", " all_node_colors = []\n", - " node_colors = {k : 'white' for k in problem.graph.nodes()}\n", + " node_colors = dict(initial_node_colors)\n", " \n", " node = Node(problem.initial)\n", " \n", @@ -695,41 +1505,58 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": {}, - "outputs": [], + "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(romania_graph_data, user_input=False, \n", - " algorithm=breadth_first_search, \n", - " problem=romania_problem)" + "display_visual(user_input = False, algorithm = breadth_first_search, problem = romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 4. Depth-First Graph Search: \n", + "## 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": null, + "execution_count": 19, "metadata": { "collapsed": true }, "outputs": [], "source": [ - "def graph_search_for_vis(problem, frontier):\n", + "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 = {k : 'white' for k in problem.graph.nodes()}\n", + " node_colors = dict(initial_node_colors)\n", " \n", " frontier.append(Node(problem.initial))\n", " explored = set()\n", @@ -776,41 +1603,58 @@ "\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_for_vis(problem, Stack())\n", + " iterations, all_node_colors, node = graph_search(problem, Stack())\n", " return(iterations, all_node_colors, node)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": {}, - "outputs": [], + "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(romania_graph_data, user_input=False, \n", - " algorithm=depth_first_graph_search, \n", - " problem=romania_problem)" + "display_visual(user_input = False, algorithm = depth_first_graph_search, problem = romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 5. BEST FIRST SEARCH\n", + "## 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": null, + "execution_count": 21, "metadata": { "collapsed": true }, "outputs": [], "source": [ - "def best_first_graph_search_for_vis(problem, f):\n", + "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", @@ -822,7 +1666,7 @@ " # we use these two variables at the time of visualisations\n", " iterations = 0\n", " all_node_colors = []\n", - " node_colors = {k : 'white' for k in problem.graph.nodes()}\n", + " node_colors = dict(initial_node_colors)\n", " \n", " f = memoize(f, 'f')\n", " node = Node(problem.initial)\n", @@ -884,14 +1728,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 6. UNIFORM COST SEARCH\n", + "## 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": null, + "execution_count": 22, "metadata": { "collapsed": true }, @@ -900,21 +1744,38 @@ "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_for_vis(problem, lambda node: node.path_cost)\n", - " return(iterations, all_node_colors, node)\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": null, + "execution_count": 23, "metadata": {}, - "outputs": [], + "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(romania_graph_data, user_input=False, \n", - " algorithm=uniform_cost_search, \n", - " problem=romania_problem)" + "display_visual(user_input = False, algorithm = uniform_cost_search, problem = romania_problem)" ] }, { @@ -927,7 +1788,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": { "collapsed": true }, @@ -938,35 +1799,52 @@ " 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_for_vis(problem, lambda n: h(n))\n", - " return(iterations, all_node_colors, node)\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": null, + "execution_count": 25, "metadata": {}, - "outputs": [], + "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(romania_graph_data, user_input=False, \n", - " algorithm=greedy_best_first_search, \n", - " problem=romania_problem)" + "display_visual(user_input = False, algorithm = greedy_best_first_search, problem = romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 9. A\\* SEARCH\n", + "## 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": null, + "execution_count": 25, "metadata": { "collapsed": true }, @@ -977,41 +1855,97 @@ " 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_for_vis(problem, \n", - " lambda n: n.path_cost + h(n))\n", - " return(iterations, all_node_colors, node)\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": null, + "execution_count": 26, "metadata": {}, - "outputs": [], + "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(romania_graph_data, user_input=False, \n", - " algorithm=astar_search, \n", - " problem=romania_problem)" + "display_visual(user_input = False, algorithm = astar_search, problem = romania_problem)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": { "scrolled": false }, - "outputs": [], + "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(romania_graph_data, user_input=True, algorithm=breadth_first_tree_search)\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", - "display_visual(romania_graph_data, algorithm=algorithms, user_input=True)" + "# display_visual(user_input = True, algorithm = breadth_first_tree_search)\n", + "display_visual(user_input = True)" ] }, { @@ -1048,7 +1982,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": { "collapsed": true }, @@ -1101,9 +2035,57 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "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", @@ -1135,11 +2117,124 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def hill_climbing(problem):\n",
+       "    """From the initial node, keep choosing the neighbor with highest value,\n",
+       "    stopping when no neighbor is better. [Figure 4.2]"""\n",
+       "    current = Node(problem.initial)\n",
+       "    while True:\n",
+       "        neighbors = current.expand(problem)\n",
+       "        if not neighbors:\n",
+       "            break\n",
+       "        neighbor = argmax_random_tie(neighbors,\n",
+       "                                     key=lambda node: problem.value(node.state))\n",
+       "        if problem.value(neighbor.state) <= problem.value(current.state):\n",
+       "            break\n",
+       "        current = neighbor\n",
+       "    return current.state\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(hill_climbing)" ] @@ -1157,7 +2252,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "metadata": { "collapsed": true }, @@ -1209,11 +2304,17 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['Arad', 'Bucharest', 'Craiova', 'Drobeta', 'Eforie', 'Fagaras', 'Giurgiu', 'Hirsova', 'Iasi', 'Lugoj', 'Mehadia', 'Neamt', 'Oradea', 'Pitesti', 'Rimnicu', 'Sibiu', 'Timisoara', 'Urziceni', 'Vaslui', 'Zerind']\n" + ] + } + ], "source": [ "distances = {}\n", "all_cities = []\n", @@ -1235,7 +2336,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "metadata": { "collapsed": true }, @@ -1262,7 +2363,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "metadata": { "collapsed": true }, @@ -1311,7 +2412,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 38, "metadata": { "collapsed": true }, @@ -1330,11 +2431,39 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['Fagaras',\n", + " 'Neamt',\n", + " 'Iasi',\n", + " 'Vaslui',\n", + " 'Hirsova',\n", + " 'Eforie',\n", + " 'Urziceni',\n", + " 'Bucharest',\n", + " 'Giurgiu',\n", + " 'Pitesti',\n", + " 'Craiova',\n", + " 'Drobeta',\n", + " 'Mehadia',\n", + " 'Lugoj',\n", + " 'Timisoara',\n", + " 'Arad',\n", + " 'Zerind',\n", + " 'Oradea',\n", + " 'Sibiu',\n", + " 'Rimnicu']" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "hill_climbing(tsp)" ] @@ -1458,11 +2587,122 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\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",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(genetic_algorithm)" ] @@ -1499,11 +2739,114 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def recombine(x, y):\n",
+       "    n = len(x)\n",
+       "    c = random.randrange(0, n)\n",
+       "    return x[:c] + y[c:]\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(recombine)" ] @@ -1519,11 +2862,121 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\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",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(mutate)" ] @@ -1539,11 +2992,122 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\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",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(init_population)" ] @@ -1595,7 +3159,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": { "collapsed": true }, @@ -1615,7 +3179,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "metadata": { "collapsed": true }, @@ -1641,7 +3205,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "metadata": { "collapsed": true }, @@ -1659,7 +3223,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "metadata": { "collapsed": true }, @@ -1677,7 +3241,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "metadata": { "collapsed": true }, @@ -1702,7 +3266,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 38, "metadata": { "collapsed": true }, @@ -1720,7 +3284,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "metadata": { "collapsed": true }, @@ -1731,7 +3295,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 40, "metadata": { "collapsed": true }, @@ -1750,7 +3314,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 41, "metadata": { "collapsed": true }, @@ -1772,7 +3336,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 42, "metadata": { "collapsed": true }, @@ -1790,7 +3354,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 43, "metadata": { "collapsed": true }, @@ -1808,11 +3372,17 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['j', 'F', 'm', 'F', 'N', 'i', 'c', 'v', 'm', 'j', 'V', 'o', 'd', 'r', 't', 'V', 'H']\n" + ] + } + ], "source": [ "print(current_best)" ] @@ -1826,11 +3396,17 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "jFmFNicvmjVodrtVH\n" + ] + } + ], "source": [ "current_best_string = ''.join(current_best)\n", "print(current_best_string)" @@ -1849,7 +3425,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 46, "metadata": { "collapsed": true }, @@ -1873,7 +3449,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 47, "metadata": { "collapsed": true }, @@ -1904,11 +3480,122 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\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",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(genetic_algorithm)" ] @@ -1922,11 +3609,17 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Current best: Genetic Algorithm\t\tGeneration: 472\t\tFitness: 17\r" + ] + } + ], "source": [ "population = init_population(max_population, gene_pool, len(target))\n", "solution, generations = genetic_algorithm_stepwise(population, fitness_fn, gene_pool, f_thres, ngen, mutation_rate)" @@ -1969,7 +3662,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "collapsed": true }, @@ -1994,11 +3687,17 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[['R', 'G', 'G', 'R'], ['R', 'G', 'R', 'R'], ['G', 'R', 'G', 'R'], ['R', 'G', 'R', 'G'], ['G', 'R', 'R', 'G'], ['G', 'R', 'G', 'R'], ['G', 'R', 'R', 'R'], ['R', 'G', 'G', 'G']]\n" + ] + } + ], "source": [ "population = init_population(8, ['R', 'G'], 4)\n", "print(population)" @@ -2015,7 +3714,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": { "collapsed": true }, @@ -2034,11 +3733,17 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['R', 'G', 'R', 'G']\n" + ] + } + ], "source": [ "solution = genetic_algorithm(population, fitness, gene_pool=['R', 'G'])\n", "print(solution)" @@ -2053,11 +3758,17 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4\n" + ] + } + ], "source": [ "print(fitness(solution))" ] @@ -2092,11 +3803,17 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[0, 2, 7, 1, 7, 3, 2, 4], [2, 7, 5, 4, 4, 5, 2, 0], [7, 1, 6, 0, 1, 3, 0, 2], [0, 3, 6, 1, 3, 0, 5, 4], [0, 4, 6, 4, 7, 4, 1, 6]]\n" + ] + } + ], "source": [ "population = init_population(100, range(8), 8)\n", "print(population[:5])" @@ -2117,7 +3834,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "collapsed": true }, @@ -2149,11 +3866,18 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[5, 0, 6, 3, 7, 4, 1, 3]\n", + "26\n" + ] + } + ], "source": [ "solution = genetic_algorithm(population, fitness, f_thres=25, gene_pool=range(8))\n", "print(solution)\n", @@ -2191,7 +3915,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.3" + "version": "3.6.4" } }, "nbformat": 4, diff --git a/search.py b/search.py index a80a48c8c..ac834d80c 100644 --- a/search.py +++ b/search.py @@ -109,10 +109,10 @@ def expand(self, problem): def child_node(self, problem, action): """[Figure 3.10]""" - next_node = problem.result(self.state, action) - return Node(next_node, self, action, + next = problem.result(self.state, action) + return Node(next, self, action, problem.path_cost(self.path_cost, self.state, - action, next_node)) + action, next)) def solution(self): """Return the sequence of actions to go from the root to this node.""" @@ -163,7 +163,7 @@ def __call__(self, percept): return None return self.seq.pop(0) - def update_state(self, state, percept): + def update_state(self, percept): raise NotImplementedError def formulate_goal(self, state): @@ -182,7 +182,7 @@ def search(self, problem): def tree_search(problem, frontier): """Search through the successors of a problem to find a goal. The argument frontier should be an empty queue. - Repeats infinites in case of loops. [Figure 3.7]""" + Don't worry about repeated paths to a state. [Figure 3.7]""" frontier.append(Node(problem.initial)) while frontier: node = frontier.pop() @@ -195,7 +195,6 @@ def tree_search(problem, frontier): def graph_search(problem, frontier): """Search through the successors of a problem to find a goal. The argument frontier should be an empty queue. - Does not get trapped by loops. If two paths reach a state, only use the first one. [Figure 3.7]""" frontier.append(Node(problem.initial)) explored = set() @@ -226,11 +225,7 @@ def depth_first_graph_search(problem): def breadth_first_search(problem): - """[Figure 3.11] - Note that this function can be implemented in a - single line as below: - return graph_search(problem, FIFOQueue()) - """ + """[Figure 3.11]""" node = Node(problem.initial) if problem.goal_test(node.state): return node @@ -576,10 +571,10 @@ def simulated_annealing(problem, schedule=exp_schedule()): neighbors = current.expand(problem) if not neighbors: return current.state - next_choice = random.choice(neighbors) - delta_e = problem.value(next_choice.state) - problem.value(current.state) + next = random.choice(neighbors) + delta_e = problem.value(next.state) - problem.value(current.state) if delta_e > 0 or probability(math.exp(delta_e / T)): - current = next_choice + current = next def simulated_annealing_full(problem, schedule=exp_schedule()): """ This version returns all the states encountered in reaching @@ -594,10 +589,10 @@ def simulated_annealing_full(problem, schedule=exp_schedule()): neighbors = current.expand(problem) if not neighbors: return current.state - next_choice = random.choice(neighbors) - delta_e = problem.value(next_choice.state) - problem.value(current.state) + next = random.choice(neighbors) + delta_e = problem.value(next.state) - problem.value(current.state) if delta_e > 0 or probability(math.exp(delta_e / T)): - current = next_choice + current = next def and_or_graph_search(problem): """[Figure 4.11]Used when the environment is nondeterministic and completely observable. @@ -735,10 +730,10 @@ def __init__(self, initial, goal, graph): self.graph = graph def actions(self, state): - return self.graph.graph_dict[state].keys() + return self.graph.dict[state].keys() def output(self, state, action): - return self.graph.graph_dict[state][action] + return self.graph.dict[state][action] def h(self, state): """Returns least possible cost to reach a goal for the given state.""" @@ -925,16 +920,16 @@ class Graph: length of the link from A to B. 'Lengths' can actually be any object at all, and nodes can be any hashable object.""" - def __init__(self, graph_dict=None, directed=True): - self.graph_dict = graph_dict or {} + def __init__(self, dict=None, directed=True): + self.dict = dict or {} self.directed = directed if not directed: self.make_undirected() def make_undirected(self): """Make a digraph into an undirected graph by adding symmetric edges.""" - for a in list(self.graph_dict.keys()): - for (b, dist) in self.graph_dict[a].items(): + for a in list(self.dict.keys()): + for (b, dist) in self.dict[a].items(): self.connect1(b, a, dist) def connect(self, A, B, distance=1): @@ -946,13 +941,13 @@ def connect(self, A, B, distance=1): def connect1(self, A, B, distance): """Add a link from A to B of given distance, in one direction only.""" - self.graph_dict.setdefault(A, {})[B] = distance + self.dict.setdefault(A, {})[B] = distance def get(self, a, b=None): """Return a link distance or a dict of {node: distance} entries. .get(a,b) returns the distance or None; .get(a) returns a dict of {node: distance} entries, possibly {}.""" - links = self.graph_dict.setdefault(a, {}) + links = self.dict.setdefault(a, {}) if b is None: return links else: @@ -960,15 +955,12 @@ def get(self, a, b=None): def nodes(self): """Return a list of nodes in the graph.""" - s1 = set([k for k in self.graph_dict.keys()]) - s2 = set([k2 for v in self.graph_dict.values() for k2, v2 in v.items()]) - nodes = s1.union(s2) - return list(nodes) + return list(self.dict.keys()) -def UndirectedGraph(graph_dict=None): +def UndirectedGraph(dict=None): """Build a Graph where every edge (including future ones) goes both ways.""" - return Graph(graph_dict = graph_dict, directed=False) + return Graph(dict=dict, directed=False) def RandomGraph(nodes=list(range(10)), min_links=2, width=400, height=300, @@ -1105,7 +1097,7 @@ def path_cost(self, cost_so_far, A, action, B): def find_min_edge(self): """Find minimum value of edges.""" m = infinity - for d in self.graph.graph_dict.values(): + for d in self.graph.dict.values(): local_min = min(d.values()) m = min(m, local_min) diff --git a/tests/test_agents.py b/tests/test_agents.py index d5f63bc48..caefe61d4 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -2,8 +2,7 @@ from agents import Direction from agents import Agent from agents import ReflexVacuumAgent, ModelBasedVacuumAgent, TrivialVacuumEnvironment, compare_agents,\ - RandomVacuumAgent, TableDrivenVacuumAgent, TableDrivenAgentProgram, RandomAgentProgram, \ - SimpleReflexAgentProgram, rule_match + RandomVacuumAgent, TableDrivenVacuumAgent, TableDrivenAgentProgram, RandomAgentProgram random.seed("aima-python") @@ -132,38 +131,6 @@ def test_ReflexVacuumAgent() : # check final status of the environment assert environment.status == {(1,0):'Clean' , (0,0) : 'Clean'} -def test_SimpleReflexAgentProgram(): - class Rule: - - def __init__(self, state, action): - self.__state = state - self.action = action - - def matches(self, state): - return self.__state == state - - loc_A = (0, 0) - loc_B = (1, 0) - - # create rules for a two state Vacuum Environment - rules = [Rule((loc_A, "Dirty"), "Suck"), Rule((loc_A, "Clean"), "Right"), - Rule((loc_B, "Dirty"), "Suck"), Rule((loc_B, "Clean"), "Left")] - - def interpret_input(state): - return state - - # create a program and then an object of the SimpleReflexAgentProgram - program = SimpleReflexAgentProgram(rules, interpret_input) - agent = Agent(program) - # create an object of TrivialVacuumEnvironment - environment = TrivialVacuumEnvironment() - # add agent to the environment - environment.add_thing(agent) - # run the environment - environment.run() - # check final status of the environment - assert environment.status == {(1,0):'Clean' , (0,0) : 'Clean'} - def test_ModelBasedVacuumAgent() : # create an object of the ModelBasedVacuumAgent From 807d29ff34b8aa9cb8979b6ee0600867c0d76914 Mon Sep 17 00:00:00 2001 From: Devesh Sawant Date: Wed, 2 May 2018 22:44:26 +0530 Subject: [PATCH 3/9] discarded HEAD changes for merge --- search.ipynb | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/search.ipynb b/search.ipynb index 030bf4859..f61d371b4 100644 --- a/search.ipynb +++ b/search.ipynb @@ -925,11 +925,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 145, -======= "execution_count": 11, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { @@ -1598,11 +1594,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 19, -======= "execution_count": 20, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -1668,11 +1660,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 20, -======= "execution_count": 21, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { @@ -1711,11 +1699,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 21, -======= "execution_count": 22, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -1802,11 +1786,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 22, -======= "execution_count": 23, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -1947,13 +1927,9 @@ "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", -<<<<<<< HEAD - "display_visual(user_input = False, algorithm = uniform_cost_search, problem = romania_problem)" -======= "display_visual(romania_graph_data, user_input=False, \n", " algorithm=depth_limited_search_for_vis, \n", " problem=romania_problem)" ->>>>>>> upstream/master ] }, { @@ -1987,11 +1963,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 25, -======= "execution_count": 26, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { From 91fb02abbf8fa433f55f8d0cf487842e8993516c Mon Sep 17 00:00:00 2001 From: Devesh Sawant Date: Wed, 9 May 2018 00:02:13 +0530 Subject: [PATCH 4/9] added ensemble_learner jpeg --- images/ensemble_learner.jpg | Bin 0 -> 16575 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/ensemble_learner.jpg diff --git a/images/ensemble_learner.jpg b/images/ensemble_learner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b1edd1ec53d303c4ad30e4c52a67987162df0551 GIT binary patch literal 16575 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 literal 0 HcmV?d00001 From 1e9bd782b41f3c0dedab0db5ae3541452b9a6dbb Mon Sep 17 00:00:00 2001 From: Devesh Sawant Date: Sun, 13 May 2018 00:20:33 +0530 Subject: [PATCH 5/9] updated travis and search --- .travis.yml | 3 - search.ipynb | 912 +++++---------------------------------------------- 2 files changed, 74 insertions(+), 841 deletions(-) diff --git a/.travis.yml b/.travis.yml index 09723eea7..e374eff1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,12 +12,9 @@ install: - pip install flake8 - pip install ipython - pip install matplotlib -<<<<<<< HEAD -======= - pip install networkx - pip install ipywidgets - pip install Pillow ->>>>>>> upstream/master script: - py.test diff --git a/search.ipynb b/search.ipynb index bf03f0a25..8edbe675d 100644 --- a/search.ipynb +++ b/search.ipynb @@ -15,6 +15,7 @@ "cell_type": "code", "execution_count": 1, "metadata": { + "collapsed": true, "scrolled": true }, "outputs": [], @@ -760,30 +761,6 @@ "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": 141, - "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": {}, @@ -799,42 +776,18 @@ }, "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", + "# node colors, node positions and node label positions\n", + "node_colors = {node: 'white' for node in romania_map.locations.keys()}\n", + "node_positions = romania_map.locations\n", + "node_label_pos = { k:[v[0],v[1]-10] for k,v in romania_map.locations.items() }\n", + "edge_weights = {(k, k2) : v2 for k, v in romania_map.graph_dict.items() for k2, v2 in v.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" + "romania_graph_data = { 'graph_dict' : romania_map.graph_dict,\n", + " 'node_colors': node_colors,\n", + " 'node_positions': node_positions,\n", + " 'node_label_positions': node_label_pos,\n", + " 'edge_weights': edge_weights\n", + " }" ] }, { @@ -844,40 +797,6 @@ "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": 143, - "metadata": {}, - "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": {}, @@ -904,7 +823,7 @@ } ], "source": [ - "show_map(node_colors)" + "show_map(romania_graph_data)" ] }, { @@ -1154,20 +1073,20 @@ } ], "source": [ - " state1 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", - " state2 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", - " state3 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", - " state4 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", - " state5 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", - " state6 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", - " state7 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", - " state8 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", + "state1 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", + "state2 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", + "state3 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", + "state4 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", + "state5 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", + "state6 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", + "state7 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", + "state8 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", "\n", - " a = vacuumAgent(state1)\n", + "a = vacuumAgent(state1)\n", "\n", - " print(a(state6)) \n", - " print(a(state1))\n", - " print(a(state3))" + "print(a(state6)) \n", + "print(a(state1))\n", + "print(a(state3))" ] }, { @@ -1178,144 +1097,29 @@ "\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", + "1. Breadth First Tree Search\n", + "2. Depth First Tree Search\n", + "3. Breadth First Search\n", + "4. Depth First Graph Search\n", + "5. Best First Graph Search\n", + "6. Uniform Cost Search\n", "7. Depth Limited Search\n", "8. Iterative Deepening Search\n", - "9. A\\*-Search - Implemented\n", + "9. A\\*-Search\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)" + "* Already explored nodes - gray" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## BREADTH-FIRST TREE SEARCH\n", + "## 1. 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." ] @@ -1336,7 +1140,7 @@ " # 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", + " node_colors = {k : 'white' for k in problem.graph.nodes()}\n", " \n", " #Adding first node to the queue\n", " frontier = deque([Node(problem.initial)])\n", @@ -1392,26 +1196,7 @@ "cell_type": "code", "execution_count": 15, "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" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", @@ -1425,7 +1210,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Depth-First Tree Search:\n", + "## 2. Depth-First Tree Search:\n", "Now let's discuss another searching algorithm, Depth-First Tree Search." ] }, @@ -1494,26 +1279,7 @@ "cell_type": "code", "execution_count": 17, "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" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", @@ -1528,7 +1294,7 @@ "collapsed": true }, "source": [ - "## BREADTH-FIRST SEARCH\n", + "## 3. BREADTH-FIRST GRAPH SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] @@ -1547,7 +1313,7 @@ " # 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", + " node_colors = {k : 'white' for k in problem.graph.nodes()}\n", " \n", " node = Node(problem.initial)\n", " \n", @@ -1599,26 +1365,7 @@ "cell_type": "code", "execution_count": 19, "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" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", @@ -1631,7 +1378,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Depth-First Graph Search: \n", + "## 4. 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." ] }, @@ -1649,7 +1396,7 @@ " # 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", + " node_colors = {k : 'white' for k in problem.graph.nodes()}\n", " \n", " frontier = [(Node(problem.initial))]\n", " explored = set()\n", @@ -1704,37 +1451,20 @@ "cell_type": "code", "execution_count": 21, "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" - } - ], + "outputs": [], "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)" + "display_visual(romania_graph_data, user_input=False, \n", + " algorithm=depth_first_graph_search, \n", + " problem=romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## BEST FIRST SEARCH\n", + "## 5. BEST FIRST SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] @@ -1747,7 +1477,7 @@ }, "outputs": [], "source": [ - "def best_first_graph_search(problem, f):\n", + "def best_first_graph_search_for_vis(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", @@ -1759,7 +1489,7 @@ " # 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", + " node_colors = {k : 'white' for k in problem.graph.nodes()}\n", " \n", " f = memoize(f, 'f')\n", " node = Node(problem.initial)\n", @@ -1821,7 +1551,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## UNIFORM COST SEARCH\n", + "## 6. UNIFORM COST SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] @@ -1837,15 +1567,12 @@ "def uniform_cost_search_graph(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)" + " iterations, all_node_colors, node = best_first_graph_search_for_vis(problem, lambda node: node.path_cost)\n", + " return(iterations, all_node_colors, node)\n" ] }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 23, -======= "execution_count": 24, "metadata": {}, "outputs": [], @@ -1944,28 +1671,8 @@ { "cell_type": "code", "execution_count": null, ->>>>>>> upstream/master "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" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", @@ -2019,11 +1726,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 24, -======= "execution_count": 25, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -2034,56 +1737,35 @@ " 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)" + " iterations, all_node_colors, node = best_first_graph_search_for_vis(problem, lambda n: h(n))\n", + " return(iterations, all_node_colors, node)\n" ] }, { "cell_type": "code", "execution_count": 26, "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" - } - ], + "outputs": [], "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)" + "display_visual(romania_graph_data, user_input=False, \n", + " algorithm=greedy_best_first_search, \n", + " problem=romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## A\\* SEARCH\n", + "## 9. A\\* SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 25, -======= "execution_count": 27, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -2094,111 +1776,41 @@ " 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)" + " iterations, all_node_colors, node = best_first_graph_search_for_vis(problem, \n", + " lambda n: n.path_cost + h(n))\n", + " return(iterations, all_node_colors, node)\n" ] }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 26, -======= "execution_count": 28, ->>>>>>> upstream/master "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" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", -<<<<<<< HEAD - "display_visual(user_input = False, algorithm = astar_search, problem = romania_problem)" -======= "display_visual(romania_graph_data, user_input=False, \n", " algorithm=astar_search_graph, \n", " problem=romania_problem)" ->>>>>>> upstream/master ] }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 27, -======= "execution_count": 29, ->>>>>>> upstream/master "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" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", - "# display_visual(user_input = True, algorithm = breadth_first_tree_search)\n", - "display_visual(user_input = True)" + "# display_visual(romania_graph_data, user_input=True, algorithm=breadth_first_tree_search)\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", + "display_visual(romania_graph_data, algorithm=algorithms, user_input=True)" ] }, { @@ -2253,11 +1865,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 11, -======= "execution_count": 31, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -2318,57 +1926,6 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "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" - ] -======= "execution_count": 32, "metadata": {}, "outputs": [ @@ -2381,7 +1938,6 @@ "execution_count": 32, "metadata": {}, "output_type": "execute_result" ->>>>>>> upstream/master } ], "source": [ @@ -2406,119 +1962,6 @@ "outputs": [ { "data": { -<<<<<<< HEAD - "text/html": [ - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "

\n", - "\n", - "
def hill_climbing(problem):\n",
-       "    """From the initial node, keep choosing the neighbor with highest value,\n",
-       "    stopping when no neighbor is better. [Figure 4.2]"""\n",
-       "    current = Node(problem.initial)\n",
-       "    while True:\n",
-       "        neighbors = current.expand(problem)\n",
-       "        if not neighbors:\n",
-       "            break\n",
-       "        neighbor = argmax_random_tie(neighbors,\n",
-       "                                     key=lambda node: problem.value(node.state))\n",
-       "        if problem.value(neighbor.state) <= problem.value(current.state):\n",
-       "            break\n",
-       "        current = neighbor\n",
-       "    return current.state\n",
-       "
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" -======= "text/plain": [ "['UP', 'LEFT', 'UP', 'LEFT', 'DOWN', 'RIGHT', 'RIGHT', 'DOWN']" ] @@ -2526,7 +1969,6 @@ "execution_count": 33, "metadata": {}, "output_type": "execute_result" ->>>>>>> upstream/master } ], "source": [ @@ -2544,12 +1986,6 @@ { "cell_type": "code", "execution_count": 34, -<<<<<<< HEAD - "metadata": { - "collapsed": true - }, - "outputs": [], -======= "metadata": {}, "outputs": [ { @@ -2563,7 +1999,6 @@ "output_type": "execute_result" } ], ->>>>>>> upstream/master "source": [ "astar_search(puzzle, linear).solution()" ] @@ -2590,17 +2025,6 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 35, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['Arad', 'Bucharest', 'Craiova', 'Drobeta', 'Eforie', 'Fagaras', 'Giurgiu', 'Hirsova', 'Iasi', 'Lugoj', 'Mehadia', 'Neamt', 'Oradea', 'Pitesti', 'Rimnicu', 'Sibiu', 'Timisoara', 'Urziceni', 'Vaslui', 'Zerind']\n" - ] -======= "execution_count": 36, "metadata": {}, "outputs": [ @@ -2613,7 +2037,6 @@ "execution_count": 36, "metadata": {}, "output_type": "execute_result" ->>>>>>> upstream/master } ], "source": [ @@ -2654,11 +2077,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 36, -======= "execution_count": 38, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -2678,9 +2097,6 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 37, -======= "execution_count": 39, "metadata": {}, "outputs": [ @@ -3059,7 +2475,6 @@ { "cell_type": "code", "execution_count": 48, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -3108,11 +2523,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 38, -======= "execution_count": 49, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -3131,41 +2542,12 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 39, -======= "execution_count": 50, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { "data": { "text/plain": [ -<<<<<<< HEAD - "['Fagaras',\n", - " 'Neamt',\n", - " 'Iasi',\n", - " 'Vaslui',\n", - " 'Hirsova',\n", - " 'Eforie',\n", - " 'Urziceni',\n", - " 'Bucharest',\n", - " 'Giurgiu',\n", - " 'Pitesti',\n", - " 'Craiova',\n", - " 'Drobeta',\n", - " 'Mehadia',\n", - " 'Lugoj',\n", - " 'Timisoara',\n", - " 'Arad',\n", - " 'Zerind',\n", - " 'Oradea',\n", - " 'Sibiu',\n", - " 'Rimnicu']" - ] - }, - "execution_count": 39, -======= "['Arad',\n", " 'Timisoara',\n", " 'Lugoj',\n", @@ -3189,7 +2571,6 @@ ] }, "execution_count": 50, ->>>>>>> upstream/master "metadata": {}, "output_type": "execute_result" } @@ -4044,11 +3425,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 2, -======= "execution_count": 51, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { @@ -4200,11 +3577,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 3, -======= "execution_count": 52, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { @@ -4327,11 +3700,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 4, -======= "execution_count": 53, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { @@ -4461,11 +3830,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 5, -======= "execution_count": 54, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { @@ -4632,11 +3997,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 33, -======= "execution_count": 55, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4656,11 +4017,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 34, -======= "execution_count": 56, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4686,11 +4043,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 35, -======= "execution_count": 57, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4708,11 +4061,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 36, -======= "execution_count": 58, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4730,11 +4079,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 37, -======= "execution_count": 59, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4759,11 +4104,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 38, -======= "execution_count": 60, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4781,11 +4122,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 39, -======= "execution_count": 61, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4796,11 +4133,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 40, -======= "execution_count": 62, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4819,11 +4152,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 41, -======= "execution_count": 63, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4845,11 +4174,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 42, -======= "execution_count": 64, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4867,11 +4192,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 43, -======= "execution_count": 65, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4889,22 +4210,14 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 44, -======= "execution_count": 66, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ -<<<<<<< HEAD - "['j', 'F', 'm', 'F', 'N', 'i', 'c', 'v', 'm', 'j', 'V', 'o', 'd', 'r', 't', 'V', 'H']\n" -======= "['J', 'y', 'O', 'e', ' ', 'h', 'c', 'r', 'C', 'W', 'H', 'o', 'r', 'R', 'y', 'P', 'U']\n" ->>>>>>> upstream/master ] } ], @@ -4921,22 +4234,14 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 45, -======= "execution_count": 67, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ -<<<<<<< HEAD - "jFmFNicvmjVodrtVH\n" -======= "JyOe hcrCWHorRyPU\n" ->>>>>>> upstream/master ] } ], @@ -4958,11 +4263,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 46, -======= "execution_count": 68, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -4986,11 +4287,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 47, -======= "execution_count": 69, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -5021,11 +4318,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 48, -======= "execution_count": 70, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { @@ -5154,22 +4447,14 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 49, -======= "execution_count": 71, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ -<<<<<<< HEAD - "Current best: Genetic Algorithm\t\tGeneration: 472\t\tFitness: 17\r" -======= "Current best: Genetic Algorithm\t\tGeneration: 985\t\tFitness: 17\r" ->>>>>>> upstream/master ] } ], @@ -5215,11 +4500,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 6, -======= "execution_count": 72, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -5244,22 +4525,14 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 7, -======= "execution_count": 73, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ -<<<<<<< HEAD - "[['R', 'G', 'G', 'R'], ['R', 'G', 'R', 'R'], ['G', 'R', 'G', 'R'], ['R', 'G', 'R', 'G'], ['G', 'R', 'R', 'G'], ['G', 'R', 'G', 'R'], ['G', 'R', 'R', 'R'], ['R', 'G', 'G', 'G']]\n" -======= "[['R', 'G', 'G', 'G'], ['G', 'R', 'R', 'G'], ['G', 'G', 'G', 'G'], ['G', 'R', 'G', 'G'], ['G', 'G', 'G', 'R'], ['G', 'R', 'R', 'G'], ['G', 'R', 'G', 'G'], ['G', 'G', 'R', 'G']]\n" ->>>>>>> upstream/master ] } ], @@ -5279,11 +4552,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 8, -======= "execution_count": 74, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -5302,11 +4571,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 9, -======= "execution_count": 75, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { @@ -5331,11 +4596,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 10, -======= "execution_count": 76, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { @@ -5380,22 +4641,14 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 11, -======= "execution_count": 77, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ -<<<<<<< HEAD - "[[0, 2, 7, 1, 7, 3, 2, 4], [2, 7, 5, 4, 4, 5, 2, 0], [7, 1, 6, 0, 1, 3, 0, 2], [0, 3, 6, 1, 3, 0, 5, 4], [0, 4, 6, 4, 7, 4, 1, 6]]\n" -======= "[[2, 6, 2, 0, 2, 3, 4, 7], [7, 2, 0, 6, 3, 3, 0, 6], [2, 3, 0, 6, 6, 2, 5, 5], [2, 6, 4, 2, 3, 5, 5, 5], [3, 1, 5, 1, 5, 1, 0, 3]]\n" ->>>>>>> upstream/master ] } ], @@ -5419,11 +4672,7 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 12, -======= "execution_count": 78, ->>>>>>> upstream/master "metadata": { "collapsed": true }, @@ -5455,24 +4704,15 @@ }, { "cell_type": "code", -<<<<<<< HEAD - "execution_count": 16, -======= "execution_count": 79, ->>>>>>> upstream/master "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ -<<<<<<< HEAD - "[5, 0, 6, 3, 7, 4, 1, 3]\n", - "26\n" -======= "[2, 5, 7, 1, 3, 6, 4, 6]\n", "25\n" ->>>>>>> upstream/master ] } ], @@ -6089,9 +5329,6 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", -<<<<<<< HEAD - "version": "3.6.4" -======= "version": "3.6.1" }, "widgets": { @@ -6126,7 +5363,6 @@ } }, "version": "1.2.0" ->>>>>>> upstream/master } }, "nbformat": 4, From 9504694ccd5f942f7c8a31cea2df5ba660e760e0 Mon Sep 17 00:00:00 2001 From: Devesh Sawant Date: Mon, 28 May 2018 01:14:35 +0530 Subject: [PATCH 6/9] Added logarithmic learner in nlp_apps (#890) --- nlp_apps.ipynb | 216 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 159 insertions(+), 57 deletions(-) diff --git a/nlp_apps.ipynb b/nlp_apps.ipynb index 089a50c26..0653d1d69 100644 --- a/nlp_apps.ipynb +++ b/nlp_apps.ipynb @@ -24,7 +24,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## LANGUAGE RECOGNITION\n", + "# LANGUAGE RECOGNITION\n", "\n", "A very useful application of text models (you can read more on them on the [`text notebook`](https://github.com/aimacode/aima-python/blob/master/text.ipynb)) is categorizing text into a language. In fact, with enough data we can categorize correctly mostly any text. That is because different languages have certain characteristics that set them apart. For example, in German it is very usual for 'c' to be followed by 'h' while in English we see 't' followed by 'h' a lot.\n", "\n", @@ -37,8 +37,10 @@ }, { "cell_type": "code", - "execution_count": 1, - "metadata": {}, + "execution_count": 2, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "from utils import open_data\n", @@ -66,8 +68,10 @@ }, { "cell_type": "code", - "execution_count": 2, - "metadata": {}, + "execution_count": 3, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "from learning import NaiveBayesLearner\n", @@ -88,8 +92,10 @@ }, { "cell_type": "code", - "execution_count": 3, - "metadata": {}, + "execution_count": 4, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "def recognize(sentence, nBS, n):\n", @@ -116,7 +122,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -132,7 +138,7 @@ "'German'" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -143,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -159,7 +165,7 @@ "'English'" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -170,7 +176,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -186,7 +192,7 @@ "'German'" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -197,7 +203,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -213,7 +219,7 @@ "'English'" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -248,8 +254,10 @@ }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, + "execution_count": 1, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "from utils import open_data\n", @@ -277,8 +285,10 @@ }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, + "execution_count": 2, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "from learning import NaiveBayesLearner\n", @@ -297,8 +307,10 @@ }, { "cell_type": "code", - "execution_count": 10, - "metadata": {}, + "execution_count": 3, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "def recognize(sentence, nBS):\n", @@ -317,7 +329,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -326,7 +338,7 @@ "'Abbott'" ] }, - "execution_count": 11, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -346,7 +358,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -355,7 +367,7 @@ "'Austen'" ] }, - "execution_count": 12, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -391,7 +403,9 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "from utils import open_data\n", @@ -437,7 +451,9 @@ { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "wordseq = words(federalist)\n", @@ -485,7 +501,9 @@ { "cell_type": "code", "execution_count": 5, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "wordseq = [w for w in wordseq if w != 'publius']" @@ -551,7 +569,9 @@ { "cell_type": "code", "execution_count": 7, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "hamilton = ''.join(hamilton)\n", @@ -571,19 +591,27 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now it is time to build our new Naive Bayes Learner. It is very similar to the one found in `learning.py`, but with an important difference: it doesn't classify an example, but instead returns the probability of the example belonging to each class. This will allow us to not only see to whom a paper belongs to, but also the probability of authorship as well.\n", + "Now it is time to build our new Naive Bayes Learner. It is very similar to the one found in `learning.py`, but with an important difference: it doesn't classify an example, but instead returns the probability of the example belonging to each class. This will allow us to not only see to whom a paper belongs to, but also the probability of authorship as well. \n", + "We will build two versions of Learners, one will multiply probabilities as is and other will add the logarithms of them.\n", "\n", - "Finally, since we are dealing with long text and the string of probability multiplications is long, we will end up with the results being rounded to 0 due to floating point underflow. To work around this problem we will use the built-in Python library `decimal`, which allows as to set decimal precision to much larger than normal." + "Finally, since we are dealing with long text and the string of probability multiplications is long, we will end up with the results being rounded to 0 due to floating point underflow. To work around this problem we will use the built-in Python library `decimal`, which allows as to set decimal precision to much larger than normal.\n", + "\n", + "Note that the logarithmic learner will compute a negative likelihood since the logarithm of values less than 1 will be negative.\n", + "Thus, the author with the lesser magnitude of proportion is more likely to have written that paper.\n", + "\n" ] }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, + "execution_count": 16, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "import random\n", "import decimal\n", + "import math\n", "from decimal import Decimal\n", "\n", "decimal.getcontext().prec = 100\n", @@ -594,6 +622,11 @@ " result *= Decimal(x)\n", " return result\n", "\n", + "def log_product(numbers):\n", + " result = 0.0\n", + " for x in numbers:\n", + " result += math.log(x)\n", + " return result\n", "\n", "def NaiveBayesLearner(dist):\n", " \"\"\"A simple naive bayes classifier that takes as input a dictionary of\n", @@ -617,7 +650,32 @@ "\n", " return pred\n", "\n", - " return predict" + " return predict\n", + "\n", + "def NaiveBayesLearnerLog(dist):\n", + " \"\"\"A simple naive bayes classifier that takes as input a dictionary of\n", + " Counter distributions and can then be used to find the probability\n", + " of a given item belonging to each class. It will compute the likelihood by adding the logarithms of probabilities.\n", + " The input dictionary is in the following form:\n", + " ClassName: Counter\"\"\"\n", + " attr_dist = {c_name: count_prob for c_name, count_prob in dist.items()}\n", + "\n", + " def predict(example):\n", + " \"\"\"Predict the probabilities for each class.\"\"\"\n", + " def class_prob(target, e):\n", + " attr = attr_dist[target]\n", + " return log_product([attr[a] for a in e])\n", + "\n", + " pred = {t: class_prob(t, example) for t in dist.keys()}\n", + "\n", + " total = -sum(pred.values())\n", + " for k, v in pred.items():\n", + " pred[k] = v/total\n", + "\n", + " return pred\n", + "\n", + " return predict\n", + "\n" ] }, { @@ -629,12 +687,15 @@ }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, + "execution_count": 17, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "dist = {('Madison', 1): P_madison, ('Hamilton', 1): P_hamilton, ('Jay', 1): P_jay}\n", - "nBS = NaiveBayesLearner(dist)" + "nBS = NaiveBayesLearner(dist)\n", + "nBSL = NaiveBayesLearnerLog(dist)" ] }, { @@ -646,8 +707,10 @@ }, { "cell_type": "code", - "execution_count": 10, - "metadata": {}, + "execution_count": 18, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "def recognize(sentence, nBS):\n", @@ -663,45 +726,84 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Paper No. 49: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 50: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 51: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 52: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 53: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 54: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 55: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 56: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 57: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 58: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 18: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 19: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 20: Hamilton: 0.00 Madison: 1.00 Jay: 0.00\n", - "Paper No. 64: Hamilton: 1.00 Madison: 0.00 Jay: 0.00\n" + "\n", + "Straightforward Naive Bayes Learner\n", + "\n", + "Paper No. 49: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 50: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 51: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 52: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 53: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 54: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 55: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 56: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 57: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 58: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 18: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 19: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 20: Hamilton: 0.0000 Madison: 1.0000 Jay: 0.0000\n", + "Paper No. 64: Hamilton: 1.0000 Madison: 0.0000 Jay: 0.0000\n", + "\n", + "Logarithmic Naive Bayes Learner\n", + "\n", + "Paper No. 49: Hamilton: -0.330591 Madison: -0.327717 Jay: -0.341692\n", + "Paper No. 50: Hamilton: -0.333119 Madison: -0.328454 Jay: -0.338427\n", + "Paper No. 51: Hamilton: -0.330246 Madison: -0.325758 Jay: -0.343996\n", + "Paper No. 52: Hamilton: -0.331094 Madison: -0.327491 Jay: -0.341415\n", + "Paper No. 53: Hamilton: -0.330942 Madison: -0.328364 Jay: -0.340693\n", + "Paper No. 54: Hamilton: -0.329566 Madison: -0.327157 Jay: -0.343277\n", + "Paper No. 55: Hamilton: -0.330821 Madison: -0.328143 Jay: -0.341036\n", + "Paper No. 56: Hamilton: -0.330333 Madison: -0.327496 Jay: -0.342171\n", + "Paper No. 57: Hamilton: -0.330625 Madison: -0.328602 Jay: -0.340772\n", + "Paper No. 58: Hamilton: -0.330271 Madison: -0.327215 Jay: -0.342515\n", + "Paper No. 18: Hamilton: -0.337781 Madison: -0.330932 Jay: -0.331287\n", + "Paper No. 19: Hamilton: -0.335635 Madison: -0.331774 Jay: -0.332590\n", + "Paper No. 20: Hamilton: -0.334911 Madison: -0.331866 Jay: -0.333223\n", + "Paper No. 64: Hamilton: -0.331004 Madison: -0.332968 Jay: -0.336028\n" ] } ], "source": [ + "print('\\nStraightforward Naive Bayes Learner\\n')\n", "for d in disputed:\n", " probs = recognize(papers[d], nBS)\n", - " results = ['{}: {:.2f}'.format(name, probs[(name, 1)]) for name in 'Hamilton Madison Jay'.split()]\n", - " print('Paper No. {}: {}'.format(d, ' '.join(results)))" + " results = ['{}: {:.4f}'.format(name, probs[(name, 1)]) for name in 'Hamilton Madison Jay'.split()]\n", + " print('Paper No. {}: {}'.format(d, ' '.join(results)))\n", + "\n", + "print('\\nLogarithmic Naive Bayes Learner\\n')\n", + "for d in disputed:\n", + " probs = recognize(papers[d], nBSL)\n", + " results = ['{}: {:.6f}'.format(name, probs[(name, 1)]) for name in 'Hamilton Madison Jay'.split()]\n", + " print('Paper No. {}: {}'.format(d, ' '.join(results)))\n", + "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "We can see that both learners classify the papers identically. Because of underflow as stated before, only one author remains with a positive value. The log learner is more accurate with marginal differences. \n", + "\n", "This is a simple approach to the problem and thankfully researchers are fairly certain that papers 49-58 were all written by Madison, while 18-20 were written in collaboration between Hamilton and Madison, with Madison being credited for most of the work. Our classifier is not that far off. It correctly identifies the papers written by Madison, even the ones in collaboration with Hamilton.\n", "\n", "Unfortunately, it misses paper 64. Consensus is that the paper was written by John Jay, while our classifier believes it was written by Hamilton. The classifier is wrong there because it does not have much information on Jay's writing; only 4 papers. This is one of the problems with using unbalanced datasets such as this one, where information on some classes is sparser than information on the rest. To avoid this, we can add more writings for Jay and Madison to end up with an equal amount of data for each author." ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] } ], "metadata": { @@ -720,7 +822,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.3" + "version": "3.6.1" } }, "nbformat": 4, From 07977b37768eaf984456f0625e656814fd363276 Mon Sep 17 00:00:00 2001 From: Devesh Sawant Date: Tue, 29 May 2018 00:48:02 +0530 Subject: [PATCH 7/9] Remove zebra tests --- tests/test_csp.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_csp.py b/tests/test_csp.py index a538d40af..2bc907b6c 100644 --- a/tests/test_csp.py +++ b/tests/test_csp.py @@ -437,8 +437,5 @@ def test_tree_csp_solver(): assert (tcs['NT'] == 'R' and tcs['WA'] == 'B' and tcs['Q'] == 'B' and tcs['NSW'] == 'R' and tcs['V'] == 'B') or \ (tcs['NT'] == 'B' and tcs['WA'] == 'R' and tcs['Q'] == 'R' and tcs['NSW'] == 'B' and tcs['V'] == 'R') -def test_zebra_problem(): - zebra_solution = solve_zebra() - assert(zebra_solution[0] == 5 and zebra_solution[1] == 1) if __name__ == "__main__": pytest.main() From 2e0903a99de8615702cbccef85e690a2159e1e33 Mon Sep 17 00:00:00 2001 From: Devesh Sawant Date: Wed, 30 May 2018 11:15:05 +0530 Subject: [PATCH 8/9] revised Bayes learner explanation --- nlp_apps.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nlp_apps.ipynb b/nlp_apps.ipynb index 0653d1d69..458c55700 100644 --- a/nlp_apps.ipynb +++ b/nlp_apps.ipynb @@ -789,7 +789,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can see that both learners classify the papers identically. Because of underflow as stated before, only one author remains with a positive value. The log learner is more accurate with marginal differences. \n", + "We can see that both learners classify the papers identically. Because of underflow in the straightforward learner, only one author remains with a positive value. The log learner is more accurate with marginal differences between all the authors. \n", "\n", "This is a simple approach to the problem and thankfully researchers are fairly certain that papers 49-58 were all written by Madison, while 18-20 were written in collaboration between Hamilton and Madison, with Madison being credited for most of the work. Our classifier is not that far off. It correctly identifies the papers written by Madison, even the ones in collaboration with Hamilton.\n", "\n", From d3214df1271b093d0e28528696b0d9fc37e94ca1 Mon Sep 17 00:00:00 2001 From: Devesh Sawant Date: Sat, 2 Jun 2018 00:35:28 +0530 Subject: [PATCH 9/9] added missing SimpleReflexAgent from upstream --- README.md | 2 +- tests/test_agents.py | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 86041b5c6..8bac287b6 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 2.3 | Table-Driven-Vacuum-Agent | `TableDrivenVacuumAgent` | [`agents.py`][agents] | Done | Included | | 2.7 | Table-Driven-Agent | `TableDrivenAgent` | [`agents.py`][agents] | Done | Included | | 2.8 | Reflex-Vacuum-Agent | `ReflexVacuumAgent` | [`agents.py`][agents] | Done | Included | -| 2.10 | Simple-Reflex-Agent | `SimpleReflexAgent` | [`agents.py`][agents] | | Included | +| 2.10 | Simple-Reflex-Agent | `SimpleReflexAgent` | [`agents.py`][agents] | Done | Included | | 2.12 | Model-Based-Reflex-Agent | `ReflexAgentWithState` | [`agents.py`][agents] | | Included | | 3 | Problem | `Problem` | [`search.py`][search] | Done | Included | | 3 | Node | `Node` | [`search.py`][search] | Done | Included | diff --git a/tests/test_agents.py b/tests/test_agents.py index 61ef531de..ded9b7d95 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -2,7 +2,8 @@ from agents import Direction from agents import Agent from agents import ReflexVacuumAgent, ModelBasedVacuumAgent, TrivialVacuumEnvironment, compare_agents,\ - RandomVacuumAgent, TableDrivenVacuumAgent, TableDrivenAgentProgram, RandomAgentProgram + RandomVacuumAgent, TableDrivenVacuumAgent, TableDrivenAgentProgram, RandomAgentProgram, \ + SimpleReflexAgentProgram, rule_match random.seed("aima-python") @@ -131,6 +132,38 @@ def test_ReflexVacuumAgent() : # check final status of the environment assert environment.status == {(1,0):'Clean' , (0,0) : 'Clean'} +def test_SimpleReflexAgentProgram(): + class Rule: + + def __init__(self, state, action): + self.__state = state + self.action = action + + def matches(self, state): + return self.__state == state + + loc_A = (0, 0) + loc_B = (1, 0) + + # create rules for a two state Vacuum Environment + rules = [Rule((loc_A, "Dirty"), "Suck"), Rule((loc_A, "Clean"), "Right"), + Rule((loc_B, "Dirty"), "Suck"), Rule((loc_B, "Clean"), "Left")] + + def interpret_input(state): + return state + + # create a program and then an object of the SimpleReflexAgentProgram + program = SimpleReflexAgentProgram(rules, interpret_input) + agent = Agent(program) + # create an object of TrivialVacuumEnvironment + environment = TrivialVacuumEnvironment() + # add agent to the environment + environment.add_thing(agent) + # run the environment + environment.run() + # check final status of the environment + assert environment.status == {(1,0):'Clean' , (0,0) : 'Clean'} + def test_ModelBasedVacuumAgent() : # create an object of the ModelBasedVacuumAgent pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy