Creare una pipeline di Machine Learning con AWS Sagemaker – Parte uno: introduzione e set-up

Articolo in lingua originale di Pierce Lamb

O meglio, crare una Pipeline ML riutilizzabile inizializzata da un singolo file config e 5 funcioni definite dall’utente che sia in grado di fare classificazione, sia basata su fine-tuning, sia distributed-first, venga eseguita us AWS Sagemaker, utilizzi Huggingface, Transformers, Accelerate, Datasets & Evaluate, Pytorch, wandb e altro.

Questo post è stato pubblicato originariamente su VISO Trust’s Blog.

Questo è il post introduttivo di una serie suddivisa in tre parti. Le altre parti in lingua originale le trovate qui: Creating a ML Pipeline Part 2: The Data Steps e Creating a ML Pipeline Part 3: Training and Inference.

 

Introduzione

Nel team Data & Machine Learning di VISO Trust, uno dei nostri obiettivi principali è fornire Document Intelligence al nostro team di revisori. Ogni documento che passa attraverso il sistema è soggetto a raccolta, analisi, riformattazione, analisi, reporting e altro ancora. Parte di questa intelligenza consiste nel determinare automaticamente il tipo di documento caricato nel sistema. Sapere che tipo di documento è stato inserito nel sistema ci permette di eseguire analisi specializzate su quel documento.

Il compito di etichettare o classificare un oggetto è un uso tradizionale del machine learning, ma la classificazione di un intero documento – che, per noi, può essere di oltre 300 pagine – è all’avanguardia nella ricerca sul machine learning. Al momento in cui scriviamo, i ricercatori stanno facendo a gara per utilizzare i progressi del Deep Learning e in particolare dei Transformers per classificare i documenti. In effetti, all’inizio di questo lavoro, ho effettuato una ricerca con parole chiave come “Document Classification/Intelligence/Representation” e mi sono imbattuto in quasi 30 diversi articoli che utilizzano il Deep Learning e sono stati pubblicati tra il 2020 e il 2022. Per chi ha familiarità, nomi come LayoutLM/v2/v3, TiLT/LiLT, SelfDoc, StructuralLM, Longformer/Reformer/Performer/Linformer, UDOP e molti altri.

Questo risultato mi ha convinto che provare una moltitudine di questi modelli sarebbe stato un uso migliore del nostro tempo piuttosto che cercare di decidere quale fosse il migliore tra loro. Ho quindi deciso di sceglierne uno e di utilizzare l’esperienza della sua messa a punto come proof-of-concept per costruire una pipeline ML riutilizzabile che il resto del mio team potesse utilizzare. L’obiettivo era ridurre il tempo di esecuzione di un esperimento da settimane a uno o due giorni. Questo ci avrebbe permesso di sperimentare rapidamente molti modelli per decidere quali sono i migliori per il nostro caso d’uso.

Quando ho iniziato questo proof-of-concept (pipeline pre-ML), ci è voluto più di un mese per raccogliere e pulire i dati, preparare il modello, eseguire l’inferenza e far funzionare tutto su Sagemaker utilizzando la distribuzione. Da quando abbiamo creato la ML Pipeline, l’abbiamo usata ripetutamente per sperimentare rapidamente nuovi modelli, riqualificare i modelli esistenti su nuovi dati e confrontare le prestazioni di più modelli. Il tempo necessario per eseguire un nuovo esperimento è in media di mezza giornata o un giorno. Questo ci ha permesso di iterare in modo incredibilmente veloce, portando rapidamente i modelli in produzione nella nostra piattaforma di Document Intelligence.

Quella che segue è una descrizione della pipeline di cui sopra; spero che vi eviti alcune delle insidie di più giorni che ho incontrato nel costruirla.

Setup dell’esperimento ML

Un’importante decisione architettonica presa all’inizio è stata quella di mantenere gli esperimenti isolati e facilmente riproducibili. Ogni volta che si esegue un esperimento, questo ha il suo insieme di dati grezzi, dati codificati, file docker, file di modello, risultati dei test di inferenza, ecc. In questo modo è facile rintracciare un determinato esperimento tra repos/S3/strumenti di metrica e la sua provenienza una volta in produzione. Tuttavia, un trade off degno di nota è che i dati di addestramento vengono copiati separatamente per ogni esperimento; per alcune organizzazioni questo potrebbe essere semplicemente inapplicabile ed è necessaria una soluzione più centralizzata. Detto questo, di seguito viene illustrato il processo di creazione di un esperimento.

Un esperimento viene creato in un repo di esperimenti e legato a un ticket (ad esempio JIRA) come EXP-3333-longformer. Questo nome seguirà l’esperimento in tutti i servizi; per noi, tutto lo storage avviene su S3, quindi nel bucket dell’esperimento gli oggetti saranno salvati sotto la cartella madre EXP-3333-longformer. Inoltre, in wandb (il nostro tracker), il nome del gruppo di primo livello sarà EXP-3333-longformer.

