Embedded HandbookApps
All sections › XIV. Machine learning on a microcontroller
STM32F407, STM32F103 · level advanced · 18 min read
Open in the interactive handbook

119. Quantization to int8: CMSIS-NN and TensorFlow Lite Micro

The perceptron from section 118 computed in float: 4 bytes per weight and a floating-point multiplication for every connection. On the F407 this is tolerable; on the F103, which has no FPU, every multiplication is a library call. Quantization replaces real numbers with eight-bit integers: the weights take a quarter of the space, and you can multiply two at a time per clock cycle with the SMLAD instruction. This section quantizes two models with the real TensorFlow Lite converter, runs them on CMSIS-NN kernels — the same ones TensorFlow Lite Micro calls on Cortex-M — and checks that the C code reproduces the reference interpreter bit for bit.

Training and the converter run in the stm-tf:1 image — tensorflow-cpu 2.21.0 and numpy 2.3.3 — and CMSIS-NN 7.0.0 is taken from the Keil pack. Training is covered in section 118, features and the spectrum in section 117.

Terms used in the documentation

Term Meaning
quantization real numbers → integers
post-training quantization quantizing a model that is already trained
scale, zero point the scale and the integer that stands for real zero
affine (asymmetric) quantization quantization with a shifted zero
symmetric quantization quantization without a shift: zero point is 0
per-tensor / per-channel one scale per tensor / a separate scale for each output
representative dataset a sample of inputs used for calibration
requantization rescaling the int32 sum to the output scale
fixed-point multiplier, shift a Q31 multiplier plus a shift
saturation clamping at the ends of the range
flatbuffer (.tflite) the TensorFlow Lite model file
interpreter, op resolver the interpreter and its set of kernels
reference kernels plain kernels, without acceleration
multiply-accumulate (MAC) a multiplication added to a running sum

119.1 Why integers

There are three gains. Memory: an int8 weight is 1 byte instead of 4. Speed: the Cortex-M4 has SIMD instructions on pairs of 16-bit numbers (SMLAD does two multiply-accumulates per clock cycle), and int8 values fit into them. Portability: an integer model runs the same way on the F103 without an FPU and on the F407. The price is precision: a number has only 256 levels, and where they sit is decided by calibration.


119.2 Scale and zero point

Input quantization: the 256 int8 levels cover a segment of the real axis, and zero falls exactly on a level

A real number r is stored as an integer q by the rule r = s · (q − z): s is the scale, z is the zero point. For the input of model A the converter chose a scale of 0.0211 and a zero point of −50; the 256 levels cover the segment from −1.64 to 3.73 — the range the normalized features occupied in the representative dataset. Real zero falls exactly on level −50: this matters, because padding uses zero and ReLU compares against zero. The weights are quantized symmetrically — zero point 0 — so that the sum of products has no extra terms.

On the device the input is quantized with the same formula as in Python:

float z = (x[i] - mu[i]) / sd[i];
int v = (int)floorf(z / s + 0.5f) + zp;
q[i] = (int8_t)(v < -128 ? -128 : v > 127 ? 127 : v);

119.3 The TensorFlow Lite converter

The model is trained in Keras in float, then the converter quantizes it using a representative dataset — it runs a few hundred training windows through the model and records the range of every tensor:

conv = tf.lite.TFLiteConverter.from_keras_model(model)
conv.optimizations = [tf.lite.Optimize.DEFAULT]
conv.representative_dataset = rep
conv.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
conv.inference_input_type = tf.int8
conv.inference_output_type = tf.int8
blob = conv.convert()

The TFLITE_BUILTINS_INT8 line forbids leaving float operations in the model: if some operation cannot be quantized, the converter refuses instead of building a mixed model. The input and output tensors are int8 too: the device quantizes the input itself, and at the output all you need is the index of the largest number.


119.4 Two models: accuracy before and after

Model A takes the four features of section 117; model B gets the spectrum itself, 64 bins from 4 to 256 Hz, and finds the features on its own. Both are tested on 540 held-out windows — recordings that were not used in training:

Model Parameters Correct, float Correct, int8 Same class
Model A: 4 features → 4 → 3 35 469 474 525
Model B: 64-bin spectrum → 16 → 3 1091 462 461 537

Quantization shifts the answers in borderline windows, but the overall share of correct answers barely changes: A even gains 5, B loses 1 — that is scatter, not a gain. The small model A agrees with float less often: with four inputs and four neurons, every quantization level matters more.

