Perché la rete neurale prevede errori sui propri dati di allenamento?


17

Ho realizzato una rete neurale LSTM (RNN) con apprendimento supervisionato per la previsione dello stock di dati. Il problema è perché prevede errori sui propri dati di allenamento? (nota: esempio riproducibile di seguito)

Ho creato un modello semplice per prevedere il prezzo delle azioni nei prossimi 5 giorni:

model = Sequential()
model.add(LSTM(32, activation='sigmoid', input_shape=(x_train.shape[1], x_train.shape[2])))
model.add(Dense(y_train.shape[1]))
model.compile(optimizer='adam', loss='mse')

es = EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True)
model.fit(x_train, y_train, batch_size=64, epochs=25, validation_data=(x_test, y_test), callbacks=[es])

I risultati corretti sono in y_test(5 valori), quindi modella i treni, guardando indietro di 90 giorni precedenti e poi ripristina i pesi dal miglior val_loss=0.0030risultato ( ) con patience=3:

Train on 396 samples, validate on 1 samples
Epoch 1/25
396/396 [==============================] - 1s 2ms/step - loss: 0.1322 - val_loss: 0.0299
Epoch 2/25
396/396 [==============================] - 0s 402us/step - loss: 0.0478 - val_loss: 0.0129
Epoch 3/25
396/396 [==============================] - 0s 397us/step - loss: 0.0385 - val_loss: 0.0178
Epoch 4/25
396/396 [==============================] - 0s 399us/step - loss: 0.0398 - val_loss: 0.0078
Epoch 5/25
396/396 [==============================] - 0s 391us/step - loss: 0.0343 - val_loss: 0.0030
Epoch 6/25
396/396 [==============================] - 0s 391us/step - loss: 0.0318 - val_loss: 0.0047
Epoch 7/25
396/396 [==============================] - 0s 389us/step - loss: 0.0308 - val_loss: 0.0043
Epoch 8/25
396/396 [==============================] - 0s 393us/step - loss: 0.0292 - val_loss: 0.0056

Il risultato della previsione è fantastico, no?

inserisci qui la descrizione dell'immagine

Questo perché l'algoritmo ha ripristinato i pesi migliori dall'epoca n. 5. Okey, ora salviamo questo modello su .h5file, torniamo indietro di -10 giorni e prevediamo gli ultimi 5 giorni (nel primo esempio abbiamo creato il modello e convalidato il 17-23 aprile compresi i fine settimana, ora testiamo il 2-8 aprile). Risultato:

inserisci qui la descrizione dell'immagine

Mostra una direzione assolutamente sbagliata. Come vediamo, questo è perché il modello è stato addestrato e ha preso l'epoca n. 5 per la validazione fissata il 17-23 aprile, ma non il 2-8. Se provo ad allenarmi di più, giocando con quale epoca scegliere, qualunque cosa faccia, ci sono sempre molti intervalli di tempo in passato che hanno previsioni sbagliate.

Perché il modello mostra risultati errati sui propri dati addestrati? Ho addestrato i dati, deve ricordare come prevedere i dati su questo pezzo di set, ma predice male. Cosa ho anche provato:

  • Utilizza set di dati di grandi dimensioni con oltre 50.000 righe, 20 anni di quotazioni di borsa, aggiungendo più o meno funzionalità
  • Crea diversi tipi di modello, come l'aggiunta di più livelli nascosti, diverse dimensioni batch, attivazioni di diversi livelli, dropout, normalizzazione batch
  • Crea callback personalizzati EarlyStopping, ottieni val_loss medio da molti set di dati di validazione e scegli il migliore

Forse mi manca qualcosa? Cosa posso migliorare?

Ecco un esempio molto semplice e riproducibile . yfinancescarica i dati di stock di S&P 500.

"""python 3.7.7
tensorflow 2.1.0
keras 2.3.1"""


import numpy as np
import pandas as pd
from keras.callbacks import EarlyStopping, Callback
from keras.models import Model, Sequential, load_model
from keras.layers import Dense, Dropout, LSTM, BatchNormalization
from sklearn.preprocessing import MinMaxScaler
import plotly.graph_objects as go
import yfinance as yf
np.random.seed(4)


num_prediction = 5
look_back = 90
new_s_h5 = True # change it to False when you created model and want test on other past dates


df = yf.download(tickers="^GSPC", start='2018-05-06', end='2020-04-24', interval="1d")
data = df.filter(['Close', 'High', 'Low', 'Volume'])

# drop last N days to validate saved model on past
df.drop(df.tail(0).index, inplace=True)
print(df)


class EarlyStoppingCust(Callback):
    def __init__(self, patience=0, verbose=0, validation_sets=None, restore_best_weights=False):
        super(EarlyStoppingCust, self).__init__()
        self.patience = patience
        self.verbose = verbose
        self.wait = 0
        self.stopped_epoch = 0
        self.restore_best_weights = restore_best_weights
        self.best_weights = None
        self.validation_sets = validation_sets

    def on_train_begin(self, logs=None):
        self.wait = 0
        self.stopped_epoch = 0
        self.best_avg_loss = (np.Inf, 0)

    def on_epoch_end(self, epoch, logs=None):
        loss_ = 0
        for i, validation_set in enumerate(self.validation_sets):
            predicted = self.model.predict(validation_set[0])
            loss = self.model.evaluate(validation_set[0], validation_set[1], verbose = 0)
            loss_ += loss
            if self.verbose > 0:
                print('val' + str(i + 1) + '_loss: %.5f' % loss)

        avg_loss = loss_ / len(self.validation_sets)
        print('avg_loss: %.5f' % avg_loss)

        if self.best_avg_loss[0] > avg_loss:
            self.best_avg_loss = (avg_loss, epoch + 1)
            self.wait = 0
            if self.restore_best_weights:
                print('new best epoch = %d' % (epoch + 1))
                self.best_weights = self.model.get_weights()
        else:
            self.wait += 1
            if self.wait >= self.patience or self.params['epochs'] == epoch + 1:
                self.stopped_epoch = epoch
                self.model.stop_training = True
                if self.restore_best_weights:
                    if self.verbose > 0:
                        print('Restoring model weights from the end of the best epoch')
                    self.model.set_weights(self.best_weights)

    def on_train_end(self, logs=None):
        print('best_avg_loss: %.5f (#%d)' % (self.best_avg_loss[0], self.best_avg_loss[1]))


def multivariate_data(dataset, target, start_index, end_index, history_size, target_size, step, single_step=False):
    data = []
    labels = []
    start_index = start_index + history_size
    if end_index is None:
        end_index = len(dataset) - target_size
    for i in range(start_index, end_index):
        indices = range(i-history_size, i, step)
        data.append(dataset[indices])
        if single_step:
            labels.append(target[i+target_size])
        else:
            labels.append(target[i:i+target_size])
    return np.array(data), np.array(labels)


def transform_predicted(pr):
    pr = pr.reshape(pr.shape[1], -1)
    z = np.zeros((pr.shape[0], x_train.shape[2] - 1), dtype=pr.dtype)
    pr = np.append(pr, z, axis=1)
    pr = scaler.inverse_transform(pr)
    pr = pr[:, 0]
    return pr


step = 1

# creating datasets with look back
scaler = MinMaxScaler()
df_normalized = scaler.fit_transform(df.values)
dataset = df_normalized[:-num_prediction]
x_train, y_train = multivariate_data(dataset, dataset[:, 0], 0,len(dataset) - num_prediction + 1, look_back, num_prediction, step)
indices = range(len(dataset)-look_back, len(dataset), step)
x_test = np.array(dataset[indices])
x_test = np.expand_dims(x_test, axis=0)
y_test = np.expand_dims(df_normalized[-num_prediction:, 0], axis=0)

# creating past datasets to validate with EarlyStoppingCust
number_validates = 50
step_past = 5
validation_sets = [(x_test, y_test)]
for i in range(1, number_validates * step_past + 1, step_past):
    indices = range(len(dataset)-look_back-i, len(dataset)-i, step)
    x_t = np.array(dataset[indices])
    x_t = np.expand_dims(x_t, axis=0)
    y_t = np.expand_dims(df_normalized[-num_prediction-i:len(df_normalized)-i, 0], axis=0)
    validation_sets.append((x_t, y_t))


