Prétraitement BERT avec texte TF

Voir sur TensorFlow.org Exécuter dans Google Colab Voir sur GitHub Télécharger le cahier

Aperçu

Le prétraitement de texte est la transformation de bout en bout du texte brut en entrées entières d'un modèle. Les modèles NLP sont souvent accompagnés de plusieurs centaines (voire milliers) de lignes de code Python pour le prétraitement du texte. Le prétraitement du texte est souvent un défi pour les modèles car :

  • Asymétrie au service de la formation. Il devient de plus en plus difficile de s'assurer que la logique de prétraitement des entrées du modèle est cohérente à toutes les étapes du développement du modèle (par exemple, pré-apprentissage, réglage fin, évaluation, inférence). L'utilisation de différents hyperparamètres, de la tokenisation, d'algorithmes de prétraitement de chaînes ou simplement d'un emballage incohérent des entrées de modèle à différentes étapes pourrait entraîner des effets difficiles à déboguer et désastreux pour le modèle.

  • Efficacité et flexibilité. Alors que le prétraitement peut être effectué hors ligne (par exemple en écrivant les sorties traitées dans des fichiers sur le disque, puis en réutilisant lesdites données prétraitées dans le pipeline d'entrée), cette méthode entraîne un coût supplémentaire de lecture et d'écriture de fichier. Le prétraitement hors ligne est également gênant si des décisions de prétraitement doivent être prises de manière dynamique. Expérimenter une autre option nécessiterait de régénérer à nouveau l'ensemble de données.

  • Interface de modèle complexe. Les modèles de texte sont beaucoup plus compréhensibles lorsque leurs entrées sont du texte pur. Il est difficile de comprendre un modèle lorsque ses entrées nécessitent une étape d'encodage indirecte supplémentaire. La réduction de la complexité du prétraitement est particulièrement appréciée pour le débogage, la diffusion et l'évaluation des modèles.

De plus, des interfaces de modèle plus simples facilitent également l'essai du modèle (par exemple, inférence ou formation) sur différents ensembles de données inexplorés.

Prétraitement de texte avec TF.Text

En utilisant les API de prétraitement de texte de TF.Text, nous pouvons construire une fonction de prétraitement qui peut transformer l'ensemble de données de texte d'un utilisateur en entrées entières du modèle. Les utilisateurs peuvent emballer le prétraitement directement dans le cadre de leur modèle pour atténuer les problèmes mentionnés ci-dessus.

Ce tutoriel va vous montrer comment utiliser TF.Text opérations de pré - traitement pour transformer les données de texte en entrées pour le modèle BERT et entrées pour tâche préformation de masquage langage décrit dans « Masked LM et Masking Procédure » de BERT: Pré-formation des transformateurs profonds Bidirectionnel pour la langue comprendre . Le processus implique la segmentation du texte en unités de sous-mots, la combinaison de phrases, le découpage du contenu à une taille fixe et l'extraction d'étiquettes pour la tâche de modélisation du langage masqué.

Installer

Importons d'abord les packages et les bibliothèques dont nous avons besoin.

pip install -q -U tensorflow-text
import tensorflow as tf
import tensorflow_text as text
import functools

Nos données contient deux éléments de texte et nous pouvons créer un exemple tf.data.Dataset . Notre objectif est de créer une fonction que nous pouvons fournir Dataset.map() avec à utiliser dans la formation.

examples = {
    "text_a": [
      b"Sponge bob Squarepants is an Avenger",
      b"Marvel Avengers"
    ],
    "text_b": [
     b"Barack Obama is the President.",
     b"President is the highest office"
  ],
}

dataset = tf.data.Dataset.from_tensor_slices(examples)
next(iter(dataset))
{'text_a': <tf.Tensor: shape=(), dtype=string, numpy=b'Sponge bob Squarepants is an Avenger'>,
 'text_b': <tf.Tensor: shape=(), dtype=string, numpy=b'Barack Obama is the President.'>}

Tokenisation

Notre première étape consiste à exécuter n'importe quel prétraitement de chaîne et à tokeniser notre ensemble de données. Cela peut être fait en utilisant la text.BertTokenizer , qui est un text.Splitter qui peut tokenizer phrases en sous - mots ou wordpieces pour le modèle BERT donné un vocabulaire généré par l' algorithme Wordpiece . Vous pouvez en savoir plus sur les autres tokenizers de sous - mots disponibles dans TF.Text d' ici .

Le vocabulaire peut provenir d'un point de contrôle BERT précédemment généré, ou vous pouvez en générer un vous-même sur vos propres données. Pour les besoins de cet exemple, créons un vocabulaire de jouets :

_VOCAB = [
    # Special tokens
    b"[UNK]", b"[MASK]", b"[RANDOM]", b"[CLS]", b"[SEP]",
    # Suffixes
    b"##ack", b"##ama", b"##ger", b"##gers", b"##onge", b"##pants",  b"##uare",
    b"##vel", b"##ven", b"an", b"A", b"Bar", b"Hates", b"Mar", b"Ob",
    b"Patrick", b"President", b"Sp", b"Sq", b"bob", b"box", b"has", b"highest",
    b"is", b"office", b"the",
]

_START_TOKEN = _VOCAB.index(b"[CLS]")
_END_TOKEN = _VOCAB.index(b"[SEP]")
_MASK_TOKEN = _VOCAB.index(b"[MASK]")
_RANDOM_TOKEN = _VOCAB.index(b"[RANDOM]")
_UNK_TOKEN = _VOCAB.index(b"[UNK]")
_MAX_SEQ_LEN = 8
_MAX_PREDICTIONS_PER_BATCH = 5

_VOCAB_SIZE = len(_VOCAB)

lookup_table = tf.lookup.StaticVocabularyTable(
    tf.lookup.KeyValueTensorInitializer(
      keys=_VOCAB,
      key_dtype=tf.string,
      values=tf.range(
          tf.size(_VOCAB, out_type=tf.int64), dtype=tf.int64),
      value_dtype=tf.int64),
      num_oov_buckets=1
)

Regardons les choses en construire un text.BertTokenizer en utilisant le vocabulaire ci - dessus et tokenizer les entrées de texte dans un RaggedTensor .`.

bert_tokenizer = text.BertTokenizer(lookup_table, token_out_type=tf.string)
bert_tokenizer.tokenize(examples["text_a"])
<tf.RaggedTensor [[[b'Sp', b'##onge'], [b'bob'], [b'Sq', b'##uare', b'##pants'], [b'is'], [b'an'], [b'A', b'##ven', b'##ger']], [[b'Mar', b'##vel'], [b'A', b'##ven', b'##gers']]]>
bert_tokenizer.tokenize(examples["text_b"])
<tf.RaggedTensor [[[b'Bar', b'##ack'], [b'Ob', b'##ama'], [b'is'], [b'the'], [b'President'], [b'[UNK]']], [[b'President'], [b'is'], [b'the'], [b'highest'], [b'office']]]>

Sortie texte de text.BertTokenizer nous permet de voir comment le texte est segmenté, mais le modèle nécessite entier ID. Nous pouvons définir le token_out_type param à tf.int64 obtenir entier ID (qui sont les indices dans le vocabulaire).

bert_tokenizer = text.BertTokenizer(lookup_table, token_out_type=tf.int64)
segment_a = bert_tokenizer.tokenize(examples["text_a"])
segment_a
<tf.RaggedTensor [[[22, 9], [24], [23, 11, 10], [28], [14], [15, 13, 7]], [[18, 12], [15, 13, 8]]]>
segment_b = bert_tokenizer.tokenize(examples["text_b"])
segment_b
<tf.RaggedTensor [[[16, 5], [19, 6], [28], [30], [21], [0]], [[21], [28], [30], [27], [29]]]>

text.BertTokenizer renvoie un RaggedTensor avec la forme [batch, num_tokens, num_wordpieces] . Parce que nous ne avons pas besoin les supplémentaires num_tokens dimensions pour notre cas en cours d'utilisation, nous pouvons fusionner les deux dernières dimensions pour obtenir un RaggedTensor avec la forme [batch, num_wordpieces] :

segment_a = segment_a.merge_dims(-2, -1)
segment_a
<tf.RaggedTensor [[22, 9, 24, 23, 11, 10, 28, 14, 15, 13, 7], [18, 12, 15, 13, 8]]>
segment_b = segment_b.merge_dims(-2, -1)
segment_b
<tf.RaggedTensor [[16, 5, 19, 6, 28, 30, 21, 0], [21, 28, 30, 27, 29]]>

Découpage du contenu

L'entrée principale de BERT est une concaténation de deux phrases. Cependant, BERT exige que les intrants soient de taille et de forme fixes et nous pouvons avoir un contenu qui dépasse notre budget.

Nous pouvons aborder cela en utilisant un text.Trimmer pour couper notre contenu vers le bas à une taille prédéterminée (une fois concaténé le long du dernier axe). Il existe différents text.Trimmer types qui choisissent le contenu de préserver en utilisant des algorithmes différents. text.RoundRobinTrimmer par exemple allouera quota également pour chaque segment , mais peut couper les extrémités des phrases. text.WaterfallTrimmer taillera à partir de la fin de la dernière phrase.

Pour notre exemple, nous utiliserons RoundRobinTrimmer qui sélectionne les éléments de chaque segment d'une manière de gauche à droite.

trimmer = text.RoundRobinTrimmer(max_seq_length=[_MAX_SEQ_LEN])
trimmed = trimmer.trim([segment_a, segment_b])
trimmed
[<tf.RaggedTensor [[22, 9, 24, 23], [18, 12, 15, 13]]>,
 <tf.RaggedTensor [[16, 5, 19, 6], [21, 28, 30, 27]]>]

trimmed contient maintenant les segments où le nombre d'éléments à travers un lot est de 8 éléments (lorsque concaténée le long de l' axe = -1).

Combiner des segments

Maintenant que nous avons des segments parés, nous pouvons les combiner ensemble pour obtenir un seul RaggedTensor . BERT utilise des jetons spéciaux pour indiquer le début ( [CLS] ) et à la fin d'un segment ( [SEP] ). Nous avons également besoin d' un RaggedTensor indiquant quels éléments de la combinaison Tensor appartiennent à quel segment. Nous pouvons utiliser text.combine_segments() pour obtenir ces deux Tensor avec des jetons spéciaux insérés.

segments_combined, segments_ids = text.combine_segments(
  [segment_a, segment_b],
  start_of_sequence_id=_START_TOKEN, end_of_segment_id=_END_TOKEN)
segments_combined, segments_ids
(<tf.RaggedTensor [[3, 22, 9, 24, 23, 11, 10, 28, 14, 15, 13, 7, 4, 16, 5, 19, 6, 28, 30, 21, 0, 4], [3, 18, 12, 15, 13, 8, 4, 21, 28, 30, 27, 29, 4]]>,
 <tf.RaggedTensor [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]]>)

Tâche de modèle de langage masqué

Maintenant que nous avons nos entrées de base, nous pouvons commencer à extraire les intrants nécessaires à la « Masked LM et Masking procédure » tâche décrite dans BERT: Pré-formation des transformateurs pour la compréhension profonde Bidirectionnel Langue

La tâche du modèle de langage masqué comporte deux sous-problèmes auxquels nous devons réfléchir : (1) quels éléments sélectionner pour le masquage et (2) quelles valeurs leur sont-elles attribuées ?

Sélection d'articles

Parce que nous allons choisir de sélectionner des éléments au hasard pour le masquage, nous utiliserons un text.RandomItemSelector . RandomItemSelector sélectionne de manière aléatoire des éléments dans un objet de traitement par lots à des restrictions données ( max_selections_per_batch , selection_rate et unselectable_ids ) et renvoie un masque booléenne indiquant quels éléments ont été sélectionnés.

random_selector = text.RandomItemSelector(
    max_selections_per_batch=_MAX_PREDICTIONS_PER_BATCH,
    selection_rate=0.2,
    unselectable_ids=[_START_TOKEN, _END_TOKEN, _UNK_TOKEN]
)
selected = random_selector.get_selection_mask(
    segments_combined, axis=1)
selected
<tf.RaggedTensor [[False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, True, True, True, False, False], [False, False, False, False, False, True, False, False, False, False, False, True, False]]>

Choix de la valeur masquée

La méthodologie décrite dans l'article original du BERT pour choisir la valeur de masquage est la suivante :

Pour mask_token_rate du temps, remplacer l'élément avec le [MASK] jeton:

"my dog is hairy" -> "my dog is [MASK]"

Pour random_token_rate du temps, remplacer l'élément avec un mot au hasard:

"my dog is hairy" -> "my dog is apple"

Pour 1 - mask_token_rate - random_token_rate du temps, garder l'élément inchangé:

"my dog is hairy" -> "my dog is hairy."

text.MaskedValuesChooser encapsule cette logique et peut être utilisé pour notre fonction de pré - traitement. Voici un exemple de ce que MaskValuesChooser rendement donné une mask_token_rate de 80% et par défaut random_token_rate :

input_ids = tf.ragged.constant([[19, 7, 21, 20, 9, 8], [13, 4, 16, 5], [15, 10, 12, 11, 6]])
mask_values_chooser = text.MaskValuesChooser(_VOCAB_SIZE, _MASK_TOKEN, 0.8)
mask_values_chooser.get_mask_values(input_ids)
<tf.RaggedTensor [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1], [1, 10, 1, 1, 6]]>

Lorsqu'il est fourni avec une RaggedTensor entrée, text.MaskValuesChooser renvoie un RaggedTensor de la même forme avec soit _MASK_VALUE (0), un identifiant aléatoire, ou le même identifiant inchangé.

Génération d'entrées pour la tâche de modèle de langage masqué

Maintenant que nous avons un RandomItemSelector pour nous aider à sélectionner des éléments pour masquer et text.MaskValuesChooser pour affecter les valeurs, nous pouvons utiliser text.mask_language_model() pour assembler toutes les entrées de cette tâche pour notre modèle BERT.

masked_token_ids, masked_pos, masked_lm_ids = text.mask_language_model(
  segments_combined,
  item_selector=random_selector, mask_values_chooser=mask_values_chooser)
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.7/site-packages/tensorflow/python/util/dispatch.py:206: batch_gather (from tensorflow.python.ops.array_ops) is deprecated and will be removed after 2017-10-25.
Instructions for updating:
`tf.batch_gather` is deprecated, please use `tf.gather` with `batch_dims=-1` instead.

La plongée Let profonde et examiner les résultats de mask_language_model() . La sortie de masked_token_ids est:

masked_token_ids
<tf.RaggedTensor [[3, 22, 1, 24, 23, 1, 10, 28, 1, 15, 1, 7, 4, 16, 5, 19, 6, 28, 30, 21, 0, 4], [3, 18, 12, 15, 13, 1, 4, 21, 28, 30, 27, 1, 4]]>

N'oubliez pas que notre entrée est codée à l'aide d'un vocabulaire. Si on décode masked_token_ids en utilisant notre vocabulaire, nous obtenons:

tf.gather(_VOCAB, masked_token_ids)
<tf.RaggedTensor [[b'[CLS]', b'Sp', b'[MASK]', b'bob', b'Sq', b'[MASK]', b'##pants', b'is', b'[MASK]', b'A', b'[MASK]', b'##ger', b'[SEP]', b'Bar', b'##ack', b'Ob', b'##ama', b'is', b'the', b'President', b'[UNK]', b'[SEP]'], [b'[CLS]', b'Mar', b'##vel', b'A', b'##ven', b'[MASK]', b'[SEP]', b'President', b'is', b'the', b'highest', b'[MASK]', b'[SEP]']]>

Notez que certains jetons de wordpiece ont été remplacés par deux [MASK] , [RANDOM] ou une valeur d'identité différente. masked_pos sortie nous donne les indices (dans le lot respectif) des jetons qui ont été remplacés.

masked_pos
<tf.RaggedTensor [[2, 5, 8, 10], [5, 11]]>

masked_lm_ids nous donne la valeur d' origine du jeton.

masked_lm_ids
<tf.RaggedTensor [[9, 11, 14, 13], [8, 29]]>

Nous pouvons à nouveau décoder les identifiants ici pour obtenir des valeurs lisibles par l'homme.

tf.gather(_VOCAB, masked_lm_ids)
<tf.RaggedTensor [[b'##onge', b'##uare', b'an', b'##ven'], [b'##gers', b'office']]>

Remplissage des entrées du modèle

Maintenant que nous avons toutes les entrées pour notre modèle, la dernière étape de notre pré - traitement est de les emballer dans deux dimensions fixe Tensor s avec rembourrage et générer également un masque Tensor indiquant les valeurs qui sont des valeurs de pad. Nous pouvons utiliser text.pad_model_inputs() pour nous aider dans cette tâche.

# Prepare and pad combined segment inputs
input_word_ids, input_mask = text.pad_model_inputs(
  masked_token_ids, max_seq_length=_MAX_SEQ_LEN)
input_type_ids, _ = text.pad_model_inputs(
  masked_token_ids, max_seq_length=_MAX_SEQ_LEN)

# Prepare and pad masking task inputs
masked_lm_positions, masked_lm_weights = text.pad_model_inputs(
  masked_token_ids, max_seq_length=_MAX_PREDICTIONS_PER_BATCH)
masked_lm_ids, _ = text.pad_model_inputs(
  masked_lm_ids, max_seq_length=_MAX_PREDICTIONS_PER_BATCH)

model_inputs = {
    "input_word_ids": input_word_ids,
    "input_mask": input_mask,
    "input_type_ids": input_type_ids,
    "masked_lm_ids": masked_lm_ids,
    "masked_lm_positions": masked_lm_positions,
    "masked_lm_weights": masked_lm_weights,
}
model_inputs
{'input_word_ids': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[ 3, 22,  1, 24, 23,  1, 10, 28],
        [ 3, 18, 12, 15, 13,  1,  4, 21]])>,
 'input_mask': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1]])>,
 'input_type_ids': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[ 3, 22,  1, 24, 23,  1, 10, 28],
        [ 3, 18, 12, 15, 13,  1,  4, 21]])>,
 'masked_lm_ids': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[ 9, 11, 14, 13,  0],
        [ 8, 29,  0,  0,  0]])>,
 'masked_lm_positions': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[ 3, 22,  1, 24, 23],
        [ 3, 18, 12, 15, 13]])>,
 'masked_lm_weights': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1]])>}

Passer en revue

Passons en revue ce que nous avons jusqu'à présent et assemblons notre fonction de prétraitement. Voici ce que nous avons :

def bert_pretrain_preprocess(vocab_table, features):
  # Input is a string Tensor of documents, shape [batch, 1].
  text_a = features["text_a"]
  text_b = features["text_b"]

  # Tokenize segments to shape [num_sentences, (num_words)] each.
  tokenizer = text.BertTokenizer(
      vocab_table,
      token_out_type=tf.int64)
  segments = [tokenizer.tokenize(text).merge_dims(
      1, -1) for text in (text_a, text_b)]

  # Truncate inputs to a maximum length.
  trimmer = text.RoundRobinTrimmer(max_seq_length=6)
  trimmed_segments = trimmer.trim(segments)

  # Combine segments, get segment ids and add special tokens.
  segments_combined, segment_ids = text.combine_segments(
      trimmed_segments,
      start_of_sequence_id=_START_TOKEN,
      end_of_segment_id=_END_TOKEN)

  # Apply dynamic masking task.
  masked_input_ids, masked_lm_positions, masked_lm_ids = (
      text.mask_language_model(
        segments_combined,
        random_selector,
        mask_values_chooser,
      )
  )

  # Prepare and pad combined segment inputs
  input_word_ids, input_mask = text.pad_model_inputs(
    masked_input_ids, max_seq_length=_MAX_SEQ_LEN)
  input_type_ids, _ = text.pad_model_inputs(
    masked_input_ids, max_seq_length=_MAX_SEQ_LEN)

  # Prepare and pad masking task inputs
  masked_lm_positions, masked_lm_weights = text.pad_model_inputs(
    masked_input_ids, max_seq_length=_MAX_PREDICTIONS_PER_BATCH)
  masked_lm_ids, _ = text.pad_model_inputs(
    masked_lm_ids, max_seq_length=_MAX_PREDICTIONS_PER_BATCH)

  model_inputs = {
      "input_word_ids": input_word_ids,
      "input_mask": input_mask,
      "input_type_ids": input_type_ids,
      "masked_lm_ids": masked_lm_ids,
      "masked_lm_positions": masked_lm_positions,
      "masked_lm_weights": masked_lm_weights,
  }
  return model_inputs

Nous avons déjà construit un tf.data.Dataset et nous pouvons maintenant utiliser notre fonction pré - traitement assemblé bert_pretrain_preprocess() dans Dataset.map() . Cela nous permet de créer un pipeline d'entrée pour transformer nos données de chaîne brutes en entrées entières et alimenter directement notre modèle.

dataset = tf.data.Dataset.from_tensors(examples)
dataset = dataset.map(functools.partial(
    bert_pretrain_preprocess, lookup_table))

next(iter(dataset))
{'input_word_ids': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[ 3, 22,  9,  1,  4, 16,  5, 19],
        [ 3, 18,  1, 15,  4,  1, 28, 30]])>,
 'input_mask': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1]])>,
 'input_type_ids': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[ 3, 22,  9,  1,  4, 16,  5, 19],
        [ 3, 18,  1, 15,  4,  1, 28, 30]])>,
 'masked_lm_ids': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[24, 19,  0,  0,  0],
        [12, 21,  0,  0,  0]])>,
 'masked_lm_positions': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[ 3, 22,  9,  1,  4],
        [ 3, 18,  1, 15,  4]])>,
 'masked_lm_weights': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1]])>}
  • Classifier texte avec BERT - Un tutoriel sur la façon d'utiliser un modèle BERT au texte pré - entraîné Classifier. C'est un bon suivi maintenant que vous savez comment prétraiter les entrées utilisées par le modèle BERT.

  • Tokenizing avec TF Texte - Tutoriel détaillant les différents types de tokenizers qui existent dans TF.Text.

  • Manipulation Texte RaggedTensor - Guide détaillé sur la façon de créer, d' utiliser et de manipuler RaggedTensor s.