349 lines
17 KiB
Markdown
349 lines
17 KiB
Markdown
# 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.
|