Il seguente documento delinea le specifiche per lo schema di quantizzazione a 8 bit di TensorFlow Lite. Lo scopo è assistere gli sviluppatori hardware nel fornire supporto hardware per l'inferenza con modelli TensorFlow Lite quantizzati.
Riepilogo delle specifiche
Stiamo fornendo una specifica e possiamo fornire solo alcune garanzie sul comportamento se la specifica viene seguita. Comprendiamo inoltre che hardware diversi potrebbero avere preferenze e restrizioni che potrebbero causare lievi deviazioni durante l'implementazione delle specifiche che si traducono in implementazioni non esatte in termini di bit. Sebbene ciò possa essere accettabile nella maggior parte dei casi (e forniremo una serie di test che, per quanto a nostra conoscenza, includono le tolleranze per operazione che abbiamo raccolto da diversi modelli), la natura dell'apprendimento automatico (e dell'apprendimento profondo nei più comuni caso) rende impossibile fornire garanzie concrete.
La quantizzazione a 8 bit si avvicina ai valori in virgola mobile utilizzando la seguente formula.
\[real\_value = (int8\_value - zero\_point) \times scale\]
I pesi per asse (ovvero per canale in Conv ops) o per tensore sono rappresentati da int8
valori di complemento a due nell'intervallo [-127, 127]
con punto zero uguale a 0. Le attivazioni/input per tensore sono rappresentati da int8
valori del complemento a due nell'intervallo [-128, 127]
, con un punto zero nell'intervallo [-128, 127]
.
Esistono altre eccezioni per operazioni particolari documentate di seguito.
Intero con segno e intero senza segno
La quantizzazione di TensorFlow Lite darà priorità principalmente agli strumenti e ai kernel per la quantizzazione int8
per 8 bit. Questo serve per la comodità della quantizzazione simmetrica rappresentata da un punto zero uguale a 0. Inoltre molti backend hanno ottimizzazioni aggiuntive per l'accumulo int8xint8
.
Per asse vs per tensore
La quantizzazione per tensore significa che ci sarà una scala e/o un punto zero per l'intero tensore. La quantizzazione per asse significa che ci sarà una scala e/o zero_point
per sezione in quantized_dimension
. La dimensione quantizzata specifica la dimensione della forma del Tensore a cui corrispondono le scale e i punti zero. Ad esempio, un tensore t
, con dims=[4, 3, 2, 1]
con parametri di quantizzazione: scale=[1.0, 2.0, 3.0]
, zero_point=[1, 2, 3]
, quantization_dimension=1
verrà quantizzato attraverso la seconda dimensione di 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
Spesso, quantized_dimension
è il output_channel
dei pesi delle convoluzioni, ma in teoria può essere la dimensione che corrisponde a ciascun prodotto scalare nell'implementazione del kernel, consentendo una maggiore granularità della quantizzazione senza implicazioni sulle prestazioni. Ciò comporta grandi miglioramenti in termini di precisione.
TFLite dispone del supporto per asse per un numero crescente di operazioni. Al momento della stesura di questo documento, esiste il supporto per Conv2d e DepthwiseConv2d.
Simmetrico vs asimmetrico
Le attivazioni sono asimmetriche: possono avere il loro punto zero ovunque all'interno dell'intervallo int8
con segno [-128, 127]
. Molte attivazioni sono di natura asimmetrica e un punto zero è un modo relativamente economico per ottenere effettivamente un bit binario extra di precisione. Poiché le attivazioni vengono moltiplicate solo per pesi costanti, il valore costante del punto zero può essere ottimizzato in modo piuttosto significativo.
I pesi sono simmetrici: costretti ad avere il punto zero uguale a 0. I valori dei pesi vengono moltiplicati per i valori di input dinamico e di attivazione. Ciò significa che moltiplicare il punto zero del peso per il valore di attivazione comporta un costo di esecuzione inevitabile. Imponendo che il punto zero sia 0 possiamo evitare questo costo.
Spiegazione dei calcoli: è simile alla sezione 2.3 in arXiv:1712.05877 , tranne per la differenza che consentiamo che i valori di scala siano per asse. Questo si generalizza facilmente, come segue:
\(A\) è una matrice \(m \times n\) di attivazioni quantizzate.
\(B\) è una matrice \(n \times p\) di pesi quantizzati.
Considera la possibilità di moltiplicare la \(j\)a riga di \(A\), \(a_j\) per la \(k\)a colonna di\(B\), \(b_k\), entrambi di lunghezza \(n\). I valori interi quantizzati e i valori del punto zero sono \(q_a\), \(z_a\) e \(q_b\), \(z_b\) rispettivamente.
\[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\]
Il termine \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) è inevitabile poiché esegue il prodotto scalare del valore di input e del valore del peso.
I termini \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) e \(\sum_{i=0}^{n} z_a z_b\) sono costituiti da costanti che rimangono le stesse per invocazione di inferenza e quindi possono essere precalcolati.
Il termine \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) deve essere calcolato ad ogni inferenza poiché l'attivazione modifica ogni inferenza. Imponendo che i pesi siano simmetrici possiamo rimuovere il costo di questo termine.
specifiche dell'operatore quantizzato int8
Di seguito descriviamo i requisiti di quantizzazione per i nostri kernel 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