SlideShare a Scribd company logo
UNIVERSITÀ DEGLI STUDI DI TRIESTE
DIPARTIMENTO DI INGEGNERIA E ARCHITETTURA
Corso di Laurea Magistrale in Ingegneria
Elettronica e Informatica
Progettazione e Sviluppo di un Sistema per
Migliorare il Codice Generato da un Large
Language Model Tramite Genetic
Improvement
Laureando Relatore
Damiano Ravalico Prof. Andrea De Lorenzo
Correlatore
Dott. Giovanni Pinna
Anno Accademico 2022/2023
Indice
Introduzione iii
1 Stato dell’arte 1
1.1 Generazione di programmi . . . . . . . . . . . . . . . . . . . . 1
1.2 Intelligenza artificiale . . . . . . . . . . . . . . . . . . . . . . . 2
2 Large Language Model 3
2.1 Artificial Neural Network . . . . . . . . . . . . . . . . . . . . 3
2.1.1 Definizione . . . . . . . . . . . . . . . . . . . . . . . . 3
2.1.2 Addestramento . . . . . . . . . . . . . . . . . . . . . . 4
2.1.3 Recurrent Neural Network . . . . . . . . . . . . . . . . 6
2.2 Tecnologia attuale . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.1 Attention Mechanism . . . . . . . . . . . . . . . . . . 9
2.2.2 Transformer . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3 Modelli . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.3.1 LLaMA . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.3.2 Alpaca . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.3.3 ChatGPT e GPT-4 . . . . . . . . . . . . . . . . . . . . 16
3 Evolutionary Computation 18
3.1 Genetic Algorithm . . . . . . . . . . . . . . . . . . . . . . . . 18
3.1.1 Schema generale . . . . . . . . . . . . . . . . . . . . . 19
3.1.2 Modello generazionale . . . . . . . . . . . . . . . . . . 19
3.1.3 Riproduzione . . . . . . . . . . . . . . . . . . . . . . . 22
3.1.4 Problematiche . . . . . . . . . . . . . . . . . . . . . . . 25
3.2 Genetic Programming . . . . . . . . . . . . . . . . . . . . . . 25
3.2.1 Inizializzazione e selezione . . . . . . . . . . . . . . . . 26
3.2.2 Operatori genetici . . . . . . . . . . . . . . . . . . . . 27
3.3 Grammatical Evolution . . . . . . . . . . . . . . . . . . . . . 30
3.3.1 Backus Normal Form . . . . . . . . . . . . . . . . . . . 30
3.3.2 Da BNF a GE . . . . . . . . . . . . . . . . . . . . . . 31
3.4 Genetic Improvement . . . . . . . . . . . . . . . . . . . . . . . 31
3.4.1 PonyGE2 . . . . . . . . . . . . . . . . . . . . . . . . . 32
i
INDICE
4 Metodologia 35
4.1 Selezione dei problemi . . . . . . . . . . . . . . . . . . . . . . 36
4.2 Inizializzazione . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.3 Generazione dinamica dei files di evoluzione . . . . . . . . . . 41
4.3.1 Grammatica . . . . . . . . . . . . . . . . . . . . . . . . 41
4.3.2 Genotipi e files di configurazione del GI . . . . . . . . 44
5 Risultati 47
5.1 Risposte dei LLMs . . . . . . . . . . . . . . . . . . . . . . . . 48
5.1.1 Problemi legati alla descrizione testuale dei problemi . 50
5.2 Applicazione del GI . . . . . . . . . . . . . . . . . . . . . . . . 52
5.2.1 Alpaca . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.2.2 ChatGPT e GPT-4 . . . . . . . . . . . . . . . . . . . . 57
5.2.3 Considerazioni finali e tempi di esecuzione . . . . . . . 65
Conclusioni 69
ii
Introduzione
I Large Language Models (LLMs) sono sistemi di intelligenza artificiale che
hanno dimostrato notevoli capacità nella comprensione e generazione di te-
sto in risposta a domande espresse in linguaggio naturale. Negli ultimi anni,
hanno guadagnato crescente importanza in vari settori, sia scientifici che pra-
tici, grazie alla loro capacità di accelerare e semplificare numerosi compiti.
Molti di questi sono difficilmente automatizzabili o la cui risoluzione è limi-
tata alla sfera umana, come ad esempio la generazione del codice sorgente
per i programmi software. L’utilizzo di questi modelli può fornire un valido
supporto agli sviluppatori quando i requisiti sono chiaramente definiti e cir-
coscritti. Tuttavia, in situazioni in cui i requisiti sono vaghi o il problema è
estremamente complesso da codificare, il codice generato può risultare errato
o incompleto. Ciò è dovuto alla limitazione intrinseca di questi modelli, che
basano le loro risposte su dati precedentemente osservati.
Il presente lavoro di tesi si propone di sviluppare una metodologia, e
presentarne l’implementazione, allo scopo di assistere gli utenti nel risolve-
re problemi di programmazione nel linguaggio Python in modo automatico
tramite Large Language Model (LLM). Tuttavia, il contributo distintivo di
questa ricerca risiede nell’ottimizzazione ulteriore dell’output ottenuto, sfrut-
tando approcci basati sul Genetic Improvement (GI), al fine di proporre un
risultato il più accurato possibile. Il processo di risoluzione del problema si
basa sulla sua descrizione in linguaggio naturale e su esempi forniti dall’u-
tente. Lo strumento sviluppato sfrutta una versione iniziale della soluzione
data da un LLM a scelta e ne valuta la correttezza rispetto ai valori di in-
put e output. Se quest’ultima non soddisfa tutti i requisiti implicitamente
definiti tramite queste coppie di valori, vengono utilizzate tecniche di Ge-
netic Improvement per raffinare il risultato, eliminando incompletezze e/o
correggendo evidenti problemi logici. Questo processo comporta l’utilizzo
della Grammatical Evolution (GE) per guidare l’evoluzione, attraverso la
definizione di una grammatica che formalizza un linguaggio, in questo caso
Python. Un aspetto innovativo proposto in questo lavoro è l’adozione di una
tecnica che consente di generare una grammatica in modo dinamico rispetto
al problema in analisi, accelerando così il processo evolutivo. Il risultato
sarà un programma Python valido che supera il maggior numero possibile
di test basati sui valori di esempio, senza richiedere all’utente di scrivere
iii
INTRODUZIONE
manualmente ulteriore codice.
Oltre alla definizione della metodologia presentata, essa è stata sperimen-
talmente testata per dimostrarne l’efficacia. I risultati ottenuti evidenziano
come la scelta del LLM da impiegare per il codice iniziale ha un’influenza
evidente sulla qualità della soluzione finale. Modelli di dimensioni maggiori
conducono a codici più precisi. Questa caratteristica si traduce in un’effica-
cia maggiore del GI e nella semplificazione della scrittura della grammatica
necessaria. In generale, il metodo proposto tende sempre a migliorare la
soluzione finale, seppur in modo marginale, portando ad una percentuale
di test superati più alta. È importante notare infine che la descrizione del
problema riveste un ruolo cruciale.
Nel Capitolo 1, verrà esaminata la storia delle tecniche conosciute nel
contesto della sintesi di programmi software. Seguirà un’analisi delle ricerche
condotte in letteratura e dell’implementazione di tali tecniche in software,
con particolare attenzione al codice Python. Il capitolo si concluderà con
una visione generale dei progetti simili a questo studio che utilizzano LLMs
per generare codice, migliorandone il risultato finale.
Il Capitolo 2 introdurrà all’argomento dei LLMs, iniziando dalle reti neu-
rali, che costituiscono la base dei recenti modelli. Verranno poi esposti alcuni
metodi specifici per l’elaborazione del linguaggio naturale basati su queste
tecnologia. Infine seguirà una panoramica sull’attuale tecnologia, mostrando
alcuni modelli che hanno riscosso molto successo e che verranno poi utilizzati
per l’implementazione dello strumento proposto.
Il Capitolo 3 proporrà una panoramica sul mondo delle tecniche geneti-
che. Verrà offerta una prima descrizione generale di come funzionano questi
algoritmi ispirati alla natura, in particolare al processo di evoluzione. Succes-
sivamente si tratterà del caso legato alla generazione completa di programmi
software, per poi definire il GI, fulcro di questo lavoro.
Nel Capitolo 4 verrà descritto il metodo seguito per lo sviluppo del pro-
getto. Per validare a metodologia applicata sono stati scelti dei problemi di
benchmark, su cui saranno effettuati tutti i test del caso, al fine di mostrare
l’efficacia dello strumento. L’intero processo, che parte dai dati forniti dal-
l’utente e termina con l’ottenimento di un codice migliorato, verrà descritto
per esteso, motivando ogni scelta fatta.
Nel Capitolo 5, verrà presentato ciò che è stato ottenuto, ponendo enfasi
sulle differenze tra i LLMs e mostrando il miglioramento effettivamente otte-
nuto tramite GI. Il lavoro si concluderà con un riassunto dei principali risul-
tati ottenuti, delineando le possibili direzioni future di ricerca e sottolineando
inoltre alcune caratteristiche di quanto proposto.
iv
Capitolo 1
Stato dell’arte
1.1 Generazione di programmi
La scrittura di programmi software è un’attività complessa e impegnativa,
che richiede abilità, conoscenze e competenze specifiche. Il programmatore
deve infatti comprendere il problema da risolvere, progettare una soluzio-
ne efficace e implementarla in un linguaggio di programmazione. Questo
processo può essere molto lungo e laborioso, e può richiedere un notevole
investimento di tempo e risorse. Già da diversi decenni, la ricerca si è con-
centrata sull’ideare delle tecniche il cui scopo è quello di generare programmi
in modo più semplice o automatico. La prima idea di ottimizzare un software
fu addirittura prima dell’avvento degli elaboratori elettronici. Essa risale al
1843, quando Luigi Menabrea [22] propose un metodo per generare automa-
ticamente il codice per una macchina analitica. In [9] viene invece definito il
problema di sintetizzare un circuito a partire da una descrizione matematica.
Questo è simile al voler generare automaticamente un programma software,
e ha dato origine a un campo di ricerca attivo, noto come sintesi di circuiti.
Negli anni ’60 e ’70 del XX secolo, si affermarono due correnti di ricerca
per automatizzare il processo di scrittura di codice sorgente: program syn-
tesis [12] e program transformation [35]. Nel primo caso l’obiettivo è quello
di costruire un intero programma senza un punto di partenza, mentre nel se-
condo è quello di trasformarne uno già esistente, raffinandolo. Quest’ultimo
viene implicitamente usato dai compilatori, come in [27], allo scopo di mini-
mizzare la forma del codice per questioni di efficienza di memoria e tempo,
preservandone però la correttezza semantica [5]. Nella sintesi di programmi
[19] invece, la costruzione avviene gradualmente, garantendone la correttez-
za. Questa tecnica ha destato continuo interesse negli anni a seguire come
nei lavori [18] e [3].
1
CAPITOLO 1. STATO DELL’ARTE
1.2 Intelligenza artificiale
Un ulteriore metodo teorizzato per primo da Alan Turing [32] consiste nell’e-
volvere i programmi, partendo da una popolazione di programmi applicando
operazioni analoghe ai processi genetici naturali alla popolazione di pro-
grammi. La prima implementazione però risale agli anni ’80, in cui durante
una conferenza vengono mostrati dei programmi evoluti [6] tramite linguaggi
ideati appositamente. Questa metodologia è chiamata Genetic Programming
(GP), nome coniato in [10]. Il GP condivide con il program synthesis lo scopo
di costruire da zero un programma funzionante. Le applicazioni sono mol-
teplici e spaziano in diversi campi, come ad esempio per modelli finanziari
[31], per l’industria dell’acciaio [14] o tutte le applicazioni puramente infor-
matiche. Sia la sintesi tradizionale del programma che il GP sono limitati
nella dimensione dei programmi che possono generare.
Un sottoinsieme di GP è il Genetic Improvement (GI) inizia da un pro-
gramma di base, di grandezza arbitraria, permettendo la generazione di pro-
grammi più complessi, e ne ottimizza le caratteristiche [16]. Questa tecni-
ca può essere applicata a qualsiasi linguaggio di programmazione, come ad
esempio C++ [25]. Esistono molti progetti che permettono di eseguire GI di
software. In [20] viene presentato un framework, che mostra ottimi risultati
nel miglioramento di codice C, Java e Python. Anche i lavori [1] e [4] permet-
tono un’evoluzione di programmi Java e Python, ma essi sono pensati per
migliorare i tempi di esecuzione o eliminare bachi, non per integrare codice
al fine di migliorare le funzionalità del programma. Con [8] è possibile effet-
tuare GI aggiungendo materiale genetico, estendendo quindi il programma
al fine di renderlo corretto o completarlo, ed è pensato per Python.
Un ulteriore approccio moderno per la generazione di codice senza la sua
scrittura è tramite utilizzo dei Large Language Models (LLMs). Questi mo-
delli di intelligenza artificiale, grazie all’enorme quantità di dati su cui sono
stati addestrati, permettono di ottenere programmi, anche di complessità
elevata, in molteplici linguaggi di programmazione. In [2] vengono esposti i
limiti dei LLMs in questo compito, effettuando dei test su due dataset di ben-
chmark. Sono inoltre presenti lavori che combinano la generazione di LLMs
con tecniche simili al GI, come in [17]. La parte di evoluzione però è fatta
ancora dal LLM, reiterando la richiesta ma modificandone le specifiche.
2
Capitolo 2
Large Language Model
L’attuale panorama dell’informatica sta subendo un cambiamento grazie al-
l’introduzione e all’evoluzione dei cosiddetti Large Language Models (LLMs).
Questi modelli, appartenenti all’ambito del Natural Language Processing
(NLP), hanno acquisito crescente rilevanza e stanno ridefinendo il modo in
cui le applicazioni informatiche interagiscono con il linguaggio umano.
Nel corso di questo capitolo, viene esplorato il mondo dei LLMs, ana-
lizzando in dettaglio il loro funzionamento. Ci si concentrerà sulla capacità
che questi modelli possiedono nel comprendere il contesto e nel generare te-
sto inerente. Infine verrà data una panoramica sulla storia e lo sviluppo di
queste moderne tecnologie.
2.1 Artificial Neural Network
Prima di capire come un LLM viene implementato, è necessario introdurre il
concetto di Artificial Neural Network (ANN), in quanto genitore di suddetta
tecnologia.
2.1.1 Definizione
Le ANN costituiscono una branca dei modelli computazionali di apprendi-
mento automatico i quali si ispirano principi dell’organizzazione neuronale
scoperti nelle reti neurali biologiche [21].
Una ANN è basata su una collezione di nodi interconnessi, detti neu-
roni, che modellano il concetto di neurone presente nei cervelli biologici,
seppur in modo semplificato. Anche in questo caso, ogni connessione, rap-
presentante una sinapsi (la struttura che consente il passaggio di segnali tra
neuroni), permette di inviare e ricevere segnali ai diversi neuroni che presen-
tano una connessione tra loro. Il segnale trasmesso da un neurone all’altro
è un numero reale che viene elaborato, tramite una funzione non lineare
dal ricevente. Ai nodi e alle loro connessioni vengono associati dei pesi che
3
CAPITOLO 2. LARGE LANGUAGE MODEL
contribuiscono ad aumentare o diminuire la potenza del segnale. Se il se-
gnale non supera una certa soglia prefissata allora esso non viene trasmesso
ai neuroni successivi. Il segnale ricevuto può essere rappresentato come un
vettore x = (x1, x2, . . . , xn) che associa ogni sua componente ad una diversa
connessione con altri neuroni. I pesi della rete sono raggruppati nel vettore
w = (w1, w2, . . . , wn). Il neurone combina linearmente i segnali di input ed
i loro pesi secondo la formula:
vj =
n
X
j=1
xj × wj + bk
dove bk rappresenta la soglia precedentemente menzionata. vj viene elabo-
rato tramite la funzione di attivazione, una funzione che calcola l’output
del nodo in base ai suoi input e ai pesi sui singoli input, generando il valo-
re di output, nonché input del prossimo layer. Questo procedimento viene
effettuato da ogni neurone della rete.
Infine è fondamentale notare che i neuroni sono raggruppati in più strati,
detti layers che eseguono diverse trasformazioni del segnale, a seconda dello
strato. I neuroni di un certo layer sono connessi solamente ai neuroni del layer
immediatamente precedente e successivo. Come schematizzato in Figura 2.1,
la struttura di una ANN è la seguente:
• Input layer: sempre presente, fornisce i dati di input alla rete, rappre-
senta quindi il punto di partenza.
• Hidden layer: ne possono essere presenti da uno a molti e il loro compito
è quello di processare i dati forniti in input.
• Output layer: produce il risultato finale.
Quando una nuova istanza di dati viene passata attraverso la rete neura-
le, essi scorrono attraverso ciascun layer, in una sola direzione, in modo da
determinare l’output; questo processo viene detto forward propagation. Al
contrario, durante la fase di addestramento, dopo che la fase di forward pro-
pagation è stata completata, viene calcolato l’errore commesso dalla rete
rispetto all’output desiderato. Successivamente tale errore viene propaga-
to all’indietro attraverso l’architettura dell’ ANN viene propagato all’indie-
tro nella rete per migliorare la correttezza dell’output; questa fase viene
chiamata backforward propagation.
2.1.2 Addestramento
L’obiettivo principale di una ANN è quello di imparare a svolgere una spe-
cifica attività, come ad esempio il riconoscimento di immagini, la traduzione
di testi, la previsione di dati futuri, e molto altro ancora. Prima di poter
4
CAPITOLO 2. LARGE LANGUAGE MODEL
Figura 2.1: Rappresentazione schematica di una ANN con un solo hidden layer.
eseguire correttamente il compito desiderato, la rete va addestrata, fornen-
dole degli input, e valutando la correttezza dell’output. L’input, output e
definizione di metrica di errore dipendono dal tipo di apprendimento scelto.
Di seguito vengono citati i principali metodi di apprendimento automatico,
senza entrare nel dettaglio.
• Il supervised learning consiste in uno sviluppo del modello attraverso
un processo di emulazione. Al modello vengono forniti dei campioni,
composti da un valore di input e l’output corrispondente atteso e l’o-
biettivo è quello di minimizzare la differenza tra output atteso e output
predetto dalla rete. I dati vengono chiamati etichettati, in quanto è
nota la corrispondenza input-output.
• Nell’unsupervised learning invece sono disponibili soltanto i valori di
input, e l’obiettivo è quello di inferire relazioni e/o strutture nei dati.
Ossia, si cerca di decodificare la regola statistica che lega gli input.
• Infine, nel reinforcement learning, si cerca di realizzare un model-
lo in grado di scegliere azioni da compiere per il conseguimento di
determinati obiettivi tramite l’interazione con l’ambiente esterno.
Questa fase di addestramento comporta l’aggiustamento dei pesi delle
connessioni tra i neuroni, e opzionalmente anche quello dei valori di soglia.
Tramite questi cambiamenti è possibile migliorare l’accuratezza del risultato
dato dal modello, ovvero minimizzando gli errori sulle osservazioni. L’ap-
prendimento può essere considerato ultimato quando osservazioni aggiuntive
non riducono il valore di errore.
5
CAPITOLO 2. LARGE LANGUAGE MODEL
2.1.3 Recurrent Neural Network
Le ANN rappresentano una tecnologia estremamente versatile, infatti vengo-
no utilizzate per diversi scopi. Esse trovano applicazioni in molteplici ambiti,
anche se sono molto conosciute per il loro utilizzo nei campi di computer
vision and pattern recognition, classificazione/predizione di dati e Natural
Language Processing (NLP). Quest’ultimo è un campo interdisciplinare che
combina linguistica, informatica e intelligenza artificiale al fine di consentire
ai calcolatori di comprendere, interpretare e generare il linguaggio umano in
modo naturale. L’obiettivo principale del NLP è quello di creare modelli e
algoritmi che permettano agli elaboratori di interagire con il linguaggio uma-
no in modo simile a come lo fanno gli esseri umani. Tale campo si concentra
su molteplici aspetti del linguaggio, tra cui:
• Elaborazione e generazione di testo.
• Analisi sentimentale.
• Estrazione di informazioni.
• Risposta a domande.
Di grande interesse risultano quindi essere le Recurrent Neural Network
(RNN), una delle tipologie principali di rete neurale artificiale, nonché uno
dei modelli più utilizzati in ambito NLP fino all’avvento di tecnologie più re-
centi. Tali reti vengono tipicamente addestrate tramite supervised learning,
ma rispetto alle reti descritte nella Sezione 2.1.1, presentano una memoria,
detta hidden state, in quanto usano le informazioni passate per influenzare
l’input e l’output correnti; non c’è quindi indipendenza tra coppie di input-
output, bensì lo stato attuale dipende dagli elementi precedenti all’interno
della sequenza. Grazie a questa loro capacità, esse risultano essere applicabi-
li a compiti come il riconoscimento vocale, ad esempio negli assistenti vocali
o in generale a bot usati per interazione con umani e a predizioni su cosa
verrà dopo una data sequenza di parole.
A fine esemplificativo, si immagini una sequenza di valori di input x =
(x1, x2, . . . , xn) con n arbitrario. Ad ogni step t, la rete produce un output
yt ed è caratterizzata da un hidden state ht, che funge da rappresentazione
della sottosequenza x1, x2, . . . , xt. ht viene calcolato combinando ht−1 e xt−1,
mentre l’output è dato da yt = f(xt, ht−1). Chiaramente vanno considerati
anche i pesi della rete, riguardanti le connessioni con il layer precedente e
il vettore ht, definiti rispettivamente dalle matrici Wxh e Whh, ottenendo
quindi
yt = f(Wxh · xt, Whh · ht−1)
In Figura 2.2 è illustrato il diagramma rappresentate dei concetti appena
esposti.
6
CAPITOLO 2. LARGE LANGUAGE MODEL
Figura 2.2: Rappresentazione del meccanismo attuato dalle RNN per mantenere nella
rete informazioni passate.
Alla fine della Sezione 2.1.1 viene introdotto il concetto della backpropa-
gation. Si consideri ora, per esempio, il processo di backfoward propagation
durante l’addestramento, in una rete a singolo layer:
1. La prima parte consiste nel forward propagation, in cui vengono cal-
colati i pesi e l’output.
2. Nell’output layer viene calcolato il gradiente della funzione di costo;
essa rappresenta la misura della qualità dell’output proposto dalla rete
rispetto a quello atteso. Il gradiente misura quindi il cambiamento nei
pesi rispetto alla variazione dell’errore.
3. I pesi vengono aggiornati ripetutamente fino a quando non c’è conver-
genza tra output e risultato atteso oppure fino a quando non si verifica
un qualche criterio di stop imposto a priori.
Durante il processo di addestramento di una RNN, il gradiente può di-
ventare troppo piccolo o troppo grande, portando a basse prestazioni, bassa
accuratezza e tempi di addestramento molto lunghi. Si parla rispettivamente
di vanishing gradient problem e di exploding gradient problem. Allo scopo
di analizzare una delle problematiche principali che affliggono le RNN, ver-
rà trattato solamente il primo problema. Esso si verifica quando il valore
del gradiente calcolato diminuisce ad ogni iterazione. La situazione appena
descritta può essere causata da diversi fattori:
• Scelta della funzione di attivazione.
• Pesi dei primi layers molto piccoli.
• Numerosità dei layers.
Nel peggiore dei casi, il vanishing gradient problem può portare ad un blocco
totale dell’apprendimento. A scopo esemplificativo si consideri la funzione
7
CAPITOLO 2. LARGE LANGUAGE MODEL
sigmoidea con relativa derivata, rappresentata in Figura 2.3. Si noti come,
quando l’input della funzione aumenta o diminuisce, ovvero |x| aumenta, il
valore della derivata tende a zero. Supponendo che ci siano n hidden layers
con funzione di attivazione sigmoidea, allora n derivate a valore molto basso
vengono moltiplicate per ottenere il gradiente, che risulterà decrescere espo-
nenzialmente. Un gradiente molto piccolo implica che i pesi e le soglie dei
livelli iniziali non verranno aggiornati in modo efficace. Una rete con mol-
teplici layers, come nell’esempio appena proposto, viene detta Deep Neural
Network (DNN).
Figura 2.3: La funzione sigmoidea con al sua derivata.
Questo tipo di problema è intrinseco nelle RNN vanilla. Tramite backfor-
ward propagation, il termine associato alla memoria della rete può svanire,
rendendo la rete incapace di tenere conto del passato. Per ovviare a questa
debolezza, sono state introdotte le reti Long Short-Term Memory (LSTM).
L’intuizione alla base dell’architettura LSTM è quella di creare un modulo
aggiuntivo che sia capace di capire quando ricordare e quando dimenticare
le informazioni. In altre parole, la rete apprende effettivamente quali infor-
mazioni potrebbero essere necessarie in seguito in una sequenza e quando
tali informazioni non sono più necessarie. Ad esempio, nel contesto dell’e-
laborazione del linguaggio naturale, la rete può apprendere le dipendenze
grammaticali.
Tuttavia, nonostante i successi ottenuti dalle ANN e dalle RNN, assieme
alle migliorie come le LSTM, queste architetture presentano alcune limita-
zioni significative. Nei casi delle ANN, possono emergere problemi legati
alla scomparsa e all’esplosione dei gradienti durante il processo di addestra-
mento. Tali fenomeni ostacolano l’apprendimento di relazioni intricate e a
lungo termine nei dati, limitando la capacità di catturare dipendenze estese
all’interno delle sequenze. Inoltre, manca la considerazione della struttura
sequenziale innata dei dati, poiché trattano ogni punto della sequenza come
se fosse indipendente dagli altri, con il rischio di perdere informazioni cruciali
in problemi sequenziali.
Inoltre, nonostante le migliorie apportate delle RNN, come le LSTM,
nel risolvere il problema delle dipendenze più lontane, permane una limi-
8
CAPITOLO 2. LARGE LANGUAGE MODEL
tazione fondamentale. La modalità sequenziale nell’elaborazione delle RNN
conduce a una parallelizzazione lenta durante l’allenamento, generando oneri
computazionali e restrizioni di scalabilità, soprattutto per modelli di grandi
dimensioni. Infine, le RNN non sempre riescono a tenere in considerazione le
relazioni temporali intricate, specialmente in sequenze lunghe e complesse.
Di conseguenza nel tempo è emersa l’esigenza di un nuovo paradigma
che superi queste limitazioni e apra nuove prospettive nell’elaborazione delle
sequenze. L’introduzione dei Transformers ha rappresentato proprio que-
sto avanzamento, ridefinendo il modo in cui le relazioni all’interno dei dati
sequenziali vengono modellate e comprese.
2.2 Tecnologia attuale
L’evoluzione tecnologica dei Transformers, introdotti in [33], ha portato a
una rivoluzione nell’ambito dell’elaborazione del linguaggio naturale. Que-
sto progresso è emerso in modo eclatante con l’avvento di modelli Generative
Pre-trained Transformers (GPT) [26] e Bidirectional Encoder Representa-
tions from Transformers (BERT) [7], che si basano sull’architettura di rete
neurale a Transformer. Il principio di base su cui sono costruiti, detto Atten-
tion Mechanism, pone l’enfasi sulla parallelizzazione delle operazioni e sulla
capacità di catturare relazioni semantiche complesse nei dati testuali, ma
non solo. Esso si ispira ai sistemi biologici degli esseri umani che tendono a
concentrarsi sulle parti distintive durante la lavorazione di grandi quantità
di informazioni. Attraverso questo approccio, i Transformers riescono a pro-
cessare simultaneamente diverse parti del testo, catturando contesti profondi
e generando in modo coerente e preciso testi di alta qualità. Questa capacità
di parallelizzazione e Attenzione ha aperto nuove frontiere nell’intelligenza
artificiale, con applicazioni che spaziano dalla traduzione avanzata alla crea-
zione di testo coinvolgente, dall’assistenza virtuale all’analisi dei dati su larga
scala e molto altro.
2.2.1 Attention Mechanism
Il concetto chiave dell’Attention Mechanism è consentire alla rete di appren-
dere le relazioni tra i pezzi di una sequenza di input (nota come token). Ad
esempio, considerando la frase “il gatto è saltato sopra il cane pigro”, ci aspet-
teremmo che la rete impari ad associare “gatto” con “saltato” e “pigro” con
“cane”. L’Attenzione può essere applicata a qualsiasi tipo di dato che può es-
sere formattato come una sequenza. Ad esempio, può essere utilizzato anche
con i dati visuali, in cui un’immagine è rappresentata come una sequenza di
correzioni che vengono poi utilizzate come token in una sequenza.
Questo meccanismo permette al modello di focalizzarsi su parti specifiche
di un input mentre elabora o genera un output. Nell’ambito di interesse di
9
CAPITOLO 2. LARGE LANGUAGE MODEL
questo studio, l’Attenzione è ciò che consente ai Transformers di compren-
dere le relazioni tra le diverse parole o token in un testo. Il meccanismo di
Attenzione funziona nel seguente modo:
1. Input e query: l’input consiste in una serie di token, che possono essere
parole o frammenti di testo. Per essere elaborato dal modello, ciascun
token viene rappresentato come un valore numerico, ottenuto da tecni-
che di Embedding (rappresentazione di caratteri, parole, frasi o testo
come vettori numerici per consentirne l’elaborazione), che cattura le
sue caratteristiche semantiche. Per generare un token nell’output, vie-
ne creato un vettore di query associato a quel token. Questo vettore
di query è ciò su cui il modello di attenzione si baserà per determinare
quali parti dell’input sono rilevanti.
2. Creazione delle query: la query è un vettore numerico che rappresenta
il token che si sta cercando di generare nell’output. Questo vettore di
query viene generato dalla combinazione di informazioni contestuali e
dallo stato interno del modello. Essa è fondamentale perché definisce
su cosa il modello sta cercando di porre la sua attenzione mentre decide
come generare il token successivo.
3. Calcolo dell’Attenzione: per ogni token di input, il modello calcola un
peso di Attenzione che rappresenta quanto quel token è rilevante per
la query corrente. Il calcolo di questo peso si basa su una misura di
similarità tra la query e il token di input. Una tecnica comune per
calcolare questa similarità è l’uso del prodotto scalare tra i vettori di
query e token di input.
4. Calcolo dei pesi normalizzati: dopo aver calcolato i pesi di attenzio-
ne, vengono normalizzati utilizzando funzioni di attivazione come la
Softmax. Questo passaggio trasforma i pesi in una distribuzione di
probabilità, assicurando che la somma di tutti i pesi sia uguale a 1.
La normalizzazione è importante poiché fa sì che l’Attenzione sia di-
stribuita tra i token di input in base alla loro rilevanza relativa per la
query.
5. Calcolo degli input ponderati: i pesi normalizzati vengono utilizzati
per ponderare le rappresentazioni di input associate a ciascun token. I
token più rilevanti ricevono pesi maggiori, mentre i token meno rilevanti
ricevono pesi minori. Questo passaggio crea un insieme di input che
riflette l’importanza relativa di ciascun token di input per la query.
6. Aggregazione degli input ponderati: infine, le rappresentazioni di in-
put ponderate vengono sommate per creare un vettore di output fi-
nale. Questo vettore rappresenta l’attenzione complessiva data alla
10
CAPITOLO 2. LARGE LANGUAGE MODEL
query rispetto all’input. L’output aggregato cattura quindi l’informa-
zione chiave dall’input che è rilevante per generare il token corrente
nell’output.
Il meccanismo di Attenzione consente ai modelli di considerare le connessioni
tra le parole in un testo e di dare maggior peso a quelle parole che sono più
importanti per il contesto corrente. Questo processo di Attenzione distribui-
ta su input diversi è ciò che consente di catturare relazioni complesse e di
generare testo coerente e significativo.
Si noti che questo modello può essere ulteriormente migliorato combinan-
do diverse rappresentazioni di Attenzione indipendenti, tramite il cosiddetto
Multi-Head Attention Mechanism. Esso consente ai modelli di cogliere rela-
zioni più complesse tra le parole all’interno di una frase o di una sequenza.
Invece di dipendere da una singola testa di attenzione, il modello utilizza
più Attenzioni per catturare diversi aspetti delle relazioni semantiche tra le
parole. Ciascuna calcola un peso di Attenzione differente per ogni parola
in base alle sue relazioni con altre parole. Queste diverse prospettive sono
quindi concatenate e combinate per ottenere un risultato finale.
2.2.2 Transformer
Come menzionato alla fine della Sezione 2.1.3, il problema con i modelli
discussi sta nel fatto che le informazioni a lungo termine tendono a essere
dimenticate, all’aumentare della lunghezza della sequenza. Teoricamente, le
informazioni di un token possono propagarsi molto più in profondità nella
sequenza, ma nella pratica, la probabilità che le informazioni vengano con-
servate diminuisce in modo esponenziale, man mano che ci si allontana da
una parola specifica.
Si supponga di voler tradurre una frase: nelle RNN, la frase verrebbe pro-
cessata parola per parola, mentre nei Transformers la frase viene elaborata
per intera in una sola volta, reiterando il processo di traduzione per più volte
al fine di migliorare il risultato. L’architettura originale del Transformer è
composta è composta da un Encoder e un Decoder. L’Encoder si occupa
di convertire l’input testuale in una rappresentazione contestuale, mentre
il Decoder genera una sequenza di output basandosi sulla rappresentazione
data dall’Encoder. Con l’analisi successiva si andranno a descrivere ad alto
livello l’Encoder e il Decoder.
Come mostrato in Figura 2.4, il processo inizia con l’Input Embedding.
l’informazione che viene data come input deve essere rappresentata in forma
numerica. Tale rappresentazione cattura il significato semantico di ciò che
viene incorporato. La proprietà principale degli Embeddings è che, secon-
do una qualche metrica definita, sono più simili tra loro se i corrispondenti
valori da cui sono stati generati hanno un significato simile o sono contestual-
mente correlati. Di conseguenza gli Embeddings consentono di comprendere
11
CAPITOLO 2. LARGE LANGUAGE MODEL
meglio il significato e le relazioni tra le parole all’interno delle sequenze di
testo, facilitando così l’apprendimento e l’elaborazione del modello di intel-
ligenza artificiale. Vista la computazione parallela della sequenza di testo, i
Transformers non posseggono la nozione di ordine temporale in cui appaio-
no le parole, il che comporterebbe una perdita di informazione nell’output.
La fase di Positional Encoding serve ad inserire un’informazione posizionale
all’interno degli Embeddings. Va osservato che un semplice indice numerico
crescerebbe troppo per sequenze grandi, mentre un indice normalizzato cree-
rebbe problemi per sequenze di lunghezza variabile. In questa fase quindi,
ogni indice delle parole nella frase di partenza viene mappato in un vettore.
Il risultato dato da Embeddings e Positional Encoding, è una matrice dove
ogni riga rappresenta un oggetto codificato della sequenza sommato alle sue
informazioni di posizione. A fini esemplificativi, si supponga di avere una
sequenza di lunghezza L e di voler ottenere la codifica del k-esimo elemento;
essa è data dalle funzioni seno e coseno a diverse frequenze.
PEk,2i = sin