Two models on 540 held-out windows before and after quantization: model A with 35 parameters — 469 correct in float, 474 in int8, 525 matching classes; model B with 1091 parameters — 462, 461 and 537; the differences of 5 and 1 windows are scatter, not a gain


119.5 The representative dataset

If you calibrate on data that does not look like real operating data, the ranges come out wrong. An experiment: the same model B, with a representative dataset made only of healthy motors. More inputs fell outside the range — 330 saturated inputs instead of 221 out of 34560 — and 460 correct instead of 461. Here it hardly mattered: saturation hit less than 1 % of the inputs. But the rule is general: the representative dataset must contain all classes and all operating modes, otherwise a rare high-amplitude defect will hit 127 and become indistinguishable from a merely strong signal.


119.6 Scale per tensor or per output

By default the converter gives each neuron its own weight scale: a neuron with small weights does not waste its 256 levels on the empty range of its neighbour. You can ask for one scale for the whole tensor — the file is smaller, the kernel simpler. For model B: one scale per tensor gives 460 correct versus 461, and the .tflite is 2968 bytes instead of 3376. On small fully connected networks the difference in accuracy is a few windows; on convolutional networks with filters of different strength a per-output scale is noticeably better, and CMSIS-NN supports both cases.


119.7 A layer in integers

A fully connected int8 layer is a sum of products in int32, an int32 bias and a rescaling of the sum to the output scale. The real multiplier s_in · s_weight / s_out is replaced by a Q31 integer and a shift — the same way as in TensorFlow Lite Micro:

q, shift = math.frexp(real)
qi = int(round(q * (1 << 31)))
if qi == (1 << 31):
    qi //= 2
    shift += 1

The last three lines handle the edge case: when q is right next to one, rounding gives exactly 2^31, which does not fit in int32. Then the multiplier is halved and the shift is increased by one.

A layer on CMSIS-NN is a single function; the quantization parameters are passed in structures:

cmsis_nn_fc_params fp = { -za, 0, zo, { relu ? zo : -128, 127 } };
cmsis_nn_per_channel_quant_params qp = { (int32_t *)mult, (int32_t *)shift };   /* свой масштаб у каждого выхода */
arm_fully_connected_per_channel_s8(&ctx, &fp, &qp, &id, in, &fd, w, &bd, b, &od, out);

ReLU is not computed separately: the lower bound of the output equals the zero point, that is, real zero — negative values simply do not get through. Model B does 1072 multiply-accumulates per answer.


119.8 Bit for bit with the interpreter

The reference is the TensorFlow Lite interpreter with reference kernels. The first build of CMSIS-NN with default settings: the outputs matched in only 533 and 536 windows out of 540 — in the rest an output number differs by one. The number of correct answers is the same, but the mismatch is real. How rounding is done during rescaling is set by a build switch: desktop TensorFlow 2.21 rounds once, while CMSIS-NN and TensorFlow Lite Micro round twice by default (in TFLM this is the TFLITE_SINGLE_ROUNDING switch, and in pack 1.23.2 it is not set). With the CMSIS_NN_USE_SINGLE_ROUNDING switch the int8 outputs matched bit for bit in all 540 windows for both models, and input quantization in C matched Python in all 2160 and 34560 numbers.

Hence the rule: the kernels and the reference must round the same way. The CMSIS_NN_USE_SINGLE_ROUNDING switch is needed to compare against desktop TensorFlow; a device that is checked against TFLM in its default mode keeps double rounding. Check this by comparing outputs, not the share of correct answers — a difference of one in the last digit in a borderline window changes the class.


119.9 Memory and code

For model B armclang gave: int8 weights — 1072 bytes; int32 biases, Q31 multipliers and shifts — 228; float normalization — 512; in total 1812 bytes. The same model in float takes 4364 bytes. The gain is 2.4 times, not 4: the per-output scales and the normalization take space too, and in small models their share is large. In models with tens of thousands of weights the gain approaches four.

Code: the CMSIS-NN kernel is 3564 bytes on the F407 and 2160 on the F103. On the F407 it is larger because it is built with DSP instructions: it unrolls loops and computes four outputs at once. Where there is no DSP, a plain C branch runs — the same numbers, only slower.


119.10 Pitfalls

On the board the answers differ from Python by one. Different rounding during rescaling: desktop TensorFlow 2.21 rounds once, CMSIS-NN and TFLM round twice by default. To compare against the desktop reference use CMSIS_NN_USE_SINGLE_ROUNDING; against TFLM in default mode, use the default mode; compare outputs, not classes.