Successivamente, i file stubbed di esempio vengono copiati e modificati in base alle particolarità dell’esperimento. Questo include il file di configurazione e gli stub delle funzioni definite dall’utente menzionati in precedenza. Sono inclusi anche due file docker; un dockerfile rappresenta le dipendenze necessarie per l’esecuzione della pipeline, l’altro rappresenta le dipendenze necessarie per l’esecuzione di 4 diverse fasi su AWS Sagemaker: preparazione dei dati, addestramento o tuning e inferenza. Entrambi i file docker sono stati semplificati estendendo i file docker di base mantenuti nella libreria della pipeline ML; l’intento è quello di includere solo le librerie extra richieste dall’esperimento. Questo segue la convenzione stabilita dai Deep Learning Containers (DLC) di AWS e, infatti, il nostro container sagemaker di base inizia estendendo uno di questi DLC.

C’è un importante trade off in questo caso: usiamo un contenitore monolitico per eseguire tre diverse fasi su Sagemaker. Abbiamo preferito una configurazione più semplice per gli sperimentatori (un unico dockerfile) rispetto alla necessità di creare un contenitore diverso per ogni fase di Sagemaker. Lo svantaggio è che per un determinato passo, il contenitore conterrà probabilmente alcune dipendenze non necessarie che lo renderanno più grande. Vediamo un esempio per chiarire questo punto.

Nel nostro contenitore Sagemaker di base, estendiamo:

FROM 
763104351884.dkr.ecr.us-west-2.amazonaws.com/huggingface-pytorch-training:1.10.2-transformers4.17.0-gpu-py38-cu113-ubuntu20.04

Questo ci dà pytorch 1.10.2 con binding a cuda 11.3, transformers 4.17, python 3.8 e ubuntu, tutti pronti per essere eseguiti sulla GPU. È possibile vedere i DLC disponibili qui. Aggiungiamo quindi sagemaker-training, accelerate, evaluate, datasets e wandb. Ora, quando uno sperimentatore va a estendere questa immagine, deve solo preoccuparsi di tutte le dipendenze extra di cui il suo modello potrebbe avere bisogno. Per esempio, un modello potrebbe dipendere da detectron2, che è una dipendenza improbabile tra gli altri esperimenti. Quindi lo sperimentatore deve solo pensare a estendere il contenitore sagemaker di base e installare detectron2 e non preoccuparsi più delle dipendenze.

Con i contenitori docker di base al loro posto, i file necessari per l’avvio di un esperimento sarebbero i seguenti:

EXP-3333-longformer/
src/
__init__.py
user_defined_funcs.py
build_and_push.sh
run.sh
run.Dockerfile
sagemaker.Dockerfile
settings.ini

In breve questi file sono

  • settings.ini: un singolo fil di configurazione (ignorato da git) che prende tutti i setting per ogni step della pipeline ML (copiata in dockerfiles)
  • sagemaker.Dockerfile: estende il contenitore di addestramento di base discusso precedentemente e aggiunge qualsiasi dipendenza extra-modello. In molti casi il solo contenitore di base sarà sufficiente.
  • run.Dockerfile: estende il contenitore di esecuzione di base discusso precedentemente e aggiunge qualsiasi dipendenza extra-esecuzione che lo sperimentatore necessita. In molti casi il contenitore di base sarà di per sé sufficiente.
  • run.sh: uno script shell che costruisce ed esegue run.Dockerfile
  • build_and_push.sh: uno script shell che costruisce e porta sagemaker.Dockerfile a ECR
  • user_defined_funcs.py: contiene le 5 funzioni definite dall’utente che saranno chiamate dalla pipeline ML a diverse fasi (copiati in dockerfiles). Discuteremo di questi dettagli più avanti.

Questi file rappresentano i requisiti necessari e sufficienti affinché uno sperimentatore possa eseguire un esperimento sulla pipeline ML. Quando discuteremo della pipeline ML, esamineremo questi file in modo più dettagliato. Prima di questa discussione, però, diamo un’occhiata all’interfaccia di S3 e wandb. Supponiamo di aver impostato ed eseguito l’esperimento come mostrato sopra. Le directory risultanti su S3 avranno l’aspetto di:

EXP-3333-longformer/
data/
reconciled_artifacts/
<all raw training data>
prepared_data/
<all encoded training data>
run_1/
tuning/ (or training/)
<all files emitted by training>
inference/
<all statistics gathered from the test set>

