Come distribuire i modelli TensorFlow alla produzione usando TF Serving

introduzione

La messa in produzione di modelli di Machine Learning (ML) è diventata un argomento popolare e ricorrente. Molte aziende e strutture offrono diverse soluzioni che mirano ad affrontare questo problema.

Per risolvere questo problema, Google ha rilasciato TensorFlow (TF) Servendo nella speranza di risolvere il problema di distribuire modelli ML alla produzione.

Questo pezzo offre un tutorial pratico sul servizio di una rete di segmentazione semantica convoluzionale pre-addestrata. Entro la fine di questo articolo, sarai in grado di utilizzare TF Serving per distribuire ed effettuare richieste a una Deep CNN addestrata in TF. Inoltre, presenterò una panoramica dei principali blocchi di TF Serving e discuterò delle sue API e di come tutto funzioni.

Una cosa che noterai subito è che richiede pochissimo codice per servire effettivamente un modello TF. Se vuoi seguire l'esercitazione ed eseguire l'esempio sul tuo computer, seguilo così com'è. Ma, se vuoi solo conoscere TensorFlow Serving, puoi concentrarti sulle prime due sezioni.

Questo pezzo sottolinea alcuni dei lavori che stiamo svolgendo qui nel gruppo Daitan.

Librerie di servizio TensorFlow: una panoramica

Dedichiamo un po 'di tempo a capire come TF Serving gestisce l'intero ciclo di vita dei modelli ML. Qui, esamineremo (ad alto livello) ciascuno dei principali elementi costitutivi di TF Serving. L'obiettivo di questa sezione è fornire una presentazione introduttiva delle API di servizio TF. Per una panoramica approfondita, vai alla pagina della documentazione di TF Serving.

Il servizio TensorFlow è composto da alcune astrazioni. Queste astrazioni implementano API per diverse attività. I più importanti sono Servable, Loader, Source e Manager. Vediamo come interagiscono.

In breve, il ciclo di vita del servizio inizia quando TF Serving identifica un modello su disco. Il componente Source se ne occupa. È responsabile dell'identificazione di nuovi modelli che devono essere caricati. In pratica, tiene d'occhio il file system per identificare quando una nuova versione del modello arriva sul disco. Quando vede una nuova versione, procede creando un Caricatore per quella versione specifica del modello.

In breve, Loader sa quasi tutto del modello. Include come caricarlo e come stimare le risorse richieste del modello, come la memoria RAM e GPU richiesta. Loader ha un puntatore al modello su disco insieme a tutti i metadati necessari per caricarlo. Ma qui c'è un problema: al Caricatore non è ancora consentito caricare il modello.

Dopo aver creato il Caricatore, l'origine lo invia al Manager come una versione aspirata.

Dopo aver ricevuto la versione Aspired del modello, il Manager procede con il processo di pubblicazione. Qui ci sono due possibilità. Uno è che la prima versione del modello è stata spinta per la distribuzione. In questa situazione, il Manager si assicurerà che le risorse necessarie siano disponibili. Una volta che lo sono, il Manager concede l'autorizzazione Loader per caricare il modello.

Il secondo è che stiamo spingendo una nuova versione di un modello esistente. In questo caso, il Manager deve consultare il plug-in Policy versione prima di andare oltre. La politica della versione determina come avviene il processo di caricamento di una nuova versione del modello.

In particolare, quando si carica una nuova versione di un modello, è possibile scegliere tra preservare (1) disponibilità o (2) risorse. Nel primo caso, siamo interessati ad assicurarci che il nostro sistema sia sempre disponibile per le richieste dei clienti in arrivo. Sappiamo che il Manager consente al Caricatore di creare un'istanza del nuovo grafico con i nuovi pesi.

A questo punto, abbiamo due versioni di modello caricate contemporaneamente. Ma il Manager scarica la versione precedente solo dopo il completamento del caricamento ed è sicuro passare da un modello all'altro.

D'altra parte, se vogliamo risparmiare risorse non avendo il buffer aggiuntivo (per la nuova versione), possiamo scegliere di preservare le risorse. Potrebbe essere utile per i modelli molto pesanti avere un piccolo spazio nella disponibilità, in cambio del risparmio di memoria.