if new_s_h5:
    model = Sequential()
    model.add(LSTM(32, return_sequences=False, activation = 'sigmoid', input_shape=(x_train.shape[1], x_train.shape[2])))
    # model.add(Dropout(0.2))
    # model.add(BatchNormalization())
    # model.add(LSTM(units = 16))
    model.add(Dense(y_train.shape[1]))
    model.compile(optimizer = 'adam', loss = 'mse')

    # EarlyStoppingCust is custom callback to validate each validation_sets and get average
    # it takes epoch with best "best_avg" value
    # es = EarlyStoppingCust(patience = 3, restore_best_weights = True, validation_sets = validation_sets, verbose = 1)

    # or there is keras extension with built-in EarlyStopping, but it validates only 1 set that you pass through fit()
    es = EarlyStopping(monitor = 'val_loss', patience = 3, restore_best_weights = True)

    model.fit(x_train, y_train, batch_size = 64, epochs = 25, shuffle = True, validation_data = (x_test, y_test), callbacks = [es])
    model.save('s.h5')
else:
    model = load_model('s.h5')



predicted = model.predict(x_test)
predicted = transform_predicted(predicted)
print('predicted', predicted)
print('real', df.iloc[-num_prediction:, 0].values)
print('val_loss: %.5f' % (model.evaluate(x_test, y_test, verbose=0)))


fig = go.Figure()
fig.add_trace(go.Scatter(
    x = df.index[-60:],
    y = df.iloc[-60:,0],
    mode='lines+markers',
    name='real',
    line=dict(color='#ff9800', width=1)
))
fig.add_trace(go.Scatter(
    x = df.index[-num_prediction:],
    y = predicted,
    mode='lines+markers',
    name='predict',
    line=dict(color='#2196f3', width=1)
))
fig.update_layout(template='plotly_dark', hovermode='x', spikedistance=-1, hoverlabel=dict(font_size=16))
fig.update_xaxes(showspikes=True)
fig.update_yaxes(showspikes=True)
fig.show()

3
Gli esempi riproducibili sono così rari al giorno d'oggi (contrariamente ai gazzilioni di domande simili senza) che è probabilmente una buona idea pubblicizzare la sua esistenza all'inizio del tuo post (aggiunto);)
desertnaut

7
Il problema potrebbe essere che ti aspetti troppa prevedibilità dal mercato azionario. Se hai addestrato un modello su una sequenza di 1 milione di lanci di monete e poi hai cercato di prevederlo per prevedere i lanci di monete, non sarebbe sorprendente per il modello sbagliarlo, anche se i lanci provenivano dai dati di addestramento - il modello non è previsto che memorizzi i suoi dati di allenamento e li rigurgiti.
user2357112 supporta Monica il

2
Oltre a ciò che ha detto @ user2357112supportsMonica, il tuo modello ha ottenuto la media giusta, che è davvero tutto ciò che mi aspetto che un modello come questo ottenga davvero (almeno con qualsiasi coerenza), e ti aspetti troppo da 5 giorni dati. Hai davvero bisogno di molti più dati per poter dire con qualsiasi significato quale sia l'errore nel tuo modello.
Aaron,

Ci sono molti più parametri per mettere a punto il modello. Ho provato un paio di loro come l'arresto anticipato (pazienza = 20), aumento del numero di epoche, aumento delle unità di lstm da 32 a 64 ecc. controlla qui github.com/jvishnuvardhan/Stackoverflow_Questions/blob/master/… . Come accennato da @sirjay aggiungendo più funzioni (attualmente solo 4), aggiungendo più livelli (lstm, batchnorm, dropout, ecc.), L'esecuzione dell'ottimizzazione dei parametri multipli comporterebbe prestazioni molto migliori.
Vishnuvardhan Janapati,

@VishnuvardhanJanapati grazie per il controllo. Ho compilato il tuo codice, salvato il modello, quindi impostato df.drop(df.tail(10).index, inplace=True), ha mostrato lo stesso brutto risultato che avevo.
sabato

Risposte:


5

Il PO postula una scoperta interessante. Consentitemi di semplificare la domanda originale come segue.

Se il modello viene addestrato su una determinata serie storica, perché non può ricostruire i dati delle serie storiche precedenti, su cui era già addestrato?

