Utilizzo della Cross-Validation per ottimizzare un metodo di Machine Learning: configurazione della Regressione

cross-validation-machine-learning-trading-algoritmico

Una delle aree più problematiche del trading quantitativo è l’ottimizzazione di una strategia di previsione per migliorarne le prestazioni.

I trader quantistici esperti sono ben consapevoli che è fin troppo facile generare una strategia con capacità predittive stellari durante un backtest. Tuttavia, alcuni backtest possono mascherare il pericolo di un modello overfit , che può portare a una drastica sottoperformance quando viene implementata una strategia.

In questo articolo descriviamo un approccio per ridurre il problema dell’overfitting di un modello di machine learning, utilizzando una tecnica nota come cross-validation.

Per prima cosa introduciamo la definizione della cross-validation e poi descriviamo il funzionamento. In secondo luogo, costruiamo un modello di previsione utilizzando un indice azionario e quindi applichiamo due metodi di validazione incrociata a questo esempio: il validation set approach e k-fold cross-validation. Infine discuteremo il codice per le simulazioni utilizzando Python, Pandas , Matplotlib e Scikit-Learn .

Questo articolo è il “successore spirituale” di un precedente articolo scritto di recente sul compromesso tra bias e varianza . In quell’articolo abbiamo menzionato la convalida incrociata come un mezzo per risolvere alcuni dei problemi causati dal compromesso bias-varianza.

Il nostro obiettivo è infine creare una serie di strumenti statistici che possono essere utilizzati all’interno di un motore di backtesting per aiutarci a ridurre al minimo il problema dell’overfitting di un modello e quindi limitare le perdite future a causa di una strategia scarsamente performante.

Panoramica della Cross-Validation

Nel precedente articolo sul compromesso bias-varianza sono state introdotte le definizioni di errore di test e flessibilità:

  • Errore di test: l’errore medio, dove la media è calcolata tra molte osservazioni, associato alle prestazioni predittive di un particolare modello statistico quando è valutato su nuove osservazioni che non sono state utilizzate per addestrare il modello .
  • Flessibilità : i gradi di libertà a disposizione del modello per “adattarsi” ai dati di addestramento. Una regressione lineare è molto rigida (ha solo due gradi di libertà) mentre un polinomio di alto grado è molto flessibile (e come tale può avere molti gradi di libertà).

Con questi concetti in mente possiamo ora definire la cross-validation:

L’obiettivo della cross-validation è stimare l’errore di test associato a un modello statistico o selezionare il livello di flessibilità appropriato per un particolare metodo statistico.

Ancora una volta, possiamo ricordare dall’articolo sul compromesso bias-varianza che l’ errore di addestramento associato a un modello può sottovalutare notevolmente l’errore di test del modello. La convalida incrociata ci fornisce la capacità di stimare più accuratamente l’errore di test, che non conosceremo mai nella pratica.

La convalida incrociata consiste nell’escludere specifici sottoinsiemi dai dati di addestramento al fine di usarli come osservazioni di test. In questo articolo discuteremo i vari modi in cui tali sottoinsiemi vengono distribuiti e implementeremo i metodi usando Python su un modello di previsione di esempio basato su dati storici precedenti.

Esempio di previsione

Per rendere concreta la seguente discussione teorica prenderemo in considerazione lo sviluppo di una nuova strategia di trading basata sulla previsione dei livelli di prezzo di un indice azionario. Prenderemo in considerazione l’indicie S&P500, che contiene un raggruppamento ponderato delle cinquecento aziende più grandi società quotate (per capitalizzazione di mercato) a Wall Street. Allo stesso modo potremmo scegliere l’Euro Stoxx 50  o il DAX .

Per questa strategia considereremo semplicemente il prezzo di chiusura delle barre giornaliere storiche Open-High-Low-Close (OHLC) come predittori e il prezzo di chiusura del giorno successivo come risposta. Quindi stiamo tentando di prevedere il prezzo di domani utilizzando i prezzi storici giornalieri.

Un’osservazione sarà costituito da una coppia di vettori , \(X\) e \(y\), che contengono rispettivamente i valori predittori e il valore di risposta. Se consideriamo un ritardo giornaliero di \(p\) giorni, \(X\) ha \(p\) componenti. Ciascuno di questi componenti rappresenta il prezzo di chiusura di un giorno precedente. \(X_p\) rappresenta il prezzo di chiusura di oggi (noto), mentre \(X_{p-1}\) rappresenta il prezzo di chiusura di ieri, mentre \(X_1\) rappresenta il prezzo di \(p-1\) giorni fa.

\(Y\) contiene un solo valore, vale a dire il prezzo di chiusura di domani, ed è quindi uno scalare. Ogni osservazione è una tupla \((X, y)\). Considereremo una serie di \(n\) osservazioni corrispondenti a \(n\) giorni di informazioni storiche sui prezzi del SP500.

Il nostro obiettivo è trovare un modello statistico che tenti di prevedere il livello di prezzo del SP500 in base ai prezzi dei giorni precedenti. Se dovessimo ottenere una previsione accurata, potremmo usarla per generare segnali di trading di base. Questo articolo è principalmente interessato alla parte precedente del modello, quella della componente predittiva.

Useremo la convalida incrociata in due modi: in primo luogo per stimare l’errore di test di particolari metodi di apprendimento statistico (cioè le loro separate prestazioni predittive), e in secondo luogo per selezionare la flessibilità ottimale del metodo scelto al fine di minimizzare gli errori associati a bias e varianza.

Descriveremo ora i diversi modi di eseguire la convalida incrociata, iniziando con l’ approccio dell’insieme di convalida e poi infine con la convalida incrociata k-fold . In ogni caso useremo Pandas e Scikit-Learn per implementare questi metodi.

Validation Set Approach

L’approccio del set di convalida alla cross-validation è molto semplice da eseguire. Essenzialmente prendiamo l’insieme delle osservazioni (\(n\) giorni di dati) e le dividiamo casualmente in due metà. Una metà è nota come set di addestramento mentre la seconda metà è nota come set di convalida . Il modello si adatta utilizzando solo i dati nel set di addestramento, mentre il suo errore di test viene stimato utilizzando solo il set di convalida.

Questo è facilmente riconoscibile come una tecnica spesso utilizzata nel trading quantitativo come meccanismo per valutare le prestazioni predittive. Tuttavia, è più comune trovare due terzi dei dati utilizzati per il set di addestramento, mentre il terzo rimanente viene utilizzato per la convalida. Inoltre è più comune mantenere l’ordine delle serie temporali in modo tale che i primi due terzi rappresentino cronologicamente i primi due terzi dei dati storici.

Meno frequente è l’applicazione di questo metodo per la randomizzazione delle osservazioni in ciascuno dei due set. Ancora meno frequente è una discussione su quali sottili problemi possono sorgere quando si esegue questa randomizzazione.

In primo luogo, e soprattutto in situazioni con dati limitati, la procedura può portare ad un’elevata varianza per la stima dell’errore di test dovuto alla randomizzazione dei campioni. Questo è un tipico “trucco” quando si esegue l’approccio del set di convalida alla cross-validation. È fin troppo facile ottenere un basso errore di test semplicemente tramite un caso fortunato nel dividere appropriatamente attraverso la fortuna cieca nel ricevere una divisione del campione casuale appropriata. Quindi il vero errore di test (cioè il potere predittivo) può essere notevolmente sottostimato .

In secondo luogo, si noti che nella divisione 50-50 dei dati  addestramento/testing tralasciamo la metà di tutte le osservazioni. Quindi stiamo riducendo le informazioni che altrimenti sarebbero utilizzate per addestrare il modello. Quindi è probabile che abbia prestazioni peggiori rispetto a se avessimo usato tutte le osservazioni, comprese quelle nel set di convalida. Ciò porta a una situazione in cui potremmo effettivamente sovrastimare l’errore di test per l’intero set di dati.

Al fine di ridurre l’impatto di questi problemi, prenderemo in considerazione una suddivisione più sofisticata dei dati nota come convalida incrociata k-fold.

k-Fold Cross Validation

La convalida incrociata K-fold migliora validation set approach dividendo le \(n\) osservazioni in  \(k\) sottoinsiemi che si escludono a vicenda e di dimensioni approssimativamente uguali note come “fold”.

Il primo fold diventa un set di convalida, mentre i restanti \(k-1\) fold (aggregati insieme) diventano il set di addestramento. Il modello si adatta al set di addestramento e il suo errore di test viene stimato sul set di convalida. Questa procedura viene ripetuta \(k\) volte, con ciascuna ripetizione che offre un fold come set di convalida, mentre i restanti \(k-1\) vengono utilizzati per l’addestramento.

Ciò consente di calcolare una stima complessiva del test, \(\text{CV}_k\), che è una media di tutti i singoli errori quadratici medi, \(\text{MSE}_i\), per ogni fold:

\(\begin{eqnarray} \text {CV} _k = \frac {1} {k} \sum ^ {k} _ {i = 1} \text {MSE} _i \end{eqnarray}\)

La domanda ovvia che ci si pone in questa fase è come scegliere il valore di \(k\)? La risposta semplice (basata su studi empirici) è scegliere \(k = 5\) o \( k = 10 \). La risposta completa a questa domanda si riferisce sia alla spesa computazionale sia , ancora una volta, al compromesso bias-varianza.

Leave-One-Out Cross Validation

Possiamo effettivamente scegliere \(k = n\), cioè adattiamo il modello \(n\) volte, con una sola osservazione tralasciata per ogni adattamento. Questo è noto come validazione incrociata leave-one-out (LOOCV). Può essere molto costoso in termini di calcolo, soprattutto se \(n\) è grande e il modello ha una procedura di adattamento costosa.

Sebbene LOOCV sia vantaggioso per ridurre il bias , poiché quasi tutti i campioni vengono utilizzati per l’adattamento, in realtà soffre del problema dell’elevata varianza. Questo perché stiamo calcolando l’errore di test ogni volta su una singola risposta per ogni osservazione nel set di dati.

La convalida incrociata k-fold riduce la varianza a scapito dell’introduzione di qualche bias in più, dato che alcune delle osservazioni non sono utilizzate per l’addestramento. Con \(k = 5\) o \(k = 10\) il compromesso bias-varianza è generalmente ottimizzato.

Implementazione in Python

Siamo abbastanza fortunati quando lavoriamo con Python e il suo ecosistema di librerie, poiché gran parte del “lavoro pesante” è già stato implementato e così risparmiamo molto tempo e mal di testa! Utilizzando Pandas, Scikit-Learn e Matplotlib, possiamo creare rapidamente alcuni esempi per mostrare l’utilizzo e le problematiche relative alla convalida incrociata.

Se non hai ancora configurato un ambiente di ricerca Python, ti consiglio vivamente di scaricare il pacchetto Anaconda di Continuum Analytics che fornisce tutte le librerie che utilizzeremo in questo articolo e un IDE pronto per l’uso chiamato Spyder.

Ottenere i dati

Il primo compito è ottenere i dati e metterli in un formato che possiamo usare. In realtà abbiamo già eseguito questa procedura in un articolo precedente , ma vale la pena provare ad avere questi articoli il più autonomi possibile! Quindi, puoi utilizzare il seguente codice per ottenere dati storici di qualsiasi serie temporale finanziaria disponibile su Yahoo Finanza, nonché i valori di ritardo predittivi giornalieri associati:

import datetime
import numpy as np
import pandas as pd
import sklearn
import pandas_datareader as pdr


def create_lagged_series(symbol, start_date, end_date, lags=5):
    """
    Si crea un DataFrame pandas che memorizza i rendimenti percentuali dei
    prezzi di chiusura rettificata di un titolo azionario ottenuta da Yahoo
    Finance, insieme a una serie di rendimenti ritardati dai giorni di negoziazione
    precedenti (i valori predefiniti ritardano di 5 giorni).
    Sono inclusi anche il volume degli scambi, così come la direzione del giorno
    precedente.
    """

    # Ottieni informazioni sulle azioni da Yahoo Finance
    ts = pdr.get_data_yahoo(symbol, start_date-datetime.timedelta(days=365), end_date)

    # Crea un nuovo Dataframe per i ritardi
    tslag = pd.DataFrame(index=ts.index)
    tslag["Today"] = ts["Adj Close"]
    tslag["Volume"] = ts["Volume"]

    # Crea la serie traslata dei ritardi dei prezzi di chiusura del 
    # periodo (giorno) precedente
    for i in range(0,lags):
        tslag["Lag%s" % str(i+1)] = ts["Adj Close"].shift(i+1)

    # Crea il DataFrame dei ritorni
    tsret = pd.DataFrame(index=tslag.index)
    tsret["Volume"] = tslag["Volume"]
    tsret["Today"] = tslag["Today"].pct_change()*100.0

    # Se qualsiasi valore dei ritorni percentuali è uguale a zero, si imposta 
    # a un numero piccolo (per non avere problemi con il QDA in scikit-learn)
    for i,x in enumerate(tsret["Today"]):
        if (abs(x) < 0.0001):
            tsret["Today"][i] = 0.0001

    # Crea la serie dei ritorni precedenti percentuali
    for i in range(0,lags):
        tsret["Lag%s" % str(i+1)] = tslag["Lag%s" % str(i+1)].pct_change()*100.0

    # Crea la serie "Direction" (+1 o -1) che indica un giorno up/down
    tsret["Direction"] = np.sign(tsret["Today"])
    tsret = tsret[tsret.index >= start_date]

    return tsret

Da notare che non memorizziamo i valori del prezzo di chiusura direttamente nelle colonne “Today” o “Lags”. Invece, stiamo memorizzando il rendimento percentuale tra i prezzi di chiusura di un giorno e del giorno precedente.

Dobbiamo ottenere i dati per i prezzi giornalieri del SP500 per un periodo di tempo adeguato. Si considera dal 1 ° gennaio 2004 al 31 dicembre 2004. Tuttavia questa è una scelta arbitraria. Si può regolare l’intervallo di tempo come meglio si crede. Per ottenere i dati e inserirli in un Pandas DataFrame chiamato sp500_lags possiamo utilizzare il seguente codice:

if __name__ == "__main__":
    symbol = "^GSPC"
    start_date = datetime.datetime(2004, 1, 1)
    end_date = datetime.datetime(2004, 12, 31)
    sp500_lags = create_lagged_series(symbol, start_date, end_date, lags=5)

A questo punto abbiamo i dati necessari per iniziare a creare una serie di modelli statistici di machine learning..

Validation Set Approach

Ora che abbiamo i dati finanziari necessari per creare una serie di modelli di regressione predittiva, possiamo utilizzare i metodi di convalida incrociata sopra riportati per ottenere stime per l’errore di test.

Il primo compito è importare i modelli da Scikit-Learn. Scegliamo un modello di regressione lineare con caratteristiche polinomiali. Questo ci fornisce la possibilità di scegliere diversi gradi di flessibilità semplicemente aumentando il grado dell’ordine polinomiale delle features. Inizialmente si considera l’approccio del set di convalida per la cross-validation.

Scikit-Learn fornisce un approccio a set di convalida tramite il metodo train_test_split trovato nel modulo cross_validation. Successivamente dobbiamo importare il metodo KFold per la convalida incrociata k-fold, così come il modello di regressione lineare stesso. Dobbiamo importare il calcolo MSE così come Pipeline PolynomialFeatures. Gli ultimi due metodi ci consentono di creare facilmente un insieme di modelli di regressione lineare di feature polinomiali con una minima codifica aggiuntiva:

..
from sklearn.model_selection import train_test_split, KFold
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
..

Una volta importati i moduli, possiamo creare un DataFrame SP500 che utilizza i rendimenti in ritardo dei cinque giorni precedenti come predittori. Possiamo quindi creare dieci separate suddivisioni casuali dei dati in un set di addestramento e convalida.

Infine, per gradi multipli delle features polinomiali della regressione lineare, possiamo calcolare l’errore di test. Questo ci fornisce dieci separate curve di errore di test, ogni valore delle quali mostra il test MSE per un grado diverso del kernel polinomiale:

..
..

def validation_set_poly(random_seeds, degrees, X, y):
    """
    Utilizza il metodo train_test_split per creare un set
    di addestramento e un set di convalida (50% per ciascuno)
    utilizzando separati campionamenti casuali "random_seeds"
    per modelli di regressione lineare di varia flessibilità
    """
    sample_dict = dict([("seed_%s" % i,[]) for i in range(1, random_seeds+1)])
    # Esegui un ciclo su ogni suddivisione casuale in una suddivisione train-test
    for i in range(1, random_seeds+1):
        print("Random: %s" % i)
        # Aumenta il grado di ordine polinomiale della regressione lineare
        for d in range(1, degrees+1):
            print("Degree: %s" % d)
            # Crea il modello, divide gli insiemi e li addestra
            polynomial_features = PolynomialFeatures(
                degree=d, include_bias=False
            )
            linear_regression = LinearRegression()
            model = Pipeline([
                ("polynomial_features", polynomial_features),
                ("linear_regression", linear_regression)
            ])
            X_train, X_test, y_train, y_test = train_test_split(
                X, y, test_size=0.5, random_state=i
            )
            model.fit(X_train, y_train)
            # Calcola il test MSE e lo aggiunge al
            # dizionario di tutte le curve di test
            y_pred = model.predict(X_test)
            test_mse = mean_squared_error(y_test, y_pred)
            sample_dict["seed_%s" % i].append(test_mse)
        # Converte queste liste in array numpy per calcolare la media
        sample_dict["seed_%s" % i] = np.array(sample_dict["seed_%s" % i])
    # Crea la serie delle "medie dei test MSE" colcolando la media
    # del test MSE per ogni grado dei modelli di regressione lineare,
    # attraverso tutti i campionamenti casuali
    sample_dict["avg"] = np.zeros(degrees)
    for i in range(1, random_seeds+1):
        sample_dict["avg"] += sample_dict["seed_%s" % i]
    sample_dict["avg"] /= float(random_seeds)
    return sample_dict

..
..

Possiamo usare Matplotlib per tracciare il grafico di questi dati. Dobbiamo importare pylab e quindi creare una funzione per tracciare le curve di errore di test:

..
import pylab as plt
..
..
def plot_test_error_curves_vs(sample_dict, random_seeds, degrees):
    fig, ax = plt.subplots()
    ds = range(1, degrees+1)
    for i in range(1, random_seeds+1):
        ax.plot(ds, sample_dict["seed_%s" % i], lw=2,
                label='Test MSE - Sample %s' % i)

    ax.plot(ds, sample_dict["avg"], linestyle='--', color="black",
                    lw=3, label='Avg Test MSE')
    ax.legend(loc=0)
    ax.set_xlabel('Degree of Polynomial Fit')
    ax.set_ylabel('Mean Squared Error')
    ax.set_ylim([0.0, 4.0])
    fig.set_facecolor('white')
    plt.show()
..
..

Abbiamo selezionato il grado delle nostre features polinomiali al variare tra \(d = 1\) e \(d = 3\), prevedendo così un ordine cubico nelle nostre features. La seguente Figura 1 mostra le dieci diverse suddivisioni casuali dei dati di addestramento e test, insieme alla media del test MSE (la linea tratteggiata nera):

trading-algoritmico-cross-val-fig1
Figura 1 - Le curve di test MSE per più divisioni di convalida dell'addestramento per una regressione lineare con features polinomiali di grado crescente.

È immediatamente evidente quanta variazione ci sia tra diverse suddivisioni casuali in un set di addestramento e convalida. Poiché non c’è una grande quantità di segnale predittivo nell’utilizzo dei prezzi di chiusura storici dei giorni precedenti del SP500, vediamo che all’aumentare del grado delle features polinomiali, effettivamente il test MSE aumenta.

Inoltre è chiaro che il set di convalida soffre di una elevata varianza. Il test MSE medio per l’approccio del set di validazione sul modello di grado \(d = 3\) è di circa 1,9.

Per ridurre al minimo questo problema, implementeremo ora la convalida incrociata k-fold sullo stesso set di dati dell’SP500.

k-Fold Cross Validation

Poiché ci siamo già occupati delle importazioni di cui sopra, ci limitiamo a delineare le nuove funzioni per eseguire la convalida incrociata k-fold. Sono quasi identiche alle funzioni utilizzate per la divisione del test di addestramento. Tuttavia, dobbiamo usare l’oggetto  KFold per iterare su \(k \) “fold”.

In particolare l’oggetto KFold fornisce un iteratore che ci permette di indicizzare correttamente i campioni nel data set e creare fold separati di training/test. In questo esempio abbiamo scelto \(k = 10\).

Come con l’approccio dell’insieme di convalida, creiamo una pipeline di trasformazione delle features polinomiali e applichiamo un modello di regressione lineare. Quindi calcoliamo il test MSE e costruiamo curve di test MSE separate per ogni fold. Infine, creiamo una curva MSE media tra le fold:

..
..
def k_fold_cross_val_poly(folds, degrees, X, y):
    n = len(X)
    kf = KFold(n, n_folds=folds)
    kf_dict = dict([("fold_%s" % i,[]) for i in range(1, folds+1)])
    fold = 0
    for train_index, test_index in kf:
        fold += 1
        print("Fold: %s" % fold)
        X_train, X_test = X.ix[train_index], X.ix[test_index]
        y_train, y_test = y.ix[train_index], y.ix[test_index]
        # Aumenta il grado di ordine polinomiale della regressione lineare
        for d in range(1, degrees+1):
            print("Degree: %s" % d)
            # Crea il modello e lo addestra
            polynomial_features = PolynomialFeatures(
                degree=d, include_bias=False
            )
            linear_regression = LinearRegression()
            model = Pipeline([
                ("polynomial_features", polynomial_features),
                ("linear_regression", linear_regression)
            ])
            model.fit(X_train, y_train)
            # Calcola il test MSE e lo aggiunge al 
            # dizionario di tutte le curve di test
            y_pred = model.predict(X_test)
            test_mse = mean_squared_error(y_test, y_pred)
            kf_dict["fold_%s" % fold].append(test_mse)
        # Converte queste liste in array numpy per calcolare la media
        kf_dict["fold_%s" % fold] = np.array(kf_dict["fold_%s" % fold])
    # Crea la serie dei "test MSE medi" calcolando la media dei 
    # test MSE per ogni grado del modello di regressione lineare,
    # tramite ogni k folds.
    kf_dict["avg"] = np.zeros(degrees)
    for i in range(1, folds+1):
        kf_dict["avg"] += kf_dict["fold_%s" % i]
    kf_dict["avg"] /= float(folds)
    return kf_dict
..
..

Possiamo tracciare queste curve con la seguente funzione:

..
..
def plot_test_error_curves_kf(kf_dict, folds, degrees):
    fig, ax = plt.subplots()
    ds = range(1, degrees+1)
    for i in range(1, folds+1):
        ax.plot(ds, kf_dict["fold_%s" % i], lw=2, label='Test MSE - Fold %s' % i)

    ax.plot(ds, kf_dict["avg"], linestyle='--', color="black", 
                                lw=3, label='Avg Test MSE')
    ax.legend(loc=0)
    ax.set_xlabel('Degree of Polynomial Fit')
    ax.set_ylabel('Mean Squared Error')
    ax.set_ylim([0.0, 4.0])
    fig.set_facecolor('white')
    plt.show()
..
..

L’output è riportato nella seguente Figura 2:

trading-algoritmico-cross-val-fig2
Figura 2 - Curve dei test MSE per più fold di convalida incrociata k-fold per una regressione lineare con features polinomiali di grado crescente.

Si noti che la variazione tra le curve di errore è molto inferiore rispetto al validation set approch. Questo è l’effetto desiderato dell’esecuzione della convalida incrociata. In particolare, per \(d = 3\) abbiamo un errore di test medio ridotto di circa 0,8.

La convalida incrociata fornisce generalmente una stima molto migliore del vero test MSE, a scapito di qualche lieve bias. Questo di solito è un compromesso accettabile nelle applicazioni di machine learning.

Negli articoli futuri prenderemo in considerazione approcci di ricampionamento alternativi , inclusi Bootstrap, Bootstrap Aggregation (“Bagging”) e Boosting. Si tratta di tecniche più sofisticate che ci aiuteranno a selezionare meglio i nostri modelli e (si spera) a ridurre ulteriormente i nostri errori.

Codice Python Completo

Di seguito il codice Python completo per il file cross_validation.py:

import datetime
import numpy as np
import pandas as pd
import sklearn
import pandas_datareader as pdr
import pylab as plt

from sklearn.model_selection import train_test_split, KFold
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures



def create_lagged_series(symbol, start_date, end_date, lags=5):
    """
    Si crea un DataFrame pandas che memorizza i rendimenti percentuali dei
    prezzi di chiusura rettificata di un titolo azionario ottenuta da Yahoo
    Finance, insieme a una serie di rendimenti ritardati dai giorni di negoziazione
    precedenti (i valori predefiniti ritardano di 5 giorni).
    Sono inclusi anche il volume degli scambi, così come la direzione del giorno precedente.
    """

    # Ottieni informazioni sulle azioni da Yahoo Finance
    ts = pdr.get_data_yahoo(symbol, start_date-datetime.timedelta(days=365), end_date)

    # Crea un nuovo Dataframe per i ritardi
    tslag = pd.DataFrame(index=ts.index)
    tslag["Today"] = ts["Adj Close"]
    tslag["Volume"] = ts["Volume"]

    # Crea la serie traslata dei ritardi dei prezzi di chiusura del periodo (giorno) precedente
    for i in range(0,lags):
        tslag["Lag%s" % str(i+1)] = ts["Adj Close"].shift(i+1)

    # Crea il DataFrame dei ritorni
    tsret = pd.DataFrame(index=tslag.index)
    tsret["Volume"] = tslag["Volume"]
    tsret["Today"] = tslag["Today"].pct_change()*100.0

    # Se uno qualsiasi dei valori dei ritorni percentuali è uguale a zero, si impostano
    # a un numero piccolo (per non avere problemi con il modello QDA in scikit-learn)
    for i,x in enumerate(tsret["Today"]):
        if (abs(x) < 0.0001):
            tsret["Today"][i] = 0.0001

    # Crea la serie dei ritorni precedenti percentuali
    for i in range(0,lags):
        tsret["Lag%s" % str(i+1)] = tslag["Lag%s" % str(i+1)].pct_change()*100.0

    # Crea la serie "Direction" (+1 o -1) che indica un giorno up/down
    tsret["Direction"] = np.sign(tsret["Today"])
    tsret = tsret[tsret.index >= start_date]

    return tsret


def validation_set_poly(random_seeds, degrees, X, y):
    """
    Utilizza il metodo train_test_split per creare un set
    di addestramento e un set di convalida (50% per ciascuno)
    utilizzando separati campionamenti casuali "random_seeds"
    per modelli di regressione lineare di varia flessibilità
    """
    sample_dict = dict([("seed_%s" % i,[]) for i in range(1, random_seeds+1)])
    # Esegui un ciclo su ogni suddivisione casuale in una suddivisione train-test
    for i in range(1, random_seeds+1):
        print("Random: %s" % i)
        # Aumenta il grado di ordine polinomiale della regressione lineare
        for d in range(1, degrees+1):
            print("Degree: %s" % d)
            # Crea il modello, divide gli insiemi e li addestra
            polynomial_features = PolynomialFeatures(
                degree=d, include_bias=False
            )
            linear_regression = LinearRegression()
            model = Pipeline([
                ("polynomial_features", polynomial_features),
                ("linear_regression", linear_regression)
            ])
            X_train, X_test, y_train, y_test = train_test_split(
                X, y, test_size=0.5, random_state=i
            )
            model.fit(X_train, y_train)
            # Calcola il test MSE e lo aggiunge al
            # dizionario di tutte le curve di test
            y_pred = model.predict(X_test)
            test_mse = mean_squared_error(y_test, y_pred)
            sample_dict["seed_%s" % i].append(test_mse)
        # Converte queste liste in array numpy per calcolare la media
        sample_dict["seed_%s" % i] = np.array(sample_dict["seed_%s" % i])
    # Crea la serie delle "medie dei test MSE" colcolando la media
    # del test MSE per ogni grado dei modelli di regressione lineare,
    # attraverso tutti i campionamenti casuali
    sample_dict["avg"] = np.zeros(degrees)
    for i in range(1, random_seeds+1):
        sample_dict["avg"] += sample_dict["seed_%s" % i]
    sample_dict["avg"] /= float(random_seeds)
    return sample_dict


def plot_test_error_curves_vs(sample_dict, random_seeds, degrees):
    fig, ax = plt.subplots()
    ds = range(1, degrees+1)
    for i in range(1, random_seeds+1):
        ax.plot(ds, sample_dict["seed_%s" % i], lw=2, label='Test MSE - Sample %s' % i)
    ax.plot(ds, sample_dict["avg"], linestyle='--', color="black", lw=3, label='Avg Test MSE')
    ax.legend(loc=0)
    ax.set_xlabel('Degree of Polynomial Fit')
    ax.set_ylabel('Mean Squared Error')
    ax.set_ylim([0.0, 4.0])
    fig.set_facecolor('white')
    plt.show()


def k_fold_cross_val_poly(folds, degrees, X, y):
    n = len(X)
    kf = KFold(n, n_folds=folds)
    kf_dict = dict([("fold_%s" % i,[]) for i in range(1, folds+1)])
    fold = 0
    for train_index, test_index in kf:
        fold += 1
        print("Fold: %s" % fold)
        X_train, X_test = X.ix[train_index], X.ix[test_index]
        y_train, y_test = y.ix[train_index], y.ix[test_index]
        # Aumenta il grado di ordine polinomiale della regressione lineare
        for d in range(1, degrees+1):
            print("Degree: %s" % d)
            # Crea il modello e lo addestra
            polynomial_features = PolynomialFeatures(
                degree=d, include_bias=False
            )
            linear_regression = LinearRegression()
            model = Pipeline([
                ("polynomial_features", polynomial_features),
                ("linear_regression", linear_regression)
            ])
            model.fit(X_train, y_train)
            # Calcola il test MSE e lo aggiunge al
            # dizionario di tutte le curve di test
            y_pred = model.predict(X_test)
            test_mse = mean_squared_error(y_test, y_pred)
            kf_dict["fold_%s" % fold].append(test_mse)
        # Converte queste liste in array numpy per calcolare la media
        kf_dict["fold_%s" % fold] = np.array(kf_dict["fold_%s" % fold])
    # Crea la serie dei "test MSE medi" calcolando la media dei
    # test MSE per ogni grado del modello di regressione lineare,
    # tramite ogni k folds.
    kf_dict["avg"] = np.zeros(degrees)
    for i in range(1, folds+1):
        kf_dict["avg"] += kf_dict["fold_%s" % i]
    kf_dict["avg"] /= float(folds)
    return kf_dict


