Dipartimento di Ingegneria dell’Informazione Università degli Studi di Parma Intelligenza Artificiale Risoluzione dei Problemi (parte 4) Agostino Poggi Stefano Cagnoni Algoritmi di Miglioramento Iterativo Ci sono dei problemi (es. 8 regine) per cui la descrizione dello stato contiene tutte le informazioni necessarie per la soluzione indipendentemente dal percorso che ha condotto a quello stato. In questi casi l’idea generale per la soluzione può essere quella di partire da uno stato iniziale “completo” (es. 8 regine già sulla scacchiera) e poi modificarlo per raggiungere una soluzione (o una soluzione migliore, se esiste). Questi metodi fanno parte dei cosiddetti algoritmi di ottimizzazione il cui scopo è massimizzare (minimizzare) il valore di una funzione di valutazione che misura la bontà (costo) di una soluzione. Questo metodo può essere rappresentato come il movimento su una superficie che rappresenta lo spazio degli stati alla ricerca della vetta più elevata. Risoluzione dei Problemi 2 Ricerca in Salita (Hill Climbing) L’algoritmo di ricerca in salita è un algoritmo ciclico che, ad ogni passo, raggiunge una posizione migliore all’interno di un’area di ricerca, se esiste, seguendo la direzione di massima pendenza. Possiamo definire l’algoritmo di ricerca in salita come segue: function Hill-Climbing(problem) { current = Make-Node(Initial-State(problem)); while true { next = ‘a higher valued successor’; if Value(next) < Value(current) return current ; current = next; } } Risoluzione dei Problemi 3 Ricerca in Salita Questo algoritmo deve affrontare tre tipi di situazioni che lo possono “bloccare”. Nella rappresentazione dello spazio di ricerca come una superficie, corrispondono al raggiungimento di: massimi locali pianori crinali Il metodo per superare questi problemi è fare ripartire la ricerca da un altro punto scelto a caso Questo è ciò che fa una variante dell’algoritmo di ricerca in salita (Random-restart-hill-climbing). Risoluzione dei Problemi 4 Ricerca in Salita Questa variante ad ogni iterazione mantiene in memoria il risultato migliore e, periodicamente o al verificarsi di condizioni di “blocco”, riparte con una nuova iterazione da uno stato scelto in modo random. Dopo un certo numero fisso di iterazioni o dopo che per un certo numero di iterazioni questo risultato non viene migliorato, l’algoritmo termina ritornando il risultato migliore. In genere una buona soluzione può essere trovata dopo poche iterazioni. Se il problema è NP-completo, la soluzione non può che essere fornita in un tempo esponenziale poiché, per tale classe di problemi, lo spazio degli stati ha un numero esponenziale di massimi locali. Risoluzione dei Problemi 5 Simulated Annealing L’algoritmo di simulated annealing ha un comportamento molto simile all’algoritmo di ricerca in salita. La differenza consiste nell’eseguire, ad ogni passo, una mossa casuale. Se la mossa migliora la situazione, allora viene sempre eseguita, altrimenti viene eseguita con probabilità eE/T dove: E è il peggioramento (<0) causato dall’esecuzione della mossa. T è un valore che tende a zero con l’aumentare dei cicli e quindi rende sempre meno probabile l’esecuzione di mosse negative. Risoluzione dei Problemi 6 Simulated Annealing L’algoritmo è così chiamato perché simula l’annealing, cioè il processo graduale di raffreddamento di una sostanza che passa dallo stato liquido allo stato solido. Se la temperatura viene diminuita abbastanza lentamente, allora il liquido raggiunge una configurazione a minima energia. Risoluzione dei Problemi 7 Simulated Annealing Possiamo definire l’algoritmo di simulated annealing come segue: function Simulated-Annealing(problem) { current = Make-Node(Initial-State(problem)); for t = 1 to { T = schedule[t]; schedule = mappatura tempo/temperatura if (T = = 0) return current; next = ‘a randomly selected successor’; E = Value(next) - Value(current); if (E > 0) current = next; else ‘current = next with probability eE/T’ } } Risoluzione dei Problemi 8 I Giochi I giochi sono stati uno dei primi campi in cui è stata applicata l’IA. La presenza di un avversario rende più complicato il problema della ricerca dato che l’avversario introduce incertezza. La caratteristica che spesso li distingue è il grandissimo spazio di ricerca. Risoluzione dei Problemi 9 I Giochi Un gioco per due persone può essere definito come un problema di ricerca con le seguenti componenti: Lo stato iniziale. Gli operatori, le mosse legali. Un test di terminazione. Una funzione di utilità (utility or payoff function) indicante il risultato della partita. Risoluzione dei Problemi 10 Il Gioco del Filetto Risoluzione dei Problemi 1 2 3 4 5 6 7 8 9 11 Soluzione 1 Struttura dati int Quadro[9] dove: – Quadro[i] = 0 se vuoto – Quadro[i] = 1 se riempito con X – Quadro[i] = 2 se riempito con O Quadro Mosse[19683] Algoritmo Si considera il vettore Quadro come se fosse un numero in base 3 e si converte in decimale. Si usa questo numero per accedere ad un elemento del vettore Mosse. I valore dell’elemento è il nuovo valore di Quadro. Risoluzione dei Problemi 12 Soluzione 2 Strutture dati int Quadro[9] dove: – Quadro[i] = 2 se vuoto – Quadro[i] = 3 se riempito con X – Quadro[i] = 5 se riempito con O int Turno Funzioni Vai(n) Esegue Quadro[n] = 3 se Turno è dispari Esegue Quadro[n] = 5 se Turno è pari Fai2() Ritorna il primo quadro vuoto tra i quadri: 5,2,4,6,8 Vincente(p) Ritorna 0 se il giocatore p non può vincere alla prossima mossa Altrimenti ritorna il quadro vincente Risoluzione dei Problemi 13 Soluzione 2 Algoritmo se Turno = 1 allora Vai(1) se Turno = 2 e Quadro[5] = 2 allora Vai(5) altrimenti Vai(1) se Turno = 3 e Quadro[9] = 2 allora Vai(9) altrimenti Vai(3) se Turno = 4 e Vincente(x) > 0 allora Vai(Vincente(x)) altrimenti Vai(Fai2()) se Turno = 5 e Vincente(x) > 0 allora Vai(Vincente(x)) altrimenti se Vincente(o) > 0 allora Vai(Vincente(o)) altrimenti se quadro[7] = 2 allora Vai(7) altrimenti Vai(3) se Turno = 6 e Vincente(o) > 0 allora Vai(Vincente(o)) altrimenti se Vincente(x) > 0 allora Vai(Vincente(x)) altrimenti Vai(Fai2()) se Turno = 7 e Vincente(x) > 0 allora Vai(Vincente(x)) altrimenti se Vincente(o) > 0 allora Vai(Vincente(o)) altrimenti Vai(i) tale che Quadro[i] = 2 se Turno = 8 e Vincente(o) > 0 allora Vai(Vincente(o)) altrimenti se Vincente(x) > 0 allora Vai(Vincente(x)) altrimenti Vai(i) tale che Quadro[i] = 2 se Turno = 9 e Vincente(x) > 0 allora Vai(Vincente(x)) altrimenti se Vincente(o) > 0 allora Vai(Vincente(o)) altrimenti Vai(i) tale che Quadro[i] = 2 Risoluzione dei Problemi 14 Soluzione 3 Strutture Dati Struct PosizioneQuadro { int Quadro[9] PosizioneQuadro ProssimaPosizione[] int Valore } Algoritmo Si generano tutte le configurazioni raggiungibili in una mossa e si sceglie la migliore: Se una configurazione è vincente allora è la migliore. Altrimenti si considerano tutte le mosse che l’avversario può compiere da quella configurazione. Si determina la peggiore posizione e si assegna il suo valore alla configurazione che stiamo considerando. La configurazione migliore ha il valore più alto. Risoluzione dei Problemi 15 Soluzione 1 Pregi Efficiente in termini di tempo. Capace di giocare in modo ottimale. Difetti Poco efficiente in termini di spazio. Occorre molto tempo per definire i valori del vettore Mosse. Sono probabili errori nell’inserimento dei valori del vettore Mosse. La soluzione è difficilmente estensibile ad altri giochi. Non soddisfa nessun requisito di una buona tecnica di IA. Risoluzione dei Problemi 16 Soluzione 2 Pregi Efficiente in termini di spazio. Capace di giocare in modo ottimale. La strategia di gioco è più comprensibile. Difetti Meno efficiente in termini di tempo. La soluzione è difficilmente estensibile ad altri giochi. Non soddisfa nessun requisito di una buona tecnica di IA. Risoluzione dei Problemi 17 Soluzione 3 Pregi Capace di essere efficiente in termini di spazio. Capace di giocare in modo ottimale. La soluzione è estensibile ad altri giochi. Difetti Poco efficiente in termini di tempo. È un buon esempio di tecnica di IA. Risoluzione dei Problemi 18 Gli scacchi Ad esempio gli scacchi hanno un fattore di ramificazione circa uguale a 35 e le partite spesso arrivano a 50 mosse per giocatore. Quindi per il gioco degli scacchi abbiamo uno spazio di ricerca di 35100 (in realtà le posizioni legali sono solo 1040). Occorre quindi una funzione di valutazione. Ad esempio, si potrà dare un peso a ciascun pezzo, ma questo non è sufficiente. Risoluzione dei Problemi 19 Funzione di Valutazione Una funzione di valutazione in genere si basa sull’importanza dei pezzi, ma l’importanza di un pezzo non è costante, ma varia in base alla posizione e alla fase della partita. Ad esempio per un cavallo: Le posizioni centrali pesano di più di quelle agli angoli. Le posizioni a distanza due da un pezzo nemico pesano di più. La posizione favorevoli rispetto ai pedoni nemici pesano di più. La distanza dal re pesa in modo negativo. Il peso del cavallo diminuisce con l’avanzamento della partita. Risoluzione dei Problemi 20 Deep Blue Il 10/5/1997, a New York, una macchina, Deep Blue, ha battuto in un match di sei partite (2-1 e tre patte) il campione del mondo Kasparov. In particolare, Deep Blue utilizza una macchina parallela general-purpose a 32 processori più 512 chip specializzati per la generazione di mosse e valutazione. Deep Blue arriva a esplorare alberi profondi 12/14 semimosse (~ 1011 posizioni) in circa 3 minuti. Deep Blue consulta anche librerie di aperture (ci sono un centinaio di aperture ormai completamente esplorate e che possono condizionare tutta la partita). Risoluzione dei Problemi 21 L’Algoritmo Minimax L’algoritmo Minimax calcola la mossa ottima per il primo giocatore (Max) supponendo che l’avversario (Min) giochi una partita perfetta. L’algoritmo è composto da 4 passi: Genera l’albero completo del gioco. Applica la funzione di utilità a tutti gli stati terminali (foglie). Partendo dalle foglie assegna ricorsivamente agli stati intermedi: Il massimo della funzione di utilità degli stati figli se i figli corrispondono a una mossa di Max. Il minimo della funzione di utilità degli stati figli se i figli corrispondono a una mossa di Min. L’algoritmo termina con l’assegnazione a Max della prima mossa, che corrisponde al massimo della funzione di utilità al primo livello. Risoluzione dei Problemi 22 L’Algoritmo Minimax 1 -1 1 1 -1 1 1 Risoluzione dei Problemi Max -1 Min 1 1 1 Max 1 -1 Min 23 L’Algoritmo Minimax Possiamo definire l’algoritmo Minimax come segue: function Minimax(game) { for each op in operator Value[op] = Minimax-value(apply(op, game), game); return ‘the op with highest Value’; } function Minimax-Value(state, game) { if Terminal-Test[game](state) return Utility[game](state); else if ‘Max is to move’ return ‘the highest Minimax-Value of state’s successors’ else return ‘the lowest Minimax-Value of state’s successors’ } Risoluzione dei Problemi 24 L’Algoritmo Minimax L’algoritmo Minimax si basa sull’ipotesi che sia possibile generare tutti gli stati terminali. Questo è di solito impraticabile, anche per giochi non molto complicati. E’ necessario un criterio che faccia generare una mossa all’algoritmo Minimax senza dover esplorare tutto l’albero di ricerca. Questo si può ottenere introducendo una funzione euristica di valutazione, che valuterà la bontà anche di stati non terminali, e un test di terminazione (Cutoff-Test) per decidere a che livello di mosse terminare la ricerca. Risoluzione dei Problemi 25 L’Algoritmo Minimax La funzione di valutazione dà una stima della bontà della mossa. Ad esempio negli scacchi la stima può essere data in base al valore dei pezzi in possesso dei due giocatori. Il test di terminazione cerca di interrompere la ricerca ad un livello d tale che sia rispettato il limite di tempo fissato per il gioco. Tuttavia un test di terminazione così semplice può fornire pessimi risultati se associato a una funzione di valutazione non molto sofisticata. Risoluzione dei Problemi 26 L’algoritmo Minimax Per superare questi problemi bisogna arrestare la ricerca in configurazioni quiescenti, cioè in cui le prime mosse successive (quelle entro l’orizzonte di valutazione) non cambiano sensibilmente il valore della funzione di valutazione. Un problema difficilmente superabile è il problema dell’orizzonte, cioè la previsione di mosse avversarie di grande rilevanza che per il limite spaziale dello spazio di ricerca non sono raggiungibili e quindi sono invisibili all’algoritmo. Risoluzione dei Problemi 27 Alpha-Beta Pruning L’associazione di una buona funzione di valutazione e un buon test per decidere dove interrompere l’espansione dell’albero di ricerca può non essere sufficiente a realizzare un programma Minimax che possa giocare entro limiti temporali accettabili. È tuttavia possibile decidere una mossa corretta senza dover controllare tutti i nodi che l’algoritmo Minimax controlla. Questo viene fatto usando la tecnica della potatura alpha-beta: se un giocatore può muoversi in un nodo n, ma ha una migliore scelta m in un nodo di livello più elevato nell’albero, allora il nodo n non sarà mai raggiunto. Risoluzione dei Problemi 28 Alpha-Beta Pruning Max 0,8 0,8 0,9 0,8 0,1 0,8 0,7 0,5 0,6 Risoluzione dei Problemi Min 0,7 0,8 0,1 0,1 0,7 0,8 0,8 Max 0,7 Min Max 29 Alpha-Beta Pruning Possiamo definire l’algoritmo di ricerca alpha-beta con le funzioni MaxValue e Min-Value. Max-Value ha la forma: function Max-Value(state, game, alpha, beta) { if Cutoff-test(state) return Eval(state); for each s in Successor(state) { alpha = Max(alpha, Min-Value(s, game, alpha, beta)); if (alpha >= beta) return beta; } return alpha; } Se si trova un alpha maggiore o uguale a beta si può uscire perché sicuramente Min potrà eseguire almeno una mossa di valore beta. Risoluzione dei Problemi 30 Alpha-Beta Pruning Min-Value ha la forma: function Min-Value(state, game, alpha, beta) { if Cutoff-Test(state) return Eval(state); for each s in Successor(state) { beta = Min(beta, Max-Value(s, game, alpha, beta)); if (beta <= alpha) return alpha; } return beta; } Se si trova un beta minore di alpha si può uscire perché sicuramente Max potrà eseguire almeno una mossa di valore alpha. Risoluzione dei Problemi 31 Alpha-Beta Pruning L’efficienza della potatura alpha-beta dipende fortemente dall’ordine in cui i nodi sono esaminati. Se i nodi fossero bene ordinati il numero di nodi da esaminare sarebbe O(bd/2) invece di O(bd). Questa riduzione di nodi corrisponde a passare da una complessità d alla sua radice quadrata. Questo, in giochi simili agli scacchi, permette ad esempio di esplorare sequenze di mosse lunghe il doppio. Risoluzione dei Problemi 32