Don't try this at work: Machine Learning efficace, ma contrario alle "buone norme"
Suggerimenti pratici, anti-intuitivi ma funzionanti... e qualche riflessione sull'approfondire realmente la conoscenza del ML
Intro
Ogni tanto mi capita di leggere post, articoli o cheatsheet su come pre-trattare i dati prima di alimentare un modello di machine learning.
E si sa: la pulizia e l’encoding dei dati sono i passaggi più importanti (e più tricky) quando si usano algoritmi avanzati nel mondo reale.
Senza girarci attorno: nel 99% dei casi mi sembrano usciti da un manuale di data mining degli anni ‘90, con un’abbondanza di buone norme molto ragionevoli, ma superate dalla prova dei fatti. Il problema è uno: in molte guide, si considerano le fasi di data cleaning e feature engineering come strettamente antecedenti allo sviluppo del modello di ML in sé.
La realtà è un po’ diversa. Diverse famiglie di algoritmi richiedono diversi tipi di accorgimenti. Una serie di considerazioni che erano validissime sulle classiche regressioni lineari o logistiche perdono completamente senso con algoritmi diversi.
Qualche punto fisso
Mi focalizzerò sui dati tabulari, che sono quelli su cui ho maggiore esperienza e che rappresentano l’oggetto del lavoro della maggior parte dei data scientist.
Ormai è stato dimostrato in lungo e in largo1 che su questo tipo di dati i modelli ad albero avanzati (da XGBoost, presentato nel 2016, a Lightgbm e Catboost) hanno una flessibilità e una capacità di modellizzare fenomeni complessi ben superiore ai modelli di deep learning (che però hanno dalla loro un nome molto più cool).
Oltre alla pura performance, sono tanti gli aspetti interessanti di algoritmi come XGBoost: penso in primis al livello di explainability (che grazie a librerie come SHAP (2018) è diventato molto elevato), ma anche alla facilità di tuning e maintenance.
Per questo mi concentrerò su questa famiglia di algoritmi: se qualcuno vuol capire di più sul funzionamento sottostante, su cosiddetti internals, ne ho parlato due anni fa al re:Invent 2020, in una sessione a due con Denis Batalov, Worldwide Technical Leader in Machine Learning e Artificial Intelligence di AWS (e amazonian da 17 anni).
Variabili categoriche
Inizio subito con uno dei più classici problemi del ML: come gestire le variabili categoriche. Parlo di variabili non numeriche e che non hanno nativamente un ordinamento2.
Prendiamo ad esempio una variabile che assuma valori “Roma”, “Milano”, “Torino”, “Napoli”.
Soluzione canonica
L’idea da manuale è quella di trasformare la variabile originaria in 4 variabili binarie (“è Roma”: 0/1, “è Milano”: 0/1, “è Torino”: 0/1, “è Napoli”: 0/1). In alcuni casi può essere opportuno o necessario rimuovere una delle variabili, ottenendo questa codifica:
Roma: 1-0-0
Milano: 0-1-0
Torino: 0-0-1
Napoli: 0-0-0
È quello che si chiama one hot encoding.
In realtà è meglio fare…
Sono due le alternative:
Rendersi conto che librerie come XGBoost supportano nativamente le categoriche (hint: Catboost si chiama così proprio perché ha introdotto il supporto per le categoriche)
Optare per una soluzione apparentemente mostruosa: codificare le 4 città come 1, 2, 3, 4. La spiegazione è intuitiva: algoritmi greedy come quelli di cui stiamo parlando sono in grado di segmentare lo spazio in maniera molto efficace e ritornando più volte sulla stessa variabile (nello stesso albero, o in alberi diversi).
Questo secondo punto può essere (molto) difficile da digerire al primo tentativo, ma fermandosi un attimo a pensare, dovrebbe risultare chiaro: sia per motivi strettamente algoritmici, sia per temi di efficienza computazionale3.
Per chi è più rigoroso, consiglio fortemente quest’ottimo articolo che spiega nel dettaglio, con esempi sul campo, le ottime performance che porta un approccio naive di questo tipo, rispetto ad altre opzioni.
Ricampionamento
Altro grande classico del machine learning: come gestire i fenomeni rari, i cosiddetti problemi molto sbilanciati (che rappresentano una buona fetta dei problemi di classificazione).
Pensiamo alla probabilità che un cliente abbandoni una compagnia (telefonica, assicurativa, etc.) in una specifica settimana: sicuramente è una percentuale molto bassa del totale dei clienti.
Soluzione canonica
Tipicamente parliamo di undersampling (ridurre il numero dei casi maggioritari, nell’esempio sopra i clienti che rimangono con la compagnia) o di oversampling (creare artificialmente dei casi simili a quei pochi che abbandonano la compagnia, ad esempio con algoritmi come SMOTE).
In realtà è meglio fare…
Anche qui possiamo optare per soluzioni alternative che sfruttano le feature degli algoritmi più avanzati:
Ignorare il problema: sì, gli alberi sono abbastanza capaci “da soli” di intercettare casi rari
Giocare con la parametrizzazione nativa dell’algoritmo4
Usare loss functions ad hoc, come la focal loss
Tutte opzioni più semplici5 (e nella mia esperienza, anche più efficaci) di quanto la teoria classica insegni.
Normalizzazione, standardizzazione e affini
E cosa fare quando le variabili non sono indipendenti ed identicamente distribuite? Ossia… sempre, nel mondo reale.
Soluzione canonica
Esiste una pletora di trasformazioni che hanno l’obiettivo di superare questo problema, appartenenti a due macro famiglie:
Trasformazioni sulla singola variabile, al fine di alterarne la distribuzione
Trasformazioni dello spazio delle variabili (da algoritmi di feature selection per rimuovere tout court alcune variabili, a trasformazioni complete come la PCA)
In realtà è meglio fare…
Lavorando con algoritmi ad albero, le trasformazioni di cui sopra sono spesso irrilevanti o dannose (come la PCA).
Mi limito ad un esempio: pensiamo ad una qualsiasi trasformazione monotòna (ossia che non cambia l’ordine di valori, come una trasformazione logaritmica ad una variabile). Visto il funzionamento di qualsiasi algoritmo ad albero, che appunto segmenta una variabile sulla base del suo ordinamento, è naturale che questa trasformazione sia inutile.
Valori mancanti
Chiudo con il mondo dei missing values.
Soluzione canonica
I dati mancanti vanno imputati: “e non bisogna farlo con la media, ma con la mediana”!
O magari con tanti metodi più ricercati.
In realtà è meglio fare…
Nel mondo reale, il fatto che un valore manchi è un’informazione. Non va imputato in nessun caso in cui l’algoritmo usato supporti nativamente i valori mancanti.
Altrimenti, che si parli di media o mediana… si va a perdere un’informazione utile!
Conclusioni
Ho fatto una rapida carrellata di approcci largamente insegnati, largamente usati… e largamente inutili o controproducenti, nel moderno machine learning. Senza l’obiettivo di essere esaustivo (servirebbe una lezione di un paio d’ore), ma con l’intenzione di far sorgere qualche dubbio.
Il punto è uno: capire a fondo quello che si sta facendo, mettere in dubbio anche alcuni capisaldi e non accontentarsi di una conoscenza superficiale (come spiega John Carmack in un talk di cui ho già parlato).
La tentazione è forte: tra librerie favolose, approcci no-code, AI democratization (e mettiamoci pure ChatGPT, già che sono l’unico a non averne ancora parlato), lavorare con dati ed algoritmi senza avere pienamente il polso di quello che si sta facendo è diventato sempre più facile.
Ho scherzato nel titolo di questo articolo, consigliando di non usare al lavoro questi trick. Rettifico parzialmente: usateli, se li padroneggiate e siete in grado di spiegarli realmente.
Anche perché con ChatGPT e affini il rischio è alto, ma l’opportunità lo è ancora di più: chiunque sarà in grado di fare qualcosa con i dati, ma alzando l’asticella rimarranno solo quei data scientist in grado di porsi le domande giuste ed acquisire una conoscenza approfondita. Questo è il deep learning che ci serve!
Ad esempio nello studio “Tabular Data: Deep Learning is Not All You Need” reperibile qui: https://arxiv.org/abs/2106.03253
Quindi non sono variabili che possono assumere valori come “in forte disaccordo, in disaccordo, neutro, d’accordo, molto d’accordo” che tipicamente vengono codificate con numeri da 1 a 5.
Nessuno vuole ampliare enormemente lo spazio delle varibili (se ho due variabili con 10 possibili valori, non vorrei creare uno spazio delle variabili con 20 - o 18 - colonne). Potrei ottimizzare qualcosa usando una rappresentazione a matrice sparsa (e non densa), ma se ho un mix di variabili di varia natura… può diventare difficile.
Tramite parametro scale_pos_weight
Forse sul semplici si può discutere: in realtà si potrebbe parlare ampiamente di ciascuna opzione. Ma dal punto di vista di un utilizzatore, sono opzioni semplici da implementare.
Bellissimo articolo, Alberto! Mi sono appena iscritto alla newsletter e spero di leggerne tanti altri
Come sempre chiaro e utilissimo, grazie mille Alberto, colgo l'occasione per augurarti un sereno Natale ed un grande 2023
Michele Gnecchi