El siguiente documento describe la especificación del esquema de cuantificación de 8 bits de TensorFlow Lite. Esto tiene como objetivo ayudar a los desarrolladores de hardware a brindar soporte de hardware para la inferencia con modelos cuantificados de TensorFlow Lite.
Resumen de especificaciones
Proporcionamos una especificación y solo podemos ofrecer algunas garantías de comportamiento si se siguen las especificaciones. También entendemos que diferentes hardware pueden tener preferencias y restricciones que pueden causar ligeras desviaciones al implementar las especificaciones que resultan en implementaciones que no son exactas en bits. Si bien eso puede ser aceptable en la mayoría de los casos (y proporcionaremos un conjunto de pruebas que, hasta donde sabemos, incluyen tolerancias por operación que recopilamos de varios modelos), la naturaleza del aprendizaje automático (y el aprendizaje profundo en los casos más comunes) caso) hace imposible ofrecer garantías concretas.
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\]
Los pesos por eje (también conocido como por canal en operaciones Conv) o por tensor están representados por valores de complemento a dos int8
en el rango [-127, 127]
con punto cero igual a 0. Las activaciones/entradas por tensor están representadas por int8
valores de complemento a dos en el rango [-128, 127]
, con un punto cero en el rango [-128, 127]
.
Existen otras excepciones para operaciones particulares que se documentan a continuación.
Entero con signo versus entero sin signo
La cuantización de TensorFlow Lite priorizará principalmente las herramientas y los núcleos para la cuantización int8
para 8 bits. Esto es para la conveniencia de que la cuantificación simétrica se represente mediante un punto cero igual a 0. Además, muchos backends tienen optimizaciones adicionales para la acumulación int8xint8
.
Por eje versus por tensor
La cuantización por tensor significa que habrá una escala y/o punto cero por tensor completo. La cuantificación por eje significa que habrá una escala y/o zero_point
por segmento en la quantized_dimension
. La dimensión cuantificada especifica la dimensión de la forma del tensor a la que corresponden las escalas y los puntos cero. Por ejemplo, un tensor t
, con dims=[4, 3, 2, 1]
con parámetros de cuantificación: scale=[1.0, 2.0, 3.0]
, zero_point=[1, 2, 3]
, quantization_dimension=1
se cuantificará en la segunda dimensión de t
:
t[:, 0, :, :] will have scale[0]=1.0, zero_point[0]=1
t[:, 1, :, :] will have scale[1]=2.0, zero_point[1]=2
t[:, 2, :, :] will have scale[2]=3.0, zero_point[2]=3
A menudo, la quantized_dimension
es el output_channel
de los pesos de las convoluciones, pero en teoría puede ser la dimensión que corresponde a cada producto escalar en la implementación del núcleo, lo que permite una mayor granularidad de cuantificación sin implicaciones de rendimiento. Esto tiene grandes mejoras en la precisión.
TFLite tiene soporte por eje para un número creciente de operaciones. En el momento de redactar este documento, existe soporte para Conv2d y DepthwiseConv2d.
Simétrico vs asimétrico
Las activaciones son asimétricas: pueden tener su punto cero en cualquier lugar dentro del rango int8
con signo [-128, 127]
. Muchas activaciones son de naturaleza asimétrica y un punto cero es una forma relativamente económica de alcanzar efectivamente un bit binario extra de precisión. Dado que las activaciones sólo se multiplican por pesos constantes, el valor constante del punto cero se puede optimizar bastante.
Los pesos son simétricos: se obliga a tener un punto cero igual a 0. Los valores de peso se multiplican por los valores dinámicos de entrada y activación. Esto significa que existe un costo de tiempo de ejecución inevitable al multiplicar el punto cero del peso por el valor de activación. Al hacer cumplir que el punto cero es 0, podemos evitar este costo.
Explicación de las matemáticas: esto es similar a la sección 2.3 en arXiv:1712.05877 , excepto por la diferencia de que permitimos que los valores de escala sean por eje. Esto se generaliza fácilmente, de la siguiente manera:
\(A\) es una matriz \(m \times n\) de activaciones cuantificadas.
\(B\) es una matriz \(n \times p\) de pesos cuantificados.
Considere multiplicar la fila \(j\)de \(A\), \(a_j\) por la columna \(k\)de\(B\), \(b_k\), ambas de longitud \(n\). Los valores enteros cuantificados y los valores de punto cero son \(q_a\), \(z_a\) y \(q_b\), \(z_b\) respectivamente.
\[a_j \cdot b_k = \sum_{i=0}^{n} a_{j}^{(i)} b_{k}^{(i)} = \sum_{i=0}^{n} (q_{a}^{(i)} - z_a) (q_{b}^{(i)} - z_b) = \sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)} - \sum_{i=0}^{n} q_{a}^{(i)} z_b - \sum_{i=0}^{n} q_{b}^{(i)} z_a + \sum_{i=0}^{n} z_a z_b\]
El término \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) es inevitable ya que realiza el producto escalar del valor de entrada y el valor de peso.
Los términos \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) y \(\sum_{i=0}^{n} z_a z_b\) se componen de constantes que permanecen iguales por invocación de inferencia y, por lo tanto, se pueden calcular previamente.
El término \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) debe calcularse en cada inferencia, ya que la activación cambia cada inferencia. Al hacer que las ponderaciones sean simétricas, podemos eliminar el costo de este término.
especificaciones del operador cuantificado int8
A continuación describimos los requisitos de cuantificación para nuestros núcleos int8 tflite:
ADD
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
AVERAGE_POOL_2D
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
CONCATENATION
Input ...:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
CONV_2D
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1 (Weight):
data_type : int8
range : [-127, 127]
granularity: per-axis (dim = 0)
restriction: zero_point = 0
Input 2 (Bias):
data_type : int32
range : [int32_min, int32_max]
granularity: per-axis
restriction: (scale, zero_point) = (input0_scale * input1_scale[...], 0)
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
DEPTHWISE_CONV_2D
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1 (Weight):
data_type : int8
range : [-127, 127]
granularity: per-axis (dim = 3)
restriction: zero_point = 0
Input 2 (Bias):
data_type : int32
range : [int32_min, int32_max]
granularity: per-axis
restriction: (scale, zero_point) = (input0_scale * input1_scale[...], 0)
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
FULLY_CONNECTED
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1 (Weight):
data_type : int8
range : [-127, 127]
granularity: per-axis (dim = 0)
restriction: zero_point = 0
Input 2 (Bias):
data_type : int32
range : [int32_min, int32_max]
granularity: per-tensor
restriction: (scale, zero_point) = (input0_scale * input1_scale[...], 0)
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
L2_NORMALIZATION
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: (scale, zero_point) = (1.0 / 128.0, 0)
LOGISTIC
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: (scale, zero_point) = (1.0 / 256.0, -128)
MAX_POOL_2D
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
MUL
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
RESHAPE
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
RESIZE_BILINEAR
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
SOFTMAX
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: (scale, zero_point) = (1.0 / 256.0, -128)
SPACE_TO_DEPTH
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
TANH
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: (scale, zero_point) = (1.0 / 128.0, 0)
PAD
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
GATHER
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
BATCH_TO_SPACE_ND
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
SPACE_TO_BATCH_ND
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
TRANSPOSE
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
MEAN
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
SUB
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
SUM
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
SQUEEZE
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
LOG_SOFTMAX
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: (scale, zero_point) = (16.0 / 256.0, 127)
MAXIMUM
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
ARG_MAX
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
MINIMUM
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
LESS
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1:
data_type : int8
range : [-128, 127]
granularity: per-tensor
PADV2
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
GREATER
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1:
data_type : int8
range : [-128, 127]
granularity: per-tensor
GREATER_EQUAL
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1:
data_type : int8
range : [-128, 127]
granularity: per-tensor
LESS_EQUAL
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1:
data_type : int8
range : [-128, 127]
granularity: per-tensor
SLICE
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
restriction: Input and outputs must all have same scale/zero_point
EQUAL
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1:
data_type : int8
range : [-128, 127]
granularity: per-tensor
NOT_EQUAL
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Input 1:
data_type : int8
range : [-128, 127]
granularity: per-tensor
SHAPE
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
QUANTIZE (Requantization)
Input 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor
Output 0:
data_type : int8
range : [-128, 127]
granularity: per-tensor