APIs SavedModel comuns para tarefas de texto

Esta página descreve como TF2 SavedModels para tarefas relacionadas a texto devem implementar a API Reusable SavedModel . (Isso substitui e estende as assinaturas comuns para texto para o formato TF1 Hub agora obsoleto.)

Visão geral

Existem várias APIs para calcular incorporações de texto (também conhecidas como representações densas de texto ou vetores de recursos de texto).

  • A API para incorporações de texto a partir de entradas de texto é implementada por um SavedModel que mapeia um lote de strings para um lote de vetores de incorporação. É muito fácil de usar e muitos modelos do TF Hub o implementaram. No entanto, isso não permite o ajuste fino do modelo na TPU.

  • A API para incorporações de texto com entradas pré-processadas resolve a mesma tarefa, mas é implementada por dois SavedModels separados:

    • um pré-processador que pode ser executado dentro de um pipeline de entrada tf.data e converte strings e outros dados de comprimento variável em tensores numéricos,
    • um codificador que aceita os resultados do pré-processador e executa a parte treinável da computação de incorporação.

    Essa divisão permite que as entradas sejam pré-processadas de forma assíncrona antes de serem inseridas no loop de treinamento. Em particular, permite construir codificadores que podem ser executados e ajustados em TPU .

  • A API para incorporações de texto com codificadores Transformer estende a API para incorporações de texto de entradas pré-processadas para o caso específico de BERT e outros codificadores Transformer.

    • O pré-processador é estendido para construir entradas do codificador a partir de mais de um segmento de texto de entrada.
    • O codificador Transformer expõe as incorporações sensíveis ao contexto de tokens individuais.

Em cada caso, as entradas de texto são strings codificadas em UTF-8, normalmente de texto simples, a menos que a documentação do modelo forneça o contrário.

Independentemente da API, diferentes modelos foram pré-treinados em textos de diferentes idiomas e domínios, e com diferentes tarefas em mente. Portanto, nem todo modelo de incorporação de texto é adequado para todos os problemas.

Incorporação de texto a partir de entradas de texto

Um SavedModel para incorporações de texto de entradas de texto aceita um lote de entradas em um tensor de string de forma [batch_size] e os mapeia para um tensor float32 de forma [batch_size, dim] com representações densas (vetores de recursos) das entradas.

Sinopse de uso

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

Lembre-se da API Reusable SavedModel que executar o modelo no modo de treinamento (por exemplo, para abandono) pode exigir um argumento de palavra-chave obj(..., training=True) e que obj fornece atributos .variables , .trainable_variables e .regularization_losses conforme aplicável .

Em Keras, tudo isso é cuidado por

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

Treinamento distribuído

Se a incorporação de texto for usada como parte de um modelo que é treinado com uma estratégia de distribuição, a chamada para hub.load("path/to/model") ou hub.KerasLayer("path/to/model", ...) , respectivamente, deve acontecer dentro do escopo DistributionStrategy para criar as variáveis ​​do modelo de forma distribuída. Por exemplo

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

Exemplos

Incorporações de texto com entradas pré-processadas

Uma incorporação de texto com entradas pré-processadas é implementada por dois SavedModels separados:

  • um pré-processador que mapeia um tensor de string de forma [batch_size] para um ditado de tensores numéricos,
  • um codificador que aceita um ditado de tensores retornado pelo pré-processador, executa a parte treinável da computação de incorporação e retorna um ditado de saídas. A saída na chave "default" é um Tensor float32 de forma [batch_size, dim] .

Isso permite executar o pré-processador em um pipeline de entrada, mas ajustar os embeddings calculados pelo codificador como parte de um modelo maior. Em particular, permite construir codificadores que podem ser executados e ajustados em TPU .

É um detalhe de implementação quais tensores estão contidos na saída do pré-processador e quais (se houver) tensores adicionais além de "default" estão contidos na saída do codificador.

A documentação do codificador deve especificar qual pré-processador usar com ele. Normalmente, há exatamente uma escolha correta.