After quantization the model answers with a single class. The input was quantized with the wrong scale or without normalization. Take the scale and zero point from the input tensor of the .tflite, and use the same normalization as in training.

A strong signal is indistinguishable from a very strong one. Saturation: calibration never saw such amplitudes. Put all modes and classes into the representative dataset.

The converter left float operations in. TFLITE_BUILTINS_INT8 was not set, the model was built mixed, and on Cortex-M it will pull in float kernels. Forbid it.

The int8 model is barely smaller. A small model: the per-output scales and the normalization are comparable to the weights. For a few dozen weights quantization pays off in speed on the F103, not in memory.

The interpreter on the computer gives different numbers. Accelerated kernels (XNNPACK) compute differently. For comparison, use the reference kernels.


119.11 Summary

Full sources

train119.py — quantizes the models to int8 with the TensorFlow Lite converter and exports them for CMSIS-NN.

# -*- coding: utf-8 -*-
"""Раздел 119: квантование в int8 — конвертер TensorFlow Lite и ядра CMSIS-NN.

Запуск — в образе stm-tf:1:  python train119.py <каталог_вывода>

Данные — генератор раздела 118 (записи моторов, калибровка крепления уже
сделана), зерно 119. Две модели:
  A — четыре признака раздела 117 → 4 нейрона → 3 класса;
  B — сам спектр: 64 бина (4…256 Гц) → 16 нейронов → 3 класса.
Каждая учится в Keras, затем конвертер TFLite квантует её в int8 по
представительной выборке. Из готового .tflite вынимаются целые веса,
сдвиги, масштабы и нулевые точки — они и уезжают в model119.h; интерпретатор
TFLite считает ответы int8 на отложенных окнах, и CMSIS-NN на C обязан
повторить их бит в бит. Опыт с плохой калибровкой: представительная выборка
только из нормальных моторов. Сводка — в summary119.txt."""
import math
import os
import sys

os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
import numpy as np
import tensorflow as tf
from sklearn.model_selection import train_test_split

OUT = sys.argv[1] if len(sys.argv) > 1 else "."
tf.keras.utils.set_random_seed(119)
tf.config.experimental.enable_op_determinism()
rng = np.random.RandomState(119)
N, FS, WPR, NB = 256, 1024.0, 20, 64
t = np.arange(N) / FS
hann = 0.5 * (1 - np.cos(2 * np.pi * np.arange(N) / N))


