Pre-elaborazione BERT con testo TF

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza su GitHub Scarica taccuino

Panoramica

La preelaborazione del testo è la trasformazione end-to-end del testo non elaborato negli input interi di un modello. I modelli NLP sono spesso accompagnati da diverse centinaia (se non migliaia) di righe di codice Python per la preelaborazione del testo. La pre-elaborazione del testo è spesso una sfida per i modelli perché:

  • Inclinazione al servizio della formazione. Diventa sempre più difficile garantire che la logica di pre-elaborazione degli input del modello sia coerente in tutte le fasi dello sviluppo del modello (ad es. pre-addestramento, messa a punto, valutazione, inferenza). L'utilizzo di diversi iperparametri, tokenizzazione, algoritmi di pre-elaborazione delle stringhe o semplicemente impacchettare gli input del modello in modo incoerente in fasi diverse potrebbe produrre effetti disastrosi e difficili da debug per il modello.

  • Efficienza e flessibilità. Sebbene la preelaborazione possa essere eseguita offline (ad esempio scrivendo gli output elaborati su file su disco e quindi riutilizzando i dati preelaborati nella pipeline di input), questo metodo comporta un costo aggiuntivo di lettura e scrittura del file. Anche la pre-elaborazione offline è scomoda se sono presenti decisioni di pre-elaborazione che devono essere eseguite in modo dinamico. Sperimentare con un'opzione diversa richiederebbe di rigenerare nuovamente il set di dati.

  • Interfaccia del modello complesso. I modelli di testo sono molto più comprensibili quando i loro input sono puro testo. È difficile comprendere un modello quando i suoi input richiedono un passaggio di codifica indiretto aggiuntivo. La riduzione della complessità della pre-elaborazione è particolarmente apprezzata per il debug, il servizio e la valutazione del modello.

Inoltre, le interfacce del modello più semplici rendono anche più conveniente provare il modello (ad es. inferenza o addestramento) su insiemi di dati diversi e inesplorati.

Pre-elaborazione del testo con TF.Text

Utilizzando le API di preelaborazione del testo di TF.Text, possiamo costruire una funzione di preelaborazione in grado di trasformare il set di dati di testo di un utente negli input interi del modello. Gli utenti possono impacchettare la pre-elaborazione direttamente come parte del loro modello per alleviare i problemi sopra menzionati.

Questo tutorial vi mostrerà come utilizzare tf.text ops pre-elaborazione per trasformare i dati di testo in input per il modello BERT e ingressi per la lingua di mascheramento pretraining compito descritto in "Masked LM e procedura mascheramento" del BERT: Pre-training di profonda bidirezionale Trasformatori di Lingua comprensione . Il processo prevede la tokenizzazione del testo in unità di sottoparole, la combinazione di frasi, il taglio del contenuto a una dimensione fissa e l'estrazione di etichette per l'attività di modellazione del linguaggio mascherato.

Impostare

Importiamo prima i pacchetti e le librerie di cui abbiamo bisogno.

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

I nostri dati contiene due caratteristiche del testo e siamo in grado di creare un esempio tf.data.Dataset . Il nostro obiettivo è quello di creare una funzione che possiamo fornire Dataset.map() con da utilizzare in allenamento.

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.'>}

Tokenizzazione

Il nostro primo passo è eseguire qualsiasi pre-elaborazione di stringhe e tokenizzare il nostro set di dati. Questo può essere fatto utilizzando la text.BertTokenizer , che è un text.Splitter che può tokenize frasi in sottoparole o wordpieces per il modello di BERT dato un vocabolario generato dal algoritmo di Wordpiece . È possibile saperne di più su altri tokenizers disponibili in tf.text Subword da qui .

Il vocabolario può provenire da un checkpoint BERT generato in precedenza oppure puoi generarne uno tu stesso sui tuoi dati. Ai fini di questo esempio, creiamo un vocabolario giocattolo:

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

Costrutto di Let un text.BertTokenizer utilizzando il vocabolario sopra e tokenize ingressi di testo in 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']]]>

Output di testo da text.BertTokenizer ci permette di vedere come il testo viene tokenizzato, ma il modello richiede interi ID. Possiamo impostare il token_out_type param per tf.int64 avere integer ID (che sono gli indici nella vocabolario).

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 restituisce un RaggedTensor di forma [batch, num_tokens, num_wordpieces] . Perché non abbiamo bisogno l'extra num_tokens dimensioni per il nostro caso d'uso corrente, siamo in grado di unire le ultime due dimensioni per ottenere un RaggedTensor di forma [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]]>

Ritaglio del contenuto

L'input principale a BERT è una concatenazione di due frasi. Tuttavia, BERT richiede che gli input siano di dimensioni e forma fisse e potremmo avere contenuti che superano il nostro budget.

Siamo in grado di affrontare questo utilizzando un text.Trimmer per tagliare il nostro basso contenuto di una dimensione predeterminata (una volta concatenato lungo l'ultimo asse). Ci sono diversi text.Trimmer tipi che selezionano i contenuti di preservare utilizzando algoritmi differenti. text.RoundRobinTrimmer per esempio assegnerà contingente ugualmente per ogni segmento, ma può tagliare le estremità di frasi. text.WaterfallTrimmer taglierà a partire dalla fine dell'ultima frase.

Per il nostro esempio, useremo RoundRobinTrimmer che seleziona articoli da ciascun segmento in modo da sinistra a destra.

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 ora contiene i segmenti in cui il numero di elementi attraverso un batch è 8 elementi (quando concatenate lungo l'asse = -1).

Combinazione di segmenti

Ora che abbiamo segmenti rifilato, li possiamo combinare insieme per ottenere un unico RaggedTensor . BERT utilizza gettoni speciali per indicare l'inizio ( [CLS] ) e alla fine di un segmento ( [SEP] ). Abbiamo anche bisogno di un RaggedTensor indicando quali elementi nella combinata Tensor appartenere a quale segmento. Possiamo usare text.combine_segments() per ottenere entrambi questi Tensor con gettoni speciali inseriti.

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

Compito del modello di linguaggio mascherato

Ora che abbiamo i nostri ingressi di base, possiamo cominciare a estrarre gli input necessari per la "Masked LM e procedura di mascheratura" incarico di cui BERT: Pre-training di profonda bidirezionale Transformers per la comprensione del linguaggio

L'attività del modello di linguaggio mascherato ha due problemi secondari a cui pensare: (1) quali elementi selezionare per il mascheramento e (2) quali valori vengono assegnati?

Selezione articolo

Perché noi sceglieremo di selezionare gli elementi in modo casuale per il mascheramento, useremo un text.RandomItemSelector . RandomItemSelector seleziona casualmente gli articoli di un lotto soggetta a limitazioni poste ( max_selections_per_batch , selection_rate e unselectable_ids ) e ritorna una maschera booleano che indica che sono stati selezionati elementi.

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

La scelta del valore mascherato

La metodologia descritta nel documento BERT originale per la scelta del valore per il mascheramento è la seguente:

Per mask_token_rate del tempo, sostituire l'articolo con la [MASK] token:

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

Per random_token_rate del tempo, sostituire l'articolo con una parola a caso:

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

Per 1 - mask_token_rate - random_token_rate del tempo, mantenere invariata la voce:

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

text.MaskedValuesChooser incapsula questa logica e può essere utilizzato per la nostra funzione di pre-elaborazione. Ecco un esempio di ciò che MaskValuesChooser rendimenti dato un mask_token_rate del 80% e di default 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]]>

Quando alimentato con una RaggedTensor ingresso, text.MaskValuesChooser restituisce un RaggedTensor della stessa forma sia con _MASK_VALUE (0), un ID casuale, o lo stesso ID invariato.

Generazione di input per l'attività del modello di linguaggio mascherato

Ora che abbiamo un RandomItemSelector di aiuto a selezionare gli elementi per mascheramento e text.MaskValuesChooser per assegnare i valori, possiamo usare text.mask_language_model() per assemblare tutti gli ingressi di questo compito per il nostro modello 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.

Dive di Let più profondo ed esaminare le uscite di mask_language_model() . L'uscita di masked_token_ids è:

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

Ricorda che il nostro input è codificato usando un vocabolario. Se noi decodificare masked_token_ids utilizzando il nostro vocabolario, otteniamo:

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

Si noti che alcuni gettoni wordpiece sono stati sostituiti con uno [MASK] , [RANDOM] o un valore ID diverso. masked_pos uscita ci dà gli indici (nel rispettivo lotto) dei gettoni che sono stati sostituiti.

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

masked_lm_ids ci dà il valore originale del token.

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

Possiamo nuovamente decodificare gli ID qui per ottenere valori leggibili dall'uomo.

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

Ingressi modello di riempimento

Ora che abbiamo tutti gli ingressi per il nostro modello, l'ultimo passo nel nostro preelaborazione è imballarli in fisso 2-dimensionale Tensor s con imbottitura e anche generare una maschera Tensor indicando i valori che sono valori pad. Possiamo usare text.pad_model_inputs() per aiutarci con questo compito.

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

Revisione

Rivediamo ciò che abbiamo finora e assembliamo la nostra funzione di pre-elaborazione. Ecco cosa abbiamo:

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

Abbiamo già costruito un tf.data.Dataset e ora possiamo usare il nostro assemblati pre-elaborazione funzione bert_pretrain_preprocess() in Dataset.map() . Questo ci consente di creare una pipeline di input per trasformare i nostri dati di stringa non elaborati in input interi e alimentarli direttamente nel nostro modello.

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