Sinopse de uso

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")
enocder_outputs = encoder(encoder_inputs)
embeddings = enocder_outputs["default"]

Lembre-se da API Reusable SavedModel que a execução do codificador no modo de treinamento (por exemplo, para abandono) pode exigir um argumento de palavra-chave encoder(..., training=True) e esse encoder fornece atributos .variables , .trainable_variables e .regularization_losses conforme aplicável .

O modelo preprocessor pode ter .variables , mas não deve ser treinado posteriormente. O pré-processamento não depende do modo: se preprocessor() tiver um argumento training=... , ele não terá efeito.

Em Keras, tudo isso é cuidado por

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

Treinamento distribuído

Se o codificador for usado como parte de um modelo que é treinado com uma estratégia de distribuição, a chamada para hub.load("path/to/encoder") ou hub.KerasLayer("path/to/encoder", ...) , respectivamente, deve acontecer dentro

  with strategy.scope():
    ...

para recriar as variáveis ​​do codificador de forma distribuída.

Da mesma forma, se o pré-processador fizer parte do modelo treinado (como no exemplo simples acima), ele também precisará ser carregado no escopo da estratégia de distribuição. Se, no entanto, o pré-processador for usado em um pipeline de entrada (por exemplo, em um callable passado para tf.data.Dataset.map() ), seu carregamento deverá acontecer fora do escopo da estratégia de distribuição, para que suas variáveis ​​(se houver) sejam colocadas ) na CPU host.

Exemplos

Incorporações de texto com Transformer Encoders

Os codificadores transformadores para texto operam em um lote de sequências de entrada, cada sequência compreendendo n ≥ 1 segmentos de texto tokenizado, dentro de algum limite específico do modelo em n . Para o BERT e muitas de suas extensões, esse limite é 2, portanto eles aceitam segmentos únicos e pares de segmentos.

A API para incorporações de texto com codificadores Transformer estende a API para incorporações de texto com entradas pré-processadas para esta configuração.

Pré-processador

Um SavedModel de pré-processador para incorporações de texto com codificadores Transformer implementa a API de um SavedModel de pré-processador para incorporações de texto com entradas pré-processadas (veja acima), que fornece uma maneira de mapear entradas de texto de segmento único diretamente para entradas do codificador.

Além disso, o pré-processador SavedModel fornece tokenize de subobjetos chamáveis ​​para tokenização (separadamente por segmento) e bert_pack_inputs para empacotar n segmentos tokenizados em uma sequência de entrada para o codificador. Cada subobjeto segue a API Reusable SavedModel .

Sinopse de uso

Como exemplo concreto para dois segmentos de texto, vejamos uma tarefa de implicação de sentença que pergunta se uma premissa (primeiro segmento) implica ou não uma hipótese (segundo 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.

Em Keras, este cálculo pode ser expresso como

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])

Detalhes da tokenize

Uma chamada para preprocessor.tokenize() aceita um tensor de string de forma [batch_size] e retorna um RaggedTensor de forma [batch_size, ...] cujos valores são ids de token int32 que representam as strings de entrada. Pode haver r ≥ 1 dimensões irregulares após batch_size , mas nenhuma outra dimensão uniforme.

  • Se r =1, a forma é [batch_size, (tokens)] e cada entrada é simplesmente tokenizada em uma sequência plana de tokens.
  • Se r >1, existem r -1 níveis adicionais de agrupamento. Por exemplo, tensorflow_text.BertTokenizer usa r =2 para agrupar tokens por palavras e produz forma [batch_size, (words), (tokens_per_word)] . Depende do modelo em questão quantos desses níveis extras existem, se houver, e quais agrupamentos eles representam.

O usuário pode (mas não precisa) modificar entradas tokenizadas, por exemplo, para acomodar o limite seq_length que será aplicado no empacotamento de entradas do codificador. Dimensões extras na saída do tokenizer podem ajudar aqui (por exemplo, respeitar os limites das palavras), mas tornam-se sem sentido na próxima etapa.

Em termos da API Reusable SavedModel , o objeto preprocessor.tokenize pode ter .variables , mas não deve ser treinado posteriormente. A tokenização não depende do modo: se preprocessor.tokenize() tiver um argumento training=... , não terá efeito.

Detalhes de bert_pack_inputs

Uma chamada para preprocessor.bert_pack_inputs() aceita uma lista Python de entradas tokenizadas (em lote separadamente para cada segmento de entrada) e retorna um ditado de tensores representando um lote de sequências de entrada de comprimento fixo para o modelo do codificador Transformer.

Cada entrada tokenizada é um int32 RaggedTensor de formato [batch_size, ...] , onde o número r de dimensões irregulares após batch_size é 1 ou o mesmo que na saída de preprocessor.tokenize(). (O último é apenas por conveniência; as dimensões extras são achatadas antes da embalagem.)

A embalagem adiciona tokens especiais em torno dos segmentos de entrada conforme esperado pelo codificador. A chamada bert_pack_inputs() implementa exatamente o esquema de empacotamento usado pelos modelos BERT originais e muitas de suas extensões: a sequência compactada começa com um token de início de sequência, seguido pelos segmentos tokenizados, cada um terminado por um fim de segmento símbolo. As posições restantes até seq_length, se houver, são preenchidas com tokens de preenchimento.

Se uma sequência compactada exceder seq_length, bert_pack_inputs() trunca seus segmentos para prefixos de tamanhos aproximadamente iguais para que a sequência compactada caiba exatamente dentro de seq_length.

A embalagem não depende do modo: se preprocessor.bert_pack_inputs() tiver um argumento training=... , não terá efeito. Além disso, não se espera que preprocessor.bert_pack_inputs tenha variáveis ​​ou suporte ajuste fino.

Codificador

O codificador é chamado no dict de encoder_inputs da mesma forma que na API para embeddings de texto com entradas pré-processadas (veja acima), incluindo as provisões da API Reusable SavedModel .

Sinopse de uso

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

ou equivalentemente em Keras:

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

Detalhes

Os encoder_outputs são um ditado de tensores com as seguintes chaves.

  • "sequence_output" : um tensor float32 de forma [batch_size, seq_length, dim] com a incorporação sensível ao contexto de cada token de cada sequência de entrada compactada.
  • "pooled_output" : um tensor float32 de forma [batch_size, dim] com a incorporação de cada sequência de entrada como um todo, derivado de sequence_output de alguma maneira treinável.
  • "default" , conforme exigido pela API para incorporações de texto com entradas pré-processadas: um tensor float32 de formato [batch_size, dim] com a incorporação de cada sequência de entrada. (Isso pode ser apenas um alias de pooled_output.)

O conteúdo de encoder_inputs não é estritamente exigido por esta definição de API. No entanto, para codificadores que usam entradas estilo BERT, é recomendado usar os seguintes nomes (do NLP Modeling Toolkit do TensorFlow Model Garden ) para minimizar o atrito na troca de codificadores e na reutilização de modelos de pré-processador:

  • "input_word_ids" : um tensor int32 de formato [batch_size, seq_length] com os ids de token da sequência de entrada compactada (ou seja, incluindo um token de início de sequência, tokens de final de segmento e preenchimento).
  • "input_mask" : um tensor int32 de forma [batch_size, seq_length] com valor 1 na posição de todos os tokens de entrada presentes antes do preenchimento e valor 0 para os tokens de preenchimento.
  • "input_type_ids" : um tensor int32 de formato [batch_size, seq_length] com o índice do segmento de entrada que deu origem ao token de entrada na respectiva posição. O primeiro segmento de entrada (índice 0) inclui o token de início de sequência e seu token de fim de segmento. O segundo segmento e os posteriores (se presentes) incluem seu respectivo token de final de segmento. Os tokens de preenchimento obtêm o índice 0 novamente.

Treinamento distribuído

Para carregar os objetos pré-processador e codificador dentro ou fora de um escopo de estratégia de distribuição, aplicam-se as mesmas regras da API para incorporações de texto com entradas pré-processadas (veja acima).

Exemplos