Bene, la risposta è integrata nel progresso della formazione stessa. Poiché EarlyStoppingviene utilizzato qui per evitare un eccesso di adattamento, viene salvato il modello migliore epoch=5, dove val_loss=0.0030indicato dall'OP. In questo caso, la perdita di allenamento è uguale 0.0343, vale a dire l'RMSE dell'allenamento 0.185. Poiché il set di dati viene ridimensionato utilizzando MinMaxScalar, è necessario annullare il ridimensionamento di RMSE per capire cosa sta succedendo.

I valori minimo e massimo della sequenza temporale risultano essere 2290e 3380. Pertanto, avere 0.185come RMSE dell'addestramento significa che, anche per l'insieme di addestramento, i valori previsti possono differire dai valori di verità di base di circa 0.185*(3380-2290), cioè ~200in media unità.

Questo spiega perché esiste una grande differenza nella previsione dei dati di allenamento stessi in un passaggio temporale precedente.

Cosa devo fare per emulare perfettamente i dati di allenamento?

Ho fatto questa domanda da solo. La semplice risposta è, avvicinare la perdita di allenamento 0, che è troppo adatta al modello.

Dopo un po 'di allenamento, mi sono reso conto che un modello con solo 1 livello LSTM con 32celle non è abbastanza complesso per ricostruire i dati di allenamento. Pertanto, ho aggiunto un altro livello LSTM come segue.

model = Sequential()
model.add(LSTM(32, return_sequences=True, activation = 'sigmoid', input_shape=(x_train.shape[1], x_train.shape[2])))
# model.add(Dropout(0.2))
# model.add(BatchNormalization())
model.add(LSTM(units = 64, return_sequences=False,))
model.add(Dense(y_train.shape[1]))
model.compile(optimizer = 'adam', loss = 'mse')

E il modello è addestrato per 1000epoche senza considerare EarlyStopping.

model.fit(x_train, y_train, batch_size = 64, epochs = 1000, shuffle = True, validation_data = (x_test, y_test))

Alla fine 1000dell'epoca abbiamo una perdita di allenamento 0.00047che è molto inferiore alla perdita di allenamento nel tuo caso. Quindi ci aspetteremmo che il modello ricostruisca meglio i dati di addestramento. Di seguito è riportato il diagramma di previsione per 2-8 aprile.

predizione

Una nota finale:

La formazione su un particolare database non significa necessariamente che il modello dovrebbe essere in grado di ricostruire perfettamente i dati di formazione. In particolare, quando vengono introdotti metodi come l'arresto anticipato, la regolarizzazione e l'abbandono per evitare un eccesso di adattamento, il modello tende a essere più generalizzabile piuttosto che a memorizzare i dati di allenamento.


Sono curioso. Perché hai raccomandato una dimensione del lotto di 64 per un overfitting (rispetto a 1)? E perché shuffle = True? Non ci vorrebbe solo più tempo per convergere alla soluzione?
Daniel Scott

2

Perché il modello mostra risultati errati sui propri dati addestrati? Ho addestrato i dati, deve ricordare come prevedere i dati su questo pezzo di set, ma predice male.

Volete che il modello apprenda la relazione tra input e output invece della memorizzazione. Se un modello memorizza l'output corretto per ciascun input, possiamo dire che è troppo adatto ai dati di training. Spesso è possibile forzare l'overfit del modello utilizzando un piccolo sottoinsieme dei dati, quindi se questo è il comportamento che si desidera vedere, è possibile provarlo.


2

Sospetto n. 1: regolarizzazione

Le reti neurali sono ottime per il sovradimensionamento dei dati di allenamento, in realtà c'è un esperimento che sostituisce le etichette CIFAR10 (attività di classificazione delle immagini) (valori y) con etichette casuali sul set di dati di allenamento e la rete si adatta alle etichette casuali con una perdita quasi zero.

inserisci qui la descrizione dell'immagine

sul lato sinistro possiamo vedere che date abbastanza epoche le etichette casuali aggirano la perdita 0 - punteggio perfetto (dalla comprensione del deep learning è necessario ripensare la generalizzazione di zhang et al 2016 )

Quindi perché non sta accadendo tutto il tempo? regolarizzazione .

la regolarizzazione sta (approssimativamente) cercando di risolvere un problema più difficile del problema di ottimizzazione (la perdita) che abbiamo definito per il modello.

