En el siguiente documento, se describe la especificación del esquema de cuantización de 8 bits de TensorFlow Lite. El objetivo es ayudar a los desarrolladores de hardware a proporcionar compatibilidad de hardware para inferencias con modelos cuantizados de TensorFlow Lite.
Resumen de la especificación
Proporcionamos una especificación y solo podemos dar algunas garantías de comportamiento si se sigue la especificación. También comprendemos que los distintos tipos de hardware pueden tener preferencias y restricciones que pueden provocar pequeñas desviaciones cuando se implementa la especificación que dan como resultado implementaciones que no son bit a exactamente. Si bien esto puede ser aceptable en la mayoría de los casos (y proporcionaremos un conjunto de pruebas que, a nuestro leal saber, incluyen las tolerancias por operación que recopilamos de varios modelos), la naturaleza del aprendizaje automático (y el aprendizaje profundo en el caso más común) hace que sea imposible proporcionar garantías estrictas.
La cuantización de 8 bits se aproxima a los valores de punto flotante con la siguiente fórmula.
\[real\_value = (int8\_value - zero\_point) \times scale\]
Los pesos por eje (o por canal en operaciones de conv.) o por tensor se representan con int8
valores de complemento de dos en el rango [-127, 127]
con el punto cero igual a 0. Las activaciones o entradas por tensor se representan con los valores del complemento de dos de int8
en el rango [-128, 127]
, con un punto cero en el rango [-128, 127]
.
Hay otras excepciones para operaciones particulares que se documentan a continuación.
Número entero firmado frente a número entero sin firma
La cuantización de TensorFlow Lite priorizará principalmente las herramientas y los kernels para la cuantización de int8
de 8 bits. Esto es para la conveniencia de que la cuantización simétrica se represente con un punto cero igual a 0. Además, muchos backends tienen optimizaciones adicionales para la acumulación de int8xint8
.
Comparación por eje frente a tensor
La cuantización por tensor significa que habrá una escala o un punto cero por cada tensor completo. La cuantización por eje significa que habrá una escala o zero_point
por porción en la quantized_dimension
. La dimensión cuantizada indica 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 cuantización: scale=[1.0, 2.0, 3.0]
, zero_point=[1, 2, 3]
y 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, 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 kernel, lo que permite un mayor nivel de detalle de la cuantización sin implicaciones de rendimiento. Esto tiene grandes mejoras en cuanto a la precisión.
TFLite es compatible con los ejes para una cantidad creciente de operaciones. Al momento de este documento, se admite Conv2d y DepthwiseConv2d.
Simétrica frente a asimétrica
Las activaciones son asimétricas: pueden tener su punto cero en cualquier lugar del rango de int8
firmado [-128, 127]
. Muchas activaciones son asimétricas por naturaleza, y un punto cero es una forma relativamente económica de obtener hasta un poco de precisión binaria adicional. Dado que las activaciones solo se multiplican por pesos constantes, el valor de punto cero constante se puede optimizar bastante.
Los pesos son simétricos, es decir, se ven forzados a tener un punto cero igual a 0. Los valores de las ponderaciones se multiplican por la entrada dinámica y los valores de activación. Esto significa que existe un costo inevitable de entorno de ejecución multiplicado por el punto cero del peso por el valor de activación. Si aplicamos que el punto cero es 0, podemos evitar este costo.
Explicación de las operaciones matemáticas: es similar a la sección 2.3 de arXiv:1712.05877, excepto por la diferencia de que permitimos que los valores de la escala sean por eje. Esto se generaliza con facilidad de la siguiente manera:
$A$ es una matriz de $m \times n$ de activaciones cuantificadas.
$B$ es una matriz de $n \times p$ de pesos cuantizados.
Considera multiplicar la fila $j$th de $A$, $a_j$ por la columna $k$th de $B$, $b_k$, ambos con una longitud $n$. Los valores de números enteros cuantificados y puntos 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\) constan 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. Si aplicamos ponderaciones simétricas, podemos quitar el costo de este término.
Especificaciones del operador cuantizado int8
A continuación, describimos los requisitos de cuantización para nuestros kernels de tflite de int8:
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