Files
Omika/docs/VALIDATION_TRADUCTION_ONNX.md
2026-10-23 18:46:50 +02:00

17 KiB
Raw Permalink Blame History

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_RUNTIME et 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) et TRADUCTION_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)

  1. Directory.Packages.props :
    <PackageVersion Include="Microsoft.ML.OnnxRuntime" Version="1.20.1" />
    
  2. src/Omika.App.Local/Omika.App.Local.csproj :
    <ItemGroup>
      <PackageReference Include="Microsoft.ML.OnnxRuntime" />
    </ItemGroup>
    <PropertyGroup>
      <DefineConstants>$(DefineConstants);OMIKA_ONNX_RUNTIME</DefineConstants>
    </PropertyGroup>
    
  3. 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.cs est 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.onnx ne 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. OnnxTranslationService l'exclut désormais explicitement (nom contenant past), mais autant ne pas le copier.

⚠️ Un modèle Marian a DEUX vocabulaires : source.spm (langue d'entrée) et target.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 est LIVRAISON_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.spm suffit, il sert aux deux sens. Modèle Marian/OPUS-MT : source.spm + target.spm sont obligatoires. Les noms d'entrée du graphe (input_ids vs decoder_input_ids, encoder_hidden_states vs memory) 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 --dossier sur le dossier du sens (langues/pt/versFrancais). La commande lit les poids dans le commun/ voisin et affiche la ligne Jeton cible : >>fra<< — si cette ligne manque, le pack produira la mauvaise langue sans aucune erreur.

Ce qu'il faut regarder, dans cet ordre :

  1. inspecter — ProfilNormalisation, RepliOctetDisponible, et surtout les avertissements. S'il annonce precompiled_charsmap … NON appliquée, c'est attendu (§3.2) : notez-le, ne le corrigez pas encore.
  2. inspecter — les aller-retours texte → ids → texte. Tous doivent être IDENTIQUE. Un DIFFERENT sur du texte ordinaire est le symptôme d'un découpage divergent : passez au §3.3 avant de chercher ailleurs.
  3. 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
  1. 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 de TRADUCTION_HORS_LIGNE_MOBILE.md).
  2. 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.
  3. Dicter en espagnol dans une note interne → le champ doit recevoir du français.
  4. Ouvrir le panneau de dictée → la bascule ⇄ doit redémarrer l'écoute en espagnol.
  5. 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-retour DIFFERENT sur 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é (fi ligature, chiffres pleine largeur 123, 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 .spm ne sont pas versionnés (poids + licences) — voir tests/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.