Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
AI-Lisp / Lab 3 - Graph search 11 Lab 3 Graph search 3.0 INTRODUCTION This lab continues to train recursion by searching graphs. You will continue with search in the AI-labs during the second part of this course. We will start with a simple depth-first search in a graph, and then go over to a breath-first search and in successive steps make the search smarter and reach to the A*-algorithm. You can read more in Russel-Norvig’s Artificial Intelligence book chapter 4 and section 4.1 about A*, which is a heuristic search algorithm using extra knowledge from the problem domain which will speed up the search. We will also use higher-order functions in Common Lisp to be able to reuse the code that we have developed. 3.1 DEPTH-FIRST SEARCH Exercise 3.1.a. We start with some training to find a path in a maze or graph. We follow the description and the code from Haraldsson’s Lisp book section 8.3. The book is not necessary to read for this lab. We assume a maze with an entrance and an exit. We represent the maze as a graph with nodes and arcs. We want to find a path through the maze. There is no demand to find the best path, only one path but the path must be without cycles and not include blind alleys. We give here a maze and its representation as a graph. IN IN D A B K OUT C D A C B G E F I H J K G E F I OUT H J We represent the graph as an association list, where every entry in the list is a pair with the name (as a symbol) of the node and a list of its neighbors. We call the above maze a-maze. We also introduce a primitive to find the neighbors from a node. We let path-maze-depth-first to be the main function, which takes a maze, an entry and an exit node. 12 AI- Lisp / Lab 3 - Seaarch in graphs The code1 from Haraldsson’s book. You will also find all code in a Common Lisp file on the course webpages. (defun path-maze-depth-first (maze start end) (path maze start end ’())) (defun path (maze from to way) ; Find a way from the node from to the node to in the graph maze ; The parameter way holds the way from the initial start node to the node ; from (if (eq from to) ; Come to the final node (add-to-the-end to way) (path-iter maze (nodes-to from maze) to (add-to-the-end from way)))) (defun path-iter (maze alternatives to way) (cond ((endp alternatives) ’nil) ; no possible way ((member (first alternatives) way) ; already been here (path-iter maze (rest alternatives) to way)) (t (let ((try-path (path maze (first alternatives) to way))) ; check for a way to the end (if (eq try-path ’nil) (path-iter maze (rest alternatives) to way) ; no way, next alerantive try-path))))) ; a way found to the end (defun nodes-to (node maze) (first (cdr (assoc node maze)))) ; gives the neighbors (defun add-to-the-end (node way) (append way (list node))) (setq a-maze ’((in (a)) (a (in b c)) (b (a)) (c (a d e)) (d (c)) (e (c f k j)) (f (g e h)) (g (f)) (h (f i j)) (i (h)) (j (e h k out)) (k (e j)) (out (j)))) Load the code from the www-page. Load it into Lisp, trace the functions path and path-iter and follow the execution so you really understand how the depth-first search works. Observations: Notice the way we do back-tracking in the last case in the cond-expression in the function path-iter, where we have a parameter alternatives of possible nodes to continue with. We take the first alternative from the list and call the function path, to try to find a path to the goal. We bind up the resulting value and check to see if we found a path or not (when the value is nil). If we received a path (a non-nil list) we return that value. If there was no path we continue with the next alternative. We can also see that it is no guidance to find a good path. The path depends on in what order the neighbors are placed in the list holding the maze. Another result is giving by (setq another-maze ’((in (a)) (a (in c b)) (b (a)) (c (e d a)) (d (c)) (e (c k f j)) (f (e g h)) (g (f)) (h (f i j)) (i (h)) (j (h e k out)) (k (e j)) (out (j)))) Compare the results from the two different representations of the maze. 1. There are small changes from the book. The termination condition is moved to the function path. There was no arc from node K to node J. Here the representation. consists of the node and a list of the list of neighbors. AI-Lisp / Lab 3 - Graph search 13 ASSIGNMENT 3A - BREADTH-FIRST SEARCH IN THE MAZE In breath-first search we expand the nodes in order from the first node to all its neighbors before we expand a neighbor node. For a breadth-first search algorithm we need to maintain two lists. We need one list with all nodes which are candidates to expand and to continue the search from. These nodes are neighbors to an expanded node (not already expanded or stored in the queue), which later, in its turn, may be expanded. We call that parameter que-of-not-visited-nodes. A "queue element " may contain information about the node. This queue could be sorted to make a more efficient search. See assignments 3B and 3C. On the other list visited-node we store all nodes we have visited and expanded. We also introduce a "queue update function" (bound to the parameter que-update-fn), which updates the queue que-of-not-visited-nodes every time we have expanded a node with information about the neighbors we reached from that node. This function will differ depending on type of search as in assignments 3B and 3C. The structure of the code is such that it can be reused in assignments 3B and 3C. The structure is still such that the main search has been divided into two functions, where the function path-b searches from a given node from-node and finds all of its successors and updates the queue with respect to the "queue update function". (defun path-maze-breadth-first (maze start end) (path-b maze (build-que-element start ’()) end ’() ’() #’update-que-breadth-first)) (defun path-b (maze from-node to que-of-not-visited-nodes visited-nodes que-update-fn) (print-info from-node que-of-not-visited-nodes visited-nodes) (if (eq (node-name from-node) to) (add-to-the-end to (way from-node)) (path-iter-b maze to (funcall que-update-fn from-node to (nodes-to (node-name from-node) maze) que-of-not-visited-nodes visited-nodes maze) (cons (node-name from-node) visited-nodes) que-update-fn))) (defun path-iter-b (maze to que-of-not-visited-nodes visited-nodes que-update-fn) (if (endp que-of-not-visited-nodes) ’nil ; no way is found (path-b maze (first que-of-not-visited-nodes) to (rest que-of-not-visited-nodes) visited-nodes que-update-fn))) For this first exercise we need an update function of the queue which takes the old queue and at the end appends the new nodes which have not yet been visited or which already have been stored into the queue. We introduce a higher order function filter-and-map which for a list applies a filter-function to decide if the element should be stored in the queue of not yet visited nodes and a function which updates a node to be a queue element. Here a queue element is a list with two elements, a node name and the way from the start node to this actual node. 14 AI- Lisp / Lab 3 - Seaarch in graphs (defun update-que-breadth-first (from-node to new-nodes-to-visit que-of-not-visited-nodes visited-nodes maze) ; Parameters to and maze are not needed here (append que-of-not-visited-nodes (filter-and-map #’(lambda (new-node) ; filter out new nodes to put on the queue (not (or ; the node has already been visited (member new-node visited-nodes) ; the node is already on the queue (member-if2 #’(lambda (que-element) (eq new-node (node-name que-element))) que-of-not-visited-nodes)))) #’(lambda (node) ; creates a queue-object to remember the way up to the node (build-que-element node (add-to-the-end (node-name from-node) (way from-node)))) new-nodes-to-visit))) We introduce functions for creating a queue element and to retrieve its parts. We do this function more general and have two optional parameters which we do not need to use here3. They will be used in exercises 3B and 3C. (defun build-que-element (node-name way &optional dist-from-start estimated-total-dist) (cond ((eq dist-from-start nil) (list node-name way)) ; For 3A ((eq estimated-total-dist nil) (list node-name way dist-from-start)) ; For 3B (t (list node-name way dist-from-start estimated-total-dist)))) ; For 3C (defun node-name (que-element) (first que-element)) (defun way (que-element) (second que-element)) (defun distance-from-start (que-element) (third que-element)) (defun estimated-total-dist (que-element) (fourth que-element)) We also introduce a test print function print-info, which prints the nodes we visit and the queue and the visited nodes. The printing can be on or off (is on from the beginning) and can be changed by giving the global variable *print-info* the value t respectively nil. (defun print-info (from-node que-of-not-visited-nodes visited-nodes) (when *print-info* (format t "~&node= ~s~%queue= ~s~%visited nodes= ~s~%~%" from-node que-of-not-visited-nodes visited-nodes))) (setf *print-info* t) ; initially print information 2. We use here a variant of member, where the first argument is a function describing what kindf of membership for an element should be used in the list. 3. If an optional parameter is not given it is initialized to nil. AI-Lisp / Lab 3 - Graph search 15 Assignment 3A. Define the higher order function filter-and-map. It takes three arguments. First argument is a predicate function which will be applied on every element in the list and tells if the element should remain in the result or be filtered away. The second arguments is a function which is applied on every elements which should remain and may update that element. The third argument is the list. (defun filter-and-map (filter-fn? map-fn l) .... your solution ...) (filter-and-map #’numberp #’(lambda (n) (* n n)) ’(1 a 2 b 3 4 c)) => (1 4 9 16) ; take every numebr and square it (filter-and-map #’(lambda (n) (> n 0)) #’list ’(1 -2 3 -4 -5 6)) => ((1) (3) (6)) ; take every positive number and put it into a list With this function you should test the breath-first search and follow its behavior by making i.e. trace with the print-info function, so you understand how breath-first search works. It is important for the remaining assignments 3B and 3C. Compare also its behavior with depth-first search. ASSIGNMENT 3B - FIND THE SHORTEST PATH WITH DIJKSTRAS ALGORITHM In this assignment you will start with the code from 3A and implement an algorithm to find the shortest path. Search is described in the Russel-Norvig Artificial Intelligence book. We introduce now a distance between nodes. We change the representation to let every node to be a list (node-name ((node-name distance) (node-name distance) ...)) (setq a-maze-D ’((in ((a 5))) (a ((in 5) (b 2) (c 4))) (b ((a 2))) (c ((a 4) (d 10) (e 20))) (d ((c 10))) (e ((c 20) (f 4) (k 8) (j 15))) (f ((g 6) (e 4) (h 1))) (g ((f 6))) (h ((f 1) (i 3) (j 4))) (i ((h 3))) (j ((e 15) (h 4) (k 18) (out 14))) (k ((e 8) (j 18))) (out ((j 14))))) IN 5 D A 4 10 2 K 18 C 20 B G 8 E 15 6 4 F I 3 1 H 4 OUT 14 J 16 AI- Lisp / Lab 3 - Seaarch in graphs We introduce now a new main function path-maze-Dijkstra: (defun path-maze-Dijkstra (maze start end) (path-b maze (build-que-element start ’() 0) end ’() ’() #’que-update-D)) The functions path-b and path-iter-b should not be changed. Assignment 3B. We need a new update function which we here call que-update-D. The elements in the queue may look as (nodname path distance-from-start).We store in every queue node element also the distance from the start node, by adding the distances between nodes on the way up to the actual node. We also store the elements in sorted order depending of the distance from the start node. This guaranties that we always take the node to expand, which is closest to the start node. We also guarantee that we never process nodes on paths which are longer than the final path. Observe the following properties of the algorithm. When you expand a node you have then found the shortest path up to that node. If you come to that node later it should not be placed on the queue again. You may get to a node again (not yet expanded) which is already stored on the queue. If the way is shorter to that node it should replace the one already stored on the queue. Test the function and show by examples that you have understood the algorithm and that you will find the shortest path. ASSIGNMENT 3C - HEURISTIC SEARCH WITH THE A*-ALGORITHM Now we will implement a much better algorithm using knowledge from the problem domain. Read carefully the section in Norvig-Russels Artificial Intelligence book in section 4.1. The information from the problem domain is that we know in a coordinate system where the nodes are. This will be used to make much better estimates how close a node is to the goal node. Now we will store the maze in a coordinate system, so that every node has a coordinate. We expand the representation so that every node is represented with (node-name list-with-neighbor-nodes (x-coordinate y-coordinate)) (setq a-maze-A* ’((in ((a 5)) (2 19)) (a ((in 5) (b 2) (c 4)) (2 15)) (b ((a 2)) (1 14)) (c ((a 4) (d 10) (e 20)) (5 16)) (d ((c 10)) (6 19)) (e ((c 20) (f 4) (k 8) (j 15)) (7 12)) (f ((g 6) (e 4) (h 1)) (10 14)) (g ((f 6)) (11 17)) (h ((f 1) (i 3) (j 4)) (10 13)) (i ((h 3)) (8 11)) (j ((e 15) (h 4) (k 18) (out 14)) (11 10)) (k ((e 8) (j 18)) (3 9)) (out ((j 14)) (8 1)) )) We will now use the coordinates to speed up the search. We give a merit to each node we have put on the queue. The merit is the sum of the actual distance from the start node to the actual node and an estimate of the remaining distance from the actual node to the goal node. The estimated distance can be calculated as the “straight distance” between the node and the goal node. In the real world the real distance is equal or longer than this value, but it is a good estimate how good the node is compared with the other nodes. We give this merit to each node which we store onto the queue. The A* algorithm works optimal if the estimated merit is equal or shorter than the real distance and the algorithm will find the shortest path. There are proofs in the AI book, but is not needed for this assignment. AI-Lisp / Lab 3 - Graph search 17 We introduce a new main function path-maze-A*: (defun path-maze-A* (maze start end) (path-b maze (build-que-element start ’() 0 (estimated-distance start end maze)) end ’() ’() #’que-update-A*)) It is appropriate to introduce some primitives: (defun x-koord (node maze) (first (koordinates node maze))) (defun y-koord (node maze) (second (koordinates node maze))) (defun koordinates (node maze) (second (cdr (assoc node maze)))) Assignment 3C. Define the function que-update-A*. The structure of the function should follow the get-update-D function from assignment 3B, but the calculations of the distances differ, 18 AI- Lisp / Lab 3 - Seaarch in graphs