Apprendimento Supervisionato per la Classificazione dei Documenti con Scikit-Learn Trading algoritmico machine learning

Apprendimento Supervisionato per la Classificazione dei Documenti

Sommario Tutorial

TUTORIAL

Questo è il primo articolo di quella che diventerà una serie di tutorial relativi alla classificazione dei documenti in linguaggio naturale, al fine di realizzare l’analisi del sentiment e, in definitiva, un filtro per il trading automatico o per la generazione dei segnali. Questo specifico articolo descrive l’uso delle di Support Vector Machines (SVM) per classificare i documenti di testo in gruppi che si escludono a vicenda.

Classificazione dei Documenti per il Trading Quantitativo

Esiste un numero significativo di passi da eseguire tra la visualizzazione di un documento di testo su un sito Web, ad esempio, e l’utilizzo del suo contenuto come input per una strategia di trading automatica per generare filtri o segnali di trading. In particolare, devono essere eseguite le seguenti operazioni:

  • Automatizzare il download di più articoli generati continuamente da fonti esterne tramite una elevata velocità di esecuzione.
  • Analizzare le sezioni rilevanti di testo / informazioni di questi documenti che richiedono analisi, anche nel caso di formati diversi tra i documenti.
  • Convertire paragrafi di testo arbitrariamente lunghi (attraverso molte lingue possibili) in una struttura dati coerente che può essere compresa da un sistema di classificazione.
  • Determinare un insieme di gruppi (o etichette) dove poter inserire ogni documento. Ad esempio, possono essere “positivo” e “negativo” o “rialzista” e “ribassista”.
  • Creare un “training corpus” di documenti a cui sono associate etichette note. Ad esempio, un migliaio di articoli finanziari potrebbe dover essere etichettato con le etichette “rialzista” o “ribassista”
  • Addestrare i classificatori su questo corpus mediante una libreria software come scikit-learn di Python (che useremo di seguito)
  • Utilizzare il classificatore per etichettare nuovi documenti, in modo automatico e continuo.
  • Valutare il “tasso di classificazione” e altre metriche di rendimento associate al classificatore
  • Integrare il classificatore in un sistema di trading automatico, filtrando altri segnali di trading o generandone di nuovi.
  • Monitorare continuamente il sistema e regolarlo secondo necessità, se le sue prestazioni iniziano a peggiorare

In questo articolo eviteremo di  descrivere come scaricare articoli da diverse fonti esterne e faremo uso direttamente di un dataset di dati già fornito con le proprie etichette. Questo ci permetterà di concentrarci sull’attuazione della “pipeline di classificazione”, piuttosto che dedicare una notevole quantità di tempo all’ottenimento e all’etichettatura dei documenti.

Negli articoli successivi di questa serie faremo uso delle librerie Python, come ScraPy e BeautifulSoup per ottenere automaticamente molti articoli basati sul web ed estrarre efficacemente i loro dati basati sul testo dall’HTML.

Inoltre non considereremo, all’interno di questo specifico articolo, come integrare un tale classificatore in un sistema di trading algoritmico pronto per andare live. Tuttavia, questo aspetto sarà oggetto di articoli successivi.
È estremamente importante non solo creare esempi “studio”, come in questo articolo, ma anche discutere su come integrare completamente un classificatore in un sistema che potrebbe essere utilizzato in produzione. Quindi gli articoli successivi considereranno l’implementazione in un sistema reale.

Supponiamo quindi di avere un corpus di documenti pre-etichettato (da delineare di seguito!), iniziamo prendendo il training corpus e lo includiamo in una struttura dati Python adatta alla pre-elaborazione e l’utilizzo tramite il classificatore.

Tuttavia, prima di essere in grado di entrare nei dettagli di questo processo, dobbiamo discutere brevemente i concetti di classificazione supervisionata e macchine vettoriali di supporto.

Classificazione supervisionata e Macchine Vettoriali di Supporto

Per una panoramica più approfondita su concetti base dell’apprendimento automatico statistico, puoi consultare questo articolo.

Classificatori Supervisionati

I classificatori supervisionati sono un gruppo di tecniche di apprendimento automatico statistico che tentano di associare una “classe”, o “etichetta”, a un particolare insieme di funzionalità, sulla base di etichette note in precedenza collegate ad altri set di features simili.

