API SavedModel courantes pour les tâches de texte

Cette page décrit comment TF2 SavedModels pour les tâches liées au texte doit implémenter l' API Reusable SavedModel . (Ceci remplace et étend les signatures communes pour le texte pour le format TF1 Hub désormais obsolète.)

Aperçu

Il existe plusieurs API pour calculer les incorporations de texte (également appelées représentations denses de texte ou vecteurs d'entités de texte).

  • L'API pour les incorporations de texte à partir d'entrées de texte est implémentée par un SavedModel qui mappe un lot de chaînes à un lot de vecteurs d'incorporation. C'est très facile à utiliser, et de nombreux modèles sur TF Hub l'ont implémenté. Cependant, cela ne permet pas d'affiner le modèle sur TPU.

  • L'API pour les incorporations de texte avec des entrées prétraitées résout la même tâche, mais est implémentée par deux SavedModels distincts :

    • un préprocesseur qui peut s'exécuter dans un pipeline d'entrée tf.data et convertit les chaînes et autres données de longueur variable en Tenseurs numériques,
    • un codeur qui accepte les résultats du préprocesseur et effectue la partie pouvant être entraînée du calcul d'intégration.

    Cette division permet aux entrées d'être prétraitées de manière asynchrone avant d'être introduites dans la boucle d'apprentissage. En particulier, il permet de créer des encodeurs pouvant être exécutés et affinés sur TPU .

  • L'API pour les intégrations de texte avec les encodeurs Transformer étend l'API pour les intégrations de texte des entrées prétraitées au cas particulier de BERT et d'autres encodeurs Transformer.

    • Le préprocesseur est étendu pour créer des entrées d'encodeur à partir de plusieurs segments de texte d'entrée.

    • L' encodeur Transformer expose les intégrations contextuelles de jetons individuels.

Dans chaque cas, les entrées de texte sont des chaînes encodées en UTF-8, généralement en texte brut, à moins que la documentation du modèle n'en dispose autrement.

Indépendamment de l'API, différents modèles ont été pré-formés sur du texte provenant de différentes langues et domaines, et avec différentes tâches à l'esprit. Par conséquent, tous les modèles d'incorporation de texte ne conviennent pas à tous les problèmes.

Incorporation de texte à partir d'entrées de texte

Un SavedModel pour les incorporations de texte à partir d'entrées de texte accepte un lot d'entrées dans une chaîne Tensor of shape [batch_size] et les mappe à un float32 Tensor of shape [batch_size, dim] avec des représentations denses (vecteurs de caractéristiques) des entrées.

Synthèse d'utilisation

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

Rappel de l' API réutilisable SavedModel que l' exécution du modèle en mode de formation (par exemple, pour l' abandon) peut exiger un argument mot - clé obj(..., training=True) , et que obj fournit des attributs .variables , .trainable_variables et .regularization_losses selon le cas .

A Keras, tout cela est pris en charge par

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

Formation distribuée

Si l'incorporation de texte est utilisée dans le cadre d'un modèle qui est entraîné avec une stratégie de distribution, l'appel à hub.load("path/to/model") ou hub.KerasLayer("path/to/model", ...) , resp., doit se produire à l'intérieur de la portée DistributionStrategy afin de créer les variables du modèle de manière distribuée. Par example

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

Exemples

Incorporations de texte avec des entrées prétraitées

Une incorporation de texte avec des entrées prétraitées est implémentée par deux SavedModels distincts:

  • un préprocesseur qui mappe une chaîne Tensor of shape [batch_size] à un dict de Tensors numériques,
  • un encodeur qui accepte un dict de Tensors tel que renvoyé par le préprocesseur, effectue la partie pouvant être entraînée du calcul d'intégration et renvoie un dict de sorties. La sortie sous la clé "default" est un float32 Tensor de forme [batch_size, dim] .

Cela permet d'exécuter le préprocesseur dans un pipeline d'entrée mais d'affiner les plongements calculés par l'encodeur dans le cadre d'un modèle plus large. En particulier, il permet de construire des encodeurs qui peuvent être exécutés et affinés sur TPU .

Il s'agit d'un détail d'implémentation indiquant quels Tensors sont contenus dans la sortie du préprocesseur, et quels Tensors supplémentaires (le cas échéant) en plus de "default" sont contenus dans la sortie du codeur.

La documentation du codeur doit spécifier le préprocesseur à utiliser avec lui. Typiquement, il y a exactement un bon choix.

Synopsis d'utilisation

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

Rappel de l' API réutilisable SavedModel que l' exécution de l'encodeur en mode de formation (par exemple, pour l' abandon) peut exiger un argument mot - clé encoder(..., training=True) , et que l' encoder fournit des attributs .variables , .trainable_variables et .regularization_losses selon le cas .

Le modèle de preprocessor peut avoir des .variables mais n'est pas destiné à être entraîné davantage. Le prétraitement ne dépend pas du mode : si preprocessor() a un argument training=... , il n'a aucun effet.

A Keras, tout cela est pris en charge par

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

Formation distribuée

Si l'encodeur est utilisé dans le cadre d'un modèle qui est entraîné avec une stratégie de distribution, l'appel à hub.load("path/to/encoder") ou hub.KerasLayer("path/to/encoder", ...) , resp., doit se produire à l'intérieur

  with strategy.scope():
    ...

afin de recréer les variables du codeur de manière distribuée.

De même, si le préprocesseur fait partie du modèle entraîné (comme dans l'exemple simple ci-dessus), il doit également être chargé dans le cadre de la stratégie de distribution. Si, cependant, le préprocesseur est utilisé dans un pipeline d'entrée (par exemple, dans un appelable passé à tf.data.Dataset.map() ), son chargement doit se produire en dehors de la portée de la stratégie de distribution, afin de placer ses variables (le cas échéant ) sur le processeur hôte.

Exemples

Intégration de texte avec les encodeurs Transformer

Les encodeurs de transformateur pour le texte fonctionnent sur un lot de séquences d'entrée, chaque séquence comprenant n 1 segments de texte tokenisé, dans une limite spécifique au modèle sur n . Pour BERT et beaucoup de ses extensions, cette borne est de 2, donc ils acceptent des segments uniques et des paires de segments.

L'API pour les incorporations de texte avec les encodeurs Transformer étend l'API pour les incorporations de texte avec des entrées prétraitées à ce paramètre.

Préprocesseur

Un préprocesseur SavedModel pour les incorporations de texte avec des encodeurs Transformer implémente l'API d'un préprocesseur SavedModel pour les incorporations de texte avec des entrées prétraitées (voir ci-dessus), qui permet de mapper des entrées de texte à segment unique directement aux entrées d'encodeur.

En outre, le préprocesseur SavedModel fournit des sous-objets appelables tokenize pour la tokenisation (séparément par segment) et bert_pack_inputs pour regrouper n segments tokenisés dans une séquence d'entrée pour le codeur. Chaque sous-objet suit l' API Reusable SavedModel .

Synopsis d'utilisation

Comme exemple concret pour deux segments de texte, regardons une tâche d'implication de phrase qui demande si une prémisse (premier segment) implique ou non une hypothèse (deuxième segment).

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.

Dans Keras, ce calcul peut être expulsé comme

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

Détails de tokenize

Un appel à preprocessor.tokenize() accepte une chaîne Tensor de forme [batch_size] et renvoie un RaggedTensor de forme [batch_size, ...] dont les valeurs sont des identifiants de jeton int32 représentant les chaînes d'entrée. Il peut y avoir r 1 dimensions batch_size après batch_size mais aucune autre dimension uniforme.

  • Si r = 1, la forme est [batch_size, (tokens)] , et chaque entrée est simplement tokenisée en une séquence plate de tokens.
  • Si r > 1, il existe r -1 niveaux de regroupement supplémentaires. Par exemple, tensorflow_text.BertTokenizer utilise r =2 pour regrouper les jetons par mots et donne la forme [batch_size, (words), (tokens_per_word)] . Il appartient au modèle en question de savoir combien de ces niveaux supplémentaires existent, le cas échéant, et quels groupements ils représentent.

L'utilisateur peut (mais n'a pas besoin) de modifier les entrées tokenisées, par exemple, pour tenir compte de la limite seq_length qui sera appliquée dans les entrées de l'encodeur de compression. Des dimensions supplémentaires dans la sortie du tokenizer peuvent aider ici (par exemple, pour respecter les limites des mots) mais n'ont plus de sens à l'étape suivante.

En termes de l' API réutilisable SavedModel , le preprocessor.tokenize objet peut avoir .variables mais ne vise pas à former davantage. La tokenisation ne dépend pas du mode : si preprocessor.tokenize() a un argument training=... , cela n'a aucun effet.

Détails de bert_pack_inputs

Un appel à preprocessor.bert_pack_inputs() accepte une liste Python d'entrées tokenisées (lotées séparément pour chaque segment d'entrée) et renvoie un dict de Tensors représentant un lot de séquences d'entrée de longueur fixe pour le modèle d'encodeur Transformer.

