Search as a problem solving technique. Consider a goal-based agent capable of formulating a search problem by: – providing a description of the current problem state, – providing a description of its own actions that can transform one problem state into another one, – providing a description of the goal state where a desired goal holds. The solution of such a problem consists of finding a path from the current state to the goal state. The problem space can be huge, which is why the agent must know how to efficiently search it and evaluate solutions.To define how “good” the solution is, a path cost function can be assigned to a path. Different solutions of the same problem can be compared by means of the corresponding path cost functions. We shall discuss two types of searches: – Uninformed search (no problem-specific information is available to direct the search). We shall use the Missionaries and Cannibals (M&C) problem to illustrate uninformed search. – Informed search (there is a problem-specific information helping the agent through the search process). We shall use the 5-puzzle problem (a downsized version of the 8-puzzle problem) to illustrate informed search. Uninformed search example: the Missionaries and Cannibals problem. The search problem is defined as follows: Description of the current state: a sequence of six numbers, representing the number of missionaries, cannibals and boats on each bank of the river. Assuming 3 missionaries, 3 cannibals and one boat, the initial state is (setf start '(3 3 1 0 0 0)) Description of possible actions (or operators): take either one missionary, one cannibal, two missionaries, two cannibals, or one of each across the river in the boat, i.e. (setf list-of-actions '((1 0 1) (0 1 1) (2 0 1) (0 2 1) (1 1 1))) Description of the goal state, i.e. (setf finish '(0 0 0 3 3 1)) Note that some world states are illegal (the number of cannibals must always be less or equal to the number of missionaries on each side of the river. Therefore, we must impose certain constraints on the search to avoid illegal states. We also must guarantee that search will not fall in a loop (some actions may “undo” the result of a previous action). The problem space for the M&C problem Problem space is a complete description of the domain. It can be huge, which is why it is only procedurally defined. Here is the problem space for the M&C problem. 3,3,1,0,0,0 2,2,0,1,1,1 3,2,1,0,1,0 2,3,1,1,0,0 2,3,0,1,0,1 2,0,0,1,3,1 3,3,0,0,0,1 1,3,1,2,0,0 0,3,1,3,0,0 3,2,0,0,1,1 1,3,0,2,0,1 3,1,1,0,2,0 3,1,0,0,2,1 2,2,1,1,1,0 3,0,1,0,3,0 2,1,1,1,2,0 3,0,0,0,3,1 2,1,0,1,2,1 2,0,1,1,3,0 1,2,1,2,1,0 1,2,0,2,1,1 1,1,1,2,2,0 1,1,0,2,2,1 1,0,1,2,3,0 0,3,0,3,0,1 0,2,1,3,1,0 0,2,0,3,1,1 0,1,1,3,2,0 0,1,0,3,2,1 0,0,0,3,3,1 Search (or solution) space is a part of the problem space which is actually examined [1,1,1] 3,3,1,0,0,0 [0,1,1] 2,2,0,1,1,1 [0,2,1] 3,2,0,0,1,1 [1,0,1] 3,1,0,0,2,1 [0,1,1] Dead end 3,2,1,0,1,0 3,2,1,0,1,0 [0,1,1] [0,2,1] 3,0,0,0,3,1 3,1,0,0,2,1 Dead end ... [1,0,1] 2,2,0,1,1,1 [0,2,1] 3,0,0,0,3,1 Dead end ... Depth-first search: always expand the path to one of the nodes at the deepest level of the search tree Each path is a list of states on that path, where each state is a list of six elements (m1 c1 b1 m2 c2 b2). Initially, the only path contains only the start state, i.e. ((3 3 1 0 0 0)). (defun depth-first (start finish &optional (queue (list (list start)))) (cond ((endp queue) nil) ((equal finish (first (first queue))) (reverse (first queue))) (t (depth-first start finish (append (extend (first queue)) (rest queue)))))) (defun extend (path) (setf extensions (get-extensions path)) (mapcar #'(lambda (new-node) (cons new-node path)) (filter-extensions extensions path))) Breadth-first search: always expand all nodes at a given level, before expanding any node at the next level (defun breadth-first (start finish &optional (queue (list (list start)))) (cond ((endp queue) nil) ((equal finish (first (first queue))) (reverse (first queue))) (t (breadth-first start finish (append (rest queue) (extend (first queue))))))) (defun extend (path) (setf extensions (get-extensions path)) (mapcar #'(lambda (new-node) (cons new-node path)) (filter-extensions extensions path))) Depth-first vs breadth-first search Depth-first search 1. Space complexity: O(bd), where b is the branching factor, and d is the depth of the search. 2. Time complexity: O(b^d). 3. Not guaranteed to find the shortest path (not optimal). 4. Not guaranteed to find a solution (not complete) 5. Polynomial space complexity makes it applicable for non-toy problems. Breadth-first search 1. Space complexity: O(b^d) 2. Time complexity: O(b^d). 3. Guaranteed to find the shortest path (optimal). 4. Guaranteed to find a solution (complete). 5. Exponential space complexity makes it impractical even for toy problems. Other uninformed search strategies. Depth-limited is the same as depth-first search, but a limit on how deep into a given path the search can go, is imposed. In M&C example, we avoided unlimited depth by checking for cycles. If the depth level is appropriately chosen, depth-limited search is complete, but not optimal. Its time and space complexity are the same as for the depth-first search, i.e. O(b^d) and O(bd), respectively. Iterative deepening is a combination of breadth-first and depth-first searches, where the best depth limit is determined by trying all possible depth limits. Its space complexity is O(bd), which makes it practical for large spaces where loops are possible, and therefore the depth-first search cannot be successful. It is optimal, i.e. guaranteed to find the shortest path. Bi-directional search is initiated simultaneously from the initial state and goal state in a hope that the two paths will eventually meet. It is complete and optimal, but its time and space efficiencies are exponential, i.e. O(b^(d/2)). Informed search strategies: best-first “greedy” search Best-first search always expends the node that is believed to be the closest to the goal state. This is defined by means of the selected evaluation function. Example: consider the following graph whose nodes are represented by means of their property lists: (setf (get (get (get (get (get (get (get (setf (get (get (get (get (get (get (get 's 'a 'b 'c 'd 'e 'f 's 'a 'b 'c 'd 'e 'f 'neighbors) '(a d) 'neighbors) '(s b d) 'neighbors) '(a c e) 'neighbors) '(b) 'neighbors) '(s a e) 'neighbors) '(b d f) 'neighbors) '(e)) 'coordinates) '(0 3) 'coordinates) '(4 6) 'coordinates) '(7 6) 'coordinates) '(11 6) 'coordinates) '(3 0) 'coordinates) '(6 0) 'coordinates) '(11 3)) To see the description of a node, we can say: * (describe 'a) ........ Property: COORDINATES, Value: (4 6) Property: NEIGHBORS, Value: (S B D) To find how close a given node is to the goal, we can use the formula computing the straight line distance between the two nodes: (defun distance (node-1 node-2) (let ((coordinates-1 (get node-1 'coordinates)) (coordinates-2 (get node-2 'coordinates))) (sqrt (+ (expt (- (first coordinates-1) (first coordinates-2)) 2) (expt (- (second coordinates-1) (second coordinates-2)) 2))))) Given two partial paths, whose final node is closest to the goal, can be defined by means of the following closerp predicate: (defun closerp (path-1 path-2 finish) (< (distance (first path-1) finish) (distance (first path-2) finish))) The best-first search now means “expand the path believed to be the closest to the goal”, i.e. (defun best-first (start finish &optional (queue (list (list start)))) (cond ((endp queue) nil) ((equal finish (first (first queue))) (reverse (first queue))) (t (best-first start finish (sort (append (extend (first queue)) (rest queue)) #'(lambda (p1 p2) (closerp p1 p2 finish))))))) (defun extend (path) (mapcar #'(lambda (new-node) (cons new-node path)) (remove-if #'(lambda (neighbor) (member neighbor path)) (get (first path) 'neighbors)))) A* search: a combination of the best-first greedy search and uniform-cost search Uniform-cost search takes into account the path cost, and expands always the lowest cost node. Assume that this path cost is g(n). Best-first search expands the node which is believed to be the closest to the goal. Assume that the estimated cost to reach the goal from this node is h(n). A* search always expands the node with the minimum f(n), where f(n) = g(n) + h(n). We assume here that f(n) never decreases, i.e. f(n) is a monotonic function. Under this condition, A* search is both optimal and complete. A* is hard to implement because any time a shorter path between the start node and any node is found, A* must update cost of paths going through that node. The 5-puzzle problem (a downsized version of the 8-puzzle problem) Here is an example of the 5-puzzle problem: 4 3 2 1 2 3 1 5 0 4 5 0 Consider the following representation: – Initial state description: (4 3 2 1 5 0) – Possible moves: move the empty “0” tile up, down, left or right depending on its current position. – Goal state description: (1 2 3 4 5 0) The problem space contains 6! = 720 different states (for the 8-puzzle, it is 9! = 362,880 different states). However, assuming the branching factor of 2 and a length of a typical solution of about 15, exhaustive search would generate about 2^15 = 32,768 states (for the 8-puzzle, these numbers are: branching factor 3, typical solution is about 20 steps, or 3^20 = 3.5 * 10^9 states). Solving the 5-puzzle problem We shall compare the following searches for solving the 5-puzzle problem (some of this comparison will be done by you as part of homework 2): 1. 2. 3. Breadth-first search (as it guarantees to find the shortest path given enough time and space). Best-first search with 2 admissible heuristic functions : – Number of tiles out of place (or the equivalent one – number of tiles in place). – Manhattan distance. It computes the distance of each tile from its final place, i.e. the distance between the tile’s current and final position in the horizontal direction plus the distance in the vertical direction. Depth-limited search (similar to depth-first, but the maximum path length is limited to prevent infinite paths). Notes: 1. The search space for this type of puzzles is known to be not fully interconnected, i.e. it is not possible to get from one state to any other state. Initial states must be carefully selected so that the final state is reachable from the initial state. 2. Best-first search using an admissible heuristic is known to be equivalent to A* search with all advantages and disadvantages from here (still may take an exponential time and may involve backtracking), but is both optimal and complete. Iterative improvement methods: hill-climbing search If the current state contains all the information needed to solve the problem, then we try the best modification possible to transform the current state into the goal state. Example: map search. (defun hill-climb (start finish &optional (queue (list (list start)))) (cond ((endp queue) nil) ((equal finish (first (first queue))) (reverse (first queue))) (t (hill-climb start finish (append (sort (extend (first queue)) #'(lambda (p1 p2) (closerp p1 p2 finish))) (rest queue)))))) Best applications for a hill-climbing search are those where initial state contains all the information needed for finding a solution. Example: n-queens problem, where initially all queens are on the board, and they are moved around until no queen attacks any other. Notice that the initial state is not fixed. We may start with any configuration of n queens, but there is no guarantee that a solution exists for that particular configuration. If a dead end is encountered, we “forget” everything done so far, and re-start from a different initial configuration. That is, the search tree generated so far is erased, and a new search tree is started. Best-first search vs hill-climbing search Best-first search 1. Space complexity: O(b^d), because the whole search tree is stored in the memory. 2. Time complexity: O(b^d). A good heuristic function can substantially improve this worst case. 3. Greedy search: not complete, not optimal. A* search: complete and optimal if the estimated cost for the cheapest solution through n, f(n), is a monotonic function. Hill-climbing search 1. Space complexity: O(1), because only a single state is maintained in the memory. 2. Time complexity: O(b^d). 3. Not complete, because of the local maxima phenomena (the goal state is not reached, but no state is better that the current state). Possible improvement: simulated annealing, which allows the algorithm to backtrack from the local maxima in an attempt to find a better continuation. 4. Not optimal. Constraint satisfaction problems A constraint satisfaction problem is a triple (V, D, C) where: 1. 2. 3. V = {v1, v2, …, vn} is a finite set of variables; D = {d1, d2, …, dm} is a finite set of values for vi V (i = 1, n); C = {c1, c2, …, cj} is a finite set of constraints on the values that can be assigned to different variables at the same time. The solution of the constraint satisfaction problem consists of defining substitutions for variables from corresponding sets of possible values so as to satisfy all the constraints in C. Traditional approach: “generate and test” methods or chronological backtracking. But, these methods only work on small problems, because they have exponential complexity. The N-Queens example: the constraint satisfaction approach The most important question that must be addressed with respect to this problem is how to find consistent column placements for each queen. The solution in the book is based on the idea of "choice sets". A choice set is a set of alternative placements. Consider, for example, the following configuration for N = 4: 0 0 Q 1 Q 2 Q 3 Q 1 2 3 choice set 1 = {(0,0), (1,0), (2,0), (3,0)} Q choice set 2 = {(0,1), (1,1), (2,1), (3,1)} Q Q choice set 1 choice set 2 choice set 3 = {(0,2), (1,2), (2,2), (3,2)} choice set 4 = {(0,3), (1,3), (2,3), (3,3)} choice set 4 choice set 3 Notice that in each choice set, choices are mutually exclusive and exhaustive. Each solution (legal placement of queens) is a consistent combination of choices - one from each set. To find a solution, we must: 1. Identify choice sets. 2. Use search through the set of choice sets to find a consistent combination of choices (one or all). A possible search strategy, utilizing chronological backtracking is the following one (partial graph shown): Choice set 1 (0,0) (0,1) (0,1) (2,1) (1,1) (3,1) … Choice set 2 X X Choice set 3 X Choice set 4 X X X X (inconsistent combinations of choices) X X X X A generic procedure for searching through choice sets utilizing chronological backtracking The following is a generic procedure that searches through choice sets. When an inconsistent choice is detected, it backtracks to the most recent choice looking for an alternative continuation. This strategy is called chronological backtracking. (defun Chrono (choice-sets) (if (null choice-sets) (record-solution) (dolist (choice (first choice-sets)) (while-assuming choice (if (consistent?) (Chrono (rest choice-sets))))))) Notice that when an inconsistent choice is encountered, the algorithm backtracks to the previous choice it made. This algorithm is not efficient because: (1) it is exponential, and (2) it re-invents contradictions. We shall discuss another approach called, dependency-directed backtracking handles this type of search problems in a more efficient way. Types of search In CS, there are at least three overlapping meanings of “search”: 1. 2. 3. Search for stored data. This assumes an explicitly described collection of information (for example, a DB), and the goal is to search for a specified item. An example of such search is the binary search. Search for a path to a specified goal. This suggests a search space which is not explicitly defined, except for the initial state, the goal state and the set of operators to move from one state to another. The goal is to find a path from the initial state to the goal state by examining only a small portion of the search space. Examples of this type of search are depth-first search, A* search, etc. Search for solutions. This is a more general type of a search compared to the search for a path to a goal. The idea is to efficiently find a solution to a problem among a large number of candidate solutions comprising the search space. It is assumed that at least some (but not all) candidate solutions are known in advance. The problem is how to select a subset of a presumably large set of candidate solutions to evaluate. Examples of this type of search are hill-climbing and simulated annealing. Another example is the Genetic Algorithm (GA) search, which is discussed next. Genetic Algorithms: another way of searching for solutions. The Genetic Algorithm (GA) is an example of the evolutionary approach to AI. The underlying idea is to evolve a population of candidate solutions to a given problem using operators inspired by natural genetic variation and selection. Note that evolution is not a purposive or directed process; in biology, it seems to boil down to different individuals competing for resources in the environment. Some are better than others, and they are more likely to survive and propagate their genetic material. In very simplistic terms, we can think of evolution as: A method of searching through a huge number of possibilities for solutions. In biology, this huge number of possibilities is the set of possible genetic sequences, and the desired outcome are highly fit organisms able to survive and reproduce. As a massively parallel search, where rather than working on one species at a time, evolution tests and changes millions of species in parallel. Genetic algorithms: basic terminology Chromosomes: strings of DNA that serve as a “blueprint” for the organism. Relative to GAs, the term chromosome means a candidate solution to a problem and is encoded as a string of bits. Genes: a chromosome can be divided into functional blocks of DNA, genes, which encode traits, such as eye color. A different settings for a trait (blue, green, brown, etc.) are called alleles. Each gene is located at a particular position, called a locus, on the chromosome. In a GA context, genes are single bits or short blocks of adjacent bits. An allele in a bit string is either 0 or 1 (for larger alphabets, more alleles are possible at each locus). Genome: if an organism contains multiple chromosomes in each cell, the complete collection of chromosomes is called the organism’s genome. Genotype: a set of genes contained in a genome. Crossover (or recombination): occurs when two chromosomes bump into one another exchanging chunks of genetic information, resulting in an offspring. Mutation: offspring is subject to mutation, in which elementary bits of DNA are changed from parent to offspring. In GAs, crossover and mutation are the two most widely used operators. Fitness: the probability that the organism will live to reproduce. Genetic Algorithm search: more definitions Search space: in a GA context, this refers to a (huge) collection of candidate solutions to a problem with some notion of distance between them. Searching this space means choosing which candidate solutions to test in order to identify the real (best or acceptable) solution. In most cases, the choice of the next candidate solution to be tested depends on the results of the previous tests; this is because some correlation between the quality of neighboring candidate solutions is assumed. It is also assumed that good “parent” candidate solutions from different regions in the search space can be combined via crossover to produce even better offspring candidate solutions. Fitness landscape: let each genotype be a string of j bits, and the distance between two genotypes be the number of locations at which the corresponding bits differ. Also suppose that each genotype can be assigned a real-valued fitness. A fitness landscape can be represented as a (j + 1) dimensional plot in which each genotype is a point in j dimensions and its fitness is plotted along the (j + 1)st axis. Such landscapes can have hills, peaks, valleys. Evolution can be interpreted as a process of moving populations along landscapes in particular ways, and “adaptation” can be seen as movement towards local peaks. In a GA context, crossover and mutation can be seen as ways of moving a population around on the landscape defined by the fitness function. GA operators Simplest genetic algorithms involve the following three operators: Selection: this operator selects chromosomes in the population according to their fitness for reproduction. Some GAs use a simple function of the fitness measure to select individuals to undergo genetic operation. This is called fitness-proportionate selection. Other implementations use a model in which certain randomly selected individuals in a subgroup compete and the fittest is selected. This is called tournament selection. Crossover: this operator randomly chooses a locus and exchanges the subsequences before and after that locus between two chromosomes to create two offspring. For example, consider chromosomes 11000001 and 00011111. If they crossover after their forth locus, the two offspring will be 11001111 and 00010001. Mutation: this operator randomly converts some of the bits in a chromosome. For example, if mutation occurs at the second bit in chromosome 11000001, the result is 10000001. A simple genetic algorithm The outline of a simple genetic algorithm is the following: 1. 2. 3. 4. 5. Start with the randomly generated population of “n” j-bit chromosomes. Evaluate the fitness of each chromosome. Repeat the following steps until n offspring have been created: a. Select a pair of parent chromosomes from the current population based on their fitness. b. With the probability pc, called the crossover rate, crossover the pair at a randomly chosen point to form two offspring. If no crossover occurs, the two offspring are exact copies of their respective parents. c. Mutate the two offspring at each locus with probability pm, called the mutation rate, and place the resulting chromosomes in the new population. If n is odd, one member of the new population is discarded at random. Replace the current population with the new population. Go to step 2. Each iteration of this process is called a generation. It is typical for a GA to produce between 50 to 500 generations in one run of the algorithm. Since randomness plays a large role in this process, the results of two runs are different, but each run at the end typically produces one or more highly fit chromosomes. Example Assume the following: length of each chromosome = 8, fitness function f(x) = the number of ones in the bit string, population size n = 4, crossover rate pc = 0.7, mutation rate pm = 0.001 The initial, randomly generated, population is the following: Chromosome label A B C D Chromosome string 00000110 11101110 00100000 00110100 Fitness 2 6 1 3 Example (cont.): step 3a We will use a fitness-proportionate selection, where the number of times an individual is selected for reproduction is equal to its fitness divided by the average of the fitnesses in the population, which is (2 + 6 + 1 + 3) / 4 For chromosome A, this number is 2 / 3 = 0.667 For chromosome B, this number is 6 / 3 = 2 For chromosome C, this number is 1 / 3 = 0.333 For chromosome D, this number is 3 / 3 = 1 (0.667 + 2 + 0.333 + 1 = 4) To implement this selection method, we can use “roulette-wheel sampling”, which gives each individual a slice of a circular roulette wheel equal to the individual’s fitness, i.e. Assume that the roulette wheel is spun, and the ball comes to rest on some slice; the D individual corresponding to that slice is selected B C for reproduction. Because n = 4, the roulette A wheel will be spun four times. Let the first two spins choose B and D to be parents, and the second two spins choose B and C to be parents. Example (cont.): steps 3b and 3c Step 3b Apply the crossover operator on the selected parents: Given that B and D are selected as parents, assume they crossover after the first locus with probability pc to form two offspring, say E = 10110100 and F = 01101110. Assume that B and C do not crossover thus forming two offspring which are exact copies of B and C. Step 3c: Apply the mutation operator on the selected parents: Each offspring is subject to mutation at each locus with probability pm. Let E is mutated after the sixth locus to form E’ = 10110000, and offspring B is mutated after the first locus to form B’ = 01101110. The new population now becomes: Chromosome label E’ F C B’ Chromosome string 10110000 01101110 00100000 01101110 Fitness 3 5 1 5 Note that the best string, B, with fitness 6 was lost, but the average fitness of the population increased to (3 + 5 + 1 + 5) / 4. Iterating this process will eventually result in a string with all ones.