Questa è chiaramente una definizione abbastanza astratta, quindi può essere utile avere un esempio. Consideriamo una serie di documenti di testo. Ogni documento è associato ad insieme di parole, che chiameremo “features” o caratteristiche. Ciascuno di questi documenti potrebbe essere associato a un’etichetta che descrive l’argomento dell’articolo.

Ad esempio, una serie di articoli da un sito web che parlano di animali domestici potrebbe contenere articoli che riguardano principalmente cani, gatti o criceti. Alcune parole, come “gabbia” (criceto), “guinzaglio” (cane) o “latte” (gatto) potrebbero essere più rappresentative di alcuni animali domestici rispetto ad altri. I classificatori supervisionati sono in grado di isolare alcune parole rappresentative di determinate etichette (animali) “apprendendo” da una serie di articoli di “addestramento”, che sono già pre-etichettati, spesso in modo manuale, da un essere umano.

Matematicamente, ciascuno degli \(j\) articoli sugli animali domestici all’interno di un corpus di addestramento è associato ad un vettore \(j\) di features, le cui componenti rappresentano la “forza” delle parole (in seguito definiremo il concetto di “forza”). Ogni articolo è associata anche un’etichetta di classe, \(j\), che in questo caso sarebbe il nome dell’animale più associato all’articolo.

La “supervisione” della procedura di addestramento si verifica quando un modello viene addestrato o si adatta a questi dati particolari. Nell’esempio seguente useremo la Support Vector Machine come nostro modello e la “addestreremo” su un corpus (una raccolta di documenti) generato in precedenza.

Support Vector Machines

Per una panoramica matematica più approfondita e completa di come funzionano le Support Vector Machines, si può consultare questo articolo.

Le Support Vector Machine sono una sottoclasse di classificatori supervisionati che tentano di suddividere uno spazio di elementi in due o più gruppi, cioè nel nostro caso significa separare una raccolta di articoli in due o più etichette di classe.

Gli SVM ottengono questo risultato trovando un mezzo ottimale per separare tali gruppi in base alle loro etichette di classe già note. Nei casi più semplici, il “confine” di separazione è lineare e quindi si ottiene due o più gruppi che sono divisi da linee (o piani) in spazi multi-dimensionali.

Nei casi più complicati (dove i gruppi non sono ben separati da linee o piani), gli SVM sono in grado di eseguire partizioni non lineari. Ciò si ottiene mediante un metodo kernel. In definitiva, questo li rende classificatori molto sofisticati e capaci, ma con il solito svantaggio di poter essere soggetti a overfitting. Maggiori dettagli possono essere trovati qui.

Nella figura seguente ci sono due esempi di limiti decisionali non lineari (rispettivamente kernel polinomiale e kernel radiale) per due etichette di classe (arancione e blu), attraverso due features [lavel]X_1[/label] e [label]X_2[/label].

Gli SVM sono potenti classificatori se usati correttamente e possono fornire risultati molto promettenti. Utilizzeremo SVM per il resto di questo articolo.

trading-machine-learning-svm-0010
Confini decisionali di Support Vector Machine per due diversi kernel

Preparare un Dataset per la Classificazione

Un famoso set di dati utilizzato nella progettazione della classificazione dell’apprendimento automatico è il set Reuters 21578. È uno dei set di dati di test più utilizzati per la classificazione del testo, ma oggigiorno è un po ‘obsoleto. Tuttavia, per gli scopi di questo articolo sarà più che sufficiente.

Il set è costituito da una raccolta di articoli di notizie (un “corpus”) contrassegnati da una selezione di argomenti e posizioni geografiche.E’ quindi “ready made” per essere utilizzato nei test di classificazione, poiché è già pre-etichettato.

Ora vediamo come scaricare, estrarre e preparare il set di dati. Sto effettuando questo tutorial su una macchina Ubuntu 18.04, quindi ho accesso al terminal di riga di comando. Se usi Linux o Mac OSX potrai tranquillamente seguire questi comandi. Se usi Windows, dovrei scaricare uno strumento di estrazione Tar / GZIP per ottenere i dati.

Il set di dati Reuters 21578 può essere scaricato da questo link, in formato tar GZIP compresso. La prima operazione da fare è creare una nuova directory di lavoro e scaricare il file al suo interno. Puoi modificare il nome della directory come meglio credi:

Possiamo quindi decomprimere il file:
Se elenchiamo il contenuto della directory (ls -l) possiamo vedere quanto segue (ho rimosso i permessi e i dettagli di proprietà per brevità):

Vedrai che tutti i file che iniziano con reut2- sono .sgm, il che significa che sono file SGML. Sfortunatamente, Python ha deprecato sgmllib a partire da Python 2.6 e lo ha completamente rimosso in Python 3. Tuttavia, non tutto è perduto perché possiamo creare la nostra classe SGML Parser che sovrascrive quella HTMLParser incorporata in Python.

Ecco un singolo elemento presente in uno dei file:

Sebbene possa essere alquanto laborioso analizzare i dati in questo modo, specialmente se confrontati con l’effettivo apprendimento automatico, posso assicurarti che gran parte della giornata di un data scientist o di un ricercatore quantistico consiste nell’ottenere effettivamente i dati in un formato utilizzabile dai software di analisi! Questa particolare attività viene spesso chiamata scherzosamente “data wrangling”. Quindi è opportuno fare un po ‘di pratica!

Se diamo un’occhiata al file topics, all-topics-strings.lc.txt, digitando less all-topics-strings.lc.tx possiamo vedere quanto segue (per sintesi ne ho rimosso la maggior parte):

Eseguendo il comando cat all-topics-strings.lc.txt | wc -l possiamo vedere che ci sono 135 argomenti separati tra gli articoli. Ciò rappresenterà la vera sfida della classificazione!

In questa fase dobbiamo creare quello che è noto come un elenco di coppie predittore-risposta. Questo è un elenco di due tuple che contengono l’etichetta di classe più appropriata e il testo del documento non elaborato, come due componenti separati. Ad esempio, l’obbiettivo dell’analisi è ottenere una struttura dati simile alla seguente:

Per creare questa struttura è necessario analizzare individualmente tutti i file Reuters e aggiungerli a un grande elenco di “corpus”. Poiché la dimensione del file del corpus è piuttosto bassa, si adatterà facilmente alla RAM disponibile sulla maggior parte dei laptop / desktop moderni.

Tuttavia, nelle applicazioni in produzione è solitamente necessario trasmettere i dati di addestramento in un sistema di apprendimento automatico ed eseguire un “adattamento parziale” su ciascun lotto, in modo iterativo. Negli articoli successivi descriveremo questo scenario quando studieremo set di dati estremamente grandi (in particolare i dati tick).

Come affermato in precedenza, il nostro primo obiettivo è creare l’SGML Parser che raggiunga effettivamente questo obiettivo. Per fare ciò, ereditiamo la classe HTMLParser di Python per gestire i specifici tag nel set di dati Reuters.

Quando si eridita la classe HTMLParser, dobbiamo sovrascrivere tre metodi, handle_starttag, handle_endtag e handle_data, che dicono al parser cosa fare all’inizio dei tag SGML, cosa fare alla chiusura dei tag SGML e come gestire i dati intermedi.

Creiamo anche due metodi aggiuntivi, _reset e parse, che vengono utilizzati per monitorare lo stato interno della classe e per analizzare i dati effettivi in ​​modo frammentato, in modo da non utilizzare troppa memoria.

Infine, implementiamo una elementare funzione __main__ di per testare il parser sul primo set di dati all’interno del corpus Reuters.

Come per la maggior parte, se non tutti, dei codici presenti su DataTrading, ho inserito commenti parlanti in modo che si possa capire cosa si sta implementando ad ogni passaggio:

In questa fase vedremo una quantità significativa di output simile a questa:

In particolare, si tenga presente che invece di avere una singola etichetta di argomento associata a un documento, abbiamo più argomenti. Per aumentare l’efficacia del classificatore, è necessario assegnare una sola etichetta di classe a ciascun documento. Tuttavia, noterai anche che alcune delle etichette sono in realtà tag di posizione geografica, come “giappone” o “thailandia”. Poiché ci occupiamo esclusivamente di argomenti e non di paesi, desideriamo rimuoverli prima di selezionare il nostro argomento.

Lo specifico metodo che useremo per eseguire questa operazione è piuttosto semplice. Elimineremo i nomi dei paesi e quindi selezioneremo il primo argomento rimanente nell’elenco. Se non ci sono argomenti associati, elimineremo l’articolo dal nostro corpus. Nell’output sopra, questo si ridurrà a una struttura di dati che assomiglia a:

Per rimuovere i tag geografici e selezionare il principale tag dell’argomento possiamo aggiungere il seguente codice:

L’output è il seguente:

Siamo ora in grado di pre-elaborare i dati per l’input nel classificatore.

Vettorizzazione

In questa fase abbiamo una vasta raccolta di coppie di tuple, ciascuna coppia contenente un’etichetta di classe e un corpo di testo grezzo dagli articoli. La ovvia domanda da porsi è come poter convertire il corpo del testo grezzo in una rappresentazione di dati che può essere utilizzata da un classificatore (numerico)?

La risposta sta in un processo noto come vettorizzazione. La vettorizzazione consente la conversione di lunghezze molto variabili di testo grezzo in un formato numerico che può essere elaborato dal classificatore.

Si ottiene questo risultato creando alcuni token da una stringa. Un token è una singola parola (o gruppo di parole) estratta da un documento, utilizzando spazi bianchi o punteggiatura come separatori. Questo può, ovviamente, includere i numeri presenti all’interno della stringa come “parole” aggiuntive. Una volta creato questo elenco di token, è possibile assegnargli un identificatore intero, che consente loro di essere elencati.

Una volta che l’elenco dei token è stato generato, viene conteggiato il numero di token all’interno di un documento. Infine, questi token sono normalizzati per de-enfatizzare i token che appaiono frequentemente all’interno di un documento (come “a”, “the”). Questo processo è noto come Bag Of Words.

La rappresentazione Bag Of Words consente di associare un vettore a ciascun documento, ogni componente del quale è a valore reale e rappresenta l’importanza dei token (cioè “parole”) che compaiono all’interno di quel documento.

Inoltre, significa che dopo aver iterato un intero corpus di documenti (e quindi sono stati valutati tutti i possibili token), il numero totale di token separati è noto e quindi anche la lunghezza del vettore token, ed è anche fisso e identico per qualsiasi documento di qualsiasi lunghezza.

Ciò significa che il classificatore ha una serie di features tramite la frequenza di occorrenza del token. Inoltre il token-vector del documento rappresenta un campione per il classificatore.

In sostanza, l’intero corpus può essere rappresentato come una grande matrice, ogni riga della quale rappresenta uno dei documenti e ogni colonna rappresenta l’occorrenza del token all’interno di quel documento. Questo è il processo di vettorizzazione.

Si noti che la vettorizzazione non tiene conto del posizionamento relativo delle parole all’interno del documento, ma solo della frequenza di occorrenza. Tuttavia, tecniche di apprendimento automatico più sofisticate utilizzano questa informazioni per migliorare la classificazione.

Term-Frequency Inverse Document-Frequency

Uno dei problemi principali con la vettorizzazione, tramite la rappresentazione Bag Of Words, è che c’è molto “rumore” sotto forma di parole di arresto, come “un”, “il”, “lui”, “lei” ecc. Queste parole forniscono poco contesto al documento, ma la loro alta frequenza significa che possono mascherare parole che forniscono contesto al documento.

Ciò motiva un processo di trasformazione, noto come Term-Frequency Inverse Document-Frequency (TF-IDF). Il valore TF-IDF per un token aumenta proporzionalmente alla frequenza della parola nel documento ma è normalizzato dalla frequenza della parola nel corpus. Ciò riduce essenzialmente l’importanza per le parole che appaiono molto in generale, invece di apparire molto all’interno di un particolare documento.

Questo è esattamente ciò di cui abbiamo bisogno in quanto parole come “un”, “il” avranno occorrenze estremamente elevate all’interno dell’intero corpus, ma la parola “gatto” può apparire spesso solo in un particolare documento. Ciò significherebbe che stiamo dando a “gatto” una forza relativa maggiore di “un” o “lui”, per quel documento.

Non mi soffermerò sul calcolo del TF-IDF, ma se sei interessato leggi l’articolo di Wikipedia sull’argomento, che entra più in dettaglio.

Desideriamo quindi combinare il processo di vettorizzazione con quello di TF-IDF per produrre una matrice normalizzata di occorrenze documento-token. Questo verrà quindi utilizzato per fornire un elenco “features” al classificatore su cui allenarsi.

