Allenare, ottimizzare e distribuire efficientemente insiemi personalizzati utilizzando Amazon SageMaker

Allenare, ottimizzare e distribuire insiemi personalizzati con efficienza tramite Amazon SageMaker

L’intelligenza artificiale (AI) è diventata un argomento importante e popolare nella comunità tecnologica. Con l’evoluzione dell’AI, abbiamo visto emergere diversi tipi di modelli di machine learning (ML). Un approccio, noto come modellazione ensemble, sta guadagnando rapidamente consenso tra gli scienziati dei dati e i professionisti. In questo post, discutiamo cosa sono i modelli ensemble e perché il loro uso può essere vantaggioso. Forniamo poi un esempio di come è possibile addestrare, ottimizzare e distribuire i propri ensemble personalizzati utilizzando Amazon SageMaker.

L’apprendimento ensemble si riferisce all’uso di modelli e algoritmi di apprendimento multipli per ottenere previsioni più accurate rispetto a qualsiasi singolo algoritmo di apprendimento individuale. Sono stati dimostrati efficienti in diverse applicazioni e contesti di apprendimento come la sicurezza informatica [1] e il rilevamento delle frodi, il telerilevamento, la previsione dei migliori passi successivi nella decisione finanziaria, la diagnosi medica e persino le attività di visione artificiale e di elaborazione del linguaggio naturale (NLP). Solitamente classifichiamo gli ensemble in base alle tecniche utilizzate per addestrarli, alla loro composizione e al modo in cui combinano le diverse previsioni in un’unica inferenza. Queste categorie includono:

  • Boosting – Addestramento sequenziale di vari modelli deboli, in cui ogni previsione errata dei modelli precedenti nella sequenza viene ponderata maggiormente e utilizzata come input per il modello successivo, creando così un modello più forte. Gli esempi includono AdaBoost, Gradient Boosting e XGBoost.
  • Bagging – Utilizza più modelli per ridurre la varianza di un singolo modello. Gli esempi includono Random Forest e Extra Trees.
  • Stacking (blending) – Spesso utilizza modelli eterogenei, in cui le previsioni di ogni singolo stimatore vengono unite insieme e utilizzate come input per un stimatore finale che gestisce la previsione. Il processo di addestramento di questo stimatore finale spesso utilizza la cross-validazione.

Esistono molti metodi per combinare le previsioni in un’unica previsione prodotta dal modello, ad esempio utilizzando un meta-stimatore come un apprendista lineare, un metodo di voto che utilizza più modelli per effettuare una previsione in base al voto maggioritario per compiti di classificazione o una media degli ensemble per la regressione.

Anche se diverse librerie e framework forniscono implementazioni di modelli ensemble, come XGBoost, CatBoost o il random forest di scikit-learn, in questo post ci concentriamo su come utilizzare i propri modelli e utilizzarli come ensemble di stacking. Tuttavia, anziché utilizzare risorse dedicate per ogni modello (addestramento dedicato e ottimizzazione dei modelli e hosting degli endpoint per ogni modello), addestriamo, ottimizziamo e distribuiamo un ensemble personalizzato (con più modelli) utilizzando un singolo lavoro di addestramento SageMaker e un singolo lavoro di ottimizzazione, e distribuiamo tutto su un singolo endpoint, riducendo così i possibili costi e l’onere operativo.

BYOE: Porta il tuo ensemble

Esistono diverse modalità per addestrare e distribuire modelli ensemble eterogenei con SageMaker: è possibile addestrare ogni modello in un lavoro di addestramento separato e ottimizzare ogni modello singolarmente utilizzando Amazon SageMaker Automatic Model Tuning. Quando si ospitano questi modelli, SageMaker offre varie modalità economiche per ospitare più modelli sulla stessa infrastruttura del tenant. I modelli di distribuzione dettagliati per questo tipo di impostazioni possono essere trovati in Model hosting patterns in Amazon SageMaker, Part 1: Common design patterns for building ML applications on Amazon SageMaker. Questi modelli includono l’utilizzo di più endpoint (per ogni modello addestrato) o un singolo endpoint multi-modello, o addirittura un singolo endpoint multi-container in cui i container possono essere invocati singolarmente o concatenati in una pipeline. Tutte queste soluzioni includono un meta-stimatore (ad esempio in una funzione AWS Lambda) che invoca ogni modello e implementa la funzione di blending o voto.

