Cuantificación posterior al entrenamiento

La cuantización posterior al entrenamiento es una técnica de conversión que puede reducir el tamaño del modelo y al mismo tiempo mejorar la latencia del acelerador de hardware y de la CPU, con poca degradación en la precisión del modelo. Puedes cuantizar un modelo flotante de TensorFlow ya entrenado cuando lo conviertes al formato TensorFlow Lite usando TensorFlow Lite Converter .

Métodos de optimización

Hay varias opciones de cuantificación posteriores al entrenamiento para elegir. Aquí hay una tabla resumen de las opciones y los beneficios que brindan:

Técnica Beneficios Hardware
Cuantización de rango dinámico 4 veces más pequeño, aceleración 2x-3x UPC
Cuantización entera completa 4 veces más pequeño, 3 veces más velocidad CPU, Edge TPU, microcontroladores
Cuantización float16 2 veces más pequeño, aceleración de GPU CPU, GPU

El siguiente árbol de decisiones puede ayudar a determinar qué método de cuantificación posterior al entrenamiento es mejor para su caso de uso:

opciones de optimización post-entrenamiento

Cuantización de rango dinámico

La cuantificación del rango dinámico es un punto de partida recomendado porque proporciona un uso reducido de la memoria y un cálculo más rápido sin tener que proporcionar un conjunto de datos representativo para la calibración. Este tipo de cuantificación cuantifica estáticamente solo los pesos de punto flotante a entero en el momento de la conversión, lo que proporciona 8 bits de precisión:

import tensorflow as tf
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_quant_model = converter.convert()

Para reducir aún más la latencia durante la inferencia, los operadores de "rango dinámico" cuantifican dinámicamente las activaciones en función de su rango a 8 bits y realizan cálculos con pesos y activaciones de 8 bits. Esta optimización proporciona latencias cercanas a las inferencias de punto completamente fijo. Sin embargo, las salidas todavía se almacenan utilizando punto flotante, por lo que el aumento de velocidad de las operaciones de rango dinámico es menor que un cálculo completo de punto fijo.

Cuantización entera completa

Puede obtener más mejoras de latencia, reducciones en el uso máximo de memoria y compatibilidad con aceleradores o dispositivos de hardware que solo contienen números enteros asegurándose de que todas las matemáticas del modelo estén cuantificadas con números enteros.

Para una cuantificación entera completa, es necesario calibrar o estimar el rango, es decir, (mínimo, máximo) de todos los tensores de punto flotante en el modelo. A diferencia de los tensores constantes, como los pesos y los sesgos, los tensores variables, como la entrada del modelo, las activaciones (salidas de capas intermedias) y la salida del modelo, no se pueden calibrar a menos que ejecutemos algunos ciclos de inferencia. Como resultado, el convertidor requiere un conjunto de datos representativo para calibrarlos. Este conjunto de datos puede ser un pequeño subconjunto (alrededor de entre 100 y 500 muestras) de los datos de entrenamiento o validación. Consulte la función representative_dataset() a continuación.

Desde la versión TensorFlow 2.7, puede especificar el conjunto de datos representativo mediante una firma como en el siguiente ejemplo:

def representative_dataset():
  for data in dataset:
    yield {
      "image": data.image,
      "bias": data.bias,
    }

Si hay más de una firma en el modelo de TensorFlow dado, puede especificar el conjunto de datos múltiple especificando las claves de firma:

def representative_dataset():
  # Feed data set for the "encode" signature.
  for data in encode_signature_dataset:
    yield (
      "encode", {
        "image": data.image,
        "bias": data.bias,
      }
    )

  # Feed data set for the "decode" signature.
  for data in decode_signature_dataset:
    yield (
      "decode", {
        "image": data.image,
        "hint": data.hint,
      },
    )

Puede generar el conjunto de datos representativo proporcionando una lista de tensores de entrada:

def representative_dataset():
  for data in tf.data.Dataset.from_tensor_slices((images)).batch(1).take(100):
    yield [tf.dtypes.cast(data, tf.float32)]

Desde la versión TensorFlow 2.7, recomendamos utilizar el enfoque basado en firmas en lugar del enfoque basado en listas de tensores de entrada porque el orden de los tensores de entrada se puede invertir fácilmente.

Para fines de prueba, puede utilizar un conjunto de datos ficticio de la siguiente manera:

def representative_dataset():
    for _ in range(100):
      data = np.random.rand(1, 244, 244, 3)
      yield [data.astype(np.float32)]
 

Entero con respaldo flotante (usando entrada/salida flotante predeterminada)

Para cuantificar completamente un modelo con números enteros, pero usar operadores flotantes cuando no tienen una implementación de números enteros (para garantizar que la conversión se produzca sin problemas), siga los siguientes pasos:

import tensorflow as tf
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
tflite_quant_model = converter.convert()

Sólo entero

La creación de modelos solo de números enteros es un caso de uso común de TensorFlow Lite para microcontroladores y Coral Edge TPU .

Además, para garantizar la compatibilidad con dispositivos que solo contienen números enteros (como microcontroladores de 8 bits) y aceleradores (como Coral Edge TPU), puede aplicar la cuantificación de números enteros completos para todas las operaciones, incluidas la entrada y la salida, siguiendo los siguientes pasos:

import tensorflow as tf
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8  # or tf.uint8
converter.inference_output_type = tf.int8  # or tf.uint8
tflite_quant_model = converter.convert()

Cuantización float16

Puede reducir el tamaño de un modelo de coma flotante cuantificando los pesos a float16, el estándar IEEE para números de coma flotante de 16 bits. Para habilitar la cuantificación de pesos float16, siga los siguientes pasos:

import tensorflow as tf
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_types = [tf.float16]
tflite_quant_model = converter.convert()

Las ventajas de la cuantificación float16 son las siguientes:

  • Reduce el tamaño del modelo hasta a la mitad (ya que todos los pesos pasan a ser la mitad de su tamaño original).
  • Provoca una pérdida mínima de precisión.
  • Admite algunos delegados (por ejemplo, el delegado de GPU) que pueden operar directamente con datos float16, lo que resulta en una ejecución más rápida que los cálculos float32.

Las desventajas de la cuantificación float16 son las siguientes:

  • No reduce la latencia tanto como una cuantificación a matemáticas de punto fijo.
  • De forma predeterminada, un modelo cuantificado float16 "descuantificará" los valores de peso a float32 cuando se ejecute en la CPU. (Tenga en cuenta que el delegado de GPU no realizará esta descuantización, ya que puede operar con datos float16).

Solo números enteros: activaciones de 16 bits con pesos de 8 bits (experimental)

Este es un esquema de cuantificación experimental. Es similar al esquema de "solo entero", pero las activaciones se cuantifican según su rango a 16 bits, los pesos se cuantifican en un entero de 8 bits y el sesgo se cuantifica en un entero de 64 bits. Esto se conoce como cuantización 16x8.

La principal ventaja de esta cuantificación es que puede mejorar significativamente la precisión, pero solo aumentar ligeramente el tamaño del modelo.

import tensorflow as tf
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.representative_dataset = representative_dataset
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_ops = [tf.lite.OpsSet.EXPERIMENTAL_TFLITE_BUILTINS_ACTIVATIONS_INT16_WEIGHTS_INT8]
tflite_quant_model = converter.convert()

Si la cuantificación 16x8 no es compatible con algunos operadores del modelo, entonces el modelo aún se puede cuantificar, pero los operadores no compatibles se mantienen en flotación. La siguiente opción debe agregarse a target_spec para permitir esto.

import tensorflow as tf
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.representative_dataset = representative_dataset
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_ops = [tf.lite.OpsSet.EXPERIMENTAL_TFLITE_BUILTINS_ACTIVATIONS_INT16_WEIGHTS_INT8,
tf.lite.OpsSet.TFLITE_BUILTINS]
tflite_quant_model = converter.convert()

Ejemplos de casos de uso en los que las mejoras de precisión proporcionadas por este esquema de cuantificación incluyen:

  • súper resolución,
  • procesamiento de señales de audio como cancelación de ruido y formación de haces,
  • eliminación de ruido de imagen,
  • Reconstrucción HDR a partir de una sola imagen.

La desventaja de esta cuantificación es:

  • Actualmente, la inferencia es notablemente más lenta que la de un entero completo de 8 bits debido a la falta de una implementación optimizada del kernel.
  • Actualmente es incompatible con los delegados TFLite acelerados por hardware existentes.

Puede encontrar un tutorial para este modo de cuantificación aquí .

Precisión del modelo

Dado que los pesos se cuantifican después del entrenamiento, podría haber una pérdida de precisión, especialmente en redes más pequeñas. Se proporcionan modelos completamente cuantificados previamente entrenados para redes específicas en TensorFlow Hub . Es importante verificar la precisión del modelo cuantificado para verificar que cualquier degradación en la precisión esté dentro de los límites aceptables. Existen herramientas para evaluar la precisión del modelo TensorFlow Lite .

Alternativamente, si la caída de la precisión es demasiado alta, considere utilizar un entrenamiento consciente de la cuantificación . Sin embargo, hacerlo requiere modificaciones durante el entrenamiento del modelo para agregar nodos de cuantificación falsos, mientras que las técnicas de cuantificación posteriores al entrenamiento en esta página utilizan un modelo previamente entrenado existente.

Representación de tensores cuantificados

La cuantificación de 8 bits se aproxima a los valores de punto flotante utilizando la siguiente fórmula.

\[real\_value = (int8\_value - zero\_point) \times scale\]

La representación tiene dos partes principales:

  • Pesos por eje (también conocido como por canal) o por tensor representados por valores de complemento a dos int8 en el rango [-127, 127] con punto cero igual a 0.

  • Activaciones/entradas por tensor representadas por valores de complemento a dos int8 en el rango [-128, 127], con un punto cero en el rango [-128, 127].

Para obtener una vista detallada de nuestro esquema de cuantificación, consulte nuestra especificación de cuantificación . Se recomienda a los proveedores de hardware que quieran conectarse a la interfaz de delegado de TensorFlow Lite a implementar el esquema de cuantificación que se describe allí.