Modelli TensorFlow più veloci in Hugging Face Transformers

Modelli più veloci in Hugging Face Transformers TensorFlow.

Negli ultimi mesi, il team di Hugging Face ha lavorato duramente per migliorare i modelli TensorFlow di Transformers al fine di renderli più robusti e veloci. Gli ultimi miglioramenti sono principalmente focalizzati su due aspetti:

  1. Prestazioni computazionali: BERT, RoBERTa, ELECTRA e MPNet sono stati migliorati al fine di ottenere tempi di calcolo molto più veloci. Questo guadagno di prestazioni computazionali è evidente per tutti gli aspetti computazionali: modalità grafica/istantanea, TF Serving e per dispositivi CPU/GPU/TPU.
  2. TF Serving di TensorFlow: ciascuno di questi modelli TensorFlow può essere distribuito con TensorFlow Serving per beneficiare di questo guadagno di prestazioni computazionali per l’inferenza.

Prestazioni Computazionali

Per dimostrare i miglioramenti delle prestazioni computazionali, abbiamo effettuato un benchmark approfondito in cui confrontiamo le prestazioni di BERT con TensorFlow Serving di v4.2.0 con l’implementazione ufficiale di Google. Il benchmark è stato eseguito su una GPU V100 utilizzando una lunghezza di sequenza di 128 (i tempi sono in millisecondi):

L’attuale implementazione di Bert in v4.2.0 è più veloce dell’implementazione di Google fino al ~10%. Inoltre, è anche due volte più veloce delle implementazioni nella versione 4.1.1.

TensorFlow Serving

La sezione precedente dimostra che il nuovo modello Bert ha avuto un aumento drammatico delle prestazioni computazionali nell’ultima versione di Transformers. In questa sezione, ti mostreremo passo dopo passo come distribuire un modello Bert con TensorFlow Serving per beneficiare dell’aumento delle prestazioni computazionali in un ambiente di produzione.

Cos’è TensorFlow Serving?

TensorFlow Serving fa parte dell’insieme di strumenti forniti da TensorFlow Extended (TFX) che rende più facile che mai il compito di distribuire un modello su un server. TensorFlow Serving fornisce due API, una che può essere richiamata tramite richieste HTTP e un’altra tramite gRPC per eseguire l’inferenza sul server.

Cos’è un SavedModel?

Un SavedModel contiene un modello TensorFlow autonomo, compresi i suoi pesi e la sua architettura. Non richiede l’originale sorgente del modello per essere eseguito, il che lo rende utile per la condivisione o la distribuzione con qualsiasi backend che supporti la lettura di un SavedModel come Java, Go, C++ o JavaScript, tra gli altri. La struttura interna di un SavedModel è rappresentata come segue:

savedmodel
    /assets
        -> qui gli asset necessari per il modello (se presenti)
    /variables
        -> qui i checkpoint del modello che contengono i pesi
   saved_model.pb -> file protobuf che rappresenta il grafo del modello

Come installare TensorFlow Serving?

Esistono tre modi per installare e utilizzare TensorFlow Serving:

  • attraverso un contenitore Docker,
  • attraverso un pacchetto apt,
  • o utilizzando pip.

Per rendere le cose più facili e conformi a tutti i sistemi operativi esistenti, utilizzeremo Docker in questo tutorial.

Come creare un SavedModel?

SavedModel è il formato previsto da TensorFlow Serving. A partire da Transformers v4.2.0, la creazione di un SavedModel ha tre funzionalità aggiuntive:

  1. La lunghezza della sequenza può essere modificata liberamente tra le esecuzioni.
  2. Tutti gli input del modello sono disponibili per l’inferenza.
  3. hidden states o attention sono ora raggruppati in un’unica uscita quando vengono restituiti con output_hidden_states=True o output_attentions=True.

Di seguito, puoi trovare le rappresentazioni degli input e degli output di un TFBertForSequenceClassification salvato come un TensorFlow SavedModel:

Il SignatureDef del SavedModel fornito contiene i seguenti input:
  inputs['attention_mask'] tensor_info:
      dtype: DT_INT32
      shape: (-1, -1)
      name: serving_default_attention_mask:0
  inputs['input_ids'] tensor_info:
      dtype: DT_INT32
      shape: (-1, -1)
      name: serving_default_input_ids:0
  inputs['token_type_ids'] tensor_info:
      dtype: DT_INT32
      shape: (-1, -1)
      name: serving_default_token_type_ids:0
Il SignatureDef del SavedModel fornito contiene i seguenti output:
  outputs['attentions'] tensor_info:
      dtype: DT_FLOAT
      shape: (12, -1, 12, -1, -1)
      name: StatefulPartitionedCall:0
  outputs['logits'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, 2)
      name: StatefulPartitionedCall:1
Il nome del metodo è: tensorflow/serving/predict

Per passare direttamente inputs_embeds (gli embedding dei token) invece di input_ids (gli ID dei token) come input, è necessario creare una sottoclasse del modello con una nuova firma di servizio. Il seguente frammento di codice mostra come farlo:

from transformers import TFBertForSequenceClassification
import tensorflow as tf

# Creazione di una sottoclasse per definire una nuova firma di servizio
class MyOwnModel(TFBertForSequenceClassification):
    # Decorare il metodo di servizio con la nuova input_signature
    # Una input_signature rappresenta il nome, il tipo di dati e la forma di un input previsto
    @tf.function(input_signature=[{
        "inputs_embeds": tf.TensorSpec((None, None, 768), tf.float32, name="inputs_embeds"),
        "attention_mask": tf.TensorSpec((None, None), tf.int32, name="attention_mask"),
        "token_type_ids": tf.TensorSpec((None, None), tf.int32, name="token_type_ids"),
    }])
    def serving(self, inputs):
        # chiamare il modello per elaborare gli input
        output = self.call(inputs)

        # restituire l'output formattato
        return self.serving_output(output)

# Istanziare il modello con il nuovo metodo di servizio
model = MyOwnModel.from_pretrained("bert-base-cased")
# salvarlo con saved_model=True per avere una versione SavedModel insieme ai pesi h5
model.save_pretrained("my_model", saved_model=True)

Il metodo di servizio deve essere sovrascritto dall’argomento input_signature del decoratore tf.function. Consulta la documentazione ufficiale per saperne di più sull’argomento input_signature. Il metodo serving viene utilizzato per definire il comportamento di un SavedModel quando viene distribuito con TensorFlow Serving. Ora il SavedModel ha l’aspetto desiderato, osserva il nuovo input inputs_embeds:

La SignatureDef del SavedModel fornito contiene i seguenti input:
  inputs['attention_mask'] tensor_info:
      dtype: DT_INT32
      shape: (-1, -1)
      name: serving_default_attention_mask:0
  inputs['inputs_embeds'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, -1, 768)
      name: serving_default_inputs_embeds:0
  inputs['token_type_ids'] tensor_info:
      dtype: DT_INT32
      shape: (-1, -1)
      name: serving_default_token_type_ids:0
La SignatureDef del SavedModel fornito contiene i seguenti output:
  outputs['attentions'] tensor_info:
      dtype: DT_FLOAT
      shape: (12, -1, 12, -1, -1)
      name: StatefulPartitionedCall:0
  outputs['logits'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, 2)
      name: StatefulPartitionedCall:1
Il nome del metodo è: tensorflow/serving/predict

Come distribuire e utilizzare un SavedModel?

Vediamo passo dopo passo come distribuire e utilizzare un modello BERT per la classificazione del sentiment.

Passo 1

Crea un SavedModel. Per creare un SavedModel, la libreria Transformers ti consente di caricare un modello PyTorch chiamato nateraw/bert-base-uncased-imdb addestrato sul dataset IMDB e convertirlo in un modello TensorFlow Keras per te:

from transformers import TFBertForSequenceClassification

model = TFBertForSequenceClassification.from_pretrained("nateraw/bert-base-uncased-imdb", from_pt=True)
# il parametro saved_model è un flag per creare una versione SavedModel del modello contemporaneamente ai pesi h5
model.save_pretrained("my_model", saved_model=True)

Passo 2

Crea un contenitore Docker con il SavedModel e avvialo. Prima, scarica l’immagine Docker di TensorFlow Serving per la CPU (per la GPU sostituisci serving con serving:latest-gpu):

docker pull tensorflow/serving

Successivamente, avvia un’immagine di servizio come daemon chiamata serving_base:

docker run -d --name serving_base tensorflow/serving

copia il SavedModel appena creato nella cartella models del contenitore serving_base:

docker cp my_model/saved_model serving_base:/models/bert

Committere il contenitore che serve il modello cambiando MODEL_NAME per corrispondere al nome del modello (qui bert), il nome (bert) corrisponde al nome che vogliamo dare al nostro SavedModel:

docker commit --change "ENV MODEL_NAME bert" serving_base my_bert_model

e uccidere l’immagine serving_base eseguita come daemon perché non ci serve più:

docker kill serving_base

Infine, esegui l’immagine per servire il nostro SavedModel come daemon e mappiamo le porte 8501 (REST API) e 8500 (gRPC API) nel contenitore all’host e diamo il nome al contenitore bert.

docker run -d -p 8501:8501 -p 8500:8500 --name bert my_bert_model

Passaggio 3

Interrogare il modello attraverso la REST API:

from transformers import BertTokenizerFast, BertConfig
import requests
import json
import numpy as np

sentence = "Amo il nuovo aggiornamento di TensorFlow in transformers."

# Carica il tokenizer corrispondente al nostro SavedModel
tokenizer = BertTokenizerFast.from_pretrained("nateraw/bert-base-uncased-imdb")

# Carica la configurazione del modello del nostro SavedModel
config = BertConfig.from_pretrained("nateraw/bert-base-uncased-imdb")

# Tokenizza la frase
batch = tokenizer(sentence)

# Converti il batch in un dizionario corretto
batch = dict(batch)

# Metti l'esempio in una lista di dimensione 1, che corrisponde alla dimensione del batch
batch = [batch]

# La REST API richiede un JSON che contiene la chiave "instances" per dichiarare gli esempi da elaborare
input_data = {"instances": batch}

# Interroga la REST API, il percorso corrisponde a http://host:port/model_version/models_root_folder/model_name:method
r = requests.post("http://localhost:8501/v1/models/bert:predict", data=json.dumps(input_data))

# Analizza il risultato JSON. I risultati sono contenuti in una lista con una chiave radice chiamata "predictions"
# e poiché c'è solo un esempio, prende il primo elemento della lista
result = json.loads(r.text)["predictions"][0]

# I risultati restituiti sono probabilità, che possono essere positive o negative quindi prendiamo il loro valore assoluto
abs_scores = np.abs(result)

# Prendi l'argmax che corrisponde all'indice della massima probabilità.
label_id = np.argmax(abs_scores)

# Stampa l'ETICHETTA corretta con il suo indice
print(config.id2label[label_id])

Dovrebbe restituire POSITIVE. È anche possibile passare attraverso la gRPC (google Remote Procedure Call) API per ottenere lo stesso risultato:

from transformers import BertTokenizerFast, BertConfig
import numpy as np
import tensorflow as tf
from tensorflow_serving.apis import predict_pb2
from tensorflow_serving.apis import prediction_service_pb2_grpc
import grpc

sentence = "Amo il nuovo aggiornamento di TensorFlow in transformers."
tokenizer = BertTokenizerFast.from_pretrained("nateraw/bert-base-uncased-imdb")
config = BertConfig.from_pretrained("nateraw/bert-base-uncased-imdb")

# Tokenizza la frase ma questa volta con tensori TensorFlow come output già dimensionati a 1. Es:
# {
#    'input_ids': <tf.Tensor: shape=(1, 3), dtype=int32, numpy=array([[  101, 19082,   102]])>,
#    'token_type_ids': <tf.Tensor: shape=(1, 3), dtype=int32, numpy=array([[0, 0, 0]])>,
#    'attention_mask': <tf.Tensor: shape=(1, 3), dtype=int32, numpy=array([[1, 1, 1]])>
# }
batch = tokenizer(sentence, return_tensors="tf")

# Crea un canale che sarà collegato alla porta gRPC del contenitore
channel = grpc.insecure_channel("localhost:8500")

# Crea uno stub fatto per la previsione. Questo stub verrà utilizzato per inviare la richiesta gRPC al TF Server.
stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)

# Crea una richiesta gRPC fatta per la previsione
request = predict_pb2.PredictRequest()

# Imposta il nome del modello, per questo caso d'uso è bert
request.model_spec.name = "bert"

# Imposta quale firma viene utilizzata per formattare la query gRPC, qui quella predefinita
request.model_spec.signature_name = "serving_default"

# Imposta gli input_ids in input dagli input_ids dati dal tokenizer
# tf.make_tensor_proto trasforma un tensore TensorFlow in un tensore Protobuf
request.inputs["input_ids"].CopyFrom(tf.make_tensor_proto(batch["input_ids"]))

# Stessa cosa con attention mask
request.inputs["attention_mask"].CopyFrom(tf.make_tensor_proto(batch["attention_mask"]))

# Stessa cosa con token type ids
request.inputs["token_type_ids"].CopyFrom(tf.make_tensor_proto(batch["token_type_ids"]))

# Invia la richiesta gRPC al TF Server
result = stub.Predict(request)

# L'output è un protobuf in cui l'unico output è una lista di probabilità
# assegnate alla chiave logits. Poiché le probabilità sono in virgola mobile, la lista viene
# convertita in un array numpy di float con .float_val
output = result.outputs["logits"].float_val

# Stampa l'ETICHETTA corretta con il suo indice
print(config.id2label[np.argmax(np.abs(output))])

Conclusione

Grazie agli ultimi aggiornamenti applicati ai modelli TensorFlow in transformers, ora è possibile distribuire facilmente i propri modelli in produzione utilizzando TensorFlow Serving. Uno dei prossimi passi che stiamo considerando è quello di integrare direttamente la parte di preelaborazione all’interno del SavedModel per rendere le cose ancora più facili.