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