diff --git a/README.md b/README.md index 2dcf7d368..fc5f38bb5 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 3.22 | Best-First-Search | `best_first_graph_search` | [`search.py`][search] | Done | Included | | 3.24 | A\*-Search | `astar_search` | [`search.py`][search] | Done | Included | | 3.26 | Recursive-Best-First-Search | `recursive_best_first_search` | [`search.py`][search] | Done | | -| 4.2 | Hill-Climbing | `hill_climbing` | [`search.py`][search] | Done | | +| 4.2 | Hill-Climbing | `hill_climbing` | [`search.py`][search] | Done | Included | | 4.5 | Simulated-Annealing | `simulated_annealing` | [`search.py`][search] | Done | | | 4.8 | Genetic-Algorithm | `genetic_algorithm` | [`search.py`][search] | Done | Included | | 4.11 | And-Or-Graph-Search | `and_or_graph_search` | [`search.py`][search] | Done | | diff --git a/images/hillclimb-tsp.png b/images/hillclimb-tsp.png new file mode 100644 index 000000000..8446bbafc Binary files /dev/null and b/images/hillclimb-tsp.png differ diff --git a/search.ipynb b/search.ipynb index 332ba11b9..2ac393ea0 100644 --- a/search.ipynb +++ b/search.ipynb @@ -85,12 +85,160 @@ { "cell_type": "code", "execution_count": 2, - "metadata": { - "collapsed": true - }, - "outputs": [], + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " Codestin Search App\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" + "psource(Problem)" ] }, { @@ -128,13 +276,173 @@ }, { "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " Codestin Search App\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" + "psource(Node)" ] }, { @@ -171,13 +479,150 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " Codestin Search App\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" + "psource(GraphProblem)" ] }, { @@ -484,13 +929,146 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " Codestin Search App\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" + "psource(SimpleProblemSolvingAgentProgram)" ] }, { @@ -1482,6 +2060,388 @@ "puzzle.solve([2,4,3,1,5,6,7,8,0], [1,2,3,4,5,6,7,8,0],sqrt_manhanttan) # Sqrt_manhattan" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## HILL CLIMBING\n", + "\n", + "Hill Climbing is a heuristic search used for optimization problems.\n", + "Given a large set of inputs and a good heuristic function, it tries to find a sufficiently good solution to the problem. \n", + "This solution may or may not be the global optimum.\n", + "The algorithm is a variant of generate and test algorithm. \n", + "
\n", + "As a whole, the algorithm works as follows:\n", + "- Evaluate the initial state.\n", + "- If it is equal to the goal state, return.\n", + "- Find a neighboring state (one which is heuristically similar to the current state)\n", + "- Evaluate this state. If it is closer to the goal state than before, replace the initial state with this state and repeat these steps.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " Codestin Search App\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)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will find an approximate solution to the traveling salespersons problem using this algorithm.\n", + "
\n", + "We need to define a class for this problem.\n", + "
\n", + "`Problem` will be used as a base class." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "class TSP_problem(Problem):\n", + "\n", + " \"\"\" subclass of Problem to define various functions \"\"\"\n", + "\n", + " def two_opt(self, state):\n", + " \"\"\" Neighbour generating function for Traveling Salesman Problem \"\"\"\n", + " neighbour_state = state[:]\n", + " left = random.randint(0, len(neighbour_state) - 1)\n", + " right = random.randint(0, len(neighbour_state) - 1)\n", + " if left > right:\n", + " left, right = right, left\n", + " neighbour_state[left: right + 1] = reversed(neighbour_state[left: right + 1])\n", + " return neighbour_state\n", + "\n", + " def actions(self, state):\n", + " \"\"\" action that can be excuted in given state \"\"\"\n", + " return [self.two_opt]\n", + "\n", + " def result(self, state, action):\n", + " \"\"\" result after applying the given action on the given state \"\"\"\n", + " return action(state)\n", + "\n", + " def path_cost(self, c, state1, action, state2):\n", + " \"\"\" total distance for the Traveling Salesman to be covered if in state2 \"\"\"\n", + " cost = 0\n", + " for i in range(len(state2) - 1):\n", + " cost += distances[state2[i]][state2[i + 1]]\n", + " cost += distances[state2[0]][state2[-1]]\n", + " return cost\n", + "\n", + " def value(self, state):\n", + " \"\"\" value of path cost given negative for the given state \"\"\"\n", + " return -1 * self.path_cost(None, None, None, state)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will use cities from the Romania map as our cities for this problem.\n", + "
\n", + "A list of all cities and a dictionary storing distances between them will be populated." + ] + }, + { + "cell_type": "code", + "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", + "\n", + "for city in romania_map.locations.keys():\n", + " distances[city] = {}\n", + " all_cities.append(city)\n", + " \n", + "all_cities.sort()\n", + "print(all_cities)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we need to populate the individual lists inside the dictionary with the manhattan distance between the cities." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "for name_1, coordinates_1 in romania_map.locations.items():\n", + " for name_2, coordinates_2 in romania_map.locations.items():\n", + " distances[name_1][name_2] = np.linalg.norm(\n", + " [coordinates_1[0] - coordinates_2[0], coordinates_1[1] - coordinates_2[1]])\n", + " distances[name_2][name_1] = np.linalg.norm(\n", + " [coordinates_1[0] - coordinates_2[0], coordinates_1[1] - coordinates_2[1]])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The way neighbours are chosen currently isn't suitable for the travelling salespersons problem.\n", + "We need a neighboring state that is similar in total path distance to the current state.\n", + "
\n", + "We need to change the function that finds neighbors." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def hill_climbing(problem):\n", + " \n", + " \"\"\"From the initial node, keep choosing the neighbor with highest value,\n", + " stopping when no neighbor is better. [Figure 4.2]\"\"\"\n", + " \n", + " def find_neighbors(state, number_of_neighbors=100):\n", + " \"\"\" finds neighbors using two_opt method \"\"\"\n", + " \n", + " neighbors = []\n", + " \n", + " for i in range(number_of_neighbors):\n", + " new_state = problem.two_opt(state)\n", + " neighbors.append(Node(new_state))\n", + " state = new_state\n", + " \n", + " return neighbors\n", + "\n", + " # as this is a stochastic algorithm, we will set a cap on the number of iterations\n", + " iterations = 10000\n", + " \n", + " current = Node(problem.initial)\n", + " while iterations:\n", + " neighbors = find_neighbors(current.state)\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", + " current.state = neighbor.state\n", + " iterations -= 1\n", + " \n", + " return current.state" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An instance of the TSP_problem class will be created." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "tsp = TSP_problem(all_cities)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now generate an approximate solution to the problem by calling `hill_climbing`.\n", + "The results will vary a bit each time you run it." + ] + }, + { + "cell_type": "code", + "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)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The solution looks like this.\n", + "It is not difficult to see why this might be a good solution.\n", + "
\n", + "![title](images/hillclimb-tsp.png)" + ] + }, { "cell_type": "markdown", "metadata": {},