Per fortuna, gli sviluppatori di scikit-learn si sono resi conto che vettorializzare e trasformare i file di testo in questo modo sarebbe stata un’operazione estremamente utile e comune, quindi hanno incluso la classe TfidfVectorizer nella libreria.

Possiamo usare questa classe per prendere il nostro elenco di coppie di tuple che rappresentano le etichette di classe e il testo del documento grezzo, per produrre sia un vettore di etichette di classe che una matrice sparsa, che rappresentano rispettivamente la vettorizzazione applicata ai dati di testo non elaborati e la procedura TF-IDF.

Poiché i classificatori di scikit-learn prendono due strutture di dati separate per l’addestramento, vale a dire, \(y\), il vettore delle etichette di classe o “risposte” associate a un insieme ordinato di documenti, e, \(X\), la matrice sparsa TF-IDF del testo del documento, modifichiamo la nostra lista di coppie di tuple per creare \(y\) e \(X\). Il codice per creare questi oggetti è il seguente:

A questo punto abbiamo due componenti per i nostri dati di addestramento. Il primo,[label]X[/label], è una matrice di occorrenze di token di documento. Il secondo, [label]y[/label], è un vettore (che corrisponde all’ordine della matrice) che contiene le corrette etichette di classe per ciascuno dei documenti. Questo è tutto ciò di cui abbiamo bisogno per iniziare l’addestramento e il test della Support Vector Machine.

Addestrare un Support Vector Machine

Per addestrare la Support Vector Machine è necessario fornirle sia un insieme di features (la matrice [label]X[/label]) sia un insieme di etichette di addestramento “supervisionato”, in questo caso le classi [label]$[/label]. Tuttavia, abbiamo anche bisogno di un mezzo per valutare le prestazioni del classificatore dopo la sua fase di addestramento.

Un approccio consiste nel provare semplicemente a classificare alcuni dei documenti che formano il corpus utilizzato per addestrarlo. Tale procedura di valutazione è nota come test in-sample. Tuttavia, questo non è un meccanismo particolarmente efficace per valutare le prestazioni del sistema.

In poche parole, il classificatore ha già “visto” questi dati e gli è stato detto come agire su di essi, quindi è molto probabile che classifichi correttamente il documento. Questo quasi certamente sovrastimerà le reali prestazioni di test out-of-sample. Quindi dobbiamo fornire al classificatore i dati che non ha utilizzato per l’addestramento, come mezzo di test più realistico.

Tuttavia, non è chiaro da dove ottenere questi nuovi dati. Un approccio potrebbe essere quello di creare un nuovo corpus con alcuni nuovi dati. Tuttavia, in realtà è probabile che ciò sia costoso in termini di tempo e / o processi aziendali. Un approccio alternativo consiste nel suddividere l’insieme di addestramento in due sottoinsiemi distinti, uno dei quali viene utilizzato per l’addestramento e l’altro per i test. Questo è noto come training-test split.

Tale partizione ci consente di addestrare il classificatore esclusivamente sulla prima partizione e quindi di classificare le sue prestazioni con la seconda partizione. Questo permette di avere una visione migliore circa le possibili prestazioni future con dati reali “out-of-sample”.

A questo punto ci si può domandare quale percentuale dei dati utilizzare per l’addestramento e quale per i  test. Chiaramente quanto più viene utilizzato per l’addestramento, tanto “migliore” sarà il classificatore perché avrà visto più dati. Tuttavia, più dati di addestramento significano meno dati di test e di conseguenza una stima più scarsa della sua reale capacità di classificazione. In pratica, è comune prevedere circa il 70-80% dei dati per l’addestramento e utilizzare il resto per i test.

Dato che il training-test split è un’operazione così comune nell’apprendimento automatico, gli sviluppatori di scikit-learn hanno fornito il metodo train_test_split per creare automaticamente la divisione da un dataset di input. Ecco il codice che fornisce la suddivisione:

L’argomento della parola chiave test_size controlla la dimensione del set di test, in questo caso il 20%. L’argomento della parola chiave random_state controlla il fonte casuale per la selezione casuale della partizione.

Il passaggio successivo consiste nel creare effettivamente la Support Vector Machine e addestrarla. In questo caso useremo la classe SVC (Support Vector Classifier) ​​di scikit-learn. Gli diamo i parametri [label]C = 1000000.0[/label], [label]\gamma = 0.0[/label] e scegliamo un kernel radiale. Per capire da dove provengono questi parametri, consultare l’articolo su Support Vector Machines.