Tuttavia, l’esecuzione di più lavori di addestramento potrebbe comportare un onere operativo e di costo, specialmente se il tuo ensemble richiede l’addestramento sugli stessi dati. Allo stesso modo, ospitare modelli diversi su endpoint o container separati e combinare i risultati delle previsioni per una maggiore accuratezza richiede invocazioni multiple e quindi introduce ulteriori sforzi di gestione, costi e monitoraggio. Ad esempio, SageMaker supporta modelli ensemble ML utilizzando Triton Inference Server, ma questa soluzione richiede che i modelli o gli ensemble di modelli siano supportati dal backend di Triton. Inoltre, il cliente deve compiere sforzi aggiuntivi per configurare il server Triton e imparare come funzionano i diversi backend di Triton. Pertanto, i clienti preferiscono un modo più diretto per implementare soluzioni in cui è sufficiente inviare l’invocazione una volta all’endpoint e avere la flessibilità di controllare come i risultati vengono aggregati per generare l’output finale.

Panoramica della soluzione

Per affrontare queste problematiche, esaminiamo un esempio di addestramento ensemble utilizzando un singolo lavoro di addestramento, ottimizzando gli iperparametri del modello e distribuendolo utilizzando un singolo container su un endpoint serverless. Utilizziamo due modelli per il nostro ensemble: CatBoost e XGBoost (entrambi ensemble di boosting). Per i nostri dati, utilizziamo il dataset sul diabete [2] della libreria scikit-learn: è composto da 10 caratteristiche (età, sesso, massa corporea, pressione sanguigna e sei misurazioni del siero sanguigno) e il nostro modello prevede la progressione della malattia dopo 1 anno dalle caratteristiche di base raccolte (un modello di regressione).

Il repository completo del codice può essere trovato su GitHub.

Allenare più modelli in un singolo job SageMaker

Per allenare i nostri modelli, utilizziamo i job di allenamento di SageMaker in modalità script. Con la modalità script, è possibile scrivere codice di allenamento personalizzato (e successivamente di inferenza) utilizzando i container del framework SageMaker. I container del framework consentono di utilizzare ambienti preconfigurati gestiti da AWS che includono tutte le configurazioni e i moduli necessari. Per dimostrare come sia possibile personalizzare un container del framework, ad esempio, utilizziamo il container pre-costruito di SKLearn, che non include i pacchetti XGBoost e CatBoost. Ci sono due opzioni per aggiungere questi pacchetti: estendere il container integrato per installare CatBoost e XGBoost (e quindi distribuirli come container personalizzato) o utilizzare la funzionalità di modalità script dei job di allenamento di SageMaker, che consente di fornire un file requirements.txt durante la creazione dell’estimatore di allenamento. Il job di allenamento di SageMaker installa le librerie elencate nel file requirements.txt durante l’esecuzione. In questo modo, non è necessario gestire il proprio repository di immagini Docker e offre maggiore flessibilità nell’esecuzione di script di allenamento che richiedono pacchetti Python aggiuntivi.

Il seguente blocco di codice mostra il codice che utilizziamo per avviare l’allenamento. Il parametro entry_point punta al nostro script di allenamento. Utilizziamo anche due delle funzionalità più interessanti dell’API SDK di SageMaker:

  • Innanzitutto, specifichiamo il percorso locale della nostra directory di origine e delle dipendenze nei parametri source_dir e dependencies. L’SDK comprimerà e caricherà quelle directory su Amazon Simple Storage Service (Amazon S3) e SageMaker le renderà disponibili nell’istanza di allenamento nella directory di lavoro /opt/ml/code.
  • In secondo luogo, utilizziamo l’oggetto estimator SKLearn dell’SDK con la nostra versione preferita di Python e del framework, in modo che SageMaker possa scaricare il container corrispondente. Abbiamo anche definito una metrica di allenamento personalizzata ‘validation:rmse‘, che verrà emessa nei log di allenamento e catturata da SageMaker. Successivamente, utilizzeremo questa metrica come metrica obiettivo nel job di ottimizzazione.
hyperparameters = {"num_round": 6, "max_depth": 5}
estimator_parameters = {
    "entry_point": "multi_model_hpo.py",
    "source_dir": "code",
    "dependencies": ["my_custom_library"],
    "instance_type": training_instance_type,
    "instance_count": 1,
    "hyperparameters": hyperparameters,
    "role": role,
    "base_job_name": "xgboost-model",
    "framework_version": "1.0-1",
    "keep_alive_period_in_seconds": 60,
    "metric_definitions":[
       {'Name': 'validation:rmse', 'Regex': 'validation-rmse:(.*?);'}
    ]
}
estimator = SKLearn(**estimator_parameters)

Successivamente, scriviamo il nostro script di allenamento (multi_model_hpo.py). Il nostro script segue un flusso semplice: acquisisce gli iperparametri con cui è stato configurato il job e addestra il modello CatBoost e il modello XGBoost. Implementiamo anche una funzione di k-fold cross validation. Vedere il seguente codice:

if __name__ == "__main__":
    parser = argparse.ArgumentParser()

    # Argomenti specifici di Sagemaker. I valori predefiniti sono impostati nelle variabili di ambiente.
    parser.add_argument("--output-data-dir", type=str, default=os.environ["SM_OUTPUT_DATA_DIR"])
    parser.add_argument("--model-dir", type=str, default=os.environ["SM_MODEL_DIR"])
    parser.add_argument("--train", type=str, default=os.environ["SM_CHANNEL_TRAIN"])
    parser.add_argument("--validation", type=str, default=os.environ["SM_CHANNEL_VALIDATION"])
    .
    .
    .
    
    """
    Addestramento di CatBoost
    """
    
    K = args.k_fold    
    catboost_hyperparameters = {
        "max_depth": args.max_depth,
        "eta": args.eta,
    }
    rmse_list, model_catboost = cross_validation_catboost(train_df, K, catboost_hyperparameters)
    .
    .
    .
    
    """
    Addestramento del modello XGBoost
    """

    iperparametri = {
        "max_depth": args.max_depth,
        "eta": args.eta,
        "obiettivo": args.objective,
        "num_round": args.num_round,
    }

    rmse_list, model_xgb = cross_validation(train_df, K, iperparametri)

Dopo che i modelli sono addestrati, calcoliamo la media delle previsioni sia di CatBoost che di XGBoost. Il risultato, pred_mean, è la previsione finale del nostro ensemble. Successivamente, determiniamo l’errore quadratico medio (mean_squared_error) rispetto al set di validazione. val_rmse viene utilizzato per valutare l’intero ensemble durante l’addestramento. Nota che stampiamo anche il valore di RMSE in un formato che corrisponde all’espressione regolare utilizzata nelle metric_definitions. Successivamente, SageMaker Automatic Model Tuning lo utilizzerà per catturare la metrica obiettivo. Ecco il codice seguente:

pred_mean = np.mean(np.array([pred_catboost, pred_xgb]), axis=0)
val_rmse = mean_squared_error(y_validation, pred_mean, squared=False)
print(f"Risultato finale di valutazione: validation-rmse:{val_rmse}")

Infine, lo script salva entrambi gli artefatti del modello nella cartella di output situata in /opt/ml/model.

Quando un lavoro di addestramento è completo, SageMaker impacchetta e copia il contenuto della directory /opt/ml/model come un singolo oggetto in formato TAR compresso nella posizione S3 specificata nella configurazione del lavoro. Nel nostro caso, SageMaker raggruppa i due modelli in un file TAR e lo carica su Amazon S3 alla fine del lavoro di addestramento. Ecco il codice seguente:

model_file_name = 'catboost-regressor-model.dump'
path = os.path.join(args.model_dir, model_file_name)
print('salvataggio del file del modello in {}'.format(path))
model.save_model(path)
...
...
...
model_location = args.model_dir + "/xgboost-model"
pickle.dump(model, open(model_location, "wb"))
logging.info("Modello addestrato salvato in {}".format(model_location))

In sintesi, dovresti notare che in questa procedura abbiamo scaricato i dati una volta e addestrato due modelli utilizzando un singolo lavoro di addestramento.

Tuning automatico di un ensemble

Poiché stiamo costruendo una collezione di modelli di ML, esplorare tutte le possibili permutazioni degli iperparametri è impraticabile. SageMaker offre il tuning automatico del modello (AMT), che cerca i migliori iperparametri del modello concentrandosi sulle combinazioni più promettenti di valori all’interno degli intervalli che specificate (spetta a voi definire gli intervalli corretti da esplorare). SageMaker supporta più metodi di ottimizzazione tra cui scegliere.

Iniziamo definendo le due parti del processo di ottimizzazione: la metrica obiettivo e gli iperparametri che vogliamo ottimizzare. Nel nostro esempio, utilizziamo il RMSE di validazione come metrica target e ottimizziamo eta e max_depth (per altri iperparametri, fare riferimento agli iperparametri di XGBoost e ai parametri di CatBoost):

