Natural Computing

 

 

 

 

 

 

 

 
Algoritmi Genetici

Gli Algoritmi Genetici sono dei sistemi ad evoluzione dinamica.

Questi sistemi consentono di affrontare, con notevole efficacia, problemi di ottimizzazione e di ricerca molto complessi, in tempo reale e ad un relativo basso costo computazionale.

Si definiscono problemi di ottimizzazione tutti quei problemi nei quali, a partire da una certa quantità, dipendente dai valori di un insieme di variabili, occorre individuare i valori che quelle variabili devono assumere per rendere tale quantità massima o minima.

E' possibile individuare una vasta gamma di problematiche, appartenenti a campi d'indagine anche molto diversi tra loro, più o meno interessate da problemi di ottimizzazione. La definizione e l'ottimizzazione dei palinsesti nell'ambito della programmazione delle trasmissioni televisive, ad esempio, rappresenta un tipico problema di ottimizzazione nel campo della organizzazione dei media.

Per molti dei problemi di ottimizzazione esistono diversi algoritmi di risoluzione ben noti. Tuttavia, la maggior parte di essi, sebbene teoricamente corretti, prevedono dei tempi di calcolo assolutamente non praticabili anche solo per problemi di non elevata complessità.

Imitando la natura, sfruttando il rapporto dinamico tra evoluzione e selezione, gli Algoritmi Genetici consentono di esplorare, se non l’intero spazio, sicuramente una regione molto ampia delle possibili soluzioni di qualsiasi problema complesso, sia esso di tipo culturale o materiale.

Ci sono molti modi per implementare un algoritmo genetico. Quella che espongo di seguito è stata proposta da Holland ed è una delle metodologie più note e studiate. Possiamo dunque considerare Holland, in qualche modo, il padre dei primi algoritmi genetici.

Dato un qualsiasi problema di ottimizzazione, possiamo pensare all’insieme delle possibili soluzioni di quel problema come ad una “popolazione” di “individui soluzioni”. In questo insieme dunque ogni “individuo” sarà definito come l’insieme delle caratteristiche o dei valori di variabili capaci di fornire una soluzione al problema dato.

Ad esempio, se devo fare dei giri in città per acquistare diverse merci (pane, latte, scarpe, bulloni, antivirus per il PC, pezzi di ricambio per la mountain bike ecc…) il mio problema, se non posso accedere ad un unico magazzino con tutto ciò che mi occorre, è come definire un tracciato, una sequenza di visite ai negozi sparsi per la città, tale da:

1. percorrere il minor numero di chilometri possibile
2. spendere la minor quantità di denaro
3. impiegare il minor tempo possibile

Se non abitiamo in un paesino di frontiera con 100 abitanti ma, diciamo, in una città come Milano o Roma ciascuno può capire che di soluzioni possibili e, in qualche modo, accettabili ce ne sono diverse … molte magari non riusciamo neanche ad immaginarle, anche avendo un insieme ben definito di negozi e punti vendita della città.

Questo è un problema ben noto in letteratura definito “TSP” (Travelling Sales Person Problem), il problema del commesso viaggiatore, che appartiene alla classe dei problemi NP completi.

Dato un insieme di punti P(x, y), trovare il cammino minimo che connette tutti i punti in un grafo hamiltoniano (in modo tale che ogni punto sia connesso a due soli altri punti e il grafo sia chiuso). Il numero di percorsi possibili cresce esponenzialmente al crescere del numero dei punti, secondo l’equazione:

Numero Percorsi = (Numero Punti – 1)/2

Il TSP è uno dei classici problemi affrontabili con gli algoritmi genetici. Non che non si possa, almeno in linea di principio, affrontare con altre metodologie “classiche” di ricerca algoritmica, il fatto è che queste “altre soluzioni” classiche spesso o sono inefficienti o richiedono dei tempi di ricerca e di calcolo incredibilmente onerosi.

Per implementare il nostro algoritmo genetico dunque occorre innanzitutto definire una "popolazione" iniziale costituita da un numero casuale di possibili soluzioni, codificate in un “genotipo” di caratteri binari (0-1). Ogni individuo della popolazione di partenza rappresenta così una possibile soluzione al problema dato (nell’esempio una certa sequenza di negozi da visitare: fornaio-ferramenta-sportpertutti-planetsoftware; oppure ferramenta- planetsoftware-fornaio-sportpertutti ecc…).

Nella figura riportata sopra la serie di valori binari (bit string) codifica le caratteristiche di un "individuo" della popolazione nelle sue variabili principali. L’aspetto interessante qui è che abbiamo iniziato a rappresentare il nostro problema in un linguaggio diciamo, in qualche modo, più vicino a quello di un computer … e questo, considerando che il nostro problema in fondo è quello di esplorare uno spazio enorme di possibili soluzioni, ci fa ben sperare perché sappiamo che un computer, per molti aspetti un cretino velocissimo, è formidabile a fare un enorme quantità di cose molto semplici in un tempo incredibilmente breve.

