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

349 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```powershell
# 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` :
```xml
<PackageVersion Include="Microsoft.ML.OnnxRuntime" Version="1.20.1" />
```
2. `src/Omika.App.Local/Omika.App.Local.csproj` :
```xml
<ItemGroup>
<PackageReference Include="Microsoft.ML.OnnxRuntime" />
</ItemGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants);OMIKA_ONNX_RUNTIME</DefineConstants>
</PropertyGroup>
```
3. `src/Omika.App/AppServices.cs` — une ligne :
```csharp
IOnnxInferenceSessionFactory onnxFactory = OnnxRuntimeInferenceSessionFactory.Instance;
```
**Vérification immédiate** — les deux commandes doivent passer :
```powershell
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.
```powershell
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.**
```powershell
# 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
```powershell
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.
```powershell
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 :
```powershell
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.