from sagemaker.tuner import (
    IntegerParameter,
    ContinuousParameter,
    HyperparameterTuner,
)

hyperparameter_ranges = {
    "eta": ContinuousParameter(0.2, 0.3),
    "max_depth": IntegerParameter(3, 4)
}
metric_definitions = [{"Name": "validation:rmse", "Regex": "validation-rmse:([0-9\\.]+)"}]
objective_metric_name = "validation:rmse"

Dobbiamo anche assicurarci nello script di addestramento che i nostri iperparametri non siano codificati rigidamente e vengano estratti dagli argomenti dell’esecuzione di SageMaker:

catboost_hyperparameters = {
    "max_depth": args.max_depth,
    "eta": args.eta,
}

SageMaker scrive anche gli iperparametri in un file JSON che può essere letto da /opt/ml/input/config/hyperparameters.json nell’istanza di addestramento.

Come per CatBoost, catturiamo anche gli iperparametri per il modello XGBoost (nota che objective e num_round non vengono ottimizzati):

catboost_hyperparameters = {
    "max_depth": args.max_depth,
    "eta": args.eta,
}

Infine, lanciamo il lavoro di tuning degli iperparametri utilizzando queste configurazioni:

tuner = HyperparameterTuner(
    estimator, 
    objective_metric_name,
    hyperparameter_ranges, 
    max_jobs=4, 
    max_parallel_jobs=2, 
    objective_type='Minimize'
)
tuner.fit({"train": train_location, "validation": validation_location}, include_cls_metadata=False)

Quando il lavoro è completo, è possibile recuperare i valori per il miglior lavoro di addestramento (con RMSE minimo):

job_name=tuner.latest_tuning_job.name
attached_tuner = HyperparameterTuner.attach(job_name)
attached_tuner.describe()["BestTrainingJob"]

Per ulteriori informazioni su AMT, fare riferimento a Eseguire il tuning automatico del modello con SageMaker.

Deploy

Per distribuire il nostro ensemble personalizzato, è necessario fornire uno script per gestire la richiesta di inferenza e configurare l’hosting di SageMaker. In questo esempio, è stato utilizzato un singolo file che include sia il codice di addestramento che di inferenza (multi_model_hpo.py). SageMaker utilizza il codice sotto if __ name __ == "__ main __" per l’addestramento e le funzioni model_fn, input_fn e predict_fn durante la distribuzione e il servizio del modello.

Script di inferenza

Come per l’addestramento, utilizziamo il contenitore del framework SKLearn di SageMaker con il nostro script di inferenza personalizzato. Lo script implementerà tre metodi richiesti da SageMaker.

Innanzitutto, il metodo model_fn legge i file artefatto del modello salvato e li carica in memoria. Nel nostro caso, il metodo restituisce il nostro ensemble come all_model, che è una lista Python, ma è anche possibile utilizzare un dizionario con nomi di modelli come chiavi.

def model_fn(model_dir):
   catboost_model = CatBoostRegressor()
   catboost_model.load_model(os.path.join(model_dir, model_file_name))
  
   model_file = "xgboost-model"
   model = pickle.load(open(os.path.join(model_dir, model_file), "rb"))
  
   all_model = [catboost_model, model]
   return all_model

In secondo luogo, il metodo input_fn deserializza i dati di input della richiesta da passare al nostro gestore di inferenza. Per ulteriori informazioni sugli handler di input, fare riferimento all’adattamento del proprio contenitore di inferenza.

def input_fn(input_data, content_type):
   dtype=None
   payload = StringIO(input_data)
   return np.genfromtxt(payload, dtype=dtype, delimiter=",")

In terzo luogo, il metodo predict_fn è responsabile per ottenere le previsioni dai modelli. Il metodo prende il modello e i dati restituiti da input_fn come parametri e restituisce la previsione finale. Nel nostro esempio, otteniamo il risultato di CatBoost dal primo membro della lista dei modelli (model[0]) e XGBoost dal secondo membro (model[1]), e utilizziamo una funzione di blending che restituisce la media delle due previsioni:

def predict_fn(input_data, model):
   predictions_catb = model[0].predict(input_data)
    
   dtest = xgb.DMatrix(input_data)
   predictions_xgb = model[1].predict(dtest,
                                      ntree_limit=getattr(model, "best_ntree_limit", 0),
                                      validate_features=False)
  
   return np.mean(np.array([predictions_catb, predictions_xgb]), axis=0)

Ora che abbiamo i nostri modelli addestrati e lo script di inferenza, possiamo configurare l’ambiente per distribuire il nostro ensemble.

Inferenza serverless di SageMaker

Anche se esistono molte opzioni di hosting in SageMaker, in questo esempio utilizziamo un endpoint serverless. Gli endpoint serverless lanciano automaticamente risorse di calcolo e le scalano dentro e fuori a seconda del traffico. Ciò elimina il lavoro pesante indifferenziato di gestire i server. Questa opzione è ideale per carichi di lavoro che hanno periodi di inattività tra spurts di traffico e possono tollerare i cold start.

La configurazione dell’endpoint serverless è semplice perché non è necessario scegliere tipi di istanze o gestire le politiche di scaling. È sufficiente fornire due parametri: dimensione della memoria e concorrenza massima. L’endpoint serverless assegna automaticamente risorse di calcolo proporzionali alla memoria selezionata. Se si sceglie una dimensione della memoria più grande, il contenitore ha accesso a più vCPU. Si dovrebbe sempre scegliere la dimensione della memoria del proprio endpoint in base alla dimensione del modello. Il secondo parametro che è necessario fornire è la concorrenza massima. Per un singolo endpoint, questo parametro può essere impostato fino a 200 (al momento della scrittura, il limite per il numero totale di endpoint serverless in una regione è 50). Si deve notare che la concorrenza massima per un singolo endpoint impedisce a quell’endpoint di utilizzare tutte le invocazioni consentite per il proprio account, perché le invocazioni dell’endpoint oltre il massimo vengono limitate (per ulteriori informazioni sulla concorrenza totale per tutti gli endpoint serverless per regione, fare riferimento a endpoint e quote di Amazon SageMaker).

from sagemaker.serverless.serverless_inference_config import ServerlessInferenceConfig
serverless_config = ServerlessInferenceConfig(
    memory_size_in_mb=6144,
    max_concurrency=1,
)

Ora che abbiamo configurato il punto di accesso, possiamo finalmente distribuire il modello selezionato nel nostro lavoro di ottimizzazione degli iperparametri:

estimator=attached_tuner.best_estimator()
predictor = estimator.deploy(serverless_inference_config=serverless_config)

Pulizia

Anche se i punti di accesso serverless non hanno costi quando non vengono utilizzati, quando hai finito di eseguire questo esempio, assicurati di eliminare il punto di accesso:

predictor.delete_endpoint(predictor.endpoint)

Conclusione

In questo post, abbiamo coperto un approccio per addestrare, ottimizzare e distribuire un insieme personalizzato. Abbiamo dettagliato il processo di utilizzo di un singolo lavoro di addestramento per addestrare più modelli, come utilizzare l’ottimizzazione automatica dei modelli per ottimizzare gli iperparametri dell’insieme e come distribuire un singolo punto di accesso serverless che combina le inferenze di più modelli.

Utilizzando questo metodo si risolvono potenziali problemi di costo e operatività. Il costo di un lavoro di addestramento si basa sulle risorse utilizzate per la durata dell’utilizzo. Scaricando i dati solo una volta per addestrare i due modelli, abbiamo ridotto della metà la fase di download dei dati del lavoro e il volume utilizzato per archiviare i dati, riducendo così il costo complessivo del lavoro di addestramento. Inoltre, il lavoro AMT ha eseguito quattro lavori di addestramento, ognuno con il tempo e lo storage ridotti sopra menzionati, rappresentando quindi un risparmio di costo del 4 volte! Per quanto riguarda la distribuzione del modello su un punto di accesso serverless, poiché si paga anche per la quantità di dati elaborati, richiamando il punto di accesso solo una volta per due modelli, si pagano la metà delle spese per i dati di I/O.

Sebbene questo post abbia mostrato solo i benefici con due modelli, è possibile utilizzare questo metodo per addestrare, ottimizzare e distribuire numerosi modelli di insieme per ottenere un effetto ancora maggiore.

Riferimenti

[1] Raj Kumar, P. Arun; Selvakumar, S. (2011). “Distributed denial of service attack detection using an ensemble of neural classifier”. Computer Communications. 34 (11): 1328–1341. doi:10.1016/j.comcom.2011.01.012.

[2] Bradley Efron, Trevor Hastie, Iain Johnstone and Robert Tibshirani (2004) “Least Angle Regression,” Annals of Statistics (with discussion), 407-499. (https://web.stanford.edu/~hastie/Papers/LARS/LeastAngle_2002.pdf)