Le défi
Une application mobile qui communique avec un instrument de mesure terrain , forage, géotechnique, environnement, doit collecter des signaux en temps réel, les interpréter, et restituer une information exploitable à l'opérateur. Le tout sans connexion internet, sur un smartphone ou une tablette Android durcie.
Le cas concret : des capteurs envoient des flux de données mécaniques (pression, couple, avancement, profondeur, GPS) via liaison sans fil directe. L'application doit, à partir de ces signaux bruts, produire une classification en temps réel de la nature du terrain traversé, sans cloud, sans serveur, intégralement sur le device.
Le modèle de classification (LightGBM, entraîné sur données réelles annotées) existe, mais l'intégrer dans une app React Native soulève des questions d'architecture qui vont bien au-delà du simple appel à une librairie.
L'architecture en trois couches
Le système s'organise en trois grands domaines, strictement séparés :
Couche terrain : L'instrument de mesure communique en WiFi direct avec l'application. Pas de câble, pas d'infrastructure réseau, pas de cloud. Le flux est unidirectionnel : le capteur pousse ses mesures, l'application les reçoit. Un polling à fréquence fixe garantit la fraîcheur des données sans saturer le canal.
Couche applicative : C'est le cœur du système, lui-même divisé en trois responsabilités :
- Acquisition et restitution : l'interface utilisateur reçoit les données, les affiche en temps réel sous forme de courbes et de représentations visuelles. L'opérateur peut interagir (lancer, suspendre, arrêter une mesure) et exporter les résultats.
- Pipeline de transformation : les données brutes ne sont pas consommables directement par le modèle. Une chaîne de transformations successives (normalisation, calibration, fenêtrage, agrégation) les convertit en vecteurs de caractéristiques numériques.
- Inférence et agrégation : le modèle ONNX s'exécute sur le CPU du device, produit des prédictions par point de mesure, qui sont ensuite consolidées par tranche pour donner un résultat stable et exploitable.
Couche stockage : Intégralement locale. Le modèle ML est embarqué dans l'application. Les données de mesure sont persistées en fichier CSV structuré, organisé par chantier. Les métadonnées utilisateur (préférences d'affichage, configuration) utilisent le stockage clé-valeur natif.
┌─────────────────────────────────────────────┐
│ Couche terrain │
│ Instrument → WiFi direct → Flux mesures │
└─────────────────────┬───────────────────────┘
│
┌─────────────────────▼────────────────────────┐
│ Couche applicative │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌─────────┐ │
│ │Interface │ │ Pipeline de │ │Inférence│ │
│ │temps réel│ │transformation│ │ONNX │ │
│ │+ export │ │+ calibration │ │+ vote │ │
│ └──────────┘ └──────────────┘ └─────────┘ │
└─────────────────────┬────────────────────────┘
│
┌─────────────────────▼───────────────────────┐
│ Couche stockage │
│ Modèle embarqué · CSV · Préférences │
└─────────────────────────────────────────────┘
Les décisions d'architecture structurantes
1. Un préprocesseur 100% JavaScript
Le modèle ML attend des vecteurs de caractéristiques normalisées, pas des données brutes. La tentation aurait été d'utiliser Python (numpy, pandas) pour cette étape, puisque c'est comme ça que le modèle a été entraîné, mais Python n'existe pas sur un smartphone React Native.
Deux options :
- Écrire un binding natif (C++/Kotlin) qui réimplémente le preprocessing
- Tout faire en JavaScript pur
Nous avons choisi la seconde. Pourquoi ? Parce que le preprocessing est exclusivement arithmétique : médianes, quantiles, tris, statistiques par fenêtre. Pas de calcul matriciel, pas de GPU. En JavaScript pur, ce code est :
- Testable unitairement sans infrastructure (Jest)
- Sans dépendance externe (pas de versionnage à synchroniser avec le runtime ONNX)
- Performant (quelques dizaines de millisecondes pour des milliers de points)
- Portable (le même code peut tourner sur iOS si besoin)
C'est un contre-pied intéressant à la tendance "tout en natif pour la perf". Ici, le JavaScript est parfaitement adapté.
2. Une calibration par référence embarquée
Un problème classique de la classification sur signaux de capteurs : deux instruments identiques, deux jours différents, deux opérateurs différents, les valeurs brutes peuvent varier significativement, même sur le même matériau. Un modèle entraîné naïvement sur ces valeurs serait fragile et dépendant du contexte.
La solution : avant chaque campagne, une mesure de référence à vide est effectuée. Le modèle ne travaille jamais sur les valeurs brutes, mais sur les écarts à cette référence. Cette approche différentielle rend le système invariant au matériel, aux conditions climatiques, à l'usure mécanique, seul ce qui change par rapport à la référence du jour porte l'information utile.
C'est un pattern qui dépasse la géotechnique : toute application qui classifie à partir de signaux de capteurs en environnement réel devrait envisager une forme de normalisation contextuelle.
3. Une inférence qui ne bloque jamais l'interface
Le runtime ONNX (onnxruntime-react-native) présente une caractéristique d'architecture méconnue : l'appel session.run() est asynchrone côté JavaScript, mais le travail réel s'exécute sur un thread C++ natif dédié, indépendant du thread JS principal.
Concrètement :
- Le thread JS prépare les données et déclenche l'appel
- Le runtime C++ crée un thread secondaire, y exécute l'inférence, et retourne le résultat via une Promise
- Pendant ce temps, le thread JS continue de gérer l'interface, le polling, les animations
C'est exactement ce qu'on veut pour une application temps réel : l'inférence, même sur des lots de plusieurs milliers de points, ne provoque aucune latence perceptible sur l'interface.
4. Une agrégation pour lisser le bruit
Le modèle a été entraîné sur des points de mesure individuels. Inférer point par point produirait des prédictions bruttées et instables. La solution : inférer sur tous les points d'une tranche de profondeur, puis voter.
Le mécanisme est simple : chaque point vote pour une classe, la classe majoritaire l'emporte. En cas d'égalité, c'est la confiance moyenne qui départage. Ce vote majoritaire lisse les points aberrants et ne retient que le comportement dominant de la tranche.
C'est un pattern qu'on retrouve dans beaucoup de systèmes de classification embarquée : plutôt que de chercher à améliorer la précision point-par-point (ce qui serait coûteux en données d'entraînement), on exploite la redondance naturelle des mesures pour gagner en robustesse.
5. Un pipeline temps réel idempotent
En mode temps réel, les données arrivent par lots successifs (polling). Le défi : ne pas re-inférer des tranches déjà traitées, et détecter le moment où une tranche est complète.
L'architecture retenue est un singleton avec état qui :
- S'initialise avec les données de calibration (une fois)
- Ingère les nouveaux points à chaque cycle de polling
- Déclenche l'inférence uniquement quand une tranche est complète (dès qu'un point de la tranche suivante est reçu)
- Garantit l'idempotence : si les mêmes données sont passées deux fois, elles sont ignorées
Le cycle de vie est simple : init() → process() (en boucle) → reset(). Pas de gestion d'état complexe, pas de machine à états. Un singleton, un Set pour l'idempotence, et une règle de complétion de tranche.
Ce qu'on a appris
L'embarqué a ses propres règles
Ce qui marche en backend (inférer par lots, tout charger en mémoire, tolérer 500ms de latence) ne marche pas sur un appareil mobile terrain. Les contraintes sont différentes :
- RAM limitée : pas question de charger tout le dataset en mémoire
- CPU partagé : le thread JS fait tourner le polling, le rendu, les animations ET le preprocessing
- Pas de fallback cloud : si le modèle est corrompu ou absent, l'app doit le détecter et le signaler sans crasher
Le format de sortie du modèle est un maillon faible
Les modèles exportés en ONNX peuvent avoir des formats de sortie différents selon la version de l'exportateur : int64, Int32, string, dictionnaire, parfois selon l'humeur du pipeline. Si l'application suppose un format et reçoit l'autre, le résultat est une erreur silencieuse ou un crash.
La leçon : toujours parser les sorties de manière défensive. Vérifier le type, prévoir un fallback, logger l'écart. Une prédiction fausse vaut mieux qu'un crash, surtout sur un chantier où redémarrer l'app signifie perdre du temps.
La validation croisée Python/JS est indispensable
Le preprocessing a été développé et testé en Python (numpy, pandas) pour l'entraînement, puis réécrit en JavaScript pour l'embarqué. Sans une validation que les deux implémentations produisent exactement les mêmes résultats, bit pour bit, sur les mêmes données, on ne peut pas avoir confiance dans les prédictions.
La méthode : un jeu de données de référence, deux exécutables standalone (Python et JS), et une batterie de tests qui compare les sorties avec une tolérance au bruit Float32. Cette approche devrait être systématique dès qu'on porte un pipeline ML d'un environnement de recherche vers un environnement de production.
La calibration, pas la donnée brute
La décision la plus importante du projet n'est pas technique mais méthodologique : ne jamais travailler sur les valeurs brutes, toujours sur des écarts normalisés à une référence contextuelle. C'est ce qui rend le système robuste en conditions réelles, sur du matériel variable, dans des environnements non contrôlés.
Pour conclure
Ce projet illustre un constat qui devient difficile à ignorer : les modèles ML ne sont plus cantonnés aux serveurs. On peut, et on doit, les embarquer sur les appareils terrain, là où les données sont produites, là où la décision doit être prise.
Mais embarquer un modèle ne se résume pas à appeler une librairie. C'est repenser toute l'architecture : comment normaliser les entrées sans Python, comment garantir la fiabilité sans serveur, comment valider la cohérence entre l'entraînement et l'inférence, comment faire tourner le tout sur un smartphone sous la pluie, avec des gants, à côté d'une foreuse qui vibre.
L'edge AI, en 2025, ce n'est plus un laboratoire. C'est un chantier.
En savoir plus sur Prolog : https://prolog-system.ai/

Top comments (0)