Preprocesamiento BERT con texto TF

Ver en TensorFlow.org Ejecutar en Google Colab Ver en GitHub Descargar cuaderno

Descripción general

El preprocesamiento de texto es la transformación de un extremo a otro de texto sin formato en entradas enteras de un modelo. Los modelos de PNL suelen ir acompañados de varios cientos (si no miles) de líneas de código Python para preprocesar texto. El preprocesamiento de texto suele ser un desafío para los modelos porque:

  • Sesgo entrenamiento-servicio. Se vuelve cada vez más difícil garantizar que la lógica de preprocesamiento de las entradas del modelo sea coherente en todas las etapas del desarrollo del modelo (por ejemplo, preentrenamiento, ajuste fino, evaluación, inferencia). El uso de diferentes hiperparámetros, tokenización, algoritmos de preprocesamiento de cadenas o simplemente empaquetar entradas del modelo de manera inconsistente en diferentes etapas podría producir efectos desastrosos y difíciles de depurar en el modelo.

  • Eficiencia y flexibilidad. Si bien el preprocesamiento se puede realizar fuera de línea (por ejemplo, escribiendo las salidas procesadas en archivos en el disco y luego volviendo a consumir dichos datos preprocesados ​​en la canalización de entrada), este método incurre en un costo adicional de lectura y escritura de archivos. El preprocesamiento fuera de línea también es un inconveniente si hay decisiones de preprocesamiento que deben realizarse de forma dinámica. Experimentar con una opción diferente requeriría volver a regenerar el conjunto de datos.

  • Interfaz de modelo complejo. Los modelos de texto son mucho más comprensibles cuando sus entradas son texto puro. Es difícil entender un modelo cuando sus entradas requieren un paso de codificación indirecto adicional. Se agradece especialmente la reducción de la complejidad del preprocesamiento para la depuración, el servicio y la evaluación de modelos.

Además, las interfaces de modelo más simples también hacen que sea más conveniente probar el modelo (por ejemplo, inferencia o entrenamiento) en diferentes conjuntos de datos inexplorados.

Preprocesamiento de texto con TF.Text

Usando las API de preprocesamiento de texto de TF.Text, podemos construir una función de preprocesamiento que puede transformar el conjunto de datos de texto de un usuario en las entradas enteras del modelo. Los usuarios pueden empaquetar el preprocesamiento directamente como parte de su modelo para aliviar los problemas mencionados anteriormente.

Este tutorial le mostrará cómo utilizar TF.Text operaciones de pre-procesamiento para transformar datos de texto en las entradas para el modelo BERT y entradas para el lenguaje de enmascaramiento pre-entrenamiento tarea se describe en "enmascarado LM y procedimiento de enmascaramiento" de BERT: Pre-entrenamiento de profunda bidireccionales Transformadores para Idioma la comprensión . El proceso implica convertir el texto en fichas en unidades de subpalabras, combinar oraciones, recortar el contenido a un tamaño fijo y extraer etiquetas para la tarea de modelado del lenguaje enmascarado.

Configuración

Primero importemos los paquetes y las bibliotecas que necesitamos.

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

Nuestros datos contiene dos características del texto y podemos crear un ejemplo tf.data.Dataset . Nuestro objetivo es crear una función que podemos suministrar Dataset.map() con el que se utilizará en la formación.

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

Tokenización

Nuestro primer paso es ejecutar cualquier preprocesamiento de cadenas y tokenizar nuestro conjunto de datos. Esto se puede hacer usando el text.BertTokenizer , que es un text.Splitter que pueden tokenize oraciones en palabras parciales o wordpieces para el modelo BERT dado un vocabulario generado a partir del algoritmo Wordpiece . Usted puede aprender más sobre otros tokenizers palabra parcial disponibles en TF.Text desde aquí .

El vocabulario puede provenir de un punto de control BERT generado previamente, o puede generar uno usted mismo con sus propios datos. Para los propósitos de este ejemplo, creemos un vocabulario de juguetes:

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

Constructo de dejar que un text.BertTokenizer utilizando el vocabulario arriba y tokenize las entradas de texto en 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']]]>

Los resultados de texto de text.BertTokenizer nos permite ver cómo se está tokenized el texto, pero el modelo requiere enteros identificaciones. Podemos establecer la token_out_type PARAM a tf.int64 obtener número entero IDs (que son los índices en el vocabulario).

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 devuelve un RaggedTensor con forma [batch, num_tokens, num_wordpieces] . Debido a que no es necesario el extra num_tokens dimensiones para nuestro caso de uso corriente, podemos combinar las dos últimas dimensiones para obtener una RaggedTensor con 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]]>

Recorte de contenido

La entrada principal de BERT es una concatenación de dos oraciones. Sin embargo, BERT requiere que los insumos tengan un tamaño y una forma fijos y es posible que tengamos contenido que exceda nuestro presupuesto.

Podemos hacer frente a este mediante el uso de un text.Trimmer para recortar nuestra abajo el contenido de un tamaño predeterminado (una vez concatenado a lo largo del último eje). Hay diferentes text.Trimmer tipos de contenido que seleccionan para preservar el uso de diferentes algoritmos. text.RoundRobinTrimmer por ejemplo asignará cuota igual para cada segmento, pero puede recortar los extremos de oraciones. text.WaterfallTrimmer recortará a partir del final de la última frase.

Para nuestro ejemplo, vamos a utilizar RoundRobinTrimmer que selecciona los artículos en cada segmento de forma de izquierda a derecha.

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 ahora contiene los segmentos en los que el número de elementos a través de un lote es de 8 elementos (cuando concatenado lo largo del eje = -1).

Combinar segmentos

Ahora que hemos recortado segmentos, podemos combinarlos para obtener una única RaggedTensor . BERT utiliza fichas especiales para indicar el comienzo ( [CLS] ) y el final de un segmento ( [SEP] ). También necesitamos un RaggedTensor indicando qué elementos en el combinado Tensor pertenecen a cada segmento. Podemos utilizar text.combine_segments() para conseguir ambas cosas Tensor con fichas especiales insertados.

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

Tarea de modelo de lenguaje enmascarado

Ahora que tenemos nuestros insumos básicos, podemos empezar a extraer los insumos necesarios para el "enmascarado LM y procedimiento de enmascaramiento" tarea describe en el BERT: Pre-entrenamiento de profunda bidireccionales Transformadores para la comprensión del lenguaje

La tarea del modelo de lenguaje enmascarado tiene dos subproblemas en los que debemos pensar: (1) qué elementos seleccionar para enmascarar y (2) ¿qué valores se les asignan?

Selección de artículos

Debido a que vamos a elegir para seleccionar elementos al azar para el enmascaramiento, vamos a utilizar una text.RandomItemSelector . RandomItemSelector selecciona aleatoriamente artículos en un lote sujeto a las restricciones dadas ( max_selections_per_batch , selection_rate y unselectable_ids ) y devuelve una máscara booleano que indica que se seleccionaron artículos.

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

Elegir el valor enmascarado

La metodología descrita en el documento BERT original para elegir el valor de enmascaramiento es la siguiente:

Para mask_token_rate del tiempo, sustituir el elemento con el [MASK] token:

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

Para random_token_rate del tiempo, sustituir el artículo con una palabra al azar:

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

Para 1 - mask_token_rate - random_token_rate del tiempo, mantener el tema sin cambios:

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

text.MaskedValuesChooser encapsula esta lógica y se puede utilizar para nuestra función de pre-procesamiento. He aquí un ejemplo de lo que MaskValuesChooser vuelve dan un mask_token_rate del 80% y por defecto 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]]>

Cuando se suministra con un RaggedTensor de entrada, text.MaskValuesChooser devuelve un RaggedTensor de la misma forma, ya sea con _MASK_VALUE (0), un ID aleatorio, o el mismo ID sin cambios.

Generación de entradas para la tarea del modelo de lenguaje enmascarado

Ahora que tenemos un RandomItemSelector que ayuda a seleccionar artículos para el enmascaramiento y text.MaskValuesChooser para asignar los valores, podemos utilizar text.mask_language_model() para ensamblar todas las entradas de esta tarea para nuestro modelo 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.

Vamos inmersión más profunda y examinar las salidas de mask_language_model() . La salida del masked_token_ids es:

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

Recuerde que nuestra entrada está codificada usando un vocabulario. Si desciframos masked_token_ids utilizando nuestro vocabulario, obtenemos:

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

Tenga en cuenta que algunas fichas wordpiece han sido reemplazados con cualquiera de [MASK] , [RANDOM] o un valor de ID diferente. masked_pos salida nos da los índices (en el respectivo lote) de las fichas que han sido reemplazados.

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

masked_lm_ids nos da el valor original de la ficha.

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

Podemos decodificar nuevamente los ID aquí para obtener valores legibles por humanos.

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

Entradas del modelo de relleno

Ahora que tenemos todas las entradas para nuestro modelo, el último paso en nuestra pre-procesamiento es fijo empaquetarlos en 2 dimensiones Tensor s con el acolchado y también generar una máscara Tensor indicando los valores que son valores del relleno. Podemos utilizar text.pad_model_inputs() para ayudarnos con esta tarea.

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

Revisar

Repasemos lo que tenemos hasta ahora y montemos nuestra función de preprocesamiento. Esto es lo que tenemos:

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

Previamente se construyó una tf.data.Dataset y ahora podemos utilizar nuestra función de pre-procesamiento montado bert_pretrain_preprocess() en Dataset.map() . Esto nos permite crear una canalización de entrada para transformar nuestros datos de cadena sin procesar en entradas enteras y alimentar directamente a nuestro modelo.

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]])>}
  • Clasifica texto con BERT - Un tutorial sobre cómo utilizar un modelo BERT pretrained al texto clasificar. Este es un buen seguimiento ahora que está familiarizado con cómo preprocesar las entradas utilizadas por el modelo BERT.

  • Tokenizar con texto TF - Tutorial que detalla los diferentes tipos de tokenizers que existen en TF.Text.

  • Manejo de Texto RaggedTensor - guía detallada sobre cómo crear, utilizar y manipular RaggedTensor s.