Chaque entrée tokenisée est un int32 RaggedTensor de forme [batch_size, ...] , où le nombre r de dimensions irrégulières après batch_size est soit 1 soit le même que dans la sortie de preprocessor.tokenize(). (Ce dernier n'est que pour des raisons de commodité; les dimensions supplémentaires sont aplaties avant l'emballage.)

L'emballage ajoute des jetons spéciaux autour des segments d'entrée comme prévu par l'encodeur. L'appel bert_pack_inputs() implémente exactement le schéma d'emballage utilisé par les modèles BERT d'origine et nombre de leurs extensions: la séquence compressée commence par un jeton de début de séquence, suivi des segments tokenisés, chacun terminé par une fin de segment jeton. Les positions restantes jusqu'à seq_length, le cas échéant, sont remplies avec des jetons de remplissage.

Si une séquence compressée dépasse seq_length, bert_pack_inputs() tronque ses segments en préfixes de tailles approximativement égales afin que la séquence compressée tienne exactement dans seq_length.

L'emballage n'est pas dépendant du mode: si preprocessor.bert_pack_inputs() a un argument training=... du tout, cela n'a aucun effet. De plus, preprocessor.bert_pack_inputs ne devrait pas avoir de variables ni prendre en charge le réglage fin.

Encodeur

