API SavedModel communes pour les tâches de texte,API SavedModel communes 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 . (Cela 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 représentations incorporées de texte (également appelées représentations denses de texte ou vecteurs de caractéristiques 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 le 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 à l'intérieur d'un pipeline d'entrée tf.data et convertit les chaînes et autres données de longueur variable en Tensors numériques,
    • un codeur qui accepte les résultats du préprocesseur et exécute la partie entraînable 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, cela permet de créer des encodeurs pouvant être exécutés et ajustés sur TPU .

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

    • Le préprocesseur est étendu pour construire des entrées d'encodeur à partir de plus d'un segment 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, sauf indication contraire dans la documentation du modèle.

Indépendamment de l'API, différents modèles ont été pré-formés sur du texte 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 de forme [batch_size] et les mappe à un float32 Tensor de forme [batch_size, dim] avec des représentations denses (vecteurs de caractéristiques) des entrées.

Résumé d'utilisation

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

Rappelez-vous de l' API SavedModel réutilisable que l'exécution du modèle en mode formation (par exemple, pour l'abandon) peut nécessiter un argument de mot-clé obj(..., training=True) , et que obj fournit les 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 formé avec une stratégie de distribution, l'appel à hub.load("path/to/model") ou hub.KerasLayer("path/to/model", ...) , respectivement, 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 exemple

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

Exemples

Incorporations de texte avec 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 de forme [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 entraînable 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 intégrations calculées 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 des tenseurs contenus dans la sortie du préprocesseur et des tenseurs supplémentaires (le cas échéant) en plus de "default" contenus dans la sortie de l'encodeur.

La documentation de l'encodeur doit spécifier quel préprocesseur utiliser avec lui. Typiquement, il y a exactement un choix correct.

Résumé 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"]

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

Le modèle de preprocessor peut avoir .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 formé 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 formé (comme dans l'exemple simple ci-dessus), il doit également être chargé dans la portée de la stratégie de distribution. Si, toutefois, le préprocesseur est utilisé dans un pipeline d'entrée (par exemple, dans un callable 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

Incorporations de texte avec Transformer Encoders

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 certaine limite spécifique au modèle sur n . Pour BERT et bon nombre de ses extensions, cette limite est de 2, ils acceptent donc 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 fournit un moyen de mapper des entrées de texte à segment unique directement aux entrées d'encodeur.

De plus, 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 l'encodeur. Chaque sous-objet suit l' API réutilisable SavedModel .

Résumé d'utilisation

Comme exemple concret pour deux segments de texte, examinons 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 exprimé 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 irrégulières après batch_size mais aucune autre dimension uniforme.

  • Si r =1, la forme est [batch_size, (tokens)] , et chaque entrée est simplement symbolisée en une séquence plate de jetons.
  • Si r >1, il y a r -1 niveaux supplémentaires de regroupement. 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'est pas obligé) modifier les entrées tokenisées, par exemple, pour s'adapter à la limite seq_length qui sera appliquée lors du conditionnement des entrées d'encodeur. Des dimensions supplémentaires dans la sortie du tokenizer peuvent aider ici (par exemple, pour respecter les limites des mots) mais perdent leur sens à l'étape suivante.

En ce qui concerne l' API SavedModel réutilisable , l'objet preprocessor.tokenize peut avoir .variables mais n'est pas destiné à être formé 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 (regroupé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 est uniquement pour plus 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 emballée commence par un jeton de début de séquence, suivi des segments à jeton, 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 compactée devait dépasser seq_length, bert_pack_inputs() tronque ses segments en préfixes de tailles approximativement égales afin que la séquence compactée tienne exactement dans seq_length.

Le compactage ne dépend pas du mode : si preprocessor.bert_pack_inputs() a un argument training=... , 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 .

Résumé d'utilisation

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

ou de manière équivalente en Keras :

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

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 tenseur float32 de forme [batch_size, dim] avec l'intégration de chaque séquence d'entrée dans son ensemble, dérivée de sequence_output d'une manière entraînable.
  • "default" , tel que requis par l'API pour les incorporations de texte avec des entrées prétraitées : un tenseur float32 de forme [batch_size, dim] avec l'incorporation de chaque séquence d'entrée. (Il peut s'agir simplement d'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 ) afin de minimiser les frictions lors de l'échange d'encodeurs et de la réutilisation des modèles de préprocesseur :

  • "input_word_ids" : un tenseur int32 de forme [batch_size, seq_length] avec les identifiants de jeton de la séquence d'entrée compressée (c'est-à-dire, y compris un jeton de début de séquence, des jetons de fin de segment et un rembourrage).
  • "input_mask" : un tenseur 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 tenseur 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