def plot_test_error_curves_kf(kf_dict, folds, degrees):
    fig, ax = plt.subplots()
    ds = range(1, degrees+1)
    for i in range(1, folds+1):
        ax.plot(ds, kf_dict["fold_%s" % i], lw=2, label='Test MSE - Fold %s' % i)
    ax.plot(ds, kf_dict["avg"], linestyle='--', color="black", lw=3, label='Avg Test MSE')
    ax.legend(loc=0)
    ax.set_xlabel('Degree of Polynomial Fit')
    ax.set_ylabel('Mean Squared Error')
    ax.set_ylim([0.0, 4.0])
    fig.set_facecolor('white')
    plt.show()



if __name__ == "__main__":
    symbol = "^FTSE"
    symbol = "^GSPC"
    start_date = datetime.datetime(2004, 1, 1)
    end_date = datetime.datetime(2004, 12, 31)
    sp500_lags = create_lagged_series(symbol, start_date, end_date, lags=5)

    # Uso tutti e venti i ritorni di 2 giorni precedenti come  
    # valori di predizione, con "Today" come risposta
    X = sp500_lags[[
        "Lag1", "Lag2", "Lag3", "Lag4", "Lag5",
        # "Lag6", "Lag7", "Lag8", "Lag9", "Lag10",
        # "Lag11", "Lag12", "Lag13", "Lag14", "Lag15",
        # "Lag16", "Lag17", "Lag18", "Lag19", "Lag20"
    ]]
    y = sp500_lags["Today"]
    degrees = 3

    # Visualizza le curve dell'errore di test per il set di validazione
    random_seeds = 10
    sample_dict_val = validation_set_poly(random_seeds, degrees, X, y)
    plot_test_error_curves_vs(sample_dict_val, random_seeds, degrees)

    # Visualizza le curve dell'errore di test per il set di k-fold CV
    folds = 10
    kf_dict = k_fold_cross_val_poly(folds, degrees, X, y)
    plot_test_error_curves_kf(kf_dict, folds, degrees)

Il Compromesso Bias-Varianza nel Machine Learning Statistico: configurazione della Regressione

BIAS-VARIANZA MACHINE LEARNING trading algoritmico

In questo articolo introduciamo uno dei problemi più importanti e delicati del machine learning, cioè la selezione del modello e il  compromesso bias-varianza . Quest’ultimo è uno dei problemi più cruciali nella realizzazione di redditizie strategie di trading basate su tecniche di machine learning.

La selezione del modello si riferisce alla capacità di valutare le prestazioni di diversi modelli di machine learning al fine di scegliere il migliore.

Il compromesso bias-varianza è una proprietà specifica di tutti i modelli di machine learning (supervisionati), che impone un compromesso tra la “flessibilità” del modello e il comportamento su dati che non ha mai visto (out of sample). Quest’ultimo è noto come prestazione di generalizzazione dei modelli .

Inizieremo descrivendo l’importanza della selezione del modello e successivamente vedremo qualitativamente il compromesso bias-varianza. Concluderemo l’articolo derivando matematicamente il compromesso bias-varianza e discuteremo le misure per minimizzare i problemi che si introducono.

In questo articolo prendiamo in considerazione modelli di regressione supervisionati . Cioè, modelli che vengono addestrati su una serie di dati di addestramento etichettati e producono una risposta quantitativa . Un esempio di ciò potrebbe essere il tentativo di prevedere i futuri prezzi delle azioni sulla base di altri fattori come i prezzi passati, i tassi di interesse o i tassi di cambio.

Questo è in contrasto con un modello di risposta categoriale o binario come nel caso della classificazione supervisionata . Un esempio di classificazione è l’assegnazione (o almeno il tentativo) di un argomento a un documento di testo, da un insieme finito di argomenti. Le situazioni di bias-varianza e selezione del modello per la classificazione sono estremamente simili al modello di regressione e richiedono semplicemente una modifica per gestire i diversi modi in cui vengono misurati gli errori e le prestazioni. Discuteremo queste modifiche in un ultimo articolo.

Nota: se si desidera saperne di più sulla notazione del modello di base che useremo in questo articolo, vale la pena leggere l’ articolo sulle basi del machine learning statistico .

Modelli di machine learning

Come per la maggior parte delle nostre discussioni sul machine learning, il modello di base è il seguente:

\(\begin{eqnarray} Y = f (X) + \epsilon \end{eqnarray}\)

Questo afferma che il vettore di risposta, \(Y\), è una funzione (potenzialmente non lineare), \(f\), del vettore predittore, \(X\), con un insieme di termini di errore con distribuzione normale che hanno media pari a 0 e deviazione standard pari a 1.

Cosa significa in pratica?

Ad esempio, il vettore \(X\) potrebbe rappresentare un insieme di prezzi finanziari ritardati. Potrebbe anche rappresentare i tassi di interesse, i prezzi dei derivati, i prezzi degli immobili, le frequenze delle parole in un documento o qualsiasi altro fattore che consideriamo utile per fare una previsione .

Il vettore \(Y\) può essere singolo o multi-valore. Il primo caso potrebbe semplicemente essere il prezzo delle azioni di domani, nel secondo caso potrebbe essere i prezzi giornalieri previsionali della prossima settimana.

\(f\) rappresenta la visione della sottostante relazione tra \(Y\) e \(X\). Questa potrebbe essere lineare , nel qual caso possiamo stimare \(f\) tramite un modello di regressione lineare. Potrebbe essere non lineare , in questo caso possiamo stimare \(f\) con una Support Vector Machine o, per esempio, un metodo basato su spline.

I termini di errore \(\epsilon\) rappresentano tutti i fattori che influenzano \(Y\) che non abbiamo preso in considerazione con la nostra funzione \(f\). Sono essenzialmente le componenti “sconosciute” del modello di previsione. Solitamente si presumere che questi hanno una distribuzione normale, con media pari a 0 e deviazione standard pari a 1.

In questo articolo descriviamo come misurare le prestazioni di una stima per la funzione (sconosciuta) \(f\). Tale stima utilizza la notazione “hat”. Quindi, \(\hat {f} \) può essere letto come “la stima di \(f\)”.

Inoltre descriviamo l’effetto sulle prestazioni del modello man mano che lo rendiamo più flessibile . La flessibilità descrive la capacità di aumentare i gradi di libertà disponibili per il modello al fine di “adattarsi” ai dati di addestramento. Vedremo che la relazione tra flessibilità ed errore di prestazione è non lineare e quindi dobbiamo essere estremamente attenti nella scelta del modello “migliore”.

Da notare che non esiste mai un modello “migliore” per la totalità delle statistiche e del machine learning. Diversi modelli hanno diversi punti di forza e di debolezza. Un modello può funzionare molto bene su un set di dati, ma può funzionare male su un altro. La sfida nel machine learning statistico è scegliere il modello “migliore” per il problema in questione, a seconda dei dati disponibili.

Selezione del modello

Quando si cerca di trovare il “migliore” metodo di machine learning statistico, abbiamo bisogno di alcuni mezzi per caratterizzare le prestazioni dei vari modelli.

Per ottenere ciò, dobbiamo confrontare i valori noti della relazione sottostante con quelli previsti da un modello stimato .

Ad esempio, se stiamo provando a prevedere i prezzi delle azioni di domani, allora desideriamo valutare quanto  le previsioni dei nostri modelli siano vicine al valore reale, in un particolare giorno.

Ciò motiva il concetto di una funzione di perdita , che confronta quantitativamente la differenza tra i valori reali con i valori previsti.

Supponiamo di aver creato una stima \(\hat{f}\) della relazione sottostante \(f\). \(\hat {f}\) potrebbe essere, per esempio, una regressione lineare o un modello di random forest. \(\ hat {f}\) sarà stato addestrato su un particolare set di dati, \(\tau \), che contiene coppie predittore-risposta. Se ci sono \(N\) coppie, \( \tau \) è ricavato come:

\(\begin{eqnarray} \tau = \{(X_1, Y_1), …, (X_N, Y_N) \} \end{eqnarray}\)

\(X_i\) rappresentano i fattori di previsione, che potrebbero essere i prezzi precedenti ritardati per una serie o altri fattori, come menzionato sopra. \(Y_i\) potrebbero essere le previsioni per i nostri prezzi delle azioni nel periodo successivo. In questo caso, \(N\) rappresenta il numero di giorni di dati che abbiamo a disposizione.

La funzione di perdita è indicata con \(L (Y, \hat{f} (X))\). Il suo compito è confrontare le previsioni fatte da \(\hat {f}\) su valori specifici di $ X $ con i loro veri valori dati da \(Y\). Una scelta comune per \(L\) è l’ errore assoluto

\(\begin{eqnarray} L (Y, \hat{f} (X)) = | Y – \hat{f} (X) | \end{eqnarray}\)

Un’altra scelta popolare è l’errore quadratico

\(\begin{eqnarray} L (Y, \hat{f} (X)) = (Y – \hat{f} (X)) ^2 \end{eqnarray}\)

Notare che entrambe le scelte della funzione di perdita non sono negative. Quindi la “migliore” perdita per un modello è zero, cioè non c’è differenza tra la previsione e il valore reale.

Errore di addestramento contro errore di prova

Ora che abbiamo una funzione di perdita, abbiamo bisogno di un modo per aggregare le varie differenze tra i valori veri e quelli previsti. Un modo per farlo è definire l’Errore al quadrato medio (MSE), che è semplicemente la media, o il valore atteso, della perdita al quadrato:

\(\begin{eqnarray} MSE: = \frac {1} {N} \sum ^ {N} _ {i = 1} (Y_i – \hat {f} (X_i)) ^ 2 \end{eqnarray}\)

La definizione afferma semplicemente che l’errore quadratico medio corrisponde alla media di tutte le differenze al quadrato tra i valori veri \(Y_i\) e i valori previsti \(\hat {f} (X_i) \). Un MSE più piccolo significa che la stima è più accurata.

È importante rendersi conto che questo valore MSE viene calcolato utilizzando solo i dati di addestramento . Cioè, viene calcolato utilizzando solo i dati su cui è stato costruito il modello. Quindi, in realtà è noto come training MSE.

In pratica, questo valore ci interessa poco. Quello che bisogna veramente valutare è la bontà del modello nel prevedere i corretti valori per un set di dati mai visti in precedenza.

Ad esempio, non siamo realmente interessati alla qualità di previsione del modello per i prezzi delle azioni del giorno successivo nel passato, ci interessa solo come può prevedere i prezzi delle azioni dei giorni successivi in futuro. Questa quantificazione delle prestazioni di un modello è nota come prestazioni di generalizzazione. È quello che ci interessa veramente.

Matematicamente, se abbiamo un nuovo valore di previsione \(X_0\) e una risposta vera \(Y_0\), allora desideriamo prendere l’aspettativa su tutti questi nuovi valori per ottenere il test MSE:

\(\begin{eqnarray} \text {Test MSE}: = \mathbb {E} \left [(Y_0 – \hat{f} (X_0)) ^2 \right] \end{eqnarray}\)

Dove l’aspettativa viene presa attraverso tutte le nuove coppie di predittore-risposta invisibili \((X_0, Y_0)\).

Il nostro obiettivo è selezionare il modello per cui il test MSE è più basso tra tutti gli altri modelli possibili.

Purtroppo è difficile calcolare il test MSE! Questo perché spesso ci troviamo in una situazione in cui non abbiamo a disposizione dati di test .

In generale, nei domini di machine learning questo può essere abbastanza comune. Nel trading quantitativo ci troviamo (di solito) in un ambiente “ricco di dati” e quindi possiamo conservare alcuni dei nostri dati per l’addestramento e altri per i test. In articoli futuri discuteremo della convalida incrociata , che è uno dei mezzi per utilizzare sottoinsiemi dei dati di addestramento al fine di stimare il test MSE.

Una domanda pertinente da porsi in questa fase è “Perché non possiamo semplicemente utilizzare il modello con la training MSE più basso?”. La risposta è che non siamo in grado di utilizzare questo approccio perché non vi è alcuna garanzia che il modello con il MSE di training MSE più basso sarà anche il modello con il test MSE più basso. Per quale motivo? La risposta consiste in una particolare proprietà dei metodi di apprendimento automatico statistico nota come compromesso bias-varianza .

Il compromesso bias-varianza

Consideriamo una situazione leggermente artificiosa in cui conosciamo la “vera” relazione tra \(Y\) e \(X\), che affermerò è data da una funzione sinusoidale, \(f = \sin\), tale che \(Y = f (X ) = \sin (X)\). Da notare che in realtà non conosceremo mai il sottostante \(f\), motivo per cui lo stiamo stimando in prima analisi!

Per questa situazione artificiosa abbiamo creato una serie di punti di allenamento, \(\tau\), dati da \(Y_i = \sin (X_i) + \epsilon_i\), dove \(\epsilon_i\) sono tratti da una distribuzione normale standard (media di zero, deviazione standard uguale a uno). Questo può essere visto nella Figura 1. La curva nera è la “vera” funzione \(f\), limitata all’intervallo \([0, 2 \ pi]\), mentre i punti cerchiati rappresentano i valori dei dati simulati \(Y_i\).

trading-algoritmico-bias-variance-fig1
Figura 1 - Varie stime del modello sinusoidale sottostante, f = sin(x)

Ora possiamo provare ad adattare alcuni diversi modelli a questi dati di addestramento. Il primo modello, dato dalla linea verde, è una regressione lineare dotata di stima dei minimi quadrati ordinari. Il secondo modello, dato dalla linea blu, è un modello polinomiale con grado \(m = 3\). Il terzo modello, dato dalla curva rossa, è un polinomio di grado superiore con grado \(m = 20\). Tra ciascuno dei modelli abbiamo variato la flessibilità , cioè i gradi di libertà (DoF). Il modello lineare è il meno flessibile con solo due DoF. Il modello più flessibile è il polinomio di ordine \(m = 20\). Si può vedere che il polinomio di ordine \(m = 3\) è l’apparente più vicino adattamento alla relazione sinusoidale sottostante.

Per ciascuno di questi modelli possiamo calcolare il training MSE . Si può vedere nella Figura 2 che il training MSE (dato dalla curva verde) diminuisce monotonicamente all’aumentare della flessibilità del modello. Ciò ha senso, poiché l’adattamento polinomiale può diventare flessibile quanto necessario per ridurre al minimo la differenza tra i suoi valori e quelli dei dati sinusoidali.

trading-algoritmico-bias-variance-fig2
Figura 2 - Training MSE e Test MSE in funzione della flessibilità del modello.

Tuttavia, se tracciamo il test MSE (dato dalla curva blu) la situazione è molto diversa. Il test MSE inizialmente diminuisce man mano che aumentiamo la flessibilità del modello, ma alla fine ricomincia ad aumentare dopo aver introdotto molta flessibilità. Questo avviene perché prevedendo un modello estremamente flessibile, permettiamo che si adatti ai “modelli” nei dati di addestramento.

