Ho sempre più la sensazione che sia un’attività inutile: la spiegazione teorica di quello che fa è praticamente zero e il codice è quasi incomprensibile.
Ormai però, prima di passare a provare il sito che ho scoperto recentemente (AdventuresInMachineLearning.com), volevo ragionare un po’ sull’ultimo esperimento.
Non è stata un’operazione particolarmente felice ma comunque a qualche conclusione interessante mi ha portato: per non parlare del fatto che, seppure lentamente, sto migliorando col Python!
Ah! Prima di continuare, la forma della “mia” RN è la seguente:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input (InputLayer) (None, None, 103) 0
_________________________________________________________________
lstm_layer_1 (LSTM) (None, None, 44) 26048
_________________________________________________________________
lstm_layer_2 (LSTM) (None, None, 44) 15664
_________________________________________________________________
lstm_layer_3 (LSTM) (None, None, 44) 15664
_________________________________________________________________
time_distributed_1 (TimeDist (None, None, 103) 4635
=================================================================
Total params: 62,011
Trainable params: 62,011
Non-trainable params: 0
_________________________________________________________________
Un primo elemento interessante è come anche la mia RN psicotica riesca a concludere correttamente le parole ammezzate prima di entrare nel suo ciclo di ripetizione ossessiva.
In particolare in uno degli esempi pubblicati rimane incompleto il frammento “No” e la RN lo completa con “Nota (*1): ”. Mi ha colpito molto…
Perché, è bene ricordarlo, la RN non memorizza alcuna parola ma semplicemente calcola il carattere successivo a quello che gli viene fornito in ingresso: solo modificando i parametri di questo enorme sistema di equazioni si ottiene l’effetto complessivo di memorizzare, globalmente per tutta la rete, parole diverse. A me sembra quasi una magia: ho provato a chiarirmi la logica/matematica che vi sta dietro ma è tutto troppo complicato: in verità ho la netta sensazione che anche gli scienziati viaggino parecchio “a naso” scegliendo di usare funzioni e strutture non tanto per motivi matematici ma per intuizioni (*1). In altre parole la pratica ha di gran lunga sopravanzato la teoria.
Forse mi sono dimenticato di spiegare che in questo esempio Osinga usa degli strati interni per la rete neurale molto più complessi dei semplici “Dense” (dove semplicemente tutti i nodi di uno strato erano collegati con tutti quelli del precedente) già visti nello scorso esercizio.
Usa infatti i nodi LSTM che sta per Long Short-Term Memory layer: la caratteristica di questi strati è che la loro uscita viene rifusa con l’ingresso all’istante successivo. Ovviamente per fare tutto questo la sua struttura è molto più complessa: non per niente quando ho provato a calcolare i numeri di nodi della mia RN supponendo una struttura analoga al “Dense” (ma con funzioni diverse pensavo) mi era venuto 13.171 parametri invece di 62.011. A occhio questo significa che uno strato LSTM ha quasi quattro volte i parametri di uno strato Dense…
Tutto questo è compatibile con quanto ho frettolosamente scorso sul sito AdventuresInMachineLearning.com…
Ovviamente lo strato di entrata (non è uno strato vero e proprio ma definisce la “forma” dei dati di ingresso) e quello di uscita sono diversi. In particolare quello di uscita è un Dense con funzione di attivazione Softmax.
La funzione di attivazione Softmax è quella che fa sì che i valori di uscita di ogni nodo equivalgano a probabilità con somma 1. Questo significa che, dato un carattere di ingresso, la RN non risponde “il carattere successivo è X” ma piuttosto dà, per ognuno dei 103 caratteri, la probabilità che questo sia il successivo a quello di ingresso.
Giocando col parametro di indeterminatezza (v. RN psicotica) è possibile farle restituire come carattere di uscita non il più probabile ma qualcosa di più o meno prossimo a esso.
Inizialmente i 103 caratteri dei miei testi vengono tradotti con una serie di 103 cifre: ogni carattere è composta da un 1 e 102 zeri. Qualcosa come:
Carattere 1:
1000...0000 (103 caratteri)
Carattere 2:
0100...0000 (103 caratteri)
Carattere 3:
0010...0000 (103 caratteri)
Etc…
Carattere 102:
0000...0010 (103 caratteri)
Carattere 103:
0000...0001 (103 caratteri)
Il primo strato della rete neurale ha infatti esattamente 103 ingressi così come l’ultimo ha 103 nodi (e altrettante uscite).
Quindi ci si sarebbe potuti anche aspettare che l’uscita fosse costituita da 102 zeri e un unico 1 in maniera da poter identificare il carattere calcolato dalla RN.
In realtà che questa struttura non potesse essere corretta ci ero già arrivato da solo senza guardare il codice.
Consideriamo ad esempio la parola “babbo ” con (per semplicità) “b”=0100, “a”=1000, “o”=0010 e “ ”=0001; allora si potrebbe supporre di avere una RN che all’ingresso 0100 (b) restituisca 1000 (a) e che poi a 1000 (a) restituisca 0100 (b) ottenendo così “bab”. A questo punto però all’ingresso 0100 (b) corrisponderebbe di nuovo (le RN sono deterministiche e quindi allo stesso ingresso corrisponde la stessa uscita) 1000 (a): si avrebbe così la stringa “baba” che proseguirebbe all’infinito. Per ottenere “babbo” la RN deve essere invece in grado di rispondere alle diverse “b” di ingresso prima con una “a”, poi con una “b” e infine con una “o”.
Questo significa che la “storia” (a quale “b” siamo) deve essere codificata nell’uscita. E infatti la funzione di attivazione Softmax restituisce tanti numeri compresi fra 0 e 1 che indicano la probabilità di ogni carattere di essere il seguente di quello fornito in ingresso.
Sono queste probabilità che “fanno capire” alla RN a quale “b” di “babbo” si trova in maniere da farle restituire il carattere seguente corretto.
In realtà, se ho ben capito dal poco che ho visto su AdventuresInMachineLearning.com, i singoli nodi degli strati LSTM hanno una loro “memoria” interna. Ma comunque l’uscita dello strato finale della rete è molto importante perché va poi ad alimentare l'ingresso all'iterazione successiva…
E questo aspetto mi ha portato alla seguente riflessione: con 103 caratteri la probabilità media di ciascuno di essi sarà un numero compreso fra 0 e 0,01 con magari i caratteri più probabili con valori di .1 o .2 (valori a casaccio). Questo significa che i valori di uscita della RN sono molto schiacciati e compressi sullo 0: intuitivamente in questa maniera l’informazione sullo “stato” (a quale “b” siamo) si deve basare su piccole variazioni numeriche: meglio sarebbe se questa codifica fosse inserita a parte in maniera da poter sfruttare una gamma numerica maggiore e non limitata dal valore del carattere più probabile.
Allora ho pensato alla seguente architettura che mi piacerebbe provare a implementare (ma per adesso non ne sono in grado…). Aggiungere allo strato di uscito (composto da 103 nodi) una ventina di nodi extra per i quali la funzione di Loss ritornerà sempre 0 (ovvero li considererà sempre corretti indipendentemente dal loro valore). Il valore di questi nodi sarà comunque passato anche all’entrata (e comunque l’aggiornamento causato dagli altri 103 nodi porterà anche alla modifica del valore di questi nodi extra) e questo, almeno nella mia fantasia, dovrebbero servire da memoria che aiuterà la RN a “ricordare” con maggiore facilità (visto che di per sé non avranno vincoli) lo stato in cui si trova.
Ovviamente molto dipende anche dal funzionamento degli strati LSTM che, come detto, hanno già una loro memoria interna: magari la mia idea non servirebbe a niente o peggiorerebbe soltanto le prestazioni…
Comunque in futuro (se e quando ne sarò capace) mi piacerebbe provare a implementarla.
Avrei ancora molte altre considerazioni da fare ma mi pare di aver già scritto abbastanza.
Aggiungo solo che mi chiedevo come mai Osinga avesse basato la sua RN sui singoli caratteri e non sulle singole parole: intuitivamente dovrebbe essere più semplice ottenere frase casuali realistiche.
Ebbene ho poi scoperto che su AdventuresInMachineLearning.com c’è proprio un minicorso che usa le parole invece dei singoli caratteri per addestrare una RN!
Conclusione: con il libro di Osinga per il momento ho chiuso. Adesso voglio provare a guardare il materiale del sito che ho scoperto ed, eventualmente, altri libri che comprai in blocco. Se mi viene voglia potrei guardare un corso in linea, che mi è stato suggerito, che spiega Python e le librerie più usate per le RN come Numpy: proprio ieri ho perso diverse ore lottando con Numpy per fare una semplice verifica che avevo in mente… Insomma il corso anche se non troppo entusiasmante dovrebbe essere molto utile...
Nota (*1): Non vi sono cioè teoremi che dicono che per risolvere il problema X occorre una RN con certe specifiche funzioni, tot nodi e che vada allenata per N cicli… O magari, non lo so, questo teorema esiste per problemi molto semplici, ma sicuramente non per la montagna di applicazioni attuali.
Nessun commento:
Posta un commento