L'encodeur est appelé sur le dict de encoder_inputs de la même manière que dans l'API pour les incorporations de texte avec des entrées prétraitées (voir ci-dessus), y compris les dispositions de l' API Reusable SavedModel .

Synposis d'utilisation

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

ou équivalent à Keras :

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

Des détails

Les encoder_outputs sont un dict de Tensors avec les clés suivantes.

  • "sequence_output" : un tenseur float32 de forme [batch_size, seq_length, dim] avec l'intégration contextuelle de chaque jeton de chaque séquence d'entrée compressée.
  • "pooled_output" : un float32 Tensor de forme [batch_size, dim] avec l'incorporation de chaque séquence d'entrée dans son ensemble, dérivée de sequence_output d'une manière [batch_size, dim] .
  • "default" , comme requis par l'API pour les incorporations de texte avec des entrées prétraitées: un float32 Tensor de forme [batch_size, dim] avec l'incorporation de chaque séquence d'entrée. (Cela peut être juste un alias de pooled_output.)

Le contenu des encoder_inputs n'est pas strictement requis par cette définition d'API. Cependant, pour les encodeurs qui utilisent des entrées de style BERT, il est recommandé d'utiliser les noms suivants (tirés du NLP Modeling Toolkit de TensorFlow Model Garden ) pour minimiser les frictions lors de l'échange d'encodeurs et de la réutilisation des modèles de préprocesseur :

  • "input_word_ids" : un "input_word_ids" int32 de forme [batch_size, seq_length] avec les identifiants de jeton de la séquence d'entrée compressée (c'est-à-dire comprenant un jeton de début de séquence, des jetons de fin de segment et un remplissage).
  • "input_mask" : un Tensor int32 de forme [batch_size, seq_length] avec la valeur 1 à la position de tous les jetons d'entrée présents avant le remplissage et la valeur 0 pour les jetons de remplissage.
  • "input_type_ids" : un "input_type_ids" int32 de forme [batch_size, seq_length] avec l'index du segment d'entrée qui a donné lieu au jeton d'entrée à la position respective. Le premier segment d'entrée (index 0) comprend le jeton de début de séquence et son jeton de fin de segment. Le deuxième segment et les segments suivants (le cas échéant) incluent leur jeton de fin de segment respectif. Les jetons de remplissage obtiennent à nouveau l'index 0.

Formation distribuée

Pour charger les objets préprocesseur et encodeur à l'intérieur ou à l'extérieur d'une portée de stratégie de distribution, les mêmes règles s'appliquent que dans l'API pour les incorporations de texte avec des entrées prétraitées (voir ci-dessus).

Exemples