zipformer-rnnt-v3 / README.md
trunghlt's picture
Add zipformer-rnnt-v3: meeting wer16 fine-tune, best-valid-loss, model card
2badf20 verified
|
Raw
History Blame Contribute Delete
6.93 kB
---
language:
- vi
license: apache-2.0
tags:
- automatic-speech-recognition
- vietnamese
- zipformer
- transducer
- rnnt
- icefall
- meeting
library_name: icefall
base_model: actableai/zipformer-rnnt-v2
datasets:
- actableai/vi-meeting-soniox-wer16
metrics:
- wer
pipeline_tag: automatic-speech-recognition
---
# zipformer-rnnt-v3
Vietnamese **Zipformer2 transducer (RNNT)** fine-tuned for **Actable meeting audio**, starting from
[`actableai/zipformer-rnnt-v2`](https://huggingface.co/actableai/zipformer-rnnt-v2) and trained on
[`actableai/vi-meeting-soniox-wer16`](https://huggingface.co/datasets/actableai/vi-meeting-soniox-wer16)
with **Soniox transcripts as golden labels**.
| | |
|---|---|
| **Base** | `actableai/zipformer-rnnt-v2` (policy-domain Zipformer, BPE-3000) |
| **Fine-tune data** | Meeting segments with WER(Soniox, baseline Qwen) ≤ 16% |
| **Labels** | Soniox `text` only (mixed-case; not uppercased) |
| **Params** | ~70.7 M (transducer; no CTC head in this fine-tune) |
| **Sample rate** | 16 kHz, 80-dim log-Mel fbank (on-the-fly) |
---
## Performance
### Meeting dev (Soniox references)
Eval set: `session_soniox_dev.jsonl` → 5,453 utterances, ~13.8 h
Decoding: **greedy search**, icefall `greedy_search_batch`
Text norm for WER: lowercase, strip punctuation / bracket tags, collapse whitespace
Checkpoint: `best-valid-loss.pt` using **`model_avg`** weights
| Model | Meeting-dev WER | Errors / words |
|-------|----------------:|---------------:|
| **v2 baseline** (`actableai/zipformer-rnnt-v2`) | **31.07%** | 56,369 / 181,450 |
| **v3 (this model)** | **20.71%** | 37,580 / 181,450 |
| **Δ absolute** | **−10.36** | — |
| **Δ relative** | **−33.3%** | — |
> Measured 2026-07-27 with `eval_zipformer_meeting_wer16.py` (full meeting dev).
### Validation RNNT loss (during fine-tune)
Loss on meeting dev CutSet at the start of each epoch (icefall validation):
| Epoch | Valid loss | Simple | Pruned |
|------:|-----------:|-------:|-------:|
| 1 | 0.4787 | 0.4529 | 0.2577 |
| 2 | 0.3846 | 0.3782 | 0.1805 |
| 3 | 0.3605 | 0.3684 | 0.1725 |
| 4 | 0.3478 | 0.3633 | 0.1662 |
| 5 | 0.3433 | 0.3604 | 0.1631 |
| 6 | 0.3416 | 0.3589 | 0.1622 |
| 7 | 0.3398 | 0.3578 | 0.1609 |
| 8 | 0.3385 | 0.3565 | 0.1603 |
| 9 | 0.3369 | 0.3558 | 0.1590 |
| **10 (best)** | **0.3362** | 0.3552 | 0.1586 |
- Best valid loss: **0.3362** @ epoch 10
- Best train loss: **0.2452** @ epoch 10
- Wall time: ~1 h 38 m (single NVIDIA A100 40GB)
---
## Training details
### Data
| Split | Segments | Hours | Source |
|-------|--------:|------:|--------|
| train | 22,693 | ~68.1 | `actableai/vi-meeting-soniox-wer16` (= local `session_soniox_train_filtered.jsonl`) |
| dev | 5,453 | ~13.8 | `session_soniox_dev.jsonl` (Soniox labels) |
- **Filter:** keep train segments where WER(Soniox ref, baseline Qwen hyp) ≤ **16%**
- **Labels:** Soniox field `text` only — never Qwen hypotheses
- **Audio:** 16 kHz mono session WAVs (`full_session.wav` + start/end offsets)
- **Mix / replay:** none (`use_mux=0`) — pure meeting wer16
- **Utt duration filter:** 0.3–30 s
### Optimization
| Knob | Value |
|------|--------|
| Recipe | icefall Zipformer2 `finetune.py` |
| Init | load `encoder,encoder_embed,decoder,joiner,simple_am_proj,simple_lm_proj` from v2 |
| CTC | **disabled** (`use_ctc=0`) — joint CTC FT was unstable (CTC bias grad explosion under fp16) |
| Optimizer | ScaledAdam + Eden LR schedule |
| `base_lr` | 5e-4 |
| Epochs | 10 |
| Batching | `max_duration=300` s / batch, dynamic bucketing |
| Precision | fp16 |
| Features | on-the-fly 80-dim Kaldi fbank |
| SpecAugment | on (time-warp factor 80) |
| MUSAN | off |
| Speed perturb | off |
| Seed | 42 |
| Hardware | 1× A100-SXM4-40GB |
| Global steps | ~8,674 |
### Architecture (unchanged from v2)
| Param | Value |
|-------|--------|
| `num_encoder_layers` | 2,2,3,4,3,2 |
| `encoder_dim` | 192,256,384,512,384,256 |
| `downsampling_factor` | 1,2,4,8,4,2 |
| `feedforward_dim` | 512,768,1024,1536,1024,768 |
| `num_heads` | 4,4,4,8,4,4 |
| `cnn_module_kernel` | 31,31,15,15,15,31 |
| `encoder_unmasked_dim` | 192,192,256,256,256,192 |
| `decoder_dim` / `joiner_dim` | 512 / 512 |
| Vocab | BPE-3000 (mixed-case Vietnamese; same as v2) |
---
## Files
| File | Description |
|------|-------------|
| `best-valid-loss.pt` | Best icefall checkpoint (`model` + `model_avg` + train metadata) |
| `bpe.model` | SentencePiece BPE-3000 (shared with v2) |
| `tokens.txt` | Token id map for sherpa / icefall |
| `config.json` | Architecture + training metadata |
| `train_zipformer_meeting_wer16.sh` | Launch script used for this run |
| `prep_lhotse_meeting_wer16.py` | Lhotse CutSet prep (Soniox labels) |
| `eval_zipformer_meeting_wer16.py` | Greedy WER eval helper |
---
## Usage (icefall)
```python
import torch, sys
sys.path.insert(0, "/path/to/icefall")
sys.path.insert(0, "/path/to/zipformer_work") # finetune.py / model defs
from finetune import get_model, get_params, add_model_arguments
from beam_search import greedy_search_batch
import argparse, sentencepiece as spm
parser = argparse.ArgumentParser()
add_model_arguments(parser)
params = get_params()
params.update(vars(parser.parse_args([])))
params.encoder_dim = "192,256,384,512,384,256"
params.num_encoder_layers = "2,2,3,4,3,2"
params.downsampling_factor = "1,2,4,8,4,2"
params.feedforward_dim = "512,768,1024,1536,1024,768"
params.num_heads = "4,4,4,8,4,4"
params.cnn_module_kernel = "31,31,15,15,15,31"
params.encoder_unmasked_dim = "192,192,256,256,256,192"
params.decoder_dim = 512
params.joiner_dim = 512
params.causal = False
params.vocab_size = 3000
params.blank_id = 0
params.context_size = 2
params.use_transducer = True
params.use_ctc = False
model = get_model(params)
ckpt = torch.load("best-valid-loss.pt", map_location="cpu")
state = ckpt.get("model_avg") or ckpt["model"]
model.load_state_dict(state, strict=False)
model.eval().cuda()
sp = spm.SentencePieceProcessor()
sp.load("bpe.model")
# feature: (1, T, 80) float32 fbank; feature_lens: (1,) int
with torch.no_grad():
encoder_out, encoder_out_lens = model.forward_encoder(feature, feature_lens)
token_ids = greedy_search_batch(model, encoder_out, encoder_out_lens)
text = sp.decode(token_ids[0])
print(text)
```
---
## Notes / limitations
- Optimized for **Vietnamese meeting / conversational** audio; not re-evaluated on open-domain policy or YouTube suites in this card.
- Greedy decoding only in the reported WER; beam search may improve further.
- Transcripts are **Soniox** pseudo-labels (filtered), not human gold.
- CTC head from v2 was **not** fine-tuned / not present in the shipped transducer-only graph.
## Changelog
| Date | Event |
|------|--------|
| 2026-07-27 | Fine-tune v2 → meeting wer16, 10 epochs; valid loss 0.479 → 0.336 |
| 2026-07-27 | Meeting-dev WER 31.07% (v2) → **20.71%** (v3); push `actableai/zipformer-rnnt-v3` |