alcuni metodi di regolarizzazione comuni nelle reti neurali:

  • arresto anticipato
  • buttare fuori
  • normalizzazione in lotti
  • riduzione del peso (ad es. norme l1 l2)
  • aumento dei dati
  • aggiungendo rumore casuale / gaussiano

questi metodi aiutano a ridurre il sovradimensionamento e di solito comportano una migliore convalida e prestazioni di prova, ma comportano una riduzione delle prestazioni del treno (che non importa in realtà come spiegato nell'ultimo paragrafo).

le prestazioni dei dati del treno di solito non sono così importanti e per questo usiamo il set di validazione.

Sospetto n. 2 - Dimensioni del modello

stai usando un singolo livello LSTM con 32 unità. è piuttosto piccolo. prova ad aumentare le dimensioni e persino a mettere due livelli LSTM (o bidirezionale) e sono sicuro che il modello e l'ottimizzatore si adatteranno ai tuoi dati finché li lascerai - cioè rimuovi l'arresto anticipato, restore_last_weights e qualsiasi altra regolarizzazione sopra specificata.

Nota sulla complessità del problema

cercare di prevedere i futuri prezzi delle azioni semplicemente osservando la storia non è un compito facile, e anche se il modello può adattarsi perfettamente al set di addestramento, probabilmente non farà nulla di utile sul set di test o nel mondo reale.

ML non è magia nera, i campioni x devono essere correlati in qualche modo ai tag y, di solito assumiamo che (x, y) siano disegnati da una certa distribuzione insieme.

Un modo più intuitivo per pensarci, quando è necessario taggare un'immagine manualmente per la classe cane / gatto - piuttosto semplice. ma puoi "taggare" manualmente il prezzo delle azioni osservando da solo la storia di quella borsa?

Questa è un'intuizione su quanto sia difficile questo problema.

Nota sull'adattamento eccessivo

Non si dovrebbe inseguire la prestazione di allenamento superiore è quasi inutile per provare a sovrautilizzare i dati di allenamento, poiché di solito si cerca di ottenere buoni risultati con un modello su nuovi dati invisibili con proprietà simili ai dati del treno. l'idea generale è cercare di generalizzare e apprendere le proprietà dei dati e la correlazione con il target, ecco cos'è l'apprendimento :)


1

Fondamentalmente Se si desidera ottenere risultati migliori per i dati di allenamento, la precisione di allenamento dovrebbe essere il più elevata possibile. Dovresti usare un modello migliore rispetto ai dati che hai. Fondamentalmente dovresti controllare se la tua precisione di allenamento per questo scopo indipendentemente dall'accuratezza del test. Questo è anche chiamato come overfitting che offre una maggiore precisione nei dati di allenamento piuttosto che nei dati di test.

L'arresto anticipato potrebbe influire su questo scenario in cui viene presa la migliore precisione di test / convalida anziché l'accuratezza dell'allenamento.


1

La breve risposta:

Impostato:

batch_size = 1
epochs = 200
shuffle = False

Intuizione: stai descrivendo la priorità dell'alta precisione nei dati di allenamento. Questo sta descrivendo il sovradimensionamento. Per fare ciò, impostare la dimensione del lotto su 1, le epoche alte e rimescolare.


1

Dopo aver modificato l'architettura del modello e l'ottimizzatore in Adagrad, sono stato in grado di migliorare i risultati in una certa misura.

Il motivo per utilizzare l'ottimizzatore Adagrad qui è:

Adatta il tasso di apprendimento ai parametri, eseguendo aggiornamenti più piccoli (ovvero tassi di apprendimento bassi) per i parametri associati a funzioni ricorrenti e aggiornamenti più grandi (ovvero tassi di apprendimento elevati) per i parametri associati a funzioni poco frequenti. Per questo motivo, è adatto per gestire dati sparsi.

Si prega di fare riferimento al codice seguente:

model = Sequential()
model.add(LSTM(units=100,return_sequences=True, kernel_initializer='random_uniform', input_shape=(x_train.shape[1], x_train.shape[2])))
model.add(Dropout(0.2))
model.add(LSTM(units=100,return_sequences=True, kernel_initializer='random_uniform'))
model.add(LSTM(units=100,return_sequences=True, kernel_initializer='random_uniform'))
model.add(Dropout(0.20))
model.add(Dense(units=25, activation='relu'))
model.add(Dense(y_train.shape[1]))

# compile model
model.compile(loss="mse", optimizer='adagrad', metrics=['accuracy'])
model.summary()

La previsione di borsa è un compito molto impegnativo, quindi piuttosto che attenerci alla previsione di un singolo modello, possiamo avere diversi modelli che lavorano insieme per fare una previsione e quindi sulla base del risultato massimo votato rispondere alla chiamata, simile a un approccio di apprendimento d'insieme. Inoltre, possiamo impilare insieme alcuni modelli come:

  1. Rete neurale Auto-Encoder Deep Feed-forward per ridurre le dimensioni + Rete neurale ricorrente profonda + ARIMA + Extreme Boosting Gradient Regressor

  2. Adaboost + Insacco + Alberi extra + Aumento gradiente + Foresta casuale + XGB

Gli agenti per l'apprendimento del rinforzo stanno facendo abbastanza bene nella previsione degli stock come:

  1. Agente di commercio di tartarughe
  2. Agente a media mobile
  3. Agente di rotolamento del segnale
  4. Agente con gradiente politico
  5. Agente di Q-learning
  6. Agente di strategia dell'evoluzione

Trova un link molto intraprendente qui .


Adamo ha anche queste proprietà, in realtà Adamo è una sorta di evoluzione di Adagrad
ShmulikA

1

Come altri hanno già detto, non dovresti aspettarti molto da questo.

Tuttavia, ho trovato quanto segue nel tuo codice:

  1. Si sono ri-montaggio lo scaler ogni volta durante l'allenamento e test. È necessario salvare il sacler e trasformare i dati solo durante il test, altrimenti i risultati saranno leggermente diversi:

    from sklearn.externals import joblib
    scaler_filename = "scaler.save"
    if new_s_h5:
        scaler = MinMaxScaler()
        df_normalized = scaler.fit_transform(df.values)
        joblib.dump(scaler, scaler_filename)
    
    else:
        scaler = joblib.load(scaler_filename)
        df_normalized = scaler.transform(df.values)
  2. Set shuffle=False. Poiché è necessario mantenere l'ordine del set di dati.

  3. Set batch_size=1. Poiché sarà meno soggetto a sovralimentazione e l'apprendimento sarà più rumoroso e l'errore sarà mediato.

  4. Set epochs=50o più.


Con le impostazioni sopra menzionate, il modello raggiunto loss: 0.0037 - val_loss: 3.7329e-04.

Controllare i seguenti esempi di previsione:

Dal 17/04/2020 -> 23/04/2020:

inserisci qui la descrizione dell'immagine

Dal 02/04/2020 -> 08/04/2020:

inserisci qui la descrizione dell'immagine

Dal 25/03/2020 -> 31/03/2020:

inserisci qui la descrizione dell'immagine


0

È inadatto e per migliorare quella cosa che devi aggiungere i neuroni nei tuoi strati nascosti. !! Un altro punto è provare la funzione di attivazione 'relu'. Sigmoid non dà buoni risultati. Inoltre è necessario definire "softmax" nel livello di output.!


Sembra che tu abbia i segreti per predire il mercato. Cos'altro dovrebbe fare?
Daniel Scott

2
softmax è per la classificazione, è un problema di regressione.
ShmulikA

2
@DanielScott non capisci. Nel profondo (miliardi di strati sotto) c'è un problema di classificazione che decide tra profitti o perdite. Perché anche preoccuparsi di prevedere una serie temporale?
Sowmya

@Sowmya Mi piace il tuo umorismo. ;)
Daniel Scott

-1

Perché il modello mostra risultati errati sui propri dati addestrati? Ho addestrato i dati, deve ricordare come prevedere i dati su questo pezzo di set, ma predice male.

Guarda cosa stai facendo:

  1. Costruire un modello con alcuni livelli
  2. Modello di allenamento con training_data
  3. Quando hai allenato il modello, tutti i parametri addestrabili vengono allenati (cioè, i pesi del modello sono stati salvati)
  4. Questi pesi ora rappresentano la relazione tra input e output.
  5. Quando si prevede nuovamente lo stesso training_data, questa volta il modello addestrato utilizza i pesi per ottenere l'output.
  6. La qualità del tuo modello ora decide le previsioni e quindi sono diverse dai risultati originali anche se i dati sono gli stessi.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.