API SavedModel comuni per attività di testo

Questa pagina descrive come TF2 SavedModels per le attività relative al testo dovrebbe implementare l' API Reusable SavedModel . (Questo sostituisce ed estende le firme comuni per il testo per il formato TF1 Hub ormai deprecato.)

Panoramica

Esistono diverse API per calcolare gli incorporamenti di testo (noti anche come rappresentazioni dense di testo o vettori di funzionalità di testo).

  • L'API per l'incorporamento di testo da input di testo è implementata da un SavedModel che mappa un batch di stringhe su un batch di vettori di incorporamento. Questo è molto facile da usare e molti modelli su TF Hub lo hanno implementato. Tuttavia, ciò non consente la messa a punto del modello su TPU.

  • L'API per l'incorporamento di testo con input preelaborati risolve lo stesso compito, ma è implementata da due SavedModel separati:

    • un preprocessore che può essere eseguito all'interno di una pipeline di input tf.data e converte stringhe e altri dati di lunghezza variabile in tensori numerici,
    • un codificatore che accetta i risultati del preprocessore ed esegue la parte addestrabile del calcolo di incorporamento.

    Questa suddivisione consente di preelaborare gli input in modo asincrono prima di essere inseriti nel ciclo di training. In particolare, consente di costruire codificatori che possono essere eseguiti e ottimizzati su TPU .

  • L'API per l'incorporamento di testo con i codificatori Transformer estende l'API per l'incorporamento di testo da input preelaborati al caso particolare di BERT e altri codificatori Transformer.

    • Il preprocessore viene esteso per creare input del codificatore da più di un segmento di testo di input.
    • Il codificatore Transformer espone gli incorporamenti sensibili al contesto di singoli token.

In ogni caso, gli input di testo sono stringhe codificate UTF-8, in genere di testo semplice, a meno che la documentazione del modello non disponga diversamente.

Indipendentemente dall'API, diversi modelli sono stati pre-addestrati su testo proveniente da lingue e domini diversi e con in mente compiti diversi. Pertanto, non tutti i modelli di incorporamento del testo sono adatti a ogni problema.

Incorporamento di testo da input di testo

Un SavedModel per incorporamenti di testo da input di testo accetta un batch di input in un tensore di forma stringa [batch_size] e li mappa su un tensore di forma float32 [batch_size, dim] con rappresentazioni dense (vettori di funzionalità) degli input.

Sinossi sull'utilizzo

obj = hub.load("path/to/model")
text_input = ["A long sentence.",
              "single-word",
              "http://example.com"]
embeddings = obj(text_input)

Ricordiamo dall'API Reusable SavedModel che l'esecuzione del modello in modalità training (ad esempio, per dropout) può richiedere un argomento chiave obj(..., training=True) e che obj fornisce gli attributi .variables , .trainable_variables e .regularization_losses come applicabile .

A Keras, tutto questo è curato da

embeddings = hub.KerasLayer("path/to/model", trainable=...)(text_input)

Formazione distribuita

Se l'incorporamento del testo viene utilizzato come parte di un modello che viene addestrato con una strategia di distribuzione, la chiamata a hub.load("path/to/model") o hub.KerasLayer("path/to/model", ...) , risp., deve avvenire all'interno dell'ambito DistributionStrategy per creare le variabili del modello in modo distribuito. Per esempio

  with strategy.scope():
    ...
    model = hub.load("path/to/model")
    ...

Esempi

Incorporamenti di testo con input preelaborati

Un incorporamento di testo con input preelaborati è implementato da due SavedModel separati:

  • un preprocessore che mappa un tensore di stringa di forma [batch_size] su un dettato di tensori numerici,
  • un codificatore che accetta un dict di tensori come restituito dal preprocessore, esegue la parte addestrabile del calcolo di incorporamento e restituisce un dict di output. L'output sotto la chiave "default" è un tensore float32 di forma [batch_size, dim] .

Ciò consente di eseguire il preprocessore in una pipeline di input ma di ottimizzare gli incorporamenti calcolati dal codificatore come parte di un modello più ampio. In particolare, consente di costruire encoder che possono essere eseguiti e messi a punto su TPU .

È un dettaglio di implementazione quali tensori sono contenuti nell'output del preprocessore e quali (se presenti) tensori aggiuntivi oltre a "default" sono contenuti nell'output del codificatore.

La documentazione dell'encoder deve specificare quale preprocessore utilizzare con esso. In genere, esiste esattamente una scelta corretta.

Sinossi sull'utilizzo

text_input = tf.constant(["A long sentence.",
                          "single-word",
                          "http://example.com"])
preprocessor = hub.load("path/to/preprocessor")  # Must match `encoder`.
encoder_inputs = preprocessor(text_input)

encoder = hub.load("path/to/encoder")
encoder_outputs = encoder(encoder_inputs)
embeddings = encoder_outputs["default"]

Ricordiamo dall'API Reusable SavedModel che l'esecuzione del codificatore in modalità training (ad esempio, per dropout) può richiedere un argomento chiave encoder(..., training=True) e che encoder fornisce gli attributi .variables , .trainable_variables e .regularization_losses come applicabile .

Il modello preprocessor può avere .variables ma non è pensato per essere addestrato ulteriormente. La preelaborazione non dipende dalla modalità: se preprocessor() ha un argomento training=... , non ha alcun effetto.

A Keras, tutto questo è curato da

encoder_inputs = hub.KerasLayer("path/to/preprocessor")(text_input)
encoder_outputs = hub.KerasLayer("path/to/encoder", trainable=True)(encoder_inputs)
embeddings = encoder_outputs["default"]

Formazione distribuita

Se il codificatore viene utilizzato come parte di un modello che viene addestrato con una strategia di distribuzione, la chiamata a hub.load("path/to/encoder") o hub.KerasLayer("path/to/encoder", ...) , risp., deve avvenire all'interno

  with strategy.scope():
    ...

in modo da ricreare le variabili dell'encoder in modo distribuito.

Allo stesso modo, se il preprocessore fa parte del modello addestrato (come nel semplice esempio sopra), deve essere caricato anche nell'ambito della strategia di distribuzione. Se, tuttavia, il preprocessore viene utilizzato in una pipeline di input (ad esempio, in un callable passato a tf.data.Dataset.map() ), il suo caricamento deve avvenire al di fuori dell'ambito della strategia di distribuzione, in modo da posizionare le sue variabili (se presenti ) sulla CPU host.

Esempi

Incorporamenti di testo con Transformer Encoder

I codificatori trasformatori per il testo operano su un batch di sequenze di input, ciascuna sequenza comprende n ≥ 1 segmenti di testo tokenizzato, all'interno di un limite specifico del modello su n . Per BERT e molte delle sue estensioni, il limite è 2, quindi accettano segmenti singoli e coppie di segmenti.

L'API per l'incorporamento di testo con i codificatori Transformer estende l'API per l'incorporamento di testo con input preelaborati a questa impostazione.

Preprocessore

Un SavedModel del preprocessore per incorporamenti di testo con codificatori Transformer implementa l'API di un SavedModel del preprocessore per incorporamenti di testo con input preelaborati (vedi sopra), che fornisce un modo per mappare input di testo a segmento singolo direttamente sugli input del codificatore.

Inoltre, il preprocessore SavedModel fornisce sottooggetti richiamabili tokenize per la tokenizzazione (separatamente per segmento) e bert_pack_inputs per comprimere n segmenti tokenizzati in un'unica sequenza di input per il codificatore. Ogni oggetto secondario segue l' API riutilizzabile SavedModel .

Sinossi sull'utilizzo

Come esempio concreto per due segmenti di testo, consideriamo un compito di implicazione della frase che chiede se una premessa (primo segmento) implica o meno un'ipotesi (secondo segmento).

preprocessor = hub.load("path/to/preprocessor")

# Tokenize batches of both text inputs.
text_premises = tf.constant(["The quick brown fox jumped over the lazy dog.",
                             "Good day."])
tokenized_premises = preprocessor.tokenize(text_premises)
text_hypotheses = tf.constant(["The dog was lazy.",  # Implied.
                               "Axe handle!"])       # Not implied.
tokenized_hypotheses = preprocessor.tokenize(text_hypotheses)

# Pack input sequences for the Transformer encoder.
seq_length = 128
encoder_inputs = preprocessor.bert_pack_inputs(
    [tokenized_premises, tokenized_hypotheses],
    seq_length=seq_length)  # Optional argument.

In Keras, questo calcolo può essere espresso come

tokenize = hub.KerasLayer(preprocessor.tokenize)
tokenized_hypotheses = tokenize(text_hypotheses)
tokenized_premises = tokenize(text_premises)

bert_pack_inputs = hub.KerasLayer(
    preprocessor.bert_pack_inputs,
    arguments=dict(seq_length=seq_length))  # Optional argument.
encoder_inputs = bert_pack_inputs([tokenized_premises, tokenized_hypotheses])

Dettagli di tokenize

Una chiamata a preprocessor.tokenize() accetta un tensore di stringa di forma [batch_size] e restituisce un RaggedTensor di forma [batch_size, ...] i cui valori sono int32 token id che rappresentano le stringhe di input. Possono esserci r ≥ 1 dimensioni irregolari dopo batch_size ma nessun'altra dimensione uniforme.

  • Se r = 1, la forma è [batch_size, (tokens)] e ogni input viene semplicemente tokenizzato in una sequenza piatta di token.
  • Se r >1, ci sono r -1 livelli aggiuntivi di raggruppamento. Ad esempio, tensorflow_text.BertTokenizer utilizza r =2 per raggruppare i token per parole e produce shape [batch_size, (words), (tokens_per_word)] . Dipende dal modello in questione quanti di questi livelli aggiuntivi esistono, se presenti, e quali raggruppamenti rappresentano.

L'utente può (ma non è obbligato) modificare gli input tokenizzati, ad esempio, per soddisfare il limite seq_length che verrà applicato nel confezionamento degli input del codificatore. Dimensioni extra nell'output del tokenizzatore possono aiutare in questo caso (ad esempio, per rispettare i limiti delle parole) ma diventano prive di significato nel passaggio successivo.

In termini di Reusable SavedModel API , l'oggetto preprocessor.tokenize può avere .variables ma non è pensato per essere addestrato ulteriormente. La tokenizzazione non dipende dalla modalità: se preprocessor.tokenize() ha un argomento training=... , non ha alcun effetto.

Dettagli di bert_pack_inputs

Una chiamata a preprocessor.bert_pack_inputs() accetta un elenco Python di input tokenizzati (raggruppati separatamente per ciascun segmento di input) e restituisce un dict di tensori che rappresenta un batch di sequenze di input a lunghezza fissa per il modello di codifica Transformer.

Ogni input tokenizzato è un RaggedTensor int32 di forma [batch_size, ...] , dove il numero r di dimensioni ragged dopo batch_size è 1 o uguale all'output di preprocessor.tokenize(). (Quest'ultimo è solo per comodità; le dimensioni extra vengono appiattite prima dell'imballaggio.)

L'imballaggio aggiunge token speciali attorno ai segmenti di input come previsto dal codificatore. La chiamata bert_pack_inputs() implementa esattamente lo schema di imballaggio utilizzato dai modelli BERT originali e molte delle loro estensioni: la sequenza compressa inizia con un token di inizio sequenza, seguito dai segmenti tokenizzati, ciascuno terminato da un segmento di fine gettone. Le posizioni rimanenti fino a seq_length, se presenti, vengono riempite con token di riempimento.

Se una sequenza compressa supera seq_length, bert_pack_inputs() tronca i suoi segmenti in prefissi di dimensioni approssimativamente uguali in modo che la sequenza compressa rientri esattamente in seq_length.

L'imballaggio non dipende dalla modalità: se preprocessor.bert_pack_inputs() ha un argomento training=... , non ha alcun effetto. Inoltre, non è previsto che preprocessor.bert_pack_inputs contenga variabili o supporti la regolazione fine.

Codificatore

Il codificatore viene chiamato sul comando encoder_inputs allo stesso modo dell'API per l'incorporamento di testo con input preelaborati (vedi sopra), comprese le disposizioni della Reusable SavedModel API .

Sinossi sull'utilizzo

encoder = hub.load("path/to/encoder")
encoder_outputs = encoder(encoder_inputs)

o equivalentemente in Keras:

encoder = hub.KerasLayer("path/to/encoder", trainable=True)
encoder_outputs = encoder(encoder_inputs)

Dettagli

Gli encoder_outputs sono un detto di tensori con le seguenti chiavi.

  • "sequence_output" : un tensore float32 di forma [batch_size, seq_length, dim] con l'incorporamento sensibile al contesto di ciascun token di ogni sequenza di input compressa.
  • "pooled_output" : un tensore float32 di forma [batch_size, dim] con l'incorporamento di ciascuna sequenza di input nel suo insieme, derivato da sequence_output in qualche modo addestrabile.
  • "default" , come richiesto dall'API per gli incorporamenti di testo con input preelaborati: un tensore float32 di forma [batch_size, dim] con l'incorporamento di ciascuna sequenza di input. (Potrebbe essere solo un alias di pooled_output.)

Il contenuto di encoder_inputs non è strettamente richiesto da questa definizione API. Tuttavia, per i codificatori che utilizzano input di tipo BERT, si consiglia di utilizzare i seguenti nomi (dal NLP Modeling Toolkit di TensorFlow Model Garden ) per ridurre al minimo l'attrito nello scambio di codificatori e nel riutilizzo dei modelli del preprocessore:

  • "input_word_ids" : un tensore int32 di forma [batch_size, seq_length] con gli ID token della sequenza di input compressa (ovvero, incluso un token di inizio sequenza, token di fine segmento e riempimento).
  • "input_mask" : un tensore int32 di forma [batch_size, seq_length] con valore 1 nella posizione di tutti i token di input presenti prima del riempimento e valore 0 per i token di riempimento.
  • "input_type_ids" : un tensore int32 di forma [batch_size, seq_length] con l'indice del segmento di input che ha dato origine al token di input nella rispettiva posizione. Il primo segmento di input (indice 0) include il token di inizio sequenza e il relativo token di fine segmento. Il secondo e i segmenti successivi (se presenti) includono il rispettivo token di fine segmento. I token di riempimento ottengono nuovamente l'indice 0.

Formazione distribuita

Per caricare gli oggetti preprocessore e codificatore all'interno o all'esterno dell'ambito della strategia di distribuzione, si applicano le stesse regole dell'API per l'incorporamento di testo con input preelaborati (vedi sopra).

Esempi