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 réutilisable 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 intégrations de texte (également appelées représentations denses de texte ou vecteurs de caractéristiques de texte).

  • L'API pour les intégrations 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'intégration. C'est très simple à 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 l'intégration 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 Tensors numériques,
    • un encodeur qui accepte les résultats du préprocesseur et effectue 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 de formation. Il permet notamment de créer des encodeurs pouvant être exécutés et affinés sur TPU .

  • L'API pour l'incorporation de texte avec les encodeurs Transformer étend l'API pour l'incorporation 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 codées en UTF-8, généralement du texte brut, sauf indication contraire dans la documentation du modèle.

Quelle que soit l'API, différents modèles ont été pré-entraîné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’intégration de texte ne conviennent pas à tous les problèmes.

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

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

Synopsis 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 réutilisable SavedModel 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 , le cas échéant. .

A Keras, tout cela est pris en charge par

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

Formation distribuée

Si l'intégration 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 exemple

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

Exemples

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

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

  • un préprocesseur qui mappe un Tensor de chaîne 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 tenseur float32 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. Il permet notamment de construire des encodeurs pouvant être exécutés et affinés sur TPU .

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

La documentation de l'encodeur doit préciser quel préprocesseur utiliser avec celui-ci. En règle générale, il existe exactement un choix correct.

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

Rappelons de l' API réutilisable SavedModel 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 encoder fournit les attributs .variables , .trainable_variables et .regularization_losses , le cas échéant. .

Le modèle 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=... , cela 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 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. Toutefois, si le préprocesseur est utilisé dans un pipeline d'entrée (par exemple, dans un appelable passé à tf.data.Dataset.map() ), son chargement doit avoir lieu 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 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 bon nombre de ses extensions, cette limite est de 2, ils acceptent donc des segments simples 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 les entrées de texte à segment unique directement aux entrées de l'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 .

Synopsis d'utilisation

À titre d'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.

En 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 un Tensor de chaîne 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 tokenisée en une séquence plate de jetons.
  • 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 dépend du modèle concerné 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 regroupement des entrées de l'encodeur. Des dimensions supplémentaires dans la sortie du tokenizer peuvent être utiles 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 , l'objet preprocessor.tokenize peut avoir .variables mais n'est pas destiné à être entraîné 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 (groupé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 identique à celui de la sortie de preprocessor.tokenize(). (Ce dernier est uniquement destiné à des fins 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 de compression utilisé par les modèles BERT d'origine et plusieurs 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 de jetons de remplissage.

Si une séquence compressée devait dépasser seq_length, bert_pack_inputs() tronque ses segments en préfixes de tailles approximativement égales afin que la séquence compressée s'adapte exactement à 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 réutilisable SavedModel .

Synopsis d'utilisation

encoder = hub.load("path/to/encoder")
encoder_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 séquence_output d'une manière pouvant être entraînée.
  • "default" , comme l'exige l'API pour les intégrations de texte avec des entrées prétraitées : un tenseur float32 de forme [batch_size, dim] avec l'intégration de chaque séquence d'entrée. (Cela pourrait être juste un alias de pooled_output.)

Le contenu de 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 de 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 comprenant un jeton de début de séquence, des jetons de fin de segment et un remplissage).
  • "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é naissance 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 du périmètre d'une stratégie de distribution, les mêmes règles s'appliquent que dans l'API pour les intégrations de texte avec des entrées prétraitées (voir ci-dessus).

Exemples