API de SavedModel comunes para tareas de texto

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

Descripción general

Existen 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 SavedModel que asigna un lote de cadenas a un lote de vectores de incrustación. Esto es muy fácil de usar y muchos modelos de 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 SavedModels 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 que las entradas se preprocesen de forma asíncrona antes de introducirlas en el bucle 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 al 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 contextuales de tokens individuales.

En cada caso, las entradas de texto son cadenas codificadas en UTF-8, normalmente 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 incrustación de texto son adecuados para todos los problemas.

Incrustación de texto a partir de 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 forma de cadena [batch_size] y las asigna a un tensor de forma flotante32 [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 de SavedModel reutilizable 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 los 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 incrustación de texto se utiliza 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", ...) , resp., debe suceder 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

Incrustaciones de texto con entradas preprocesadas

Una incrustación de texto con entradas preprocesadas se implementa mediante dos SavedModels separados:

  • 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 incorporaciones 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 qué tensores adicionales (si los hay) además del "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 de SavedModel reutilizable que ejecutar el codificador en modo de entrenamiento (por ejemplo, para abandono) puede requerir un argumento de palabra clave encoder(..., training=True) , y ese encoder proporciona atributos .variables , .trainable_variables y .regularization_losses según corresponda. .

El modelo preprocessor puede tener .variables , pero no está diseñado para entrenarse 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 poder colocar sus variables (si las hay). ) en la CPU del host.

Ejemplos

Incrustaciones de texto con codificadores Transformer

Los codificadores 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 individuales y pares de segmentos.

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

Preprocesador

Un SavedModel de preprocesador para incrustaciones de texto con codificadores Transformer implementa la API de un SavedModel de preprocesador para incrustaciones de texto con entradas preprocesadas (ver arriba), lo 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 vinculació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 se puede expresar 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 produce la 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 acomodar el límite de longitud de secuencia que se aplicará al empaquetar las entradas del codificador. Las dimensiones adicionales en la salida del tokenizador pueden ayudar aquí (por ejemplo, respetar los límites de las palabras), pero pierden sentido en el siguiente paso.

En términos de la API reutilizable SavedModel , el objeto preprocessor.tokenize puede tener .variables pero no debe entrenarse 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 (en 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 RaggedTensor int32 de forma [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(). (Esto último es sólo por conveniencia; las dimensiones adicionales se aplanan antes de empacar).

El embalaje agrega tokens especiales alrededor de los segmentos de entrada como lo espera 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 de los cuales termina con un token de fin de segmento. simbólico. Las posiciones restantes hasta seq_length, si las hay, se llenan con tokens de relleno.

Si una secuencia empaquetada excede seq_length, bert_pack_inputs() trunca sus segmentos a prefijos de tamaños aproximadamente iguales 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 finos.

Codificador

El codificador se llama mediante el dictado de encoder_inputs de la misma manera que en la API para incrustaciones de texto con entradas preprocesadas (ver arriba), incluidas las disposiciones de la API reutilizable SavedModel .

Sinopsis de uso

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

o equivalente en Keras:

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

Detalles

Las 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 consciente del 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 secuencia_output de alguna manera entrenable.
  • "default" , según 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).

Esta definición de API no requiere estrictamente el contenido de encoder_inputs . Sin embargo, para los codificadores que usan entradas de estilo BERT, se recomienda usar los siguientes nombres (del kit de herramientas de modelado NLP 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 fin 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 origen 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 fin de segmento. El segundo segmento y los posteriores (si están presentes) incluyen su respectivo token de fin de segmento. Los tokens de relleno vuelven a obtener el índice 0.

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 arriba).

Ejemplos