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 fornecer suporte de hardware para inferência com modelos quantizados do TensorFlow Lite.
Resumo das especificações
Estamos fornecendo uma especificação e só podemos fornecer algumas garantias de comportamento se a especificação for seguida. Também entendemos que diferentes hardwares podem ter preferências e restrições que podem causar pequenos desvios ao implementar as especificações que resultam em implementações que não são exatas em termos de bits. Embora isso possa ser aceitável na maioria dos casos (e forneceremos um conjunto de testes que, até onde sabemos, incluem tolerâncias por operação que coletamos de vários modelos), a natureza do aprendizado de máquina (e do aprendizado profundo nos casos mais comuns) caso) torna impossível fornecer quaisquer garantias concretas.
A quantização de 8 bits aproxima os valores de ponto flutuante usando a seguinte fórmula.
\[real\_value = (int8\_value - zero\_point) \times scale\]
Os pesos por eixo (também conhecidos como por canal em Conv ops) ou por tensor são representados por valores de complemento de dois int8
no intervalo [-127, 127]
com ponto zero igual a 0. Ativações/entradas por tensor são representadas por int8
valores de complemento de dois no intervalo [-128, 127]
, com um ponto zero no intervalo [-128, 127]
.
Existem outras exceções para operações específicas documentadas abaixo.
Inteiro assinado versus inteiro não assinado
A quantização do TensorFlow Lite priorizará principalmente ferramentas e kernels para quantização int8
para 8 bits. Isso ocorre para a conveniência da quantização simétrica ser representada pelo ponto zero igual a 0. Além disso, muitos back-ends têm otimizações adicionais para acumulação int8xint8
.
Por eixo vs por tensor
A quantização por tensor significa que haverá uma escala e/ou ponto zero por tensor inteiro. A quantização por eixo significa que haverá uma escala e/ou zero_point
por fatia na quantized_dimension
. A dimensão quantizada especifica a dimensão da forma do Tensor à qual correspondem as escalas e os pontos zero. 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]
, quantization_dimension=1
será quantizado em a 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
Freqüentemente, o quantized_dimension
é o output_channel
dos pesos das convoluções, mas em teoria pode ser a dimensão que corresponde a cada produto escalar na implementação do kernel, permitindo mais granularidade de quantização sem implicações de desempenho. Isso traz grandes melhorias na precisão.
TFLite tem suporte por eixo para um número crescente de operações. No momento deste documento, existe suporte para Conv2d e DepthwiseConv2d.
Simétrico vs assimétrico
As ativações são assimétricas: elas podem ter seu ponto zero em qualquer lugar dentro do intervalo int8
assinado [-128, 127]
. Muitas ativações são de natureza assimétrica e um ponto zero é uma maneira relativamente barata de obter efetivamente um bit binário extra de precisão. Como as ativações são multiplicadas apenas por pesos constantes, o valor constante do ponto zero pode ser bastante otimizado.
Os pesos são simétricos: forçados a ter ponto zero igual a 0. Os valores dos pesos são multiplicados pelos valores de entrada dinâmica e ativação. Isso significa que há um custo de tempo de execução inevitável de multiplicar o ponto zero do peso pelo valor de ativação. Ao impor que o ponto zero é 0, podemos evitar esse custo.
Explicação da matemática: é semelhante à seção 2.3 em arXiv:1712.05877 , exceto pela diferença de que permitimos que os valores da escala sejam por eixo. Isso generaliza prontamente, como segue:
\(A\) é uma matriz \(m \times n\) de ativações quantizadas.
\(B\) é uma matriz \(n \times p\) de pesos quantizados.
Considere multiplicar a \(j\)-ésima linha de \(A\), \(a_j\) pela \(k\)-ésima coluna de\(B\), \(b_k\), ambas de 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, pois realiza o produto escalar do valor de entrada e do 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, pois a ativação altera cada inferência. Ao impor que os pesos sejam simétricos, podemos remover o custo deste 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