smdesai's picture
Remove host code; update README
e3f565c verified
|
Raw History Blame Contribute Delete
7.36 kB
---
license: cc-by-4.0
base_model: nvidia/speakerverification_en_titanet_large
language:
- en
library_name: coreai
tags:
- coreai
- apple
- ios
- on-device
- speaker-verification
- speaker-recognition
- speaker-embedding
- titanet
- audio
---
# TitaNet-Large for Core AI
[NVIDIA TitaNet-Large](https://huggingface.co/nvidia/speakerverification_en_titanet_large) (23M parameters) ported
to Apple's **Core AI** runtime for on-device speaker verification and identification on iPhone, iPad and Mac.
Give it 16 kHz mono speech and it returns NeMo's 192-d speaker embedding, numerically matching the original:
**cosine β‰₯ 0.9999987 against NeMo's `get_embedding` on real recordings from 4 to 65 seconds.**
One small bundle (47 MB) handles audio of any length: the graph is compiled for a single 5.12 s window, and a short
host loop sweeps that window over the clip and rebuilds the model's global statistics exactly. On an iPhone it peaks
at **117 MB** of memory and embeds a 65-second clip in **1.1 s** on the GPU.
## Results
All comparisons are against NeMo's own embedding of the same audio (the `.nemo` checkpoint, `get_embedding`).
| Check | Clips | Cosine to NeMo (min / mean) | Notes |
|---|---|---|---|
| WAV β†’ Python host β†’ Core AI (Mac, GPU) | 40 VoxCeleb1-test, 4.1–64.7 s | **0.9999987** / 0.9999993 | the whole pipeline: mel front end, windowed host, graph |
| NeMo's mel β†’ Swift host β†’ Core AI (iPhone, GPU) | same 40 | **0.9999987** / 0.9999993 | all embeddings finite |
Because the embeddings agree with NeMo's to within 1.3e-6 in cosine, verification behaves as the original does.
NVIDIA reports **0.66 % EER on VoxCeleb1 (cleaned trials)** for the base model; this port was not separately
re-scored on that benchmark.
## Files
| Path | What it is |
|---|---|
| `titanet.aimodel/` | the Core AI bundle: functions `scan` and `head`, fp16 weights, fp32 compute |
| `export.json` | window length (512 frames), context margin (46), function signatures, precision recipe |
| `frontend/mel_fb.f32` | slaney mel filterbank [80, 257], float32 little-endian (the checkpoint's own buffer) |
| `frontend/mel_window.f32` | Hann window [400], periodic=False (the checkpoint's own buffer) |
| `frontend/config.json` | front-end constants: 16 kHz, n_fft 512, hop 160, win 400, 80 mels, 192-d embedding, threshold 0.7 |
## Input
- **16 kHz, mono**, float samples in [-1, 1]. The model was trained on 16 kHz audio; resample anything else first.
- Any length from one 10 ms frame up. A few seconds of speech gives a stable embedding.
- The front end is NeMo's `AudioToMelSpectrogramPreprocessor` in eval mode, reproduced step for step: pre-emphasis
0.97, centred 512-point STFT (hop 160, Hann 400), power, 80 slaney mel bins, `log(x + 2^-24)`, per-feature
normalization over the valid frames. The filterbank and window are the checkpoint's own buffers. Dither is
training-only and is not applied.
## How it works
### Why one window
In a Core AI bundle the activation arena is sized per compiled shape and allocated for every function in the bundle.
A ladder of input-length buckets (the usual way to handle variable-length audio) therefore costs the sum of all its
shapes: 768 MB to over 1.1 GB measured for seven buckets. This port compiles exactly one shape, a 512-frame (5.12 s)
window, and handles longer audio on the host.
### Why a naive window is wrong, and the fix
TitaNet has three reductions over the whole time axis:
1. **five SqueezeExcite gates**: each block scales its channels by `sigmoid(W2 relu(W1 mean_t(x)))` over the entire
utterance;
2. **the pooling mean / std** of the encoder output;
3. **the attention softmax over time** in attentive statistics pooling.
Computing these per window gives the wrong function: chunk-local SqueezeExcite alone drops the cosine to about 0.36.
The graph here makes all three reductions *injectable* (taken as inputs) and *reportable* (partial sums returned as
outputs), so the host can rebuild them exactly:
| Pass | Calls | Rebuilds |
|---|---|---|
| 1–5 | `scan` per window, gate sums fed back as `s0..s4` | the SqueezeExcite fixed point: block *j*'s mean depends only on gates < *j*, so sweep *k* makes gate *kβˆ’1* exact and five sweeps make all five exact |
| 6 | `scan` per window | pooling mean / std from running sums `x_sum`, `x_sqsum` |
| 7 | `scan` per window, with the pooling stats | the attention softmax, merged across windows by running maximum (`lmax`, `den`, `ax`, `axx`) |
| β€” | `head` once | the 192-d embedding from the attentive mean and std |
Every convolution is stride 1 with "same" padding and the stack's receptive field is 93 frames, so each window carries
46 frames of real context on each side and keeps only its interior; the interiors tile the clip exactly once. A clip
of *n* mel frames (10 ms each) uses 1 window if *n* ≀ 512, otherwise ⌈(*n* βˆ’ 512) / 420βŒ‰ + 1, and
**7 Γ— windows + 1** model calls. Host accumulation is float64, and host memory is O(channels), never O(frames).
All five sweeps are needed: the gate residual falls to 6e-4 after two sweeps and *looks* converged while the
embedding cosine is still 0.64. Cosine by number of sweeps: 0.066, 0.639, 0.993, 0.99996, 0.9999987.
### Precision
`wfp16`: weights stored as fp16 (half the bytes), all compute in fp32. A full-fp16 graph was rejected for long
audio: its whole-utterance reductions sum thousands of frames in fp16, and the worst-case cosine fell to 0.72 on
clips up to 65 s, although it looked fine (0.99998) on 4-second clips.
### Interface
`scan`: one window of the clip.
| Input | Shape | Meaning |
|---|---|---|
| `mel` | [1, 80, 512] | the window's normalized log-mel, zero past the clip's end |
| `valid` | [1, 1, 512] | 1 for frames inside the clip |
| `keep` | [1, 1, 512] | 1 for the window's interior frames, whose statistics this call reports |
| `n_inv` | [1, 1] | 1 / (valid frames in the whole clip) |
| `s0`..`s3`, `s4` | [1, 1024] Γ—4, [1, 3072] | SqueezeExcite sums from the previous sweep (zeros on the first) |
| `mean`, `std` | [1, 3072] each | pooling statistics for the attention pass (zeros and ones before they are known) |
Outputs, all [1, C]: `se0`..`se4` (this window's SqueezeExcite contributions), `x_sum`, `x_sqsum`, `lmax`, `den`,
`ax`, `axx` (C = 3072).
`head`: `mu`, `sg` [1, 3072] β†’ `emb` [1, 192].
## License and attribution
This is a derivative of [nvidia/speakerverification_en_titanet_large](https://huggingface.co/nvidia/speakerverification_en_titanet_large)
by NVIDIA, released under [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/); this port is released under the
same license.
Modifications: converted to Core AI with fp16 weight storage, and the network re-authored so its global reductions
can be supplied and reported per window (the attention-pooling concat is folded into its first convolution). The
weights are NVIDIA's; no retraining or fine-tuning was done.
```bibtex
@inproceedings{koluguri2022titanet,
title = {TitaNet: Neural Model for Speaker Representation with 1D Depth-wise Separable Convolutions and Global Context},
author = {Koluguri, Nithin Rao and Park, Taejin and Ginsburg, Boris},
booktitle = {ICASSP 2022 - 2022 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP)},
pages = {8102--8106},
year = {2022}
}
```