Spécification de quantification TensorFlow Lite 8 bits

Le document suivant présente la spécification du schéma de quantification 8 bits de TensorFlow Lite. Ceci est destiné à aider les développeurs de matériel à fournir une prise en charge matérielle pour l'inférence avec des modèles TensorFlow Lite quantifiés.

Résumé des spécifications

Nous fournissons une spécification et nous ne pouvons fournir certaines garanties sur le comportement que si la spécification est respectée. Nous comprenons également que différents matériels peuvent avoir des préférences et des restrictions qui peuvent entraîner de légers écarts lors de la mise en œuvre des spécifications, ce qui entraîne des implémentations qui ne sont pas exactes. Bien que cela puisse être acceptable dans la plupart des cas (et nous fournirons une suite de tests qui, à notre connaissance, incluent les tolérances par opération que nous avons recueillies à partir de plusieurs modèles), la nature de l'apprentissage automatique (et de l'apprentissage profond dans les cas les plus courants) cas), il est impossible de fournir des garanties concrètes.

La quantification sur 8 bits se rapproche des valeurs à virgule flottante à l'aide de la formule suivante.

\[real\_value = (int8\_value - zero\_point) \times scale\]

Les poids par axe (alias par canal dans Conv ops) ou par tenseur sont représentés par des valeurs de complément à deux int8 dans la plage [-127, 127] avec le point zéro égal à 0. Les activations/entrées par tenseur sont représentées par int8 valeurs du complément à deux dans la plage [-128, 127] , avec un point zéro dans la plage [-128, 127] .

Il existe d'autres exceptions pour des opérations particulières qui sont documentées ci-dessous.

Entier signé vs entier non signé

La quantification TensorFlow Lite donnera principalement la priorité aux outils et aux noyaux pour la quantification int8 pour 8 bits. Ceci est pour la commodité de la quantification symétrique représentée par un point zéro égal à 0. De plus, de nombreux backends ont des optimisations supplémentaires pour l'accumulation int8xint8 .

Par axe vs par tenseur

La quantification par tenseur signifie qu'il y aura une échelle et/ou un point zéro par tenseur entier. La quantification par axe signifie qu'il y aura une échelle et/ou zero_point par tranche dans la quantized_dimension . La dimension quantifiée spécifie la dimension de la forme du Tenseur à laquelle correspondent les échelles et les points zéro. Par exemple, un tenseur t , avec dims=[4, 3, 2, 1] avec les paramètres de quantification : scale=[1.0, 2.0, 3.0] , zero_point=[1, 2, 3] , quantization_dimension=1 sera quantifié sur la deuxième dimension 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

Souvent, la quantized_dimension est le output_channel des poids des convolutions, mais en théorie, il peut s'agir de la dimension qui correspond à chaque produit scalaire dans l'implémentation du noyau, permettant une plus grande granularité de quantification sans implications en termes de performances. Cela améliore considérablement la précision.

TFLite prend en charge par axe un nombre croissant d'opérations. Au moment de la rédaction de ce document, la prise en charge existe pour Conv2d et DepthwiseConv2d.

Symétrique vs asymétrique

Les activations sont asymétriques : elles peuvent avoir leur point zéro n'importe où dans la plage signée int8 [-128, 127] . De nombreuses activations sont de nature asymétrique et un point zéro est un moyen relativement peu coûteux d'atteindre efficacement un bit binaire supplémentaire de précision. Étant donné que les activations ne sont multipliées que par des poids constants, la valeur constante du point zéro peut être considérablement optimisée.

Les poids sont symétriques : forcés d'avoir le point zéro égal à 0. Les valeurs de poids sont multipliées par les valeurs d'entrée dynamique et d'activation. Cela signifie qu'il existe un coût d'exécution inévitable lié à la multiplication du point zéro du poids par la valeur d'activation. En imposant que le point zéro soit 0, nous pouvons éviter ce coût.

Explication du calcul : ceci est similaire à la section 2.3 dans arXiv:1712.05877 , à l'exception de la différence selon laquelle nous autorisons les valeurs d'échelle par axe. Cela se généralise facilement, comme suit :

\(A\) est une matrice \(m \times n\) d'activations quantifiées.
\(B\) est une matrice \(n \times p\) de poids quantifiés.
Envisagez de multiplier la \(j\)ème ligne de \(A\), \(a_j\) par la \(k\)ème colonne de\(B\), \(b_k\), toutes deux de longueur \(n\). Les valeurs entières quantifiées et les valeurs des points zéro sont respectivement \(q_a\), \(z_a\) et \(q_b\), \(z_b\) .

\[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\]

Le terme \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) est inévitable car il effectue le produit scalaire de la valeur d'entrée et de la valeur de poids.

Les termes \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) et \(\sum_{i=0}^{n} z_a z_b\) sont constitués de constantes qui restent les mêmes par invocation d'inférence et peuvent donc être pré-calculées.

Le terme \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) doit être calculé à chaque inférence puisque l'activation modifie chaque inférence. En imposant des pondérations symétriques, nous pouvons supprimer le coût de ce terme.

spécifications de l'opérateur quantifié int8

Ci-dessous, nous décrivons les exigences de quantification pour nos noyaux 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

Les références

arXiv:1712.05877