Tuttavia, non appena introduciamo nuovi dati mai visti nel set di test, il modello non riesce a generalizzare in modo corretto perché questi “modelli” sono solo artefatti casuali dei dati di addestramento e non sono una proprietà sottostante della vera forma sinusoidale. Siamo in una situazione di overfitting .

In effetti, questa proprietà di un test MSE “a forma di U” in funzione della flessibilità del modello è una proprietà intrinseca dei modelli statistici di machine learning, nota come compromesso bias-varianza .

Si può dimostrare (vedere di seguito nella sezione Spiegazione matematica) che il test MSE previsto, in cui l’aspettativa viene presa in molti set di addestramento, è dato da:

\(\begin{eqnarray} \mathbb{E}(Y_0 – \hat{f}(X_0))^2 = \text{Var}(\hat{f}(X_0)) + \left[ \text{Bias} \hat{f}(X_0)\right]^2 + \text{Var}(\epsilon) \end{eqnarray}\)

Il primo termine a destra è la varianza della stima in molti set di addestramento. Determina la deviazione della stima del modello medio quando vengono provati diversi dati di addestramento. In particolare, un modello con un’elevata varianza suggerisce che sia troppo adattato ai dati di allenamento.

Il termine medio è il bias al quadrato , che caratterizza la differenza tra le medie della stima e i valori reali. Un modello con un alto bias non sta catturando il comportamento base della “vera” forma funzionale. Si può immaginare una situazione dove si usa una regressione lineare per modellare una curva sinusoidale (come sopra). Non importa quanto bene il modello si “adatti” ai dati, non catturerà mai la non linearità inerente a una curva sinusoidale.

Il termine finale è noto come errore irriducibile . È il limite inferiore minimo per il test MSE. Dato che abbiamo accesso solo ai punti dati di addestramento (inclusa la casualità associata ai valori \(\epsilon\)) non è mai possibile ottenere un adattamento “più accurato” di quello che offre la varianza dei residui.

In generale, con l’aumentare della flessibilità, vediamo un aumento della varianza e una diminuzione del bias. Tuttavia è il relativo tasso di variazione tra questi due fattori che determina se ile test MSE atteso aumenta o diminuisce.

Man mano che la flessibilità aumenta, il bias tenderà a diminuire rapidamente (più velocemente di quanto la varianza possa aumentare) e quindi vediamo un calo nel test MSE. Tuttavia, man mano che la flessibilità aumenta ulteriormente, vi è meno riduzione del bias (perché la flessibilità del modello può adattarsi facilmente ai dati di addestramento) mentre la varianza aumenta rapidamente, a causa del sovradimensionamento del modello.

L’obiettivo finale del machine learning è cercare di ridurre al minimo il test MSE previsto , ovvero scegliere un modello di machine learning statistico che abbia contemporaneamente una bassa varianza e un basso bias .

Al fine di stimare il test MSE previsto, possiamo utilizzare tecniche come la convalida incrociata . Tali tecniche saranno oggetto di articoli futuri.

Se desideri ottenere una definizione matematicamente più precisa del compromesso bias-varianza, puoi leggere la sezione successiva.

Una spiegazione matematica

Abbiamo finora delineato qualitativamente i problemi che circondano la flessibilità, il bias e la varianza del modello. Nel seguente riquadro  eseguiremo una scomposizione matematica dell’errore di previsione atteso per una particolare stima del modello, \(\hat {f} (X) \) con il vettore di previsione \( X = x_0 \) utilizzando l’ultima delle nostre funzioni di perdita, la perdita di errore al quadrato:

La definizione della perdita dell’errore quadratico, nel punto di previsione \(X_0\) $, è data da:

\(\begin{eqnarray} \text{Err} (X_0) = \mathbb{E} \left[ \left( Y – \hat{f}(X_0) \right)^2 | X = X_0 \right] \end{eqnarray}\)

Tuttavia, possiamo espandere l’aspettativa sul lato destro in tre termini:

\(\begin{eqnarray} \text{Err} (X_0) = \sigma^{2}_{\epsilon} + \left[ \mathbb{E} \hat{f} (X_0) – f(X_0)\right]^2 + \mathbb{E} \left[ \hat{f}(X_0) – \mathbb{E} \hat{f}(X_0) \right]^2 \end{eqnarray}\)

Il primo termine sulla RHS è noto come errore irriducibile . È il limite inferiore del possibile errore di previsione.

Il termine medio è il bias al quadrato e rappresenta la differenza tra valore medio di tutte le previsioni a \(X_0\), in tutti i possibili set di addestramento, e il vero valore medio della funzione sottostante a \(X_0\).

Questo può essere visto come l’errore introdotto dal modello nel non rappresentare il comportamento base della vera funzione. Ad esempio, utilizzando un modello lineare quando il fenomeno è intrinsecamente non lineare.

Il terzo termine è noto come varianza . Caratterizza l’errore introdotto quando il modello diventa più flessibile e quindi più sensibile alle variazioni tra diversi set di addestramento, \(\tau\).

\(\begin{eqnarray} \text{Err} (X_0) &=& \sigma^{2}_{\epsilon} + \text{Bias}^2 + \text{Var} (\hat{f}(X_0))\\ &=& \text{Irreducible Error} + \text{Bias}^2 + \text{Variance} \end{eqnarray}\)

È importante ricordare che \(\sigma^{2} _ \epsilon\) rappresenta un limite inferiore assoluto dell’errore di previsione. Mentre l’errore di addestramento atteso può essere ridotto monotonicamente a zero (semplicemente aumentando la flessibilità del modello), l’errore di previsione atteso sarà sempre almeno l’errore irriducibile, anche se il bias al quadrato e la varianza sono entrambi zero.

Nei prossimi articoli analizzeremo gli strumenti per stimare il test MSE previsto, tramite tecniche di ricampionamento come la convalida incrociata . Inoltre vedremo come le cose cambiano quando si considerano problemi di classificazione.

Apprendimento Supervisionato per la Classificazione dei Documenti con Scikit-Learn

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

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:

cd ~
mkdir -p datatrading/classification/data
cd datatrading/classification/data
wget http://kdd.ics.uci.edu/databases/reuters21578/reuters21578.tar.gz
Possiamo quindi decomprimere il file:
tar -zxvf reuters21578.tar.gz
Se elenchiamo il contenuto della directory (ls -l) possiamo vedere quanto segue (ho rimosso i permessi e i dettagli di proprietà per brevità):
...     186 Dec  4  1996 all-exchanges-strings.lc.txt
...     316 Dec  4  1996 all-orgs-strings.lc.txt
...    2474 Dec  4  1996 all-people-strings.lc.txt
...    1721 Dec  4  1996 all-places-strings.lc.txt
...    1005 Dec  4  1996 all-topics-strings.lc.txt
...   28194 Dec  4  1996 cat-descriptions_120396.txt
...  273802 Dec 10  1996 feldman-cia-worldfactbook-data.txt
...    1485 Jan 23  1997 lewis.dtd
...   36388 Sep 26  1997 README.txt
... 1324350 Dec  4  1996 reut2-000.sgm
... 1254440 Dec  4  1996 reut2-001.sgm
... 1217495 Dec  4  1996 reut2-002.sgm
... 1298721 Dec  4  1996 reut2-003.sgm
... 1321623 Dec  4  1996 reut2-004.sgm
... 1388644 Dec  4  1996 reut2-005.sgm
... 1254765 Dec  4  1996 reut2-006.sgm
... 1256772 Dec  4  1996 reut2-007.sgm
... 1410117 Dec  4  1996 reut2-008.sgm
... 1338903 Dec  4  1996 reut2-009.sgm
... 1371071 Dec  4  1996 reut2-010.sgm
... 1304117 Dec  4  1996 reut2-011.sgm
... 1323584 Dec  4  1996 reut2-012.sgm
... 1129687 Dec  4  1996 reut2-013.sgm
... 1128671 Dec  4  1996 reut2-014.sgm
... 1258665 Dec  4  1996 reut2-015.sgm
... 1316417 Dec  4  1996 reut2-016.sgm
... 1546911 Dec  4  1996 reut2-017.sgm
... 1258819 Dec  4  1996 reut2-018.sgm
... 1261780 Dec  4  1996 reut2-019.sgm
... 1049566 Dec  4  1996 reut2-020.sgm
...  621648 Dec  4  1996 reut2-021.sgm
... 8150596 Mar 12  1999 reuters21578.tar.gz
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:
..
..
<REUTERS TOPICS="YES" LEWISSPLIT="TRAIN" CGISPLIT="TRAINING-SET" OLDID="5544" NEWID="1">
<DATE>26-FEB-1987 15:01:01.79</DATE>
<TOPICS><D>cocoa</D></TOPICS>
<PLACES><D>el-salvador</D><D>usa</D><D>uruguay</D></PLACES>
<PEOPLE></PEOPLE>
<ORGS></ORGS>
<EXCHANGES></EXCHANGES>
<COMPANIES></COMPANIES>
<UNKNOWN> 
C T
f0704reute
u f BC-BAHIA-COCOA-REVIEW   02-26 0105</UNKNOWN>
<TEXT>
<TITLE>BAHIA COCOA REVIEW</TITLE>
<DATELINE>    SALVADOR, Feb 26 - </DATELINE>
<BODY>Showers continued throughout the week in
the Bahia cocoa zone, alleviating the drought since early
January and improving prospects for the coming temporao,
although normal humidity levels have not been restored,
Comissaria Smith said in its weekly review.
    The dry period means the temporao will be late this year.
    Arrivals for the week ended February 22 were 155,221 bags
of 60 kilos making a cumulative total for the season of 5.93
mln against 5.81 at the same stage last year. Again it seems
that cocoa delivered earlier on consignment was included in the
arrivals figures.
    Comissaria Smith said there is still some doubt as to how
much old crop cocoa is still available as harvesting has
practically come to an end. With total Bahia crop estimates
around 6.4 mln bags and sales standing at almost 6.2 mln there
are a few hundred thousand bags still in the hands of farmers,
middlemen, exporters and processors.
    There are doubts as to how much of this cocoa would be fit
for export as shippers are now experiencing dificulties in
obtaining +Bahia superior+ certificates.
    In view of the lower quality over recent weeks farmers have
sold a good part of their cocoa held on consignment.
    Comissaria Smith said spot bean prices rose to 340 to 350
cruzados per arroba of 15 kilos.
    Bean shippers were reluctant to offer nearby shipment and
only limited sales were booked for March shipment at 1,750 to
1,780 dlrs per tonne to ports to be named.
    New crop sales were also light and all to open ports with
June/July going at 1,850 and 1,880 dlrs and at 35 and 45 dlrs
under New York july, Aug/Sept at 1,870, 1,875 and 1,880 dlrs
per tonne FOB.
    Routine sales of butter were made. March/April sold at
4,340, 4,345 and 4,350 dlrs.
    April/May butter went at 2.27 times New York May, June/July
at 4,400 and 4,415 dlrs, Aug/Sept at 4,351 to 4,450 dlrs and at
2.27 and 2.28 times New York Sept and Oct/Dec at 4,480 dlrs and
2.27 times New York Dec, Comissaria Smith said.
    Destinations were the U.S., Covertible currency areas,
Uruguay and open ports.
    Cake sales were registered at 785 to 995 dlrs for
March/April, 785 dlrs for May, 753 dlrs for Aug and 0.39 times
New York Dec for Oct/Dec.
    Buyers were the U.S., Argentina, Uruguay and convertible
currency areas.
    Liquor sales were limited with March/April selling at 2,325
and 2,380 dlrs, June/July at 2,375 dlrs and at 1.25 times New
York July, Aug/Sept at 2,400 dlrs and at 1.25 times New York
Sept and Oct/Dec at 1.25 times New York Dec, Comissaria Smith
said.
    Total Bahia sales are currently estimated at 6.13 mln bags
against the 1986/87 crop and 1.06 mln bags against the 1987/88
crop.
    Final figures for the period to February 28 are expected to
be published by the Brazilian Cocoa Trade Commission after
carnival which ends midday on February 27.
 Reuter
</BODY></TEXT>
</REUTERS>
..
..

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):

acq
alum
austdlr
austral
barley
bfr
bop
can
carcass
castor-meal
castor-oil
castorseed
citruspulp
cocoa
coconut
coconut-oil
coffee
copper
copra-cake
corn
...
...
silver
singdlr
skr
sorghum
soy-meal
soy-oil
soybean
stg
strategic-metal
sugar
sun-meal
sun-oil
sunseed
tapioca
tea
tin
trade
tung
tung-oil
veg-oil
wheat
wool
wpi
yen
zinc
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:
[
    ("cat", "It is best not to give them too much milk"),
    ("dog", "Last night we took him for a walk, but he had to remain on the leash"),
    ..
    ..
    ("hamster", "Today we cleaned out the cage and prepared the sawdust"),
    ("cat", "Kittens require a lot of attention in the first few months")
]

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:

import html
import pprint
import re
from html.parser import HTMLParser


class ReutersParser(HTMLParser):
    """
    ReutersParser è una sottoclasse HTMLParser e viene utilizzato per aprire file SGML
    associati al dataset di Reuters-21578.

    Il parser è un generatore e produrrà un singolo documento alla volta.
    Poiché i dati verranno suddivisi in blocchi durante l'analisi, è necessario mantenere
    alcuni stati interni di quando i tag sono stati "inseriti" e "eliminati".
    Da qui le variabili booleani in_body, in_topics e in_topic_d.
    """

    def __init__(self, encoding='latin-1'):
        """
        Inizializzo la superclasse (HTMLParser) e imposto il parser.
        Imposto la decodifica dei file SGML con latin-1 come default.
        """
        html.parser.HTMLParser.__init__(self)
        self._reset()
        self.encoding = encoding

    def _reset(self):
        """
        Viene chiamata solo durante l'inizializzazione della classe parser
        e quando è stata generata una nuova tupla topic-body. Si
        resetta tutto lo stato in modo che una nuova tupla possa essere
        successivamente generato.
        """
        self.in_body = False
        self.in_topics = False
        self.in_topic_d = False
        self.body = ""
        self.topics = []
        self.topic_d = ""

    def parse(self, fd):
        """
        parse accetta un descrittore di file e carica i dati in blocchi
        per ridurre al minimo l'utilizzo della memoria.
        Quindi produce nuovi documenti man mano che vengono analizzati.
        """
        self.docs = []
        for chunk in fd:
            self.feed(chunk.decode(self.encoding))
            for doc in self.docs:
                yield doc
            self.docs = []
        self.close()

    def handle_starttag(self, tag, attrs):
        """
        Questo metodo viene utilizzato per determinare cosa fare quando
        il parser incontra un particolare tag di tipo "tag".
        In questo caso, impostiamo semplicemente i valori booleani
        interni su True se è stato trovato quel particolare tag.
        """
        if tag == "reuters":
            pass
        elif tag == "body":
            self.in_body = True
        elif tag == "topics":
            self.in_topics = True
        elif tag == "d":
            self.in_topic_d = True

    def handle_endtag(self, tag):
        """
        Questo metodo viene utilizzato per determinare cosa fare
        quando il parser termina con un particolare tag di tipo "tag".

        Se il tag è un tag <REUTERS>, rimuoviamo tutti gli spazi bianchi
        con un'espressione regolare e quindi aggiungiamo la tupla topic-body.

        Se il tag è un tag <BODY> o <TOPICS>, impostiamo semplicemente lo
        stato interno su False per questi valori booleani, rispettivamente.

        Se il tag è un tag <D> (che si trova all'interno di un tag <TOPICS>),
        aggiungiamo l'argomento specifico all'elenco "topics" e infine lo resettiamo.
        """
        if tag == "reuters":
            self.body = re.sub(r'\s+', r' ', self.body)
            self.docs.append((self.topics, self.body))
            self._reset()
        elif tag == "body":
            self.in_body = False
        elif tag == "topics":
            self.in_topics = False
        elif tag == "d":
            self.in_topic_d = False
            self.topics.append(self.topic_d)
            self.topic_d = ""

    def handle_data(self, data):
        """
        I dati vengono semplicemente aggiunti allo stato appropriato
        per quel particolare tag, fino a quando non viene visualizzato
        il tag di chiusura finale.
        """
        if self.in_body:
            self.body += data
        elif self.in_topic_d:
            self.topic_d += data


if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    filename = "data/reut2-000.sgm"
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    doc = parser.parse(open(filename, 'rb'))
    pprint.pprint(list(doc))
In questa fase vedremo una quantità significativa di output simile a questa:
..
..
(['grain', 'rice', 'thailand'],
  'Thailand exported 84,960 tonnes of rice in the week ended February 24, '
  'up from 80,498 the previous week, the Commerce Ministry said. It said '
  'government and private exporters shipped 27,510 and 57,450 tonnes '
  'respectively. Private exporters concluded advance weekly sales for '
  '79,448 tonnes against 79,014 the previous week. Thailand exported '
  '689,038 tonnes of rice between the beginning of January and February 24, '
  'up from 556,874 tonnes during the same period last year. It has '
  'commitments to export another 658,999 tonnes this year. REUTER '),
 (['soybean', 'red-bean', 'oilseed', 'japan'],
  'The Tokyo Grain Exchange said it will raise the margin requirement on '
  'the spot and nearby month for U.S. And Chinese soybeans and red beans, '
  'effective March 2. Spot April U.S. Soybean contracts will increase to '
  '90,000 yen per 15 tonne lot from 70,000 now. Other months will stay '
  'unchanged at 70,000, except the new distant February requirement, which '
  'will be set at 70,000 from March 2. Chinese spot March will be set at '
  '110,000 yen per 15 tonne lot from 90,000. The exchange said it raised '
  'spot March requirement to 130,000 yen on contracts outstanding at March '
  '13. Chinese nearby April rises to 90,000 yen from 70,000. Other months '
  'will remain unchanged at 70,000 yen except new distant August, which '
  'will be set at 70,000 from March 2. The new margin for red bean spot '
  'March rises to 150,000 yen per 2.4 tonne lot from 120,000 and to 190,000 '
  'for outstanding contracts as of March 13. The nearby April requirement '
  'for red beans will rise to 100,000 yen from 60,000, effective March 2. '
  'The margin money for other red bean months will remain unchanged at '
  '60,000 yen, except new distant August, for which the requirement will '
  'also be set at 60,000 from March 2. REUTER '),
..
..

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:

..
..
 ('grain',
  'Thailand exported 84,960 tonnes of rice in the week ended February 24, '
  'up from 80,498 the previous week, the Commerce Ministry said. It said '
  'government and private exporters shipped 27,510 and 57,450 tonnes '
  'respectively. Private exporters concluded advance weekly sales for '
  '79,448 tonnes against 79,014 the previous week. Thailand exported '
  '689,038 tonnes of rice between the beginning of January and February 24, '
  'up from 556,874 tonnes during the same period last year. It has '
  'commitments to export another 658,999 tonnes this year. REUTER '),
 ('soybean',
  'The Tokyo Grain Exchange said it will raise the margin requirement on '
  'the spot and nearby month for U.S. And Chinese soybeans and red beans, '
  'effective March 2. Spot April U.S. Soybean contracts will increase to '
  '90,000 yen per 15 tonne lot from 70,000 now. Other months will stay '
  'unchanged at 70,000, except the new distant February requirement, which '
  'will be set at 70,000 from March 2. Chinese spot March will be set at '
  '110,000 yen per 15 tonne lot from 90,000. The exchange said it raised '
  'spot March requirement to 130,000 yen on contracts outstanding at March '
  '13. Chinese nearby April rises to 90,000 yen from 70,000. Other months '
  'will remain unchanged at 70,000 yen except new distant August, which '
  'will be set at 70,000 from March 2. The new margin for red bean spot '
  'March rises to 150,000 yen per 2.4 tonne lot from 120,000 and to 190,000 '
  'for outstanding contracts as of March 13. The nearby April requirement '
  'for red beans will rise to 100,000 yen from 60,000, effective March 2. '
  'The margin money for other red bean months will remain unchanged at '
  '60,000 yen, except new distant August, for which the requirement will '
  'also be set at 60,000 from March 2. REUTER '),
..
..
Per rimuovere i tag geografici e selezionare il principale tag dell’argomento possiamo aggiungere il seguente codice:
...
...

def obtain_topic_tags():
    """
    Apre il file dell'elenco degli argomenti e importa tutti i nomi 
    degli argomenti facendo attenzione a rimuovere il finale "\ n" da ogni parola.
    """
    topics = open(
        "data/all-topics-strings.lc.txt", "r"
    ).readlines()
    topics = [t.strip() for t in topics]
    return topics


def filter_doc_list_through_topics(topics, docs):
    """
    Legge tutti i documenti e crea un nuovo elenco di due tuple che
    contengono una singola voce di funzionalità e il corpo del testo,
    invece di un elenco di argomenti. Rimuove tutte le caratteristiche
    geografiche e conserva solo quei documenti che hanno almeno un
    argomento non geografico.
    """
    ref_docs = []
    for d in docs:
        if d[0] == [] or d[0] == "":
            continue
        for t in d[0]:
            if t in topics:
                d_tup = (t, d[1])
                ref_docs.append(d_tup)
                break
    return ref_docs


if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    filename = "data/reut2-000.sgm"
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    doc = parser.parse(open(filename, 'rb'))

    # Ottenere i tags e filtrare il documento con essi
    topics = obtain_topic_tags()
    ref_docs = filter_doc_list_through_topics(topics, docs)
    pprint.pprint(list(doc))

L’output è il seguente:

..
..
('acq',
  'Security Pacific Corp said it completed its planned merger with Diablo '
  'Bank following the approval of the comptroller of the currency. Security '
  'Pacific announced its intention to merge with Diablo Bank, headquartered '
  'in Danville, Calif., in September 1986 as part of its plan to expand its '
  'retail network in Northern California. Diablo has a bank offices in '
  'Danville, San Ramon and Alamo, Calif., Security Pacific also said. '
  'Reuter '),
 ('earn',
  'Shr six cts vs five cts Net 188,000 vs 130,000 Revs 12.2 mln vs 10.1 mln '
  'Avg shrs 3,029,930 vs 2,764,544 12 mths Shr 81 cts vs 1.45 dlrs Net '
  '2,463,000 vs 3,718,000 Revs 52.4 mln vs 47.5 mln Avg shrs 3,029,930 vs '
  '2,566,680 NOTE: net for 1985 includes 500,000, or 20 cts per share, for '
  'proceeds of a life insurance policy. includes tax benefit for prior qtr '
  'of approximately 150,000 of which 140,000 relates to a lower effective '
  'tax rate based on operating results for the year as a whole. Reuter '),
..
..
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:

..
from sklearn.feature_extraction.text import TfidfVectorizer
..
..

def create_tfidf_training_data(docs):
    """
    Crea un elenco di corpus di documenti (rimuovendo le etichette della classe),
    quindi applica la trasformazione TF-IDF a questo elenco.

   La funzione restituisce sia il vettore etichetta di classe (y) che
   la matrice token / feature corpus (X).
    """
    # Crea le classi di etichette per i dati di addestramento
    y = [d[0] for d in docs]

    # Crea la lista dei corpus del documenti
    corpus = [d[1] for d in docs]

    # Create la vettorizzazione TF-IDF e trasforma il corpus
    vectorizer = TfidfVectorizer(min_df=1)
    X = vectorizer.fit_transform(corpus)
    return X, y


if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    filename = "data/reut2-000.sgm"
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    docs = list(parser.parse(open(filename, 'rb')))

    # Ottenere i tags e filtrare il documento con essi
    topics = obtain_topic_tags()
    ref_docs = filter_doc_list_through_topics(topics, docs)

    # Vettorizzazione e TF-IDF
    X, y = create_tfidf_training_data(ref_docs)
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:

from sklearn.model_selection import train_test_split
..
..
X_train, X_test, y_train, y_test = train_test_split(
  X, y, test_size=0.2, random_state=42
)

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:

from sklearn.svm import SVC
..
..

def train_svm(X, y):
    """
    Crea e addestra la Support Vector Machine.
    """
    svm = SVC(C=1000000.0, gamma=0.0, kernel='rbf')
    svm.fit(X, y)
    return svm


if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    filename = "data/reut2-000.sgm"
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    docs = list(parser.parse(open(filename, 'rb')))

    # Ottenere i tags e filtrare il documento con essi
    topics = obtain_topic_tags()
    ref_docs = filter_doc_list_through_topics(topics, docs)

    # Vettorizzazione e TF-IDF
    X, y = create_tfidf_training_data(ref_docs)

    # Crea il training-test split dei dati
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42
    )

    # Crea ed addestra la Support Vector Machine
    svm = train_svm(X_train, y_train)

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:

..
..
from sklearn.metrics import confusion_matrix
..
..


if __name__ == "__main__":

    ..
    ..

    # Crea ed addestra la Support Vector Machine
    svm = train_svm(X_train, y_train)

    # Crea un array delle predizioni con i dati di test
    pred = svm.predict(X_test)

    # Calcolo del hit-rate e della confusion matrix per ogni modello
    print(svm.score(X_test, y_test))
    print(confusion_matrix(pred, y_test))
L’output dello script è il seguente:
0.660194174757
[[21  0  0  0  2  3  0  0  0  1  0  0  0  0  1  1  1  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  4  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  1  0  0  1 26  0  0  0  1  0  1  0  1  0  0  0  0  0]
 [ 0  0  0  0  0  0  2  0  0  0  0  0  0  0  0  0  0  0  1]
 [ 0  0  0  0  0  0  0  1  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  1  0  0  0  0  3  0  0  0  0  0  0  0  0  0]
 [ 3  0  0  1  2  2  3  0  1  1  6  0  1  0  0  0  2  3  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  1  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  1  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  1  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]]

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.

if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    files = ["data/reut2-%03d.sgm" % r for r in range(0, 22)]
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    docs = []
    for fn in files:
        for d in parser.parse(open(fn, 'rb')):
            docs.append(d)

    ..
    ..

    print(svm.score(X_test, y_test))

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

0.835971855761

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:

import html
import pprint
import re
from html.parser import HTMLParser
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import confusion_matrix


class ReutersParser(HTMLParser):
    """
    ReutersParser è una sottoclasse HTMLParser e viene utilizzato per aprire file SGML
    associati al dataset di Reuters-21578.

    Il parser è un generatore e produrrà un singolo documento alla volta.
    Poiché i dati verranno suddivisi in blocchi durante l'analisi, è necessario 
    mantenere alcuni stati interni di quando i tag sono stati "inseriti" e 
    "eliminati".
    Da qui le variabili booleani in_body, in_topics e in_topic_d.
    """

    def __init__(self, encoding='latin-1'):
        """
        Inizializzo la superclasse (HTMLParser) e imposto il parser.
        Imposto la decodifica dei file SGML con latin-1 come default.
        """
        html.parser.HTMLParser.__init__(self)
        self._reset()
        self.encoding = encoding

    def _reset(self):
        """
        Viene chiamata solo durante l'inizializzazione della classe parser
        e quando è stata generata una nuova tupla topic-body. Si
        resetta tutto lo stato in modo che una nuova tupla possa essere
        successivamente generato.
        """
        self.in_body = False
        self.in_topics = False
        self.in_topic_d = False
        self.body = ""
        self.topics = []
        self.topic_d = ""

    def parse(self, fd):
        """
        parse accetta un descrittore di file e carica i dati in blocchi
        per ridurre al minimo l'utilizzo della memoria.
        Quindi produce nuovi documenti man mano che vengono analizzati.
        """
        self.docs = []
        for chunk in fd:
            self.feed(chunk.decode(self.encoding))
            for doc in self.docs:
                yield doc
            self.docs = []
        self.close()

    def handle_starttag(self, tag, attrs):
        """
        Questo metodo viene utilizzato per determinare cosa fare quando
        il parser incontra un particolare tag di tipo "tag".
        In questo caso, impostiamo semplicemente i valori booleani
        interni su True se è stato trovato quel particolare tag.
        """
        if tag == "reuters":
            pass
        elif tag == "body":
            self.in_body = True
        elif tag == "topics":
            self.in_topics = True
        elif tag == "d":
            self.in_topic_d = True

    def handle_endtag(self, tag):
        """
        Questo metodo viene utilizzato per determinare cosa fare
        quando il parser termina con un particolare tag di tipo "tag".

        Se il tag è un tag <REUTERS>, rimuoviamo tutti gli spazi bianchi
        con un'espressione regolare e quindi aggiungiamo la tupla topic-body.

        Se il tag è un tag <BODY> o <TOPICS>, impostiamo semplicemente lo
        stato interno su False per questi valori booleani, rispettivamente.

        Se il tag è un tag <D> (che si trova all'interno di un tag <TOPICS>),
        aggiungiamo l'argomento specifico all'elenco "topics" e infine lo resettiamo.
        """
        if tag == "reuters":
            self.body = re.sub(r'\s+', r' ', self.body)
            self.docs.append((self.topics, self.body))
            self._reset()
        elif tag == "body":
            self.in_body = False
        elif tag == "topics":
            self.in_topics = False
        elif tag == "d":
            self.in_topic_d = False
            self.topics.append(self.topic_d)
            self.topic_d = ""

    def handle_data(self, data):
        """
        I dati vengono semplicemente aggiunti allo stato appropriato
        per quel particolare tag, fino a quando non viene visualizzato
        il tag di chiusura finale.
        """
        if self.in_body:
            self.body += data
        elif self.in_topic_d:
            self.topic_d += data


def obtain_topic_tags():
    """
    Apre il file dell'elenco degli argomenti e importa tutti i nomi
    degli argomenti facendo attenzione a rimuovere il finale "\ n" da ogni parola.
    """
    topics = open(
        "data/all-topics-strings.lc.txt", "r"
    ).readlines()
    topics = [t.strip() for t in topics]
    return topics


def filter_doc_list_through_topics(topics, docs):
    """
    Legge tutti i documenti e crea un nuovo elenco di due tuple che
    contengono una singola voce di funzionalità e il corpo del testo,
    invece di un elenco di argomenti. Rimuove tutte le caratteristiche
    geografiche e conserva solo quei documenti che hanno almeno un
    argomento non geografico.
    """
    ref_docs = []
    for d in docs:
        if d[0] == [] or d[0] == "":
            continue
        for t in d[0]:
            if t in topics:
                d_tup = (t, d[1])
                ref_docs.append(d_tup)
                break
    return ref_docs


