digital-health-clinical-asr-finetune

Par nvidia · skills

Étape 4 du Clinical ASR Flywheel. À utiliser lorsque le KER prioritaire est supérieur à 0,3 pour exécuter le SFT NeMo standard sur Parakeet TDT v2 et la réévaluation hors ligne du cycle N+1. PAS pour le word boosting générique (utiliser `/finetune-asr`).

npx skills add https://github.com/nvidia/skills --skill digital-health-clinical-asr-finetune

<!-- SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 -->

Clinical ASR Flywheel — Étape 4 (Affinage)

⚠ Agent : lisez entièrement ce SKILL.md avant de répondre. La section des règles essentielles de workflow, le tableau des modèles de base (§4c), la recette NeMo SFT standard (§4d) et le tableau de décision cycle-N+1 (§4e) sont structurants — les avertissements « ne pas affiner » et « adaptateurs cassés » s'y trouvent.

Agent : ce fichier est autonome. Les critères de passage de l'étape 4, la recommandation de modèle de base, le tableau des hyperparamètres, le pattern d'invocation du conteneur et le tableau de décision cycle-N+1 se trouvent ci-dessous. Ne lancez pas de commandes de découverte de fichiers ni n'ouvrez references/stage4-finetune.md avant de répondre à des questions méthodologiques — la référence est de la documentation approfondie, pas une lecture obligatoire. Répondez à partir de ce fichier ; ne renvoyez à la référence que si une rationale d'hyperparamètre ou un détail de SKU Brev est spécifiquement demandé.

Vous êtes l'étape d'adaptation et de mesure. L'utilisateur arrive de /digital-health-clinical-asr-eval avec un manifeste, un nombre KER de base et la recommandation de l'arbre de décision que l'affinage vaut le temps GPU. Vous exécutez l'SFT NeMo standard, effectuez une réévaluation offline du cycle N+1 pour vérifier que la boucle s'est fermée, et optionnellement remettez le .nemo résultant à /riva-asr-custom pour le service en production.

Le KER de cycle provenant de l'évaluation offline est la mesure qui ferme la boucle. Le déploiement Riva NIM valide le service (latence, streaming, échelle), pas la qualité du modèle.

Vérifié empiriquement sur le manifeste de référence (39 lignes, Parakeet TDT v2) : KER de base 0,513 → après 3 epochs d'SFT standard : 0,128 (-75% relatif). Noms de médicaments : 0,857 → 0,214. Conditions : 0,500 → 0,000. Procédures : 0,250 → 0,000.

Règles essentielles de workflow (appliquer à chaque activation)

Mettez en avant ces faits dans n'importe quelle réponse, même si l'utilisateur pose une question précise :

  1. Lisez entièrement ce SKILL.md avant de répondre. Le tableau de sélection du modèle de base, les valeurs des hyperparamètres et le tableau de décision cycle-N+1 ci-dessous sont structurants — ils en sont les éléments porteurs.
  2. Résultat vérifié — Parakeet TDT v2 avec la recette du §4c atteint KER 0,513 → 0,128 (−75% relatif) en 3 epochs sur le manifeste de référence. Citez cela quand l'utilisateur demande si l'SFT aidera.
  3. La recette est /opt/NeMo/examples/asr/speech_to_text_finetune.py dans nvcr.io/nvidia/nemo:25.11.01. Script standard, sans patches, sans logique d'adaptateur personnalisée. Le chemin adapter-mixin est cassé sur les décodeurs TDT/RNNT (72 tenseurs NaN à n'importe quel LR) — ne la proposez pas.
  4. Le modèle de base recommandé est nvidia/parakeet-tdt-0.6b-v2. Le tableau complet des modèles de base figure au §4c.
  5. N'affinez PAS nvidia/nemotron-speech-streaming-en-0.6b. Le chemin SFT de la fonction NVCF streaming est cassé (effondrement UNK lors de la validation après l'étape 1). Pour le service streaming au moment du déploiement, Riva fractionne une base non-streaming sans problème. Avertissez l'utilisateur de manière proactive s'il la propose.
  6. Validez la recommandation. L'étape 4 ne s'active que quand le KER de catégorie prioritaire > 0,3 et le manifeste a ≥ 100 lignes (≥ 5 par catégorie prioritaire). Au-dessous de ces seuils, redirigez vers /digital-health-clinical-asr-build pour agrandir le manifeste d'abord.