Inizialmente, i valori codificati in binario della nostra popolazione di possibili soluzioni, saranno costituiti da valori inizializzati in modo random e, dunque, non rappresenteranno una popolazione efficace per risolvere il problema dato … a meno che non apparteniamo a quella classe di odiose persone che giocano una sola schedina nella vita e riescono a fare quattordici al primo colpo.
Come facciamo a quantificare la “bontà” di ciascun individuo e quella della popolazione nel suo complesso? Per fare questo occorre definire a priori una funzione di fitness o di bontà.

La definizione della funzione di fitness, insieme alla codifica binaria delle possibili soluzioni, è il perno centrale di tutta la questione. Se sbaglio a definirla, perché ho caratterizzato male il mio problema, mi mancano informazioni ecc.. molto probabilmente la mia popolazione non seguirà un destino evolutivo ed io non troverò mai una soluzione efficace o, forse, non troverò affatto una soluzione.

Nell’esempio citato sopra la mia funzione di fitness sarà in una forma tale da sommare, per ciascun individuo, le variabili chilometri_percorsi + denaro_speso + tempo_impiegato … quindi nella forma:

F(km,Euro,tmp …);

la mia popolazione, i miei agenti artificiali, dovranno minimizzare tale funzione.

La questione quindi si pone nei termini:

ho una popolazione iniziale costituita da "individui-soluzioni" non ottimali. Come faccio a fare in modo che questa popolazione, inizialmente inefficace, possa seguire un destino evolutivo capace di far emergere una o più soluzioni efficaci?

Qui interviene l’emulazione di alcune dinamiche tipiche del mondo naturale come, ad esempio, la mutazione genetica, la selezione e l’accoppiamento tra specie capace di generare individui nel tempo più adatti a manipolare e a sopravvivere in un ambiente dato. Questa in fondo è stata l’intuizione principale di Holland quando pose le basi per i primi algoritmi genetici della Natural Computation.

Adesso, credo, siamo pronti per individuare e formalizzare un pochino le caratteristiche di un algoritmo genetico.

Definito dunque ogni individuo (soluzione possibile) come una sequenza di bit (bit string);
definita una popolazione di individui come una matrice NxN di “genomi” binari (bit string);
definita in modo preciso la mia funzione di fitness;
... siamo pronti a far evolvere il nostro piccolo mondo artificiale.

Imitando ciò che avviene in natura, la dinamica di evoluzione di ogni algoritmo genetico è caratterizzata da:

TRE OPERANDI

· Gene (bit string)
· Individuo
· Popolazione

TRE OPERATORI CICLICI

· Riproduzione
· Crossover
· Mutazione

I tre operandi (gene, individuo, popolazione) li abbiamo già visti e caratterizzati anche se, per il momento, non in modo formale. Ora proviamo a considerare anche gli operatori ciclici, cioè: riproduzione, crossover e mutazione.

La mutazione è piuttosto semplice. Immaginate che ogni tanto, agli individui che presentano il più basso livello di fitness viene data la possibilità di mutare a caso (random) qualcuno dei loro geni … un po’ come avviene in natura quando, per opera della radiazione solare, qualche gene di una pianta muta e produce una forma nuova magari più efficace e bella della precedente.

Per la riproduzione ed il crossover avviene quanto segue.

Immaginiamo di aver computato il fitness di ogni individuo (bit string) e della popolazione complessiva (matrice NxN di bit string) … molto probabilmente ci saranno alcuni individui buoni (soluzioni accettabili o quasi) e altri assolutamente inetti (soluzioni inaccettabili con fitness bassissimo) … questo lo possiamo verificare grazie al fatto che alcuni non riescono a minimizzare la funzione di fitness definita per il problema.

Imitando la natura decido che, al di sotto di una certa soglia di fitness alcuni individui avranno la possibilità di mutare random qualcuno dei loro geni, altri invece (senza speranza) moriranno perché sono troppo inadatti a vivere nell’ambiente dato.

Ma se alcuni individui muoiono, per evitare nel tempo di sterminare l’intera popolazione, come faccio a rimpiazzarli? Semplice, faccio "accoppiare" alcuni altri individui in modo tale da rigenerare la quantità di individui deceduti.

Quali individui faccio accoppiare? … ecco, qui le soluzioni sono tra le più diverse, quella che vi presento è la soluzione classica originariamente adottata da Holland … ma ce ne sono di più efficaci ed intelligenti. Quella originale di Holland tuttavia, è una delle più semplici e quindi è bene iniziare da questa.