Il codice seguente importa la classe SVC e quindi la adatta ai dati di addestramento:

Ora che l’SVM è stata addestrata, dobbiamo valutarne le prestazioni sui dati di test.

Term-Frequency Inverse Document-Frequency

Le due principali metriche delle prestazioni che prenderemo in considerazione per questo classificatore supervisionato sono il tasso di successo(hit-rate) e la confusion-matrix. Il primo è semplicemente il rapporto tra le associazioni corrette e le associazioni totali ed è solitamente espresso in percentuale.

La matrice di confusione entra più in dettaglio e fornisce statistiche sui veri positivi, veri negativi, falsi positivi e falsi negativi. In un sistema di classificazione binario, con un’etichettatura di classe “vero” o “falso”, questi caratterizzano la velocità con cui il classificatore classifica correttamente qualcosa come vero o falso quando è, rispettivamente, vero o falso, e classifica anche erroneamente qualcosa come vero o falso quando è, rispettivamente, falso o vero.

Una matrice di confusione non deve essere limitata a una situazione di classificatore binario. Per più gruppi di classi (come nel nostro esempio con il dataset di Reuters) avremo una matrice \(N\times N\), dove \(N\) è il numero di etichette di classe (o argomenti del documento).

Scikit-learn ha funzioni sia per il calcolo dell hit-rate sia per la matrice di confusione di un classificatore supervisionato. Il primo è un metodo dello stesso classificatore chiamato score. Quest’ultimo deve essere importato dalla libreria metrics.

La prima attività è creare un array di previsioni dal set di test X_test. Questo conterrà semplicemente le etichette di classe previste dall’SVM tramite il set di dati previsto per il test (20%). Questo array di previsione viene utilizzata per creare la matrice di confusione. Da notare che la funzione confusion_matrix accetta sia l’array di previsione pred sia le etichette della classe corretta y_test per produrre la matrice. Inoltre, si crea l’hit-rate tramite lo score di entrambi i sottoinsiemi X_test e y_test del set di dati:

L’output dello script è il seguente:
Quindi abbiamo un tasso di successo della classificazione del 66%, con una matrice di confusione che ha voci principalmente sulla diagonale (cioè la corretta assegnazione dell’etichetta di classe). Si noti che poiché stiamo utilizzando solo un singolo file dal set Reuters (numero 000), non vedremo l’intero set di etichette di classe e quindi la nostra matrice di confusione è di dimensioni inferiori rispetto a quella in cui avessimo usato l’intero set di dati. Per utilizzare il set di dati completo, possiamo modificare la funzione __main__ per caricare tutti i 21 file Reuters e addestrare SVM sul set di dati completo. Possiamo quindi calcolare la completa performance dell’hit-rate. Ho trascurato di includere l’output della matrice di confusione poiché è di grandi dimensioni a causa del numero totale di etichette di classe all’interno di tutti i documenti. Da notare che ci vorrà del tempo! Sul mio sistema sono necessari ci vogliono circa 30-45 per completare l’esecuzione.

Per tutti i corpus, l’hit-rate del sistema è: 

Ci sono molti modi per migliorare questo valore. In particolare, possiamo eseguire una Grid Search Cross-Validation, che è un metodo per determinare i parametri ottimali per il classificatore in modo da raggiungere il miglior tasso di successo (o altra metrica di scelta).

Negli articoli successivi discuteremo tali procedure di ottimizzazione e spiegheremo come un classificatore come questo può essere aggiunto a un sistema di produzione in un contesto di data-science o di finanza quantitativa.

Codice completo dell'implementazione in Python

Di seguito il codice completo per reuters_svm.py, scritto in Python 3.7.x:

Gli altri articoli di questa serie

Benvenuto su DataTrading!

Sono Gianluca, ingegnere software e data scientist. Sono appassionato di coding, finanza e trading. Leggi la mia storia.

Ho creato DataTrading per aiutare le altre persone ad utilizzare nuovi approcci e nuovi strumenti, ed applicarli correttamente al mondo del trading.

DataTrading vuole essere un punto di ritrovo per scambiare esperienze, opinioni ed idee.

SCRIVIMI SU TELEGRAM

Per informazioni, suggerimenti, collaborazioni...

Torna in alto
Scroll to Top