Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Tecniche di risoluzione dei problemi

SOLUZIONE DEI PROBLEMI E PIANIFICAZIONE Maurizio Matteuzzi It has come to my attention that every time we solve a problem, we create two more. From now on, all problem solving is forbidden La risoluzione dei problemi è in un certo senso l’anima, la quintessenza dell’IA; il modello fisico-simbolico di Simon e Newell è stato ciò che ha caratterizzato i primordi della disciplina, e il suo contraltare algoritmico è stato il GPS, il progetto di un programma in grado di risolvere, in senso del tutto generale, i problemi, ovvero, qualsiasi problema. Questo approccio, evidentemente troppo ambizioso, fu poi nelle fasi successive abbandonato, e sostituito da quello dei così detti “expert systems”, sistemi riferiti a un “micromondo”, molto meno ambiziosi dunque, ma più prossimi ad una realizzazione concreta. Ciò non di meno, il GPS costituisce di fatto una pietra miliare dello sviluppo della nostra materia, e un termine di paragone ineludibile. La prima questione da affrontare è come si formalizza un problema. Se un problema non è opportunamente formalizzato, è chiaro che non potrà mai essere “trattato” con mezzi informatici. Il tema della rappresentazione della conoscenza rimane centrale in tutte le applicazioni di intelligenza artificiale1. Le esigenze del problem solving, da questo punto di vista, si incentrano sulla necessità di rappresentare il problema che si vuole risolvere. Una base matematica estremamente utile per questo aspetto, e in un certo senso imprescindibile in quanto ne costituisce la strumentazione essenziale, è la teoria dei grafi. Si faccia bene attenzione a non confondere “grafo” con “grafico” (che disgraziatamente si dicono in inglese tutti e due “graph”, il che spesso ingenera confusione nell’abbondantissima letteratura tradotta): un grafico è una 1 Per una discussione e una trattazione approfondita si rimanda a Pezzulo (in questo volume). 110 Capitolo 3 rappresentazione, schematica o pittorica, ma sempre di tipo “iconico”, ovvero la cui espressività è basata sulla verosimiglianza, ossia la similitudine del segno con l’oggetto denotato. Un grafo, viceversa, è una precisa struttura matematica. Sia dato un insieme di “vertici”, che chiameremo V; sia dato un insieme di “spigoli”, che chiameremo S. Allora un grafo è una funzione iniettiva di S sul prodotto cartesiano di V per se stesso. Intuitivamente ciò significa che, data una coppia di vertici qualsiasi, non è detto che ci sia lo spigolo che li collega. È facile, da questa definizione, passare a una forma di rappresentazione grafica. Tracceremo uno spigolo di collegamento tra quei vertici che sono correlati: A B A B Figura 1 Un grafo è quindi il modo canonico per rappresentare una relazione: gli elementi diventano i vertici, e si tracciano gli spigoli colleganti quelle coppie di vertici per i quali la relazione vale. Così, se abbiamo Aldo, Brando, Carlo, Damiano ed Ermanno, e Carlo è figlio di Aldo, mentre Damiano è figlio di Ermanno, il grafo corrispondente prenderà la seguente forma: B A C E D Figura 2 Soluzione dei problemi e pianificazione 111 È ovvio che in questo modo noi rappresentiamo una relazione in senso matematico, cioè un insieme di coppie, eventualmente ordinate; questo significa che la distanza e la forma dei collegamenti perdono di significato dal nostro punto di vista. E significa anche, quindi, che si possono dare forme apparentemente diverse che in realtà costituiscono lo stesso grafo: C A B D E Figura 3 Ciò vale a dire che un grafo può essere disegnato in diversi modi, pur conservando le stesse proprietà. La teoria dei grafi si fa risalire a un lavoro di Eulero, che vale la pena di richiamare, e che si riferisce ai “Sette ponti di Koenigsberg”. La città di Koenigsberg (oggi Caliningrad), ben nota per avere dato i natali a Kant, era costituita di quattro parti, due isolette sul fiume e le due sponde dello stesso. Tra queste quattro parti si collocavano sette ponti, secondo la figura seguente Figura 4 112 Capitolo 3 Tra queste quattro parti si collocavano sette ponti, secondo la figura seguente: B C A D Figura 5 – I ponti di Koenigsberg in forma stilizzata. Ora, venne posto ad Eulero il problema che non si riusciva a visitare tutta la città passando una e una sola volta per ciascun ponte e ritornando al punto iniziale. Il ragionamento di Eulero è il seguente: se devo visitare una zona, ogni volta che c’è una entrata ci deve essere anche un’uscita. Dunque il numero dei collegamenti deve essere pari. Ragioniamo in termini di grafi: B A C D Figura 6 – Grafo dei ponti di Koenigsberg. In termini moderni oggi diremmo: “In un grafo, esiste una linea di Eulero (cioè un percorso che collega tutti i nodi attraversando ogni spigolo una sola volta) se e soltanto se i nodi sono tutti di grado locale pari”. Come si vede, nel grafo che rappresenta il problema di Koenigsberg, tutti e quattro i nodi hanno grado locale dispari, rispettivamente 5, 3, 3, 3. Analogamente, un percorso che colleghi tutti i nodi partendo da un nodo A e finendo in un nodo B, ciò che si chiama “linea di Hamilgton”, esiste se e solo se A e B sono gli unici due nodi di grado dispari. A queste considerazioni, relativamente Soluzione dei problemi e pianificazione 113 semplici, si riconducono tutti quei problemi che richiedono di tracciare una figura senza mai staccare la penna: A B Figura 7 – Busta aperta. Qui si vede che il problema è risolubile, a patto di partire da A o da B. Viceversa, non è risolubile l’analogo della figura che segue: Figura 8 – Busta chiusa. Per i nostri scopi, non è in questa sede necessario sviluppare la teoria dei grafi. È però importante che se ne capisca la natura, e che ci si familiarizzi con un particolare tipo di grafo, che gioca un ruolo essenziale nel problem solving: l’albero. Un albero si definisce come un grafo aciclico, ossia privo di cammini che tornino su se stessi, ciò che in gergo informatico si chiama “loop”. I grafi alberi presentano diverse proprietà interessanti dal nostro punto di vista. L’assenza di loop può infatti essere sfruttata per l’importante caratteristica che, dato un nodo, esiste ed è unico il percorso che lo collega al nodo radice. Ogni nodo ha infatti un solo nodo padre, mentre può avere un numero qualsiasi di figli. Così questa proprietà, della unicità di percorso, è utilizzata dai sistemi operativi per rendere univoca la denominazione di un file. La metafora delle cartelle (“directories”) che contengono altre cartelle, o files, oggi universalmente adottata dal software di base, costituisce in realtà l’implementazione di una struttura ad albero. 114 Capitolo 3 DESKTOP PROGRAMMI WINDOWS ALTRI DOCUMENTI LETTERE FORMALI IMMAGINI PRIVATE Figura 9 – Struttura ad albero di file system generico: desktop, programmi, lettere, etc. Diverse sono le proprietà notevoli dei grafi alberi. Tra esse, va notato che un albero è una struttura ricorsiva. Se separiamo un qualsiasi nodo, con tutto quello che da esso dipende, abbiamo un sottoalbero, mentre anche la struttura decurtata è un albero. Se in un albero sostituiamo un nodo terminale, cioè una foglia, con un albero, quanto viene prodotto è ancora un albero. E questo è naturalmente molto comodo per il così detto “mounting”, ovvero l’aggiunta a un file system di una nuova area di memoria. Un albero è anche una struttura strettamente gerarchica: abbiamo figli di primo livello, poi si passa ai figli dei figli, e così via. Ad ogni livello il numero dei nodi cresce, secondo un parametro che viene detto “fattore di ramificazione”. Ad esempio, se pensiamo all’albero genealogico di una persona, dato che ognuno ha un padre e una madre, avremo un fattore di ramificazione 2, ossia un albero binario. Una caratteristica notevole del grafo albero è che si presta a rappresentare in modo naturale l’andamento di una risoluzione di un problema. In un problema noi abbiamo una situazione di partenza, che viene identificata con il nodo radice, e delle possibili azioni, ossia operatori che fanno transire da uno stato ad un altro. Si parla dunque del così detto “spazio degli stati” di un problema, che si sviluppa secondo un grafo albero. Si pensi ad una partita a scacchi. Abbiamo una situazione iniziale, che è la disposizione dei pezzi sulla scacchiera, e che costituisce il nodo radice. Il bianco ha a disposizione venti possibili mosse, quindi il nodo radice ha venti figli. Il nero può rispondere a sua volta in venti modi, dunque ciascun figlio del nodo radice ha a sua volta venti figli. E così via. L’insieme teorico di tutte le possibili mosse, e quindi di tutte le possibili “partite”, prende il nome di “spazio degli stati” del problema. I nodi così detti “foglia”, cioè terminali, sono i possibili esiti (vince il bianco – vince il nero – stallo). Cosa vuol dire allora “risolvere” un problema? Vuol dire determinare un percorso che, partendo dal nodo radice, e applicando solo operatori ammessi, giunga ad una foglia del valore voluto (ad esempio, se gioco col bianco, ad una delle foglie di valore “vince il bianco”). Supponiamo ora che sia data una certa situazione sulla scacchiera. Ad esempio, uno dei classici problemi di Soluzione dei problemi e pianificazione 115 scacchi, del tipo “il bianco muove e matta in due mosse”. Se pensiamo all’albero dei possibili stati del caso dato, riscontreremo facilmente che esso è un sottoalbero dello spazio degli stati come definito prima. Esso prende il nome di “spazio problemico”, ed è costituito da tutti i possibili nodi effettivamente raggiungibili, con gli operatori dati, a partire dal problema considerato. Infine, supponiamo adesso di cercare una soluzione per il nostro problema. È chiaro che solo nel caso più sfortunato dovremo percorrere tutto lo spazio problemico prima di trovare una soluzione. Abbiamo dunque un ulteriore sottoalbero, che prende il nome di “spazio di ricerca”. Pertanto, se denominiamo: ฀ ฀ ฀ ฀ ฀ ฀ ฀ ฀ ฀ ฀ ฀ ฀ ฀ ฀ ฀ avremo che, in generale, SR Œ SP Œ SS Alla luce delle considerazioni su esposte, possiamo concludere che trovare una soluzione a un problema diviene prima di tutto, una questione di “navigazione” entro un grafo albero. Il procedimento risolutivo sarà tanto più efficiente quanto più lo spazio di ricerca sarà ridotto rispetto allo spazio problemico. In quest’ordine di idee, ecco che possiamo assumere una prima, fondamentale distinzione. Noi possiamo pensare ad un modo meccanico di navigare un grafo, seguendo regole deterministicamente predefinite, oppure assumere una strategia, che faccia appello a un qualche criterio di scelta tra le possibili vie da seguire. Nel primo caso avremo la certezza del successo, ma dovremo confrontarci con il fattore di ramificazione, ossia con la crescita esponenziale degli stati, nel secondo avremo una efficienza molto maggiore, ma ci assumeremo il rischio dell’insuccesso. La prima via prende il nome di “ricerca cieca”, la seconda di “ricerca euristica”. Tecniche di ricerca cieca Come abbiamo detto, la ricerca cieca costituisce un modo di analizzare sistematicamente, secondo un procedimento deterministico, tutti i nodi di un albero, alla ricerca della soluzione del problema. Un algoritmo di ricerca è una procedura che prende in input un problema, e restituisce il fallimento, oppure una soluzione, ossia un cammino che collega il nodo radice con una foglia voluta, o “goal”. Data la struttura di un albero, l’esplorazione può essere organizzata in profondità, cioè passando da un nodo padre a un nodo figlio, e poi al figlio del figlio e così via, scendendo sempre più verso il basso (la rappresentazione tradizionale di un albero è con la radice in alto e i nodi foglie in basso), oppure analizzando tutti i nodi dello stesso livello, per poi passare al livello successivo. Si hanno pertanto due famiglie di algoritmi. Nel primo caso parliamo di “ricerca in profondità”, nel secondo di “ricerca in ampiezza”. Come vedremo, ciascuna presenta comparativamente vantaggi e svantaggi. Capitolo 3 116 Algoritmo di ricerca in profondità 0 Vuota la lista 1 Inserisci il nodo radice nella lista 2 SE non ci sono più nodi della lista ALLORA fermati: la procedura ha fallito 3 SE il nodo all’inizio della lista è un nodo finale ALLORA fermati: hai risolto il problema 4 SE il nodo all’inizio della lista non ha figli ALLORA toglilo dalla lista ALTRIMENTI sostituisci tale nodo con i suoi figli inserendoli all’inizio della lista stessa 5 Ritorna al passo 22 È opportuno notare il comportamento della ricerca in profondità quando viene raggiunto un nodo foglia che non costituisce una soluzione. In questo caso, l’algoritmo compie la così detta operazione di “backtracking”, che consiste nel risalire al nodo padre, e proseguire esplorando un diverso figlio. In questa forma, il backtracking si dice “cronologico”, perché si ritorna all’ultimo nodo visitato in precedenza. La scansione dell’albero avviene pertanto da sinistra a destra, vale a dire, verrà trovata la soluzione più a sinistra di tutte. In questa versione, l’algoritmo è tuttavia esposto ad alcuni non trascurabili rischi. Il primo e più importante è che non tiene memoria dei nodi già visitati, e questo può portarlo a un loop infinto. Un primo miglioramento indispensabile è allora la così detta “detezione di cicli”. L’algoritmo è sostanzialmente lo stesso, ma anziché gestire una sola lista, ne gestisce due; in una vengono elencati i nodi ancora da analizzare, nell’altra quelli già visitati. Se un nodo è già stato visitato, viene escluso dal novero delle possibilità di prosecuzione. Questa modifica non garantisce però ancora che la ricerca in profondità non prenda un andamento che va all’infinto, pur visitando nodi sempre diversi. Supponiamo ad esempio che il nostro problema sia quello di generare una terna pitagorica, cioè tre numeri tali che il quadrato del primo sia uguale alla somma dei quadrati degli altri due. Supponiamo che il nostro punto di partenza sia la terna <1, 1, 1>, e che gli operatori consentiti siano “aggiungi 1 al primo elemento”, “aggiungi 1 al secondo elemento”, “aggiungi 1 al terzo elemento”. Il programma, applicandoli in quest1ordine, genererà successivamente le terne <2, 1, 1> <3, 1, 1> <4, 1, 1> … 2 Cfr Fum (1994). Soluzione dei problemi e pianificazione 117 Si vede pertanto che l’algoritmo ha imboccato un ramo infinito dell’albero. Situazioni come questa possono essere affrontate con un algoritmo che scandaglia l’albero a livelli crescenti di profondità, ovvero per approfondimento iterativo. Algoritmo di ricerca per approfondimento iterativo 0 Vuota la lista 1 Poni il limite corrente P uguale a 1 2 Inserisci il nodo radice nella lista 3 SE non ci sono più nodi da esplorare ALLORA fermati: la procedura ha fallito 4 SE non ci sono più nodi della lista ALLORA 4.1 Incrementa di 1 il valore di P 4.2 Ritorna al passo 2 5 SE il nodo all’inizio della lista è un nodo finale ALLORA fermati: hai risolto il problema 6 SE il nodo all’inizio della lista non ha figli OPPURE il nodo ha una profondità uguale a P ALLORA toglilo dalla lista ALTRIMENTI sostituisci tale nodo con i suoi figli inserendoli all’inizio della lista stessa 7 Ritorna al passo 3 Questa versione rappresenta il migliore algoritmo di ricerca cieca in profondità. La ricerca in ampiezza si muove, anziché in profondità, esplorando i nodi per livelli successivi; prima di passare al livello N, analizza quindi tutti i nodi del livello N – 1. L’algoritmo può essere schematizzato come segue:. Algoritmo di ricerca per ampiezza 0 Vuota la lista 1 Inserisci il nodo radice nella lista 2 SE non ci sono più nodi della lista ALLORA fermati: la procedura ha fallito 3 SE il nodo all’inizio della lista è un nodo finale ALLORA fermati: hai risolto il problema 4 SE il nodo all’inizio della lista non ha figli ALLORA toglilo dalla lista ALTRIMENTI sostituisci tale nodo con i suoi figli inserendoli alla fine della lista stessa 5 Ritorna al passo 2 118 Capitolo 3 Si confronti l’algoritmo con la prima versione dell’algoritmo di ricerca in profondità. Come si vede, essi sono identici, a meno dell’istruzione 4. Nel caso della ricerca in profondità, i nodi figlio devono essere inseriti all’inizio, nella ricerca in ampiezza alla fine della lista. La struttura dati utilizzata è, cioè, nel primo caso una pila, o stack, nel secondo una coda, o queue. Tali strutture di dati sono dette anche, rispettivamente, strutture di tipo LIFO (Last In First Out) e di tipo FIFO (First In First Out), e trovano un impiego assai diffuso nella rappresentazione dei dati in informatica. Vale la pena di notare come a volte la strutturazione dei dati rivesta un ruolo preponderante rispetto al comportamento di un algoritmo; troppo spesso si commette l’errore di attribuire molta enfasi allo sviluppo dei programmi, trascurando l’aspetto della strutturazione dei dati, o, più in generale, della rappresentazione della conoscenza, che viceversa gioca sistematicamente un ruolo preminente. Abbiamo accennato al fatto che sia la ricerca in profondità che quella in ampiezza presentano comparativamente pregi e difetti. A favore della ricerca in ampiezza va ascritta la robustezza: essa, per come è concepita, non può andare in loop, né imboccare un sottoalbero infinito. Pertanto non è sensato introdurre routine di detezione di cicli o altri accorgimenti. Inoltre, nel caso di un problema che ammetta soluzioni multiple, essa troverà per prima la soluzione a minore distanza dalla radice, cioè, a parità di costo degli archi dell’albero, di costo minimo. D’altra parte, la ricerca in ampiezza presenta il difetto che la rappresentazione dei dati in memoria ha crescita esponenziale: il passaggio da un livello a quello successivo fa aumentare il numero di nodi da rappresentare secondo il fattore di ramificazione. Viceversa, l’occupazione in memoria nella ricerca in profondità cresce in modo lineare. Questo per quanto riguarda il fattore spazio. Per quanto riguarda il fattore tempo, entrambe le procedure sono a crescita esponenziale. Si consideri il caso peggiore, ossia che non vi sia una soluzione, o, equivalentemente, che essa sia rappresentata dall’ultimo nodo a destra: entrambe percorreranno tutto l’albero. La ricerca euristica Finora ci siamo occupati di tecniche di ricerca “cieche”, o altri dicono “non informate”, o ancora basate sulla “forza bruta”. Da un certo punto di vista si può dire che un approccio di questo genere rappresenta quanto di meno “intelligente” si può fare nella soluzione dei problemi. La ricerca cieca considera, infatti, tutti i nodi ugualmente promettenti. È tipico dell’intelligenza umana, viceversa, individuare percorsi più promettenti di altri. Ad esempio, il giocatore di scacchi non considererà tutte le mosse possibili da sinistra a destra, ma si concentrerà sul centro dell’azione, su un pezzo attaccato, su una minaccia Questo secondo approccio prende il nome di “ricerca euristica” (dal verbo greco eurisko, “trovo”, reso celebre dal famoso “eureka”, “ho trovato”, di Archimede). Da un punto di vista matematico, un’euristica è una funzione che ha come dominio l’insieme dei nodi, e ad ognuno di essi attribuisce un valore, detto appunto “valore euristico”. Normalmente, la funzione euristica stima il costo del nodo, e dunque un valore euristico sarà tanto migliore quanto più è basso. Introdurre una funzione di valutazione euristica ci consente di abbassare la complessità di un problema, riducendo considerevolmente lo spazio di ricerca. Soluzione dei problemi e pianificazione 119 Ciò risulta di fondamentale importanza nei problemi a crescita esponenziale. Se un problema è a crescita esponenziale, infatti, dato che il lavoro che può essere svolto da un elaboratore cresce in modo uniforme nel tempo, vi sarà inevitabilmente un punto nel tempo oltre il quale il problema diventa insolubile. Mettendo la situazione in assi cartesiani, avremo Figura 10 È chiaro che, oltre il punto P, il lavoro da svolgere per risolvere il problema cresce “di più” delle capacità del processore. Si noti che il discorso è del tutto teorico, e non dipende pertanto dall’hardware coinvolto: cambiare processore assumendone uno più potente non fa altro che spostare in avanti nel tempo il punto P, ma non ne esorcizza l’inevitabilità. L’assunzione di una euristica diventa quindi non tanto un’opportunità, quanto una vera e propria necessità logica. L’assunzione di una euristica è un atteggiamento naturale dell’uomo. Si potrebbero fare centinaia di esempi tratti dalla vita di ogni giorno. Di fronte a una scelta complessa, o in vista di un obiettivo non direttamente raggiungibile, è del tutto normale, per parte nostra, dotarsi di una euristica. Così, quando dobbiamo scegliere un prodotto, in assenza di ulteriori informazioni, sceglieremo per esempio la marca più nota, o l’articolo più caro, o viceversa il più economico. Difficilmente invece sceglieremo in modo deterministico quello che sta più a sinistra. Ecco dunque che l’atteggiamento euristico è preso talvolta come sinonimo di “atteggiamento intelligente”, in contrapposizione con la scelta casuale o con quella predeterminata da un criterio rigido. Ma proprio la varietà cui si fa riferimento vale a capire che la bontà di un’euristica non si dà come valore assoluto, ma è strettamente dipendente da molte circostanze, prima di tutto dal problema. Non esiste dunque un’euristica buona tout court, ma piuttosto una determinata euristica si dimostra più o meno adeguata ad un certo problema. Gli algoritmi di ricerca euristica ricalcano quelli di ricerca cieca, con la sostanziale differenza che il passo successivo da compiere non viene scelto secondo un criterio predefinito, ma privilegiando il nodo che ha valore euristico migliore. Così abbiamo l’algo- 120 Capitolo 3 ritmo così detto “hill climbing”, che riproduce in forma euristica la ricerca in profondità, e l’algoritmo “best first”, che si rifà a quella in ampiezza. Hill climbing La denominazione dell’algoritmo si ispira ad una metafora alpinistica. Si tratta di imitare il comportamento di uno scalatore che, cercando di raggiungere la vetta di una montagna, di fronte ad ogni bivio sceglie sempre il cammino che sale di più. L’algoritmo analizza dunque i figli del nodo attuale, e sceglie, per proseguire, il figlio di valore euristico migliore. Un algoritmo di questo genere è adeguato quando il problema è tale che la soluzione possa essere raggiunta in modo monotòno, cioè passando costantemente a nodi il cui valore euristico è migliore dei precedenti. Si dimostra invece debole di fronte a soluzioni sub-ottimali, o, in altre parole, ai così detti “massimi locali”. Riprendendo la metafora alpinistica, il problema è quello delle vette secondarie: se si raggiunge una vetta che rappresenta una soluzione sub-ottimale, l’algoritmo non trova figli di valore euristico migliore della situazione attuale, e quindi termina. Pertanto, tutte le volte che il percorso per raggiungere la soluzione migliore presuppone un andamento non uniforme, ovvero che, nella solita metafora alpinistica, non è sempre in ascesa, l’algoritmo fallisce. Si possono introdurre tecniche per affrontare quest’ultimo problema. Ad esempio, far ripartire il computo in modo casuale e uscire così dalla vetta secondaria. L’hill climbing è considerato un algoritmo “tattico”, o “locale”. Il suo comportamento è molto simile a quello della ricerca cieca in profondità, perché si procede sempre dal padre ai figli, ma con l’essenziale differenza che nella ricerca cieca il figlio viene individuato secondo un criterio prestabilito, mentre qui viene scelto in base alla funzione euristica. Best first L’algoritmo best first applica l’euristica in un modo diverso. Esso si chiede quale sia il nodo con il valore euristico migliore, indipendentemente dal fatto che sia figlio del nodo attuale. Il suo comportamento è dunque simile a una ricerca per ampiezza. Algoritmo A L’algoritmo A è una variante del best first. Anziché calcolare la stima di un nodo esclusivamente con la funzione euristica, l’algoritmo A valuta il costo di un cammino dalla radice a una foglia come la somma di due componenti, la parte del cammino già coperta, per la quale assume il costo effettivamente speso, e la parte residua, che viene stimata dall’euristica. In altri termini, supponiamo di chiederci il costo del cammino che va dalla radice A alla foglia Z, e di essere nel nodo N. Allora la stima sarà data dalla somma del costo da A a N, che è noto, e del costo da N a Z, che va valutato con l’euristica. Adesso chiediamoci che cosa succede al crescere di N, cioè passando da N a N+1. Avverrà che la parte certa della stima aumenta, mentre quella valutata dall’euristica diminuisce. Un importante risultato legato all’algoritmo A è che si può dimostrare che, se l’euristica adottata è uniformemente ottimistica, esso trova la soluzione migliore, caratteristica Soluzione dei problemi e pianificazione 121 che in gergo tecnico viene detta “ammissibilità”. Un’euristica si dice uniformemente ottimistica quando, detto R(x) il costo reale di un’operazione che porta al nodo x, F(x) è sempre minore o uguale ad R(x). In altre parole, l’euristica non stima mai il costo maggiore del costo reale. In questo modo si capisce intuitivamente che non viene potato alcun cammino che potrebbe essere migliore di quelli analizzati. Date due euristiche uniformemente ottimistiche, si dice che l’euristica F1 è più informata dell’euristica F2 quando F1 è costantemente maggiore di F2. Così un’euristica più informata definirà uno spazio di ricerca che è un sottoalbero di quello generato dall’euristica meno informata. In altre parole, più un’euristica è informata e più riduce lo spazio di ricerca. All’opposto, l’euristica più ottimistica è quella che stima zero il costo di ogni nodo; essa è pertanto anche la meno informata, e di fatto si ritorna alla ricerca cieca: lo spazio di ricerca non viene diminuito affatto. Problem solving con i grafi AND/OR Una delle strategie più importanti nell’ambito delle tecniche di risoluzione dei problemi è quella di scomporre un problema in sottoproblemi più semplici. Si può dire che questo approccio è connaturato con il pensiero umano. Tanto per fare l’esempio di un precedente illustre, la seconda regola che Cartesio propone nel suo metodo è proprio l’analisi, cioè la scomposizione di un problema complesso in sottoproblemi più semplici. Per venire alla nostra disciplina, la strategia che stiamo considerando è già ben presente nell’ormai storico GPS di Simon e Newell. Ma esiste un modo canonico per compiere una tale suddivisione? Non esiste certo una procedura meccanica, e il modo di procedere dipende ampiamente dallo specifico problema. Lo schema di ragionamento potrebbe essere sintetizzato così: ho un problema P; individuo i sottoproblemi SP1, SP2, SP3. Allora P è risolto quando sia stata trovata una soluzione per SP1, SP2, SP3. Tuttavia proprio questo modo di procedere suggerisce una considerazione naturale: non è detto che tutti i sottoproblemi siano legati da una congiunzione logica. Si può dare viceversa il caso che essi siano in alternativa. Supponiamo che il nostro problema sia di recarci a Roma. Allora si può ragionare così: si può andare a Roma in treno oppure in aereo. È chiaro che i due sottoproblemi che sono emersi da questa prima scomposizione non vanno soddisfatti entrambi, ma è sufficiente che ne sia soddisfatto uno solo. Per andare a Roma, ad esempio, in treno, si dovrà poi: avere soldi, comprare il biglietto, raggiungere la stazione, salire sul treno giusto. Questi altri sottoproblemi vanno soddisfatti tutti, pena il fallimento. Ecco dunque che dobbiamo affidarci ad un altro modo di rappresentare la situazione, diverso da quello di un grafo albero che esprima lo spazio degli stati. Qui abbiamo a che fare con i grafi AND/OR. Questo significa che i figli di un nodo possono essere congiunti, e allora vanno soddisfatti tutti, oppure disgiunti, e allora basta che uno solo sia soddisfatto. 122 Capitolo 3 ANDARE A ROMA IN TRENO IN MACCHINA IN AEREO COMPERARE BIGLETTO IN TAXI RAGGIUNGERE AEROPORTO IN MACCHINA FARE BENZINA FARE CHECK-IN IN BUS TROVARE PARCHEGGIO Figura 11 – Diversi modi per “andare a Roma”. Come si vede in figura, tracciamo una linea orizzontale a congiungere i figli dei nodi AND, mentre non mettiamo niente nel caso dei nodi OR. Per determinare una soluzione in un grafo di questo tipo, dovremo far sì che per ogni nodo OR sia soddisfatto almeno un figlio, e per ogni nodo AND siano soddisfatti tutti. La soluzione non è più, dunque, un cammino dalla radice a una foglia voluta, come nei casi visti in precedenza, ma è costituita da un sottoalbero, con le caratteristiche appena dette. Vediamo una soluzione del problema “andare a Roma” ANDARE A ROMA IN TRENO IN MACCHINA IN AEREO COMPERARE BIGLETTO IN TAXI RAGGIUNGERE AEROPORTO IN MACCHINA FARE BENZINA FARE CHECK-IN IN BUS TROVARE PARCHEGGIO Figura 12 – Grafo AND/OR del problema “andare a Roma”. Soluzione dei problemi e pianificazione 123 Come si calcola il costo di una soluzione in un grafo AND/OR? Non essendo un cammino, esso non può essere infatti assimilato alla somma dei costi degli archi. Facciamo l’ipotesi che la scomposizione del problema in sottoproblemi si arresti quando troviamo una “azione elementare”, definita come una azione direttamente eseguibile di cui è noto il costo. Allora avremo che, per ogni nodo del grafo, ➢ se è un’azione elementare il costo è dato per definizione ➢ se è una azione elementare non eseguibile, allora il costo è infinito ➢ se è un nodo in OR assume il valore minimo tra quelli dei suoi figli ➢ se è un nodo AND allora vale la somma del valore dei figli. In questo modo è possibile ribaltare all’indietro i costi del sottoalbero risolutivo, e computare il costo dei padri in dipendenza dei figli. Il planning Il planning è in un certo senso una evoluzione, o meglio una branca, del problem solving. Si tratta di assumere entro la panoramica della risoluzione dei problemi la dimensione della temporalità, o, quanto meno, quella dell’ordine sequenziale, confrontandosi con la tematica di collegare tra di loro in una sequenza logica più problemi da risolvere. Non manca chi vede nel planning la principale caratteristica dell’intelligenza umana. Pianificare significa, in prima approssimazione, concepire l’esecuzione di una sequenza di azioni che conducano ad un risultato voluto. Storicamente, i primi tentativi in questo campo furono fatti cercando di riadattare al nuovo contesto problemico la programmazione logica, e dunque, in definitiva, il calcolo dei predicati del primo ordine. Assumendo un tempo discreto, ossia come una successione di stati, si assdocia ad ogni predicato un ulteriore argomento, che specifica appunto a quale degli stati ci si riferisce. Così P(a,s1) significa che il predicato P vale di a allo stato s1. In questo modo, secondo il consueto approccio della programmazione logica, lo stato finale voluto diventa il goal da dimostrare, e la catena delle successive unificazioni che si devono compiere per raggiungerlo esprime il “piano” voluto. È questo il così detto “calcolo situazionale” proposto da McCarthy e Hayes. Ma seguendo questa prospettiva insorgono notevoli difficoltà. La più rilevante di esse è che, in programmazione logica, non si può fare a meno, per ragioni di computabilità, della così detta “ipotesi del mondo chiuso”, ossia, tutto ciò che non è esplicitamente assunto nel programma, o dedotto dalle premesse, è falso. Questo assunto consente la chiusura computazionale, ossia consente di rendere decidibile un calcolo, quello dei predicati appunto, che di per sé sarebbe “semi-decidibile”. La conseguenza di questo assunto è che, essendo falso tutto ciò che non è esplicitamente asserito, si crea la necessità di riaffermare ogni volta, ad ogni passaggio di stato, anche ogni premessa che non è stata affetta dalle conseguenze di una azione. Questa circostanza prende il nome di “frame problem”. Secondo il buon senso, ci pare ovvio che tutto ciò che non è stato cambiato rimanga tale e quale, e quindi che 124 Capitolo 3 valgano nello stato successivo i predicati che valevano in quello precedente; tuttavia, per le ragioni tecniche anzidette, questa semplice caratteristica non si può assumere entro il prim’ordine. Si sono così sviluppati, per affrontare le problematiche del planning, degli ambienti ad hoc, appositamente concepiti per la rappresentazione delle azioni nella loro temporalità. Il nuovo punto di vista si apre con il pianificatore STRIPS (Standford Research Institut Problem Solver). La novità essenziale è costituita da un nuovo modo di rappresentare le azioni. Anziché affidarsi al calcolo dei predicati, Fikes e Nilsson, gli autori di STRIPS, rappresentano un’azione come un insieme di precondizioni e un insieme di conseguenze. L’insieme delle precondizioni è una lista di enunciati che devono essere verificati perché l’azione possa avere luogo. L’insieme delle conseguenze è strutturato in due liste, quella delle aggiunte e quella delle cancellazioni. Questo approccio riesce pertanto a superare lo scoglio del frame problem, rappresentando soltanto i cambiamenti in modo esplicito. Un’azione in STRPS avrà dunque la struttura: Azione A 1. precondizioni: x, y, z 2. aggiunte: u, v 3. cancellazioni: k, w dove x, y, etc. sono enunciati del calcolo dei predicati. Questo approccio rappresenta una svolta nella pianificazione; STRIPS, malgrado i suoi limiti, di cui parleremo oltre, rimane per questo una pietra di paragone per i pianificatori successivi. L’algoritmo che governa l’andamento di STRIPS si basa sulla “analisi mezzi-fini”, tecnica in larga misura derivante dal GPS. Essa consiste nel confrontare ricorsivamente lo stato attuale con quello voluto, nel ricercare un operatore che possa diminuire la differenza tra i due stati, e nell’assumere poi come nuovo stato voluto quello in cui si verificano le precondizioni dell’operatore che si vuole applicare in quanto appunto diminuisce la differenza rispetto allo stato finale. Il limite essenziale di STRIPS risulta essere quello che è un pianificatore “tattico”, ossia che assume un obiettivo per volta, tentando di soddisfarli in sequenza. Il programma non prende quindi in alcuna considerazione il fatto che gli obiettivi possano interferire l’uno con l’altro, e debbano di conseguenza essere considerati in modo correlato. Più banalmente: operando su un obiettivo per volta, nulla vieta che, dopo il conseguimento di un obiettivo, l’attività svolta per raggiungerne un secondo di fatto infici il raggiungimento del primo. Infatti, se una delle azioni eseguite per raggiungere il secondo obiettivo ha nella lista delle cancellazioni un enunciato che compare nella lista delle aggiunte delle azioni che hanno realizzato il primo obiettivo, questo viene vanificato e distrutto. Avviene così che in situazioni anche non molto complesse STRIPS compia del lavoro inutile, o, ancora peggio, si immetta in un circolo vizioso, realizzando e poi distruggendo i suoi obiettivi. Soluzione dei problemi e pianificazione 125 I progressi successivi del planning hanno consentito di passare da pianificatori “tattici”, come STRIPS appunto, a pianificatori “strategici”, ossia in grado di trattare gli obiettivi congiuntamente. Le tecniche relative a questa evoluzione sono sostanzialmente tre: a) la protezione degli obiettivi; b) la regressione degli obiettivi; c) la modifica dei piani. La prima consiste nel “bloccare” le condizioni relative ad un obiettivo già raggiunto, in modo tale che esse diventino un vincolo per le azioni che vengono svolte successivamente. Così, se una condizione appartiene allo stato finale voluto, ed essa è stata già soddisfatta, non si potrà applicare un’azione che la contempli nella lista delle cancellazioni. La regressione degli obiettivi è in un certo senso una forma di backwordchaining; essa consiste nel calcolare a ritroso lo stato che precede lo stato finale. Se lo stato finale st dovrà soddisfare un certo obiettivo, e questo obiettivo è ottenuto con una certa azione A, allora è chiaro che nello stato immediatamente precedente, st-1, dovranno valere tutte le precondizioni di A. Infine, la terza tecnica, la modifica dei piani, consiste nel non dare per scontato che i vari sotto-piani debbano essere concatenati sequenzialmente. Supponiamo di avere ottenuto un primo obiettivo grazie al piano P1. Supponiamo ora di elaborare un secondo piano, P2, per raggiungere un secondo obiettivo. Il comportamento di STRIPS sarebbe quello di collocare P2 in coda al primo. Ma in P2 potrebbe comparire una qualche azione che danneggia il risultato di P1. Ecco allora che, riscontrata questa situazione, il sistema tenta di collocare P2 non in coda a P1, ma allo stato precedente alla sua ultima azione. E così via ricorsivamente fino a trovare il punto corretto in cui P2 può essere immerso in P1 senza creare danni. Si può dire che, ragionando in questo modo, emerge una regola del tutto generale: quando un’azione A danneggia un’azione B, ossia quando nella lista delle cancellazioni di A compare una condizione che appartiene alla lista delle aggiunte di B, allora si deve far sì che B sia eseguita in un momento che precede l’esecuzione di A. Un ulteriore sviluppo nella pianificazione discende dalla constatazione che il modo di pianificare dell’uomo è in grado di attribuire un’importanza diversa agli obiettivi, ossia di graduarne la criticità. In analogia con quanto avviene nel passaggio dalla ricerca cieca, in cui tutti i nodi sono equivalenti, e quella euristica, in cui i nodi vengono valutati secondo una funzione, si ha qui l’instaurarsi della pianificazione gerarchica. Questo significa, appunto, attribuire agli obiettivi un diverso indice di criticalità. L’algoritmo di planning cercherà dunque di soddisfare prima gli obiettivi di indice di criticalità maggiore. Si realizza così il classico modo di procedere top-down. Il vantaggio che si consegue secondo questo modo di procedere è che si evita di elaborare piani di dettaglio rispetto a un obiettivo di livello alto che non può essere raggiunto. Facciamo un esempio estremamente semplice. Supponiamo che io voglia produrre un piano per laurearmi in filosofia o in giurisprudenza. Se io procedessi sequenzialmente, secondo una programmazione non gerarchica, a questo punto dovrei andare a sviluppare i dettagli del primo obiettivo, ossia le azioni che lo rendono realizzabile. Avrei quindi “iscriversi a lettere e filosofia”, “frequentare storia della filosofia”, “frequentare…”; “leggere La Metafisica di Aristotele”, “leggere… “, “sostenere l’esame di storia della filosofia”, “sostenere l’esame di…”, etc. E così via: ogni sotto-obiettivo deve ulteriormente essere decomposto, fino alle azioni elementari. Così “leggere La Metafisica” si svolge in OR in “comprare il libro” oppure “prenderlo a prestito in biblioteca” oppure “chiederlo in prestito a un amico”. E così via ricorsivamente: “prenderlo a prestito” si svolge in “prenderlo a prestito nella biblioteca A”, “prenderlo a prestito nella biblioteca B”, Sup- 126 Capitolo 3 poniamo infine che nella università della mia città, dove ho deciso di laurearmi, non esista la facoltà di lettere, ma solo quella di giurisprudenza. Ecco che tutto il piano sviluppato nei dettagli si rivela come un lavoro del tutto inutile. La gestione di un opportuno indice di criticalità eviterebbe tutto ciò, andando a verificare prima di tutto la soddisfacibilità degli obiettivi di più alto livello: è chiaro che, se non mi posso iscrivere, è del tutto inutile sviluppare i livelli più bassi del piano. L’approccio gerarchico consente quindi la realizzazione di piani sempre più di dettaglio, con una razionalità maggiore rispetto ai pianificatori lineari. Un esempio di riferimento per la pianificazione gerarchica è ABSTRIPS, sviluppato da Sacerdoti, che rappresenta una evoluzione di STRIPS con l’aggiunta, appunto, della gestione degli indici di criticalità. Infine, prendiamo in considerazione un ulteriore miglioramento nel campo della pianificazione: la non linearità. L’esempio di riferimento è un pianificatore, anch’esso sviluppato da Sacerdoti come ABSTRIPS, che prende il nome di NOAH. L’idea di base può essere sintetizzata così. Il programma non deve produrre direttamente il piano definitivo, ma deve invece generare una successione di piani, ognuno dei quali è più raffinato del precedente. Un’altra idea fondativa di NOAH è il così detto principio del least commitment: il programma opera una scelta tra un’alternativa solo quando vi è costretto, il più tardi possibile. Il loop principale dell’algoritmo è dunque intuitivamente sintetizzabile come segue: 0 - elabora un piano 1 - critica il piano generato e miglioralo 2 - SE il risultato raggiunto è adeguato ALLORA fermati ALTRIMENTI torna al passo 1. La tecnica che realizza il meccanismo del least commitment è quella di condurre in parallelo lo sviluppo di due azioni, fino a che non si è costretti a decidere quale delle due si deve fare prima dell’altra. Ciò si ottiene introducendo i nodi SPLIT e JOINT. Supponendo che debbano essere eseguite le azioni A e B. Anziché assumerle in sequenza, NOAH costruisce una struttura del tipo: A J J B Figura 13 Soluzione dei problemi e pianificazione 127 Nell’ambito dello sviluppo di un piano complesso, i miglioramenti successivi si otterranno, essenzialmente, dunque, con lo spostamento dei nodi SPLIT e JOINT. Il criterio di base per questo tipo di problemi è molto semplice, ed è sempre il solito in tutta la pianificazione: se un’azione A ha nella sua lista di cancellazioni un elemento che appartiene alla lista delle precondizioni di una seconda azione B, si dice che “A danneggia B” in quanto la rende irrealizzabile. Da ciò segue l’ovvia conclusione che l’azione danneggiata deve comunque essere eseguita prima dell’azione danneggiante. Nella presentazione informale del loop principale sopra abbiamo introdotto l’espressione: “critica il piano”. La nozione appare in prima battuta assai vaga. In realtà, essa rimanda invece ad una precisa strategia algoritmica, e cioè l’invocazione da parte del programma di una serie di sub-routine che vengono appunto dette “critici”. I critici sono dunque sottoprogrammi il cui scopo è quello di valutare la bontà di un piano, rilevarne le incongruenze, e modificarlo di conseguenza, migliorandolo. Cominciamo ad esemplificare dal caso più semplice, il “critico delle ridondanze”. Supponiamo che la precondizione P sia presente tanto nella lista dell’azione A quanto nella lista dell’azione B, che sono entrambe da compiere. Allora è evidente che essa è ridondante, ovvero è inutile che nel piano definitivo compaia due volte, posto che deve essere soddisfatta prima dell’esecuzione della prima delle due azioni considerate. Il più importante dei critici è senz’altro quello che rileva i conflitti, ossia individua quali siano le azioni danneggianti e quali quelle danneggiate. Questo critico, una volta riscontrata tale anomalia, sposterà i nodi SPLIT-JOINT, in modo che l’azione danneggiante debba necessariamente essere eseguita dopo l’azione danneggiata. È importante notare che, se non vi sono conflitti, viene mantenuta la struttura in parallelo dello sviluppo del piano. Un pianificatore non lineare come NOAH può dunque generare un piano finale che lascia alcuni segmenti dello stesso in parallelo, il che va interpretato come il fatto che è indifferente eseguire, ad un certo punto, prima A e poi B o prima B e poi A. Bibliografia Per approfondire queste tematiche e per una bibliografia cartacea, ben documentata ed annotata, rimando al seguente volume: Fum D. (1994), Intelligenza artificiale, Bologna, Il Mulino. Ulteriori e più recenti testi di riferimento sono: Nilsson N. J. (1998), Artificial intelligence: a new synthesis, San Francisco, Morgan Kaufmann, (trad. it., Intelligenza artificiale, Milano, Apogeo, 2002). Poole D., Mackworth A., Goebel R. (1998), Computational intelligence: a logical approach, Oxford, Oxford University Press. Russell S., Norvig P. (2003), Artificial intelligence: a modern approach, 2nd ed., Upper Saddle River, NJ, Prentice Hall/Pearson Education (trad. it., Intelligenza artificiale: un approccio moderno, 2a ed., Milano, Pearson Education Italia, 2005). 128 Capitolo 3 È indubbio però che, data la modernità e la dinamicità degli argomenti qui affrontati, la cosa migliore è ricercare sulle numerosissime informazioni on line. Riporto di seguito l’indirizzo di alcuni siti tra i più interessanti e completi, a mio modo di vedere, di manuali on line o bibliografie; molti di essi, a loro volta, rimandano ad altri siti, attraverso un’infinità di link. http://www.ing.unife.it/informatica/AppliIA/testi.shtml http://www.di.unipi.it/~simi/AI/SI2005/lucidi.html http://aima.cs.berkeley.edu/ http://citeseer.ist.psu.edu/articles.html http://liinwww.ira.uka.de/bibliography/Ai/ http://liinwww.ira.uka.de/bibliography/Ai/others.html http://consc.net/biblio/4.html http://users.ox.ac.uk/~econec/cogsciai.html http://bubl.ac.uk/link/a/artificialintelligence.htm