APPRENDIMENTO ED EVOLUZIONE
2.1 L’apprendimento supervisionato
2.1.3 Codice e implementazione
− =
∑
k jk pk pj act pj pj pj act j f net w o t net fδ
δ
'' (( ))( ) (9)dove la prima formula è applicata ai neuroni di uscita e la seconda ai nodi nascosti della rete.
In questo caso la funzione logistica si dimostra un’ottima soluzione per la semplicità della sua derivata, che equivale a flog(x)(1− flog(x)). La derivata della funzione logistica è quindi riscritta come prodotto del valore di output del neurone, ossia f'act(netpj)= fact(netpj)(1− fact(netpj))=opj(1−opj). Sostituendo questo risultato nella formula precedente, si ottiene la formula dell’errore
− − − =
∑
k jk pk pj pj pj pj pj pj j o o w o t o oδ
δ
((11 ))( ) (10)dove la prima formula è applicata ai neuroni di uscita e la seconda ai nodi nascosti della rete (Zell 2003, 108-110). Questa è la formula nella forma più concreta e pratica, che viene utilizzata nella costruzione dei programmi.
ö importante aver sviluppato questo discorso, perché solo in questo modo è
possibile cogliere il significato vero e proprio della backpropagation. Ciò permette di capire come essa venga tradotta nel linguaggio informatico e come sia utilizzata nella costruzione di modelli a reti neurali. Il particolare vantaggio di questa formula, che è stato motivo di tanto successo, è la possibilità di applicarla a reti neurali a più strati e con funzioni di attivazioni non lineari: in questo modo è possibile costruire, in uno spazio di assi cartesiani, veri e propri poligoni convessi e aree definite.
2.1.3 Codice e implementazione.
Si è a lungo discusso del carattere parallelo e distribuito delle operazioni e dei diversi livelli di analisi a cui può essere analizzato un processo cognitivo. In questo
paragrafo l’attenzione si sofferma al livello del programma, mostrando alcune parti essenziali di una rete neurale a diversi strati nascosti, costruita in linguaggio di programmazione Java (Appendice A). L’aspetto fondamentale, che merita di essere approfondito, è il modo in cui è organizzato il carattere distribuito dell’apprendimento.
La struttura stessa del modello è definita con la costruzione delle variabili, in cui la memorizzazione e il calcolo dei pesi avviene attraverso l’utilizzo di collezioni di variabili a più dimensioni, che in Java sono definite multi-dimensional array (Schildt 2005, 158). È costruita una tabella a più dimensioni in cui collocare i valori della rete, tra cui i valori dei pesi, degli input e degli output di ciascun neurone: il metodo qui spiegato permette di gestire la complessità delle reti utilizzando collezioni di valori, e di costruire dei metodi o delle funzioni che siano in grado di identificare la collocazione del valore per poterlo utilizzare ed eventualmente modificare.
In un primo momento sono definite le variabili in base al metodo sopra descritto:
public class MultiLayerPerceptron {
public double[][][] weights;
public double[][] net;
public double[][] output;
Successivamente i valori delle variabili vengono organizzati in base al numero di strati della rete, che è espresso dalla lunghezza .lenght della variabile layersize. Qui di seguito è definita la struttura delle variabili e non il loro valore.
public MultiLayerPerceptron(int[] layersize) {
weights = new double[layersize.length-1][][];
output = new double[layersize.length][];
delta = new double[layersize.length][];
net = new double[layersize.length][];
nosigout = new double[layersize.length][];
for (int i =0; i < layersize.length;i++) {
net[i] = new double[layersize[i]];
output[i] = new double[layersize[i]]; delta[i] = new double[layersize[i]];
nosigout[i] = new double[layersize[i]]; }
La collezione che rappresenta il numero di strati di pesi, equivalente al numero di strati della rete meno uno, è ora completata: il secondo array rappresenta i singoli neuroni dello strato successivo, layersize[i] + 1, mentre l’ultima collezione rappresenta il numero di neuroni dello strato precedente al quale viene sommato il bias,
layersize[i+1],il cui ruolo fondamentale è quello di determinare nell’apprendimento il valore ottimale della funzione di soglia.
for (int i =0; i < layersize.length-1;i++) {
weights[i]=new double[ layersize[i] + 1 ][ layersize[i+1] ]; }
A ciascun peso viene infine attribuito un valore casuale a cui viene sottratto mezza unità.
for (int layer = 0; layer < weights.length; layer++) {
for (int i = 0; i < weights[layer].length; i++) {
for (int j = 0; j < weights[layer][i].length; j++) {
weights[layer][i][j] = (Math.random()-.5); }
} }
Una volta create delle liste di oggetti è possibile, in molti linguaggi di programmazione, richiamare in ordine ciascuno di essi ed eseguire una determinata operazione. La primitiva, ossia un comando originario del programma, più classica per generare questo processo, detto loop, è il comando for seguito da (initialization;
condition; iteration) {statement}. L’utilizzo ripetuto del comando permette di richiamare una ad una tutte le variabili contenute nelle arrays.
public double[] predict(double[] input) {
output[0] = input;
nosigout[0] = input;
for (int layer = 0; layer < output.length-1; layer++) {
for (int j = 0; j < output[layer+1].length; j++) {
double sum = 0;
for (int i = 0; i < output[layer].length; i++){ sum += weights[layer][i][j] * output[layer][i];
}
sum += weights[layer][output[layer].length][j] * onneuronout;
nosigout[layer+1][j] = sum ;
output[layer+1][j] = calcActivation(sum); }
}
return output[output.length-1]; }
Il metodo predict calcola il valore di output di ciascun neurone: per ciascun strato della rete, escluso quello di input: per ogni neurone dello strato j, esso calcola la somma dell’output di ciascun neurone i dello strato precedente moltiplicato per il corrispettivo peso. Il valore sum ottenuto per ogni neurone di ogni strato della rete diventa argomento della funzione di attivazione calcActivation() e diventa output del neurone.
Lo stesso metodo è utilizzato per correggere l’errore della rete rispetto ai valori esterni. È qua espressa la formula della backpropagation così come è stata introdotta nel paragrafo precedente. Il primo passo consiste nel calcolare l’errore (t[j] - out[j]), moltiplicarlo per il valore dell’output.
private void trainpattern(double[] input, double[] t) {
double[] out = predict(input);
for (int j = 0; j < delta[output.length-1].length ; j++ ){
delta[delta.length-1][j] =
calcActivationDerivative(nosigout[delta.length-1][j]) * (t[j] - out[j]); }
for (int layer = output.length-2; layer >= 0; layer--) {
for (int j = 0; j < delta[layer].length; j++) { double sum = 0;
for(int i = 0 ; i < delta[layer+1].length; i++){
sum += weights[layer][j][i] *
delta[layer+1][i];
}
delta[layer][j]=calcActivationDerivative(nosigout[layer][j]) * sum;
} }
double eta = 0.5
for (int layer = 0; layer < weights.length; layer++) {
for (int i = 0; i < weights[layer].length-1; i++) {
for (int j = 0; j < weights[layer][i].length; j++) {
weights[layer][i][j] += eta * delta[layer+1][j] * output[layer][i]; }
}
for (int j = 0; j <
weights[layer][weights[layer].length-1].length; j++) {
weights[layer][weights[layer].length-1][j] += eta *
delta[layer+1][j] * onneuronout; }
}
La modifica dei nodi nascosti delta[layer][j] avviene calcolando la derivata della funzione di attivazione calcActivationDerivative (nosigout[layer][j])
per la somma sum per la variazione dei nodi dello strato successivo moltiplicato per il la forza delle connessioni. La modifica dei pesi weights[layer][i][j] è uguale al prodotto della variazione per il valore dell’output, delta[layer+1][j], moltiplicato per un coefficiente di apprendimento eta. L’ultimo ciclo for del codice calcola la variazione del peso del bias: è un neurone particolare il cui valore di output è sempre uguale a 1, e la cui funzione è quella di cambiare il valore della funzione di attivazione.