17 KiB
Valider la traduction ONNX — activation du moteur et mesure de la qualité
Objet : traiter les deux suites de la traduction hors ligne mobile : (1) activer
OMIKA_ONNX_RUNTIMEet faire tourner une vraie paire FR⇄ES ; (2) lever le doute sur le tokeniseur SentencePiece.Cible : poste de développement (accès NuGet) + un appareil Android. Prérequis :
DEPLOIEMENT_API_LANGUES.md(service serveur) etTRADUCTION_HORS_LIGNE_MOBILE.md(côté application) déjà lus.
1. Les deux problèmes ne se traitent pas de la même façon
| Nature | Qui peut le faire | Coût | |
|---|---|---|---|
| Point 1 — activer le moteur | Procédure d'exécution : aucun code à écrire, 3 modifications de projet + une validation à exécuter | Toute machine avec accès NuGet | ~1 h |
| Point 2 — tokeniseur | Travail de code déjà largement fait (§3.1), puis mesure : on ne corrige plus à l'aveugle, on compare à la référence | Développeur | 2 h pour mesurer, variable ensuite |
Règle directrice pour le point 2 : ne jamais « améliorer » le tokeniseur sans mesure. Une normalisation devinée est pire que pas de normalisation : elle produit un découpage différent de la référence de façon invisible. On mesure d'abord (§3.3), on corrige ensuite — et seulement ce qui diverge.
2. Point 1 — activer le moteur et faire tourner une vraie paire FR⇄ES
2.1 Prérequis
# Vérifier que NuGet est joignable (c'est le seul prérequis bloquant)
dotnet nuget list source
# Un pack de langue de test (voir §2.3), et ~5 Go de disque libre
2.2 Étape A — activer le moteur (3 modifications, aucune ligne de code)
Directory.Packages.props:<PackageVersion Include="Microsoft.ML.OnnxRuntime" Version="1.20.1" />src/Omika.App.Local/Omika.App.Local.csproj:<ItemGroup> <PackageReference Include="Microsoft.ML.OnnxRuntime" /> </ItemGroup> <PropertyGroup> <DefineConstants>$(DefineConstants);OMIKA_ONNX_RUNTIME</DefineConstants> </PropertyGroup>src/Omika.App/AppServices.cs— une ligne :IOnnxInferenceSessionFactory onnxFactory = OnnxRuntimeInferenceSessionFactory.Instance;
Vérification immédiate — les deux commandes doivent passer :
dotnet build src/Omika.App.Local/Omika.App.Local.csproj -c Debug
dotnet build tools/Omika.LanguesProbe/Omika.LanguesProbe.csproj -c Debug
OnnxRuntimeInferenceSessionFactory.csest le seul fichier dont la compilation dépend du paquet : c'est voulu. S'il ne compile pas, c'est une incompatibilité d'API avec la version du paquet — corrigez ce fichier uniquement, rien d'autre n'en dépend.
2.3 Étape B — obtenir une paire FR⇄ES réelle
Les modèles de traduction ne sont pas livrés avec Omika (poids + licences). Le plus simple est un modèle OPUS-MT de Helsinki-NLP, exporté en ONNX.
pip install "optimum[onnxruntime]" transformers sentencepiece
# fr -> es (sert le sens « depuisFrancais » du pack « es »)
optimum-cli export onnx --model Helsinki-NLP/opus-mt-fr-es .\export\fr-es
# es -> fr (sert le sens « versFrancais » du pack « es »)
optimum-cli export onnx --model Helsinki-NLP/opus-mt-es-fr .\export\es-fr
L'export produit notamment :
encoder_model.onnx
decoder_model.onnx
decoder_with_past_model.onnx <-- NE PAS LIVRER
source.spm
target.spm
⚠️
decoder_with_past_model.onnxne doit jamais être livré dans le pack. C'est un décodeur à cache de clés/valeurs (past_key_values) destiné à la génération incrémentale : il attend des entrées que ce pipeline ne fournit pas.OnnxTranslationServicel'exclut désormais explicitement (nom contenantpast), mais autant ne pas le copier.
⚠️ Un modèle Marian a DEUX vocabulaires :
source.spm(langue d'entrée) ettarget.spm(langue de sortie). Les livrer tous les deux est obligatoire — voir §2.4.
2.4 Étape C — construire le pack local
Cette étape construit un pack de test sur le poste. Pour livrer un pack en production (dépôt sous
Langues__RootPath, génération du manifeste, contrôles), la procédure dédiée estLIVRAISON_MODELES_LANGUES.md— l' arborescence et les règles de nommage y sont les mêmes.
Arborescence à créer, par exemple dans C:\Omika\TestLangues\es :
es/
├── versFrancais/ (es -> fr)
│ ├── encoder_model.onnx <- depuis .\export\es-fr\
│ ├── decoder_model.onnx <- depuis .\export\es-fr\
│ ├── source.spm <- depuis .\export\es-fr\source.spm (ESPAGNOL)
│ └── target.spm <- depuis .\export\es-fr\target.spm (FRANÇAIS)
└── depuisFrancais/ (fr -> es)
├── encoder_model.onnx <- depuis .\export\fr-es\
├── decoder_model.onnx <- depuis .\export\fr-es\
├── source.spm <- depuis .\export\fr-es\source.spm (FRANÇAIS)
└── target.spm <- depuis .\export\fr-es\target.spm (ESPAGNOL)
Règles de nommage (résolues automatiquement par le service, aucune configuration nécessaire) :
| Fichier | Rôle | Repli si absent |
|---|---|---|
source.spm |
vocabulaire d'entrée | vocab.spm, puis premier .spm |
target.spm |
vocabulaire de sortie | vocab.spm, puis premier .spm |
vocab.spm |
vocabulaire commun aux deux langues (NLLB, mBART, M2M-100) | premier .spm |
encoder_model.onnx / encoder.onnx / tout autre .onnx |
modèle principal | — |
decoder_model.onnx / decoder.onnx |
décodeur séparé | mode « modèle unique » |
*_with_past*.onnx |
ignoré | — |
Modèle à vocabulaire partagé (NLLB, mBART) : un seul
vocab.spmsuffit, il sert aux deux sens. Modèle Marian/OPUS-MT :source.spm+target.spmsont obligatoires. Les noms d'entrée du graphe (input_idsvsdecoder_input_ids,encoder_hidden_statesvsmemory) sont eux aussi résolus automatiquement sur le modèle réellement chargé.
2.5 Étape D — valider sur le POSTE, sans téléphone
C'est le cœur du traitement du point 1 : on ne débogue pas un modèle de 120 Mo par déploiement d'APK.
# 1) Le vocabulaire est-il lisible, et le tokeniseur cohérent ?
dotnet run --project tools/Omika.LanguesProbe -- inspecter .\TestLangues\es\versFrancais\source.spm
# 2) Que produit exactement la tokenisation ? (à comparer à la référence, §3.3)
dotnet run --project tools/Omika.LanguesProbe -- encoder .\TestLangues\es\versFrancais\source.spm "Buenos días, ¿cómo está?"
# 3) La traduction complète, avec les temps de chargement et d'inférence
dotnet run --project tools/Omika.LanguesProbe -- traduire `
--dossier .\TestLangues\es\versFrancais --code es --sens versFrancais `
--texte "Sustitución de la pieza"
Pack familial (modèle multilingue, §2.1.1 de
LIVRAISON_MODELES_LANGUES.md) : pointer--dossiersur le dossier du sens (langues/pt/versFrancais). La commande lit les poids dans lecommun/voisin et affiche la ligneJeton cible : >>fra<<— si cette ligne manque, le pack produira la mauvaise langue sans aucune erreur.
Ce qu'il faut regarder, dans cet ordre :
inspecter—ProfilNormalisation,RepliOctetDisponible, et surtout les avertissements. S'il annonceprecompiled_charsmap … NON appliquée, c'est attendu (§3.2) : notez-le, ne le corrigez pas encore.inspecter— les aller-retourstexte → ids → texte. Tous doivent êtreIDENTIQUE. UnDIFFERENTsur du texte ordinaire est le symptôme d'un découpage divergent : passez au §3.3 avant de chercher ailleurs.traduire— la sortie doit être une phrase française plausible. Pas parfaite : plausible. « Remplacement de la pièce » ✅. Une suite de mots espagnols, du texte répété ou des caractères isolés ❌ → voir §3.4.
Repères de performance à noter (ils décideront de la faisabilité mobile) :
| Mesure | Attendu sur poste | Seuil d'alerte |
|---|---|---|
| Chargement du modèle | < 2 s | > 5 s |
| Inférence d'une phrase courte | < 1 s | > 3 s |
| Aller-retour tokeniseur seul | < 5 ms | > 50 ms |
2.6 Étape E — valider sur Android
dotnet publish src/Omika.App.Android/Omika.App.Android.csproj -c Release -r android-arm64
- Copier le dossier
es\complet dans le stockage de l'appareil, sous<données app>/Omika/langues/es/— ou, plus réaliste, publier le pack sur le service Langues et le télécharger depuis Paramètres → Langue secondaire (c'est aussi le test du §3 deTRADUCTION_HORS_LIGNE_MOBILE.md). - Avant de tester la traduction, vérifier le poids : un modèle Marian fait ~300 Mo pour les deux sens. C'est la décision de livrable la plus lourde de tout le lot.
- Dicter en espagnol dans une note interne → le champ doit recevoir du français.
- Ouvrir le panneau de dictée → la bascule
⇄doit redémarrer l'écoute en espagnol. - Sur une note existante, appuyer sur « ⇄ Traduire » → affichage espagnol, et rien ne doit apparaître dans l'Outbox (vérifier la page Synchronisation).
2.7 Critères go / no-go
Le moteur est validé si et seulement si :
inspecter: aucun aller-retourDIFFERENTsur du texte ordinaire ;traduire: phrases françaises plausibles sur 10 phrases variées (avec accents, chiffres, ponctuation, une phrase longue) ;- inférence < 3 s par phrase sur l'appareil Android cible ;
- poids de l'APK mesuré et accepté ;
- la dictée traduite insère du français, et aucune donnée traduite n'apparaît dans l'Outbox.
Sinon → §3.4.
2.8 Rollback
Retirer les 3 modifications du §2.2 (le symbole, le PackageReference, la
ligne d'AppServices). L'application repasse à
UnavailableOnnxInferenceSessionFactory : la traduction est annoncée
indisponible, rien d'autre ne change. Aucune migration de données, aucun
état à nettoyer — c'est précisément pourquoi le moteur est isolé derrière une
fabrique.
3. Point 2 — le tokeniseur
3.1 Ce qui a été corrigé (et pourquoi c'était faux)
Quatre défauts réels ont été corrigés. Aucun n'était visible sur les modèles les plus courants, ce qui les rendait d'autant plus dangereux :
| Défaut | Conséquence avant correction | État |
|---|---|---|
normalizer_spec ignoré : préfixe ▁ ajouté d'office |
Un modèle formé avec add_dummy_prefix=false (fréquent sur les modèles récents) était découpé — donc traduit — différemment de la référence |
✅ lu et honoré |
| Espaces multiples / de bord jamais réduits | « Bonjour le monde » produisait des marqueurs ▁ parasites absents de la référence |
✅ remove_extra_whitespaces honoré (espace, tabulation, \n, \r, \v, \f) |
escape_whitespaces supposé vrai |
Un modèle à espaces littéraux était découpé de travers | ✅ honoré |
| Repli octet absent | Sur un modèle multilingue, un caractère hors vocabulaire devenait <unk> : les accents et les alphabets non latins étaient purement perdus |
✅ implémenté (encodage et décodage) |
| Un seul vocabulaire par pack | Le défaut le plus grave : un modèle Marian a un vocabulaire par langue. Décoder la sortie avec le vocabulaire d'entrée produit des identifiants interprétés dans le mauvais référentiel → du charabia | ✅ source.spm + target.spm |
Deux points qui n'étaient pas des défauts, vérifiés :
- la pénalité du jeton inconnu (
min_score − 10) est bien celle de la référence (kUnkPenalty = 10.0) ; - un
<unk>par point de code (et non par unité UTF-16) est correct : une paire de substituts ne doit jamais être coupée en deux.
3.2 Ce qui reste approximé — et l'impact attendu
precompiled_charsmap n'est pas appliqué. C'est la table de normalisation
Unicode compilée du modèle (structure de trie à double tableau, format propre à
SentencePiece). La réimplémenter représenterait plusieurs centaines de lignes
pour un gain nul sur la majorité des textes.
Impact réel, à connaître :
- Aucun sur du texte déjà normalisé en NFC (ce que produit tout clavier Android/iOS, et tout ce que l'API renvoie) ;
- Quelques cas sur les formes de compatibilité (
filigature, chiffres pleine largeur123, exposants²) et sur les langues dont l'écriture demande un repli de casse ou une normalisation de présentation.
Le tokeniseur le signale désormais lui-même (Avertissements,
ProfilNormalisation). C'est visible dans inspecter et dans les diagnostics.
3.3 Mesurer la divergence (avant de corriger quoi que ce soit)
La référence est la bibliothèque Python sentencepiece, l'implémentation
d'origine de Google.
pip install sentencepiece
# Génère une fixture de référence par modèle trouvé (récursif)
.\scripts\Generer-Fixtures-SentencePiece.ps1 -DossierModeles .\TestLangues
La fixture contient, pour chaque cas d'essai, les identifiants de référence
et le décodage de référence. Le test SentencePieceFixturesTests les
compare ensuite au tokeniseur Omika :
dotnet test tests/Omika.Tests --filter SentencePieceFixtures
- Dossier de fixtures absent → le test passe (sinon toute compilation sans
modèle serait rouge). Les cas d'essai couvrent accents français, accents et
¿¡espagnols, espaces de bord et doubles, chaîne vide, espaces seuls, chiffres et ponctuation, casse mixte, phrase longue, et un alphabet non latin. - Un échec nomme le cas fautif, le premier index divergent, les deux identifiants et le texte de la pièce : de quoi corriger sans deviner.
- Les
.spmne sont pas versionnés (poids + licences) — voirtests/fixtures/sentencepiece/LISEZMOI.md.
3.4 Arbre de décision selon ce que révèle la mesure
| Symptôme mesuré | Cause la plus probable | Action |
|---|---|---|
Aller-retour DIFFERENT sur du texte ordinaire (accents, ponctuation) |
Divergence de découpage | Comparer avec encoder : si les identifiants diffèrent dès le premier, c'est la normalisation ; s'ils divergent plus loin, c'est le Viterbi |
Divergence uniquement sur les cas de compatibilité (fi, 123) |
precompiled_charsmap non appliqué |
Accepter et documenter : impact réel nul sur une dictée |
| Traduction = suite de mots de la langue source, ou texte répété | Vocabulaire cible faux (ou absent) | Vérifier que target.spm est bien livré dans le sens concerné (§2.4) |
Traduction = <unk>, ▁, ou caractères isolés |
Mauvais vocabulaire, ou modèle non aligné sur le vocabulaire | Vérifier que source.spm/target.spm viennent bien du même modèle que les .onnx |
| Erreur « n'expose aucune des entrées attendues (…) » | Noms d'entrée atypiques | Le message énumère les entrées réelles du modèle : renseigner OnnxTranslationOptions en conséquence |
| Sortie entière ignorée / erreur de type | ONNX Runtime refuse un type inattendu | Corrigé : le type est testé avant AsTensor<T> (qui lève au lieu de retourner null) |
| Divergence après avoir tout vérifié | Vraie limite de l'implémentation | Ouvrir un ticket avec la fixture en échec jointe : elle contient tout ce qu'il faut pour reproduire |
Ce qu'il ne faut PAS faire : assouplir la fixture attendue pour faire passer le test. Une fixture modifiée est une divergence rendue invisible.
4. Hors périmètre (à ne pas confondre)
- Qualité de traduction : ce document valide que le pipeline fonctionne, pas que le modèle traduit bien. Un modèle médiocre restera médiocre — c'est un choix de modèle, pas un défaut de code.
- Poids du livrable mobile : l'activation du moteur ONNX Runtime ajoute la bibliothèque native par architecture, et les modèles pèsent 80 à 300 Mo par langue. C'est une décision produit, pas technique.
- Reprise/streaming du téléchargement : non implémenté (le re-téléchargement repart de zéro par fichier, mais les fichiers déjà conformes sont ignorés).
- Beam search : le décodage est glouton (compromis de latence assumé).
5. Récapitulatif
| # | Action | Qui | Où |
|---|---|---|---|
| 1 | Activer le symbole + le paquet + la fabrique (§2.2) | Dev (NuGet) | 3 fichiers |
| 2 | Exporter une paire FR⇄ES en ONNX (§2.3) | Dev | poste |
| 3 | Construire le pack source.spm + target.spm (§2.4) |
Dev | disque |
| 4 | inspecter / encoder / traduire (§2.5) |
Dev | poste — pas de téléphone |
| 5 | Générer les fixtures et lancer le test (§3.3) | Dev | scripts\ + dotnet test |
| 6 | Trancher avec l'arbre de décision (§3.4) | Dev | — |
| 7 | Publier sur Android et mesurer poids + latence (§2.6) | Dev | appareil |
| 8 | Décider go / no-go (§2.7) | Produit + Dev | — |
Si une seule chose doit être retenue : les étapes 4 et 5 se font sur le poste, en quelques minutes. Tout le reste — déploiement d'APK, téléchargement de 300 Mo sur un téléphone, instrumentation à distance — est un détour.