121д. Промышленные кейсы TinyML: состояние мотора, насоса и вентилятора, утечка по звуку, счёт и обнаружение объектов в кадре — что решали, чем и где ошиблись
Разделы 116–121г разобрали инструменты по одному: модели, признаки, обучение, квантование, вывод на плате, аномалии, звук, камеру и весь путь от задачи до прибора. Здесь — пять типовых задач цеха как кейсы: мотор, насос, вентилятор, утечка сжатого воздуха и счёт деталей на конвейере. Для каждого — постановка и цена ошибки, датчик и где его ставить, частота и окно, признаки, модель, её цена на F407, мера приёмки и, главное, что сломалось при переходе с обучающих данных на «поле» и чем это исправили.
Данные синтетические, но собраны из физики задачи: удары дефекта подшипника на частотах BPFO и BPFI, кавитация как широкополосный шум пачками, лопастная частота вентилятора, турбулентный ультразвук утечки, детали на ленте со светом и бликами. Зерно фиксировано, поэтому каждое число повторяется. Это не данные настоящих заводов: кейсы показывают, какие ошибки бывают и как их ловить, а не какой точности ждать на вашем моторе.
Опоры: numpy 2.3.3 и scikit-learn 1.7.2 в образе stm-ml:1 (как
в разделах 116 и 118) — данные, признаки, обучение; свой вывод на C
(infer121f.c) — признаки из сырого окна и ответ модели, сверка с Python
в gcc 12.2.0 образа stm-sys:1; armclang из Keil MDK 5.43 — размеры кода
и буферов для Cortex-M4F.
Как это называется в документации
Оригинал Перевод condition monitoring контроль состояния unbalance, misalignment дисбаланс, расцентровка BPFO, BPFI (ball pass frequency outer / inner race) частота перекатывания тел качения по наружному и внутреннему кольцу envelope analysis анализ огибающей order analysis анализ в порядках: частоты в долях оборота cavitation, dry running кавитация, сухой ход blade pass frequency лопастная частота anomaly detection обнаружение аномалий false alarm, missed detection ложная тревога, пропуск recall, false alarm rate полнота, доля ложных тревог domain shift сдвиг условий: поле не похоже на обучение data augmentation расширение данных искажёнными копиями heatmap, center detection карта, обнаружение центров
121д.1 Пять кейсов: постановка и сводная таблица
Каждый кейс разобран по одной схеме. Сначала — цена ошибки: что дороже, ложная тревога или пропуск. От неё зависит порог и мера приёмки. Затем датчик и крепление, частота выборки и окно, признаки (раздел 117), модель (разделы 116, 116а, 120) и её цена на F407. Последним — опыт, который обычно и съедает проект: модель обучили, а в поле поменялись обороты, экземпляр, свет или фон.
| Кейс | Датчик, окно | Модель | Приёмка в лаборатории | Что сломалось | После исправления |
|---|---|---|---|---|---|
| Мотор | акселерометр, 5120 Гц, 2048 | перцептрон 6 → 8 → 5 | 296 из 300 | частотник 900…1000 об/мин: 65 из 300 | 290 из 300 |
| Насос | вибрация и ток, 5000 Гц, 2048 | дерево, 9 узлов | 395 из 400 | задвижка: 99 «кавитаций» из 100 | 0 из 100 |
| Вентилятор | микрофон, 16 кГц, 8192 | Махаланобис, 343 числа | AUC 0,925 | обдув в цехе: 564 тревоги из 600 | 2 из 600 |
| Утечка | ультразвук, 128 кГц, 16 × 1024 | логистическая, 3 числа | 156 из 200 найдено | пневматика: 198 тревог из 200 | 11 из 200 |
| Детали | камера, 48 × 48 | перцептрон 81 → 8 → 1 | 200 из 200 кадров | другой свет: 106 из 200 | 196 из 200 |
Все признаки и модели повторены на C. На отложенных окнах C и Python ответили одинаково: мотор — 100 из 100, насос — 60 из 60, вентилятор — 60 из 60, утечка — 60 из 60, детали — 60 кадров из 60; признаки мотора, насоса и вентилятора разошлись не больше чем на 3,2·10⁻⁵ по относительной мере, уровни утечки — на 8,2·10⁻⁵ дБ, карта деталей — на 3,8·10⁻⁶.
121д.2 Мотор: дисбаланс, расцентровка и подшипник по вибрации
Постановка. Асинхронный мотор, 1480 об/мин, с частотником. Пять состояний: норма, дисбаланс, расцентровка, дефект наружного и внутреннего кольца подшипника. Ложная тревога — лишний выезд мастера; пропуск подшипника — авария и простой. Поэтому приёмка — по полноте для подшипника при малой доле ложных тревог, а не по средней точности.
Датчик. Акселерометр на корпусе подшипника, на плоской площадке, на шпильке или клею. Частота 5120 Гц, окно 2048 отсчётов — 0,4 с и 2,5 Гц на бин.
Физика. Дисбаланс поднимает пик 1× — частоту вращения, расцентровка — 2×. Дефект кольца даёт удар при каждом проходе тела качения. Частоты ударов — по геометрии подшипника: 9 шариков диаметром 7,94 мм на делительном диаметре 39,04 мм, угол контакта ноль,
BPFO = n/2 · fr · (1 − d/D), BPFI = n/2 · fr · (1 + d/D),
то есть 3,585 и 5,415 частоты вращения: при 1480 об/мин fr = 24,67 Гц, BPFO = 88,4 Гц, BPFI = 133,6 Гц. Удары слабые, но каждый раскачивает резонанс корпуса — в модели 1800 Гц. Поэтому подшипник ищут не в обычном спектре, а в спектре огибающей: полосовой фильтр вокруг резонанса, выпрямление, спектр. Внутреннее кольцо вращается вместе с валом и проходит зону нагрузки раз за оборот, его удары промодулированы 1×.
Признаки — шесть, все в логарифме: СКЗ, пики 1× и 2×, пики BPFO и BPFI
в спектре огибающей, делённые на его средний уровень, и эксцесс сигнала
в полосе резонанса. Полоса — два звена biquad, огибающая — модуль:
biquad(MOT_BP, g_x, g_y, MOT_N); /* полоса резонанса корпуса */
biquad(MOT_BP, g_y, g_y, MOT_N);
f[3] = peak(g_a, MOT_KO * fr, df, 2) / floor;
f[4] = peak(g_a, MOT_KI * fr, df, 2) / floor;
Модель — перцептрон 6 → 8 → 5 (раздел 116.6), 113 чисел с масштабом.
Что сломалось: обороты. Первую версию учили при 1480 об/мин, и частоты признаков были вписаны числами. На проверке при тех же оборотах — 300 верных из 300. Потом мотор перевели на частотник, 900…1000 об/мин: при 950 об/мин fr = 15,83 Гц, BPFO = 56,8 Гц, BPFI = 85,7 Гц, и признаки смотрят мимо пиков. Верных — 65 из 300: норма 60 из 60, а из 240 дефектов узнаны 5. Признаки в порядках — частоты от оборотов частотника, — но обучение всё ещё на 1480: 185 из 300. Дисбаланс узнан в 12 случаях из 60: в модели его амплитуда растёт как квадрат оборотов, и на 950 об/мин она в 2,4 раза меньше, чем при обучении. Исправление — порядки и обучение на всём диапазоне частотника 900…1500 об/мин (600 окон): 296 из 300 на 1480 и 290 из 300 на частотнике. Ошибки на 1480 — 4 нормы, принятые за дисбаланс; все 240 дефектов узнаны.
Что сломалось: крепление. Датчик поставили на пластиковый кожух вентилятора: кожух срезает всё выше нескольких сотен герц, резонанс корпуса до датчика не доходит. Та же модель — 213 из 300, подшипник найден в 34 случаях из 120, и 77 дефектов подшипника названы нормой. Модель тут ни при чём, её нельзя «доучить». Помогает самопроверка: СКЗ в полосе резонанса на исправном моторе при хорошем креплении — от 10,0 мг (медиана 10,9 мг), на кожухе — не больше 2,0 мг (медиана 1,8 мг). Порог между ними даёт прибору сказать «проверьте крепление» вместо «норма». Крепление и калибровка — раздел 118.7.
121д.3 Насос: кавитация и сухой ход по вибрации и току
Постановка. Центробежный насос, 2900 об/мин, крыльчатка с 6 лопатками: fr = 48,3 Гц, лопастная частота 290,0 Гц. Три состояния: норма, кавитация, сухой ход. Пропуск сухого хода губит уплотнение; ложная тревога останавливает линию. Мера приёмки — полнота по обеим неисправностям и число тревог на длинной записи нормы.
Датчики. Акселерометр на корпусе насоса и трансформатор тока на фазе мотора, оба — 5000 Гц, окно 2048 отсчётов (0,41 с). Частота выбрана так, чтобы период сети 50 Гц был ровно 100 отсчётов.
Физика. Кавитация — схлопывание пузырей: широкополосный шум пачками в килогерцах, неровный ток. Сухой ход — нагрузки нет: ток падает, лопастная частота почти пропадает.
Признаки — четыре: ток к номиналу, неровность тока — разброс СКЗ по 20 периодам сети, делённый на среднее, — уровень лопастной частоты и СКЗ шума в полосе 1–2,4 кГц, оба в логарифме:
for (int b = 0; b < 20; b++) { /* 20 периодов сети по 100 отсчётов */
Модель — дерево глубиной 4, 9 узлов (раздел 116.4).
Что сломалось: задвижка. Первую версию учили на четырёх насосах: норма, кавитация, сухой ход — 299 верных из 300 на проверке. В поле оператор прикрыл задвижку. Насос исправен, но ток упал и стал неровным, и из 100 окон с прикрытой задвижкой 99 модель назвала кавитацией, 1 — сухим ходом. Режим «прикрыта задвижка» добавили в обучение как норму.
Что сломалось: другой насос. Новый насос стоит на другой плите, его шум и лопастная частота выше: все 50 окон нормы — «кавитация». Даже с задвижкой в обучении — снова 50 из 50. Исправление — своя база: при пуске прибор пишет 20 окон нормы и запоминает медианы двух уровней, а дерево видит уровни относительно них (раздел 120.4):
float x[4] = { f[0], f[1], f[2] - base[0], f[3] - base[1] };
Итог: 395 из 400 на проверке — 5 кавитаций названы нормой, — прикрытая задвижка 100 из 100 названа нормой, новый насос — 200 из 200. На потоке из 1000 окон нормы и задвижки нового насоса — ни одной тревоги; в потоке неисправностей найдено 198 кавитаций из 200 и 200 сухих ходов из 200.
121д.4 Вентилятор: аномалия по звуку
Постановка. Вентилятор с 7 лопастями на частотнике: 900, 1200 и 1500 об/мин, лопастная частота 105, 140 и 175 Гц. Записей неисправностей нет, поэтому модель учится только на норме (раздел 120) и тревожит, когда звук далёк от нормы. Ложная тревога в звуковом контроле дешевле, чем в насосе, но если их много, прибор выключат.
Датчик. Микрофон рядом с вентилятором (тракт — раздел 121б), 16 кГц, окно 16 кадров по 512 отсчётов — 0,51 с.
Признаки — 18 полос, от бина 1 до 256, то есть от 31 Гц до 8 кГц, шириной, растущей с частотой; уровень — средняя мощность по 16 кадрам в децибелах. Нижние полосы нужны ради 1×: без них повреждённую лопасть не видно.
Модель — расстояние Махаланобиса до нормы: среднее 18 полос и обратная матрица разброса, 343 числа. Порог — 99,5-й процентиль расстояния на отдельной выборке нормы.
for (int i = 0; i < FAN_NB; i++) {
float r = 0;
for (int j = 0; j < FAN_NB; j++)
r += FAN_P[i * FAN_NB + j] * d[j];
s += d[i] * r;
}
Три аномалии: повреждённая лопасть (пик 1× и модуляция лопастной частоты), подшипник (шум в полосе около 4 кГц), задевание (щелчок раз за оборот).
Что сломалось: скорость. Норму записали на 1500 об/мин. На 1500 — 7 тревог из 200 окон нормы, на 1200 и 900 — 200 из 200: другая скорость — другой спектр, и для модели это аномалия. AUC по всем скоростям — 0,608. Исправление — норма на всех трёх скоростях: 12 тревог из 600, найдено 156 повреждённых лопастей из 200, 180 подшипников и 108 задеваний, AUC 0,925.
Что сломалось: фон. В цехе включили обдув сжатым воздухом — шипение 2–8 кГц. Тревог — 564 из 600 окон нормы. Записи с обдувом добавили в норму: тревог 0 из 600 без обдува и 2 из 600 с обдувом. Но порог поднялся с 19,61 до 38,89, и модель ослепла там, где шипит воздух: найдено 37 лопастей из 200, 129 подшипников и 42 задевания, а при включённом обдуве — 51, 30 и 27. Это честная цена: микрофон слышит всё вокруг. Для подшипника в шумном цехе лучше датчик на корпусе, как в 121д.2; микрофон оставляют там, где фон известен.
121д.5 Утечка сжатого воздуха по ультразвуку
Постановка. Обход трубопровода с прибором или стационарный датчик у участка. Утечка — турбулентный шум, в модели с максимумом около 40 кГц, где шум цеха уже затих. Ложная тревога — лишний поиск по трассе; пропуск — деньги на компрессор, но не авария. Мера приёмки — дальность, на которой находится утечка данного размера.
Датчик. Микрофон с полосой до 64 кГц, 128 кГц, кадр 1024 отсчёта — 125 Гц на бин и 8 мс, окно — 16 кадров. Уровни — в полосах 35–45 кГц (утечка) и 15–25 кГц (шум цеха). В модель заложено ослабление 1/r и затухание в воздухе 1,3 дБ/м.
Что сломалось: пневматика. Первая версия: средний уровень по кадрам, логистическая регрессия, обучение на одном экземпляре датчика без помех. На проверке — 0 тревог из 200 и 156 утечек из 200. В цехе работают пневмоцилиндры: выхлоп даёт ультразвук в 1–4 кадрах из 16, и тревог — 198 из 200. Утечка шумит всё время, выхлоп — коротко; среднее этого не различает, медиана по 16 кадрам — различает:
f[0] = median16(u) - base[0];
f[1] = median16(m) - base[1];
Что сломалось: экземпляр. Чувствительность микрофонов разбросана; в модели — ±6 дБ. У экземпляра на +4…+6 дБ первая версия даёт 69 тревог из 200, у экземпляра на −6…−4 дБ находит 133 утечки из 200. Исправление — своя база: при установке прибор пишет 10 тихих окон, а признаки берёт относительно них. В обучение — 24 экземпляра и пневматика, 384 окна.
Итог: тревог 0 из 200 без помех и 11 из 200 при пневматике; найдено 124 малых утечки из 200 и 198 средних на 1–5 м. Слабые места остались: у чувствительного экземпляра 27 тревог из 200 — над шумом усилителя поднимается и сам шум цеха, а он гуляет от окна к окну; малых утечек у экземпляра на −6…−4 дБ найдено 70 из 200. Дальность — из 200:
| Расстояние | 1 м | 2 м | 3 м | 4 м | 6 м |
|---|---|---|---|---|---|
| Малая утечка | 200 | 195 | 101 | 46 | 10 |
| Средняя, на 10 дБ громче | 200 | 200 | 200 | 195 | 109 |
Ультразвук — прибор ближнего действия: малую утечку надёжно находят в пределах 2 м, и это надо писать в инструкцию обхода.
121д.6 Детали на конвейере: детектор центров
Постановка. Камера над лентой считает детали в кадре. Ошибка счёта в любую сторону — брак учёта; приёмка — доля кадров с точным счётом и доля найденных деталей. Присутствие человека в зоне решают так же — обнаружением в кадре; большой детектор на камере — раздел 121в.
Датчик и кадр. Серый кадр 48 × 48 после уменьшения. Детали — светлые круги радиусом 3–4,5 пикселя, от 0 до 6 в кадре, и часто вплотную: из 200 проверочных кадров касающиеся детали есть в 115.
Порог и пятна не справились. Порог 122,7 и связные области, как
find_blobs в разделе 121а: при обычном свете точный счёт в 178 кадрах
из 200 и 589 деталей из 613 — касающиеся сливаются в одно пятно. Когда
свет сменился — темнее, наклон яркости от окна, блики, — 51 кадр из 200
и 152 детали из 565.
Детектор центров. Маленькая сеть смотрит в окно 9 × 9 вокруг каждой точки и отвечает, насколько эта точка — центр детали. Это свёртка 9 × 9 с 8 фильтрами, ReLU и свёртка 1 × 1. Вход — окно минус его среднее, делённое на СКЗ кадра: так общий уровень и наклон света уходят до сети. Цель обучения — гауссово пятно в центре каждой детали; ответ — карта 40 × 40, детали — пики выше 0,5.
for (int i = 0; i < DET_P * DET_P; i++)
z[i] = (z[i] - pm) * inv;
665 чисел и 1 049 600 умножений на кадр.
Что сломалось: свет. Обученный при обычном свете детектор при нём безупречен: 200 кадров из 200, все 613 деталей. При другом свете — 106 кадров из 200: найдено 562 детали из 565, но пиков 777 — блики и края тени дают ложные центры. Исправление — половина обучающих кадров при изменённом свете: 196 кадров из 200, 561 деталь из 565 при 563 пиках, и при обычном свете — по-прежнему 200 из 200. Свет на объекте меняется всегда; кадры для обучения снимают утром, днём и вечером или искажают программно.
Цена на F407. Функции из объектного файла armclang:
| Кейс | Код признаков и модели | Таблицы вне кода | Чисел модели |
|---|---|---|---|
| Мотор | 3660 + 1688 байт | — | 113 |
| Насос | 1896 + 128 байт | 72 байта | 9 узлов |
| Вентилятор | 380 + 592 байта | 1372 байта | 343 |
| Утечка | 1746 + 52 байта | — | 3 |
| Детали | 3106 + 390 байт | 2592 байта | 665 |
Общий спектр с БПФ — ещё 1748 байт. Буферы общие для всех кейсов: комплексный буфер БПФ на 2048 точек — 16 384 байта, окно и выход фильтра — по 8192, спектр — 4100; всего в ОЗУ 37 896 байт. Входное окно лежит отдельно: у мотора 8 КБ во float, у утечки 16 кадров — 64 КБ, поэтому на приборе кадры утечки считают по мере прихода и хранят только 32 уровня.
121д.7 Грабли
Точность в лаборатории 100 %, в поле — треть. Признаки привязаны к частотам при одних оборотах. Частоты считают в порядках от оборотов частотника, а обучают на всём диапазоне: у мотора 65 → 290 из 300.
Модель «не видит» подшипник. Датчик на кожухе или на краске: высокие частоты до него не доходят. Модель не виновата; прибор должен проверять уровень полосы резонанса и жаловаться на крепление.
Исправный насос в тревоге. В обучении не было режима, который в поле обычен, — прикрытой задвижки. Режимы работы собирают до обучения, у оператора и технолога, а не после первой ложной тревоги.
Второй прибор врёт, первый — нет. Другой экземпляр: другая плита, другой микрофон. Абсолютные уровни заменяют уровнями к своей базе, снятой при пуске.
Добавили фон в норму — и пропуски. Модель нормы растягивает порог на всё, что ей показали: обдув убрал 562 ложные тревоги, но и половину находок.
Среднее по окну ловит короткие помехи. Выхлоп пневматики в 4 кадрах из 16 поднимает среднее; медиана его не видит.
Детектор считает блики. Свет в обучении был один. Обучающие кадры — при всех условиях освещения, вход — с вычтенным местным средним.
Сравнивали C и Python на одном окне. Ошибка в индексе бина или в порядке весов проявляется не везде. Сверяют на десятках окон всех классов и режимов: здесь 340 окон и кадров.
121д.8 Итоги
- Мотор: BPFO = 3,585·fr и BPFI = 5,415·fr по геометрии подшипника, огибающая в полосе резонанса; частоты числами — 65 из 300 на частотнике, порядки и весь диапазон — 290 из 300.
- Крепление на кожухе: 34 подшипника из 120; уровень полосы резонанса 10,9 мг против 1,8 мг — признак для самопроверки.
- Насос: прикрытая задвижка дала 99 ложных кавитаций из 100, другой насос — 50 из 50; режим в обучении и своя база — 0 и 200 верных из 200.
- Вентилятор: норма на одной скорости — 200 тревог из 200 на других; на трёх — 12 из 600 и AUC 0,925; фон в норме гасит тревоги, но и находки.
- Утечка: медиана по кадрам — 198 → 11 ложных из 200 при пневматике; малая утечка находится в 195 случаях из 200 на 2 м и в 46 на 4 м.
- Детали: порог и пятна — 178 и 51 кадр из 200, детектор центров с обучением при разном свете — 200 и 196.
- Все пять кейсов повторены на C: ответы совпали с Python на всех 340 окнах и кадрах; код признаков и модели — от 972 до 5348 байт на кейс на F407.
Полные исходники
infer121f.c — признаки и модели пяти кейсов на C во float с параметрами из model121f.h.
/* Раздел 121д: пять промышленных кейсов — признаки и модели на Cortex-M4F во float.
* Параметры — из model121f.h (выгрузил train121f.py). Каждый кейс — функция признаков
* из сырого окна датчика и функция ответа модели:
* мотор — 6 признаков вибрации, перцептрон 6 → 8 → 5;
* насос — 4 признака вибрации и тока, дерево решений;
* вентилятор — 18 полос звука, расстояние Махаланобиса до нормы;
* утечка — 2 уровня ультразвука к своей базе, логистическая регрессия;
* детали — карта центров по окнам 9 × 9, перцептрон 81 → 8 → 1, счёт пиков.
* На компьютере main читает test121f.bin и сверяет признаки и ответы с Python
* (-DML_HOST). Проверка — _verify/ml121f_check.py. */
#include <math.h>
#include "model121f.h"
#define NMAX 2048
static float g_re[2 * NMAX]; /* комплексный буфер БПФ */
static float g_a[NMAX / 2 + 1]; /* амплитудный спектр */
static float g_x[NMAX], g_y[NMAX]; /* окно и выход фильтра */
/* БПФ по основанию 2 на месте: re, im чередуются */
static void fft(float *d, int n)
{
for (int i = 1, j = 0; i < n; i++) {
int b = n >> 1;
for (; j & b; b >>= 1)
j ^= b;
j ^= b;
if (i < j) {
float t = d[2 * i]; d[2 * i] = d[2 * j]; d[2 * j] = t;
t = d[2 * i + 1]; d[2 * i + 1] = d[2 * j + 1]; d[2 * j + 1] = t;
}
}
for (int len = 2; len <= n; len <<= 1) {
for (int k = 0; k < len / 2; k++) {
float wr = cosf(-6.28318531f * k / len), wi = sinf(-6.28318531f * k / len);
for (int i = k; i < n; i += len) {
int j = i + len / 2;
float xr = d[2 * j] * wr - d[2 * j + 1] * wi, xi = d[2 * j] * wi + d[2 * j + 1] * wr;
d[2 * j] = d[2 * i] - xr;
d[2 * j + 1] = d[2 * i + 1] - xi;
d[2 * i] += xr;
d[2 * i + 1] += xi;
}
}
}
}
/* Амплитудный спектр с окном Ханна: синус амплитуды A даёт пик A */
static void amp_spec(const float *x, int n, float *a)
{
for (int i = 0; i < n; i++) {
g_re[2 * i] = x[i] * (0.5f - 0.5f * cosf(6.28318531f * i / n));
g_re[2 * i + 1] = 0.0f;
}
fft(g_re, n);
for (int k = 0; k <= n / 2; k++)
a[k] = sqrtf(g_re[2 * k] * g_re[2 * k] + g_re[2 * k + 1] * g_re[2 * k + 1]) * 4.0f / n;
}
/* Звено второго порядка, прямая форма I */
static void biquad(const float c[5], const float *x, float *y, int n)
{
float x1 = 0, x2 = 0, y1 = 0, y2 = 0;
for (int i = 0; i < n; i++) {
float v = c[0] * x[i] + c[1] * x1 + c[2] * x2 - c[3] * y1 - c[4] * y2;
x2 = x1; x1 = x[i]; y2 = y1; y1 = v;
y[i] = v;
}
}
static float peak(const float *a, float f, float df, int w)
{
int k = (int)(f / df + 0.5f);
float m = a[k - w];
for (int i = k - w + 1; i <= k + w; i++)
if (a[i] > m)
m = a[i];
return m;
}
static float mean(const float *x, int n)
{
float s = 0;
for (int i = 0; i < n; i++)
s += x[i];
return s / n;
}
/* ── 1. Мотор: 1×, 2×, BPFO и BPFI огибающей, эксцесс; fr — от частотника ── */
void mot_features(const float *x, float fr, float f[6])
{
const float df = MOT_FS / MOT_N;
float m = mean(x, MOT_N), s2 = 0, s4 = 0, floor = 0;
for (int i = 0; i < MOT_N; i++) {
g_x[i] = x[i] - m;
s2 += g_x[i] * g_x[i];
}
f[0] = sqrtf(s2 / MOT_N);
amp_spec(g_x, MOT_N, g_a);
f[1] = peak(g_a, fr, df, 1);
f[2] = peak(g_a, 2 * fr, df, 1);
biquad(MOT_BP, g_x, g_y, MOT_N); /* полоса резонанса корпуса */
biquad(MOT_BP, g_y, g_y, MOT_N);
s2 = 0;
for (int i = 0; i < MOT_N; i++) {
float v = g_y[i] * g_y[i];
s2 += v;
s4 += v * v;
g_y[i] = fabsf(g_y[i]); /* огибающая: выпрямление */
}
f[5] = s4 / MOT_N / ((s2 / MOT_N) * (s2 / MOT_N));
m = mean(g_y, MOT_N);
for (int i = 0; i < MOT_N; i++)
g_y[i] -= m;
amp_spec(g_y, MOT_N, g_a);
for (int k = 4; k < 200; k++)
floor += g_a[k];
floor /= 196;
f[3] = peak(g_a, MOT_KO * fr, df, 2) / floor;
f[4] = peak(g_a, MOT_KI * fr, df, 2) / floor;
for (int i = 0; i < 6; i++)
f[i] = log10f(f[i]);
}
int mot_classify(const float f[6])
{
float h[8], best = -3.4e38f;
int c = 0;
for (int j = 0; j < 8; j++) {
float a = MOT_B1[j];
for (int i = 0; i < 6; i++)
a += (f[i] - MOT_MU[i]) / MOT_SD[i] * MOT_W1[i * 8 + j];
h[j] = a > 0 ? a : 0;
}
for (int k = 0; k < 5; k++) {
float a = MOT_B2[k];
for (int j = 0; j < 8; j++)
a += h[j] * MOT_W2[j * 5 + k];
if (a > best) {
best = a;
c = k;
}
}
return c;
}
/* ── 2. Насос: ток к номиналу, неровность тока, лопастная частота, шум 1–2,4 кГц ── */
void pump_features(const float *v, const float *cur, float f[4])
{
const float df = PUMP_FS / PUMP_N;
float s = 0, bm = 0, bs = 0, blk[20];
for (int i = 0; i < PUMP_N; i++)
s += cur[i] * cur[i];
f[0] = sqrtf(s / PUMP_N) / PUMP_INOM;
for (int b = 0; b < 20; b++) { /* 20 периодов сети по 100 отсчётов */
float q = 0;
for (int i = 0; i < 100; i++)
q += cur[b * 100 + i] * cur[b * 100 + i];
blk[b] = sqrtf(q / 100);
bm += blk[b];
}
bm /= 20;
for (int b = 0; b < 20; b++)
bs += (blk[b] - bm) * (blk[b] - bm);
f[1] = sqrtf(bs / 20) / bm;
float m = mean(v, PUMP_N);
for (int i = 0; i < PUMP_N; i++)
g_x[i] = v[i] - m;
amp_spec(g_x, PUMP_N, g_a);
f[2] = log10f(peak(g_a, PUMP_VANES * 2900 / 60.0f, df, 3));
s = 0;
for (int k = (int)(1000 / df + 0.5f); k < (int)(2400 / df + 0.5f); k++)
s += g_a[k] * g_a[k];
f[3] = log10f(sqrtf(s / 2));
}
/* base — медианы lg A_bpf и lg шума этого насоса по 20 окнам при пуске */
int pump_classify(const float f[4], const float base[2])
{
float x[4] = { f[0], f[1], f[2] - base[0], f[3] - base[1] };
int n = 0;
while (PUMP_LEFT[n] >= 0)
n = x[PUMP_FEAT[n]] <= PUMP_THR[n] ? PUMP_LEFT[n] : PUMP_RIGHT[n];
return PUMP_CLASS[n];
}
/* ── 3. Вентилятор: 18 полос по 16 кадрам 512, расстояние до нормы ── */
void fan_features(const float *x, float f[FAN_NB])
{
static float p[FAN_FR / 2 + 1];
for (int k = 0; k <= FAN_FR / 2; k++)
p[k] = 0;
for (int fr = 0; fr < FAN_NF; fr++) {
amp_spec(x + fr * FAN_FR, FAN_FR, g_a);
for (int k = 0; k <= FAN_FR / 2; k++)
p[k] += g_a[k] * g_a[k] / FAN_NF;
}
for (int j = 0; j < FAN_NB; j++) {
float e = 0;
for (int k = FAN_KB[j]; k < FAN_KB[j + 1]; k++)
e += p[k];
f[j] = 10.0f * log10f(e + 1e-12f);
}
}
float fan_score(const float f[FAN_NB])
{
float d[FAN_NB], s = 0;
for (int i = 0; i < FAN_NB; i++)
d[i] = f[i] - FAN_MU[i];
for (int i = 0; i < FAN_NB; i++) {
float r = 0;
for (int j = 0; j < FAN_NB; j++)
r += FAN_P[i * FAN_NB + j] * d[j];
s += d[i] * r;
}
return s;
}
int fan_anomaly(const float f[FAN_NB])
{
return fan_score(f) > FAN_THR;
}
/* ── 4. Утечка: медиана по 16 кадрам уровней 35–45 и 15–25 кГц минус своя база ── */
static float band_db(int k0, int k1)
{
float e = 0;
for (int k = k0; k < k1; k++)
e += g_a[k] * g_a[k];
return 10.0f * log10f(e / 2 + 1e-15f);
}
static float median16(float *v)
{
for (int i = 1; i < 16; i++) /* вставками: 16 чисел */
for (int j = i; j > 0 && v[j - 1] > v[j]; j--) {
float t = v[j]; v[j] = v[j - 1]; v[j - 1] = t;
}
return 0.5f * (v[7] + v[8]);
}
void leak_features(const float *x, const float base[2], float f[2])
{
float u[LEAK_NF], m[LEAK_NF];
for (int fr = 0; fr < LEAK_NF; fr++) {
amp_spec(x + fr * LEAK_FR, LEAK_FR, g_a);
u[fr] = band_db(LEAK_US0, LEAK_US1);
m[fr] = band_db(LEAK_MID0, LEAK_MID1);
}
f[0] = median16(u) - base[0];
f[1] = median16(m) - base[1];
}
int leak_detect(const float f[2])
{
return LEAK_W[0] * f[0] + LEAK_W[1] * f[1] + LEAK_B > 0.0f;
}
/* ── 5. Детали на конвейере: карта центров 40 × 40 и пики ── */
#define DET_R (DET_P / 2)
#define DET_O (DET_W - 2 * DET_R)
void det_heatmap(const float *img, float *hm)
{
float s = 0, s2 = 0, z[DET_P * DET_P];
for (int i = 0; i < DET_W * DET_W; i++)
s += img[i];
s /= DET_W * DET_W;
for (int i = 0; i < DET_W * DET_W; i++)
s2 += (img[i] - s) * (img[i] - s);
float inv = 1.0f / (sqrtf(s2 / (DET_W * DET_W)) + 1.0f);
for (int y = 0; y < DET_O; y++)
for (int x = 0; x < DET_O; x++) {
float pm = 0, out = DET_B2[0];
for (int dy = 0; dy < DET_P; dy++)
for (int dx = 0; dx < DET_P; dx++)
pm += z[dy * DET_P + dx] = img[(y + dy) * DET_W + x + dx];
pm /= DET_P * DET_P;
for (int i = 0; i < DET_P * DET_P; i++)
z[i] = (z[i] - pm) * inv;
for (int j = 0; j < DET_HID; j++) {
float a = DET_B1[j];
for (int i = 0; i < DET_P * DET_P; i++)
a += z[i] * DET_W1[i * DET_HID + j];
out += (a > 0 ? a : 0) * DET_W2[j];
}
hm[y * DET_O + x] = out;
}
}
int det_count(const float *hm)
{
int n = 0;
for (int y = 0; y < DET_O; y++)
for (int x = 0; x < DET_O; x++) {
float v = hm[y * DET_O + x];
int top = v > 0.5f;
for (int dy = -1; dy <= 1 && top; dy++)
for (int dx = -1; dx <= 1; dx++) {
int yy = y + dy, xx = x + dx;
if (yy >= 0 && yy < DET_O && xx >= 0 && xx < DET_O && hm[yy * DET_O + xx] > v)
top = 0;
}
n += top;
}
return n;
}
#ifdef ML_HOST
#include <stdio.h>
#include <stdlib.h>
static FILE *fb;
static void rd(void *p, size_t n)
{
if (fread(p, 1, n, fb) != n) {
printf("short read\n");
exit(1);
}
}
static float rel(float a, float b)
{
float d = fabsf(a - b), m = fabsf(b) > 1e-3f ? fabsf(b) : 1e-3f;
return d / m;
}
int main(int argc, char **argv)
{
static float x[LEAK_FR * LEAK_NF], cur[PUMP_N], hm[DET_O * DET_O], hp[DET_O * DET_O];
float f[FAN_NB], fp[FAN_NB], e, fr, base[2], sp;
int n, cls, same, i, k;
fb = fopen(argc > 1 ? argv[1] : "test121f.bin", "rb");
if (!fb)
return 1;
rd(&n, 4); /* мотор */
for (i = same = 0, e = 0; i < n; i++) {
rd(&fr, 4); rd(x, 4 * MOT_N); rd(fp, 4 * 6); rd(&cls, 4);
mot_features(x, fr, f);
for (k = 0; k < 6; k++)
e = fmaxf(e, rel(f[k], fp[k]));
same += mot_classify(f) == cls;
}
printf("mot=%d,%d,%.1e\n", same, n, e);
rd(&n, 4); rd(base, 8); /* насос */
for (i = same = 0, e = 0; i < n; i++) {
rd(x, 4 * PUMP_N); rd(cur, 4 * PUMP_N); rd(fp, 4 * 4); rd(&cls, 4);
pump_features(x, cur, f);
for (k = 0; k < 4; k++)
e = fmaxf(e, rel(f[k], fp[k]));
same += pump_classify(f, base) == cls;
}
printf("pump=%d,%d,%.1e\n", same, n, e);
rd(&n, 4); /* вентилятор */
for (i = same = 0, e = 0; i < n; i++) {
rd(x, 4 * FAN_FR * FAN_NF); rd(fp, 4 * FAN_NB); rd(&sp, 4); rd(&cls, 4);
fan_features(x, f);
for (k = 0; k < FAN_NB; k++)
e = fmaxf(e, rel(f[k], fp[k]));
e = fmaxf(e, rel(fan_score(f), sp));
same += fan_anomaly(f) == cls;
}
printf("fan=%d,%d,%.1e\n", same, n, e);
rd(&n, 4); /* утечка */
for (i = same = 0, e = 0; i < n; i++) {
rd(base, 8); rd(x, 4 * LEAK_FR * LEAK_NF); rd(fp, 4 * 2); rd(&cls, 4);
leak_features(x, base, f);
for (k = 0; k < 2; k++)
e = fmaxf(e, fabsf(f[k] - fp[k])); /* дБ: абсолютная разница */
same += leak_detect(f) == cls;
}
printf("leak=%d,%d,%.1e\n", same, n, e);
rd(&n, 4); /* детали */
for (i = same = 0, e = 0; i < n; i++) {
rd(x, 4 * DET_W * DET_W); rd(hp, 4 * DET_O * DET_O); rd(&cls, 4);
det_heatmap(x, hm);
for (k = 0; k < DET_O * DET_O; k++)
e = fmaxf(e, fabsf(hm[k] - hp[k]));
same += det_count(hm) == cls;
}
printf("det=%d,%d,%.1e\n", same, n, e);
printf("done=1\n");
fclose(fb);
return 0;
}
#endif
Дальше: раздел 121е «Курсовой проект TinyML: своя модель под задачу с нуля и до прошивки — распознавание жестов браслетом на STM32F407» — все шаги части XIV подряд на одной задаче: прошивка сбора, записи 24 человек, разведка и чистка, две модели, int8, прошивка распознавания со сглаживанием и бюджет итога.