1 soluzione dei problemi tramite grafi
Transcript
1 soluzione dei problemi tramite grafi
1 SOLUZIONE DEI PROBLEMI TRAMITE GRAFI Questo capitolo tratta della possibilità di risolvere problemi analizzando le diverse possibilità prodotte dalla scelta dell’agente. Abbiamo già visto come un agente possa essere rappresentato da una esecuzione (un run), e come questa sia il risultato della scelta di quale azione intraprendere ad ogni stato ambientale raggiunto dall’agente; una volta applicata l’azione, si raggiungerà un altro stato ambientale (o si rimarrà nello stesso, qualora l’azione sia ininfluente), e così via fino a raggiungere auspicabilmente uno degli stati che interessa all’agente: La definizione data implica che in ogni stato ambientale ci sia, almeno in linea di principio, la possibilità di scegliere tra diverse azioni, ; se così non fosse, non vi sarebbe alcuna scelta e quindi sarebbe inutile qualsiasi atto di ragionamento. Negli altri casi invece, avremo che a fronte di una situazione ambientale si potrà scegliere più di un’azione, e quindi si procederà verso diversi stati ambientali, non necessariamente già non esplorati. a1 e0 a3 a2 a1 e1 a3 e2 e3 a2 e4 e1 e7 FIGURA 1IL GRAFO DEGLI STATI TRAMITE LE AZIONI Noi siamo interessati principalmente alla risoluzione di problemi, e quindi vogliamo utilizzare uqesto meccanismo per trovare un modo razionale ed efficiente di farlo. 1.1 DEFINIZIONE DI UN PROBLEMA Il primo passo sarà analizzare i problemi che vogliamo risolvere e definirli in modo che possano essere tradotti in una navigazione su un grafo. Per far questo dobbiamo definire lo spazio degli stati che abbiamo a disposizione, ovvero i nodi del grafo e gli archi per passare da un nodo all’altro: 1. lo stato ambientale iniziale, ovvero da quale contesto parte l’agente. 2. Le azioni che l’agente può compiere; poiché le azioni ci permettono di raggiungere altri nodi stati ambientali, possiamo dire che le azioni sono funzioni successori di quello stato, ovvero una cerca azione j è uno Sj(ei), cioè restituisce l’insieme degli stati raggiungibili dallo stato ambientale ei una volta applicata. 1 Introduzione all’Intelligenza Artificiale Stefano De Luca È evidente dunque che i nodi sono gli stati ambientali e gli archi le azioni che si possono applicare al nodo sorgente per arrivare agli altri nodi. Ad es., per un navigatore satellitare lo stato iniziale sarà la posizione in cui ci troviamo, ed eventualmente la conoscenza del mezzo che deve guidare (un’automobile, una bicicletta, una persona a piedi). Poiché possiamo vedere le diverse strade già come un grafo, le azioni saranno la scelta di una strada piuttosto che un’altra (di un arco del grafo stradale); una volta percorsa quella strada, ci troveremo in un’altra posizione. Nel caso di una partita a scacchi, la posizione iniziale sarà quella iniziale della scacchiera, le azioni saranno i movimenti dei diversi pezzi che ci permetteranno di trovarci in una diversa posizione della scacchiera. Per l’acquisto di un bene su Internet, lo stato iniziale sarà la quantità di soldi che l’agente ha a disposizione, e le azioni saranno le ricerche sui siti di e-commerce, e l’acquisto su di essi. Naturalmente, non tutti i nodi sono uguali. Poiché abbiamo un obiettivo da raggiungere, abbiamo bisogno di una funzione che ci dica se con quel particolare nodo abbiamo lo abbiamo raggiunto; nel caso del navigatore, avremo raggiunto l’obiettivo nel caso che siamo arrivati a destinazione (che è un singolo caso); per gli scacchi, una qualsiasi posizione in cui avremo vinto dando scaccomatto (e sono numerosi casi); per l’acquisto del bene, il raggiungimento dell’obiettivo sarà dato dall’acquisizione del bene, indipendentemente da dove questo sia stato acquistato. Abbiamo quindi bisogno di una funzione obiettivo, che restituisca un booleano che segnali il raggiungimento dell’obiettivo; questa funzione può anche restituire un valore numerico, che segnali quanto è adeguato quel nodo all’obiettivo (dove 0 è totalmente inadeguato e 1 totalmente adeguato), e che permetta all’agente di stabilire se andare avanti a cercare una soluzione migliore o accontentarsi di quella già trovata. Da ultimo, poiché non sempre le azioni hanno lo stesso costo, dovremmo tenere conto di questo per non raggiungere l’obiettivo in tempi troppo lunghi, o con azioni tropo costose. Per poter calcolare questo costo usiamo una funzione di costo, che indichi quanto il passaggio da un nodo all’altro usando quella particolare azione sia costoso; la funzione di costo prenderà quindi in considerazione lo stato iniziale e l’azione, g(ei, aj), per segnalare quanto questa sia efficace. In alcuni problemi tutte le azioni hanno lo stesso costo, ad es. nel gioco degli scacchi, mentre in altri no, come negli altri esempi del navigatore (il costo può essere dato dalla lunghezza del percorso, o dal tempo stimato di percorrenza), e degli acquisti (il costo è dato dall’inversa di quanto si spende, o dal tempo di acquisizione del bene). Come si vede dagli esempi, non è sempre facile definire una funzione di costo, e questa può essere il risultato di diversi parametri combinati tra loro, eventualmente ognuno definito con un suo peso; ad es. per l’acquisto di un bene si potrà stabilire che la funzione costo è dato dal 70% dal costo economico del bene e dal 30% dal tempo di consegna. Percorrendo tutto l’albero, dallo stato iniziale a quello finale, potremo calcolare il costo totale sommando i valori del costo di ogni singolo arco. Ricapitolando, gli elementi necessari per la definizione di un problema sono la definizione di: 1. stato iniziale; 2. funzione successore, ovvero le azioni applicabili; 3. funzione di raggiungimento dell’obiettivo; 4. funzione di costo. 2 Introduzione all’Intelligenza Artificiale Stefano De Luca 1.2 STRATEGIE DI RICERCA Una volta definito un problema fornendo i quattro elementi testé elencati, si dovrà scegliere come navigare all’interno del grafo prodotto dall’elaborazione dei successori a partire dal nodo base. Il punto è questo: la base del problema è il nodo iniziale, ovvero l’iniziale stato ambientale; a partire da questo, dovremo andare oltre questo nodo tramite un’operazione di espansione, ovvero di raggiungimento degli altri stati ottenuti tramite azioni. La scelta di quali nodi espandere corrisponde ad una strategia di ricerca, ovvero un modo di creare e navigare tutti gli stati possibili. Esistono diverse strategie di ricerca, che differiscono per le principali proprietà: completezza: una strategia è completa se garantisce il raggiungimento dell’obbiettivo, assunto che ne esista uno; complessità nel tempo: quanto tempo impiega l’algoritmo ad essere eseguito; complessità nello spazio: di quanta memoria ha necessità; ottimalità: supposto che esistano diverse soluzioni, ordinabili per valore, l’algoritmo ci garantisce che viene trovata la migliore? Gli algoritmi di ricerca si dividono ancora in non informati, ovvero dove non si sa nulla del problema su cui si opera, ed informati, dove è possibile ottimizzare la ricerca usando informazioni specifiche al problema. 1.2.1 RICERCA PER PROFONDITÀ (DEPTH FIRST) La ricerca per profondità (depth first search) è in assoluto il più popolare algoritmo di ricerca; questo è dovuto alla sua facilità implementativa e alla buona velocità di esecuzione. L’idea dell’algoritmo è quella di espandere il primo nodo figlio e da là continuare a scendere finché non si raggiunge un nodo obiettivo o non si può più andare avanti; in quest’ultimo caso, si torna indietro (backtracking) al nodo più recente di cui non siano stati espansi tutti i nodi. FIGURA 2RICERCA PER PROFONDITÀ, ORDINE DI ESPANSIONE DEI NODI Il limite principale di questo algoritmo è che, qualora il grafo contenga dei cicli, l’algoritmo rimarrà bloccato in esso e nessun risultato sarà dato. Guardando la prossima figura, si vede come l’algoritmo 3 Introduzione all’Intelligenza Artificiale Stefano De Luca procederà passando dal nodo 1 al 2, al 3, e poi, una volta arrivato al 4 tornerà indietro nuovamente al 2, ripetendo il ciclo già detto. FIGURA 3FINIRE IN UN CICLO Si può evitare la caduta nel ciclo mantenendo in memoria i nodi già attraversati, ed evitando di tornare su di essi. Nell’esempio, da 4 non si andrà su 2 (già attraversato), ma si passerà invece al 5. Evidentemente c’è un incremento della memoria usata, ma a vantaggio della solidità dell’algoritmo. Rispetto alle specifiche suddette, possiamo dire che la ricerca in profondità: non è completo, in quanto in presenza di cicli può non trovare la soluzione; la sua complessità nel tempo è O(bm), dove b è il branching factor, ovvero di quanto si espande l’albero, e m è la massima profondità dell’albero; potenzialmente questo vuol dire che bisogna navigare tutto l’albero per trovare la soluzione. la sua complessità nello spazio è O(bm), che è senz’altro buona; non è ottimale, in quanto è possibile trovare una soluzione sub-ottimale prima di una migliore, e in linea di principio quella migliore potrebbe essere dietro un ciclo. 1.2.2 RICERCA PER AMPIEZZA (DEPTH FIRST SEARCH) La ricerca per ampiezza differisce da quella in profondità perché prova a navigare il grafo livello livello per livello; in questo modo si evita di cadere in cicli infiniti, e diventa possibile trovare sempre la soluzione, se esiste, e la soluzione ottima se ne esiste più di una. 4 Introduzione all’Intelligenza Artificiale Stefano De Luca FIGURA 4NAVIGAZIONE TRAMITE ALGORITMO PER AMPIEZZA L’idea dell’algoritmo è esplorare tutti i nodi figli prima di scendere ulteriormente in profondità. I nodi esplorati vanno a comporre una coda, che verrà usata per navigare il livello sottostante una volta che non vi siano altri nodi figli. Rispetto alle specifiche suddette, possiamo dire che la ricerca in profondità: è completo, in quanto può scendere senza incontrare cicli fino a raggiungere la soluzione; la sua complessità nel tempo è O(bd), dove b è il branching factor, ovvero di quanto si espande l’albero, e d è la profondità della soluzione; come si vede, è migliore di quella per profondità. la sua complessità nello spazio è sempre O(bd), che è invece decisamente non buona; è ottimale, in quanto è possibile trovare tutte le soluzioni, ottimali e sub-ottimali. 1.2.3 ITERATIVE DEEPENING Abbiamo visto come i due algoritmi di depth first e breadth first abbiamo ognuno degli elementi di criticità, il primo perché è soggetto ai cicli, il secondo per un uso eccessivo di memoria. Esiste un algoritmo che cerca di prendere gli elementi migliori da entrambi; già dal nome, iterative deepening, approfondimento iterativo, vediamo quale sia la strategia scelta: l’idea è di partizionare il grafo in “fasce” della stessa ampiezza, ovvero si sceglierà una profondità n (1, 2, 3…) e si esploreranno tutti i nodi a partire dalla radice che siano a distanza ≤ n; la distanza indica quanti archi bisogna attraversare dal nodo iniziale a quello di interesse. Entro questa fascia, si utilizzerà l’algoritmo di depth first, che non potrà entrare in cicli infiniti, visto che la discesa sarà limitata dal parametro di approfondimento n. Una volta esplorati tutti i nodi, verranno posti in una coda tutti i nodi sulla frontiera, ovvero quelli raggiunti esattamente a profondità n. Da là si partirà con il medesimo approccio dell’algoritmo breadth first, per ulteriori n livelli. 5 Introduzione all’Intelligenza Artificiale Stefano De Luca FIGURA 5NAVIGAZIONE DI UN GRAFO: ITERATIVE DEEPENING Rispetto alle specifiche suddette, possiamo dire che l’iterative deepening sia : è completo, in quanto trova sempre la soluzione, se questa esiste; la sua complessità nel tempo è O(bd), come nel caso del depth first la sua complessità nello spazio è O(bd), di nuovo come il depth first; è ottimale, in quanto è possibile raggiungere tutte le soluzioni. L’uso di questo algoritmo è preferito nel caso di un grande spazio di esplorazione e non sia nota la profondità della (possibile) soluzione. 1.3 RICERCHE INFORMATE Gli algoritmi visti finora non presuppongono alcuna conoscenza del problema; ci basterà conoscere la funzione successore che ci permette di passare da un nodo all’altro. Esiste però la possibilità di avere informazioni aggiuntive sul problema, come il costo (peso) del passaggio da un nodo all’altro o una stima della distanza verso la soluzione, che possono consentire di migliorare in modo importante i tempi di ricerca e la quantità di memoria necessaria. 1.3.1 ALGORITMO GOLOSO (GREEDY) Il più semplice di questi algoritmi è l’algoritmo “goloso”, o greedy. Per poterlo applicare, abbiamo bisogno di un grafo dove ogni arco sia etichettato con il costo, o peso, dell’attraversamento. Un esempio semplice, vedi anche Figura 6, sono le distanze in chilometri tra due città. Lo stesso arco può 6 Introduzione all’Intelligenza Artificiale Stefano De Luca avere anche diversi pesi: nel caso del grafo stradale, possiamo avere un altro peso che ci indica la quantità di tempo necessaria prendendo in considerazione i limiti di velocità, un altro peso che indica la pericolosità della strada, o un altro che misura la sua bellezza paesaggistica. FIGURA 6ESEMPIO DI GRAFO PESATO: DISTANZE TRA CITTÀ L’algoritmo greedy opera in modo molto semplice: a partire da un nodo, lo si espanderà tramite la funzione successore, e si sceglierà il nodo successivo in cui l’arco abbia il costo minore. Ovviamente questo algoritmo è troppo semplice; immaginiamo di partire da Oradea e voler andare a Sibiu. L’algoritmo sceglierà l’arco che porta a Zerind, che ha un peso di 71, piuttosto che quello diretto con un costo superiore 151. Ma una volta passati a Zerind, si dovrà andare necessariamente ad Arad, con un costo complessivo di 146, e da là a Sibiu, con un costo complessivo di ben 286, mentre il percorso migliore è ovviamente soltanto 151. Per migliorare questo algoritmo (che in questa modalità è detto anche best-first, ovvero si procede per primi verso il nodo immediatamente migliore), si potrò usare una euristica. L’euristica è una funzione che approssimi la soluzione finale cercando di stimare il percorso nel grafo migliore; per quanto l’euristica sia necessariamente imperfetta (se fosse perfetta, non servirebbe alcun algoritmo di ricerca su grafo, si seguirebbero semplicemente le indicazioni dell’euristica), può darci una prima approssimazione e indirizzarci, anche se non sempre, verso la soluzione migliore. Per un grafo stradale, una semplice euristica può essere la distanza in linea d’aria (straight line) tra un nodo, una città nel nostro esempio, e la città obiettivo. Nella precedente figura a destra vi sono le distanze in linea d’aria di ogni città verso Bucharest. Non è importante che la funzione sia precisa, ma è importante che rispetti queste due caratteristiche: 7 Restituire 0 se il nodo di cui si calcola l’euristica sia un nodo obiettivo. Restituire un valore comunque inferiore a quello reale, potremmo dire con approccio ottimista. Introduzione all’Intelligenza Artificiale Stefano De Luca Vediamo che la distanza in linea d’aria rispetta questi due vincoli: la distanza in linea d’aria tra una città obiettivo e se stessa sarà sempre 0, e non sarà mai possibile che la strada percorsa da una città all’altra sia inferiore alla distanza in linea d’aria. Ora è possibile usare l’euristica anche nell’algoritmo greedy. Partendo da Arad e volendo arrivare a Bucharest, si sceglierà Sibiu e non Timisoara, poiché la funzione euristica dei nodi, potremmo definirla h(x), è h(Sibiu) = 253, mentre h(Timisoara) = 329. Naturalmente l’algoritmo continua a non essere ottimale, ovvero a non necessariamente trovare il percorso migliore; basterà pensare a una città raggiungibile da due intermedie, con una distanza in linea d’aria quasi similare: se quella con minore distanza in linea d’aria ha un numero maggiore di curve e deviazioni, sarà sub-ottimale (ovvero non la migliore) rispetto all’alternativa che magari percorrerà in via diretta il percorso. In più, l’algoritmo greedy non è nemmeno completo, perché seguendo il percorso di minore costo momentaneo può portarci in porzioni del grafo che non ci consentono di raggiungere l’obiettivo. 1.3.2 ALGORITMO DI RICERCA A* <todo> 8 Introduzione all’Intelligenza Artificiale Stefano De Luca