Il concetto di DataView in ML.NET
Uno dei problemi pressoché insormontabili che si hanno facendo machine learning in Python è il caricamento di grandi quantità di dati in memoria per elaborazioni da farsi necessariamente riga per riga. Vi sono due aspetti collegati tra loro che rendono lo scenario arduo da gestire.
- Gli oggettivi limiti di gestione della memoria da parte dell’interprete Python in uso nell’ambiente di riferimento (Jupyter, PyCharm, etc.).
- I limiti sono effettivamente bassini (ben sotto 1 GB di dati) tanto da costringere a fare acrobazie per gestire tali quantità di dati e arrivare comunque a una conclusione soddisfacente
Il fatto è che in Python non esiste un modo efficace e semplice per lavorare grandi quantità di dati senza scontrarsi con i limiti fisici della memoria installata sul computer e del modo in cui un uso eccessivo di questa impatti il resto delle applicazioni e dei servizi in esecuzione sulla stessa macchina.
In ML.NET si è tentato di offrire una soluzione con il concetto di DataView. Per capire come funziona torniamo indietro a XML.
La lezione di XML
Fin dai tempi gloriosi di XML— due buoni decenni fa — sappiamo che esiste una profonda differenza tra costruire un modello dati in memoria — allora si chiamava XML Document Object Model — e scorrere un documento XML nodo per nodo secondo la logica del cursore forward-only.
Nel primo caso la costruzione del modello richiede tempo e spazio; ma una volta fatto, l’accesso è — più o meno — istantaneo, salvo che la dimensione sia davvero grossa in rapporto alla memoria fisica. Nel secondo caso, la memoria è occupata solo per il battito d’ali di leggere i dati di un nodo o, al più, un livello di nodi. Il problema è che non resta traccia di quello che è stato letto prima, che non può essere riletto. Restano però salvati da qualche altra parte i risultati di calcoli ed elaborazioni.
Esiste una profonda differenza tra l’uso tipico di un file XML e l’uso tipico di un file dati (spesso CSV) in machine learning. Spesso era necessario tenere in memoria il dato XML per accedervi in ogni momento e procedere con il resto dell’applicazione; in questo caso, un XML DOM aveva il suo perché. In machine learning solitamente si ha bisogno di lavorare un record alla volta, più o meno come fanno i cursori forward–only delle librerie XML. Solo che molte librerie di machine learning richiedono il caricamento integrale in memoria dei dati, come richiedeva a suo tempo l’XML DOM e, se la dimensione dei dati è ampia, sorgono problemi.
In ML.NET il DataView è un concetto paragonabile al cursore forward–only di XML pensato specificamente per rendere gestibile l’elaborazione di grandi quantità di dati con relativamente poca memoria.
Come funziona un DataView
In ML.NET, un DataView è un oggetto che implementa l’interfaccia IDataView, definita come segue:
public interface IDataView { bool CanShuffle { get; } DataViewSchema Schema { get; } long? GetRowCount (); DataViewRowCursor GetRowCursor (Column[] columns, Random rand); DataViewRowCursor[] GetRowCursorSet (Column[] columns, int n, Random rand); }
Com’è facile intuire dai nomi dei metodi, l’interfaccia è fatta per favorire l’elaborazione di grandi dataset su singoli nodi computazionali. Non è fatta, invece, per dataset distribuiti ed elaborazione parallela. L’intera libreria ML.NET è costruita attorno al DataView. Ciò significa in ultima analisi che qualsiasi componente della pipeline ML.NET è fatto per lavorare in modalità cursore forward-only tramite i metodi dell’interfaccia.
Vista vs tabella
In qualche modo anche la scelta del nome, con dentro l’idea di una vista invece che di una tabella, è indice di un comportamento improntato alla massima flessibilità e capacità di virtualizzazione dei dati. In particolare, e anche con riferimento alla teoria dei database, una view è diversa da una table per i seguenti punti:
- le tabelle sono qualcosa di fisico mentre le viste sono finestre virtuali poste su di esse;
- le viste non contengono dati, ma si limitano a filtrare ciò che le tabelle contengono. In questo contesto, una vista è immutabile e una tabella è mutabile.
- le viste sono componibili nel senso che nuove viste possono essere create per trasformazione da altre viste e tabelle.
Tutte queste caratteristiche hanno un impatto diretto sulle qualità del software costruito su di esse. Per esempio, l’immutabilità della view è un fattore che abilita concorrenza e thread-safety, mentre la natura virtuale e non fisica della view minimizza I/O, gestione della memoria e calcolo.
In sostanza, il vantaggio del DataView è che i dati da file — o altre sorgenti come database — sono letti, la memoria relativa allocata e l’elaborazione eseguita solo ed unicamente quando è strettamente necessario e non ulteriormente differibile.
Uso del DataView in pipeline applicative
In una pipeline ML.NET, il DataView è un oggetto inizialmente creato per contenere dati e successivamente trasformato applicandovi modifiche rese necessarie dagli scopi dell’algoritmo da eseguire. Ove si usasse un DataFrame Python i dati verrebbero effettivamente caricati in memoria al momento della creazione del link con la sorgente dati. In ML.NET, invece, viene solo creato un grafo che descrive la sequenza di operazioni che avrà luogo su ciascun elemento pescato dal cursore. L’occupazione di memoria non supera mai la dimensione del singolo record di dati.
Chi avesse familiarità con il modello LINQ nel Microsoft .NET Framework potrebbe aver già notato la profonda similitudine. In entrambi i casi viene costruita un’espressione logica che poi viene attuata da opportuni metodi. Nel caso di LINQ, il grafo costruito rappresenta una query modello SQL e i metodi attuatori sono i vari First e ToList. In ML.NET, il grafo rappresenta la trasformazione dei dati sorgente in un dataset che sia di input per l’algoritmo prescelto. Il metodo attuatore è il metodo che lancia il training sulla pipeline.
Conclusioni
L’uso di Python per machine learning ha una serie di indiscussi vantaggi primo fra tutti la semplicità di lavoro e la disponibilità di ambienti ad hoc, come Jupyter e Azure Notebooks. Al tempo stesso vi sono una serie di problemi, primo fra tutti la gestione della memoria, che non sono risolvibili in Python a meno di passare a importare codice C dentro script Python!
Per grandi dimensioni di dati — e tutti i progetti di una qualche rilevanza hanno grandi quantità di dati almeno dell’ordine dei gigabyte — l’uso di codice Python è problematico. ML.NET si pone come alternativa per la capacità di leggere in forma di cursore e per la connessione diretta a sorgenti dati come database relazionali.