k
n
2i
d

PEk,2i+1 = cos

k
n
2i
d

dove d è la dimensione dell’Embedding, i è l’indice all’interno dell’Embedding
mentre n è una costante arbitraria, fissata a 10 000 dagli autori [33].
Il layer successivo, quello dell’Encoder, è composto da una parte di Multi-
Head Attention e da una successiva rete neurale. Il suo compito consiste nel
mappare tutte le sequenze di input in una rappresentazione che contiene
le informazioni apprese per l’intera sequenza. Come descritto nella Sezione
2.2.1, l’Attention Mechanism consente al modello di associare le parole di
input ad altre parole, quelle che dovrebbero rappresentare l’output. Succes-
sivamente, i vettori rappresentanti i pesi di Attenzione vengono aggiunti ai
vettori posizionali degli Embeddings. Le rappresentazioni contestuali pas-
sano poi attraverso la rete neurale, nel tentativo di estrarre più elementi di
interesse, aumentando quindi la qualità della rappresentazione. Tutte queste
operazioni hanno la finalità di codificare l’input in una rappresentazione che
tenga conto delle informazioni estratte dal meccanismo di Attenzione. Ciò
faciliterà il modello a concentrarsi sulle parole appropriate nell’input duran-
te il processo di decodifica. È possibile eseguire il processo di codifica più
volte, in modo tale che ogni head dell’Encoder abbia l’opportunità di appren-
dere diverse rappresentazioni, aumentando così potenzialmente la capacità
predittiva della rete.
Il procedimento continua con il layer di decodifica, che genera la sequenza
di testo. Esso presenta una struttura simile a quello dell’Encoder, infatti pre-
senta due layers di Multi-Head Attention e un layer di rete neurale. Queste
parti si comportano in modo simile rispetto a quello che avviene nell’Enco-
der, ma ogni livello di Attenzione ha un compito diverso. Il Decoder presenta
12
CAPITOLO 2. LARGE LANGUAGE MODEL
Figura 2.4: Architettura originale del modello Transformer.
inoltre un classificatore, nel caso in cui il compito del modello consista anche
nella classificazione di testo, e un layer Softmax, per ottenere le probabilità
associate alle parole. L’input passa attraverso un livello di Embedding e un
livello di Positional Encoding. Gli Embedding posizionali vengono inseriti
nel primo livello di Multi-Head Attention che calcola i pesi di Attenzione
per l’input del Decoder. Poiché il Decoder genera la sequenza parola per
parola, è necessario evitare che venga condizionato da token futuri, apparsi
successivamente nella sequenza di parole iniziale. Il metodo per evitare che
ciò avvenga è quello del mascheramento. La maschera di previsione è una
matrice delle stesse dimensioni dei pesi di Attenzione, riempita con valori
v ∈ {0, −∞}. In questo modo, quando le matrici dei pesi di Attenzione e
quelle relative al mascheramento vengono moltiplicate, si ottiene una nuo-
va matrice dei pesi. Essa ha valori diversi da −∞ solamente nel triangolo
inferiore, definito dalla diagonale principale, corrispondente alle parole pre-
cedenti alla parola in elaborazione. L’Attention Mechanism usa una funzione
Softmax per assegnare un peso a ogni token della sequenza di input. I token
con un peso negativo vengono azzerati, in modo tale che il modello non si
concentri su quelle parole. Segue il secondo livello di Multi-Head Attention,
esso abbina l’input del Encoder all’input del Decoder, consentendo al Deco-
der di decidere su quale input del Encoder è rilevante concentrarsi. L’output
passa poi attraverso una rete neurale per un’ulteriore raffinamento.
Infine, il risultato passa attraverso un classificatore, dalle dimensioni pari
13
CAPITOLO 2. LARGE LANGUAGE MODEL
al numero di classi. Ad esempio, se ci sono 10 000 classi per 10 000 parole,
l’output di questo classificatore sarà di dimensione 10 000. L’output viene
successivamente inserito in un layer Softmax, che produrrà punteggi di pro-
babilità compresi tra 0 e 1. L’indice corrispondete alla probabilità più alta
equivale all’indice della parola predetta del modello. In seguito alla predi-
zione della parola, il decodificatore provvedere ad aggiungere tale predizione
all’elenco dei suoi input. Tale sequenza di eventi continua fino alla decodifica
di un token. Più Decoder possono essere usati in concomitanza, in modo da
permettere al modello di concentrarsi su diverse combinazioni di Attenzione,
migliorando la capacità predittiva.
2.3 Modelli
Prima dell’avvento dei Transformers, i modelli a rete neurale più adatti a
problemi di Natural Language Processing, utilizzavano anche tecniche di su-
pervised learning, avendo a disposizione grosse quantità di dati etichettati.
La dipendenza dal apprendimento supervisionato ne limitava l’uso su insiemi
di dati che non erano ben etichettati e rendeva anche molto costoso e dispen-
dioso in termini di tempo l’addestramento di modelli estremamente grandi.
Con l’introduzione dei modelli GPT, il paradigma per l’addestramento di
questi modelli cambia rispetto al classico apprendimento supervisionato. Il
nuovo metodo di addestramento consiste in una una combinazione di una
piccola quantità di dati etichettati dall’uomo, seguito da una grande quan-
tità di dati senza etichetta, tale processo viene detto semi-supervised lear-
ning. Inizialmente viene creata una prima versione del modello, generando
casualmente i parametri iniziali che successivamente vengono migliorati su
ciascuno dei task desiderati, nel cosiddetto processo di fine-tuning, in cui i
pesi del modello generato precedentemente vengono adattati ai nuovi dati
non etichettati [37]. Verranno ora introdotti i modelli che sono stati scelti
per questo studio.
2.3.1 LLaMA
Il modello Large Language Model Meta AI (LLaMA) [30] sviluppato dalla
società Meta AI, rilasciato solo a scopi di ricerca, è stato il primo modello
a poter competere con i modelli generativi sviluppati nell’ultimo anno da
OpenAI. Esistono diverse varianti del modello che variano per numero di
parametri della rete: il modello LLaMA più piccolo presenta 7 miliardi di
parametri (7B) fino ad arrivare a 65 miliardi (65B). I modelli linguistici svi-
luppati da Meta AI hanno consentito ai ricercatori di utilizzare e addestrare
questo modello anche senza grandi risorse computazionali. La presenza di
modelli di base più piccoli, come LLaMA, è auspicabile nel ampio spazio dei
LLM in quanto richiedono molta meno potenza di calcolo e risorse per testa-
re nuovi approcci, convalidare il lavoro di altri ed esplorare nuovi casi d’uso.
14
CAPITOLO 2. LARGE LANGUAGE MODEL
Dataset Proporzioni Dimensioni
CommonCrawl 67.0% 3.3 TB
C4 15.0% 783 GB
GitHub 4.5% 328 GB
Wikipedia 4.5% 83 GB
Books 4.5% 85 GB
ArXiv 4.5% 92 GB
StackExchange 4.5% 78 GB
Tabella 2.1: Provenienza dei dati su cui è stato effettuato l’addestramento di LLaMA.
Questi modelli sono stati addestrati su un ampio set di dati non etichettati,
nel caso di LLaMA circa 1 trilione di tokens, il che li rende ideali per la messa
a punto di un’ampia gamma di attività di attività. Seppure questi modelli
siano di dimensioni ridotte, merita osservare che, come riportato dalla so-
cietà, l’addestramento del modello più grande è durato circa 21 giorni, su
un cluster con 2048 GPU A1001 con 80 GB di memoria principale. Si può
notare come i costi in tempo e risorse finanziare risultano essere proibitivi
per organizzazioni piccole; viene quindi ulteriormente evidenziata l’utilità di
questo progetto.
LLaMA è stato rilasciato con annesso il codice sorgente per il suo utilizzo
tramite inferenza, senza quindi il necessario per permettere il fine-tuning,
anche se ciò è senza dubbio possibile. Questa decisione da parte di Meta
ha favorito notevolmente il lavoro open-source, infatti questi modelli sono
stati poi utilizzati per svariati compiti e sono stati la base di molte loro
derivazioni. Un’ulteriore novità introdotta con questi modelli, consiste nella
loro creazione partendo da dati pubblicamente disponibili, come mostrato
in Tabella 2.1. Questo modello è basato sull’architettura a Transformers,
anche se con alcune modifiche che non verranno discusse. Come modello
di linguaggio generativo, esso permette la generazione e traduzione di testo,
scrittura di contenuti creativi, risposte a domande e generazione di codice
sorgente.
Per questo studio, LLaMA risulta essere un eccellente candidato, visto
che l’allenamento comprende anche GitHub2, famoso sito di hosting per codi-
ce. Va osservato che le risorse a disposizione risultano essere limitate, quindi
anche per la semplice inferenza, il modello da 13B è il più grande utilizzabile.
Il fine-tuning esula dagli scopi prefissati e non verrà quindi trattato.
1
https://www.nvidia.com/en-us/data-center/a100/
2
https://github.com/
15
CAPITOLO 2. LARGE LANGUAGE MODEL
2.3.2 Alpaca
Un’importante variante di LLaMA 7B è Alpaca3, derivante da un fine-tuning.
Questo lavoro è stato eseguito utilizzando il processo di self-instruct[34]. Il
dataset usato è contenuto in un file JSON da 52.000 di instruction-following
data, dove ciascun campione è formato dall’istruzione, una stringa che descri-
ve il task, un input opzionale, rappresentante più dettagli sul contesto o dei
dati, e l’output, la risposta generata dal modello scelto per la generazione di
tali dati. Nel caso dell’addestramento di Alpaca, il modello usato per creare
i dati da usare per il fine-tuning è text-davinci-0034, e la distribuzione dei
dati a seconda del task, è riportata in Figura 2.5. Va notato che, essendo un
progetto open-source, è possibile modificare alcuni parametri del modello.
Un LLM prevede molteplici possibili configurazioni, ma è necessario cita-
re la temperatura. Questo valore permette di specificare quanto l’output
del LLM sia deterministico o casuale. Come descritto precedentemente, un
LLM accetta una sequenza di input di token e tenta di prevedere il token
successivo. Lo fa generando una distribuzione di probabilità discreta su tutti
i possibili token. La temperatura T, che varia nell’insieme [0, 1], modifica
questa distribuzione: se T → 1 la distribuzione diventa uniforme, ogni token
è equamente probabile, rendendo le risposte più casuali e diverse tra loro a
parità di prompt. Al contrario, se T → 0 il modello è totalmente determini-
stico, scegliendo sempre il token più probabile. Nell’implementazione usata
il valore di questo parametro è stato mantenuto a quello di default, ovvero
0.6, tendente di poco ad essere più creativo.
Per questo studio, Alpaca si è dimostrato nettamente più interessante
rispetto alla versione di LLaMA rilasciata da Meta AI, grazie alla sua legge-
rezza ma al contempo alla precisione maggiore data dal lavoro di fine-tuning.
L’inserimento del modello in questo lavoro viene ulteriormente velocizzato
grazie a semplificazioni apportate all’implementazione del modello, rese di-
sponibili sul sito di HuggingFace5, ove sono presenti le versioni di Alpaca 7B
e Alpaca 13B.
2.3.3 ChatGPT e GPT-4
Chat Generative Pre-trained Transformer (ChatGPT) è un chatbot6 svilup-
pato dall’azienda OpenAI, che non ha però rivelato molti dettagli implemen-
tativi. Questo software ha riportato notorietà al campo dei LLMs, oltre che
rappresentare una tecnologia estremamente versatile, grazie alla quantità di
dati su cui è stato addestrato. ChatGPT è basato sul modello gpt-3.5-turbo
da 175B di parametri, per poi essere sottoposto a un processo di fine-tuning
al fine di ottimizzarlo a applicazioni conversazionali. Questo processo di
3
https://crfm.stanford.edu/2023/03/13/alpaca.html
4
https://platform.openai.com/docs/models/gpt-3-5
5
https://huggingface.co/
6
Software progettato per simulare una conversazione con un essere umano.
16
CAPITOLO 2. LARGE LANGUAGE MODEL
Figura 2.5: Diversità dei dati utilizzati. Il cerchio interno del grafico rappresenta il
verbo radice delle istruzioni e il cerchio esterno rappresenta gli oggetti interessati.
perfezionamento ha sfruttato sia il supervised learning che il reinforcement
learning in un processo chiamato reinforcement learning from human feed-
back. Entrambi gli approcci hanno impiegato esseri umani per migliorare
le prestazioni del modello, aumentando l’accuratezza del output finale for-
nito all’utente. L’obiettivo era di esporre il modello a una vasta gamma
di argomenti, stili di scrittura e voci linguistiche al fine di sviluppare una
comprensione generale del linguaggio umano.
Questo modello rappresenta uno dei modelli più utilizzati data la sua
capacità di interfacciarsi molto bene con gli esseri umani, ed il suo utilizzo è
il più semplice possibile sfruttando le API di inferenza messe a disposizione
dalla società. Anche questo modello, tra i diversi tipi di dati a cui è stato
sottoposto, comprendo codice sorgente di molti linguaggi, ed è quindi idoneo
ed interessante ai fini di questo studio. Inoltre, è stata inserita anche la ver-
sione più recente con capacità aumentate, GPT-4, accessibile dall’interfaccia
web di ChatGPT dopo aver effettuato il pagamento. Anche in questo caso
le specifiche non sono state rese note, a meno della sua capacità di accedere
ad Internet.
17
Capitolo 3
Evolutionary Computation
L’Evolutionary Computation (EC) è un paradigma computazionale che trova
la sua radice nell’osservazione dei processi evolutivi naturali. Attraverso l’uso
di algoritmi evolutivi, l’EC si propone di risolvere problemi complessi che
spesso sfuggono alle capacità di risoluzione umane o agli approcci di ricerca
tradizionali.
Una delle estensioni più rilevanti e innovative dell’EC è rappresentata dal
Genetic Improvement (GI), una disciplina che mira a migliorare algoritmi,
software e programmi esistenti attraverso l’ottimizzazione guidata dall’evo-
luzione. A differenza dello sviluppo tradizionale del software, che si basa
su iterazioni umane e decisioni di progettazione, il GI permette di raffi-
nare automaticamente e in modo sistematico le prestazioni dei programmi
esistenti.
Nel contesto del GI, i programmi vengono trattati come individui all’in-
terno di una popolazione virtuale, soggetti a processi di selezione, mutazione
e incrocio genetico allo scopo di generare varianti migliorate. Questo approc-
cio non solo accelera il processo di ottimizzazione, ma può anche portare a
scoperte inattese e soluzioni innovative, consentendo l’evoluzione continua
dei software nel tempo.
In questo capitolo viene offerta una panoramica sul mondo dell’EC per
poi trattare il GI, di principale interesse per questo studio.
3.1 Genetic Algorithm
Con Genetic Algorithm (GA) si intende un algoritmo ispirato all’evoluzione
biologica, orientato alla risoluzione un problema di ottimizzazione. Questi
algoritmi sono utilizzati per trovare soluzioni a problemi complessi attra-
verso la generazione iterativa di popolazioni di candidati e l’applicazione di
operatori genetici. L’idea alla base di un GA è modellare il processo evolu-
tivo biologico, in cui gli individui più adatti sopravvivono e si riproducono,
trasmettendo le loro caratteristiche alla prossima generazione.
18
CAPITOLO 3. EVOLUTIONARY COMPUTATION
3.1.1 Schema generale
L’evoluzione può essere riassunta dal seguente schema:
• Una popolazione di individui compete per delle risorse limitate.
• La popolazione è dinamica, gli individui muoiono e altri nascono.
• Gli individui più adatti sopravvivono e si riproducono più degli altri.
• La prole eredita alcuni tratti dai genitori.
Dove un individuo indica una soluzione candidata per il problema in esa-
me. Esempi di individuo possono essere un programma in un determinato
linguaggio di programmazione, un insieme di parametri numerici, una strin-
ga e così via. Questa rappresentazione, che definisce come sono visti da un
osservatore esterno, viene detta fenotipo. La rappresentazione interna viene
invece detta genotipo, e rappresenta la codifica di un individuo. Classiche
rappresentazioni sono stringhe di bits di lunghezza fissa o variabile, alberi
sintattici, o più in generale una qualsiasi struttura che rappresenti le infor-
mazioni necessarie per descrivere un individuo. Queste due rappresentazione
sono fortemente legate, infatti il fenotipo è la manifestazione esterna del ge-
notipo. Sono necessarie entrambe le rappresentazioni in quanto il fenotipo
esprime la risoluzione del problema in una versione interpretabile, mentre il
genotipo è necessario per la manipolazione degli individui da parte dell’al-
goritmo di ottimizzazione genetico. Per poter rappresentare un fenotipo in
genotipo e viceversa è necessario specificare una funzione che permetta tale
trasformazione. Infine, la qualità di un individuo viene decisa in base ad
una funzione di fitness: essa valuta quanto il fenotipo di una certa soluzione
sia buono, senza considerare il genotipo, associandovi un valore numerico
in R. In linguaggio naturale, essa può essere descritta come l’abilità di un
individuo di risolvere il problema. Un problema di ottimizzazione in questo
ambito può allora essere visto come argmaxx∈Sf(x) o argminx∈Sf(x) con
f : S → R funzione di fitness e S insieme delle soluzioni per un problema.
Dalla definizione iniziale si può dedurre che la popolazione è dinamica,
la grandezza è fissata e quindi la risorsa per cui gli individui competono è un
posto all’interno della popolazione. Bisogna allora determinare come avviene
la sostituzione degli individui, detto modello generazionale.
3.1.2 Modello generazionale
Data una popolazione di m individui, ne vengono generati altri n derivanti
da quelli già presenti nella popolazione. Quando bisogna decidere quali ele-
menti mantenere, bisogna anche definire il tipo di popolazione, cioè con o
senza sovrapposizione. Sapendo che il tempo è scandito dalle nascite/morti,
possiamo definire i seguenti schemi. Nel caso di modello generazionale con
sovrapposizione, ad ogni intervallo di tempo:
19
CAPITOLO 3. EVOLUTIONARY COMPUTATION
1. Generazione di n figli partendo dagli m genitori.
2. Nuova popolazione data da n + m individui (unione di figli e genitori).
3. Selezione di m individui che sopravvivranno.
Se invece viene scelto il caso senza sovrapposizione, ad ogni intervallo di
tempo:
1. Generazione di n figli partendo dagli m genitori, con n ≥ m.
2. Selezione di m individui che sopravvivranno.
Si noti che la condizione imposta su n ed m è necessaria affinché la popo-
lazione rimanga di dimensione fissata m. Inoltre, come il nome suggerisce,
in questo caso tutti i genitori muoiono. Di seguito alcuni casi comuni per la
scelta dei parametri:
• n = m con sovrapposizione.
• n = m senza sovrapposizione.
• n = 1 con sovrapposizione, detto steady state.
Si può definire una generazione misurando il tempo che scorre come numero
di nascite rispetto alla dimensione della popolazione m.
Per selezionare quali individui sopravvivono e quali genitori si riproduco-
no per originare i nuovi individui, vengono ora introdotti i principali criteri
di selezione. Il metodo più banale e meno efficace, poiché non tiene conto
delle differenze di adattamento tra gli individui, è quello della selezione uni-
forme. Ogni individuo ha una probabilità uguale di essere selezionato come
genitore per la produzione di nuovi individui nella generazione successiva.
Ovvero, non ci sono preferenze o pesi associati agli individui in termini di
quanto sono adatti a risolvere il problema. Questo significa che anche gli
individui meno adeguati hanno una possibilità di essere selezionati come ge-
nitori. Gli individui più idonei possono essere scelti meno frequentemente
rispetto agli individui meno adatti, il che potrebbe rallentare la convergenza
dell’algoritmo verso soluzioni migliori.
La selezione per troncamento, è basata sul concetto di elitismo. In que-
sto metodo, anziché assegnare la probabilità di selezione a ciascun individuo
come avviene nella selezione proporzionale alla fitness, si selezionano diret-
tamente i primi N individui con la valutazione di adattamento più alta,
dove N è un numero prefissato o calcolato in base a una percentuale della
popolazione totale. Questo tipo di selezione presenta alcune caratteristiche
interessanti:
20
CAPITOLO 3. EVOLUTIONARY COMPUTATION
• Pressione selettiva: Questo metodo favorisce la selezione di individui
con fitness migliore. Poiché solo i migliori individui sono selezionati, la
popolazione successiva avrà una concentrazione maggiore di buone so-
luzioni, aumentando la pressione selettiva verso il miglioramento delle
soluzioni.
• Perdita di diversità: Tuttavia, la selezione per troncamento può por-
tare a una rapida perdita di diversità genetica. Poiché solo i migliori
individui vengono selezionati, di conseguenza, le regioni dello spazio
delle soluzioni che potrebbero contenere soluzioni promettenti ma meno
adattate vengono ignorate.
• Convergenza precoce: A causa della pressione selettiva elevata, c’è
il rischio che l’algoritmo possa convergere prematuramente verso una
soluzione locale ottimale, tralasciando la possibilità di trovare soluzioni
migliori altrove nello spazio delle soluzioni.
Con la selezione proporzionale alla fitness [29], detta anche roulette wheel
selection, il metodo diventa più articolato. Data la fitness di ciascun indi-
viduo, si sceglie casualmente un individuo con probabilità proporzionale al
valore della funzione di fitness. Più quest’ultima sarà alta e più probabilità
avrà quell’individuo di essere selezionato come genitore. Il concetto appena
espresso può essere formulato matematicamente come segue
px,P =
f(x)
P
y∈P f(y)
dove f è la funzione di fitness e P è la popolazione. Questo metodo è sem-
plice da implementare e, in generale, consente una convergenza bilanciata in
quanto è possibile che anche individui meno adatti possono contribuire alla
diversità genetica. Tuttavia nel caso in cui un individuo presenti una fitness
molto più grande rispetto agli altri individui, la diversità potrebbe venire
ridotta a causa della continua selezione di questo individuo. Un migliora-
mento applicabile a questa selezione proporzionale consiste nel valutare gli
individui secondo una classifica piuttosto che rispetto al valore grezzo della
fitness. Questa selezione è chiamata selezione proporzionale al rango. Come
sempre viene calcolata la fitness di ciascun individuo, ma successivamente
gli individui vengono classificati in ranghi in base a questo valore. Ogni ran-
go ha una probabilità fissata di essere selezionato. Ad esempio, l’individuo
con la i-esima fitness migliore verrà selezionato con probabilità m−i+1
rsum
con
m grandezza della popolazione e rsum un valore normalizzante. Poiché la
selezione si basa sui ranghi relativi invece dei valori di fitness assoluti, que-
sto metodo è meno influenzato da variazioni significative nelle valutazioni di
fitness o dal rumore nei dati. Da osservare che questa selezione può essere
usata anche in caso di fitness non numerica.
21
CAPITOLO 3. EVOLUTIONARY COMPUTATION
La metodologia più utilizzata nel GA è la selezione a torneo. Sia t ∈ N+
la dimensione del torneo. Vengono estratti casualmente t individui dalla
popolazione, con reinserimento e viene selezionato l’individuo con fitness
migliore. Apparentemente questo metodo è soggetto a una forte pressione
selettiva, tuttavia al variare di t, tale problema può essere controllato: t
piccolo implica una bassissima pressione selettiva, t grande invece introduce
una grande preferenza verso gli individui migliori. Questa preferenza denota
il comportamento tipico della popolazione a convergere verso gli individui
più adatti, in quanto l’evoluzione si concentra sul migliorare le soluzioni
più promettenti (exploitation), rischiando però di cadere in ottimi locali.
Una pressione selettiva bassa invece permette di tenere da conto anche gli
individui meno adatti, consentendo all’evoluzione di investigare su diverse
soluzioni (exploration), anche se magari non portano a nulla, col rischio
di non ottenere una soluzione ottima. Il bilanciamento tra exploitation e
exploration è un punto molto complesso e importante quando si tratta di
algoritmi genetici.
3.1.3 Riproduzione
Per poter completare il ciclo evolutivo, è necessario che, partendo dalla po-
polazione iniziale, vengano generati nuovi individui che successivamente po-
tranno provare ad entrare nella nuova popolazione. Una prole viene generata
applicando un operatore genetico unario o binario, a partire dal genotipo del
genitore (o genitori nel caso di operatore binario).
• L’operatore unario è detto mutazione, definito come u: G → G
• L’operatore binario è detto crossover, definito come b: G2 → G
Definiti gli operatori genetici, la prole può essere generata in tre diversi
modi. I primi due richiedono il numero di figli da generare n e un peso
associato a ciascun operatore genetico, w1 e w2. Successivamente va deciso
se la generazione deve essere deterministica o stocastica. Nel primo caso, il
primo operatore viene applicato per w1 · n volte, e il secondo operatore per
w2 · n volte, in modo manifesto i genitori vengono scelti secondo il metodo
deciso a priori, ad esempio tra quelli discussi nella Sezione 3.1.2. Se la
generazione è invece stocastica, il processo viene ripetuto n volte e, ad ogni
iterazione, l’operatore genetico viene selezionato casualmente con probabilità
w1 e w2, solitamente il peso del crossover è vicino all’uno, mentre quella di
mutazione è bassa. L’ultimo metodo di generazione consiste nell’applicare
entrambi gli operatori a ciascun nuovo individuo. Ripetendo per n volte,
viene prima applicato il crossover e immediatamente dopo viene applicata
anche la mutazione all’individuo appena creato.
Il metodo più comune per implementare la mutazione per stringhe di bit
è la mutazione probabilistica a flip di bit. Dato il genotipo del genitore gp,
22
CAPITOLO 3. EVOLUTIONARY COMPUTATION
il genotipo del figlio gc è creato partendo da una copia di gp in cui ogni bit
viene negato (0 → 1 e 1 → 0) con probabilità pm. Si supponga di avere un
individuo con genotipo x = x1, . . . , xn dove xi è l’i-esimo bit. Allora, per ogni
i ∈ {1, . . . , n} il bit xi viene invertito con probabilità pm, che solitamente è
definita come pm = 1
n , in modo tale da mutare in media un bit per individuo,
come mostrato in Figura 3.1. Come si può notare, all’aumentare di pm
l’individuo generato somiglierà sempre meno al genitore.
Figura 3.1: Mutazione di un individuo per la generazione di un figlio, in cui un singolo
bit è stato invertito.
Il crossover presenta invece molteplici possibilità; di seguito vengono
elencate le modalità più note che verranno analizzate.
• Crossover a singolo punto.
• Crossover a m punti.
• Crossover uniforme.
• Crossover a singolo punto variabile.
Nel crossover a singolo punto, dati due genitori membri dell’attuale popola-
zione, x = x1, . . . , xn e y = y1, . . . , yn, viene scelto un punto k ∈ {1, . . . , n}
uniformemente, detto punto di taglio. Il genotipo del figlio è creato pren-
dendo dal primo genitore x, i bit prima del punto di taglio, e dal secondo
genitore, y, i bit dopo il punto di taglio. Va osservato che in realtà i figli ge-
nerati sono due, il secondo eredita i bit complementari al primo figlio, come
mostrato in Figura 3.2. Una diretta generalizzazione è possibile scegliendo
più di un punto di taglio, da cui il nome crossover a m punti. Gli m punti
di taglio k1, . . . , km ∈ {1, . . . , n} vengono selezionati come precedentemente
descritto, con la condizione che ki ≤ ki+1∀i ∈ {1, . . . , m−1} in modo tale che
i punti di taglio cadano al più nella stessa posizione, ma mai in una posizione
precedente del genotipo, rispetto all’ultimo punto generato. Il genotipo del
figlio è generato prendendo ordinatamente i bit di un genitore alla volta, fino
ad un punto di taglio, in cui si passa all’altro genitore, e così via. Anche
in questo caso, prendendo i bit complementari possono essere generati altri
figli dallo stesso crossover.
Nel crossover uniforme infine, dati due genitori x = x1, . . . , xn e y =
y1, . . . , yn, per ogni i ∈ {1, . . . , n} con probabilità pc = 0.5 l’i-esimo elemento
del primo figlio sarà il bit xi o yi, in modo tale che in media, metà dei bit
verrà cambiata. Un secondo figlio può essere generato, prendendo il bit non
scelto per il primo figlio, come mostrato in Figura 3.3.
23
CAPITOLO 3. EVOLUTIONARY COMPUTATION
Figura 3.2: Generazione della prole tramite crossover a singolo punto.
Figura 3.3: Generazione della prole tramite crossover uniforme.
Il crossover a singolo punto variabile è una variante del primo metodo
descritto, che viene analizzato per completezza. Anche questo metodo è ge-
neralizzabile a m punti. Il cambiamento principale sta nel fatto che i punti
di taglio possono essere diversi tra i due genitori, il che porta ad un genoti-
po del figlio di lunghezza diversa rispetto a quella dei genitori; la funzione
che mappa genotipo a fenotipo deve quindi permette genotipi di lunghezza
variabile.
Si ritiene necessario fare alcune considerazioni finali sugli operatori ge-
netici. Va notato che il crossover è una cosa completamente diversa dalla
mutazione. Tramite crossover non è possibile ottenere tutte le soluzioni pos-
sibili, in quanto il genotipo derivante è ottenuto partendo dai genotipi dei
genitori, che contengono una determinata soluzione. Si può quindi dedur-
re che il crossover favorisce l’exploitation mentre la mutazione incoraggia
l’exploration. Inoltre, è possibile generalizzare il GA per usare caratteri con-
tenuti in un alfabeto finito di simboli Σ invece che usare unicamente l’insieme
di bit {0, 1}. L’unico cambiamento da apportare è che la mutazione deve se-
lezionare (solitamente casualmente) il nuovo simbolo tra i |Σ| − 1 simboli
disponibili invece che negare un bit.
La popolazione iniziale è solitamente casuale, ma possono essere usati
approcci specifici basati però sulla forma del genotipo. Infine, l’evoluzione
è un processo che termina con l’ottimo, ma in alcune condizioni possono
essere considerate accettabili soluzioni che rispettano una qualche tipo di
condizione, detta criterio di terminazione. Ad esempio dopo un certo numero
di generazioni l’evoluzione può essere fermata, che la soluzione ottima sia
stata trovata o meno. Lo schema tipico di un GA è riassunto in Figura 3.4.
24
CAPITOLO 3. EVOLUTIONARY COMPUTATION
Figura 3.4: Ciclo di evoluzione di un GA.
3.1.4 Problematiche
Per concludere, si evidenziano alcuni problemi in cui l’evoluzione può incor-
rere:
• Diversità [28]: come già detto, la popolazione tende a evolvere verso
gli individui migliori. Se la diversità non è abbastanza, significa che
c’è troppa exploitation e bisogna intervenire tramite meccanismi per
promuovere la diversità, al fine di evitare ottimi locali.
• Eredità variazionale: se la prole è troppo simile ai genitori, anche in
questo caso si ha troppa exploitation, l’evoluzione può portare a ottimi
locali, essere lenta o addirittura inesistente. Se invece la differenza
tra gli individui è troppa, significa che l’evoluzione è completamente
casuale e manca exploitation.
• Espressività: indica se la rappresentazione, ossia il fenotipo, e suffi-
cientemente espressivo. Se l’espressività è bassa, la soluzione ottima
potrebbe non essere rappresentabile o raggiungibile, se invece è troppo
grande il tempo di convergenza potrebbe essere enorme o infinito.
3.2 Genetic Programming
Il Genetic Programming (GP) è una tecnica per far evolvere stocasticamen-
te una popolazione di individui che codificano programmi per calcolatori
elettronici [15]. La principale differenza con il GA consiste nella rappresen-
tazione del genotipo degli individui. La rappresentazione come stringa del
codice sorgente non è ottimale, in quanto non permette di capire a cosa si
sta applicando l’operatore genetico. La scelta più corretta è quella di codifi-
care il genotipo come un albero sintattico, come mostrato in Figura 3.5, e di
conseguenza gli operatori genetici andranno ad operare su sottoalberi. Per la
codifica di un programma come albero bisogna conoscere quali sono i possi-
bili nodi e quali di essi sono terminali. L’insieme dei terminali contiene tutte
25
CAPITOLO 3. EVOLUTIONARY COMPUTATION
le possibili foglie, come ad esempio costanti e variabili, mentre l’insieme dei
funzionali contiene tutti i nodi interni, come operazioni aritmetiche, opera-
tori booleani e costrutti if-else. L’insieme delle primitive, ovvero l’unione
dei sopra menzionati insiemi, deve rispettare la proprietà di chiusura:
• Consistenza di tipo: i terminali e l’output di qualsiasi funzione devono
essere input validi per tutte le restanti funzioni.
• Sicurezza della valutazione: le primitive che a runtime possono fallire,
come ad esempio una divisione per zero, devono essere protette per
evitare errori a runtime.
Inoltre, per trovare una soluzione, essa deve essere rappresentabile, in al-
tre parole l’insieme delle primitive deve essere sufficiente per scrivere una
soluzione. Ad esempio, con numeri reali, variabili e l’insieme di operazioni
{+, −, ∗} si può rappresentare ogni polinomio, ma ciò non è utile se la fun-
zione da trovare è un’esponenziale. Questa proprietà, detta sufficienza, non
è sempre garantita, ma le soluzioni che possono essere trovate dovrebbero
comunque essere una buona approssimazione.
Figura 3.5: Esempio di un albero sintattico rappresentante l’operazione aritmetica 5 +
(2 ∗ 7).
3.2.1 Inizializzazione e selezione
Un’altra differenza importante consiste nel modo in cui la popolazione inizia-
le viene generata. Nel caso di GA con stringhe binarie, è sufficiente scegliere
ogni bit indipendentemente con probabilità uniforme. Gli alberi finiti sono
non numerabili, quindi servono dei metodi più complessi. Il primo è chia-
mato grow e consiste nel selezionare in modo casuale una primitiva presa
dal suo insieme, fino ad arrivare ad una certa profondità massima, dmax.
Una volta raggiunto tale valore fissato, viene selezionato un nodo terminale.
Il secondo metodo è chiamato full. Concettualmente è simile a grow, con
la sola differenza che vengono selezionati solo i simboli funzionali prima di
raggiungere la profondità massima. Ciò implica che i terminali appaiano
solamente nell’ultimo livello dell’albero, portando in generale ad alberi di
dimensione maggiore. Merita ancora citare la combinazione di questi due
26
CAPITOLO 3. EVOLUTIONARY COMPUTATION
metodi, ramped half and half. Viene selezionato casualmente un metodo tra
grow e full, con dmax scelto casualmente tra un valore minimo e massimo.
Solitamente questa metodologia consente di arrivare ad alberi con un buon
grado di diversità.
Il processo di selezione avviene come spiegato nella Sezione 3.1.2, soli-
tamente si usa la selezione a torneo. Per ogni individuo nella popolazione,
che si ricorda essere un programma, il valore della funzione di fitness viene
generalmente assegnato in base al risultato ottenuto sui dati di prova, in
modo tale che gli individui che risolvono meglio il problema, otterranno una
fitness più bassa. In GP infatti, se un individuo risolve perfettamente il pro-
blema, avrà fitness zero. Non sempre l’individuo risulta essere compilabile
e/o eseguibile, in quel caso devono essere previste tecniche per gestire gli
errori.
3.2.2 Operatori genetici
Dopo che si è definito come rappresentare il genotipo dei programmi, gli ope-
ratori genetici sono facilmente descrivibili. La mutazione, nel caso di GP, è
implementabile con diverse tecniche. La prima è detta mutazione del sottoal-
bero, in cui avviene la sostituzione di un sottoalbero selezionato casualmente
con un nuovo sottoalbero generato anch’esso casualmente. Si tratta di una
tecnica aggressiva, che porta a una maggior mutazione, come mostrato in
Figura 3.6. Una versione più semplice è la mutazione a punto, Figura 3.7, in
Figura 3.6: Esempio di mutazione del sottoalbero.
cui avviene la sostituzione di un nodo selezionato casualmente con un nodo
compatibile selezionato casualmente. Vi sono poi altre due mutazioni, il cui
scopo è ridurre la dimensione dell’albero, la prima, la mutazione hoist la
quale permette la sostituzione dell’intero albero con uno dei suoi sottoalberi,
esemplificato in Figura 3.8. La seconda invece, la mutazione a restrizione,
come evidenziato in Figura 3.9, esegue la sostituzione di un sottoalbero sele-
zionato casualmente con un terminale selezionato casualmente. Esiste infine
un ultimo tipo di mutazione, chiamata mutazione a scambio, in cui viene ap-
plicata una permutazione agli argomenti di un funzionale, come raffigurato
in Figura 3.10.
27
CAPITOLO 3. EVOLUTIONARY COMPUTATION
Figura 3.7: Esempio di mutazione a punto.
Figura 3.8: Esempio di mutazione hoist.
Figura 3.9: Esempio di mutazione a restrizione.
Figura 3.10: Esempio di mutazione a scambio.
28
CAPITOLO 3. EVOLUTIONARY COMPUTATION
Per quanto riguarda il crossover, la Figura 3.11 mostra la generazione di
due figli, come nel caso di GA. Partendo dalla rappresentazione ad albero
di due individui della popolazione, per ciascuno viene scelto un punto di
crossover. La prole risulta essere costituita da una copia del primo genitore,
a meno del sottoalbero che inizia dal punto di crossover, che viene sostituito
con il sottoalbero che inizia dal punto di crossover del secondo genitore. Lo
stesso processo viene seguito per il secondo figlio, che però parte da una copia
del secondo genitore.
Figura 3.11: Esempio di crossover; gli alberi dei genitori, a sinistra, sono contraddistinti
da colori diversi.
Per come è stato descritto il GP fino ad ora, si può notare che l’evolu-
zione non è vincolata, ovvero possono essere generati individui non validi,
che contengono grandi porzioni di codice non valido. Questo perché molte
operazioni effettuate sull’albero non cambiano la funzione che quest’ultimo
rappresenta, aumentandone considerevolmente la dimensione senza un mi-
glioramento della fitness. Di conseguenza la valutazione della fitness è lenta,
e quindi anche l’evoluzione. Questi alberi sono detti bloat. Per ovviare a
questo problema, ci sono tre possibili soluzioni, di seguito elencate:
• Limitare la dimensione dell’individuo, tramite una profondità massima
dell’albero o usando operatori che limitano la dimensione della prole.
• Rimuovere le regione che non rappresentano codice valido.
• Punire gli individui che sono troppo grandi, diminuendo la loro fitness.
29
CAPITOLO 3. EVOLUTIONARY COMPUTATION
3.3 Grammatical Evolution
Il Grammatical Evolution (GE) [23] è un approccio di GP più vincolato,
il cui obiettivo però rimane quello di trovare un codice sorgente valido che
risolva un dato problema. Il punto di forza sta però nella rappresentazione
lineare dei programmi, più facile da gestire, combinandola poi indirettamente
con una rappresentazione basata sugli alberi. L’idea è quella di definire una
grammatica, solitamente tramite la Backus Normal Form (BNF), che viene
poi utilizzata per interpretare il genoma lineare come sua derivazione.
3.3.1 Backus Normal Form
Una metasintassi descrive la struttura e la composizione di frasi consentite
essa viene spesso utilizzata per descrivere un linguaggio naturale o un lin-
guaggio di programmazione. La BNF è una notazione metasintattica per
grammatiche libere dal contesto, spesso usata per descrivere la sintassi dei
linguaggi utilizzati nell’informatica, come i linguaggi di programmazione, for-
mati di documenti, set di istruzioni e protocolli di comunicazione. Essa de-
scrive il linguaggio in considerazione come un insieme di regole di produzione.
Una grammatica BNF è una quadrupla (T, N, P, S) dove:
• T è un insieme di simboli terminali.
• N è un insieme di simboli non terminali.
• P è un insieme di regole di produzione.
• S ∈ N è un assioma, ovvero il simbolo iniziale.
Una regola di produzione è definita seconda la seguente sintassi:
simbolo ::= espressione
dove simbolo rappresenta un simbolo non terminale, e espressione con-
siste di una o più sequenze di simboli terminali o non terminali, separate
dal simbolo |, ovvero barra verticale. Il simbolo ::= asserisce che la parte
sinistra deve essere sostituita con una delle espressioni a destra. I simboli che
non appaiono mai sul lato sinistro sono terminali. D’altra parte, i simboli
che appaiono sul lato sinistro non sono terminali e sono sempre racchiusi dal
simbolo . Si consideri il seguente esempio a scopo chiarificatore.
somma ::= numero ’+’ numero
numero ::= ’0’ | ’1’ | ’2’ | ’3’
In questo caso somma è una produzione che rappresenta la somma tra due
numeri, e contiene il non terminale numero, che a sua volta contiene i
primi quattro numeri naturali, che sono terminali. Con questa grammatica è
30
CAPITOLO 3. EVOLUTIONARY COMPUTATION
possibile definire la somma di tutte le combinazioni dei primi quattro numeri
naturali, ma ad esempio non è possibile eseguire la sottrazione o usare numeri
diversi da quelli definiti in numero. Può anche essere costruito un albero
partendo dalle produzioni scelte, in cui tutti i non terminali sono nodi interni,
mentre i terminali sono foglie. Si noti però che se si volesse ottenere il
risultato della somma, è necessario valutare l’espressione. Questo significa
che nel caso di una grammatica che esprime un programma, il risultato può
essere valutato dati degli input specifici.
3.3.2 Da BNF a GE
Tenendo traccia di tutte le espansioni, scelte tra tutte le possibili per i sim-
boli non terminali, nel caso di generazione a partire da una grammatica,
si nota che viene ottenuto un vettore di interi, in cui ogni valore indica la
produzione scelta. Questa sequenza di interi rappresenta il genoma dell’in-
dividuo, che può essere poi reso un albero per permettere la valutazione
ed eventualmente la riproduzione. Un importante fattore su cui porre l’at-
tenzione è che un individuo sarà sicuramente un programma valido poiché
generato a partire da una grammatica che descrive correttamente il proble-
ma e tutte le possibili combinazioni. Siccome ciascuna produzione può avere
una lunghezza diversa dalle altre, il genoma non ha vincoli sull’insieme di
simboli che lo descrive, basta considerare il simbolo scelto modulo lunghezza
della produzione in considerazione. In questo modo gli individui potreb-
bero anche avere lunghezza diverse tra loro, ma l’importante è limitare la
lunghezza massima del genoma, altrimenti l’espansione potrebbe non finire
mai. In quest’ultimo caso basta assegnare un valore di fitness molto alto,
ovvero corrispondente ad un programma molto lontano dalla soluzione del
problema da risolvere. Queste peculiarità lasciano anche intendere che la
mappatura tra genotipo e fenotipo non è uno-a-uno, in quanto più genotipi
possono rappresentare lo stesso fenotipo. Rimangono invariati gli operato-
ri genetici utilizzabili, in quanto la generazione di un programma valido è
demandata alla struttura della grammatica. La definizione delle regole di
produzione rappresenta quindi un’importante tassello per un’evoluzione che
porti a risultati accettabili.
3.4 Genetic Improvement
Con Genetic Improvement (GI) si intende una tecnica genetica, simile a GP,
ma che invece di generare completamente la soluzione del problema, parte da
codice difettoso al fine di convergere verso una sua versione migliorata. La
popolazione iniziale consiste di individui rappresentanti programmi validi,
ma che presentano bachi, eccessivo consumo di tempo, memoria o energia
infine può anche essere usato per estendere le funzionalità già presenti [24].
Le tecniche precedentemente analizzate nella Sezione 3.2.2 per la selezione e
31
CAPITOLO 3. EVOLUTIONARY COMPUTATION
la riproduzione degli individui rimangono valide anche nel GI, come anche
l’utilizzo di GE per l’evoluzione. L’inizializzazione della popolazione, anche
per il GI, può essere fatta essere fatta con tecniche come grow o full, ma
almeno un individuo deve essere inserito come membro della popolazione.
Se la popolazione non viene inizializzata con tali tecniche, il metodo più
semplice per riempire i rimanenti posti nella popolazione è quello di replicare
per m − 1 volte tale individuo, e lasciare che siano gli operatori genetici
a creare diversità. Un altro metodo di inizializzazione, che richiede però
una condizione in più, è avere più individui da cui iniziare, in modo da
dividere la popolazione equamente tra copie di questi. Va notato che, come
nelle altre tipologie di evoluzione, il principale problema risulta essere il
tempo necessario affinché una buona soluzione venga generata. Ciò non è
scontato ed è legato a molteplici fattori, tra cui la complessità del problema,
la funziona di fitness scelta, in quanto vincola la precisione dell’evoluzione,
e nel caso di GE, la definizione della grammatica.
3.4.1 PonyGE2
PonyGE2 [8] è un framework, disponibile al download1 su GitHub, che im-
plementa il concetto di GE in Python. Con l’accortezza di inserire uno o
più programmi rappresentati individui di partenza con cui inizializzare la
popolazione e scegliendo accuratamente gli iper-parametri, il GE può essere
usato anche per il GI. In questo framework è stata implementata anche que-
sta possibilità, permettendo di inserire manualmente i genomi degli individui
da usare come punto di partenza, e automaticamente la popolazione iniziale
sarà equamente costituita da copie di ciascun individuo inserito come base.
Il framework si basa principalmente su una classe, in cui sono definiti
tutti i parametri per un’evoluzione, che possono essere specificati da linea di
comando o, più comodamente in un file txt, in cui ogni riga è costituita dalla
coppia parametro: valore. Alcune di queste righe servono per specificare
la grammatica, i file contenenti le coppie input, output su cui valutare
la fitness degli individui e gli individui da usare come base per il GI. Esula
dallo scopo di questo capitolo la spiegazione di tutti i parametri inseribili,
il loro valore di default e la scelta disponibile per ciascuno di essi: si vuole
piuttosto fornire una visione globale della tipologia di software e i servizi
che mette a disposizione PonyGE2. In Figura 3.12 viene mostrato un file
contenente i parametri impostati per l’evoluzione, pubblicato come esempio
nella repository ufficiale del framework.
Bisogna osservare che PonyGE2, oltre ad essere scritto in Python, basa
anche l’intero GE su questo linguaggio, nonostante ci sia la possibilità di
estenderlo ad altri linguaggi, anche se tale possibilità non viene trattata in
questo studio. L’evoluzione all’interno del framework permette quindi di sin-
1
https://github.com/PonyGE/PonyGE2
32
CAPITOLO 3. EVOLUTIONARY COMPUTATION
Figura 3.12: Esempio di un file di configurazione di un’evoluzione in PonyGE2, presa
dalla repository ufficiale del progetto.
tetizzare o migliorare esclusivamente programmi Python, in quanto la loro
valutazione, per esprimere la fitness, è comodamente realizzabile grazie ad
alcune funzionalità predefinite del linguaggio. Come è noto, questo linguag-
gio, tra le sue specifiche, ha la peculiarità di definire blocchi di codice in base
al livello di indentazione. L’indentazione è rappresentata tramite un nume-
ro consistente di tabs che deve essere uguale all’interno dello stesso blocco
di codice; indentazioni in un punto inaspettato produrranno un errore, così
come indentazioni mancanti o non consistenti. Il framework utilizzato co-
difica in un modo proprio, il livello di indentazione. Per creare un nuovo
blocco di codice, aumentando l’indentazione, va inserita la parentesi graffa
aperta seguita dai due punti {:. Per chiudere un blocco di codice indentato
e poi riportare l’indentazione al livello di indentazione precedente, va sem-
plicemente chiuso il blocco di indentazione con i due punti seguiti da una
parentesi graffa di chiusura :}. In Figura 3.1 viene mostrato un semplice
esempio di funzione con indentazione, mentre in Figura 3.2 si può osservare
la codifica secondo le specifiche di PonyGE2. Questa rappresentazione, per-
mette di specificare il fenotipo più semplicemente e, in fase di valutazione,
verrà ricreato il codice originale a cui assegnare un valore di fitness tramite
la funzione Python exec, che permette di eseguire un qualsiasi codice valido.
def example(n):
if n = 1:
return 1
return n
Figura 3.1: Esempio di codice Python con codifica standard.
33
CAPITOLO 3. EVOLUTIONARY COMPUTATION
def example(n):{:
if n = 1:{:
return 1
:}
return n
:}
Figura 3.2: Esempio di codice Python con codifica usata in PonyGE2.
L’ultima parte, fondamentale per lo sviluppo della tesi, rigurda l’integra-
zione diretta in PonyGE2 di un parser, che permette di ottenere i genomi
di un individuo a partire dal suo fenotipo. Questa risulta essere un’enorme
facilitazione, in quanto è sufficiente usare un qualsiasi codice che rispetta
una determinata grammatica e, tramite questo strumento, vengono generati
i genomi. Questi ultimi verranno poi utilizzati per specificare i membri della
popolazione iniziale nel caso di GI.
34
Capitolo 4
Metodologia
In questo lavoro di tesi viene proposto uno strumento automatizzato1 che
risolve un problema di programmazione in Python, avendo a disposizione
unicamente la sua descrizione testuale, detta prompt, e un insieme di dati
di validazione composti da coppie input e output. Una prima soluzione del
problema in esame viene trovata da un LLM, il cui risultato viene valutato
utilizzando i dati di esempio forniti. In caso il programma Python generato
dal LLM non passi tutti i test forniti dall’utente vengono generati i files di
configurazione dell’evoluzione necessari a PonyGE2, introdotto nella Sezione
3.4.1, per evolvere il programma usando il GI. Questo strumento è scritto in
Python, in modo tale da avere un ecosistema coerente che semplifichi l’inte-
grazione di PonyGE2 al suo interno, e che permetta di valutare facilmente
le risposte dei LLM. Questo strumento si articola di tre fasi principali:
• Ottenimento del codice sorgente rappresentante una possibile soluzio-
ne del problema a partire da un LLM, valutazione della qualità della
risposta e salvataggio dati.
• Generazione dinamica di una grammatica specifica per il problema, a
partire da una versione di base con aggiunte delle nozioni estratte dal
prompt e dal codice generato dal LLM. Questa fase prevede anche la
creazione dinamica dei files di configurazione necessari all’evoluzione
in PonyGE2.
• Miglioramento del codice utilizzato il GI.
I problemi risolvibili tramite questo approccio sono ridotti ai soli che possono
essere codificati con una singola funzione. Il metodo seguito rimane valido
anche per tipologie di codice differente, come ad esempio per il costrutto
class ma, come esposto nella Sezione 4.1, il lavoro si è concentrato solamente
su questo sottoinsieme.
1
https://github.com/damianoravalico/LLMGIpy
35
CAPITOLO 4. METODOLOGIA
4.1 Selezione dei problemi
Per validare questo approccio alla risoluzione di problemi di programmazio-
ne, è stato scelto un rinomato dataset di benchmark, chiamato PSB2 [13].
Questo insieme di problemi è specificatamente pensato per la ricerca su sin-
tetizzazione automatica di programmi, con tecniche come GP. Questa suite
di benchmark contiene 25 problemi generici, presi da corsi universitari in cui
vengono proposti e vari katas2. Questa scelta è dovuta a diverse caratte-
ristiche del dataset, a partire dall’idea alla base della sua creazione, il che
lo rende ideale anche per l’applicazione di GI. Inoltre, è stata resa dispo-
nibile una repository3, che permette l’accesso a un grande quantità di cop-
pie input-output per ciascun problema, ottenibili direttamente da Python,
semplicemente importando la libreria4.
La descrizione testuale di ciascun problema, indispensabile per l’interro-
gazione al LLMs, non è direttamente ottenibile tramite libreria, bensì unica-
mente come file di Fogli Google5. Questa mancanza viene colmata tramite
lo scaricamento del file e la traduzione automatizzata in file csv, in modo
tale da rendere la sua lettura estremamente rapida grazie all’utilizzo della
libreria pandas.
Nella Tabella 4.1 vengono mostrati i dettagli di alcuni problemi, selezio-
nati per la parte di GI, secondo i criteri che verranno esposti nel Capitolo 5.
4.2 Inizializzazione
Il primo passo consiste nell’interrogare un LLM che, dato il prompt in cui
è contenuta la descrizione del problema, prova a risolvere producendo un
codice, non necessariamente valido, corretto o completo. Come preambolo
al prompt dato al LLM, viene inserita una stringa, il cui scopo è quello di
specificare il tipo di codice richiesto, una funzione Python, e incoraggiare
l’inserimento di moduli esterni, al fine di evitare codice logicamente corretto
ma non compilabile a causa di un mancato import. Di seguito viene riportata
la stringa posta prima del prompt contenente la richiesta; essa è scritta in
lingua inglese in quanto tutte le interrogazioni ai LLMs vengono eseguite in
questa lingua.
”Write a single Python function to solve the following problem
inserting the necessary modules:”
2
Esercizio ripetitivo che aiuta i programmatori a migliorare le proprie abilità in un
ambiente controllato.
3
https://github.com/thelmuth/psb2-python
4
Ottenibile con pip install psb2
5
https://shorturl.at/clMN8
36
CAPITOLO 4. METODOLOGIA
Nome problema Descrizione Input Output
Basement Given a list of integers, what
is the position of the first in-
teger such that the sum of all
integers from the start to it in-
clusive is negative?
Vettore di inte-
ri
Intero
Camel Case Take a string and convert all
of the words to camelCase.
Each group of words is delimi-
tered by “-”, and each group of
words is separated by a space
Stringa con
tutte le lettere
minuscole, gli
spazi bianchi e
“-”
Stringa
Dice Game Peter has an n sided die and
Colin has an m sided die, whe-
re n  m. If they both roll
their dice, what is the proba-
bilitiy that Peter rolls strictly
higher than Colin
Due interi Decimale
Fizz Buzz Given an integer 1 ≤ x ≤
50 000, print “Fizz” if x is di-
visible by 3, “Buzz” if x is di-
visible by 5, “FizzBuzz” if x is
divisible by 3 and 5, and x if
none of the above hold.
Intero Stringa
Snow Day Given 1 int (hours) and 3
floats (current snow, falling
snow, percent melting), deter-
mine how much snow will be
on the ground in the amount
of hours given
Intero e tre de-
cimali
Decimale
Tabella 4.1: Descrizione, tipo di input e di output per i problemi selezionati per il GI
dal dataset PSB2.
37
CAPITOLO 4. METODOLOGIA
Il risultato ottenuto è sempre una stringa di testo ma la sua formattazione è
fortemente legata al modello a cui viene chiesto di eseguire il compito. Per
poter valutare la qualità del codice generato è necessario estrarlo, eliminando
il testo superfluo che può essere stato inserito dal LLM. Assieme al codice
vengono ricavati anche i moduli dichiarati, che siano essi esterni o interni, e
il nome della funzione generata. Questo viene fatto in quanto, affinché una
funzione sia valutabile, deve essere sintatticamente corretta, in altre parole
eseguibile. Solitamente però i pacchetti importati sono dichiarati al di so-
pra della signature della funzione. Essi vengono catturati grazie all’utilizzo
di un’espressione regolare, direttamente applicabile alla risposta del LLM,
in modo tale che l’unica parte ancora da estrarre sia quella che inizia dalla
stringa def, ovvero l’inizio della funzione. Tramite il modulo ast, è possibile
verificare se un codice dato come stringa è valido oppure no. L’estrazione
quindi inizia controllando se la stringa ottenuta dalla risposta del LLM, pre-
sa dal punto in cui compare def, fino alla fine, è un codice valido. Se non
lo è, viene eliminata l’ultima riga e la verifica rieseguita. Ciò è ripetuto fino
all’ottenimento di un codice che supera la verifica. Nel peggiore dei casi, il
risultato di questo elaborazione è la sola signature, a cui viene aggiunta l’i-
struzione pass, in modo tale da rendere il codice valido. L’impiego di questo
algoritmo viene esemplificato applicato ad un codice generato da Alpaca13B
mostrato in Figura 4.1, che porta al risultato di Figura 4.2. Nel primo caso
è evidente che il codice non è completo, in quanto nessuna istruzione viene
inserita nel blocco if, il che rende invalido anche il blocco for, portando
alla rimozione di entrambi. Infine, prima della valutazione del codice, le
importazioni esterne sono aggiunte nuovamente nella funzione. Con l’inseri-
mento di questa semplice procedura, se il modello genera codice incompleto
si è automaticamente protetti. Inoltre, l’obiettivo di questo procedimento,
oltre alla mera estrazione del programma per la sua valutazione, è quello di
massimizzare il numero di futuri individui per la popolazione di GI. Merita
osservare che, nel caso di modelli più grandi, come i GPT, spesso il codice
viene automaticamente posizionato all’interno di un blocco. Ad esempio,
nel caso di ChatGPT e GPT4, il blocco inizia con la dicitura “‘python e
termina con “‘; in questo caso l’estrazione può essere velocizzata, usando
solamente delle espressioni regolari e non c’è bisogno di reinserire i pacchetti
importati. Ulteriori elaborazioni sono apportate al codice, come la rimozione
di commenti (a singola e multi linea) e l’eliminazione di righe vuote; la mo-
tivazione risiede nell’intenzione di ridurre il numero di strutture da definire
nella grammatica necessaria all’evoluzione.
def find_negative(nums): # Define a function called
find_negative with nums as input parameter.
neg = -1
for i in range(len(nums)): # Iterate through
each element in the array and calculate its
38
CAPITOLO 4. METODOLOGIA
value.
if (sum([i] + [neg])  0)  (neg == -1):
# Check whether the current number plus
the previous one gives a negative result.
If so , set the variable ’neg’ to this
index.
Figura 4.1: Esempio di risposta generata da Alpaca13B. Si può osservare che
il codice risulta incompleto a livello sintattico il che lo rende non compilabile.
def find_negative(nums): # Define a function called
find_negative with nums as input parameter.
neg = -1
Figura 4.2: Elaborazione del codice di Figura 4.1 tramite rimozione
del codice incompleto per consentirne la compilazione con conseguente
valutazione.
Una volta che il codice è disponibile, con l’uso di funzioni Python spe-
cifiche per esecuzione runtime di codice sotto forma di stringa, avviene il
processo di valutazione. La funzione generata viene definita tramite exec,
che effettua il parsing della stringa. Successivamente, usando eval che ri-
ceve come parametro un’espressione, in questo caso il nome della funzione,
viene creato un oggetto che riferisce alla funzione, così da poterla chiamare
a piacimento. Vengono poi raccolti i risultati dell’esecuzione della funzione,
passando come argomenti i valori di input, e confrontando il risultato del-
la funzione con l’output atteso. Chiaramente una valutazione più precisa
è data da un numero sufficientemente grande di coppie input-output, tale
da coprire ogni possibile risultato e i casi particolari. Per migliorare questo
aspetto, con il modulo di PSB2 è possibile specificare la quantità di dati per
ogni problema: il numero di coppie di validazione scaricate di default è 1000,
lo stesso usato anche per i test sul codice ottenuto dal LLM.
Conclusa la valutazione del codice, i risultati sono salvati in una cartella
apposita e, per ciascuna risposta del LLM, viene creato un file JSON. La
struttura del file viene mostrata in Figura 4.3, escludendo alcuni campi che
verranno discussi nella Sezione 4.3.2. Le prime chiavi del file JSON servono
a mantenere una traccia sui dati generici del problema e la configurazione
dell’esecuzione, come il modello, mentre il campo data è un vettore, esso
contiene interi elementi, ciascuno rappresentate un’iterazione del problema.
Questa scelta è stata fatta perché il software proposto può essere configura-
to in modo tale da ottenere più risposte per lo stesso problema, visto che la
generazione dei LLM non è sempre uguale. In questo modo, in ottica di mi-
glioramento del codice, si hanno a disposizione più individui, il che favorisce
la diversità della popolazione. Ogni iterazione ha la sua risposta dal mo-
dello, le importazioni di moduli, che possono anche non esserci, l’estrazione
39
CAPITOLO 4. METODOLOGIA
di nome e codice della funzione e infine, la chiave tests_results indica le
prestazioni della funzione in termini di numero di test passati, non passati e
test che non sono stati effettuati a causa di una qualche eccezione verificatasi
durante la valutazione. In caso di problemi di definizione della funzione, an-
che questo errore viene riportato. Da notare che alla fase di test è assegnato
un tempo massimo, con valore di default di 60 secondi, superati i quali il
test viene annullato e l’errore riportato. Questo limite è fondamentale in
quanto il codice generato dal modello potrebbe contenere cicli infiniti o mol-
to lunghi, ricorsioni senza fine e così via. In questo modo l’intera esecuzione
viene protetta e nel peggiore dei casi, sarà solamente necessario più tempo.
L’implementazione di questo meccanismo di sicurezza utilizza più processi
ed impone il limite su ciascuno di essi, in modo tale da aumentare anche
il parallelismo, diminuendo inoltre il tempo necessario alla valutazione, che
viene fatta solamente in seguito all’acquisizione della riposta da parte del
LLM per il numero di volte definito. Alla conclusione della valutazione di
ciascuna iterazione, si prosegue con il problema successivo, se disponibile.
{
model_name: ...,
problem_name: ...,
prompt: ...,
problem_index: 0,
data_test_size: 0,
data: [
{
iteration: 0,
model_response: ...,
imports: [],
function_name: ...,
code: ...,
tests_results: {
passed: 0,
not_passed: 0,
with_exception(s): 0
}
}
]
}
Figura 4.3: Esempio di file JSON creato dopo la valutazione dell’output di
un LLM. I valori delle chiavi rappresentano solamente il tipo di dato.
Questo strumento, eseguibile tramite linea di comando, necessita di al-
cuni parametri dell’esecuzione, definibili tramite argomenti, che sono:
40
CAPITOLO 4. METODOLOGIA
• --model definisce il LLM da usare, tra quelli descritti in precedenza
nella Sezione 2.3. Se non specificato, l’esecuzione è annullata.
• --dataset permette di specificare il dataset da usare per i problemi e
da cui attingere i relativi valori di test. Al momento, è implementato
solo PSB2, ma il codice permette l’introduzione di altri dataset. Se
non specificato, l’esecuzione è annullata.
• --data_size ovvero il numero di coppie input-output su cui effettuare
la validazione del codice. Come già detto presenta un valore di default
e può quindi essere omesso.
• --iterations permette di ripetere la richiesta al LLM per più volte.
Di default il valore è di 5 ripetizioni, aumentato a 10 per l’ottenimento
dei risultati proposti nel Capitolo 5.
Alla conclusione dell’esecuzione viene restituito il percorso assoluto della
cartella che contiene i risultati, ovvero tanti files JSON quanti sono i problemi
del dataset; per fare un esempio, nel caso di PSB2 saranno 25.
4.3 Generazione dinamica dei files di evoluzione
Vi sono ulteriori due parametri di esecuzione, entrambi opzionali, che pos-
sono essere impostati. Il primo è --with_impr_files che, se specificato,
istruisce il programma in modo tale che, dopo aver concluso la creazione dei
files JSON per ciascun problema, generi anche i files necessari per applicare
il GI, partendo dalla cartella in cui sono presenti i risultati dell’esecuzione.
Il secondo invece, --jsons_dir, è inserito nel caso in cui si voglia generare
i file ma a partire da una precedente esecuzione, inserendo il percorso asso-
luto della cartella che contiene i files JSON. Entrambi i parametri hanno lo
stesso scopo, ma consentono la generazione dinamica dei files necessari ad
un’evoluzione tramite GI, che sono:
• Grammatica BNF con cui guidare l’evoluzione.
• Una cartella con un file txt per ogni individuo (rappresentato come
genotipo) che comporrà la popolazione iniziale.
• Un file txt contenente i parametri per il GI, come richiesto per l’utilizzo
di PonyGE2.
4.3.1 Grammatica
La grammatica necessaria per il GI combinato al GE, rappresenta un tasto
dolente di questo tipo di evoluzione. La dimensione di una grammatica intac-
ca il tempo necessario all’evoluzione, all’aumentare della stessa aumentano
41
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement
Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement

More Related Content

Similar to Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement

Definizione e sviluppo di un algoritmo genetico multiobiettivo per problemi d...
Definizione e sviluppo di un algoritmo genetico multiobiettivo per problemi d...Definizione e sviluppo di un algoritmo genetico multiobiettivo per problemi d...
Definizione e sviluppo di un algoritmo genetico multiobiettivo per problemi d...
Stefano Costanzo
 
Analisi di prestazione dell'interprete tuProlog su piattaforma Java - Tesi
Analisi di prestazione dell'interprete tuProlog su piattaforma Java - TesiAnalisi di prestazione dell'interprete tuProlog su piattaforma Java - Tesi
Analisi di prestazione dell'interprete tuProlog su piattaforma Java - Tesi
MicheleDamian
 
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...daniel_zotti
 
Classificazione di frasi in linguaggio naturale per il riconoscimento di inte...
Classificazione di frasi in linguaggio naturale per il riconoscimento di inte...Classificazione di frasi in linguaggio naturale per il riconoscimento di inte...
Classificazione di frasi in linguaggio naturale per il riconoscimento di inte...
Marco Virgo
 
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...maik_o
 
Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...
Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...
Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...
Nicola Timeus
 
Un componente NER Multi-language per il TermExtractor
Un componente NER Multi-language per il TermExtractorUn componente NER Multi-language per il TermExtractor
Un componente NER Multi-language per il TermExtractor
skasof
 
