Prolog - Informatica
Transcript
Prolog - Informatica
Prolog Intelligenza artificiale Stefano De Luca Temi Basics Liste Ricursione (sulle liste) Grafi Alberi Database Predicati di secondordine Basics Cfr. slides di Marco Pirrone Liste Le liste sono oggetti nativi rappresentate con la sintassi [a, b, c, …, d ] dove a, b… sono gli elementi della lista Ogni lista termina con un elemento “nascosto” che è la lista vuota, che si rappresenta con [] È possibile dividere la lista in due parti, il suo primo elemento (la testa) e la parte restante (la coda) tramite la sintassi [H|T] . Ad es. [a, b, c] = [H|T] unifica con H=a e T=[b,c] Importante notare che la testa non è una lista, mentre la coda lo è Ricursione sulle liste Sfruttando la sintassi che abbiamo visto, possiamo operare facilmente sulle liste con la ricursione Sarà necessario definire la condizione di stop (tipicamente associata al caso zero, ovvero la lista vuota), e quindi definire il caso generale Ad esempio, se vogliamo generare una lista che contenga il doppio dei valori di un’altra (ovvero: doppio([1,2,3], [2,4,6]) potremo definire un predicato così: doppiol([], []). % un fatto. % il doppio di una lista vuota è la lista vuota doppiol([H|T], [H1|T1]) :H1 is H * 2, % qui si calcola il doppio della testa doppiol(T, T1). % avendo definito la testa, % la coda viene calcolata per ricursione Grafi Un grafo può essere rappresentato tramite l’insieme dei predicati che descrivono gli archi e i vertici edge(1, 5). edge(1, 7). edge(2, 1). edge(2, 7). edge(3, 1). edge(3, 6). edge(4, 3). edge(4, 5). edge(5, 8). edge(6, 4). edge(6, 5). edge(7, 5). edge(8, 6). edge(8, 7). vertices([1, 2, 3, 4, 5, 6, 7, 8]). Esempio di uso dei grafi Obiettivo: definire un predicato che vada da un nodo all’altro evitando i loop, Il predicato indicherà la partenza, l’arrivo, i nodi visitati e il path percorso: path(Start, Finish, Visited, Path). Definiamolo per ricursione: Quando abbiamo raggiunto il nodo finale, abbiamo completato la ricerca (il caso zero): path(Node, Node, _, [Node]). Il caso generale: un path da Start a Finish inizia con un nodo X connesso a Start, seguito da un path da X a Finish: path(Start, Finish, Visited, [Start | Path]) :edge(Start, X), not(member(X, Visited)), path(X, Finish, [X | Visited], Path). Rappresentazione degli stati Vogliamo risolvere il problemi dei cannibali e dei missionari: tre cannibali e tre missionari sono su una sponda di un fiume dove c’è una barca con soli due posti a disposizione Devono attraversare il fiume a coppie, ma i missionari non vogliono rimanere in minoranza, per paura di essere mangiati Si rappresenta lo stato finale (il goal) come uno stato che indichi cosa vogliamo ottenere Gli stati Lo stato indica la situazione ad un momento specifico della soluzione (una situazione) Nell’esempio, basta indicare: Il numero di missionari sulla sponda sinistra Il numero di cannibali sulla sponda sinistra Su quale sponda è la barca Cosa che possiamo ottenere con il predicato: state(Missionaries, Cannibals, Side) La soluzione La soluzione consiste di una lista di mosse: [move(1, 1, right), move(2, 0, left)] Che indica che un missionario e un cannibile vanno sulla sponda destra e quindi due missionari sulla sinistra Per evitare i cicli, dobbiamo verificare di non aver già raggiunto uno stato verificato usando una lista degli stati percorsi, dalla forma: [MostRecent_State | ListOfPreviousStates] Usare i grafi per risolvere problemi Per risolvere un problema, possiamo usare lo stesso approccio seguito per ricercare su un grafo: Si inizia da uno stato iniziale Si trovano (o si generano) i nodi espansi, ovvero gli stati “vicini” Si controlla che i nuovi stati non siano già stati visitati Si trova un percorso dallo stato vicino al goal Nell’esempio, la ricerca termina quando lo stato è: state(0, 0, right). Main code % mandc(CurrentState, Visited, Path) mandc(state(0, 0, right), _, []). mandc(CurrentState, Visited, [Move | RestOfMoves]) :newstate(CurrentState, NextState), not(member(NextState, Visited)), make_move(CurrentState, NextState, Move), mandc(NextState, [NextState | Visited], RestOfMoves]). make_move( state(M1, C1, left), state(M2, C2, right), move(M, C, right)) :M is M1 - M2, C is C1 - C2. make_move( state(M1, C1, right), state(M2, C2, left), move(M, C, left)) :M is M2 - M1, C is C2 - C1. Mosse possibili Bisogna sapere quali sono le mosse possibili, quindi definiamo i predicati: carry(2, carry(1, carry(1, carry(0, 0). 0). 1). 1). carry(0, 2). Dove carry(M, C) indica che la barca porta M missionari e C cannibali Mosse possibili Una volta trovata una mossa potenziale, bisogna verificare che sia possibile Nell’esempio, non è possibile muovere più missionari o più cannibali di quelli presenti su una sponda. Quando lo stato è state(M1, C1, left) e proviamo carry(M, C) deve essere vero che M <= M1 and C <= C1 Quando lo stato è state(M1, C1, right) e proviamo carry(M, C) allora si deve verificare che M + M1 <= 3 and C + C1 <= 3 sia vero Mosse legali Una volta verificato che la mossa è possibile, bisogna verificare che sia legale Nell’esempio, non si deve lasciare un missionario da solo: legal(X, X). legal(3, X). legal(0, X). Nuovi stati Da ultimo, si generano i nuovi stati: newstate(state(M1, C1, left), state(M2, C2, right)) :- carry(M, C), M <= M1, C <= C1, M2 is M1 - M, C2 is C1 - C, legal(M2, C2). newstate(state(M1, C1, right), state(M2, C2, left)) :carry(M, C), M2 is M1 + M, C2 is C1 + C, M2 <= 3, C2 <= 3, legal(M2, C2). Ricerche (attraversamenti) Depth first Breadth first Altri sistemi (iterative deepening, best-first, A* etc.) Ricerca depth-first La ricerca in un grafo depth-first è un semplice caso di ricursione, dando il caso iniziale e il caso finale (stop) dfs(S,[S]) :- stop(S). dfs(S,[S|Rest]) :arc(S,S2), dfs(S2,Rest). Ricerca breadth-first bfs :- start(S), bfs(S, Path), showPath(Path). bfs(S,Path) :- La breadth first richiede l’uso di una coda (l’elenco dei nodi attraversati all’ultimo stadio) empty_queue(Q1), head_queue([S],Q1,Q2), bfs1(Q2,Path). bfs1(Q,[G,S|Tail]) :head_queue([S|Tail],_,Q), arc(S,G), stop(G). bfs1(Q1,Solution) :queue_head([S|Tail],Q2,Q1), findall( [Succ,S|Tail], (arc(S,Succ), \+member(Succ,Tail)), NewPaths ), queue_last_list(NewPaths,Q2,Q3), bfs1(Q3,Solution). Database Finora abbiamo visto i fatti soltanto inseriti da programma, ovvero in modo statico È possibile intervenire sul database delle informazioni (la knowledge base) usando alcuni predicati: I termini possono essere rimossi con i predicati assert(Termine) e le varianti asserta(Termine) ed assertz(termine) retract(Termine) retractall(Head) Si possono indicizzare i dati con recorda(+Chiave, +Termine) recorded(+Chiave, -Termine) Nel caso si vogliano inserire i dati in parte da programma e in parte a runtime, va segnalato che il predicato è dinamico: :- dynamic arc/2. % si indica il predicato e la sua arità % ovvero il numero di argomenti arc(1, 3). arc(3, goal). Quindi si possono fare delle assert: assert(arc(1,2)). Elenco di risultati Ci sono dei metapredicati che consentono di ottenere tutte le soluzioni di una query: Es: Nell’esempio del grafo precedente, se volessimo conoscere tutti i nodi che sono collegati al nodo 5, scriveremo: setof(+Template, +Goal, -Set) bagof(+Template, +Goal, -Bag) findall(+Template, +Goal, -Bag) findall(X, edge(X, 5), L). L = [1, 4, 6, 7] ; risultato Cioè, la prima variabile è quella di cui vogliamo avere tutti i risultati, il secondo elemento è il termine che vogliamo unificare, il terzo è la lista risultante Con bagof è possibile avere altre variabili libere nel termine. Con il costrutto +Var^Goal diciamo a bagof di non fare il bind della variabile Var setof è simile a bagof, ma senza duplicati Esempio di bagof e setof 2 ?- listing(foo). foo(a, b, c). foo(a, b, d). foo(b, c, e). foo(b, c, f). foo(c, c, g). Yes 3 ?- bagof(C, foo(A, B, C), Cs). A = a, B = b, C = G308, Cs = [c, d] ; A = b, B = c, C = G308, Cs = [e, f] ; A = c, B = c, C = G308, Cs = [g] ; No 4 ?- bagof(C, A^foo(A, B, C), Cs). A = G324, B = b, C = G326, Cs = [c, d] ; A = G324, B = c, C = G326, Cs = [e, f, g] ; No