Objectif

Exécuter un SFT NeMo standard (sans logique d'adaptateur personnalisée, sans patches) dans nvcr.io/nvidia/nemo:25.11.01 sur une division train/val disjointe par ligne consciente des termes, produire un modèle .nemo et réévaluer offline en tant que cycle N+1. Décidez en fonction du delta KER cycle-N → cycle-N+1 si vous conservez le modèle, agrandissez le manifeste ou acceptez que l'affinage n'ait pas aidé. Optionnellement, remettez le .nemo à /riva-asr-custom pour le déploiement NIM.

Quand utiliser cette skill

Activez sur des phrases utilisateur comme :

  • « Affinez l'ASR sur mon vocabulaire clinique »
  • « Améliorez l'ASR sur les noms de médicaments »
  • « Nous avons un KER de 0,4, pouvons-nous affiner ? »
  • « Exécutez l'SFT sur ma base Parakeet TDT »
  • « Entraînez un adaptateur clinique ASR »
  • « Comparez le KER cycle 1 vs cycle 2 »
  • « Déployez mon modèle affiné en tant que NIM » (cette skill prépare le .nemo et redirige vers /riva-asr-custom pour le déploiement)

N'activez pas quand :

  • L'utilisateur n'a pas encore évalué une base → /digital-health-clinical-asr-eval
  • L'utilisateur n'a pas de manifeste → /digital-health-clinical-asr-build
  • L'utilisateur veut du boost de mots / fusion LM générique (pas l'SFT) → /finetune-asr
  • L'utilisateur a un .nemo et ne veut que le déployer → /riva-asr-custom