Art Everywhere: progetto per workshop Google. Sviluppo di sistemi di pattern ...
Art Everywhere: progetto per workshop Google. Sviluppo di sistemi di pattern ...Art Everywhere: progetto per workshop Google. Sviluppo di sistemi di pattern ...
Art Everywhere: progetto per workshop Google. Sviluppo di sistemi di pattern ...
Francesco Cucari
 
Rilevamento di facce in flussi video per l'ausilio ai non vedenti - Tesi
Rilevamento di facce in flussi video per l'ausilio ai non vedenti - TesiRilevamento di facce in flussi video per l'ausilio ai non vedenti - Tesi
Rilevamento di facce in flussi video per l'ausilio ai non vedenti - Tesi
temp temp
 
Tesi Specialistica - L'ottimizzazione delle risorse della Grid di EGEE median...
Tesi Specialistica - L'ottimizzazione delle risorse della Grid di EGEE median...Tesi Specialistica - L'ottimizzazione delle risorse della Grid di EGEE median...
Tesi Specialistica - L'ottimizzazione delle risorse della Grid di EGEE median...
Davide Ciambelli
 
Studio e realizzazione di un sw per la gestione dei profili e delle versioni ...
Studio e realizzazione di un sw per la gestione dei profili e delle versioni ...Studio e realizzazione di un sw per la gestione dei profili e delle versioni ...
Studio e realizzazione di un sw per la gestione dei profili e delle versioni ...artemedea
 
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Francesco Komauli
 
