
Ripartiamo dalle conclusioni dell’ articolo precedente, per continuare a parlare delle feature, ossia le proprietà che passiamo come input al nostro modello e che poi useremo per costruire una predizione.
In tutti i corsi di Machine Learning la predizione è molto chiara: il prezzo di un appartamento, la mancia che riceverà il tassista a New York, etc. Ma nel nostro caso (e più in generale) sappiamo cosa vogliamo estrarre dai nostri dati? E le nostre sono buone feature?
Una buona feature deve avere un valore noto quando vogliamo fare una predizione: non possiamo creare un modello con dati completi e invece calcolare una predizione con informazioni mancanti.
Torniamo ai nostri dati:

Supponiamo di voler fare una predizione su una partita che un giocatore X giocherà domani. Le uniche feature che sono note oggi sono: l’età del giocatore, il suo ruolo, l’anno, il mese e il giorno della partita, e l’appartenenza alla squadra che gioca in casa.
Tutto il resto è sconosciuto: quanti minuti giocherà, quanti gol e autogol segnerà, se verrà ammonito o espulso, se la squadra di casa vincerà, etc. In gergo, queste informazioni che vogliamo predire sono dette label.
Nello scorso articolo, non abbiamo inserito nelle feature del nostro dataset l’identità del calciatore e il nome delle squadre coinvolte nella partita. La probabilità che X segni un goal o che venga espulso dipende dalla sua storia oppure è un numero assoluto che non è connesso alla sua identità?
Altra domanda: il fatto che X giochi per la squadra Y e non più per la squadra Z altera questi numeri? Questi interrogativi sono troppo complessi per poter dare una risposta anche dopo anni di studi di Statistica. Ho deciso comunque di recuperare nel nostro dataset le identità per mostrare un’altra tecnica classica nella preparazione dei dati.
Supponiamo, per semplicità, di avere dati appartenenti a soli 5 giocatori, la cui identità denotiamo con un numero compreso tra 1 e 5. Ad ogni riga di dati, invece di indicare l’ID del giocatore, associamo un array di 5 elementi (ossia il numero totale di giocatori) che avrà tutti gli elementi uguali a 0 escluso quello uguale all’ID. Si tratta, ancora una volta, di un esempio di one-hot encoding, tecnica a cui ho accennato nello scorso articolo e che ottimizza i tempi di esecuzione e l’efficienza degli algoritmi di Machine Learning.