Alla fine, quando un client richiede un handle per il modello, il Manager restituisce un handle al Servable.

Con questa panoramica, siamo pronti a immergerci in un'applicazione del mondo reale. Nelle sezioni seguenti, descriviamo come servire una rete neurale convoluzionale (CNN) utilizzando TF Serving.

Esportazione di un modello per servire

Il primo passo per servire un modello ML incorporato in TensorFlow è assicurarsi che sia nel formato giusto. Per fare ciò, TensorFlow fornisce la classe SavedModel.

SavedModel è il formato di serializzazione universale per i modelli TensorFlow. Se hai familiarità con TF, probabilmente hai usato TensorFlow Saver per mantenere le variabili del tuo modello.

TensorFlow Saver offre funzionalità per salvare / ripristinare i file del punto di arresto del modello sul / dal disco. In effetti, SavedModel avvolge il TensorFlow Saver ed è pensato per essere il modo standard di esportare i modelli TF per servire.

L'oggetto SavedModel ha alcune belle funzionalità.

Innanzitutto, consente di salvare più di un meta-grafico in un singolo oggetto SavedModel. In altre parole, ci consente di avere grafici diversi per compiti diversi.

Ad esempio, supponiamo che tu abbia appena finito di addestrare il tuo modello. Nella maggior parte dei casi, per eseguire l'inferenza, il grafico non necessita di alcune operazioni specifiche dell'allenamento. Queste operazioni potrebbero includere le variabili dell'ottimizzatore, i tensori di pianificazione della frequenza di apprendimento, le operazioni di pre-elaborazione extra e così via.

Inoltre, potresti voler servire una versione quantizzata di un grafico per la distribuzione mobile.

In questo contesto, SavedModel consente di salvare grafici con diverse configurazioni. Nel nostro esempio, avremmo tre diversi grafici con tag corrispondenti come "training", "inferenza" e "mobile". Inoltre, questi tre grafici condividono tutti lo stesso insieme di variabili, il che enfatizza l'efficienza della memoria.

Non molto tempo fa, quando volevamo distribuire modelli TF su dispositivi mobili, avevamo bisogno di conoscere i nomi dei tensori di input e output per alimentare e ottenere dati da / verso il modello. Questa necessità ha costretto i programmatori a cercare il tensore di cui avevano bisogno tra tutti i tensori del grafico. Se i tensori non avessero un nome appropriato, l'attività potrebbe essere molto noiosa.

Per semplificare le cose, SavedModel offre supporto per SignatureDefs. In breve, SignatureDefs definisce la firma di un calcolo supportato da TensorFlow. Determina i tensori di input e output corretti per un grafico computazionale. In poche parole, con queste firme è possibile specificare i nodi esatti da utilizzare per l'input e l'output.

Per utilizzare le API di servizio integrate, TF Serving richiede che i modelli includano uno o più SignatureDef.