Extended summary of code building genetic programming
Extended summary of code building genetic programmingExtended summary of code building genetic programming
Extended summary of code building genetic programming
MartinaMaione1
 
Tesi Todone
Tesi TodoneTesi Todone
Tesi Todone
guestb31690c
 
Generazione automatica diagrammi di rete con template pptx
Generazione automatica diagrammi di rete con template pptxGenerazione automatica diagrammi di rete con template pptx
Generazione automatica diagrammi di rete con template pptx
GiacomoZorzin
 
Utilizzo dei processi aziendali per la co simulazione di modelli dinamici
Utilizzo dei processi aziendali per la co simulazione di modelli dinamiciUtilizzo dei processi aziendali per la co simulazione di modelli dinamici
Utilizzo dei processi aziendali per la co simulazione di modelli dinamici
Besian Pogace
 
Un sistema di persistenza per motori di workflow business-oriented BPMN
Un sistema di persistenza per motori di workflow business-oriented BPMNUn sistema di persistenza per motori di workflow business-oriented BPMN
Un sistema di persistenza per motori di workflow business-oriented BPMNAlessandro Segatto
 

Similar to Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement (20)

Definizione e sviluppo di un algoritmo genetico multiobiettivo per problemi d...
Definizione e sviluppo di un algoritmo genetico multiobiettivo per problemi d...Definizione e sviluppo di un algoritmo genetico multiobiettivo per problemi d...
Definizione e sviluppo di un algoritmo genetico multiobiettivo per problemi d...
 
tesi
tesitesi
tesi
 
Analisi di prestazione dell'interprete tuProlog su piattaforma Java - Tesi
Analisi di prestazione dell'interprete tuProlog su piattaforma Java - TesiAnalisi di prestazione dell'interprete tuProlog su piattaforma Java - Tesi
Analisi di prestazione dell'interprete tuProlog su piattaforma Java - Tesi
 
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...
Progettazione e sviluppo di un'applicazione web per la gestione di dati di at...
 
Classificazione di frasi in linguaggio naturale per il riconoscimento di inte...
Classificazione di frasi in linguaggio naturale per il riconoscimento di inte...Classificazione di frasi in linguaggio naturale per il riconoscimento di inte...
Classificazione di frasi in linguaggio naturale per il riconoscimento di inte...
 
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...
Progetto e sviluppo di un'applicazionemobile multipiattaforma per il supporto...
 
Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...
Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...
Segmentazione automatica di immagini di mosaici tramite tecniche di calcolo e...
 
Un componente NER Multi-language per il TermExtractor
Un componente NER Multi-language per il TermExtractorUn componente NER Multi-language per il TermExtractor
Un componente NER Multi-language per il TermExtractor
 
Art Everywhere: progetto per workshop Google. Sviluppo di sistemi di pattern ...
Art Everywhere: progetto per workshop Google. Sviluppo di sistemi di pattern ...Art Everywhere: progetto per workshop Google. Sviluppo di sistemi di pattern ...
Art Everywhere: progetto per workshop Google. Sviluppo di sistemi di pattern ...
 
Rilevamento di facce in flussi video per l'ausilio ai non vedenti - Tesi
Rilevamento di facce in flussi video per l'ausilio ai non vedenti - TesiRilevamento di facce in flussi video per l'ausilio ai non vedenti - Tesi
Rilevamento di facce in flussi video per l'ausilio ai non vedenti - Tesi
 
Tesi Specialistica - L'ottimizzazione delle risorse della Grid di EGEE median...
Tesi Specialistica - L'ottimizzazione delle risorse della Grid di EGEE median...Tesi Specialistica - L'ottimizzazione delle risorse della Grid di EGEE median...
Tesi Specialistica - L'ottimizzazione delle risorse della Grid di EGEE median...
 
TesiEtta
TesiEttaTesiEtta
TesiEtta
 
Studio e realizzazione di un sw per la gestione dei profili e delle versioni ...
Studio e realizzazione di un sw per la gestione dei profili e delle versioni ...Studio e realizzazione di un sw per la gestione dei profili e delle versioni ...
Studio e realizzazione di un sw per la gestione dei profili e delle versioni ...
 
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
 
Extended summary of code building genetic programming
Extended summary of code building genetic programmingExtended summary of code building genetic programming
Extended summary of code building genetic programming
 
Tesi Todone
Tesi TodoneTesi Todone
Tesi Todone
 
Tesi Tamiazzo09
Tesi Tamiazzo09Tesi Tamiazzo09
Tesi Tamiazzo09
 
Generazione automatica diagrammi di rete con template pptx
Generazione automatica diagrammi di rete con template pptxGenerazione automatica diagrammi di rete con template pptx
Generazione automatica diagrammi di rete con template pptx
 
Utilizzo dei processi aziendali per la co simulazione di modelli dinamici
Utilizzo dei processi aziendali per la co simulazione di modelli dinamiciUtilizzo dei processi aziendali per la co simulazione di modelli dinamici
Utilizzo dei processi aziendali per la co simulazione di modelli dinamici
 