def create_tfidf_training_data(docs):
    """
    Crea un elenco di corpus di documenti (rimuovendo le etichette della classe),
    quindi applica la trasformazione TF-IDF a questo elenco.

   La funzione restituisce sia il vettore etichetta di classe (y) che
   la matrice token / feature corpus (X).
    """
    # Crea le classi di etichette per i dati di addestramento
    y = [d[0] for d in docs]

    # Crea la lista dei corpus del documenti
    corpus = [d[1] for d in docs]

    # Create la vettorizzazione TF-IDF e trasforma il corpus
    vectorizer = TfidfVectorizer(min_df=1)
    X = vectorizer.fit_transform(corpus)
    return X, y



def train_svm(X, y):
    """
    Crea e addestra la Support Vector Machine.
    """
    svm = SVC(C=1000000.0, gamma=0.0, kernel='rbf')
    svm.fit(X, y)
    return svm


if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    filename = "data/reut2-000.sgm"
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    docs = list(parser.parse(open(filename, 'rb')))




if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    files = ["data/reut2-%03d.sgm" % r for r in range(0, 22)]
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    docs = []
    for fn in files:
        for d in parser.parse(open(fn, 'rb')):
            docs.append(d)
    # Ottenere i tags e filtrare il documento con essi
    topics = obtain_topic_tags()
    ref_docs = filter_doc_list_through_topics(topics, docs)

    # Vettorizzazione e TF-IDF
    X, y = create_tfidf_training_data(ref_docs)

    # Crea il training-test split dei dati
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42
    )


    # Crea ed addestra la Support Vector Machine
    svm = train_svm(X_train, y_train)

    # Crea un array delle predizioni con i dati di test
    pred = svm.predict(X_test)

    # Calcolo del hit-rate e della confusion matrix per ogni modello
    print(svm.score(X_test, y_test))
    print(confusion_matrix(pred, y_test))

Guida introduttiva alla Support Vector Machine (SVM)

Introduzione SVM Machine Learning Trading algoritmico

In questo articolo introduciamo una tecnica di machine learning estremamente potente nota come Support Vector Machine (SVM). È una delle migliori tecniche di classificazione supervisionate “fuori dagli schemi”. In quanto tale, è uno strumento importante sia per il ricercatore di trading quantitativo che per il data scientist.

E’ molto importante per un ricercatore quantistico o uno scienziato dei dati essere a proprio agio sia con gli aspetti teorici che con l’uso pratico delle tecniche presenti nella propria cassetta degli attrezzi. Quindi questo articolo costituirà la prima parte di una serie di articoli che approfondiscono le macchine a vettore di supporto. In particolare questo articolo descrive la teoria dei maximal margin classifiers, support vector classifiers e support vector machines. Negli articoli successivi si utilizzerà la libreria scikit-learn di Python per dimostrare alcuni esempi delle suddette tecniche teoriche su dati reali.

Come con qualsiasi classificatore binario supervisionato, il compito di una macchina a vettore di supporto è individuare un confine di separazione (lineare o altro) in uno spazio di elementi in modo tale che le osservazioni successive possano essere automaticamente classificate in gruppi separati. Un buon esempio di questo sistema è classificare un insieme di documenti in gruppi di documenti con sentiment positivi o negativi. Allo stesso modo, potremmo classificare le e-mail in spam o non spam. Gli SVM sono altamente applicabili a tali situazioni.

Procederemo considerando il concetto di un iperpiano di separazione ottimale, che consiste in un semplice tipo di classificatore lineare noto come maximal margin classifier. Mostreremo che spesso non sono applicabili a molte situazioni del “mondo reale” e come tali necessitano di modifiche, che prendono la forma di un classificatore di vettori di supporto (SVC). Abbondoniamo quindi il vincolo della linearità e prenderemo in considerazione classificatori non lineari, ovvero macchine vettoriali di supporto, che utilizzano le funzioni del kernel per migliorare l’efficienza computazionale.

Iperpiano di Separazione Ottimale

Prima di qualsiasi discussione sul funzionamento di un SVC o SVM, è necessario delineare un concetto noto come iperpiano di separazione ottimale (OSH).

Si consideri uno spazio \(p\)-dimensionale a valore reale (ad esempio \(\mathbb{R}^p[/ latex]). Un iperpiano di separazione ottimale è essenzialmente uno spazio affine \(p-1\)-dimensionale che vive all’interno del più grande spazio \(p\)-dimensionale. Descriveremo in seguito da dove deriva la “separazione ottimale” presente nel nome. Nel caso di \(p=2\) questo spazio affine è semplicemente una linea unidimensionale, mentre per \(p=3\) lo spazio affine è un piano bidimensionale (vedi Fig 1 e Fig 2).
trading-machine-learning-svm-0001
Fig 1 e Fig 2 - Iperpiani Uni- e Bi-dimensionali
Per dimensioni maggiori di \(p=3\) tale spazio affine è noto come iperpiano. Questo è decisamente difficile (se non impossibile!) da visualizzare, ma è certamente possibile coglierlo concettualmente. Da notare che “affine” si riferisce a un iperpiano che non deve necessariamente passare attraverso l’origine (o l’elemento zero) dello spazio più grande. Se consideriamo gli elementi nello spazio \(p\)-dimensionale, cioè \(x=(X_1, …, X_p)\in \mathbb{R}^p\), un tale iperpiano affine \(p-1\)-dimensionale è definito dalla seguente equazione:

\(\begin{eqnarray} \beta_0 + \beta_1 X_1 + … + \beta_p X_p = 0 \end{eqnarray}\)

o equivalentemente:

\(\begin{eqnarray} \beta_0 + \sum^{p}_{j=1} \beta_j X_j = 0 \end{eqnarray}\)

Se un elemento \(x\in\mathbb{R}^p\) soddisfa questa relazione allora vive sull’iperpiano latex]p-1\)-dimensionale. Possiamo anche considerare altri punti \(x\) tali che:

\(\begin{eqnarray} \beta_0 + \sum^{p}_{j=1} \beta_j X_j > 0 \end{eqnarray}\)

nel qual caso \(x\) si trova sopra l’iperpiano, o

\(\begin{eqnarray}\beta_0+\sum^{p}_{j=1}\beta_j X_j <0 \end{eqnarray}\)

in tal caso \(x\) si trova al di sotto dell’iperpiano. Quindi l’iperpiano divide lo spazio \(p\)-dimensionale in due parti (vedi la seguente Figura 3), da cui deriva la “separazione” presente nella denominazione di “iperpiano di separazione ottimale”. Non abbiamo ancora discusso della parte “ottimale”!
trading-machine-learning-svm-0002
Fig 3: Separazione dello spazio p-dimensionale di un iperpiano
Il punto chiave è la possibilità di determinare su quale lato del piano cadrà qualsiasi punto \(x\) calcolando il segno dell’espressione \(\beta_0+\sum^{p}_{j=1}\ beta_j X_j \). Questo concetto costituirà la base di una tecnica di classificazione controllata.

Classificazione

Continuando con l’esempio di filtraggio dello e-mail spam, possiamo pensare al nostro problema di classificazione (diciamo) come composto da un migliaio di e-mail (\(n=1000\)), ciascuna delle quali è contrassegnata come spam (\(+1\)) o non spam (\(-1\)). Inoltre, a ogni e-mail è associato un insieme di parole chiave (ovvero la separazione delle parole sulla spaziatura) che costituisco le features. Quindi, se prendiamo l’insieme di tutte le possibili parole chiave da tutte le email (e rimuoviamo i duplicati), rimarremo con \(p\) parole chiave in totale.

Se traduciamo questo in un problema matematico, l’impostazione standard per una procedura di classificazione supervisionata consiste nel considerare un insieme di \(n\) osservazioni di addestramento, \(x_i\), ognuna delle quali è un vettore \(p\)-dimensionale di features. Ogni osservazione di addestramento ha associata un class label, \(y_i\in\{-1,1\}\). Quindi possiamo pensare a \(n\) coppie di osservazioni di addestramento \((x_i, y_i)\) che rappresentano le caratteristiche e le class label (elenchi di parole chiave e spam / non spam). Oltre alle osservazioni di addestramento possiamo fornire osservazioni di prova, \(x^{*}=(x^{*}_1,…,x^{*}_p)\) che vengono successivamente utilizzate per testare le prestazioni dei classificatori. Queste osservazioni di prova sarebbero nuove e-mail che non sono state ancora viste.

Il nostro obiettivo è sviluppare un classificatore, basato sulle osservazioni di addestramento fornite, che classificherà correttamente le successive osservazioni di test utilizzando solo i valori delle loro caratteristiche. Ciò si traduce nella possibilità di classificare un’e-mail come spam o non spam esclusivamente in base alle parole chiave contenute al suo interno. Inizialmente si ipotizza che sia possibile, tramite un mezzo ancora da determinare, costruire un iperpiano che separa perfettamente i dati di addestramento secondo le loro etichette di classe (vedi Fig. 4 e 5). Ciò significherebbe separare in modo pulito le e-mail di spam da quelle non di spam esclusivamente utilizzando specifiche parole chiave. Il diagramma seguente mostra solo \(p=2\), mentre per gli elenchi di parole chiave potremmo avere \(p>10000\). Quindi le figure 4 e 5 sono solo rappresentative del problema.
trading-machine-learning-svm-0003
Fig 4: Iperpiani di separazione multipli; Fig 5: Separazione perfetta di classi di dati
Ciò si traduce in una proprietà di separazione matematica di:

\(\begin{eqnarray} \beta_0 + \beta_1 X_{i1} + … + \beta_p X_{ip} = \beta_0 + \sum^{p}_{j=1} \beta_j X_{ij} > 0\end{eqnarray}\) , se \(y_i=1\)

e

\(\begin{eqnarray} \beta_0 + \beta_1 X_{i1} + … + \beta_p X_{ip} = \beta_0 + \sum^{p}_{j=1} \beta_j X_{ij} < 0\end{eqnarray}\) , se \(y_i=-1\)

Cioè se ogni osservazione di addestramento è sopra o sotto l’iperpiano di separazione, secondo l’equazione geometrica che definisce il piano, la sua etichetta di classe associata sarà \(+1\) o \(-1\). Così abbiamo (potenzialmente) sviluppato un semplice processo di classificazione. Assegniamo un’osservazione di prova ad una classe a seconda del lato dell’iperpiano in cui si trova. Questo può essere formalizzato considerando la seguente funzione \(f(x)\), con un’osservazione di prova \( x^{*} = (X ^ {*} _ 1, …, X ^ {*} _ p)\):

\(\begin{eqnarray} f(x^{*}) = \beta_0 + \sum^{p}_{j=1} \beta_j X^{*}_j \end{eqnarray}\)

Se \(f (x ^ {*}) > 0 \) allora \( y ^ {*} = + 1 \), mentre se \( f (x ^ {*}) < 0 \) allora \( y ^ {*} = -1 \).

Possiamo anche considerare l’ampiezza della distanza \( f (x) \). Per \( | f (x ^ {*}) | \) molto maggiore di zero possiamo essere sicuri della nostra particolare etichetta di classe, poiché il valore è lontano dall’iperpiano di separazione. Al contrario per \( | f (x ^ {*}) | \) vicino a zero, l’osservazione del test si trova vicino all’iperpiano di separazione e quindi abbiamo meno fiducia nella nostra class label.

Maximal Margin Classifiers

Dobbiamo ancora descrivere esattamente come costruire un iperpiano di separazione, né abbiamo definito cosa significhi essere “ottimale”.
In generale, gli iperpiani di separazione non sono unici, poiché è possibile traslare o ruotare leggermente un iperpiano senza toccare alcuna osservazione di addestramento (vedi Fig 4).

Allora come possiamo decidere qual è il “migliore” iperpiano di separazione o quella più “ottimale”? Possiamo costruire un iperpiano del margine massimo (MMH), che è l’iperpiano di separazione più lontano da qualsiasi osservazione di addestramento.
Come può essere eseguita? In primo luogo, si calcola la distanza perpendicolare da ciascuna osservazione di addestramento \(x_i\) per un dato iperpiano di separazione. La distanza perpendicolare più vicina a un’osservazione di training dall’iperpiano è nota come margine. MMH è l’iperpiano di separazione in cui il margine è il più grande. Ciò garantisce che sia la distanza minima più lontana da un’osservazione di training.

La procedura di classificazione è quindi semplicemente un caso per determinare su quale lato cade un’osservazione di prova. Questo può essere eseguito utilizzando la formula sopra per \(f (x ^ {*})\). Tale classificatore è noto come classificatore del margine massimo (MMC). Dobbiamo sperare che un ampio margine sulla serie di osservazioni di addestramento porti anche a un ampio margine sulle osservazioni di prova, e quindi fornisca un buon tasso di classificazione.

Si noti tuttavia che dobbiamo fare attenzione a evitare l’overfitting quando il numero di dimensioni delle feature è elevato (ad esempio nelle applicazioni di elaborazione del linguaggio naturale come la classificazione dello spam e-mail). In questo caso l’overfitting significa che l’MMH si adatta molto bene ai dati di allenamento, ma può funzionare abbastanza male se esposto ai dati di test. L’obiettivo di tale algoritmo è produrre i valori \(\beta_j\) (cioè correggere la geometria dell’iperpiano) e quindi consentire la determinazione di \(f(x^{*})\) per qualsiasi osservazione di prova.
Se consideriamo la Fig 6, possiamo vedere che la MMH è la linea mediana del “blocco” più largo che possiamo inserire tra le due classi in modo che siano perfettamente separate.
trading-machine-learning-svm-0004
Fig 6: Iperpiano del margine massimo con vettori di supporto (A, B e C)

Una delle caratteristiche chiave della MMC (e successivamente della SVC e SVM) è la dipendenza della posizione della MMH solamente dai vettori di supporto, che sono le osservazioni di addestramento che si trovano direttamente sul confine del margine (ma non dell’iperpiano) (vedere i punti A , B e C della Fig 6). Ciò significa che la posizione dell’MMH NON dipende da altre osservazioni di addestramento.


Si può immediatamente osservare come il potenziale svantaggio dell’MMC è che il suo MMH (e quindi le sue prestazioni di classificazione) può essere estremamente sensibile alle posizioni del vettore di supporto.

Costruzione del Maximal Margin Classifier

Ritengo sia istruttivo delineare completamente il problema di ottimizzazione che deve essere risolto per creare l’MMH (e quindi lo stesso MMC). Mentre descriverò i vincoli del problema di ottimizzazione, la soluzione algoritmica a questo problema va oltre lo scopo dell’articolo. Per fortuna queste routine di ottimizzazione sono già implementate all’interno della libreria scikit-learn (in realtà, tramite la libreria LIBSVM).

La procedura per determinare un iperpiano del margine massimo per un classificatore del margine massimo è la seguente. Date \(n\) osservazioni di addestramento \(x_1,…,x_n\in\mathbb{R}^p\) e \(n\) class label \(y_1,…,y_n\in\{- 1,1\}\), l’MMH è la soluzione alla seguente procedura di ottimizzazione:

Massimizza \(M\in\mathbb{R}\), variando \(\beta_1,…,\beta_p\) in modo che:

\(\begin{eqnarray} \sum^{p}_{j=1} \beta^2_j = 1 \end{eqnarray}\)

e

\(\begin{eqnarray} y_i \left( \beta_0 + \sum^{p}_{j=1} \beta_j X_{ij} \right) \geq M, \quad \forall i = 1,…,n \end{eqnarray}\)

Nonostante i complessi vincoli formali, in realtà si afferma che ogni osservazione deve essere sul lato corretto dell’iperpiano e almeno \(M\) di distanza da esso. Poiché l’obiettivo della procedura è massimizzare \(M\), questa è precisamente la condizione necessaria per creare la MMC!


Chiaramente, il caso ideale è la perfetta separabilità. La maggior parte dei dataset del “mondo reale” non avrà una separabilità così perfetta tramite un iperpiano lineare (vedi Fig 7). Tuttavia, se non c’è separabilità, non siamo in grado di costruire un MMC con la procedura di ottimizzazione descritta sopra. Allora, come creiamo una forma di iperpiano separatore?

trading-machine-learning-svm-0005
Fig 7: Nessuna possibilità di un vero iperpiano di separazione

Essenzialmente dobbiamo approssimare il requisito che un iperpiano di separazione divida perfettamente ogni osservazione di training sul lato corretto della linea (cioè garantisca che sia associata alla sua vera class label), utilizzando quello che viene chiamato margine morbido. Ciò motiva il concetto di un classificatore di vettori di supporto (SVC).

Support Vector Classifiers (SVC)

Come accennato in precedenza, uno dei problemi con il MMC è l’estrema sensibilità all’aggiunta di nuove osservazioni di training.
Si consideri le figure 8 e 9. Nella figura 8 si può vedere che esiste un MMH che separa perfettamente le due classi. Tuttavia, nella Figura 9 se si aggiunge un punto alla classe \(+1\) vediamo che la posizione dell’MMH cambia drasticamente. Quindi in questa situazione l’MMH ha avuto chiaramente un over-fit:

trading-machine-learning-svm-0006
Fig 8 e Fig 9: L'aggiunta di un singolo punot modifica drasticamente la linea di MMH
Come accennato anche in precedenza, si può considerare un classificatore basato su un iperpiano separatore che non separa perfettamente le due classi, ma ha una maggiore robustezza all’aggiunta di nuove osservazioni individuali e ha una migliore classificazione per la maggior parte delle osservazioni di training. Ciò avviene a scapito di alcuni errori di classificazione per alcune osservazioni di addestramento.

Ecco come funziona un classificatore SVC o a margine morbido. Un SVC consente ad alcune osservazioni di trovarsi sul lato errato del margine (o iperpiano), quindi fornisce una separazione “morbida”. Le seguenti figure 10 e 11 dimostrano che le osservazioni si trovano rispettivamente sul lato sbagliato del margine e sul lato sbagliato dell’iperpiano:
trading-machine-learning-svm-0007
Fig 10 e Fig 11: Osservazioni sul lato sbagliato del margine e dell'iperpiano, rispettivamente

Come prima, un’osservazione viene classificata a seconda del lato dell’iperpiano di separazione su cui si trova, ma alcuni punti possono essere classificati erroneamente.

È istruttivo vedere come la procedura di ottimizzazione differisce da quella sopra descritta per la MMC. Dobbiamo introdurre nuovi parametri, vale a dire i valori \(n\) \(\epsilon_i\) (noti come valori di slack) e un parametro \(C\), noto come budget. Si vuole massimizzare \(M\), attraverso \(\beta_1,…,\beta_p,\epsilon_1,…,\epsilon_n\) in modo che:

\(\begin{eqnarray} \sum^{p}_{j=1} \beta^2_j = 1 \end{eqnarray}\)

e

\(\begin{eqnarray} y_i \left( \beta_0 + \sum^{p}_{j=1} \beta_j X_{ij} \right) \geq M, \quad \forall i = 1,…,n \end{eqnarray}\)

e

\(\begin{eqnarray} \epsilon_i \geq 0, \quad \sum^{n}_{i=1} \epsilon_i \leq C \end{eqnarray}\)

Dove \(C\), il budget, è un parametro di “regolazione” non negativo. \(M\) rappresenta ancora il margine e le variabili slack \(\epsilon_i\) consentono alle singole osservazioni di trovarsi sul lato sbagliato del margine o dell’iperpiano.

In sostanza, \(\epsilon_i\) ci dice dove si trova l’osservazione \(i\)-esima rispetto al margine e all’iperpiano. Per \(\epsilon_i = 0 \) si afferma che l’osservazione di addestramento \(x_i\) si trova sul lato corretto del margine. Per \(\epsilon_i>0\) abbiamo che \(x_i\) è dalla parte sbagliata del margine, mentre per \(\epsilon_i > 1\) abbiamo che \(x_i\) è dalla parte sbagliata dell’iperpiano.

\(C\) controlla collettivamente quanto il singolo \(\epsilon_i\) può essere modificato per violare il margine. \(C = 0 \) implica che \(\epsilon_i = 0, \forall i \) e quindi non è possibile alcuna violazione del margine, nel qual caso (per classi separabili) abbiamo la situazione MMC.

Per \(C>0 \) significa che non più di \(C\) osservazioni possono violare l’iperpiano. All’aumentare di \(C\), il margine aumenterà. Vedi Fig 12 e 13 per due diversi valori di \(C\):

trading-machine-learning-svm-0008
Fig 12 e Fig 13: Differenti valori per il paramentro \(C\)

Come scegliamo in pratica \(C\)? In genere questo viene fatto tramite la convalida incrociata. In sostanza \(C\) è il parametro che regola il compromesso di bias-varianza per SVC. Un piccolo valore di \(C\) indica una situazione di basso bias e alta varianza. Un valore elevato di \(C\) indica una situazione di alto bias e bassa varianza.

Come prima, per classificare una nuova osservazione di prova \(x^{*}\) calcoliamo semplicemente il segno di \(f(x^{*})=\beta_0 + \sum^{p}_{i = 1} \beta_j X^{*}_j\).

Tutto questo va bene per le classi che sono separate linearmente (o quasi linearmente). Tuttavia, che dire dei confini di separazione che non sono lineari? Come affrontiamo queste situazioni? È qui che entrano in gioco le macchine a vettore di supporto.

Macchine a Vettori di Supporto

La motivazione dietro l’estensione di una SVC è quella di consentire confini decisionali non lineari. Questo è il dominio della Support Vector Machine (SVM). Si consideri le seguenti figure 14 e 15. In una tale situazione un SVC puramente lineare avrà prestazioni estremamente scarse, semplicemente perché i dati non hanno una chiara separazione lineare:

trading-machine-learning-svm-0009
Fig 14 e Fig 15: Nessuna chiara separazione lineare tra le classi e quindi una scarsa prestazione della SVC

Quindi le SVC possono essere inutili in problemi di confine di classe altamente non lineari.

Per motivare il funzionamento di una SVM, possiamo considerare un “trucco” standard nella regressione lineare, quando si considerano situazioni non lineari. In particolare un insieme di \(p\) caratteristiche \(X_1, …, X_p\) può essere trasformato, diciamo, in un insieme di \(2p\) caratteristiche \(X_1, X^2_1, …, X_p, X^2_p \). Questo ci permette di applicare una tecnica lineare a un insieme di caratteristiche non lineari.

Mentre il confine di decisione è lineare nel nuovo spazio degli elementi \(2p\)-dimensionale, non è lineare nello spazio \(p\)-dimensionale originale. Il risultato è un confine di decisione dato da \( q(x)=0\) dove \(q\) è una funzione polinomiale quadratica delle features originali, cioè è una soluzione non lineare.

Questo chiaramente non è limitato ai polinomi quadratici. Potrebbero essere presi in considerazione polinomi di dimensione superiore, termini di interazione e altre forme funzionali. Anche se lo svantaggio è che aumenta notevolmente la dimensione dello spazio delle features al punto che alcuni algoritmi possono diventare non trattabili.

Il vantaggio principale degli SVM consiste in un ampliamento non lineare dello spazio delle funzionalità, pur mantenendo una significativa efficienza computazionale, utilizzando un processo noto come “metodo kernel“, che verrà delineato di seguito.

Allora cosa sono gli SVM? In sostanza sono un’estensione di SVC che risulta dall’ampliamento dello spazio delle features attraverso l’uso di funzioni note come kernel. Per capire i kernel, dobbiamo discutere brevemente alcuni aspetti della soluzione al problema di ottimizzazione SVC delineato sopra.

Durante il calcolo della soluzione al problema di ottimizzazione SVC, l’algoritmo deve solamente fare uso di prodotti interni tra le osservazioni e le loro stesse non-osservazioni. Ricorda che per due \(p\)-vettori dimensionali \(u, v\) un prodotto interno è definito come:

\(\begin{eqnarray} \langle u,v \rangle = \sum^{p}_{i=1} u_i v_i \end{eqnarray}\)

Quindi per due osservazioni un prodotto interno è definito come:

\(\begin{eqnarray} \langle x_i,x_k \rangle = \sum^{p}_{j=1} x_{ij} x_{kj} \end{eqnarray}\)

Anche se non ci soffermeremo sui dettagli (poiché esulano dallo scopo di questo articolo), è possibile mostrare che un classificatore di vettore di supporto lineare per una particolare osservazione \(x\) può essere rappresentato come una combinazione lineare di prodotti interni:

\(\begin{eqnarray} f(x) = \beta_0 + \sum^{n}_{i=1} \alpha_i \langle x, x_i \rangle \end{eqnarray}\)

Con i coefficienti \(n\) e \(\alpha_i\(, uno per ciascuna delle osservazioni di addestramento.

Per stimare i coefficienti [latex]\beta_0[latex] e [latex]\alpha_i\) dobbiamo solo calcolare \({n \choose 2} = n(n-1)/2\) prodotti interni tra tutte le coppie di osservazioni di addestramento. In effetti, dobbiamo SOLO calcolare i prodotti interni per il sottoinsieme di osservazioni di addestramento che rappresentano i vettori di supporto. Chiamerò questo sottoinsieme \(\mathscr{S}\). Ciò significa che:

\(\begin{eqnarray} \alpha_i = 0 \enspace \text{if} \enspace x_i \notin \mathscr{S} \end{eqnarray}\)

Quindi possiamo riscrivere la formula di rappresentazione come:

\(\begin{eqnarray} f(x) = \beta_0 + \sum_{i \in \mathscr{S}} \alpha_i \langle x, x_i \rangle \end{eqnarray}\)

Questo risulta essere un grande vantaggio per l’efficienza computazionale.

Questo motiva l’estensione agli SVM. Se consideriamo il prodotto interno \(\langle x_i, x_k \rangle\) e lo sostituiamo con la più generale funzione “kernel” del prodotto interno \(K = K(x_i, x_k) \), possiamo modificare la rappresentazione SVC per usare le funzioni non lineari del kernel e quindi modificare il modo in cui calcoliamo la “somiglianza” tra due osservazioni. Ad esempio, per ripristinare l’SVC, prendiamo solo \(K\) come segue:

\(\begin{eqnarray} K(x_i, x_k) = \sum^{p}_{j=1} x_{ij} x_{kj} \end{eqnarray}\)

Poiché questo kernel è lineare nelle sue caratteristiche, l’SVC è noto come SVC lineare. Possiamo anche considerare kernel polinomiali, di grado \(d\):

\(\begin{eqnarray} K(x_i, x_k) = (1 + \sum^{p}_{j=1} x_{ij} x_{kj})^d \end{eqnarray}\)

Ciò fornisce un confine decisionale significativamente più flessibile ed essenzialmente equivale ad adattare una SVC in uno spazio di caratteristiche di dimensione superiore che coinvolge polinomi di \(d\) gradi delle caratteristiche (vedi la Figura 16).

trading-machine-learning-svm-0010
Fig 16: kernel polinomiale di grado [latex]d\); Fig 17: un kernel radiale

Quindi, la definizione di una macchina vettoriale di supporto è un classificatore di vettori di supporto con una funzione kernel non lineare. Possiamo anche considerare il popolare kernel radiale (vedi Fig 17):

\(\begin{eqnarray} K(x_i, x_k) = \exp \left(-\gamma \sum^{p}_{j=1} (x_{ij} – x_{kj})^2 \right), \quad \gamma > 0 \end{eqnarray}\)

Allora come funzionano i kernel radiali? Sono chiaramente abbastanza diversi dai kernel polinomiali. Essenzialmente se la nostra osservazione di prova \(x^{*}\) è lontana da un’osservazione di addestramento \(x_i\) nella distanza euclidea standard, allora la somma \(\sum^{p}_{j=1} (x^{*}_j-x_{ij})^2\) sarà grande e quindi \(K(x^{*}, x_i)\) sarà molto piccolo. Questa particolare osservazione di addestramento \(x_i\) non avrà quasi alcun effetto sulla posizione dell’osservazione di prova \(x^{*}\), tramite \(f(x^{*})\).

Quindi il kernel radiale ha un comportamento estremamente localizzato e solo le osservazioni di addestramento vicine a \(x^{*}\) avranno un impatto sulla sua class label.

Per rendere concreta la teoria di cui sopra, negli articoli successivi eseguiremo classificazioni di esempio utilizzando la libreria scikit-learn di Python.

Tecniche di Machine Learning Statistico

Il machine learning statistico è un ampio campo interdisciplinare, con molte aree di ricerca disparate.
Dopo aver introdotto le basi del machine learning statistico, questo articolo descrive le tecniche più rilevanti per la finanza quantitativa e in particolare per il trading algoritmico.

Regressione

La regressione si riferisce a un ampio gruppo di tecniche di apprendimento automatico supervisionate che forniscono capacità predittive ed inferenziali. Una parte significativa della finanza quantitativa si avvale delle tecniche di regressione e quindi è essenziale avere familiarità con questo processo. La regressione tenta di modellare la relazione tra una variabile dipendente (risposta) e un insieme di variabili indipendenti (predittori). In particolare, l’obiettivo della regressione è accertare la variazione di una risposta, quando cambia una delle variabili indipendenti, nel presupposto che le restanti variabili indipendenti siano mantenute fisse.

La tecnica di regressione più conosciuta è la regressione lineare, che presuppone una relazione lineare tra i predittori e la risposta. Tale modello prevede la stima dei parametri
(solitamente indicato dal vettore β) per la risposta lineare a ciascun predittore. Questi parametri sono stimati tramite una procedura nota come metodo dei minimi quadrati (OLS). La regressione lineare può essere utilizzata sia per la previsione che per l’inferenza.

Nel primo caso è possibile aggiungere un nuovo valore del predittore (senza una risposta corrispondente) al fine di prevedere un nuovo valore di risposta. Ad esempio, si consideri un modello di regressione lineare utilizzato per prevedere il valore del S&P500 per il giorno successivo, utilizzando i dati dei prezzi degli ultimi cinque giorni. Il modello può essere allenato utilizzando OLS su dati storici. Quindi, quando arrivano nuovi dati di mercato per l’S&P500, possono essere inseriti nel modello (come predittore) per generare una risposta prevista per il prezzo di domani. Questo può costituire la base di una strategia di trading.

