Approssimazione di una curva parametrica su un piano con PyTorch

Questo post fa parte di una serie di post sull'approssimazione di oggetti matematici (funzioni, curve e superfici) tramite una rete neurale di tipo MLP (percettrone multistrato, dall'inglese Multi-Layer Perceptron); per una introduzione sull'argomento si veda il post Approssimazione con percettroni multistrato altamente configurabili.

L'argomento di questo post è l'approssimazione di una curva parametrica, con parametro $t$ appartenente a un intervallo chiuso, su un sistema di riferimento cartesiano $Oxy$ si definisce con una coppia di funzioni $x(t) \colon [a,b] \to \rm I\!R$ e $y(t) \colon [a,b] \to \rm I\!R$ le quali restituiscono rispettivamente il valore della coordinata $x$ e della coordinata $y$ al variare del parametro $t$; equivalentemente una curva parametrica sul piano è definita anche con una funzione vettoriale $f(t) \colon [a,b] \to {\rm I\!R x \rm I\!R}$ così definita: $$f(t) = \begin{bmatrix} x(t) \\ y(t) \\ \end{bmatrix}$$ ove le due componenti dei vettori dell'immagine della funzione rappresentano rispettivamente le coordinate $x$ e $y$ della curva sul piano cartesiano.

Si osservi che le due definizioni di curva parametrica, seppur matematicamente equivalenti, suggeriscono due architetture di reti neurali differenti: la prima definizione conduce a una coppia di MLP indipendenti, uno per approssimare $x(t)$ e l'altro per approssimare $y(t)$ per poi combinare i risultati in uscita dalle due reti e ottenere le coppie $(x,y)$ che approssimano la curva sul piano; la seconda definizione suggerisce invece un MLP ove il layer di input contiene un solo neurone in quando la dimensione del dominio è 1 (l'intervallo $[a,b]$) mentre il layer di output contiente 2 neuroni in quanto la dimensione del codominio è 2.

Nell'ambito dell'approssimazione di una curva parametrica continua e limitata sul piano, lo scopo di questo post è di consentire all'utilizzatore di testare diverse combinazioni di architettura MLP, funzioni di attivazione, algoritmo di addestramento e funzione di loss senza scrivere codice ma agendo solamente sulla linea di comando di quattro script Python (più due varianti) che implementano separatamente le seguenti funzionalità:

  • Creazione dei dataset
  • Definizione dell'architettura del MLP + Addestramento
  • Predizione
  • Visualizzazione del risultato
Il codice descritto da questo post richiede la versione 3 di Python e utilizza la tecnologia PyTorch; richiede inoltre le librerie NumPy e MatPlotLib.
Per ottenere il codice si veda il paragrafo Download del codice completo in fondo a questo post.

Lo stesso identico meccanismo è stato realizzato usando la tecnologia TensorFlow 2 (con Keras); si veda il post Approssimazione di una curva parametrica su un piano con TensorFlow pubblicato sempre su questo sito web.

Creazione dei dataset

Scopo del programma Python pmc2t_gen.py è di generare i dataset (di training e/o di test) da utilizzare nelle fasi successive; prende in linea di comando le due funzioni componenti da approssimare (in sintassi lambda body), l'intervallo del parametro indipendente $t$ (inizio, fine e passo di discretizzazione) e genera il dataset in un file nel formato csv applicando le due funzione all'intervallo del parametro $t$ passato.
Il file csv in uscita ha infatti tre colonne (senza header): la prima colonna contiene i valori, ordinati in modo crescente, del parametro indipendente $t$ nell'intervallo desiderato con il passo di discretizzazione specificato; la seconda colonna e terza colonna contengono rispettivamente i valori delle componenti $x$ e $y$, ovverosia i valori delle funzioni $x(t)$ e $y(t)$ corrispondenti al valore di $t$ della prima colonna.
Per ottenere l'usage del programma è sufficiente eseguire il seguente comando:

$ python pmc2t_gen.py --help
e l'output ottenuto è:

usage: pmc2t_gen.py [-h] 
-h, --help                  show this help message and exit
--dsout DS_OUTPUT_FILENAME  dataset output file (csv format)
--xt FUNCX_T_BODY           x=x(t) body (lamba format)
--yt FUNCY_T_BODY           y=y(t) body (lamba format)
--rbegin RANGE_BEGIN        begin range (default:-5.0)
--rend RANGE_END            end range (default:+5.0)
--rstep RANGE_STEP          step range (default: 0.01)
Si rimanda alla lettura del file README.md per il dettaglio esaustivo della semantica dei parametri supportati in linea di comando.

Un esempio di uso del programma pmc2t_gen.py

Si supponga di voler approssimare la curva di Lissajous nell'intervallo $[0,4\pi]$ descritta dalla seguente funzione $$f(t) = \begin{bmatrix} x(t)=2 \sin (\frac{t}{2} + 1) \\ y(t)=3 \sin t \\ \end{bmatrix}$$ Tenendo presente che np è l'alias della libreria NumPy, le due espressioni componenti si traducono in sintassi lambda body Python così:

2 * np.sin(0.5 * t + 1)
3 * np.sin(t)
Per generare il dataset di training, si esegua quindi il seguente comando:

$ python pmc2t_gen.py \
  --dsout mytrain.csv \
  --xt "2 * np.sin(0.5 * t + 1)" \
  --yt "3 * np.sin(t)" \
  --rbegin 0.0 \
  --rend 12.56 \
  --rstep 0.01
mentre per generare il dataset di test, si esegua il seguente comando:

$ python pmc2t_gen.py \
  --dsout mytest.csv \
  --xt "2 * np.sin(0.5 * t + 1)" \
  --yt "3 * np.sin(t)" \
  --rbegin 0.0 \
  --rend 12.56 \
  --rstep 0.0475
Si osservi che il passo di discretizzazione del dataset di test è più grande di quello di training e questo è normale in quanto l'addestramento, per essere accurato, deve essere eseguito su una maggiore quantità di dati. Inoltre si osservi che è opportuno che il passo di discretizzazione del dataset di test non sia un multiplo di quello di training per garantire che il dataset di test contenga la maggior parte dei dati non presenti nel dataset di training, e questo rende più interessante la predizione.

Definizione dell'architettura del MLP + Addestramento

Questa funzionalità ha due implementazioni differenti secondo le due definizioni matematiche di curva parametrica: l'implementazione ufficiale è quella che approssima con un solo MLP la curva definita come funzione vettoriale mentre la variante Twin approssima le funzioni componenti separatamente con due MLP gemelli.
La versione ufficiale è realizzata dal programma Python pmc2t_fit.py che, in accordo con i parametri passati in linea di comando, crea dinamicamente un MLP ed effettua il suo addestramento al fine di approssimare la funzione vettoriale che definisce la curva sul piano.
La variante Twin è realizzata dal programma Python pmc2t_fit_twin.py che, in accordo con i parametri passati in linea di comando, crea dinamicamente una coppia di MLP gemelli ed effettua l'addestramento di ciascuno di essi al fine di approssimare separatemente le singole funzioni componenti che definiscono la curva sul piano.
Per ottenere l'usage della versione ufficiale del programma è sufficiente eseguire il seguente comando:

$ python pmc2t_fit.py --help
e l'output ottenuto è:

usage: pmc2t_fit.py [-h]
--trainds TRAIN_DATASET_FILENAME
--modelout MODEL_PATH
[--epochs EPOCHS]
[--batch_size BATCH_SIZE]
[--hlayers HIDDEN_LAYERS_LAYOUT [HIDDEN_LAYERS_LAYOUT ...]]
[--hactivations ACTIVATION_FUNCTIONS [ACTIVATION_FUNCTIONS ...]]
[--optimizer OPTIMIZER]
[--loss LOSS]
[--device DEVICE]
Per ottenere l'usage della variante Twin è sufficiente eseguire il seguente comando:

$ python pmc2t_fit_twin.py --help
e l'output ottenuto (a parte il nome del programma) è identico a quello della versione ufficiale.

Si rimanda alla lettura del file README.md per il dettaglio esaustivo della semantica dei parametri supportati in linea di comando di entrambi i programmi.

Un esempio di uso del programma pmc2t_fit.py

Si supponga di avere a disposizione un dataset di training (ad esempio generato tramite pmc3t_fit.py come mostrato nel paragrafo precedente) e si voglia che il MLP abbia due layer nascosti con 150 neuroni ciascuno e che si voglia usare la funzione di attivazione ReLU in uscita dai due layer; inoltre si vogliano eseguire 500 epoche di training con un batch size di 200 elementi usando l'algoritmo di ottimizzazione Adamax con un learning rate di 0.02 e la MSELoss quale funzione di loss. Per mettere in azione tutto questo si esegua il seguente comando:

$ python pmc2t_fit.py \
  --trainds mytrain.csv \
  --modelout mymodel.pth \
  --hlayers 150 150 \
  --hactivation 'ReLU()' 'ReLU()' \
  --epochs 500 \
  --batch_size 200 \
  --optimizer 'Adamax(lr=0.02)' \
  --loss 'MSELoss()'
al termine del quale il file mymodel.pth conterrà il modello del MLP addestrato sul dataset mytrain.csv secondo i parametri passati in linea di comando.

Predizione

Come per l'addestramento anche questa funzionalità ha due implementazioni differenti secondo le due definizioni matematiche di curva parametrica: l'implementazione ufficiale è quella che effettua la predizione usando un solo MLP pre-addestrato che approssima la curva definita come funzione vettoriale mentre la variante Twin effettua separatamente la predizione usando due MLP pre-addestrati gemelli che approssimano separatamente le due singole funzioni componenti.

Scopo della versione ufficiale è realizzata dal programma Python pmc2t_predict.py è quello di applicare il modello di MLP generato tramite pmc2t_fit.py a un dataset in input (ad esempio il dataset di test generato tramite pmc2t_gen.py come mostrato in un paragrafo precedente); l'esecuzione del programma produce in uscita un file csv con tre colonne (senza header): la prima colonna contiene i valori del parametro indipendente $t$ presi dal dataset in input mentre la seconda e la terza colonna contengono i valori predetti delle due componenti, ovverosia i valori della predizione che approssimano la curva parametrica $f(t)$ corrispondenti al valore di $t$ della prima colonna.

Scopo della variante Twin è realizzata dal programma Python pmc2t_predict_twin.py è quello di applicare la coppia modelli di MLP generati tramite pmc2t_fit_twin.py a un dataset in input (ad esempio generato tramite pmc2t_gen.py come mostrato in un paragrafo precedente); l'esecuzione del programma produce in uscita un file strutturalmente identico a quello in uscita dalla versione ufficiale; è diverso il modo di calcolare i valori della seconda e terza colonna in quanto questi sono rispettivamente i valori delle predizioni dai due singoli MLP che approssimano separatamente le due funzioni componenti $x(t)$ e $y(t)$.
Per ottenere l'usage della versione ufficiale del programma è sufficiente eseguire il seguente comando:

$ python pmc2t_predict.py --help
e l'output ottenuto è:

usage: fx_predict.py [-h]
--model MODEL_PATH
--ds DATASET_FILENAME
--predictionout PREDICTION_DATA_FILENAME
[--device DEVICE]
Per ottenere l'usage della variante Twin è sufficiente eseguire il seguente comando:

$ python pmc2t_predict_twin.py --help
e l'output ottenuto (a parte il nome del programma) è identico a quello della versione ufficiale.

Si rimanda alla lettura del file README.md per il dettaglio esaustivo della semantica dei parametri supportati in linea di comando di entrambi i programmi.

Un esempio di uso del programma pmc2t_predict.py

Si supponga di avere a disposizione il dataset di test mytest.csv (ad esempio generato tramite pmc2t_gen.py come mostrato in un paragrafo precedente) e il modello di MLP addestrato nel file mymodel.pth (generato tramite pmc2t_fit.py come mostrato nell'esempio del paragrafo precedente); si esegua quindi il seguente comando:

$ python pmc2t_predict.py \
  --model mymodel.pth \
  --ds mytest.csv \
  --predictionout myprediction.csv
al termine del quale il file myprediction.csv conterrà l'approssimazione della curva parametrica iniziale.

Visualizzazione del risultato

Scopo del programma Python pmc2t_plot.py è quello di visualizzare la curva della predizione sovrapposta alla curva del dataset iniziale (che sia quello di test o di training) e questo consente la comparazione visuale delle due curve.
Per ottenere l'usage del programma è sufficiente eseguire il seguente comando:

$ python pmc2t_plot.py --help
e l'output ottenuto è:

usage: pmc2t_plot.py [-h]
--ds DATASET_FILENAME
--prediction PREDICTION_DATA_FILENAME
[--savefig SAVE_FIGURE_FILENAME]
Si rimanda alla lettura del file README.md per il dettaglio esaustivo della semantica dei parametri supportati in linea di comando.

Un esempio di uso del programma pmc2t_plot.py

Avendo a disposizione il dataset di test mytest.csv (ad esempio generato tramite pmc2t_gen.py come mostrato in un paragrafo precedente) e il file csv della predizione (generato tramite pmc2t_predict.py come mostrato nel precedente paragrafo), per generare i due grafici si esegua il seguente comando:

$ python pmc2t_plot.py \
  --ds mytest.csv \
  --prediction myprediction.csv
che mostra le due curve sovrapposte: in blu quella del dataset di input, in rosso quella della predizione.

Nota: Data la natura stocastica della fase di addestramento, i singoli specifici risultati possono variare. Si consideri di eseguire la fase di addestramento più volte.

Grafico generato dal programma pmc2t_plot.py che mostra l'approssimazione effettuata dal MLP della curva di Lissajous $f(t) = \begin{bmatrix} x(t)=2 \sin (\frac{t}{2} + 1) \\ y(t)=3 \sin t \\ \end{bmatrix}$

Esempi di uso in cascata dei quattro programmi

Nella cartella parametric-curve-on-plane-fitting/examples ci sono cinque script shell che mostrano l'uso dei quattro programmi in cascata, con addestramento e predizione nella versione ufficiale, in varie combinazioni di parametri (architettura del MLP, funzioni di attivazione, algoritmo, funzione di loss, parametri di prodedura di training). Inoltre ci sono altri cinque script shell che ripropongono gli stessi esempi ma con addestramento e predizione nella variante Twin. Per mandare in esecuzione i cinque esempi nella versione ufficiale eseguire i seguenti comandi:

$ cd parametric-curve-on-plane-fitting/examples
$ sh example1.sh
$ sh example2.sh
$ sh example3.sh
$ sh example4.sh
$ sh example5.sh
Invece per mandare in esecuzione i cinque esempi nella variante Twin eseguire i seguenti comandi:

$ cd parametric-curve-on-plane-fitting/examples
$ sh example1_twin.sh
$ sh example2_twin.sh
$ sh example3_twin.sh
$ sh example4_twin.sh
$ sh example5_twin.sh
Nota: Data la natura stocastica di questi esempi (relativamente alla parte di training), i singoli specifici risultati possono variare. Si consideri di eseguire i singoli esempi più volte.

Download del codice completo

Il codice completo è disponibile su GitHub.
Questo materiale è distribuito su licenza MIT; sentiti libero di usare, condividere, "forkare" e adattare tale materiale come credi.
Sentiti anche libero di pubblicare pull-request e bug-report su questo repository di GitHub oppure di contattarmi sui miei canali social disponibili nell'angolo in alto a destra di questa pagina.