def window_features(x):
    y = x - x.mean()
    rms = np.sqrt((y ** 2).mean())
    crest = np.abs(y).max() / rms
    m = np.abs(np.fft.rfft(y * hann))[:N // 2]
    m[0] = 0
    k = int(m.argmax())
    a, b, c = m[k - 1], m[k], m[k + 1]
    peak = (k + 0.5 * (a - c) / (a - 2 * b + c)) * FS / N
    band = (m[25:63] ** 2).sum() / (m[1:] ** 2).sum()
    return [rms, peak, crest, band], list(m[1:NB + 1] * (2.0 / N))


def recording(c):
    f1 = 25.0 + rng.uniform(-1.5, 1.5)
    if c == 0:
        a1, a2 = 0.45 * rng.uniform(0.6, 1.4), 0.0
    elif c == 1:
        a1, a2 = 0.74 * rng.uniform(0.6, 1.4), 0.0
    else:
        a1, a2 = 0.35 * rng.uniform(0.6, 1.4), 0.45 * rng.uniform(0.08, 1.3)
    f2 = 160.0 + rng.uniform(-20, 20)
    F, S = [], []
    for _ in range(WPR):
        ph1, ph2 = rng.uniform(0, 2 * np.pi, 2)
        x = (1.65 + a1 * rng.uniform(0.95, 1.05) * np.sin(2 * np.pi * f1 * t + ph1)
             + a2 * rng.uniform(0.95, 1.05) * np.sin(2 * np.pi * f2 * t + ph2) + rng.uniform(-0.05, 0.05, N))
        f, s = window_features(x)
        F.append(f)
        S.append(s)
    return F, S


XF, XS, y, g = [], [], [], []
for c in range(3):
    for r in range(30):
        F, S = recording(c)
        XF += F
        XS += S
        y += [c] * WPR
        g += [c * 1000 + r] * WPR
XF, XS = np.array(XF, np.float32), np.array(XS, np.float32)
y, g = np.array(y), np.array(g)
recs = np.array(sorted(set(g)))
rtr, rte = train_test_split(recs, test_size=0.3, random_state=119, stratify=recs // 1000)
mtr, mte = np.isin(g, rtr), np.isin(g, rte)
lines = ["versions=%s,%s" % (tf.__version__, np.__version__), "test=%d" % int(mte.sum())]


def quantize_multiplier(real):
    """Как в TFLite Micro: множитель Q31 и сдвиг из вещественного масштаба."""
    if real == 0.0:
        return 0, 0
    q, shift = math.frexp(real)
    qi = int(round(q * (1 << 31)))
    if qi == (1 << 31):
        qi //= 2
        shift += 1
    return qi, shift


def train(X, hidden):
    mu, sd = X[mtr].mean(axis=0).astype(np.float32), X[mtr].std(axis=0).astype(np.float32)
    Z = ((X - mu) / sd).astype(np.float32)
    model = tf.keras.Sequential([tf.keras.Input((X.shape[1],)), tf.keras.layers.Dense(hidden, activation="relu"),
                                 tf.keras.layers.Dense(3)])
    model.compile(optimizer=tf.keras.optimizers.Adam(0.01),
                  loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True))
    model.fit(Z[mtr], y[mtr], epochs=150, batch_size=64, verbose=0, shuffle=True)
    return model, mu, sd, Z


def build(trained, rep_mask, per_channel=True):
    model, mu, sd, Z = trained
    fl = model.predict(Z[mte], verbose=0).argmax(axis=1)

    def rep():
        for row in Z[rep_mask][::3]:
            yield [row[None, :]]

    conv = tf.lite.TFLiteConverter.from_keras_model(model)
    conv.optimizations = [tf.lite.Optimize.DEFAULT]
    conv.representative_dataset = rep
    conv.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
    conv.inference_input_type = tf.int8
    conv.inference_output_type = tf.int8
    conv._experimental_disable_per_channel = not per_channel
    blob = conv.convert()
    # эталонные ядра — та же арифметика, что в TFLite Micro (не XNNPACK)
    it = tf.lite.Interpreter(model_content=blob,
                             experimental_op_resolver_type=tf.lite.experimental.OpResolverType.BUILTIN_REF)
    it.allocate_tensors()
    din, dout = it.get_input_details()[0], it.get_output_details()[0]
    s_in, zp_in = din["quantization"]
    # вход квантуется так же, как на C: float32, floor(x / s + 0.5) + zp, насыщение
    q_in = np.clip(np.floor(Z[mte] / np.float32(s_in) + np.float32(0.5)) + zp_in, -128, 127).astype(np.int8)
    outs = []
    for row in q_in:
        it.set_tensor(din["index"], row[None, :])
        it.invoke()
        outs.append(it.get_tensor(dout["index"])[0].copy())
    outs = np.array(outs, np.int8)
    q8 = outs.argmax(axis=1)
    # слои — FULLY_CONNECTED: веса int8 [выход][вход], сдвиги int32
    ops = [op for op in it._get_ops_details() if op["op_name"] == "FULLY_CONNECTED"]
    td = {d["index"]: d for d in it.get_tensor_details()}
    layers = []
    for op in ops:
        a, w, b = op["inputs"][:3]
        o = op["outputs"][0]
        sa, za = td[a]["quantization"]
        sw = td[w]["quantization_parameters"]["scales"]
        so, zo = td[o]["quantization"]
        assert not td[w]["quantization_parameters"]["zero_points"].any()
        wq = it.get_tensor(w)
        ms = [quantize_multiplier(float(sa) * float(v) / float(so)) for v in sw]
        ms = ms * (wq.shape[0] // len(ms))            # по тензору — один множитель на все выходы
        layers.append(dict(w=wq, b=it.get_tensor(b), za=int(za), zo=int(zo), mult=[m for m, _ in ms],
                           shift=[k for _, k in ms], nscale=len(sw)))
    return dict(mu=mu, sd=sd, s_in=float(s_in), zp_in=int(zp_in), layers=layers, outs=outs, fl=fl, q8=q8, qin=q_in,
                blob=len(blob), s_out=dout["quantization"], params=model.count_params())


yt = y[mte]
A = build(train(XF, 4), mtr)
tB = train(XS, 16)
B = build(tB, mtr)
Bbad = build(tB, mtr & (y == 0))                   # калибровка только по нормальным моторам
Bpt = build(tB, mtr, per_channel=False)            # один масштаб на весь тензор весов
for name, M in (("A", A), ("B", B), ("Bbad", Bbad), ("Bpt", Bpt)):
    lines.append("%s_float=%d" % (name, int((M["fl"] == yt).sum())))
    lines.append("%s_int8=%d" % (name, int((M["q8"] == yt).sum())))
    lines.append("%s_same=%d" % (name, int((M["fl"] == M["q8"]).sum())))
    lines.append("%s_params=%d" % (name, M["params"]))
    lines.append("%s_tflite=%d" % (name, M["blob"]))
    lines.append("%s_in=%.9g,%d" % (name, M["s_in"], M["zp_in"]))
    raw = np.floor(((XS if name != "A" else XF)[mte] - M["mu"]) / M["sd"] / np.float32(M["s_in"]) + 0.5) + M["zp_in"]
    lines.append("%s_sat=%d,%d" % (name, int(((raw < -128) | (raw > 127)).sum()), raw.size))
    lines.append("%s_layers=%s" % (name, ";".join("%d,%d,%d" % (L["nscale"], L["za"], L["zo"]) for L in M["layers"])))


def c_float(v):
    s_ = "%.9g" % v
    return (s_ if any(ch in s_ for ch in ".en") else s_ + ".0") + "f"


def c_arr(name, a, typ):
    flat = np.asarray(a).ravel()
    body = ", ".join(c_float(v) if typ == "float" else str(int(v)) for v in flat)
    return "static const %s %s[%d] = { %s };\n" % (typ, name, len(flat), body)


h = ["/* Раздел 119: модели A и B в int8 из конвертера TFLite — сгенерировано train119.py, не править */\n",
     "#include <stdint.h>\n#define N_TEST %d\n" % len(yt)]
for name, M, X in (("A", A, XF), ("B", B, XS)):
    L1, L2 = M["layers"]
    h.append("#define %s_IN %d\n#define %s_HID %d\n" % (name, X.shape[1], name, L1["w"].shape[0]))
    h.append("#define %s_S_IN %s\n#define %s_ZP_IN %d\n" % (name, c_float(M["s_in"]), name, M["zp_in"]))
    for k, L in (("1", L1), ("2", L2)):
        h.append("#define %s_ZA%s %d\n#define %s_ZO%s %d\n" % (name, k, L["za"], name, k, L["zo"]))
        h.append(c_arr("%s_M%s" % (name, k), L["mult"], "int32_t"))
        h.append(c_arr("%s_SH%s" % (name, k), L["shift"], "int32_t"))
        h.append(c_arr("%s_W%s" % (name, k), L["w"], "int8_t"))
        h.append(c_arr("%s_B%s" % (name, k), L["b"], "int32_t"))
    h.append(c_arr("%s_MU" % name, M["mu"], "float"))
    h.append(c_arr("%s_SD" % name, M["sd"], "float"))
    h.append("#ifdef ML_HOST\n")
    h.append(c_arr("%s_TEST_X" % name, X[mte], "float"))
    h.append(c_arr("%s_TFL" % name, M["outs"], "int8_t"))
    h.append(c_arr("%s_QIN" % name, M["qin"], "int8_t"))
    h.append("#endif\n")
h.append("#ifdef ML_HOST\n" + c_arr("TEST_Y", yt, "uint8_t") + "#endif\n")
open(OUT + "/model119.h", "w", encoding="utf-8", newline="\n").write("".join(h))
open(OUT + "/summary119.txt", "w", encoding="utf-8", newline="\n").write("\n".join(lines) + "\n")
print("\n".join(lines))

infer119.c — models A and B in int8 on CMSIS-NN kernels.

/* Раздел 119: модели A и B в int8 на ядрах CMSIS-NN 7.0.0.
 * С -DML_HOST main() сверяет выходы int8 с интерпретатором TFLite бит в бит. */
#include <math.h>
#include "arm_nnfunctions.h"
#include "model119.h"

/* Вход: нормировка во float, затем квантование масштабом и нулевой точкой входного тензора. */
static void quantize_in(const float *x, const float *mu, const float *sd, int n, float s, int zp, int8_t *q)
{
    int i;
    for (i = 0; i < n; i++) {
        float z = (x[i] - mu[i]) / sd[i];
        int v = (int)floorf(z / s + 0.5f) + zp;
        q[i] = (int8_t)(v < -128 ? -128 : v > 127 ? 127 : v);
    }
}

/* Полносвязный слой: int8 вход и веса, int32 сдвиг, выход int8 после пересчёта масштаба по каждому выходу. */
static void dense(const int8_t *in, int n_in, const int8_t *w, const int32_t *b, int n_out,
                  int za, int zo, const int32_t *mult, const int32_t *shift, int relu, int8_t *out)
{
    cmsis_nn_context ctx = { 0, 0 };
    cmsis_nn_fc_params fp = { -za, 0, zo, { relu ? zo : -128, 127 } };
    cmsis_nn_per_channel_quant_params qp = { (int32_t *)mult, (int32_t *)shift };   /* свой масштаб у каждого выхода */
    cmsis_nn_dims id = { 1, 1, 1, n_in }, fd = { n_in, 1, 1, n_out };
    cmsis_nn_dims bd = { 1, 1, 1, n_out }, od = { 1, 1, 1, n_out };
    arm_fully_connected_per_channel_s8(&ctx, &fp, &qp, &id, in, &fd, w, &bd, b, &od, out);
}

static int argmax3(const int8_t *o)
{
    int best = 0, j;
    for (j = 1; j < 3; j++)
        if (o[j] > o[best])
            best = j;
    return best;
}

int classify_a(const float *f, int8_t out[3])
{
    int8_t q[A_IN], h[A_HID];
    quantize_in(f, A_MU, A_SD, A_IN, A_S_IN, A_ZP_IN, q);
    dense(q, A_IN, A_W1, A_B1, A_HID, A_ZA1, A_ZO1, A_M1, A_SH1, 1, h);
    dense(h, A_HID, A_W2, A_B2, 3, A_ZA2, A_ZO2, A_M2, A_SH2, 0, out);
    return argmax3(out);
}

int classify_b(const float *spec, int8_t out[3])
{
    int8_t q[B_IN], h[B_HID];
    quantize_in(spec, B_MU, B_SD, B_IN, B_S_IN, B_ZP_IN, q);
    dense(q, B_IN, B_W1, B_B1, B_HID, B_ZA1, B_ZO1, B_M1, B_SH1, 1, h);
    dense(h, B_HID, B_W2, B_B2, 3, B_ZA2, B_ZO2, B_M2, B_SH2, 0, out);
    return argmax3(out);
}

#ifdef ML_HOST
#include <stdio.h>
int main(void)
{
    int n, j, same_a = 0, same_b = 0, right_a = 0, right_b = 0;
    for (n = 0; n < N_TEST; n++) {
        int8_t oa[3], ob[3];
        int ca = classify_a(&A_TEST_X[n * A_IN], oa), cb = classify_b(&B_TEST_X[n * B_IN], ob), ea = 1, eb = 1;
        for (j = 0; j < 3; j++) {
            ea &= oa[j] == A_TFL[n * 3 + j];
            eb &= ob[j] == B_TFL[n * 3 + j];
        }
        same_a += ea;
        same_b += eb;
        right_a += ca == TEST_Y[n];
        right_b += cb == TEST_Y[n];
    }
    printf("same_a=%d,%d\nsame_b=%d,%d\nright_a=%d\nright_b=%d\n", same_a, N_TEST, same_b, N_TEST, right_a, right_b);
    for (n = 0, same_a = same_b = 0; n < N_TEST; n++) {       /* вход: квантование C против Python */
        int8_t qa[A_IN], qb[B_IN];
        quantize_in(&A_TEST_X[n * A_IN], A_MU, A_SD, A_IN, A_S_IN, A_ZP_IN, qa);
        quantize_in(&B_TEST_X[n * B_IN], B_MU, B_SD, B_IN, B_S_IN, B_ZP_IN, qb);
        for (j = 0; j < A_IN; j++)
            same_a += qa[j] == A_QIN[n * A_IN + j];
        for (j = 0; j < B_IN; j++)
            same_b += qb[j] == B_QIN[n * B_IN + j];
    }
    printf("qin_a=%d,%d\nqin_b=%d,%d\n", same_a, N_TEST * A_IN, same_b, N_TEST * B_IN);
    return 0;
}
#else
volatile float g_in[B_IN];
volatile int g_class;
void SystemInit(void) {}
int main(void)
{
    for (;;) {
        float x[B_IN];
        int8_t o[3];
        int i;
        for (i = 0; i < B_IN; i++)
            x[i] = g_in[i];
        g_class = classify_b(x, o) + classify_a(x, o);
    }
}
#endif

Next: section 120 "Anomaly detection" — when there are no defect recordings at all and the model learns only from normal operation.

Open in the interactive handbook