Nel secondo caso (inferenza) si può valutare la forza della relazione tra la risposta e ciascun predittore al fine di determinare il sottoinsieme di predittori che hanno un effetto sulla risposta. Questo approccio è utile quando si vuole capire le cause che fanno variare la risposta, come in una ricerca di marketing o in una sperimentazione clinica. L’inferenza è spesso meno utile per coloro che eseguono il trading algoritmico, poiché la qualità della predizione è fondamentalmente più importante della relazione sottostante. Detto questo, non ci si deve basare esclusivamente sull’approccio “black-box” a causa dell’overfitting del rumore nei dati.

Altre tecniche includono la regressione logistica, progettata per prevedere una risposta categorizzata (come “UP”, “DOWN”, “FLAT”) in contrasto con una risposta continua (come il prezzo di un’azione). Questo tecnicamente lo rende uno strumento di classificazione (vedi sotto), ma di solito è raggruppato sotto la bandiera della regressione. Una procedura statistica generale nota come stima di massima verosimiglianza (MLE) viene utilizzata per stimare i valori dei parametri di una regressione logistica.

Classificazione

La classificazione comprende tecniche di apprendimento automatico supervisionate che mirano a classificare un’osservazione (simile a un predittore) in un insieme di categorie predefinite, in base alle caratteristiche associate all’osservazione. Queste categorie possono essere non ordinate (ad es. “rosso”, “giallo”, “blu”) o ordinate (ad es. “basso medio alto”). In quest’ultimo caso tali gruppi sono noti come ordinali.
Gli algoritmi di classificazione – i classificatori – sono ampiamente usati nella finanza quantitativa, specialmente nel campo della previsione della direzione del mercato, quindi è necessario studiare approfonditamente i classificatori.

I classificatori possono essere utilizzati nel trading algoritmico per prevedere se una determinata serie temporale avrà rendimenti positivi o negativi nei successivi periodi temporali (sconosciuti). Questo approccio è simile alla regressione, tranne per il fatto che non viene previsto il valore effettivo delle serie temporali, mentre si prevede la sua direzione. Ancora una volta siamo in grado di utilizzare predittori continui, ad esempio i precedenti prezzi di mercato, come osservazioni. Considereremo i classificatori sia lineari che non lineari, tra cui la regressione logistica, l’analisi discriminante lineare / quadratica, le macchine SVM (Support Vector Machines) e le reti neurali artificiali (ANN). Si noti che alcuni dei metodi precedenti possono essere effettivamente utilizzati anche con la regressione.

Modelli di Serie Temporali

Un componente chiave del trading algoritmico è l’elaborazione e la previsione delle serie temporali finanziarie. Il nostro obiettivo è generalmente quello di prevedere i valori futuri delle serie temporali basate su valori precedenti o fattori esterni. Pertanto la modellizzazione delle serie temporali può essere vista come un sottoinsieme misto di regressione e classificazione. I modelli delle serie temporali differiscono dai modelli non temporali perché i modelli fanno un uso deliberato dell’ordine temporale della serie. Pertanto, i predittori sono spesso basati su valori passati o attuali, mentre le risposte sono spesso i valori futuri da prevedere.

Esiste una vasta letteratura su diversi modelli di serie temporali. Ci sono due ampie famiglie di modelli di serie temporali che ci interessano nel trading algoritmico. Il primo set è la famiglia di modelli autoregressione lineare integrata a media mobile (ARIMA), che vengono utilizzati per modellare le variazioni in valore assoluto di una serie storica. L’altra famiglia di serie temporali sono i modelli autoregressivi a eteroschedasticità condizionata (ARCH), che sono usati per modellare la varianza (cioè la volatilità) delle serie temporali nel tempo. I modelli ARCH utilizzano i valori precedenti (volatilità) delle serie temporali per prevedere i valori futuri (volatilità). Ciò è in contrasto con i modelli di volatilità stocastica, che utilizzano più di una serie temporale stocastica (cioè equazioni differenziali stocastiche multiple) per modellare la volatilità.

Tutte le serie storiche dei prezzi non elaborati sono discrete in quanto contengono valori finiti. Nel campo della finanza quantitativa è comune studiare modelli di serie temporali continue. In particolare, il famoso Geometric Brownian Motion, il modello Heston Stochastic Volatility e il modello Ornstein-Uhlenbeck rappresentano tutte serie temporali continue con diverse forme di comportamento stocastico. Utilizzeremo questi modelli di serie temporali nei prossimi articoli per tentare di caratterizzare il comportamento delle serie temporali finanziarie al fine di sfruttare le loro proprietà per creare pratiche strategie di trading.

Guida introduttiva al Machine Learning Statistico

Nel trading moderno il machine learning è diventato una componente estremamente importante nel complesso toolkit di un trader quantitativo di trading. E’ necessario quindi esplorare questo argomento a livello concettuale partendo dai principi base.

Questo articolo è strutturato per darti un’idea dei formalismi matematici alla base dell’apprendimento statistico, mentre gli articoli successivi descrivono esattamente come tale approccio può essere applicato a problemi di finanza quantitativa, ad esempio per progettare una strategia di trading algoritmico.

Cosa è l'Apprendimento Statistico

Prima di discutere gli aspetti teorici dell’apprendimento statistico, è opportuno considerare scenario di finanza quantitativa dove si possono applicare tali tecniche. Si consideri un fondo d’investimento che desidera effettuare previsioni a lungo termine dell’indice azionario S&P500. Il fondo è riuscito a raccogliere una notevole quantità di dati fondamentali relativi alle società che costituiscono l’indice. I dati fondamentali includono, ad esempio, il rapporto prezzo-utili o il valore contabile. Come il fondo può utilizzare questi dati per fare previsioni sull’indice al fine di creare uno strumento di trading? L’apprendimento statistico fornisce un approccio a questo problema.

In un senso più quantitativo stiamo tentando di modellare il comportamento di un risultato o di una risposta sulla base di un insieme di predittori o caratteristiche che presuppongono una relazione tra i due. Nell’esempio precedente il valore dell’indice di mercato azionario è la risposta e i dati fondamentali associati alle aziende che lo compongono sono i predittori.

Questo può essere formalizzato considerando una risposta Y con p caratteristiche diverse x1, x2, …, xp. Se utilizziamo la notazione vettoriale, possiamo definire X = (x1, x2, …, xp), che è un vettore di lunghezza p. Quindi il modello della nostra relazione è dato da:

\(\begin{eqnarray}
Y = f(X) + \epsilon
\end{eqnarray}\)

Dove f è una funzione sconosciuta dei predittori e ε rappresenta un termine di errore o rumore. È importante sottolineare che ε non dipende dai predittori e ha una media nulla. Questo termine è incluso per rappresentare informazioni che non sono considerate all’interno di f. Quindi tornando all’esempio dell’indice del mercato azionario si può dire che Y rappresenta il valore del S&P500 mentre le componenti xi rappresentano i valori dei singoli fattori fondamentali.

L’obiettivo dell’apprendimento statistico è stimare la forma di f sulla base dei dati osservati e valutare l’accuratezza di tali stime.

Predizione ed Inferenza

Ci sono due processi generali che sono di interesse nell’apprendimento statistico – la predizione e l’inferenza. La predizione si riferisce allo scenario in cui è semplice ottenere informazioni sulle caratteristiche / predittori ma è difficile (o impossibile) ottenere le risposte.

Predizione

La predizione riguarda la previsione di una risposta Y basata su un predittore recentemente osservato, X. Supponendo che sia stata determinata un modello di relazione, è semplice prevedere la risposta utilizzando una stima di f per produrre una stima per la risposta:

\(\begin{eqnarray}\hat{Y} = \hat{f}(X)\end{eqnarray}\)

L’esatta forma della funzione f() è spesso irrilevante nello scenario di predizione se si ipotizza che che le risposte stimate siano vicine alle risposte reali e quindi f() sia precisa nelle sue previsioni. Diverse stime di f produrranno diverse stime di Y. L’errore associato ad avere una scarsa stime \(\hat{f}\) di f è chiamato errore reducible. Si noti che c’è sempre un errore non reducible perché la specifica originale del problema includeva il termine di errore ε. Questo termine di errore incapsula i fattori non misurati che possono influenzare la risposta Y. L’approccio adottato è di provare a minimizzare l’errore reducible con la consapevolezza che ci sarà sempre un limite superiore di accuratezza basato sull’errore ε.

Inferenza

L’inferenza è relativa allo scenario in cui è necessario comprendere la relazione tra X e Y e quindi deve essere determinata la sua forma esatta di f(). Si potrebbe desiderare di identificare i principali predittori o determinare la relazione tra i singoli predittori e la risposta. Si potrebbe anche verificare se la relazione sia lineare o non lineare. La prima indica che il modello è probabilmente più interpretabile ma a scapito della prevedibilità, potenzialmente peggiore. Quest’ultimo fornisce modelli generalmente più predittivi ma a volte meno interpretabili. Quindi esiste spesso un compromesso tra prevedibilità e interpretabilità.

Su DataTrading siamo generalmente meno interessati ai modelli di inferenza poiché la forma effettiva di f non è importante quanto la sua capacità di fare previsioni accurate. Molti degli articoli sul trading in questo sito sono basati sul modello predittivo. Nella prossima sezione si descrive come costruire una stima \(\hat{f}\) di f.

Modelli Parametrici e Non Parametrici

In una situazione di apprendimento statistico è spesso possibile costruire un insieme di tuple di predittori e risposte della forma {(X1, Y1), (X2, Y2), …, (Xn, Yn)}, dove Xi si riferisce al vettore del predittore i-esimo e non al componente i-esima di un particolare vettore predittore (che è indicato con xi).

Questo specifico set di dati è noto come dati di addestramento in quanto verrà utilizzato per addestrare un particolare metodo di apprendimento statistico su come generare \(\hat{f}\). Per stimare effettivamente f dobbiamo trovare una \(\hat{f}\) che fornisca una ragionevole approssimazione per una particolare Y sotto un particolare predittore X. Esistono due ampie categorie di modelli statistici che ci consentono di raggiungere questo obiettivo. Sono conosciuti come modelli parametrici e non parametrici.

Modelli Parametrici

La caratteristica dei modelli parametrici è la necessità di specificare o ipotizzare una forma per f. Questa è una modellazione. La prima scelta consiste nel voler considerare un modello lineare o non lineare. Consideriamo il caso più semplice di un modello lineare. Tale modello riduce il problema dalla stima di una funzione sconosciuta di dimensione p a quella di stimare un vettore di coefficiente \(\beta=(\beta_0, \beta_1, … , \beta_p)\) di lunghezza p+1. Si considera p+1, e non p, perché i modelli lineari possono essere affini, ovvero possono non attraversare l’origine quando si crea una “line of best fit“, è necessario un coefficiente per specificare l’intersezione. In un modello lineare unidimensionale (regressione), questo coefficiente viene spesso rappresentato come α. Per il nostro modello lineare multidimensionale, dove ci sono p predittori, usiamo la notazione β0 per rappresentare la nostra intersezione tra X e Y e quindi ci sono componenti p+1 nella stima \(\hat{\beta}\) di β. Ora che abbiamo specificato una forma (lineare) di f, dobbiamo addestrarla. L'”Addestramento” in questo caso significa trovare una stima per β tale che:

\(\begin{eqnarray} Y \approx \hat{\beta}^T X = \beta_0 + \beta_1 x_1 + … + \beta_p x_p \end{eqnarray}\)

Dove il vettore \(X=(1,x_1,x_2,…,x_p)\) contiene un componente aggiuntivo unitario per avere un vettore a p+1 dimensioni. Nel modello lineare possiamo usare un algoritmo come i minimi quadrati ordinari (OLS) per determinare i coefficienti, ma sono disponibili anche altri metodi. È molto più semplice stimare β che far adattare una f (potenzialmente non lineare). Tuttavia, scegliendo un approccio parametrico lineare, è improbabile che la nostra stima possa replicare la vera forma di f. Questo può portare a stime poco veritiere perché il modello non è abbastanza flessibile. Un possibile rimedio è considerare l’aggiunta di ulteriori parametri, scegliendo forme alternative per \(\hat{f}\). Sfortunatamente se il modello diventa troppo flessibile può portare a una situazione molto pericolosa nota come overfitting, che sarà oggetto di numerosi futuri articoli. In sostanza il modello segue troppo da vicino il rumore e non il segnale!

Modelli non Parametrici

L’approccio alternativo consiste nel considerare una forma non parametrica di\(\hat{f}\). I modelli non parametrici possono potenzialmente adattarsi a una gamma più ampia di possibili forme per f e sono quindi più flessibili. Sfortunatamente, i modelli non parametrici risentono della necessità di disporre di una vasta quantità di dati osservati, spesso molto più che nei modelli parametrici. Inoltre, i metodi non parametrici sono anche soggetti ad overfitting se non trattati attentamente.

I modelli non parametrici possono sembrare una scelta naturale per i modelli di trading quantitativa in quanto vi è apparentemente un’abbondanza di dati (storici) su cui applicare i modelli. Tuttavia, i metodi non sono sempre ottimali. Nonostante la maggiore flessibilità è molto utile per modellare le non-linearità dei dati finanziari, è molto facile l’overfit dei dati a causa dello scadente rapporto segnale / rumore che si trova nelle serie temporali finanziarie.

Si preferisce quindi una “via di mezzo” nel considerare i modelli con un certo grado di flessibilità. Discuteremo di questi problemi negli articoli relativi all’ottimizzazione.

Apprendimento supervisionato e non supervisionato

Nel machine learning statistico viene spesso fatta una distinzione tra metodi supervisionati e non supervisionati. Le strategie descritte su Data Trading saranno quasi esclusivamente basate su tecniche supervisionate, ma anche le tecniche senza supervisione sono certamente applicabili ai mercati finanziari.

Un modello supervisionato richiede che per ogni vettore predittore Xi vi sia una risposta associata Yi. La “supervisione” della procedura si verifica quando il modello di f viene addestrato o adattato a questi dati particolari. Ad esempio, quando si crea un modello di regressione lineare, si utilizza l’algoritmo OLS per addestrarlo, producendo infine una stima \(\hat{\beta}\) per il vettore dei coefficienti di regressione, β.

In un modello non supervisionato non esiste una corrispondente risposta Yi per uno specifico predittore Xi. Quindi non c’è nulla per “supervisionare” l’allenamento del modello. Questo scenario è chiaramente molto più difficile affinché un algoritmo possa produrre risultati poiché non esiste alcuna forma di “funzione fitness” con cui valutare l’accuratezza. Nonostante questa criticità, le tecniche senza supervisione sono estremamente potenti. Sono particolarmente utili nel dominio del clustering.

Un modello di cluster parametrizzato, quando viene fornito con un parametro che specifica il numero di cluster da identificare, può spesso discernere relazioni impreviste all’interno dei dati che altrimenti non sarebbero stati facilmente determinati. Tali modelli generalmente rientrano nel campo dell’analisi aziendale e dell’ottimizzazione del marketing al consumo, ma hanno usi anche nell’ambito finanziario, in particolare per quanto riguarda la valutazione del clustering, ad esempio nell’ambito della volatilità.

Nel prossimo articolo considereremo diverse categorie di tecniche di apprendimento automatico e come valutare la qualità di un modello.