O documento a seguir descreve a especificação do esquema de quantização de 8 bits do TensorFlow Lite. O objetivo é ajudar os desenvolvedores de hardware a oferecer suporte de hardware para inferência com modelos quantizados do TensorFlow Lite.
Resumo das especificações
Estamos fornecendo uma especificação, e só podemos oferecer algumas garantias sobre o comportamento se a especificação for seguida. Também entendemos que hardwares diferentes podem ter preferências e restrições que causam pequenos desvios ao implementar as especificações, o que resulta em implementações não precisas. Embora isso seja aceitável na maioria dos casos (e forneceremos um conjunto de testes que incluem tolerâncias por operação coletadas de vários modelos), a natureza do machine learning (e o aprendizado profundo no caso mais comum) impossibilita fornecer garantias rígidas.
A quantização de 8 bits aproxima valores de ponto flutuante usando a fórmula a seguir.
\[real\_value = (int8\_value - zero\_point) \times scale\]
Os pesos por eixo (ou por canal em operações de conv.) ou por tensor são representados pelos valores de complemento de int8
de dois no intervalo [-127, 127]
com ponto zero igual a 0. As ativações/entradas por tensor são representadas pelos valores de complemento de int8
dois no intervalo [-128, 127]
, com um ponto zero no intervalo [-128, 127]
.
Existem outras exceções para operações específicas documentadas abaixo.
Número inteiro assinado x número inteiro não assinado
A quantização do TensorFlow Lite priorizará principalmente ferramentas e kernels para a quantização int8
para 8 bits. Isso ocorre para a conveniência da quantização simétrica sendo representada por um ponto zero igual a 0. Além disso, muitos
back-ends têm outras otimizações para o acúmulo de int8xint8
.
Por eixo x por tensor
A quantização por tensor significa que haverá uma escala e/ou ponto zero para
cada tensor. A quantização por eixo significa que haverá uma escala e/ou
zero_point
por fração no quantized_dimension
. A dimensão quantizada especifica a dimensão da forma do Tensor a que as escalas e os pontos zero correspondem. Por exemplo, um tensor t
, com dims=[4, 3, 2, 1]
com parâmetros de quantização scale=[1.0, 2.0, 3.0]
, zero_point=[1, 2, 3]
e quantization_dimension=1
será quantizado na segunda dimensão 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
Muitas vezes, quantized_dimension
é o output_channel
dos pesos das
convoluções, mas, em teoria, pode ser a dimensão que corresponde a cada
produto ponto na implementação do kernel, permitindo mais granularidade de quantização
sem implicações no desempenho. Isso melhora muito a precisão.
O TFLite é compatível com um número crescente de operações por eixo. No momento deste documento, havia suporte para Conv2d e DepthwiseConv2d.
Simétrico vs. assimétrica
As ativações são assimétricas: elas podem ter o ponto zero em qualquer lugar no
intervalo int8
assinado [-128, 127]
. Muitas ativações são de natureza assimétrica, e
um ponto zero é uma maneira relativamente barata de conseguir até um pouco
binário extra de precisão. Como as ativações só são multiplicadas por pesos constantes, o valor de ponto zero constante pode ser bastante otimizado.
Os pesos são simétricos: forçados a ter um ponto zero igual a 0. Os valores de peso são multiplicados por valores dinâmicos de entrada e ativação. Isso significa que há um custo de execução inevitável multiplicar o ponto zero do peso pelo valor de ativação. Ao aplicar o valor zero como 0, podemos evitar esse custo.
Explicação da matemática: isso é semelhante à seção 2.3 em arXiv:1712.05877, exceto pela diferença que permitimos que os valores de escala sejam por eixo. Isso generaliza facilmente, da seguinte maneira:
$A$ é uma matriz $m\times n$ de ativações quantizadas.
$B$ é uma matriz $n \times p$ de pesos quantizados.
Considere multiplicar a linha $j$th de $A$, $a_j$ pela coluna $k$th de
$B$, $b_k$, com comprimento $n$. Os valores inteiros quantizados e os valores de pontos zero são $q_a$, $z_a$ e $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\]
O termo \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) é inevitável, já que está executando o produto escalar do valor de entrada e o valor do peso.
Os termos \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) e \(\sum_{i=0}^{n} z_a z_b\) são compostos de constantes que permanecem as mesmas por invocação de inferência e, portanto, podem ser pré-calculados.
O termo \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) precisa ser calculado a cada inferência, já que a ativação muda todas elas. Ao fazer com que os pesos sejam simétricos, podemos remover o custo desse termo.
Especificações do operador quantizado int8
Abaixo, descrevemos os requisitos de quantização para nossos kernels 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
Referências
arXiv:1712.05877 (link em inglês)