Poniższy dokument przedstawia specyfikację 8-bitowego schematu kwantyzacji TensorFlow Lite. Ma to na celu pomóc twórcom sprzętu w zapewnieniu sprzętowej obsługi wnioskowania ze skwantowanymi modelami TensorFlow Lite.
Podsumowanie specyfikacji
Dostarczamy specyfikację i możemy zapewnić pewne gwarancje dotyczące zachowania tylko wtedy, gdy specyfikacja będzie przestrzegana. Rozumiemy również, że inny sprzęt może mieć preferencje i ograniczenia, które mogą powodować niewielkie odchylenia podczas wdrażania specyfikacji, co skutkuje implementacjami, które nie są dokładne bitowo. Chociaż może to być akceptowalne w większości przypadków (i zapewnimy zestaw testów, które zgodnie z naszą najlepszą wiedzą obejmują tolerancje operacji, które zebraliśmy z kilku modeli), charakter uczenia maszynowego (i głębokiego uczenia się w najpowszechniejszych przypadku) uniemożliwia udzielenie jakichkolwiek twardych gwarancji.
Kwantyzacja 8-bitowa przybliża wartości zmiennoprzecinkowe za pomocą następującego wzoru.
\[real\_value = (int8\_value - zero\_point) \times scale\]
Wagi na oś (inaczej na kanał w Conv ops) lub wagi na tensor są reprezentowane przez wartości uzupełnienia do dwóch int8
w zakresie [-127, 127]
z punktem zerowym równym 0. Aktywacje/wejścia na tensor są reprezentowane przez int8
wartości uzupełnienia do dwóch w zakresie [-128, 127]
, z punktem zerowym w zakresie [-128, 127]
.
Istnieją inne wyjątki dla określonych operacji, które opisano poniżej.
Liczba całkowita ze znakiem vs liczba całkowita bez znaku
Kwantyzacja TensorFlow Lite będzie przede wszystkim priorytetem dla narzędzi i jąder do kwantyzacji int8
dla 8-bitów. Ma to na celu wygodę symetrycznej kwantyzacji reprezentowanej przez punkt zerowy równy 0. Dodatkowo wiele backendów ma dodatkowe optymalizacje dla akumulacji int8xint8
.
Na oś vs na tensor
Kwantyzacja na tensor oznacza, że na cały tensor będzie przypadać jedna skala i/lub punkt zerowy. Kwantyzacja na osi oznacza, że będzie jedna skala i/lub zero_point
na wycinek w quantized_dimension
. Skwantowany wymiar określa wymiar kształtu tensora, któremu odpowiadają skale i punkty zerowe. Na przykład tensor t
z dims=[4, 3, 2, 1]
z parametrami kwantyzacji: scale=[1.0, 2.0, 3.0]
, zero_point=[1, 2, 3]
, quantization_dimension=1
będzie kwantowany w poprzek drugi wymiar 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
Często quantized_dimension
jest kanałem output_channel
wag splotów, ale teoretycznie może to być wymiar odpowiadający każdemu iloczynowi skalarnemu w implementacji jądra, umożliwiając większą szczegółowość kwantyzacji bez wpływu na wydajność. Zapewnia to znaczną poprawę dokładności.
TFLite obsługuje każdą oś dla rosnącej liczby operacji. W momencie tworzenia tego dokumentu dostępna była obsługa Conv2d i DepthwiseConv2d.
Symetryczny kontra asymetryczny
Aktywacje są asymetryczne: mogą mieć punkt zerowy w dowolnym miejscu w zakresie int8
ze znakiem [-128, 127]
. Wiele aktywacji ma charakter asymetryczny, a punkt zerowy jest stosunkowo niedrogim sposobem na skuteczne uzyskanie dodatkowej binarnej precyzji. Ponieważ aktywacje są mnożone tylko przez stałe wagi, stałą wartość punktu zerowego można dość mocno zoptymalizować.
Wagi są symetryczne: punkt zerowy musi być równy 0. Wartości wag są mnożone przez dynamiczne wartości wejściowe i aktywacyjne. Oznacza to, że istnieje nieunikniony koszt w czasie działania związany z pomnożeniem punktu zerowego ciężaru przez wartość aktywacji. Wymuszając, że punkt zerowy wynosi 0, możemy uniknąć tego kosztu.
Wyjaśnienie matematyki: jest to podobne do sekcji 2.3 w arXiv:1712.05877 , z tą różnicą, że pozwalamy, aby wartości skali były przypisane do osi. Można to łatwo uogólnić w następujący sposób:
\(A\) jest macierzą \(m \times n\) skwantowanych aktywacji.
\(B\) to macierz \(n \times p\) skwantowanych wag.
Rozważ pomnożenie \(j\)-tego wiersza \(A\), \(a_j\) przez \(k\)-tą kolumnę\(B\), \(b_k\), oba o długości \(n\). Skwantowane wartości całkowite i wartości punktów zerowych to odpowiednio \(q_a\), \(z_a\) i \(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\]
Termin \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) jest nieunikniony, ponieważ wykonuje iloczyn skalarny wartości wejściowej i wartości wagi.
Terminy \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) i \(\sum_{i=0}^{n} z_a z_b\) składają się ze stałych, które pozostają takie same przy każdym wywołaniu wnioskowania, dzięki czemu można je wstępnie obliczyć.
Termin \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) należy obliczyć przy każdym wnioskowaniu, ponieważ aktywacja zmienia każde wnioskowanie. Wymuszając symetryczność wag, możemy usunąć koszt tego terminu.
specyfikacje operatora kwantyzowanego int8
Poniżej opisujemy wymagania kwantyzacji dla naszych jąder 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