Per creare tali firme, dobbiamo fornire definizioni per input, output e il nome del metodo desiderato. Gli input e gli output rappresentano una mappatura dalla stringa agli oggetti TensorInfo (ulteriori informazioni su quest'ultimo). Qui, definiamo i tensori predefiniti per l'alimentazione e la ricezione di dati da e verso un grafico. Il parametro method_name è destinato a una delle API di servizio di alto livello TF.

Attualmente, ci sono tre API di servizio: Classificazione, Previsione e Regressione. Ogni definizione di firma corrisponde a un'API RPC specifica. La classificazione SegnatureDef viene utilizzata per l'API RPC di classificazione. Predict SegnatureDef viene utilizzato per l'API Predict RPC e su.

Per la firma di classificazione, ci deve essere un tensore di input (per ricevere dati) e almeno uno dei due tensori di output possibili: classi e / o punteggi. Regressione SignatureDef richiede esattamente un tensore per l'input e un altro per l'output. Infine, la firma Predict consente un numero dinamico di tensori di input e output.

Inoltre, SavedModel supporta l'archiviazione delle risorse per i casi in cui l'inizializzazione delle operazioni dipende da file esterni. Inoltre, ha meccanismi per cancellare i dispositivi prima di creare SavedModel.

Ora vediamo come possiamo fare in pratica.

Configurare l'ambiente

Prima di iniziare, clonare questa implementazione TensorFlow DeepLab-v3 da Github.

DeepLab è la migliore segmentazione semantica di Google ConvNet. Fondamentalmente, la rete prende un'immagine come input e produce un'immagine simile a una maschera che separa determinati oggetti dallo sfondo.

Questa versione è stata formata sul set di dati di segmentazione VOC Pascal. Pertanto, può segmentare e riconoscere fino a 20 classi. Se vuoi saperne di più su Semantic Segmentation e DeepLab-v3, dai un'occhiata a Diving in Deep Convolutional Semantic Segmentation Networks e Deeplab_V3.

Tutti i file relativi alla pubblicazione risiedono in: ./deeplab_v3/serving/. Lì troverai due file importanti: deeplab_saved_model.py e deeplab_client.ipynb

Prima di andare oltre, assicurati di scaricare il modello pre-addestrato Deeplab-v3. Vai al repository GitHub sopra, fai clic sul link checkpoint e scarica la cartella denominata 16645 /.

Alla fine, dovresti avere una cartella chiamata tboard_logs / con la cartella 16645 / posizionata al suo interno.

Ora, dobbiamo creare due ambienti virtuali Python. Uno per Python 3 e un altro per Python 2. Per ogni ambiente, assicurarsi di installare le dipendenze necessarie. Puoi trovarli nei file serving_requirements.txt e client_requirements.txt.

Abbiamo bisogno di due env per Python perché il nostro modello, DeepLab-v3, è stato sviluppato con Python 3. Tuttavia, l'API Python per server TensorFlow è pubblicata solo per Python 2. Pertanto, per esportare il modello ed eseguire il servizio TF, utilizziamo env Python 3 . Per eseguire il codice client utilizzando l'API python TF Serving, utilizziamo il pacchetto PIP (disponibile solo per Python 2).

Nota che puoi rinunciare all'ambiente Python 2 usando le API di servizio di bazel. Fare riferimento all'Installazione del servizio TF per ulteriori dettagli.

Con questo passaggio completo, cominciamo con ciò che conta davvero.

Come farlo

Per utilizzare SavedModel, TensorFlow fornisce una classe di utilità di alto livello facile da usare chiamata SavedModelBuilder. La classe SavedModelBuilder offre funzionalità per il salvataggio di più meta-grafici, variabili associate e risorse.

Esaminiamo un esempio corrente di come esportare un modello CNN di segmentazione profonda per la pubblicazione.

Come accennato in precedenza, per esportare il modello, utilizziamo la classe SavedModelBuilder. Genererà un file buffer del protocollo SavedModel insieme alle variabili e alle risorse del modello (se necessario).

Analizziamo il codice.

SavedModelBuilder riceve (come input) la directory in cui salvare i dati del modello. Qui, la variabile export_path è la concatenazione di export_path_base e model_version. Di conseguenza, diverse versioni del modello verranno salvate in directory separate all'interno della cartella export_path_base.

Supponiamo di avere una versione di base del nostro modello in produzione, ma vogliamo implementarne una nuova. Abbiamo migliorato l'accuratezza del nostro modello e vogliamo offrire questa nuova versione ai nostri clienti.

Per esportare una versione diversa dello stesso grafico, possiamo semplicemente impostare FLAGS.model_version su un valore intero più alto. Quindi verrà creata una cartella diversa (contenente la nuova versione del nostro modello) all'interno della cartella export_path_base.

Ora, dobbiamo specificare i tensori di input e output del nostro modello. Per fare ciò, usiamo SignatureDefs. Le firme definiscono il tipo di modello che vogliamo esportare. Fornisce una mappatura dalle stringhe (nomi logici del tensore) agli oggetti TensorInfo. L'idea è che, invece di fare riferimento ai nomi effettivi del tensore per input / output, i client possono fare riferimento ai nomi logici definiti dalle firme.

Per servire una CNN di segmentazione semantica, creeremo una firma predittiva. Si noti che la funzione build_signature_def () accetta la mappatura per i tensori di input e output e l'API desiderata.

Un SignatureDef richiede la specifica di: input, output e nome del metodo. Si noti che ci aspettiamo tre valori per gli input: un'immagine e altri due tensori che ne specificano le dimensioni (altezza e larghezza). Per gli output, abbiamo definito un solo risultato: la maschera di output di segmentazione.

Tieni presente che le stringhe "immagine", "altezza", "larghezza" e "segmentation_map" non sono tensori. Al contrario, sono nomi logici che si riferiscono ai tensori effettivi input_tensor, image_height_tensor e image_width_tensor. Pertanto, possono essere qualsiasi stringa unica che ti piace.

Inoltre, i mapping in SignatureDefs si riferiscono agli oggetti protobuf di TensorInfo, non a tensori effettivi. Per creare oggetti TensorInfo, utilizziamo la funzione di utilità: tf.saved_model.utils.build_tensor_info (tensor).

Questo è tutto. Ora chiamiamo la funzione add_meta_graph_and_variables () per creare l'oggetto buffer del protocollo SavedModel. Quindi eseguiamo il metodo save () e persisterà un'istantanea del nostro modello sul disco contenente le variabili e le risorse del modello.

Ora possiamo eseguire deeplab_saved_model.py per esportare il nostro modello.

Se tutto è andato bene vedrai la cartella ./serving/versions/1. Si noti che "1" rappresenta la versione corrente del modello. All'interno di ogni sottodirectory versione, vedrai i seguenti file:

  • saved_model.pb o saved_model.pbtxt. Questo è il file SavedModel serializzato. Include una o più definizioni dei grafici del modello, nonché le definizioni delle firme.
  • Variabili. Questa cartella contiene le variabili serializzate dei grafici.

Ora siamo pronti per il lancio del nostro modello di server. Per farlo, esegui:

$ tensorflow_model_server --port = 9000 --model_name = deeplab --model_base_path = 

Model_base_path si riferisce a dove è stato salvato il modello esportato. Inoltre, non specifichiamo la cartella della versione nel percorso. Il controllo del versioning del modello è gestito da TF Serving.

Generazione di richieste client

Il codice client è molto semplice. Dai un'occhiata in: deeplab_client.ipynb.

Innanzitutto, leggiamo l'immagine che vogliamo inviare al server e la convertiamo nel formato giusto.

Quindi, creiamo uno stub gRPC. Lo stub ci consente di chiamare i metodi del server remoto. Per fare ciò, istanziamo la classe beta_create_PredictionService_stub del modulo prediction_service_pb2. A questo punto, lo stub contiene la logica necessaria per chiamare le procedure remote (dal server) come se fossero locali.

Ora, dobbiamo creare e impostare l'oggetto richiesta. Poiché il nostro server implementa l'API Predict di TensorFlow, è necessario analizzare una richiesta Predict. Per emettere una richiesta Predict, innanzitutto, istanziamo la classe PredictRequest dal modulo predict_pb2. Dobbiamo anche specificare i parametri model_spec.name e model_spec.signature_name. Il nome param è l'argomento "nome_modello" che abbiamo definito all'avvio del server. E signature_name si riferisce al nome logico assegnato al parametro signature_def_map () della routine add_meta_graph ().

Successivamente, dobbiamo fornire i dati di input come definiti nella firma del server. Ricorda che, nel server, abbiamo definito un'API Predict che prevede un'immagine oltre a due scalari (altezza e larghezza dell'immagine). Per alimentare i dati di input nell'oggetto richiesta, TensorFlow fornisce l'utilità tf.make_tensor_proto (). Questo metodo crea un oggetto TensorProto da un oggetto numpy / Python. Possiamo usarlo per alimentare l'immagine e le sue dimensioni all'oggetto richiesta.

Sembra che siamo pronti a chiamare il server. Per fare ciò, chiamiamo il metodo Predict () (usando lo stub) e passiamo l'oggetto richiesta come argomento.

Per le richieste che restituiscono una singola risposta, gRPC supporta entrambe: chiamate sincrone e asincrone. Pertanto, se si desidera eseguire alcune operazioni durante l'elaborazione della richiesta, è possibile chiamare Predict.future () anziché Predict ().

Ora possiamo recuperare e goderci i risultati.

Spero ti sia piaciuto questo articolo. Grazie per aver letto!

Se vuoi di più, controlla: