API de modelos guardados comunes para tareas de texto

Esta página describe cómo TF2 SavedModels para tareas relacionadas con texto debe implementar la API de modelo guardado reutilizable . (Esto reemplaza y amplía las firmas comunes para texto para el formato TF1 Hub, ahora obsoleto).

Descripción general

Hay varias API para calcular incrustaciones de texto (también conocidas como representaciones densas de texto o vectores de características de texto).

  • La API para incrustaciones de texto a partir de entradas de texto se implementa mediante un modelo guardado que asigna un lote de cadenas a un lote de vectores de incrustación. Esto es muy fácil de usar y muchos modelos en TF Hub lo han implementado. Sin embargo, esto no permite ajustar el modelo en TPU.

  • La API para incrustaciones de texto con entradas preprocesadas resuelve la misma tarea, pero se implementa mediante dos modelos guardados separados:

    • un preprocesador que puede ejecutarse dentro de una canalización de entrada tf.data y convierte cadenas y otros datos de longitud variable en tensores numéricos,
    • un codificador que acepta los resultados del preprocesador y realiza la parte entrenable del cálculo de incrustación.

    Esta división permite preprocesar las entradas de forma asincrónica antes de introducirse en el ciclo de entrenamiento. En particular, permite crear codificadores que se pueden ejecutar y ajustar en TPU .

  • La API para incrustaciones de texto con codificadores Transformer extiende la API para incrustaciones de texto desde entradas preprocesadas hasta el caso particular de BERT y otros codificadores Transformer.

    • El preprocesador se amplía para crear entradas de codificador a partir de más de un segmento de texto de entrada.

    • El codificador Transformer expone las incrustaciones sensibles al contexto de tokens individuales.

En cada caso, las entradas de texto son cadenas codificadas en UTF-8, generalmente de texto sin formato, a menos que la documentación del modelo indique lo contrario.

Independientemente de la API, se han entrenado previamente diferentes modelos en texto de diferentes idiomas y dominios, y con diferentes tareas en mente. Por lo tanto, no todos los modelos de inserción de texto son adecuados para todos los problemas.

Incrustación de texto desde entradas de texto

Un modelo guardado para incrustaciones de texto a partir de entradas de texto acepta un lote de entradas en un tensor de cadena de forma [batch_size] y las asigna a un tensor float32 de forma [batch_size, dim] con representaciones densas (vectores de características) de las entradas.

Sinopsis de uso

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

Recuerde de la API Reusable SavedModel que ejecutar el modelo en modo de entrenamiento (por ejemplo, para abandono) puede requerir un argumento de palabra clave obj(..., training=True) , y que obj proporciona atributos .variables , .trainable_variables y .regularization_losses según corresponda .

En Keras, todo esto está a cargo de

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

Entrenamiento distribuido

Si la inserción de texto se usa como parte de un modelo que se entrena con una estrategia de distribución, la llamada a hub.load("path/to/model") o hub.KerasLayer("path/to/model", ...) , respectivamente, debe ocurrir dentro del alcance de DistributionStrategy para poder crear las variables del modelo de forma distribuida. Por ejemplo

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

Ejemplos de

Incrustaciones de texto con entradas preprocesadas

Dos modelos guardados independientes implementan una incrustación de texto con entradas preprocesadas :

  • un preprocesador que asigna un tensor de cadena de forma [batch_size] a un dictado de tensores numéricos,
  • un codificador que acepta un dictado de tensores devuelto por el preprocesador, realiza la parte entrenable del cálculo de incrustación y devuelve un dictado de salidas. La salida bajo la clave "default" es un tensor float32 de forma [batch_size, dim] .

Esto permite ejecutar el preprocesador en una canalización de entrada pero ajustar las incrustaciones calculadas por el codificador como parte de un modelo más grande. En particular, permite crear codificadores que se pueden ejecutar y ajustar en TPU .

Es un detalle de implementación qué tensores están contenidos en la salida del preprocesador y cuáles (si los hay) tensores adicionales además de "default" están contenidos en la salida del codificador.

La documentación del codificador debe especificar qué preprocesador usar con él. Normalmente, hay exactamente una opción correcta.

Sinopsis de uso

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

Recuerde de la API Reusable SavedModel que ejecutar el codificador en modo de entrenamiento (p. Ej., Para abandonar) puede requerir un encoder(..., training=True) argumento de palabra clave encoder(..., training=True) , y que el encoder proporciona atributos .variables , .trainable_variables y .regularization_losses según corresponda .

El modelo de preprocessor puede tener .variables pero no está destinado a ser entrenado más. El preprocesamiento no depende del modo: si preprocessor() tiene un argumento training=... , no tiene ningún efecto.

En Keras, todo esto está a cargo de

encoder_inputs = hub.KerasLayer("path/to/preprocessor")(text_input)
encoder_outputs = hub.KerasLayer("path/to/encoder", trainable=True)(encoder_inputs)
embeddings = encoder_outputs["default"]

Entrenamiento distribuido

Si el codificador se utiliza como parte de un modelo que se entrena con una estrategia de distribución, la llamada a hub.load("path/to/encoder") o hub.KerasLayer("path/to/encoder", ...) , resp., debe suceder dentro

  with strategy.scope():
    ...

para recrear las variables del codificador de forma distribuida.

Del mismo modo, si el preprocesador es parte del modelo entrenado (como en el ejemplo simple anterior), también debe cargarse bajo el alcance de la estrategia de distribución. Sin embargo, si el preprocesador se usa en una canalización de entrada (por ejemplo, en un invocable pasado a tf.data.Dataset.map() ), su carga debe ocurrir fuera del alcance de la estrategia de distribución, para colocar sus variables (si las hay ) en la CPU del host.

Ejemplos de

Incrustaciones de texto con Transformer Encoders

Los codificadores de transformadores para texto operan en un lote de secuencias de entrada, cada secuencia comprende n ≥ 1 segmentos de texto tokenizado, dentro de algún límite específico del modelo en n . Para BERT y muchas de sus extensiones, ese límite es 2, por lo que aceptan segmentos únicos y pares de segmentos.

La API para incrustaciones de texto con codificadores Transformer amplía la API para incrustaciones de texto con entradas preprocesadas para esta configuración.

Preprocesador

Un modelo guardado de preprocesador para incrustaciones de texto con codificadores Transformer implementa la API de un modelo guardado de preprocesador para incrustaciones de texto con entradas preprocesadas (ver más arriba), que proporciona una forma de asignar entradas de texto de un solo segmento directamente a las entradas del codificador.

Además, el preprocesador SavedModel proporciona subobjetos invocables tokenize para la tokenización (por separado por segmento) y bert_pack_inputs para empaquetar n segmentos tokenizados en una secuencia de entrada para el codificador. Cada subobjeto sigue la API de modelo guardado reutilizable .

Sinopsis de uso

Como ejemplo concreto para dos segmentos de texto, veamos una tarea de implicación de oraciones que pregunta si una premisa (primer segmento) implica o no una hipótesis (segundo segmento).

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, este cálculo puede expresarse como

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

Detalles de tokenize

Una llamada a preprocessor.tokenize() acepta un Tensor de cadena de forma [batch_size] y devuelve un RaggedTensor de forma [batch_size, ...] cuyos valores son identificadores de token int32 que representan las cadenas de entrada. Puede haber r ≥ 1 dimensiones irregulares después de batch_size pero ninguna otra dimensión uniforme.

  • Si r = 1, la forma es [batch_size, (tokens)] , y cada entrada simplemente se tokeniza en una secuencia plana de tokens.
  • Si r > 1, hay r -1 niveles adicionales de agrupación. Por ejemplo, tensorflow_text.BertTokenizer usa r = 2 para agrupar tokens por palabras y da forma [batch_size, (words), (tokens_per_word)] . Depende del modelo en cuestión cuántos de estos niveles adicionales existen, si los hay, y qué agrupaciones representan.

El usuario puede (pero no es necesario) modificar las entradas tokenizadas, por ejemplo, para adaptarse al límite de seq_length que se aplicará en el empaquetado de las entradas del codificador. Las dimensiones adicionales en la salida del tokenizador pueden ayudar aquí (por ejemplo, para respetar los límites de las palabras) pero pierden sentido en el siguiente paso.

En términos de la API Reusable SavedModel , el objeto preprocessor.tokenize puede tener .variables pero no está destinado a ser entrenado más. La tokenización no depende del modo: si preprocessor.tokenize() tiene un argumento training=... , no tiene ningún efecto.

Detalles de bert_pack_inputs

Una llamada a preprocessor.bert_pack_inputs() acepta una lista de Python de entradas tokenizadas (por lotes por separado para cada segmento de entrada) y devuelve un dictado de tensores que representan un lote de secuencias de entrada de longitud fija para el modelo de codificador Transformer.

Cada entrada tokenizada es un int32 RaggedTensor de shape [batch_size, ...] , donde el número r de dimensiones irregulares después de batch_size es 1 o el mismo que en la salida de preprocessor.tokenize(). (Este último es solo por conveniencia; las dimensiones adicionales se aplanan antes de empacar).

El empaquetado agrega tokens especiales alrededor de los segmentos de entrada como esperaba el codificador. La llamada bert_pack_inputs() implementa exactamente el esquema de empaquetado utilizado por los modelos BERT originales y muchas de sus extensiones: la secuencia empaquetada comienza con un token de inicio de secuencia, seguido de los segmentos tokenizados, cada uno terminado por un final de segmento simbólico. Las posiciones restantes hasta seq_length, si las hay, se llenan con tokens de relleno.

Si una secuencia empaquetada excedería seq_length, bert_pack_inputs() trunca sus segmentos a prefijos de aproximadamente el mismo tamaño para que la secuencia empaquetada encaje exactamente dentro de seq_length.

El empaquetado no depende del modo: si preprocessor.bert_pack_inputs() tiene un argumento training=... , no tiene ningún efecto. Además, no se espera que preprocessor.bert_pack_inputs tenga variables ni admita ajustes precisos.

Codificador

El codificador se llama en el dict de encoder_inputs de la misma manera que en la API para incrustaciones de texto con entradas preprocesadas (ver más arriba), incluidas las disposiciones de la API Reusable SavedModel .

Sinposis de uso

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

o equivalentemente en Keras:

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

Detalles

Los encoder_outputs son un dictado de tensores con las siguientes claves.

  • "sequence_output" : un tensor float32 de forma [batch_size, seq_length, dim] con la incrustación sensible al contexto de cada token de cada secuencia de entrada empaquetada.
  • "pooled_output" : un tensor float32 de forma [batch_size, dim] con la incrustación de cada secuencia de entrada como un todo, derivada de sequence_output de alguna manera entrenable.
  • "default" , como lo requiere la API para incrustaciones de texto con entradas preprocesadas: un tensor float32 de forma [batch_size, dim] con la incrustación de cada secuencia de entrada. (Esto podría ser solo un alias de pooled_output).

El contenido de encoder_inputs no es estrictamente requerido por esta definición de API. Sin embargo, para los codificadores que utilizan entradas de estilo BERT, se recomienda utilizar los siguientes nombres (del kit de herramientas de modelado de PNL de TensorFlow Model Garden ) para minimizar la fricción al intercambiar codificadores y reutilizar modelos de preprocesador:

  • "input_word_ids" : un tensor int32 de forma [batch_size, seq_length] con los identificadores de token de la secuencia de entrada empaquetada (es decir, incluido un token de inicio de secuencia, tokens de final de segmento y relleno).
  • "input_mask" : un tensor int32 de forma [batch_size, seq_length] con valor 1 en la posición de todos los tokens de entrada presentes antes del relleno y valor 0 para los tokens de relleno.
  • "input_type_ids" : un tensor int32 de forma [batch_size, seq_length] con el índice del segmento de entrada que dio lugar al token de entrada en la posición respectiva. El primer segmento de entrada (índice 0) incluye el token de inicio de secuencia y su token de final de segmento. El segundo y los últimos segmentos (si están presentes) incluyen su respectivo token de final de segmento. Los tokens de relleno obtienen el índice 0 nuevamente.

Entrenamiento distribuido

Para cargar los objetos de preprocesador y codificador dentro o fuera del alcance de una estrategia de distribución, se aplican las mismas reglas que en la API para incrustaciones de texto con entradas preprocesadas (ver más arriba).

Ejemplos de