Il run_number aumenterà ad ogni esecuzione consecutiva dell’esperimento. Questo numero di esecuzione sarà replicato in wandb e sarà anche anteposto a qualsiasi endpoint distribuito per la produzione, in modo da poter tracciare l’esatta esecuzione dell’esperimento attraverso l’addestramento, la raccolta delle metriche e la produzione. Infine, diamo un’occhiata alla struttura wandb risultante:

Group: EXP-3333-longformer/
Job Type: run_1
tuning (or training)
inference

Spero che, dopo aver imparato a conoscere l’interfaccia dello sperimentatore, sia più facile capire la pipeline stessa.

 

La pipeline ML

La pipeline ML esporrà (in caso) alcune genericità che specifici casi d’uso possono estendere per modificare la pipeline secondo i propri scopi. Poiché è stata sviluppata di recente nel contesto di un solo caso d’uso, la discuteremo in quel contesto; tuttavia, di seguito mostrerò come potrebbe apparire con più casi d’uso:

bin/
question_classification/
doc_classification/
your-use-case-here/
ml_pipeline/
environment/
lib/
test/

Focalizziamo l’attenzione su ml_pipeline

bin/
question_classification/
doc_classification/
your-use-case-here/
ml_pipeline/
environment/
lib/
test/

La cartella environment ospiterà i file per la costruzione dei container di base di cui abbiamo parlato in precedenza, uno per l’esecuzione del framework e uno per il codice che viene eseguito su Sagemaker (preprocessing, training/tuning, inferenza). Questi sono denominati utilizzando le stesse convenzioni di AWS DLCs, quindi è semplice crearne più versioni con dipendenze diverse. Per il resto di questo post ignoreremo la cartella test.

La cartella lib contiene la nostra implementazione della pipeline ML. Esaminiamo di nuovo solo questa cartella.

lib/
config/
config.py
inference/
inference.py
preprocessing/
data_collection.py
data_preparation.py
data_reconciliation.py
training/
train.py
job_utils.py
run_framework.py
user_defined_funcs.py

Incominciamo con run_framework.py che ci darà una visione d’insieme di ciò che sta accadendo. Lo scheletro di run_framework sarà simile a questo:

if __name__ == "__main__":
config = MLPipelineConfig(search_path=BASE_PACKAGE_PATH)
validate_run(config)
# imports one of the ML Pipeline's use case modules, e.g. doc_classification, question_classification etc
data_reconciliation_module = importlib.import_module(f"{config.use_case}.src.preprocessing.data_reconciliation", package=None)

if config.run_reconciliation:
perform_data_reconciliation(data_reconciliation_module, config)

if config.run_preparation:
perform_data_preparation(config)

if config.run_training:
perform_training(config)

if config.run_tuning:
perform_tuning(config)

if config.run_inference:
perform_inference(config)

Il file settings.ini che un utente definisce per un esperimnto verrà copiato nella stessa cartella (BASE_PACKAGE_PATH)  all’interno di ogni contenitore docker e analizzato in un oggetto chiamato MLPipelineConfig(). Nel nostro caso, abbiamo scelto di usare Python Decouple per gestire la configurazione. In questo file di configurazione, le impostazioni iniziali sono: RUN_RECONCILIATION/PREPARATION/TRAINING/TUNING/INFERENCE, in modo che la pipeline sia flessibile per rispondere esattamente alle esigenze dello sperimentatore. Questi valori costituiscono le condizioni di cui sopra.

Si noti la riga importlib. Questa riga ci permette di importare funzioni specifiche per i casi d’uso e di passarle nelle fasi (qui è mostrata solo la riconciliazione dei dati) usando un valore di configurazione impostato dallo sperimentatore per il caso d’uso.

Nel momento in cui il file di configurazione viene analizzato, vogliamo eseguire la validazione per identificare ora configurazioni errate piuttosto che nel mezzo del training. Senza entrare troppo nel dettaglio della fase di validazione, ecco come potrebbe apparire la funzione:

if config.run_training and config.run_tuning:
raise Exception('Cannot run both training and tuning. Set one to False')

if config.run_preparation:
_validate_funcs(['get_dataset_features', 'encode_data'])

if config.run_training or config.run_tuning:
_validate_funcs(['load_model', 'ordered_model_input_keys'])
_validate_run_num(config)

La funzione _validate_funcs assicura che le funzioni con queste definizioni esistano e che non siano definite come pass (cioè che un utente le abbia create e definite). Il file user_defined_funcs.py precedente le definisce semplicemente come pass, quindi l’utente deve sovrascriverle per eseguire un’esecuzione valida. _validate_run_num lancia un’eccezione se il RUN_NUM definito in settings.ini esiste già su s3. In questo modo si evitano le comuni insidie che potrebbero verificarsi dopo un’ora di training.

 

 

Share:

Contenuti
Torna in alto