Holland impiega una tecnica definita “ruota delle probabilità” … in sostanza gli individui (soluzioni possibili - bit string) con il fitness più elevato hanno maggiori probabilità di essere scelti come individui per la riproduzione. Apro una breve parentesi qui per dire che questa tecnica, la quale somiglia molto alla soluzione ariana dei nazisti, non è affatto la migliore. In sostanza, sebbene la popolazione riesca ad evolvere nel tempo, le prestazioni globali sono decisamente inferiori rispetto a soluzioni nelle quali viene riconosciuta ad una più ampia popolazione la possibilità di accoppiarsi, anche se il fitness di qualcuno non è tra i migliori. Quest'ultima metodologia consente di mantenere molto più alta la biodiversità e di raggiungere livelli di eccellenza incredibili.

Dunque, una volta scelti random, tra i migliori o quasi secondo Holland, due individui per l’accoppiamento, la prole viene generata seguendo il classico meccanismo del crossover tra due genomi. Questo è esattamente ciò che avviene in natura quando, a partire da due cromosomi, il cromosoma di ciascun figlio risulta essere un mix di caratteristiche dei due genitori. Nel nostro caso i genomi sono delle stringhe di bit (0-1) quindi la di manica è quella riportata in figura:

Come si vede in figura, si sceglie un punto (o più punti) random nella sequenza di bit e, rispetto a quel punto, si generano due nuovi individui effettuando lo swap dei genomi parziali dei genitori.

In questo modo, ad ogni generazione, avrò alcuni individui che mutano e (forse) migliorano, altri che muoiono, altri ancora che si accoppiano per rimpiazzare con la prole i deceduti. La mia popolazione, generazione dopo generazione, evolve verso soluzioni sempre più raffinate.

Gli operatori ciclici di crossover e mutazione permettono di generare biodiversità e flessibilità e di realizzare qiundi le dinamiche tipiche dei sistemi biologici in evoluzione. Questi algoritmi consentono di esplorare l'intero spazio delle possibili soluzioni selezionando ad ogni nuova generazione gli "individui" che rappresentano le migliori soluzioni al problema dato. Partendo da un piccolo insieme di soluzioni, grazie alle dinamiche di accoppiamento e riproduzione tra diversi individui della popolazione, ognuno con un suo fitness più o meno elevato, gli Algoritmi Genetici consentono inoltre di generare soluzioni nuove ed impensabili.

Vediamo uno schema utile per comprendere meglio le fasi di implementazione di un Algoritmo genetico:

L'analogia con i processi di riproduzione e di evoluzione biologici è forte e questo permette di utilizzare tutta la potenza e la flessibilità tipiche dei sistemi viventi.

Gli Algoritmi Genetici dunque offrono una notevole trasparenza nel modo di elaborare i dati permettendo di generalizzare tecniche e metodologie in contesti decisamente diversi. Essi, infine, si presentano spesso come l'unica strada praticabile per individuare una o più soluzioni delle quali si conosca l'esistenza ma non la procedura algoritmica per individuarle.

Un esempio di come implementare questi principi ed applicarli ad un problema classico di ottimizzazione si trova nel problema dei tre colori. Il problema è stato presentato per la prima volta da L. Davis [Davis, 1991], in un articolo tecnico: il problema dei “3 colori”.

Data una griglia N×N, ogni cella della griglia deve essere riempita da un colore, a partire da un insieme di 3 colori costanti e diversi (ad esempio verde, rosso e blu).

I vincoli da rispettare sono:

a. ogni cella deve aver un colore diverso sia nella cella sopra che in quella sotto;
b. ogni cella deve aver un colore diverso sia nella cella destra che in quella sinistra;
c. l’ultima colonna della griglia è contigua alla prima colonna;
d. l’ultima riga della griglia è contigua alla prima riga.

I punti c e d fanno di questa matrice una struttura toroidale chiusa. I vincoli, nel loro insieme, rendono questo problema semplice da comprendere ed efficace per effettuare un test.

Ogni individuo della nostra popolazione dunque è composto di N×N geni (bit string) ed ogni gene ha un alfabeto di 3 caratteri, uno per colore. Inoltre la fitness massima che ogni individuo può raggiungere è data da 2 ×(N×N), cioè il massimo di non contiguità di colore per riga (N×N) ed il massimo di non contiguità di colore per colonna (N×N).

Se la griglia da colorare è 5 × 5, ogni “individuo” sarà formato da 25 geni e una fitness di 50 indicherà tutti gli individui che rispettano tutti e quattro i vincoli (a, b, c, d).

Da notare che in una griglia 5 × 5 le colorazioni possibili sono 847.288.609.443, mentre le combinazioni accettabili (50/50 punti) sono 1.000 [Davis, 1991].

GenAgents, una simulazione di una società virtuale popolata da agenti artificiali in interazione per lo studio della evoluzione di dinamiche cooperative (o competitive), rappresenta un ulteriore esempio di applicazione degli algoritmi genetici a problemi di simulazione di dinamiche complesse [vedi la voce "agenti artificiali" del menu' principale].