Per creare una sparse column (altro nome usato per indicare tali colonne) occorre pre-processare i dati per estrarre tutte le chiavi e creare con esse un dizionario. Questo dizionario di chiavi deve essere disponibile al momento della predizione e non deve mutare rispetto a quello usato nella fase di training del nostro modello.
E se aggiungessi, rispetto ai dati originari, un nuovo calciatore per il quale ottenere una predizione? In tal caso, non disponiamo della sua chiave. Una tecnica classica è quella di aggiungere preventivamente una chiave per un giocatore senza identità (o una per ruolo) e magari associare ad essa i valori medi delle altre feature. Con l’accumularsi di nuovi dati, potremo creare nuovamente il dizionario delle chiavi e usarlo di nuovo per il training del nostro modello.
Come possiamo gestire una sparse column? Soprattutto se disponiamo di dati relativi a migliaia di calciatori? È ovvio che solo qualche libreria può aiutarci in questa impresa.
ML.NET offre la possibilità di aggiungere funzionalità di Machine Learning alle applicazioni .NET in scenari online o offline.
Dal portale di apprendimento di Microsoft .NET ( https://dotnet.microsoft.com/learn ) è possibile accedere alla sezione dedicata al Machine Learning.

Su Linux e macOS è possibile installare la CLI (Command Line Interface) di ML.NET mentre su Windows è disponibile l’estensione ML.NET Model Builder per Visual Studio.


Partendo da una Console Application .NET Core, possiamo accedere al menu contestuale chiamato Machine Learning come mostrato nell’immagine seguente:

Tale menu avvia un wizard che ci porta a scegliere uno tra una serie di scenari tipici:

Lo scenario Custom è quello che offre maggiore flessibilità nel nostro caso. Possiamo caricare un file di Input, indicare quale sarà la label per le predizioni e scegliere le feature tra quelle disponibili

Quando, però, si parte da zero con un progetto, è difficile che questo approccio possa funzionare subito. Abbiamo bisogno di testare passo dopo passo la nostra procedura. Installiamo quindi nella nostra soluzione il pacchetto NuGet Microsoft.ML

Partendo dal file .csv contenente tutti i dati e dai nomi utilizzati nella riga header, andiamo a definire le due seguenti classi nel nostro codice:
public class PlayerData
{
[LoadColumn(0)]
public int Id;
[LoadColumn(1)]
public float Age;
[LoadColumn(5)]
public string AwayTeam;
[LoadColumn(6)]
public float Year;
[LoadColumn(7)]
public float Month;
[LoadColumn(8)]
public float Day;
[LoadColumn(10)]
public float Minutes;
[LoadColumn(12)]
public string HomeTeam;
[LoadColumn(18)]
public float IsHomeTeam;
[LoadColumn(19)]
public float IsDefender;
[LoadColumn(20)]
public float IsMidfield;
[LoadColumn(21)]
public float IsForward;
[LoadColumn(22)]
public float IsNoRole;
}
public class PlayerDataPrediction
{
[ColumnName("Minutes")]
public float Minutes;
}
PlayerData è la classe dei dati in input e le sue proprietà corrispondono alle colonne del dataset. L’attributo LoadColumn specifica gli indici delle colonne nel dataset.
PlayerDataPrediction rappresenta la classe contenente le proprietà che vogliamo predirre. Ciascuna di esse è preceduta dall’attributo ColumnName.
Tutte le operazioni in ML.NET iniziano con la creazione di un’istanza della classe MLContext. Concettualmente, è simile alla classe DBContext in Entity Framework: ossia un contesto per l’esecuzione di tutte le attività relative al Machine Learning. Il costruttore prende come parametro un numero intero che fa da seed per il generatore di numeri pseudo-casuali che verrà usato internamente. Utilizzare lo stesso seed nei nostri esperimenti rende il MLContext deterministico: i suoi risultati diventano riproducibili nel corso dei successivi esperimenti.
var mlContext = new MLContext(seed: 0);
Le operazioni che portano alla generazione di un Model sono le seguenti:
- Caricare i Dati
- Estrarre e trasformare i dati
- Eseguire il training del Model
- Salvare il Model
ML.NET usa l’interfaccia IDataView per definire la pipeline di lettura e trasformazione dei dati in ingresso. IDataView può caricare file testuali (.csv) o in tempo reale (ad esempio da un database SQL o da file di log).
IDataView dataView = mlContext.Data.LoadFromTextFile<playerdata>(dataPath, hasHeader: true, separatorChar: ',');
Questa istruzione carica un file .csv che si trova al percorso dataPath, indicando che tale file ha la prima riga contenente un header e che il carattere separatore è la virgola.
La pipeline di trasformazione inizia indicando quale sarà la label tra le colonne del file di dati.
var pipeline = mlContext.Transforms.CopyColumns(outputColumnName: "Label", inputColumnName: "Minutes")
e poi indicando quali siano le colonne categoriche sulle quali utilizzare la tecnica del OneHotEncoding:
.Append(mlContext.Transforms.Categorical.OneHotEncoding(outputColumnName: "AwayTeamEncoded", inputColumnName: "AwayTeam"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding(outputColumnName: "IdEncoded", inputColumnName: "Id"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding(outputColumnName: "HomeTeamEncoded", inputColumnName: "HomeTeam"))
Si procede quindi concatenando tutte le feature:
.Append(mlContext.Transforms.Concatenate("Features",
"IdEncoded", "AwayTeamEncoded", "HomeTeamEncoded",
"Age", "Year", "Month", "Day","IsHomeTeam","IsDefender",
"IsMidfield", "IsForward", "IsNoRole")
e, infine, eseguendo il training del modello scegliendo un algoritmo tra quelli disponibili nella libreria:
.Append(mlContext.Regression.Trainers.Sdca()));
L’algoritmo utilizzato si chiama Stochastic Dual Coordinate Ascend (SDCA). Perché proprio questa scelta? Semplicemente perché al momento è l’unico che non solleva un’eccezione sui dati passati in ingresso. Perché gli altri falliscono? Probabilmente perché i dati non sono stati ancora normalizzati e manipolati correttamente.
Il successo del SDCA non deve portarci a credere che il modello sia utilizzabile per la fase di predizione. Occorre capire quale sia la sua accuratezza.
Il modello viene creato e salvato mediante le seguenti istruzioni:
var model = pipeline.Fit(dataView);
mlContext.Model.Save(model, dataView.Schema, "model.zip");

Tutto troppo semplice per essere vero! Siete curiosi di sapere se riusciremo a predire quanti minuti giocherà un calciatore nella prossima partita, dati tutti i valori delle feature scelte? Troverete la risposta nel prossimo articolo, dove mostreremo anche le tecniche per creare il campione dei dati da usare per valutare le performance del modello.