Un sistema di persistenza per motori di workflow business-oriented BPMN
Un sistema di persistenza per motori di workflow business-oriented BPMNUn sistema di persistenza per motori di workflow business-oriented BPMN
Un sistema di persistenza per motori di workflow business-oriented BPMN
 

Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement

  • 1. UNIVERSITÀ DEGLI STUDI DI TRIESTE DIPARTIMENTO DI INGEGNERIA E ARCHITETTURA Corso di Laurea Magistrale in Ingegneria Elettronica e Informatica Progettazione e Sviluppo di un Sistema per Migliorare il Codice Generato da un Large Language Model Tramite Genetic Improvement Laureando Relatore Damiano Ravalico Prof. Andrea De Lorenzo Correlatore Dott. Giovanni Pinna Anno Accademico 2022/2023
  • 2.
  • 3. Indice Introduzione iii 1 Stato dell’arte 1 1.1 Generazione di programmi . . . . . . . . . . . . . . . . . . . . 1 1.2 Intelligenza artificiale . . . . . . . . . . . . . . . . . . . . . . . 2 2 Large Language Model 3 2.1 Artificial Neural Network . . . . . . . . . . . . . . . . . . . . 3 2.1.1 Definizione . . . . . . . . . . . . . . . . . . . . . . . . 3 2.1.2 Addestramento . . . . . . . . . . . . . . . . . . . . . . 4 2.1.3 Recurrent Neural Network . . . . . . . . . . . . . . . . 6 2.2 Tecnologia attuale . . . . . . . . . . . . . . . . . . . . . . . . 9 2.2.1 Attention Mechanism . . . . . . . . . . . . . . . . . . 9 2.2.2 Transformer . . . . . . . . . . . . . . . . . . . . . . . . 11 2.3 Modelli . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.3.1 LLaMA . . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.3.2 Alpaca . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.3.3 ChatGPT e GPT-4 . . . . . . . . . . . . . . . . . . . . 16 3 Evolutionary Computation 18 3.1 Genetic Algorithm . . . . . . . . . . . . . . . . . . . . . . . . 18 3.1.1 Schema generale . . . . . . . . . . . . . . . . . . . . . 19 3.1.2 Modello generazionale . . . . . . . . . . . . . . . . . . 19 3.1.3 Riproduzione . . . . . . . . . . . . . . . . . . . . . . . 22 3.1.4 Problematiche . . . . . . . . . . . . . . . . . . . . . . . 25 3.2 Genetic Programming . . . . . . . . . . . . . . . . . . . . . . 25 3.2.1 Inizializzazione e selezione . . . . . . . . . . . . . . . . 26 3.2.2 Operatori genetici . . . . . . . . . . . . . . . . . . . . 27 3.3 Grammatical Evolution . . . . . . . . . . . . . . . . . . . . . 30 3.3.1 Backus Normal Form . . . . . . . . . . . . . . . . . . . 30 3.3.2 Da BNF a GE . . . . . . . . . . . . . . . . . . . . . . 31 3.4 Genetic Improvement . . . . . . . . . . . . . . . . . . . . . . . 31 3.4.1 PonyGE2 . . . . . . . . . . . . . . . . . . . . . . . . . 32 i
  • 4. INDICE 4 Metodologia 35 4.1 Selezione dei problemi . . . . . . . . . . . . . . . . . . . . . . 36 4.2 Inizializzazione . . . . . . . . . . . . . . . . . . . . . . . . . . 36 4.3 Generazione dinamica dei files di evoluzione . . . . . . . . . . 41 4.3.1 Grammatica . . . . . . . . . . . . . . . . . . . . . . . . 41 4.3.2 Genotipi e files di configurazione del GI . . . . . . . . 44 5 Risultati 47 5.1 Risposte dei LLMs . . . . . . . . . . . . . . . . . . . . . . . . 48 5.1.1 Problemi legati alla descrizione testuale dei problemi . 50 5.2 Applicazione del GI . . . . . . . . . . . . . . . . . . . . . . . . 52 5.2.1 Alpaca . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 5.2.2 ChatGPT e GPT-4 . . . . . . . . . . . . . . . . . . . . 57 5.2.3 Considerazioni finali e tempi di esecuzione . . . . . . . 65 Conclusioni 69 ii
  • 5. Introduzione I Large Language Models (LLMs) sono sistemi di intelligenza artificiale che hanno dimostrato notevoli capacità nella comprensione e generazione di te- sto in risposta a domande espresse in linguaggio naturale. Negli ultimi anni, hanno guadagnato crescente importanza in vari settori, sia scientifici che pra- tici, grazie alla loro capacità di accelerare e semplificare numerosi compiti. Molti di questi sono difficilmente automatizzabili o la cui risoluzione è limi- tata alla sfera umana, come ad esempio la generazione del codice sorgente per i programmi software. L’utilizzo di questi modelli può fornire un valido supporto agli sviluppatori quando i requisiti sono chiaramente definiti e cir- coscritti. Tuttavia, in situazioni in cui i requisiti sono vaghi o il problema è estremamente complesso da codificare, il codice generato può risultare errato o incompleto. Ciò è dovuto alla limitazione intrinseca di questi modelli, che basano le loro risposte su dati precedentemente osservati. Il presente lavoro di tesi si propone di sviluppare una metodologia, e presentarne l’implementazione, allo scopo di assistere gli utenti nel risolve- re problemi di programmazione nel linguaggio Python in modo automatico tramite Large Language Model (LLM). Tuttavia, il contributo distintivo di questa ricerca risiede nell’ottimizzazione ulteriore dell’output ottenuto, sfrut- tando approcci basati sul Genetic Improvement (GI), al fine di proporre un risultato il più accurato possibile. Il processo di risoluzione del problema si basa sulla sua descrizione in linguaggio naturale e su esempi forniti dall’u- tente. Lo strumento sviluppato sfrutta una versione iniziale della soluzione data da un LLM a scelta e ne valuta la correttezza rispetto ai valori di in- put e output. Se quest’ultima non soddisfa tutti i requisiti implicitamente definiti tramite queste coppie di valori, vengono utilizzate tecniche di Ge- netic Improvement per raffinare il risultato, eliminando incompletezze e/o correggendo evidenti problemi logici. Questo processo comporta l’utilizzo della Grammatical Evolution (GE) per guidare l’evoluzione, attraverso la definizione di una grammatica che formalizza un linguaggio, in questo caso Python. Un aspetto innovativo proposto in questo lavoro è l’adozione di una tecnica che consente di generare una grammatica in modo dinamico rispetto al problema in analisi, accelerando così il processo evolutivo. Il risultato sarà un programma Python valido che supera il maggior numero possibile di test basati sui valori di esempio, senza richiedere all’utente di scrivere iii
  • 6. INTRODUZIONE manualmente ulteriore codice. Oltre alla definizione della metodologia presentata, essa è stata sperimen- talmente testata per dimostrarne l’efficacia. I risultati ottenuti evidenziano come la scelta del LLM da impiegare per il codice iniziale ha un’influenza evidente sulla qualità della soluzione finale. Modelli di dimensioni maggiori conducono a codici più precisi. Questa caratteristica si traduce in un’effica- cia maggiore del GI e nella semplificazione della scrittura della grammatica necessaria. In generale, il metodo proposto tende sempre a migliorare la soluzione finale, seppur in modo marginale, portando ad una percentuale di test superati più alta. È importante notare infine che la descrizione del problema riveste un ruolo cruciale. Nel Capitolo 1, verrà esaminata la storia delle tecniche conosciute nel contesto della sintesi di programmi software. Seguirà un’analisi delle ricerche condotte in letteratura e dell’implementazione di tali tecniche in software, con particolare attenzione al codice Python. Il capitolo si concluderà con una visione generale dei progetti simili a questo studio che utilizzano LLMs per generare codice, migliorandone il risultato finale. Il Capitolo 2 introdurrà all’argomento dei LLMs, iniziando dalle reti neu- rali, che costituiscono la base dei recenti modelli. Verranno poi esposti alcuni metodi specifici per l’elaborazione del linguaggio naturale basati su queste tecnologia. Infine seguirà una panoramica sull’attuale tecnologia, mostrando alcuni modelli che hanno riscosso molto successo e che verranno poi utilizzati per l’implementazione dello strumento proposto. Il Capitolo 3 proporrà una panoramica sul mondo delle tecniche geneti- che. Verrà offerta una prima descrizione generale di come funzionano questi algoritmi ispirati alla natura, in particolare al processo di evoluzione. Succes- sivamente si tratterà del caso legato alla generazione completa di programmi software, per poi definire il GI, fulcro di questo lavoro. Nel Capitolo 4 verrà descritto il metodo seguito per lo sviluppo del pro- getto. Per validare a metodologia applicata sono stati scelti dei problemi di benchmark, su cui saranno effettuati tutti i test del caso, al fine di mostrare l’efficacia dello strumento. L’intero processo, che parte dai dati forniti dal- l’utente e termina con l’ottenimento di un codice migliorato, verrà descritto per esteso, motivando ogni scelta fatta. Nel Capitolo 5, verrà presentato ciò che è stato ottenuto, ponendo enfasi sulle differenze tra i LLMs e mostrando il miglioramento effettivamente otte- nuto tramite GI. Il lavoro si concluderà con un riassunto dei principali risul- tati ottenuti, delineando le possibili direzioni future di ricerca e sottolineando inoltre alcune caratteristiche di quanto proposto. iv
  • 7. Capitolo 1 Stato dell’arte 1.1 Generazione di programmi La scrittura di programmi software è un’attività complessa e impegnativa, che richiede abilità, conoscenze e competenze specifiche. Il programmatore deve infatti comprendere il problema da risolvere, progettare una soluzio- ne efficace e implementarla in un linguaggio di programmazione. Questo processo può essere molto lungo e laborioso, e può richiedere un notevole investimento di tempo e risorse. Già da diversi decenni, la ricerca si è con- centrata sull’ideare delle tecniche il cui scopo è quello di generare programmi in modo più semplice o automatico. La prima idea di ottimizzare un software fu addirittura prima dell’avvento degli elaboratori elettronici. Essa risale al 1843, quando Luigi Menabrea [22] propose un metodo per generare automa- ticamente il codice per una macchina analitica. In [9] viene invece definito il problema di sintetizzare un circuito a partire da una descrizione matematica. Questo è simile al voler generare automaticamente un programma software, e ha dato origine a un campo di ricerca attivo, noto come sintesi di circuiti. Negli anni ’60 e ’70 del XX secolo, si affermarono due correnti di ricerca per automatizzare il processo di scrittura di codice sorgente: program syn- tesis [12] e program transformation [35]. Nel primo caso l’obiettivo è quello di costruire un intero programma senza un punto di partenza, mentre nel se- condo è quello di trasformarne uno già esistente, raffinandolo. Quest’ultimo viene implicitamente usato dai compilatori, come in [27], allo scopo di mini- mizzare la forma del codice per questioni di efficienza di memoria e tempo, preservandone però la correttezza semantica [5]. Nella sintesi di programmi [19] invece, la costruzione avviene gradualmente, garantendone la correttez- za. Questa tecnica ha destato continuo interesse negli anni a seguire come nei lavori [18] e [3]. 1
  • 8. CAPITOLO 1. STATO DELL’ARTE 1.2 Intelligenza artificiale Un ulteriore metodo teorizzato per primo da Alan Turing [32] consiste nell’e- volvere i programmi, partendo da una popolazione di programmi applicando operazioni analoghe ai processi genetici naturali alla popolazione di pro- grammi. La prima implementazione però risale agli anni ’80, in cui durante una conferenza vengono mostrati dei programmi evoluti [6] tramite linguaggi ideati appositamente. Questa metodologia è chiamata Genetic Programming (GP), nome coniato in [10]. Il GP condivide con il program synthesis lo scopo di costruire da zero un programma funzionante. Le applicazioni sono mol- teplici e spaziano in diversi campi, come ad esempio per modelli finanziari [31], per l’industria dell’acciaio [14] o tutte le applicazioni puramente infor- matiche. Sia la sintesi tradizionale del programma che il GP sono limitati nella dimensione dei programmi che possono generare. Un sottoinsieme di GP è il Genetic Improvement (GI) inizia da un pro- gramma di base, di grandezza arbitraria, permettendo la generazione di pro- grammi più complessi, e ne ottimizza le caratteristiche [16]. Questa tecni- ca può essere applicata a qualsiasi linguaggio di programmazione, come ad esempio C++ [25]. Esistono molti progetti che permettono di eseguire GI di software. In [20] viene presentato un framework, che mostra ottimi risultati nel miglioramento di codice C, Java e Python. Anche i lavori [1] e [4] permet- tono un’evoluzione di programmi Java e Python, ma essi sono pensati per migliorare i tempi di esecuzione o eliminare bachi, non per integrare codice al fine di migliorare le funzionalità del programma. Con [8] è possibile effet- tuare GI aggiungendo materiale genetico, estendendo quindi il programma al fine di renderlo corretto o completarlo, ed è pensato per Python. Un ulteriore approccio moderno per la generazione di codice senza la sua scrittura è tramite utilizzo dei Large Language Models (LLMs). Questi mo- delli di intelligenza artificiale, grazie all’enorme quantità di dati su cui sono stati addestrati, permettono di ottenere programmi, anche di complessità elevata, in molteplici linguaggi di programmazione. In [2] vengono esposti i limiti dei LLMs in questo compito, effettuando dei test su due dataset di ben- chmark. Sono inoltre presenti lavori che combinano la generazione di LLMs con tecniche simili al GI, come in [17]. La parte di evoluzione però è fatta ancora dal LLM, reiterando la richiesta ma modificandone le specifiche. 2
  • 9. Capitolo 2 Large Language Model L’attuale panorama dell’informatica sta subendo un cambiamento grazie al- l’introduzione e all’evoluzione dei cosiddetti Large Language Models (LLMs). Questi modelli, appartenenti all’ambito del Natural Language Processing (NLP), hanno acquisito crescente rilevanza e stanno ridefinendo il modo in cui le applicazioni informatiche interagiscono con il linguaggio umano. Nel corso di questo capitolo, viene esplorato il mondo dei LLMs, ana- lizzando in dettaglio il loro funzionamento. Ci si concentrerà sulla capacità che questi modelli possiedono nel comprendere il contesto e nel generare te- sto inerente. Infine verrà data una panoramica sulla storia e lo sviluppo di queste moderne tecnologie. 2.1 Artificial Neural Network Prima di capire come un LLM viene implementato, è necessario introdurre il concetto di Artificial Neural Network (ANN), in quanto genitore di suddetta tecnologia. 2.1.1 Definizione Le ANN costituiscono una branca dei modelli computazionali di apprendi- mento automatico i quali si ispirano principi dell’organizzazione neuronale scoperti nelle reti neurali biologiche [21]. Una ANN è basata su una collezione di nodi interconnessi, detti neu- roni, che modellano il concetto di neurone presente nei cervelli biologici, seppur in modo semplificato. Anche in questo caso, ogni connessione, rap- presentante una sinapsi (la struttura che consente il passaggio di segnali tra neuroni), permette di inviare e ricevere segnali ai diversi neuroni che presen- tano una connessione tra loro. Il segnale trasmesso da un neurone all’altro è un numero reale che viene elaborato, tramite una funzione non lineare dal ricevente. Ai nodi e alle loro connessioni vengono associati dei pesi che 3
  • 10. CAPITOLO 2. LARGE LANGUAGE MODEL contribuiscono ad aumentare o diminuire la potenza del segnale. Se il se- gnale non supera una certa soglia prefissata allora esso non viene trasmesso ai neuroni successivi. Il segnale ricevuto può essere rappresentato come un vettore x = (x1, x2, . . . , xn) che associa ogni sua componente ad una diversa connessione con altri neuroni. I pesi della rete sono raggruppati nel vettore w = (w1, w2, . . . , wn). Il neurone combina linearmente i segnali di input ed i loro pesi secondo la formula: vj = n X j=1 xj × wj + bk dove bk rappresenta la soglia precedentemente menzionata. vj viene elabo- rato tramite la funzione di attivazione, una funzione che calcola l’output del nodo in base ai suoi input e ai pesi sui singoli input, generando il valo- re di output, nonché input del prossimo layer. Questo procedimento viene effettuato da ogni neurone della rete. Infine è fondamentale notare che i neuroni sono raggruppati in più strati, detti layers che eseguono diverse trasformazioni del segnale, a seconda dello strato. I neuroni di un certo layer sono connessi solamente ai neuroni del layer immediatamente precedente e successivo. Come schematizzato in Figura 2.1, la struttura di una ANN è la seguente: • Input layer: sempre presente, fornisce i dati di input alla rete, rappre- senta quindi il punto di partenza. • Hidden layer: ne possono essere presenti da uno a molti e il loro compito è quello di processare i dati forniti in input. • Output layer: produce il risultato finale. Quando una nuova istanza di dati viene passata attraverso la rete neura- le, essi scorrono attraverso ciascun layer, in una sola direzione, in modo da determinare l’output; questo processo viene detto forward propagation. Al contrario, durante la fase di addestramento, dopo che la fase di forward pro- pagation è stata completata, viene calcolato l’errore commesso dalla rete rispetto all’output desiderato. Successivamente tale errore viene propaga- to all’indietro attraverso l’architettura dell’ ANN viene propagato all’indie- tro nella rete per migliorare la correttezza dell’output; questa fase viene chiamata backforward propagation. 2.1.2 Addestramento L’obiettivo principale di una ANN è quello di imparare a svolgere una spe- cifica attività, come ad esempio il riconoscimento di immagini, la traduzione di testi, la previsione di dati futuri, e molto altro ancora. Prima di poter 4
  • 11. CAPITOLO 2. LARGE LANGUAGE MODEL Figura 2.1: Rappresentazione schematica di una ANN con un solo hidden layer. eseguire correttamente il compito desiderato, la rete va addestrata, fornen- dole degli input, e valutando la correttezza dell’output. L’input, output e definizione di metrica di errore dipendono dal tipo di apprendimento scelto. Di seguito vengono citati i principali metodi di apprendimento automatico, senza entrare nel dettaglio. • Il supervised learning consiste in uno sviluppo del modello attraverso un processo di emulazione. Al modello vengono forniti dei campioni, composti da un valore di input e l’output corrispondente atteso e l’o- biettivo è quello di minimizzare la differenza tra output atteso e output predetto dalla rete. I dati vengono chiamati etichettati, in quanto è nota la corrispondenza input-output. • Nell’unsupervised learning invece sono disponibili soltanto i valori di input, e l’obiettivo è quello di inferire relazioni e/o strutture nei dati. Ossia, si cerca di decodificare la regola statistica che lega gli input. • Infine, nel reinforcement learning, si cerca di realizzare un model- lo in grado di scegliere azioni da compiere per il conseguimento di determinati obiettivi tramite l’interazione con l’ambiente esterno. Questa fase di addestramento comporta l’aggiustamento dei pesi delle connessioni tra i neuroni, e opzionalmente anche quello dei valori di soglia. Tramite questi cambiamenti è possibile migliorare l’accuratezza del risultato dato dal modello, ovvero minimizzando gli errori sulle osservazioni. L’ap- prendimento può essere considerato ultimato quando osservazioni aggiuntive non riducono il valore di errore. 5
  • 12. CAPITOLO 2. LARGE LANGUAGE MODEL 2.1.3 Recurrent Neural Network Le ANN rappresentano una tecnologia estremamente versatile, infatti vengo- no utilizzate per diversi scopi. Esse trovano applicazioni in molteplici ambiti, anche se sono molto conosciute per il loro utilizzo nei campi di computer vision and pattern recognition, classificazione/predizione di dati e Natural Language Processing (NLP). Quest’ultimo è un campo interdisciplinare che combina linguistica, informatica e intelligenza artificiale al fine di consentire ai calcolatori di comprendere, interpretare e generare il linguaggio umano in modo naturale. L’obiettivo principale del NLP è quello di creare modelli e algoritmi che permettano agli elaboratori di interagire con il linguaggio uma- no in modo simile a come lo fanno gli esseri umani. Tale campo si concentra su molteplici aspetti del linguaggio, tra cui: • Elaborazione e generazione di testo. • Analisi sentimentale. • Estrazione di informazioni. • Risposta a domande. Di grande interesse risultano quindi essere le Recurrent Neural Network (RNN), una delle tipologie principali di rete neurale artificiale, nonché uno dei modelli più utilizzati in ambito NLP fino all’avvento di tecnologie più re- centi. Tali reti vengono tipicamente addestrate tramite supervised learning, ma rispetto alle reti descritte nella Sezione 2.1.1, presentano una memoria, detta hidden state, in quanto usano le informazioni passate per influenzare l’input e l’output correnti; non c’è quindi indipendenza tra coppie di input- output, bensì lo stato attuale dipende dagli elementi precedenti all’interno della sequenza. Grazie a questa loro capacità, esse risultano essere applicabi- li a compiti come il riconoscimento vocale, ad esempio negli assistenti vocali o in generale a bot usati per interazione con umani e a predizioni su cosa verrà dopo una data sequenza di parole. A fine esemplificativo, si immagini una sequenza di valori di input x = (x1, x2, . . . , xn) con n arbitrario. Ad ogni step t, la rete produce un output yt ed è caratterizzata da un hidden state ht, che funge da rappresentazione della sottosequenza x1, x2, . . . , xt. ht viene calcolato combinando ht−1 e xt−1, mentre l’output è dato da yt = f(xt, ht−1). Chiaramente vanno considerati anche i pesi della rete, riguardanti le connessioni con il layer precedente e il vettore ht, definiti rispettivamente dalle matrici Wxh e Whh, ottenendo quindi yt = f(Wxh · xt, Whh · ht−1) In Figura 2.2 è illustrato il diagramma rappresentate dei concetti appena esposti. 6
  • 13. CAPITOLO 2. LARGE LANGUAGE MODEL Figura 2.2: Rappresentazione del meccanismo attuato dalle RNN per mantenere nella rete informazioni passate. Alla fine della Sezione 2.1.1 viene introdotto il concetto della backpropa- gation. Si consideri ora, per esempio, il processo di backfoward propagation durante l’addestramento, in una rete a singolo layer: 1. La prima parte consiste nel forward propagation, in cui vengono cal- colati i pesi e l’output. 2. Nell’output layer viene calcolato il gradiente della funzione di costo; essa rappresenta la misura della qualità dell’output proposto dalla rete rispetto a quello atteso. Il gradiente misura quindi il cambiamento nei pesi rispetto alla variazione dell’errore. 3. I pesi vengono aggiornati ripetutamente fino a quando non c’è conver- genza tra output e risultato atteso oppure fino a quando non si verifica un qualche criterio di stop imposto a priori. Durante il processo di addestramento di una RNN, il gradiente può di- ventare troppo piccolo o troppo grande, portando a basse prestazioni, bassa accuratezza e tempi di addestramento molto lunghi. Si parla rispettivamente di vanishing gradient problem e di exploding gradient problem. Allo scopo di analizzare una delle problematiche principali che affliggono le RNN, ver- rà trattato solamente il primo problema. Esso si verifica quando il valore del gradiente calcolato diminuisce ad ogni iterazione. La situazione appena descritta può essere causata da diversi fattori: • Scelta della funzione di attivazione. • Pesi dei primi layers molto piccoli. • Numerosità dei layers. Nel peggiore dei casi, il vanishing gradient problem può portare ad un blocco totale dell’apprendimento. A scopo esemplificativo si consideri la funzione 7
  • 14. CAPITOLO 2. LARGE LANGUAGE MODEL sigmoidea con relativa derivata, rappresentata in Figura 2.3. Si noti come, quando l’input della funzione aumenta o diminuisce, ovvero |x| aumenta, il valore della derivata tende a zero. Supponendo che ci siano n hidden layers con funzione di attivazione sigmoidea, allora n derivate a valore molto basso vengono moltiplicate per ottenere il gradiente, che risulterà decrescere espo- nenzialmente. Un gradiente molto piccolo implica che i pesi e le soglie dei livelli iniziali non verranno aggiornati in modo efficace. Una rete con mol- teplici layers, come nell’esempio appena proposto, viene detta Deep Neural Network (DNN). Figura 2.3: La funzione sigmoidea con al sua derivata. Questo tipo di problema è intrinseco nelle RNN vanilla. Tramite backfor- ward propagation, il termine associato alla memoria della rete può svanire, rendendo la rete incapace di tenere conto del passato. Per ovviare a questa debolezza, sono state introdotte le reti Long Short-Term Memory (LSTM). L’intuizione alla base dell’architettura LSTM è quella di creare un modulo aggiuntivo che sia capace di capire quando ricordare e quando dimenticare le informazioni. In altre parole, la rete apprende effettivamente quali infor- mazioni potrebbero essere necessarie in seguito in una sequenza e quando tali informazioni non sono più necessarie. Ad esempio, nel contesto dell’e- laborazione del linguaggio naturale, la rete può apprendere le dipendenze grammaticali. Tuttavia, nonostante i successi ottenuti dalle ANN e dalle RNN, assieme alle migliorie come le LSTM, queste architetture presentano alcune limita- zioni significative. Nei casi delle ANN, possono emergere problemi legati alla scomparsa e all’esplosione dei gradienti durante il processo di addestra- mento. Tali fenomeni ostacolano l’apprendimento di relazioni intricate e a lungo termine nei dati, limitando la capacità di catturare dipendenze estese all’interno delle sequenze. Inoltre, manca la considerazione della struttura sequenziale innata dei dati, poiché trattano ogni punto della sequenza come se fosse indipendente dagli altri, con il rischio di perdere informazioni cruciali in problemi sequenziali. Inoltre, nonostante le migliorie apportate delle RNN, come le LSTM, nel risolvere il problema delle dipendenze più lontane, permane una limi- 8
  • 15. CAPITOLO 2. LARGE LANGUAGE MODEL tazione fondamentale. La modalità sequenziale nell’elaborazione delle RNN conduce a una parallelizzazione lenta durante l’allenamento, generando oneri computazionali e restrizioni di scalabilità, soprattutto per modelli di grandi dimensioni. Infine, le RNN non sempre riescono a tenere in considerazione le relazioni temporali intricate, specialmente in sequenze lunghe e complesse. Di conseguenza nel tempo è emersa l’esigenza di un nuovo paradigma che superi queste limitazioni e apra nuove prospettive nell’elaborazione delle sequenze. L’introduzione dei Transformers ha rappresentato proprio que- sto avanzamento, ridefinendo il modo in cui le relazioni all’interno dei dati sequenziali vengono modellate e comprese. 2.2 Tecnologia attuale L’evoluzione tecnologica dei Transformers, introdotti in [33], ha portato a una rivoluzione nell’ambito dell’elaborazione del linguaggio naturale. Que- sto progresso è emerso in modo eclatante con l’avvento di modelli Generative Pre-trained Transformers (GPT) [26] e Bidirectional Encoder Representa- tions from Transformers (BERT) [7], che si basano sull’architettura di rete neurale a Transformer. Il principio di base su cui sono costruiti, detto Atten- tion Mechanism, pone l’enfasi sulla parallelizzazione delle operazioni e sulla capacità di catturare relazioni semantiche complesse nei dati testuali, ma non solo. Esso si ispira ai sistemi biologici degli esseri umani che tendono a concentrarsi sulle parti distintive durante la lavorazione di grandi quantità di informazioni. Attraverso questo approccio, i Transformers riescono a pro- cessare simultaneamente diverse parti del testo, catturando contesti profondi e generando in modo coerente e preciso testi di alta qualità. Questa capacità di parallelizzazione e Attenzione ha aperto nuove frontiere nell’intelligenza artificiale, con applicazioni che spaziano dalla traduzione avanzata alla crea- zione di testo coinvolgente, dall’assistenza virtuale all’analisi dei dati su larga scala e molto altro. 2.2.1 Attention Mechanism Il concetto chiave dell’Attention Mechanism è consentire alla rete di appren- dere le relazioni tra i pezzi di una sequenza di input (nota come token). Ad esempio, considerando la frase “il gatto è saltato sopra il cane pigro”, ci aspet- teremmo che la rete impari ad associare “gatto” con “saltato” e “pigro” con “cane”. L’Attenzione può essere applicata a qualsiasi tipo di dato che può es- sere formattato come una sequenza. Ad esempio, può essere utilizzato anche con i dati visuali, in cui un’immagine è rappresentata come una sequenza di correzioni che vengono poi utilizzate come token in una sequenza. Questo meccanismo permette al modello di focalizzarsi su parti specifiche di un input mentre elabora o genera un output. Nell’ambito di interesse di 9
  • 16. CAPITOLO 2. LARGE LANGUAGE MODEL questo studio, l’Attenzione è ciò che consente ai Transformers di compren- dere le relazioni tra le diverse parole o token in un testo. Il meccanismo di Attenzione funziona nel seguente modo: 1. Input e query: l’input consiste in una serie di token, che possono essere parole o frammenti di testo. Per essere elaborato dal modello, ciascun token viene rappresentato come un valore numerico, ottenuto da tecni- che di Embedding (rappresentazione di caratteri, parole, frasi o testo come vettori numerici per consentirne l’elaborazione), che cattura le sue caratteristiche semantiche. Per generare un token nell’output, vie- ne creato un vettore di query associato a quel token. Questo vettore di query è ciò su cui il modello di attenzione si baserà per determinare quali parti dell’input sono rilevanti. 2. Creazione delle query: la query è un vettore numerico che rappresenta il token che si sta cercando di generare nell’output. Questo vettore di query viene generato dalla combinazione di informazioni contestuali e dallo stato interno del modello. Essa è fondamentale perché definisce su cosa il modello sta cercando di porre la sua attenzione mentre decide come generare il token successivo. 3. Calcolo dell’Attenzione: per ogni token di input, il modello calcola un peso di Attenzione che rappresenta quanto quel token è rilevante per la query corrente. Il calcolo di questo peso si basa su una misura di similarità tra la query e il token di input. Una tecnica comune per calcolare questa similarità è l’uso del prodotto scalare tra i vettori di query e token di input. 4. Calcolo dei pesi normalizzati: dopo aver calcolato i pesi di attenzio- ne, vengono normalizzati utilizzando funzioni di attivazione come la Softmax. Questo passaggio trasforma i pesi in una distribuzione di probabilità, assicurando che la somma di tutti i pesi sia uguale a 1. La normalizzazione è importante poiché fa sì che l’Attenzione sia di- stribuita tra i token di input in base alla loro rilevanza relativa per la query. 5. Calcolo degli input ponderati: i pesi normalizzati vengono utilizzati per ponderare le rappresentazioni di input associate a ciascun token. I token più rilevanti ricevono pesi maggiori, mentre i token meno rilevanti ricevono pesi minori. Questo passaggio crea un insieme di input che riflette l’importanza relativa di ciascun token di input per la query. 6. Aggregazione degli input ponderati: infine, le rappresentazioni di in- put ponderate vengono sommate per creare un vettore di output fi- nale. Questo vettore rappresenta l’attenzione complessiva data alla 10
  • 17. CAPITOLO 2. LARGE LANGUAGE MODEL query rispetto all’input. L’output aggregato cattura quindi l’informa- zione chiave dall’input che è rilevante per generare il token corrente nell’output. Il meccanismo di Attenzione consente ai modelli di considerare le connessioni tra le parole in un testo e di dare maggior peso a quelle parole che sono più importanti per il contesto corrente. Questo processo di Attenzione distribui- ta su input diversi è ciò che consente di catturare relazioni complesse e di generare testo coerente e significativo. Si noti che questo modello può essere ulteriormente migliorato combinan- do diverse rappresentazioni di Attenzione indipendenti, tramite il cosiddetto Multi-Head Attention Mechanism. Esso consente ai modelli di cogliere rela- zioni più complesse tra le parole all’interno di una frase o di una sequenza. Invece di dipendere da una singola testa di attenzione, il modello utilizza più Attenzioni per catturare diversi aspetti delle relazioni semantiche tra le parole. Ciascuna calcola un peso di Attenzione differente per ogni parola in base alle sue relazioni con altre parole. Queste diverse prospettive sono quindi concatenate e combinate per ottenere un risultato finale. 2.2.2 Transformer Come menzionato alla fine della Sezione 2.1.3, il problema con i modelli discussi sta nel fatto che le informazioni a lungo termine tendono a essere dimenticate, all’aumentare della lunghezza della sequenza. Teoricamente, le informazioni di un token possono propagarsi molto più in profondità nella sequenza, ma nella pratica, la probabilità che le informazioni vengano con- servate diminuisce in modo esponenziale, man mano che ci si allontana da una parola specifica. Si supponga di voler tradurre una frase: nelle RNN, la frase verrebbe pro- cessata parola per parola, mentre nei Transformers la frase viene elaborata per intera in una sola volta, reiterando il processo di traduzione per più volte al fine di migliorare il risultato. L’architettura originale del Transformer è composta è composta da un Encoder e un Decoder. L’Encoder si occupa di convertire l’input testuale in una rappresentazione contestuale, mentre il Decoder genera una sequenza di output basandosi sulla rappresentazione data dall’Encoder. Con l’analisi successiva si andranno a descrivere ad alto livello l’Encoder e il Decoder. Come mostrato in Figura 2.4, il processo inizia con l’Input Embedding. l’informazione che viene data come input deve essere rappresentata in forma numerica. Tale rappresentazione cattura il significato semantico di ciò che viene incorporato. La proprietà principale degli Embeddings è che, secon- do una qualche metrica definita, sono più simili tra loro se i corrispondenti valori da cui sono stati generati hanno un significato simile o sono contestual- mente correlati. Di conseguenza gli Embeddings consentono di comprendere 11
  • 18. CAPITOLO 2. LARGE LANGUAGE MODEL meglio il significato e le relazioni tra le parole all’interno delle sequenze di testo, facilitando così l’apprendimento e l’elaborazione del modello di intel- ligenza artificiale. Vista la computazione parallela della sequenza di testo, i Transformers non posseggono la nozione di ordine temporale in cui appaio- no le parole, il che comporterebbe una perdita di informazione nell’output. La fase di Positional Encoding serve ad inserire un’informazione posizionale all’interno degli Embeddings. Va osservato che un semplice indice numerico crescerebbe troppo per sequenze grandi, mentre un indice normalizzato cree- rebbe problemi per sequenze di lunghezza variabile. In questa fase quindi, ogni indice delle parole nella frase di partenza viene mappato in un vettore. Il risultato dato da Embeddings e Positional Encoding, è una matrice dove ogni riga rappresenta un oggetto codificato della sequenza sommato alle sue informazioni di posizione. A fini esemplificativi, si supponga di avere una sequenza di lunghezza L e di voler ottenere la codifica del k-esimo elemento; essa è data dalle funzioni seno e coseno a diverse frequenze. PEk,2i = sin k n 2i d PEk,2i+1 = cos k n 2i d dove d è la dimensione dell’Embedding, i è l’indice all’interno dell’Embedding mentre n è una costante arbitraria, fissata a 10 000 dagli autori [33]. Il layer successivo, quello dell’Encoder, è composto da una parte di Multi- Head Attention e da una successiva rete neurale. Il suo compito consiste nel mappare tutte le sequenze di input in una rappresentazione che contiene le informazioni apprese per l’intera sequenza. Come descritto nella Sezione 2.2.1, l’Attention Mechanism consente al modello di associare le parole di input ad altre parole, quelle che dovrebbero rappresentare l’output. Succes- sivamente, i vettori rappresentanti i pesi di Attenzione vengono aggiunti ai vettori posizionali degli Embeddings. Le rappresentazioni contestuali pas- sano poi attraverso la rete neurale, nel tentativo di estrarre più elementi di interesse, aumentando quindi la qualità della rappresentazione. Tutte queste operazioni hanno la finalità di codificare l’input in una rappresentazione che tenga conto delle informazioni estratte dal meccanismo di Attenzione. Ciò faciliterà il modello a concentrarsi sulle parole appropriate nell’input duran- te il processo di decodifica. È possibile eseguire il processo di codifica più volte, in modo tale che ogni head dell’Encoder abbia l’opportunità di appren- dere diverse rappresentazioni, aumentando così potenzialmente la capacità predittiva della rete. Il procedimento continua con il layer di decodifica, che genera la sequenza di testo. Esso presenta una struttura simile a quello dell’Encoder, infatti pre- senta due layers di Multi-Head Attention e un layer di rete neurale. Queste parti si comportano in modo simile rispetto a quello che avviene nell’Enco- der, ma ogni livello di Attenzione ha un compito diverso. Il Decoder presenta 12
  • 19. CAPITOLO 2. LARGE LANGUAGE MODEL Figura 2.4: Architettura originale del modello Transformer. inoltre un classificatore, nel caso in cui il compito del modello consista anche nella classificazione di testo, e un layer Softmax, per ottenere le probabilità associate alle parole. L’input passa attraverso un livello di Embedding e un livello di Positional Encoding. Gli Embedding posizionali vengono inseriti nel primo livello di Multi-Head Attention che calcola i pesi di Attenzione per l’input del Decoder. Poiché il Decoder genera la sequenza parola per parola, è necessario evitare che venga condizionato da token futuri, apparsi successivamente nella sequenza di parole iniziale. Il metodo per evitare che ciò avvenga è quello del mascheramento. La maschera di previsione è una matrice delle stesse dimensioni dei pesi di Attenzione, riempita con valori v ∈ {0, −∞}. In questo modo, quando le matrici dei pesi di Attenzione e quelle relative al mascheramento vengono moltiplicate, si ottiene una nuo- va matrice dei pesi. Essa ha valori diversi da −∞ solamente nel triangolo inferiore, definito dalla diagonale principale, corrispondente alle parole pre- cedenti alla parola in elaborazione. L’Attention Mechanism usa una funzione Softmax per assegnare un peso a ogni token della sequenza di input. I token con un peso negativo vengono azzerati, in modo tale che il modello non si concentri su quelle parole. Segue il secondo livello di Multi-Head Attention, esso abbina l’input del Encoder all’input del Decoder, consentendo al Deco- der di decidere su quale input del Encoder è rilevante concentrarsi. L’output passa poi attraverso una rete neurale per un’ulteriore raffinamento. Infine, il risultato passa attraverso un classificatore, dalle dimensioni pari 13
  • 20. CAPITOLO 2. LARGE LANGUAGE MODEL al numero di classi. Ad esempio, se ci sono 10 000 classi per 10 000 parole, l’output di questo classificatore sarà di dimensione 10 000. L’output viene successivamente inserito in un layer Softmax, che produrrà punteggi di pro- babilità compresi tra 0 e 1. L’indice corrispondete alla probabilità più alta equivale all’indice della parola predetta del modello. In seguito alla predi- zione della parola, il decodificatore provvedere ad aggiungere tale predizione all’elenco dei suoi input. Tale sequenza di eventi continua fino alla decodifica di un token. Più Decoder possono essere usati in concomitanza, in modo da permettere al modello di concentrarsi su diverse combinazioni di Attenzione, migliorando la capacità predittiva. 2.3 Modelli Prima dell’avvento dei Transformers, i modelli a rete neurale più adatti a problemi di Natural Language Processing, utilizzavano anche tecniche di su- pervised learning, avendo a disposizione grosse quantità di dati etichettati. La dipendenza dal apprendimento supervisionato ne limitava l’uso su insiemi di dati che non erano ben etichettati e rendeva anche molto costoso e dispen- dioso in termini di tempo l’addestramento di modelli estremamente grandi. Con l’introduzione dei modelli GPT, il paradigma per l’addestramento di questi modelli cambia rispetto al classico apprendimento supervisionato. Il nuovo metodo di addestramento consiste in una una combinazione di una piccola quantità di dati etichettati dall’uomo, seguito da una grande quan- tità di dati senza etichetta, tale processo viene detto semi-supervised lear- ning. Inizialmente viene creata una prima versione del modello, generando casualmente i parametri iniziali che successivamente vengono migliorati su ciascuno dei task desiderati, nel cosiddetto processo di fine-tuning, in cui i pesi del modello generato precedentemente vengono adattati ai nuovi dati non etichettati [37]. Verranno ora introdotti i modelli che sono stati scelti per questo studio. 2.3.1 LLaMA Il modello Large Language Model Meta AI (LLaMA) [30] sviluppato dalla società Meta AI, rilasciato solo a scopi di ricerca, è stato il primo modello a poter competere con i modelli generativi sviluppati nell’ultimo anno da OpenAI. Esistono diverse varianti del modello che variano per numero di parametri della rete: il modello LLaMA più piccolo presenta 7 miliardi di parametri (7B) fino ad arrivare a 65 miliardi (65B). I modelli linguistici svi- luppati da Meta AI hanno consentito ai ricercatori di utilizzare e addestrare questo modello anche senza grandi risorse computazionali. La presenza di modelli di base più piccoli, come LLaMA, è auspicabile nel ampio spazio dei LLM in quanto richiedono molta meno potenza di calcolo e risorse per testa- re nuovi approcci, convalidare il lavoro di altri ed esplorare nuovi casi d’uso. 14
  • 21. CAPITOLO 2. LARGE LANGUAGE MODEL Dataset Proporzioni Dimensioni CommonCrawl 67.0% 3.3 TB C4 15.0% 783 GB GitHub 4.5% 328 GB Wikipedia 4.5% 83 GB Books 4.5% 85 GB ArXiv 4.5% 92 GB StackExchange 4.5% 78 GB Tabella 2.1: Provenienza dei dati su cui è stato effettuato l’addestramento di LLaMA. Questi modelli sono stati addestrati su un ampio set di dati non etichettati, nel caso di LLaMA circa 1 trilione di tokens, il che li rende ideali per la messa a punto di un’ampia gamma di attività di attività. Seppure questi modelli siano di dimensioni ridotte, merita osservare che, come riportato dalla so- cietà, l’addestramento del modello più grande è durato circa 21 giorni, su un cluster con 2048 GPU A1001 con 80 GB di memoria principale. Si può notare come i costi in tempo e risorse finanziare risultano essere proibitivi per organizzazioni piccole; viene quindi ulteriormente evidenziata l’utilità di questo progetto. LLaMA è stato rilasciato con annesso il codice sorgente per il suo utilizzo tramite inferenza, senza quindi il necessario per permettere il fine-tuning, anche se ciò è senza dubbio possibile. Questa decisione da parte di Meta ha favorito notevolmente il lavoro open-source, infatti questi modelli sono stati poi utilizzati per svariati compiti e sono stati la base di molte loro derivazioni. Un’ulteriore novità introdotta con questi modelli, consiste nella loro creazione partendo da dati pubblicamente disponibili, come mostrato in Tabella 2.1. Questo modello è basato sull’architettura a Transformers, anche se con alcune modifiche che non verranno discusse. Come modello di linguaggio generativo, esso permette la generazione e traduzione di testo, scrittura di contenuti creativi, risposte a domande e generazione di codice sorgente. Per questo studio, LLaMA risulta essere un eccellente candidato, visto che l’allenamento comprende anche GitHub2, famoso sito di hosting per codi- ce. Va osservato che le risorse a disposizione risultano essere limitate, quindi anche per la semplice inferenza, il modello da 13B è il più grande utilizzabile. Il fine-tuning esula dagli scopi prefissati e non verrà quindi trattato. 1 https://www.nvidia.com/en-us/data-center/a100/ 2 https://github.com/ 15
  • 22. CAPITOLO 2. LARGE LANGUAGE MODEL 2.3.2 Alpaca Un’importante variante di LLaMA 7B è Alpaca3, derivante da un fine-tuning. Questo lavoro è stato eseguito utilizzando il processo di self-instruct[34]. Il dataset usato è contenuto in un file JSON da 52.000 di instruction-following data, dove ciascun campione è formato dall’istruzione, una stringa che descri- ve il task, un input opzionale, rappresentante più dettagli sul contesto o dei dati, e l’output, la risposta generata dal modello scelto per la generazione di tali dati. Nel caso dell’addestramento di Alpaca, il modello usato per creare i dati da usare per il fine-tuning è text-davinci-0034, e la distribuzione dei dati a seconda del task, è riportata in Figura 2.5. Va notato che, essendo un progetto open-source, è possibile modificare alcuni parametri del modello. Un LLM prevede molteplici possibili configurazioni, ma è necessario cita- re la temperatura. Questo valore permette di specificare quanto l’output del LLM sia deterministico o casuale. Come descritto precedentemente, un LLM accetta una sequenza di input di token e tenta di prevedere il token successivo. Lo fa generando una distribuzione di probabilità discreta su tutti i possibili token. La temperatura T, che varia nell’insieme [0, 1], modifica questa distribuzione: se T → 1 la distribuzione diventa uniforme, ogni token è equamente probabile, rendendo le risposte più casuali e diverse tra loro a parità di prompt. Al contrario, se T → 0 il modello è totalmente determini- stico, scegliendo sempre il token più probabile. Nell’implementazione usata il valore di questo parametro è stato mantenuto a quello di default, ovvero 0.6, tendente di poco ad essere più creativo. Per questo studio, Alpaca si è dimostrato nettamente più interessante rispetto alla versione di LLaMA rilasciata da Meta AI, grazie alla sua legge- rezza ma al contempo alla precisione maggiore data dal lavoro di fine-tuning. L’inserimento del modello in questo lavoro viene ulteriormente velocizzato grazie a semplificazioni apportate all’implementazione del modello, rese di- sponibili sul sito di HuggingFace5, ove sono presenti le versioni di Alpaca 7B e Alpaca 13B. 2.3.3 ChatGPT e GPT-4 Chat Generative Pre-trained Transformer (ChatGPT) è un chatbot6 svilup- pato dall’azienda OpenAI, che non ha però rivelato molti dettagli implemen- tativi. Questo software ha riportato notorietà al campo dei LLMs, oltre che rappresentare una tecnologia estremamente versatile, grazie alla quantità di dati su cui è stato addestrato. ChatGPT è basato sul modello gpt-3.5-turbo da 175B di parametri, per poi essere sottoposto a un processo di fine-tuning al fine di ottimizzarlo a applicazioni conversazionali. Questo processo di 3 https://crfm.stanford.edu/2023/03/13/alpaca.html 4 https://platform.openai.com/docs/models/gpt-3-5 5 https://huggingface.co/ 6 Software progettato per simulare una conversazione con un essere umano. 16
  • 23. CAPITOLO 2. LARGE LANGUAGE MODEL Figura 2.5: Diversità dei dati utilizzati. Il cerchio interno del grafico rappresenta il verbo radice delle istruzioni e il cerchio esterno rappresenta gli oggetti interessati. perfezionamento ha sfruttato sia il supervised learning che il reinforcement learning in un processo chiamato reinforcement learning from human feed- back. Entrambi gli approcci hanno impiegato esseri umani per migliorare le prestazioni del modello, aumentando l’accuratezza del output finale for- nito all’utente. L’obiettivo era di esporre il modello a una vasta gamma di argomenti, stili di scrittura e voci linguistiche al fine di sviluppare una comprensione generale del linguaggio umano. Questo modello rappresenta uno dei modelli più utilizzati data la sua capacità di interfacciarsi molto bene con gli esseri umani, ed il suo utilizzo è il più semplice possibile sfruttando le API di inferenza messe a disposizione dalla società. Anche questo modello, tra i diversi tipi di dati a cui è stato sottoposto, comprendo codice sorgente di molti linguaggi, ed è quindi idoneo ed interessante ai fini di questo studio. Inoltre, è stata inserita anche la ver- sione più recente con capacità aumentate, GPT-4, accessibile dall’interfaccia web di ChatGPT dopo aver effettuato il pagamento. Anche in questo caso le specifiche non sono state rese note, a meno della sua capacità di accedere ad Internet. 17
  • 24. Capitolo 3 Evolutionary Computation L’Evolutionary Computation (EC) è un paradigma computazionale che trova la sua radice nell’osservazione dei processi evolutivi naturali. Attraverso l’uso di algoritmi evolutivi, l’EC si propone di risolvere problemi complessi che spesso sfuggono alle capacità di risoluzione umane o agli approcci di ricerca tradizionali. Una delle estensioni più rilevanti e innovative dell’EC è rappresentata dal Genetic Improvement (GI), una disciplina che mira a migliorare algoritmi, software e programmi esistenti attraverso l’ottimizzazione guidata dall’evo- luzione. A differenza dello sviluppo tradizionale del software, che si basa su iterazioni umane e decisioni di progettazione, il GI permette di raffi- nare automaticamente e in modo sistematico le prestazioni dei programmi esistenti. Nel contesto del GI, i programmi vengono trattati come individui all’in- terno di una popolazione virtuale, soggetti a processi di selezione, mutazione e incrocio genetico allo scopo di generare varianti migliorate. Questo approc- cio non solo accelera il processo di ottimizzazione, ma può anche portare a scoperte inattese e soluzioni innovative, consentendo l’evoluzione continua dei software nel tempo. In questo capitolo viene offerta una panoramica sul mondo dell’EC per poi trattare il GI, di principale interesse per questo studio. 3.1 Genetic Algorithm Con Genetic Algorithm (GA) si intende un algoritmo ispirato all’evoluzione biologica, orientato alla risoluzione un problema di ottimizzazione. Questi algoritmi sono utilizzati per trovare soluzioni a problemi complessi attra- verso la generazione iterativa di popolazioni di candidati e l’applicazione di operatori genetici. L’idea alla base di un GA è modellare il processo evolu- tivo biologico, in cui gli individui più adatti sopravvivono e si riproducono, trasmettendo le loro caratteristiche alla prossima generazione. 18
  • 25. CAPITOLO 3. EVOLUTIONARY COMPUTATION 3.1.1 Schema generale L’evoluzione può essere riassunta dal seguente schema: • Una popolazione di individui compete per delle risorse limitate. • La popolazione è dinamica, gli individui muoiono e altri nascono. • Gli individui più adatti sopravvivono e si riproducono più degli altri. • La prole eredita alcuni tratti dai genitori. Dove un individuo indica una soluzione candidata per il problema in esa- me. Esempi di individuo possono essere un programma in un determinato linguaggio di programmazione, un insieme di parametri numerici, una strin- ga e così via. Questa rappresentazione, che definisce come sono visti da un osservatore esterno, viene detta fenotipo. La rappresentazione interna viene invece detta genotipo, e rappresenta la codifica di un individuo. Classiche rappresentazioni sono stringhe di bits di lunghezza fissa o variabile, alberi sintattici, o più in generale una qualsiasi struttura che rappresenti le infor- mazioni necessarie per descrivere un individuo. Queste due rappresentazione sono fortemente legate, infatti il fenotipo è la manifestazione esterna del ge- notipo. Sono necessarie entrambe le rappresentazioni in quanto il fenotipo esprime la risoluzione del problema in una versione interpretabile, mentre il genotipo è necessario per la manipolazione degli individui da parte dell’al- goritmo di ottimizzazione genetico. Per poter rappresentare un fenotipo in genotipo e viceversa è necessario specificare una funzione che permetta tale trasformazione. Infine, la qualità di un individuo viene decisa in base ad una funzione di fitness: essa valuta quanto il fenotipo di una certa soluzione sia buono, senza considerare il genotipo, associandovi un valore numerico in R. In linguaggio naturale, essa può essere descritta come l’abilità di un individuo di risolvere il problema. Un problema di ottimizzazione in questo ambito può allora essere visto come argmaxx∈Sf(x) o argminx∈Sf(x) con f : S → R funzione di fitness e S insieme delle soluzioni per un problema. Dalla definizione iniziale si può dedurre che la popolazione è dinamica, la grandezza è fissata e quindi la risorsa per cui gli individui competono è un posto all’interno della popolazione. Bisogna allora determinare come avviene la sostituzione degli individui, detto modello generazionale. 3.1.2 Modello generazionale Data una popolazione di m individui, ne vengono generati altri n derivanti da quelli già presenti nella popolazione. Quando bisogna decidere quali ele- menti mantenere, bisogna anche definire il tipo di popolazione, cioè con o senza sovrapposizione. Sapendo che il tempo è scandito dalle nascite/morti, possiamo definire i seguenti schemi. Nel caso di modello generazionale con sovrapposizione, ad ogni intervallo di tempo: 19
  • 26. CAPITOLO 3. EVOLUTIONARY COMPUTATION 1. Generazione di n figli partendo dagli m genitori. 2. Nuova popolazione data da n + m individui (unione di figli e genitori). 3. Selezione di m individui che sopravvivranno. Se invece viene scelto il caso senza sovrapposizione, ad ogni intervallo di tempo: 1. Generazione di n figli partendo dagli m genitori, con n ≥ m. 2. Selezione di m individui che sopravvivranno. Si noti che la condizione imposta su n ed m è necessaria affinché la popo- lazione rimanga di dimensione fissata m. Inoltre, come il nome suggerisce, in questo caso tutti i genitori muoiono. Di seguito alcuni casi comuni per la scelta dei parametri: • n = m con sovrapposizione. • n = m senza sovrapposizione. • n = 1 con sovrapposizione, detto steady state. Si può definire una generazione misurando il tempo che scorre come numero di nascite rispetto alla dimensione della popolazione m. Per selezionare quali individui sopravvivono e quali genitori si riproduco- no per originare i nuovi individui, vengono ora introdotti i principali criteri di selezione. Il metodo più banale e meno efficace, poiché non tiene conto delle differenze di adattamento tra gli individui, è quello della selezione uni- forme. Ogni individuo ha una probabilità uguale di essere selezionato come genitore per la produzione di nuovi individui nella generazione successiva. Ovvero, non ci sono preferenze o pesi associati agli individui in termini di quanto sono adatti a risolvere il problema. Questo significa che anche gli individui meno adeguati hanno una possibilità di essere selezionati come ge- nitori. Gli individui più idonei possono essere scelti meno frequentemente rispetto agli individui meno adatti, il che potrebbe rallentare la convergenza dell’algoritmo verso soluzioni migliori. La selezione per troncamento, è basata sul concetto di elitismo. In que- sto metodo, anziché assegnare la probabilità di selezione a ciascun individuo come avviene nella selezione proporzionale alla fitness, si selezionano diret- tamente i primi N individui con la valutazione di adattamento più alta, dove N è un numero prefissato o calcolato in base a una percentuale della popolazione totale. Questo tipo di selezione presenta alcune caratteristiche interessanti: 20
  • 27. CAPITOLO 3. EVOLUTIONARY COMPUTATION • Pressione selettiva: Questo metodo favorisce la selezione di individui con fitness migliore. Poiché solo i migliori individui sono selezionati, la popolazione successiva avrà una concentrazione maggiore di buone so- luzioni, aumentando la pressione selettiva verso il miglioramento delle soluzioni. • Perdita di diversità: Tuttavia, la selezione per troncamento può por- tare a una rapida perdita di diversità genetica. Poiché solo i migliori individui vengono selezionati, di conseguenza, le regioni dello spazio delle soluzioni che potrebbero contenere soluzioni promettenti ma meno adattate vengono ignorate. • Convergenza precoce: A causa della pressione selettiva elevata, c’è il rischio che l’algoritmo possa convergere prematuramente verso una soluzione locale ottimale, tralasciando la possibilità di trovare soluzioni migliori altrove nello spazio delle soluzioni. Con la selezione proporzionale alla fitness [29], detta anche roulette wheel selection, il metodo diventa più articolato. Data la fitness di ciascun indi- viduo, si sceglie casualmente un individuo con probabilità proporzionale al valore della funzione di fitness. Più quest’ultima sarà alta e più probabilità avrà quell’individuo di essere selezionato come genitore. Il concetto appena espresso può essere formulato matematicamente come segue px,P = f(x) P y∈P f(y) dove f è la funzione di fitness e P è la popolazione. Questo metodo è sem- plice da implementare e, in generale, consente una convergenza bilanciata in quanto è possibile che anche individui meno adatti possono contribuire alla diversità genetica. Tuttavia nel caso in cui un individuo presenti una fitness molto più grande rispetto agli altri individui, la diversità potrebbe venire ridotta a causa della continua selezione di questo individuo. Un migliora- mento applicabile a questa selezione proporzionale consiste nel valutare gli individui secondo una classifica piuttosto che rispetto al valore grezzo della fitness. Questa selezione è chiamata selezione proporzionale al rango. Come sempre viene calcolata la fitness di ciascun individuo, ma successivamente gli individui vengono classificati in ranghi in base a questo valore. Ogni ran- go ha una probabilità fissata di essere selezionato. Ad esempio, l’individuo con la i-esima fitness migliore verrà selezionato con probabilità m−i+1 rsum con m grandezza della popolazione e rsum un valore normalizzante. Poiché la selezione si basa sui ranghi relativi invece dei valori di fitness assoluti, que- sto metodo è meno influenzato da variazioni significative nelle valutazioni di fitness o dal rumore nei dati. Da osservare che questa selezione può essere usata anche in caso di fitness non numerica. 21
  • 28. CAPITOLO 3. EVOLUTIONARY COMPUTATION La metodologia più utilizzata nel GA è la selezione a torneo. Sia t ∈ N+ la dimensione del torneo. Vengono estratti casualmente t individui dalla popolazione, con reinserimento e viene selezionato l’individuo con fitness migliore. Apparentemente questo metodo è soggetto a una forte pressione selettiva, tuttavia al variare di t, tale problema può essere controllato: t piccolo implica una bassissima pressione selettiva, t grande invece introduce una grande preferenza verso gli individui migliori. Questa preferenza denota il comportamento tipico della popolazione a convergere verso gli individui più adatti, in quanto l’evoluzione si concentra sul migliorare le soluzioni più promettenti (exploitation), rischiando però di cadere in ottimi locali. Una pressione selettiva bassa invece permette di tenere da conto anche gli individui meno adatti, consentendo all’evoluzione di investigare su diverse soluzioni (exploration), anche se magari non portano a nulla, col rischio di non ottenere una soluzione ottima. Il bilanciamento tra exploitation e exploration è un punto molto complesso e importante quando si tratta di algoritmi genetici. 3.1.3 Riproduzione Per poter completare il ciclo evolutivo, è necessario che, partendo dalla po- polazione iniziale, vengano generati nuovi individui che successivamente po- tranno provare ad entrare nella nuova popolazione. Una prole viene generata applicando un operatore genetico unario o binario, a partire dal genotipo del genitore (o genitori nel caso di operatore binario). • L’operatore unario è detto mutazione, definito come u: G → G • L’operatore binario è detto crossover, definito come b: G2 → G Definiti gli operatori genetici, la prole può essere generata in tre diversi modi. I primi due richiedono il numero di figli da generare n e un peso associato a ciascun operatore genetico, w1 e w2. Successivamente va deciso se la generazione deve essere deterministica o stocastica. Nel primo caso, il primo operatore viene applicato per w1 · n volte, e il secondo operatore per w2 · n volte, in modo manifesto i genitori vengono scelti secondo il metodo deciso a priori, ad esempio tra quelli discussi nella Sezione 3.1.2. Se la generazione è invece stocastica, il processo viene ripetuto n volte e, ad ogni iterazione, l’operatore genetico viene selezionato casualmente con probabilità w1 e w2, solitamente il peso del crossover è vicino all’uno, mentre quella di mutazione è bassa. L’ultimo metodo di generazione consiste nell’applicare entrambi gli operatori a ciascun nuovo individuo. Ripetendo per n volte, viene prima applicato il crossover e immediatamente dopo viene applicata anche la mutazione all’individuo appena creato. Il metodo più comune per implementare la mutazione per stringhe di bit è la mutazione probabilistica a flip di bit. Dato il genotipo del genitore gp, 22
  • 29. CAPITOLO 3. EVOLUTIONARY COMPUTATION il genotipo del figlio gc è creato partendo da una copia di gp in cui ogni bit viene negato (0 → 1 e 1 → 0) con probabilità pm. Si supponga di avere un individuo con genotipo x = x1, . . . , xn dove xi è l’i-esimo bit. Allora, per ogni i ∈ {1, . . . , n} il bit xi viene invertito con probabilità pm, che solitamente è definita come pm = 1 n , in modo tale da mutare in media un bit per individuo, come mostrato in Figura 3.1. Come si può notare, all’aumentare di pm l’individuo generato somiglierà sempre meno al genitore. Figura 3.1: Mutazione di un individuo per la generazione di un figlio, in cui un singolo bit è stato invertito. Il crossover presenta invece molteplici possibilità; di seguito vengono elencate le modalità più note che verranno analizzate. • Crossover a singolo punto. • Crossover a m punti. • Crossover uniforme. • Crossover a singolo punto variabile. Nel crossover a singolo punto, dati due genitori membri dell’attuale popola- zione, x = x1, . . . , xn e y = y1, . . . , yn, viene scelto un punto k ∈ {1, . . . , n} uniformemente, detto punto di taglio. Il genotipo del figlio è creato pren- dendo dal primo genitore x, i bit prima del punto di taglio, e dal secondo genitore, y, i bit dopo il punto di taglio. Va osservato che in realtà i figli ge- nerati sono due, il secondo eredita i bit complementari al primo figlio, come mostrato in Figura 3.2. Una diretta generalizzazione è possibile scegliendo più di un punto di taglio, da cui il nome crossover a m punti. Gli m punti di taglio k1, . . . , km ∈ {1, . . . , n} vengono selezionati come precedentemente descritto, con la condizione che ki ≤ ki+1∀i ∈ {1, . . . , m−1} in modo tale che i punti di taglio cadano al più nella stessa posizione, ma mai in una posizione precedente del genotipo, rispetto all’ultimo punto generato. Il genotipo del figlio è generato prendendo ordinatamente i bit di un genitore alla volta, fino ad un punto di taglio, in cui si passa all’altro genitore, e così via. Anche in questo caso, prendendo i bit complementari possono essere generati altri figli dallo stesso crossover. Nel crossover uniforme infine, dati due genitori x = x1, . . . , xn e y = y1, . . . , yn, per ogni i ∈ {1, . . . , n} con probabilità pc = 0.5 l’i-esimo elemento del primo figlio sarà il bit xi o yi, in modo tale che in media, metà dei bit verrà cambiata. Un secondo figlio può essere generato, prendendo il bit non scelto per il primo figlio, come mostrato in Figura 3.3. 23
  • 30. CAPITOLO 3. EVOLUTIONARY COMPUTATION Figura 3.2: Generazione della prole tramite crossover a singolo punto. Figura 3.3: Generazione della prole tramite crossover uniforme. Il crossover a singolo punto variabile è una variante del primo metodo descritto, che viene analizzato per completezza. Anche questo metodo è ge- neralizzabile a m punti. Il cambiamento principale sta nel fatto che i punti di taglio possono essere diversi tra i due genitori, il che porta ad un genoti- po del figlio di lunghezza diversa rispetto a quella dei genitori; la funzione che mappa genotipo a fenotipo deve quindi permette genotipi di lunghezza variabile. Si ritiene necessario fare alcune considerazioni finali sugli operatori ge- netici. Va notato che il crossover è una cosa completamente diversa dalla mutazione. Tramite crossover non è possibile ottenere tutte le soluzioni pos- sibili, in quanto il genotipo derivante è ottenuto partendo dai genotipi dei genitori, che contengono una determinata soluzione. Si può quindi dedur- re che il crossover favorisce l’exploitation mentre la mutazione incoraggia l’exploration. Inoltre, è possibile generalizzare il GA per usare caratteri con- tenuti in un alfabeto finito di simboli Σ invece che usare unicamente l’insieme di bit {0, 1}. L’unico cambiamento da apportare è che la mutazione deve se- lezionare (solitamente casualmente) il nuovo simbolo tra i |Σ| − 1 simboli disponibili invece che negare un bit. La popolazione iniziale è solitamente casuale, ma possono essere usati approcci specifici basati però sulla forma del genotipo. Infine, l’evoluzione è un processo che termina con l’ottimo, ma in alcune condizioni possono essere considerate accettabili soluzioni che rispettano una qualche tipo di condizione, detta criterio di terminazione. Ad esempio dopo un certo numero di generazioni l’evoluzione può essere fermata, che la soluzione ottima sia stata trovata o meno. Lo schema tipico di un GA è riassunto in Figura 3.4. 24
  • 31. CAPITOLO 3. EVOLUTIONARY COMPUTATION Figura 3.4: Ciclo di evoluzione di un GA. 3.1.4 Problematiche Per concludere, si evidenziano alcuni problemi in cui l’evoluzione può incor- rere: • Diversità [28]: come già detto, la popolazione tende a evolvere verso gli individui migliori. Se la diversità non è abbastanza, significa che c’è troppa exploitation e bisogna intervenire tramite meccanismi per promuovere la diversità, al fine di evitare ottimi locali. • Eredità variazionale: se la prole è troppo simile ai genitori, anche in questo caso si ha troppa exploitation, l’evoluzione può portare a ottimi locali, essere lenta o addirittura inesistente. Se invece la differenza tra gli individui è troppa, significa che l’evoluzione è completamente casuale e manca exploitation. • Espressività: indica se la rappresentazione, ossia il fenotipo, e suffi- cientemente espressivo. Se l’espressività è bassa, la soluzione ottima potrebbe non essere rappresentabile o raggiungibile, se invece è troppo grande il tempo di convergenza potrebbe essere enorme o infinito. 3.2 Genetic Programming Il Genetic Programming (GP) è una tecnica per far evolvere stocasticamen- te una popolazione di individui che codificano programmi per calcolatori elettronici [15]. La principale differenza con il GA consiste nella rappresen- tazione del genotipo degli individui. La rappresentazione come stringa del codice sorgente non è ottimale, in quanto non permette di capire a cosa si sta applicando l’operatore genetico. La scelta più corretta è quella di codifi- care il genotipo come un albero sintattico, come mostrato in Figura 3.5, e di conseguenza gli operatori genetici andranno ad operare su sottoalberi. Per la codifica di un programma come albero bisogna conoscere quali sono i possi- bili nodi e quali di essi sono terminali. L’insieme dei terminali contiene tutte 25
  • 32. CAPITOLO 3. EVOLUTIONARY COMPUTATION le possibili foglie, come ad esempio costanti e variabili, mentre l’insieme dei funzionali contiene tutti i nodi interni, come operazioni aritmetiche, opera- tori booleani e costrutti if-else. L’insieme delle primitive, ovvero l’unione dei sopra menzionati insiemi, deve rispettare la proprietà di chiusura: • Consistenza di tipo: i terminali e l’output di qualsiasi funzione devono essere input validi per tutte le restanti funzioni. • Sicurezza della valutazione: le primitive che a runtime possono fallire, come ad esempio una divisione per zero, devono essere protette per evitare errori a runtime. Inoltre, per trovare una soluzione, essa deve essere rappresentabile, in al- tre parole l’insieme delle primitive deve essere sufficiente per scrivere una soluzione. Ad esempio, con numeri reali, variabili e l’insieme di operazioni {+, −, ∗} si può rappresentare ogni polinomio, ma ciò non è utile se la fun- zione da trovare è un’esponenziale. Questa proprietà, detta sufficienza, non è sempre garantita, ma le soluzioni che possono essere trovate dovrebbero comunque essere una buona approssimazione. Figura 3.5: Esempio di un albero sintattico rappresentante l’operazione aritmetica 5 + (2 ∗ 7). 3.2.1 Inizializzazione e selezione Un’altra differenza importante consiste nel modo in cui la popolazione inizia- le viene generata. Nel caso di GA con stringhe binarie, è sufficiente scegliere ogni bit indipendentemente con probabilità uniforme. Gli alberi finiti sono non numerabili, quindi servono dei metodi più complessi. Il primo è chia- mato grow e consiste nel selezionare in modo casuale una primitiva presa dal suo insieme, fino ad arrivare ad una certa profondità massima, dmax. Una volta raggiunto tale valore fissato, viene selezionato un nodo terminale. Il secondo metodo è chiamato full. Concettualmente è simile a grow, con la sola differenza che vengono selezionati solo i simboli funzionali prima di raggiungere la profondità massima. Ciò implica che i terminali appaiano solamente nell’ultimo livello dell’albero, portando in generale ad alberi di dimensione maggiore. Merita ancora citare la combinazione di questi due 26
  • 33. CAPITOLO 3. EVOLUTIONARY COMPUTATION metodi, ramped half and half. Viene selezionato casualmente un metodo tra grow e full, con dmax scelto casualmente tra un valore minimo e massimo. Solitamente questa metodologia consente di arrivare ad alberi con un buon grado di diversità. Il processo di selezione avviene come spiegato nella Sezione 3.1.2, soli- tamente si usa la selezione a torneo. Per ogni individuo nella popolazione, che si ricorda essere un programma, il valore della funzione di fitness viene generalmente assegnato in base al risultato ottenuto sui dati di prova, in modo tale che gli individui che risolvono meglio il problema, otterranno una fitness più bassa. In GP infatti, se un individuo risolve perfettamente il pro- blema, avrà fitness zero. Non sempre l’individuo risulta essere compilabile e/o eseguibile, in quel caso devono essere previste tecniche per gestire gli errori. 3.2.2 Operatori genetici Dopo che si è definito come rappresentare il genotipo dei programmi, gli ope- ratori genetici sono facilmente descrivibili. La mutazione, nel caso di GP, è implementabile con diverse tecniche. La prima è detta mutazione del sottoal- bero, in cui avviene la sostituzione di un sottoalbero selezionato casualmente con un nuovo sottoalbero generato anch’esso casualmente. Si tratta di una tecnica aggressiva, che porta a una maggior mutazione, come mostrato in Figura 3.6. Una versione più semplice è la mutazione a punto, Figura 3.7, in Figura 3.6: Esempio di mutazione del sottoalbero. cui avviene la sostituzione di un nodo selezionato casualmente con un nodo compatibile selezionato casualmente. Vi sono poi altre due mutazioni, il cui scopo è ridurre la dimensione dell’albero, la prima, la mutazione hoist la quale permette la sostituzione dell’intero albero con uno dei suoi sottoalberi, esemplificato in Figura 3.8. La seconda invece, la mutazione a restrizione, come evidenziato in Figura 3.9, esegue la sostituzione di un sottoalbero sele- zionato casualmente con un terminale selezionato casualmente. Esiste infine un ultimo tipo di mutazione, chiamata mutazione a scambio, in cui viene ap- plicata una permutazione agli argomenti di un funzionale, come raffigurato in Figura 3.10. 27
  • 34. CAPITOLO 3. EVOLUTIONARY COMPUTATION Figura 3.7: Esempio di mutazione a punto. Figura 3.8: Esempio di mutazione hoist. Figura 3.9: Esempio di mutazione a restrizione. Figura 3.10: Esempio di mutazione a scambio. 28
  • 35. CAPITOLO 3. EVOLUTIONARY COMPUTATION Per quanto riguarda il crossover, la Figura 3.11 mostra la generazione di due figli, come nel caso di GA. Partendo dalla rappresentazione ad albero di due individui della popolazione, per ciascuno viene scelto un punto di crossover. La prole risulta essere costituita da una copia del primo genitore, a meno del sottoalbero che inizia dal punto di crossover, che viene sostituito con il sottoalbero che inizia dal punto di crossover del secondo genitore. Lo stesso processo viene seguito per il secondo figlio, che però parte da una copia del secondo genitore. Figura 3.11: Esempio di crossover; gli alberi dei genitori, a sinistra, sono contraddistinti da colori diversi. Per come è stato descritto il GP fino ad ora, si può notare che l’evolu- zione non è vincolata, ovvero possono essere generati individui non validi, che contengono grandi porzioni di codice non valido. Questo perché molte operazioni effettuate sull’albero non cambiano la funzione che quest’ultimo rappresenta, aumentandone considerevolmente la dimensione senza un mi- glioramento della fitness. Di conseguenza la valutazione della fitness è lenta, e quindi anche l’evoluzione. Questi alberi sono detti bloat. Per ovviare a questo problema, ci sono tre possibili soluzioni, di seguito elencate: • Limitare la dimensione dell’individuo, tramite una profondità massima dell’albero o usando operatori che limitano la dimensione della prole. • Rimuovere le regione che non rappresentano codice valido. • Punire gli individui che sono troppo grandi, diminuendo la loro fitness. 29
  • 36. CAPITOLO 3. EVOLUTIONARY COMPUTATION 3.3 Grammatical Evolution Il Grammatical Evolution (GE) [23] è un approccio di GP più vincolato, il cui obiettivo però rimane quello di trovare un codice sorgente valido che risolva un dato problema. Il punto di forza sta però nella rappresentazione lineare dei programmi, più facile da gestire, combinandola poi indirettamente con una rappresentazione basata sugli alberi. L’idea è quella di definire una grammatica, solitamente tramite la Backus Normal Form (BNF), che viene poi utilizzata per interpretare il genoma lineare come sua derivazione. 3.3.1 Backus Normal Form Una metasintassi descrive la struttura e la composizione di frasi consentite essa viene spesso utilizzata per descrivere un linguaggio naturale o un lin- guaggio di programmazione. La BNF è una notazione metasintattica per grammatiche libere dal contesto, spesso usata per descrivere la sintassi dei linguaggi utilizzati nell’informatica, come i linguaggi di programmazione, for- mati di documenti, set di istruzioni e protocolli di comunicazione. Essa de- scrive il linguaggio in considerazione come un insieme di regole di produzione. Una grammatica BNF è una quadrupla (T, N, P, S) dove: • T è un insieme di simboli terminali. • N è un insieme di simboli non terminali. • P è un insieme di regole di produzione. • S ∈ N è un assioma, ovvero il simbolo iniziale. Una regola di produzione è definita seconda la seguente sintassi: simbolo ::= espressione dove simbolo rappresenta un simbolo non terminale, e espressione con- siste di una o più sequenze di simboli terminali o non terminali, separate dal simbolo |, ovvero barra verticale. Il simbolo ::= asserisce che la parte sinistra deve essere sostituita con una delle espressioni a destra. I simboli che non appaiono mai sul lato sinistro sono terminali. D’altra parte, i simboli che appaiono sul lato sinistro non sono terminali e sono sempre racchiusi dal simbolo . Si consideri il seguente esempio a scopo chiarificatore. somma ::= numero ’+’ numero numero ::= ’0’ | ’1’ | ’2’ | ’3’ In questo caso somma è una produzione che rappresenta la somma tra due numeri, e contiene il non terminale numero, che a sua volta contiene i primi quattro numeri naturali, che sono terminali. Con questa grammatica è 30
  • 37. CAPITOLO 3. EVOLUTIONARY COMPUTATION possibile definire la somma di tutte le combinazioni dei primi quattro numeri naturali, ma ad esempio non è possibile eseguire la sottrazione o usare numeri diversi da quelli definiti in numero. Può anche essere costruito un albero partendo dalle produzioni scelte, in cui tutti i non terminali sono nodi interni, mentre i terminali sono foglie. Si noti però che se si volesse ottenere il risultato della somma, è necessario valutare l’espressione. Questo significa che nel caso di una grammatica che esprime un programma, il risultato può essere valutato dati degli input specifici. 3.3.2 Da BNF a GE Tenendo traccia di tutte le espansioni, scelte tra tutte le possibili per i sim- boli non terminali, nel caso di generazione a partire da una grammatica, si nota che viene ottenuto un vettore di interi, in cui ogni valore indica la produzione scelta. Questa sequenza di interi rappresenta il genoma dell’in- dividuo, che può essere poi reso un albero per permettere la valutazione ed eventualmente la riproduzione. Un importante fattore su cui porre l’at- tenzione è che un individuo sarà sicuramente un programma valido poiché generato a partire da una grammatica che descrive correttamente il proble- ma e tutte le possibili combinazioni. Siccome ciascuna produzione può avere una lunghezza diversa dalle altre, il genoma non ha vincoli sull’insieme di simboli che lo descrive, basta considerare il simbolo scelto modulo lunghezza della produzione in considerazione. In questo modo gli individui potreb- bero anche avere lunghezza diverse tra loro, ma l’importante è limitare la lunghezza massima del genoma, altrimenti l’espansione potrebbe non finire mai. In quest’ultimo caso basta assegnare un valore di fitness molto alto, ovvero corrispondente ad un programma molto lontano dalla soluzione del problema da risolvere. Queste peculiarità lasciano anche intendere che la mappatura tra genotipo e fenotipo non è uno-a-uno, in quanto più genotipi possono rappresentare lo stesso fenotipo. Rimangono invariati gli operato- ri genetici utilizzabili, in quanto la generazione di un programma valido è demandata alla struttura della grammatica. La definizione delle regole di produzione rappresenta quindi un’importante tassello per un’evoluzione che porti a risultati accettabili. 3.4 Genetic Improvement Con Genetic Improvement (GI) si intende una tecnica genetica, simile a GP, ma che invece di generare completamente la soluzione del problema, parte da codice difettoso al fine di convergere verso una sua versione migliorata. La popolazione iniziale consiste di individui rappresentanti programmi validi, ma che presentano bachi, eccessivo consumo di tempo, memoria o energia infine può anche essere usato per estendere le funzionalità già presenti [24]. Le tecniche precedentemente analizzate nella Sezione 3.2.2 per la selezione e 31
  • 38. CAPITOLO 3. EVOLUTIONARY COMPUTATION la riproduzione degli individui rimangono valide anche nel GI, come anche l’utilizzo di GE per l’evoluzione. L’inizializzazione della popolazione, anche per il GI, può essere fatta essere fatta con tecniche come grow o full, ma almeno un individuo deve essere inserito come membro della popolazione. Se la popolazione non viene inizializzata con tali tecniche, il metodo più semplice per riempire i rimanenti posti nella popolazione è quello di replicare per m − 1 volte tale individuo, e lasciare che siano gli operatori genetici a creare diversità. Un altro metodo di inizializzazione, che richiede però una condizione in più, è avere più individui da cui iniziare, in modo da dividere la popolazione equamente tra copie di questi. Va notato che, come nelle altre tipologie di evoluzione, il principale problema risulta essere il tempo necessario affinché una buona soluzione venga generata. Ciò non è scontato ed è legato a molteplici fattori, tra cui la complessità del problema, la funziona di fitness scelta, in quanto vincola la precisione dell’evoluzione, e nel caso di GE, la definizione della grammatica. 3.4.1 PonyGE2 PonyGE2 [8] è un framework, disponibile al download1 su GitHub, che im- plementa il concetto di GE in Python. Con l’accortezza di inserire uno o più programmi rappresentati individui di partenza con cui inizializzare la popolazione e scegliendo accuratamente gli iper-parametri, il GE può essere usato anche per il GI. In questo framework è stata implementata anche que- sta possibilità, permettendo di inserire manualmente i genomi degli individui da usare come punto di partenza, e automaticamente la popolazione iniziale sarà equamente costituita da copie di ciascun individuo inserito come base. Il framework si basa principalmente su una classe, in cui sono definiti tutti i parametri per un’evoluzione, che possono essere specificati da linea di comando o, più comodamente in un file txt, in cui ogni riga è costituita dalla coppia parametro: valore. Alcune di queste righe servono per specificare la grammatica, i file contenenti le coppie input, output su cui valutare la fitness degli individui e gli individui da usare come base per il GI. Esula dallo scopo di questo capitolo la spiegazione di tutti i parametri inseribili, il loro valore di default e la scelta disponibile per ciascuno di essi: si vuole piuttosto fornire una visione globale della tipologia di software e i servizi che mette a disposizione PonyGE2. In Figura 3.12 viene mostrato un file contenente i parametri impostati per l’evoluzione, pubblicato come esempio nella repository ufficiale del framework. Bisogna osservare che PonyGE2, oltre ad essere scritto in Python, basa anche l’intero GE su questo linguaggio, nonostante ci sia la possibilità di estenderlo ad altri linguaggi, anche se tale possibilità non viene trattata in questo studio. L’evoluzione all’interno del framework permette quindi di sin- 1 https://github.com/PonyGE/PonyGE2 32
  • 39. CAPITOLO 3. EVOLUTIONARY COMPUTATION Figura 3.12: Esempio di un file di configurazione di un’evoluzione in PonyGE2, presa dalla repository ufficiale del progetto. tetizzare o migliorare esclusivamente programmi Python, in quanto la loro valutazione, per esprimere la fitness, è comodamente realizzabile grazie ad alcune funzionalità predefinite del linguaggio. Come è noto, questo linguag- gio, tra le sue specifiche, ha la peculiarità di definire blocchi di codice in base al livello di indentazione. L’indentazione è rappresentata tramite un nume- ro consistente di tabs che deve essere uguale all’interno dello stesso blocco di codice; indentazioni in un punto inaspettato produrranno un errore, così come indentazioni mancanti o non consistenti. Il framework utilizzato co- difica in un modo proprio, il livello di indentazione. Per creare un nuovo blocco di codice, aumentando l’indentazione, va inserita la parentesi graffa aperta seguita dai due punti {:. Per chiudere un blocco di codice indentato e poi riportare l’indentazione al livello di indentazione precedente, va sem- plicemente chiuso il blocco di indentazione con i due punti seguiti da una parentesi graffa di chiusura :}. In Figura 3.1 viene mostrato un semplice esempio di funzione con indentazione, mentre in Figura 3.2 si può osservare la codifica secondo le specifiche di PonyGE2. Questa rappresentazione, per- mette di specificare il fenotipo più semplicemente e, in fase di valutazione, verrà ricreato il codice originale a cui assegnare un valore di fitness tramite la funzione Python exec, che permette di eseguire un qualsiasi codice valido. def example(n): if n = 1: return 1 return n Figura 3.1: Esempio di codice Python con codifica standard. 33
  • 40. CAPITOLO 3. EVOLUTIONARY COMPUTATION def example(n):{: if n = 1:{: return 1 :} return n :} Figura 3.2: Esempio di codice Python con codifica usata in PonyGE2. L’ultima parte, fondamentale per lo sviluppo della tesi, rigurda l’integra- zione diretta in PonyGE2 di un parser, che permette di ottenere i genomi di un individuo a partire dal suo fenotipo. Questa risulta essere un’enorme facilitazione, in quanto è sufficiente usare un qualsiasi codice che rispetta una determinata grammatica e, tramite questo strumento, vengono generati i genomi. Questi ultimi verranno poi utilizzati per specificare i membri della popolazione iniziale nel caso di GI. 34
  • 41. Capitolo 4 Metodologia In questo lavoro di tesi viene proposto uno strumento automatizzato1 che risolve un problema di programmazione in Python, avendo a disposizione unicamente la sua descrizione testuale, detta prompt, e un insieme di dati di validazione composti da coppie input e output. Una prima soluzione del problema in esame viene trovata da un LLM, il cui risultato viene valutato utilizzando i dati di esempio forniti. In caso il programma Python generato dal LLM non passi tutti i test forniti dall’utente vengono generati i files di configurazione dell’evoluzione necessari a PonyGE2, introdotto nella Sezione 3.4.1, per evolvere il programma usando il GI. Questo strumento è scritto in Python, in modo tale da avere un ecosistema coerente che semplifichi l’inte- grazione di PonyGE2 al suo interno, e che permetta di valutare facilmente le risposte dei LLM. Questo strumento si articola di tre fasi principali: • Ottenimento del codice sorgente rappresentante una possibile soluzio- ne del problema a partire da un LLM, valutazione della qualità della risposta e salvataggio dati. • Generazione dinamica di una grammatica specifica per il problema, a partire da una versione di base con aggiunte delle nozioni estratte dal prompt e dal codice generato dal LLM. Questa fase prevede anche la creazione dinamica dei files di configurazione necessari all’evoluzione in PonyGE2. • Miglioramento del codice utilizzato il GI. I problemi risolvibili tramite questo approccio sono ridotti ai soli che possono essere codificati con una singola funzione. Il metodo seguito rimane valido anche per tipologie di codice differente, come ad esempio per il costrutto class ma, come esposto nella Sezione 4.1, il lavoro si è concentrato solamente su questo sottoinsieme. 1 https://github.com/damianoravalico/LLMGIpy 35
  • 42. CAPITOLO 4. METODOLOGIA 4.1 Selezione dei problemi Per validare questo approccio alla risoluzione di problemi di programmazio- ne, è stato scelto un rinomato dataset di benchmark, chiamato PSB2 [13]. Questo insieme di problemi è specificatamente pensato per la ricerca su sin- tetizzazione automatica di programmi, con tecniche come GP. Questa suite di benchmark contiene 25 problemi generici, presi da corsi universitari in cui vengono proposti e vari katas2. Questa scelta è dovuta a diverse caratte- ristiche del dataset, a partire dall’idea alla base della sua creazione, il che lo rende ideale anche per l’applicazione di GI. Inoltre, è stata resa dispo- nibile una repository3, che permette l’accesso a un grande quantità di cop- pie input-output per ciascun problema, ottenibili direttamente da Python, semplicemente importando la libreria4. La descrizione testuale di ciascun problema, indispensabile per l’interro- gazione al LLMs, non è direttamente ottenibile tramite libreria, bensì unica- mente come file di Fogli Google5. Questa mancanza viene colmata tramite lo scaricamento del file e la traduzione automatizzata in file csv, in modo tale da rendere la sua lettura estremamente rapida grazie all’utilizzo della libreria pandas. Nella Tabella 4.1 vengono mostrati i dettagli di alcuni problemi, selezio- nati per la parte di GI, secondo i criteri che verranno esposti nel Capitolo 5. 4.2 Inizializzazione Il primo passo consiste nell’interrogare un LLM che, dato il prompt in cui è contenuta la descrizione del problema, prova a risolvere producendo un codice, non necessariamente valido, corretto o completo. Come preambolo al prompt dato al LLM, viene inserita una stringa, il cui scopo è quello di specificare il tipo di codice richiesto, una funzione Python, e incoraggiare l’inserimento di moduli esterni, al fine di evitare codice logicamente corretto ma non compilabile a causa di un mancato import. Di seguito viene riportata la stringa posta prima del prompt contenente la richiesta; essa è scritta in lingua inglese in quanto tutte le interrogazioni ai LLMs vengono eseguite in questa lingua. ”Write a single Python function to solve the following problem inserting the necessary modules:” 2 Esercizio ripetitivo che aiuta i programmatori a migliorare le proprie abilità in un ambiente controllato. 3 https://github.com/thelmuth/psb2-python 4 Ottenibile con pip install psb2 5 https://shorturl.at/clMN8 36
  • 43. CAPITOLO 4. METODOLOGIA Nome problema Descrizione Input Output Basement Given a list of integers, what is the position of the first in- teger such that the sum of all integers from the start to it in- clusive is negative? Vettore di inte- ri Intero Camel Case Take a string and convert all of the words to camelCase. Each group of words is delimi- tered by “-”, and each group of words is separated by a space Stringa con tutte le lettere minuscole, gli spazi bianchi e “-” Stringa Dice Game Peter has an n sided die and Colin has an m sided die, whe- re n m. If they both roll their dice, what is the proba- bilitiy that Peter rolls strictly higher than Colin Due interi Decimale Fizz Buzz Given an integer 1 ≤ x ≤ 50 000, print “Fizz” if x is di- visible by 3, “Buzz” if x is di- visible by 5, “FizzBuzz” if x is divisible by 3 and 5, and x if none of the above hold. Intero Stringa Snow Day Given 1 int (hours) and 3 floats (current snow, falling snow, percent melting), deter- mine how much snow will be on the ground in the amount of hours given Intero e tre de- cimali Decimale Tabella 4.1: Descrizione, tipo di input e di output per i problemi selezionati per il GI dal dataset PSB2. 37
  • 44. CAPITOLO 4. METODOLOGIA Il risultato ottenuto è sempre una stringa di testo ma la sua formattazione è fortemente legata al modello a cui viene chiesto di eseguire il compito. Per poter valutare la qualità del codice generato è necessario estrarlo, eliminando il testo superfluo che può essere stato inserito dal LLM. Assieme al codice vengono ricavati anche i moduli dichiarati, che siano essi esterni o interni, e il nome della funzione generata. Questo viene fatto in quanto, affinché una funzione sia valutabile, deve essere sintatticamente corretta, in altre parole eseguibile. Solitamente però i pacchetti importati sono dichiarati al di so- pra della signature della funzione. Essi vengono catturati grazie all’utilizzo di un’espressione regolare, direttamente applicabile alla risposta del LLM, in modo tale che l’unica parte ancora da estrarre sia quella che inizia dalla stringa def, ovvero l’inizio della funzione. Tramite il modulo ast, è possibile verificare se un codice dato come stringa è valido oppure no. L’estrazione quindi inizia controllando se la stringa ottenuta dalla risposta del LLM, pre- sa dal punto in cui compare def, fino alla fine, è un codice valido. Se non lo è, viene eliminata l’ultima riga e la verifica rieseguita. Ciò è ripetuto fino all’ottenimento di un codice che supera la verifica. Nel peggiore dei casi, il risultato di questo elaborazione è la sola signature, a cui viene aggiunta l’i- struzione pass, in modo tale da rendere il codice valido. L’impiego di questo algoritmo viene esemplificato applicato ad un codice generato da Alpaca13B mostrato in Figura 4.1, che porta al risultato di Figura 4.2. Nel primo caso è evidente che il codice non è completo, in quanto nessuna istruzione viene inserita nel blocco if, il che rende invalido anche il blocco for, portando alla rimozione di entrambi. Infine, prima della valutazione del codice, le importazioni esterne sono aggiunte nuovamente nella funzione. Con l’inseri- mento di questa semplice procedura, se il modello genera codice incompleto si è automaticamente protetti. Inoltre, l’obiettivo di questo procedimento, oltre alla mera estrazione del programma per la sua valutazione, è quello di massimizzare il numero di futuri individui per la popolazione di GI. Merita osservare che, nel caso di modelli più grandi, come i GPT, spesso il codice viene automaticamente posizionato all’interno di un blocco. Ad esempio, nel caso di ChatGPT e GPT4, il blocco inizia con la dicitura “‘python e termina con “‘; in questo caso l’estrazione può essere velocizzata, usando solamente delle espressioni regolari e non c’è bisogno di reinserire i pacchetti importati. Ulteriori elaborazioni sono apportate al codice, come la rimozione di commenti (a singola e multi linea) e l’eliminazione di righe vuote; la mo- tivazione risiede nell’intenzione di ridurre il numero di strutture da definire nella grammatica necessaria all’evoluzione. def find_negative(nums): # Define a function called find_negative with nums as input parameter. neg = -1 for i in range(len(nums)): # Iterate through each element in the array and calculate its 38
  • 45. CAPITOLO 4. METODOLOGIA value. if (sum([i] + [neg]) 0) (neg == -1): # Check whether the current number plus the previous one gives a negative result. If so , set the variable ’neg’ to this index. Figura 4.1: Esempio di risposta generata da Alpaca13B. Si può osservare che il codice risulta incompleto a livello sintattico il che lo rende non compilabile. def find_negative(nums): # Define a function called find_negative with nums as input parameter. neg = -1 Figura 4.2: Elaborazione del codice di Figura 4.1 tramite rimozione del codice incompleto per consentirne la compilazione con conseguente valutazione. Una volta che il codice è disponibile, con l’uso di funzioni Python spe- cifiche per esecuzione runtime di codice sotto forma di stringa, avviene il processo di valutazione. La funzione generata viene definita tramite exec, che effettua il parsing della stringa. Successivamente, usando eval che ri- ceve come parametro un’espressione, in questo caso il nome della funzione, viene creato un oggetto che riferisce alla funzione, così da poterla chiamare a piacimento. Vengono poi raccolti i risultati dell’esecuzione della funzione, passando come argomenti i valori di input, e confrontando il risultato del- la funzione con l’output atteso. Chiaramente una valutazione più precisa è data da un numero sufficientemente grande di coppie input-output, tale da coprire ogni possibile risultato e i casi particolari. Per migliorare questo aspetto, con il modulo di PSB2 è possibile specificare la quantità di dati per ogni problema: il numero di coppie di validazione scaricate di default è 1000, lo stesso usato anche per i test sul codice ottenuto dal LLM. Conclusa la valutazione del codice, i risultati sono salvati in una cartella apposita e, per ciascuna risposta del LLM, viene creato un file JSON. La struttura del file viene mostrata in Figura 4.3, escludendo alcuni campi che verranno discussi nella Sezione 4.3.2. Le prime chiavi del file JSON servono a mantenere una traccia sui dati generici del problema e la configurazione dell’esecuzione, come il modello, mentre il campo data è un vettore, esso contiene interi elementi, ciascuno rappresentate un’iterazione del problema. Questa scelta è stata fatta perché il software proposto può essere configura- to in modo tale da ottenere più risposte per lo stesso problema, visto che la generazione dei LLM non è sempre uguale. In questo modo, in ottica di mi- glioramento del codice, si hanno a disposizione più individui, il che favorisce la diversità della popolazione. Ogni iterazione ha la sua risposta dal mo- dello, le importazioni di moduli, che possono anche non esserci, l’estrazione 39
  • 46. CAPITOLO 4. METODOLOGIA di nome e codice della funzione e infine, la chiave tests_results indica le prestazioni della funzione in termini di numero di test passati, non passati e test che non sono stati effettuati a causa di una qualche eccezione verificatasi durante la valutazione. In caso di problemi di definizione della funzione, an- che questo errore viene riportato. Da notare che alla fase di test è assegnato un tempo massimo, con valore di default di 60 secondi, superati i quali il test viene annullato e l’errore riportato. Questo limite è fondamentale in quanto il codice generato dal modello potrebbe contenere cicli infiniti o mol- to lunghi, ricorsioni senza fine e così via. In questo modo l’intera esecuzione viene protetta e nel peggiore dei casi, sarà solamente necessario più tempo. L’implementazione di questo meccanismo di sicurezza utilizza più processi ed impone il limite su ciascuno di essi, in modo tale da aumentare anche il parallelismo, diminuendo inoltre il tempo necessario alla valutazione, che viene fatta solamente in seguito all’acquisizione della riposta da parte del LLM per il numero di volte definito. Alla conclusione della valutazione di ciascuna iterazione, si prosegue con il problema successivo, se disponibile. { model_name: ..., problem_name: ..., prompt: ..., problem_index: 0, data_test_size: 0, data: [ { iteration: 0, model_response: ..., imports: [], function_name: ..., code: ..., tests_results: { passed: 0, not_passed: 0, with_exception(s): 0 } } ] } Figura 4.3: Esempio di file JSON creato dopo la valutazione dell’output di un LLM. I valori delle chiavi rappresentano solamente il tipo di dato. Questo strumento, eseguibile tramite linea di comando, necessita di al- cuni parametri dell’esecuzione, definibili tramite argomenti, che sono: 40
  • 47. CAPITOLO 4. METODOLOGIA • --model definisce il LLM da usare, tra quelli descritti in precedenza nella Sezione 2.3. Se non specificato, l’esecuzione è annullata. • --dataset permette di specificare il dataset da usare per i problemi e da cui attingere i relativi valori di test. Al momento, è implementato solo PSB2, ma il codice permette l’introduzione di altri dataset. Se non specificato, l’esecuzione è annullata. • --data_size ovvero il numero di coppie input-output su cui effettuare la validazione del codice. Come già detto presenta un valore di default e può quindi essere omesso. • --iterations permette di ripetere la richiesta al LLM per più volte. Di default il valore è di 5 ripetizioni, aumentato a 10 per l’ottenimento dei risultati proposti nel Capitolo 5. Alla conclusione dell’esecuzione viene restituito il percorso assoluto della cartella che contiene i risultati, ovvero tanti files JSON quanti sono i problemi del dataset; per fare un esempio, nel caso di PSB2 saranno 25. 4.3 Generazione dinamica dei files di evoluzione Vi sono ulteriori due parametri di esecuzione, entrambi opzionali, che pos- sono essere impostati. Il primo è --with_impr_files che, se specificato, istruisce il programma in modo tale che, dopo aver concluso la creazione dei files JSON per ciascun problema, generi anche i files necessari per applicare il GI, partendo dalla cartella in cui sono presenti i risultati dell’esecuzione. Il secondo invece, --jsons_dir, è inserito nel caso in cui si voglia generare i file ma a partire da una precedente esecuzione, inserendo il percorso asso- luto della cartella che contiene i files JSON. Entrambi i parametri hanno lo stesso scopo, ma consentono la generazione dinamica dei files necessari ad un’evoluzione tramite GI, che sono: • Grammatica BNF con cui guidare l’evoluzione. • Una cartella con un file txt per ogni individuo (rappresentato come genotipo) che comporrà la popolazione iniziale. • Un file txt contenente i parametri per il GI, come richiesto per l’utilizzo di PonyGE2. 4.3.1 Grammatica La grammatica necessaria per il GI combinato al GE, rappresenta un tasto dolente di questo tipo di evoluzione. La dimensione di una grammatica intac- ca il tempo necessario all’evoluzione, all’aumentare della stessa aumentano 41