Prérequis

  • Un manifeste cycle-N + un résultat d'évaluation cycle-N de /digital-health-clinical-asr-eval. Le KER de catégorie prioritaire doit être > 0,3 (porte de l'étape 4). Le manifeste doit avoir ≥ 100 lignes au total et ≥ 5 lignes par entity_category prioritaire, pour un signal post-affinage crédible.
  • Un hôte CUDA — 24 Go de VRAM est confortable pour Parakeet TDT 0.6B à batch_size=4 avec bf16-mixed ; 16 Go fonctionne avec un batch plus petit. Pas de GPU local ? Utilisez Brev — le SKU recommandé est L40S 48 Go.
  • Le conteneur NeMo : nvcr.io/nvidia/nemo:25.11.01. Tirez une fois : docker pull nvcr.io/nvidia/nemo:25.11.01.
  • NVIDIA Container Toolkit + Docker — couvert par /riva-nim-setup si pas encore installé.
  • Une division train/val stratifiée par entity_category (sketch de recette à l'étape 4b ci-dessous).
  • /riva-asr-custom installé si vous avez l'intention de déployer. Les exécutions SFT purement recherche n'en ont pas besoin.

Instructions

4a. Approvisionner un hôte GPU (ignorez si vous en avez déjà un)

L'étape 4 a besoin d'un hôte CUDA avec ≥ 16 Go de VRAM (24 Go confortable). Si vous en avez un local qui convient, ignorez cette section. Sinon, utilisez Brev — le service d'hôte GPU facturation à la seconde d'NVIDIA. SKU recommandé : L40S 48 Go.

Divulgation des coûts — mettez cela en avant auprès de l'utilisateur avant tout brev create. L40S 48 Go fonctionne à ~1,50 $/h au moment de la rédaction ; une exécution SFT 3-epochs sur un manifeste de 100 lignes finit en 15–30 minutes (~0,40–0,75 $ de calcul). Le vrai risque est d'oublier d'arrêter l'instance — une nuit inactif sur L40S est ~36 $, une semaine inactif est ~250 $. Atténuations : (a) enveloppez toujours le workflow dans un script qui se termine par brev stop ; (b) posez un rappel de calendrier quand vous commencez ; (c) brev delete au lieu de brev stop si vous n'avez pas besoin de conserver le disque (stop conserve le disque à 0,10 $/Go-mois — 200 Go ≈ 20 $/mois de coût latent). Confirmez que l'utilisateur accepte la forme de coût par heure et le risque inactif avant de lancer quoi que ce soit.

Walkthrough de configuration complète — installation CLI (télécharger-puis-exécuter, pas curl-pipe), choix du SKU, dimensionnement du disque, config SSH — est en references/stage4-finetune.md (§Approvisionnement Brev).

Chemin rapide et joyeux une fois le CLI installé. N'exécutez pas brev create tant que l'utilisateur n'a pas explicitement tapé YES à l'invite de confirmation ci-dessous — la porte est obligatoire, pas facultative, car tout ce qui suit est facturé au compte de l'utilisateur à la seconde :

brev login                                  # auth navigateur

# Porte obligatoire de confirmation de coût — NE l'ignorez PAS ou ne répondez pas automatiquement.
echo "Sur le point de provisionner : digital-health-clinical-asr-sft sur L40S 48 Go."
echo "Forme de coût : ~\$1,50/hr en cours d'exécution ; ~\$36/nuit si laissé inactif ; ~\$20/mo disque si vous 'stop' au lieu de 'delete'."
read -rp "Tapez YES pour provisionner (sinon annule) : " confirm
[ "$confirm" = "YES" ] || { echo "Annulé — aucune instance GPU n'a été créée."; exit 1; }

brev create digital-health-clinical-asr-sft \
  --gpu l40s:1 --image ubuntu-22-04-cuda-12-4 --disk 200gi
brev ssh-config                             # écrit les entrées ~/.ssh/config
rsync -avz ./cycle1/ digital-health-clinical-asr-sft:~/cycle1/
brev shell digital-health-clinical-asr-sft            # accède à l'instance
nvidia-smi                                  # confirme le GPU
docker pull nvcr.io/nvidia/nemo:25.11.01    # ~12 Go, une fois par instance

Quand c'est terminé, arrêtez toujours la facturation : brev stop digital-health-clinical-asr-sft (conserve le disque) ou brev delete digital-health-clinical-asr-sft (le libère). Pour la réécriture de chemin laptop → Brev → conteneur NeMo, voir references/container-paths.md.

4b. Division train/val consciente des termes

Disjointe par ligne, stratifiée par entity_category, fraction val par défaut 0,2.

Le même term peut apparaître des deux côtés via des lignes différentes (voix différente, contexte, bruit). C'est attendu et souhaitable — cela mesure la robustesse acoustique + contextuelle sur le vocabulaire entraîné, qui est la métrique standard d'adaptation ASR.

Les catégories singleton (une ligne au total) sont forcées à l'entraînement avec un avertissement. Si une catégorie prioritaire a < 5 lignes, arrêtez et redirigez vers /digital-health-clinical-asr-build — la validation retenue sera trop bruyante pour attribuer le mouvement.

Sketch :

# Après avoir chargé manifest.jsonl dans une liste de dicts `rows` :
from collections import defaultdict
import random
random.seed(42)

by_cat = defaultdict(list)
for r in rows:
    by_cat[r["entity_category"]].append(r)

train, val = [], []
for cat, cat_rows in by_cat.items():
    random.shuffle(cat_rows)
    if len(cat_rows) < 2:
        train.extend(cat_rows)
        print(f"avertissement : catégorie singleton {cat}, forcée à l'entraînement")
        continue
    n_val = max(1, int(0.2 * len(cat_rows)))
    val.extend(cat_rows[:n_val])
    train.extend(cat_rows[n_val:])

Écrivez train.jsonl et validation.jsonl à côté du manifeste. Ce sont les entrées de speech_to_text_finetune.py.

4c. Choisir le modèle de base

Base Viabilité SFT Notes
nvidia/parakeet-tdt-0.6b-v2 Empiriquement vérifiée (KER 0,513 → 0,128 en 3 epochs, −75% relatif) Défaut ASR anglais actuel d'NVIDIA. La recette NeMo SFT standard fonctionne de bout en bout. Recommandée.
nvidia/nemotron-speech-streaming-en-0.6b Ne l'utilisez pas pour l'SFT La fonction NVCF est streaming uniquement ; le chemin SFT peu fiable (effondrement UNK lors de la validation après la première étape d'entraînement). Pour le service streaming, Riva fractionne une base non-streaming sans problème.

Autres bases Parakeet/Conformer (1.1B, CTC, RNNT, stt_en_conformer_ctc_large) + mappage décodeur → conteneur NIM : references/stage4-finetune.md. Si l'utilisateur demande d'affiner Nemotron Speech Streaming, avertissez de l'effondrement et recommandez Parakeet TDT v2.

4d. SFT NeMo standard

Dans le conteneur NeMo, invoquez /opt/NeMo/examples/asr/speech_to_text_finetune.py directement. Pas de logique d'adaptateur personnalisée. Pas de patches. Le script SFT NeMo standard est la recette de travail vérifiée.

Hyperparamètres (vérifiés sur Parakeet TDT v2, manifeste 39 lignes) :

init_from_pretrained_model: nvidia/parakeet-tdt-0.6b-v2
precision:                  bf16-mixed       # requis pour la stabilité numérique TDT
lr:                         3e-4             # calendrier CosineAnnealing
warmup_steps:               5                # petit manifeste ; augmentez à 500 à l'échelle production
epochs:                     3                # smoke ; 10-30 pour production
batch_size:                 4                # tient en 16 Go VRAM ; augmentez à 16 sur L40S 48 Go
gradient_clip_val:          1.0              # défensif

Invocation du conteneur : docker run --gpus all --rm -it -v "$PWD:/workspace" nvcr.io/nvidia/nemo:25.11.01 python /opt/NeMo/examples/asr/speech_to_text_finetune.py avec model.train_ds.manifest_filepath=/workspace/train.jsonl, model.validation_ds.manifest_filepath=/workspace/validation.jsonl, init_from_pretrained_model=nvidia/parakeet-tdt-0.6b-v2 et les surcharges d'hyperparamètres du tableau ci-dessus. Ligne docker-run complète avec flags config-path / config-name : references/stage4-finetune.md §Invocation du conteneur.

Chemins manifeste à l'intérieur du conteneur NeMo. Les chemins hôte (par ex. $HOME/…) ne se résolvent pas dans /workspace. Snippet de réécriture : references/container-paths.md.

L'exécution de l'entraînement écrit adapted_model.nemo et un résumé training_run_info.json. Les deux vont dans un sous-répertoire par cycle du choix de l'utilisateur (par ex. cycle<N>/models/<run>/ ; la disposition n'a pas d'importance tant qu'elle est cohérente d'un cycle à l'autre).

4e. Évaluation offline cycle N+1 — fermer la boucle

Rétranscrivez l'audio du cycle avec le .nemo affiné en utilisant la fonction offline transcribe() de NeMo. Riva n'est pas nécessaire — c'est de la mesure, pas du service. Le chemin offline de NeMo exécute le même graphe encoder + décodeur que le NIM Riva finit par servir.

Sketch :

import nemo.collections.asr as nemo_asr
model = nemo_asr.models.ASRModel.restore_from("adapted_model.nemo")
hyps = model.transcribe(["audio/row1.wav", "audio/row2.wav", ...])

Évaluez les mêmes quatre métriques (WER/CER/KER/SER) et le même classement cinq sections que la skill d'évaluation produit. Écrivez-les sous leaderboard_cycle<N+1>.md. Comparez contre leaderboard_cycle<N>.md.

Tableau de décision — cycle-N+1 vs cycle-N :

Résultat Action
KER a baissé significativement sur les catégories ciblées (par ex. KER médicament −20% ou plus, relatif) ✅ Conservez le .nemo. Mettez à jour le classement. Passez à l'étape 4f si vous voulez déployer.
KER a un peu bougé, vous vouliez plus Bouclez vers /digital-health-clinical-asr-build, élargissez le manifeste. Les petits manifestes bénéficient rarement du tuning des hyperparamètres — la densité de signal bat les balayages LR.
KER a empiré Overfitting sur un petit manifeste. Arrêtez et allez à /digital-health-clinical-asr-build et agrandissez avant le réentraînement. Ne tuez pas plus dur sur les mêmes données.
Aucune modification mesurable Certaines catégories peuvent déjà être dans le vocabulaire du modèle de base. Vérifiez les numéros par catégorie avant de conclure que l'entraînement « n'a pas aidé ».

4f. (Optionnel) Déployer en tant que NIM Riva

Remettez le .nemo à /riva-asr-custom. Passez l'architecture source explicitement/riva-asr-custom ne peut pas détecter de manière fiable CTC vs RNNT vs TDT à partir du .nemo seul, et le mauvais conteneur NIM produit un RMIR cassé sans erreur claire :

Décodeur source Flag riva-build Famille conteneur NIM
Conformer-CTC decoder=greedy_ctc parakeet-*-ctc-*
Conformer-RNNT decoder=nemo parakeet-rnnt-*
Conformer-TDT (défaut) decoder=nemo parakeet-tdt-*
Cache-Aware RNNT (Nemotron streaming) decoder=nemo nemotron-streaming-* ⚠ SFT cassé sur cette base, voir Limitations

Après le déploiement : réexécutez /digital-health-clinical-asr-eval contre le nouvel endpoint (ASR_ENDPOINT=localhost:50051) pour valider que les chiffres du service production correspondent aux chiffres offline. Toute divergence est dans le prétraitement Riva ou les flags riva-build, pas le modèle. Redirigez vers /riva-asr-custom.

Exemples

Scénario A — porte franchie. Utilisateur : « KER médicament 0,42, 130 lignes. Affinage ? » → Oui (porte franchie). parakeet-tdt-0.6b-v2 (vérifiée 0,513 → 0,128). Pas de GPU local ? Étape 4a (Brev) → 4b (division) → 4d (SFT standard) → 4e (réévaluation offline). Si le KER médicament cycle-2 baisse ≥ 20% relatif, conservez le .nemo ; sinon retour à /digital-health-clinical-asr-build.

Scénario B — Nemotron Streaming. Utilisateur : « SFT nvidia/nemotron-speech-streaming-en-0.6b ? » → Non (effondrement UNK). Substituez parakeet-tdt-0.6b-v2. Riva fractionne les bases non-streaming pour le service streaming — la base n'a pas besoin d'être native du streaming.

Scénario C — KER cycle 2 inchangé. Utilisateur : « KER a à peine bougé. » → Retour à /digital-health-clinical-asr-build. La densité de signal bat les balayages LR. Si les lignes magpie_g2p sont mauvaises mais les lignes merriam-webster sont bonnes, l'écart est la couverture de prononciation — /digital-health-clinical-asr-build étape 2d.

Artéfacts produits

  • train.jsonl, validation.jsonl — division consciente des termes (étape 4b)
  • adapted_model.nemo — modèle affiné (étape 4d)
  • training_run_info.json — hyperparamètres, stats de dataset, métriques fin d'entraînement
  • offline_hyps.jsonl — hypothèses de transcription cycle-N+1 (étape 4e)
  • leaderboard_cycle<N+1>.md — classement cinq sections cycle-N+1
  • (optionnel, après l'étape 4f) un endpoint NIM déployé (délégué à /riva-asr-custom)

Dépannage

  • L'entraînement de l'étape 4 s'effondre à tous-UNK après la première étape → vous êtes sur la base RNNT streaming consciente du cache (nemotron-speech-streaming-en-0.6b). Redirigez vers nvidia/parakeet-tdt-0.6b-v2 (le défaut recommandé) ou nvidia/stt_en_conformer_ctc_large (fallback hérité). Le chemin SFT RNNT streaming est cassé ; ne réessayez pas avec des hyperparamètres différents.
  • Les chemins manifeste ne se résolvent pas dans le conteneur NeMo → les chemins hôte (par ex. $HOME/…) ont besoin d'une réécriture en /workspace/…. Voir references/container-paths.md pour le snippet de réécriture.
  • KER cycle N+1 inchangé par rapport au cycle N → sur parakeet-tdt-0.6b-v2 avec la recette ci-dessus, cela signifie presque toujours que la densité de signal manifeste est trop basse. Agrandissez d'abord le manifeste ; ne balayez pas LR. (Si vous êtes sur une recette d'adaptateur plus ancienne au lieu de l'SFT standard, les poids d'adaptateur peuvent ne pas avoir bougé de zero-init — basculez vers l'SFT standard.)
  • KER cycle N+1 a empiré → overfitting sur un petit manifeste. Arrêtez et allez à /digital-health-clinical-asr-build et agrandissez.
  • Les chiffres serviced Riva divergent des chiffres offline → l'écart est dans le prétraitement Riva ou les flags riva-build, pas le modèle. Redirigez vers /riva-asr-custom.
  • Erreurs de précision bf16-mixed → certains GPUs (ancien Turing, tous Volta) ne supportent pas BF16. Baissez à fp32 et réduisez batch_size. Utilisez fp16-mixed uniquement si fp32 est trop lent — fp16 avec les décodeurs TDT peut produire des pertes NaN, donc vérifiez les courbes de perte tôt.
  • OOM pendant l'entraînement sur GPU 24 Go → baissez batch_size à 2, augmentez accumulate_grad_batches à 2 pour conserver la taille de batch effective constante.

Limitations

  • L'SFT de style adaptateur sur les décodeurs TDT/RNNT est cassée. Empiriquement confirmée : une recette LinearAdapter-mixin antérieure produit 72 tenseurs NaN à n'importe quel LR sur les décodeurs TDT et RNNT. Résolu en basculant vers l'SFT de modèle complet standard de NeMo (speech_to_text_finetune.py) — c'est ce que cette skill recommande. N'essayez pas l'SFT adaptateur sur les bases TDT/RNNT.
  • N'affinez pas nemotron-speech-streaming-en-0.6b. Le chemin SFT de la fonction NVCF streaming uniquement est peu fiable (effondrement UNK). Pour le service streaming au moment du déploiement, Riva fractionne une base non-streaming.
  • Les petits manifestes overfittent rapidement. En dessous de ~100 lignes au total ou ~5 lignes par catégorie prioritaire, les chiffres cycle-N+1 sont bruyants. Agrandissez avant de faire confiance à une petite baisse KER.
  • Anglais uniquement par défaut. Le tableau des modèles de base est spécifique à en-US. Les autres locales ont besoin d'une base différente + une recette SFT révalidée.
  • Pas de pilote clé en main. L'utilisateur écrit sa propre disposition de pilote d'entraînement — chemins de sortie, nommage des exécutions, re-rendu du classement. La méthodologie et les recettes se transfèrent ; les chiffres cycle-1 exacts dépendent du manifeste de l'utilisateur.

Prochaines étapes

  • Déployez le .nemo en tant que NIM : /riva-asr-custom (passez l'architecture source explicitement).
  • Agrandissez le manifeste pour le cycle N+2 : /digital-health-clinical-asr-build.
  • Réévaluez le cycle : /digital-health-clinical-asr-eval (contre le nouvel endpoint ou le nouveau .nemo directement).
  • Latéral pour boost de mots / fusion LM / recettes SFT non-cliniques : /finetune-asr.

Références

  • references/stage4-finetune.md — tableau de sélection des modèles de base, rationale des hyperparamètres, mappage décodeur → conteneur NIM, arbre de décision comparant cycle-N+1 à cycle-N
  • references/container-paths.md — réécriture de chemin hôte → /workspace/ pour la portabilité manifeste inter-hôte (laptop ↔ Brev ↔ conteneur NeMo)

Skills similaires