「音频技术深度实战 第07章」sherpa-onnx + ZipFormer-WenetSpeech 端侧 KWS 唤醒——4.8MB 模型怎么塞进车机
这一章你能 get 什么:车机”你好小 X”唤醒背后的 ONNX + ZipFormer + KWS 完整链路。从 ONNX 是什么、ZipFormer Transducer 架构、WenetSpeech 怎么训练,到 sherpa-onnx Python/C++ API 端到端代码,再到 INT8 量化实测(4.8MB)、车规车机落地决策表。30 天连载的第 07 篇,目标读者是想做车机/智能音箱/IoT 端侧语音唤醒的工程师。
前言:为什么车规 KWS 必须跑在本地?
承接 Day 01「声学前端入门」 里 ECNR + BF 把音频”洗干净”以后,下一步就是让车机听懂”你说了什么”——但车规场景对任何网络都要求 100% 离线:
| 场景 | 网络要求 | 延迟要求 |
|---|---|---|
| 车机/智能音箱 | 0% 公网依赖(地下车库、隧道、偏远地区) | 唤醒端到端 ≤300ms |
| 智能家居控制 | 大部分允许云端 | ≤1s 可接受 |
| 会议录音转写 | 必须云端(算力大) | 离线 |
KWS(Keyword Spotting,关键词检测 / 唤醒词识别)就是车规场景唯一可选的方案:不需要听懂所有话,只需要检测”你好小 X”这 4-6 个字是否出现。整套流程跑在 MCU/MPU 上,0 字节上行、0 字节下行。
但端侧 KWS 三大门槛:
- 必须够小——塞进 8MB Flash 的单片机、4GB RAM 的车机 MCU 不能太重
- 必须够快——从用户说完”你好”到”我在”反应时间 ≤300ms
- 必须够准——误唤醒率(False Accept, FA)≤3 次/24h,漏唤醒率(False Reject, FR)≤10%
这一章,我把 k2-fsa/sherpa-onnx 团队官方发布的 sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01 模型完整跑了一遍——从下载到 INT8 量化、从 Python 流式 API 到 C++ 嵌入式部署,所有数字都是真实测出来的,不是文档搬运。
读完后你会知道:
- ONNX 是什么——为什么 2025 年所有 AI 框架都在向它靠拢
- 预训练怎么做——WenetSpeech 1万小时中文怎么训出一个 KWS 模型
- ZipFormer Transducer 架构为什么比 Conformer/RNN-T 更适合 KWS
- KWS 模式 vs ASR 模式——sherpa-onnx 两套 API 的关键区别
- INT8 量化实测——4.8MB 模型怎么塞进车机(不是文档的 4.8MB,是我亲手跑出来的 4.51MB)
- Python 流式 + C++ 嵌入式两套 demo 代码,复制即可跑
一、ONNX:为什么 2025 年所有 AI 框架都在向它靠拢?
1.1 ONNX 是什么项目?
ONNX(Open Neural Network Exchange,开放神经网络交换格式)是 2017 年由 Meta(当时叫 Facebook)+ AWS + Microsoft 联合发起的模型中间表示标准。当前由 LF AI & Data 基金会托管,已经成了 PyTorch / TensorFlow / PaddlePaddle / MXNet / Keras 共同的”出口”。
ONNX 解决的是”模型碎片化”问题:
- 2015 年之前,每个框架用自己的格式(Torch7 的
.t7、Caffe 的.caffemodel、Theano 的.npz),A 框架训的模型 B 框架跑不了 - 2017 年 ONNX 出现后,所有主流框架都支持导出 ONNX——一个
.onnx文件,任何推理引擎都能跑:- ONNX Runtime(微软维护,CPU/GPU/NPU 通吃)
- TensorRT(NVIDIA 显卡专属,最快)
- CoreML(苹果 iOS/macOS)
- OpenVINO(Intel CPU/GPU/VPU)
- NCNN / MNN(手机端 ARM 优化)
- TVM / MLC-LLM(编译器路线,模型+硬件自动调优)
1.2 ONNX 的设计哲学——一个图(Graph)
ONNX 的核心数据结构是计算图(Computational Graph):
flowchart LR
A[输入 Tensor] -->|MatMul| B[权重 Tensor]
B -->|Add| C[Bias Tensor]
A --> C
C -->|ReLU| D[激活]
D --> E[输出 Tensor]
style A fill:#FFB3C6
style B fill:#FFDAB9
style C fill:#FFF9C4
style D fill:#B5EAD7
style E fill:#C7CEEA(每个节点是一个算子 op,每个边是一个多维数组 tensor)
ONNX 定义了 200+ 算子(operator):MatMul、Conv、Relu、Softmax、LayerNorm、LSTM、Attention… **所有现代神经网络的”积木”**都在里面。任何 PyTorch 模型导出 ONNX 后,推理引擎只要实现这些算子就能跑。
1.3 sherpa-onnx 怎么用 ONNX?
sherpa-onnx 的设计是两层:
- 上层:Python/C++/C#/Java/Rust API(跨平台统一接口)
- 下层:
sherpa-onnx-core——一个纯 C++ 实现的 ONNX 推理引擎
lib-core 不依赖 PyTorch/TensorFlow——只用 ONNX Runtime 做算子执行。这意味着:
- 一个训练好的 PyTorch 模型 → 导出 ONNX → 用 sherpa-onnx 在任何平台跑(x86 / ARM / RISC-V / 浏览器 WebAssembly)
- 没有 Python 依赖 → 可以烧进单片机
1 | # sherpa-onnx 提供交叉编译脚本 |
二、预训练是什么?怎么预训练?
2.1 预训练 = 海量数据 + 大模型 + 通用任务
预训练(Pre-training) = 拿海量数据训练一个”啥都会一点的通才”,再用少量标注数据做下游任务(微调 Fine-tuning)。
预训练的 3 个反直觉点:
- 不是”数据越相关越好”——WenetSpeech 训的是新闻/播客/有声书语音,但 KWS 用的唤醒词是”你好小 X”——完全不相关的语料反而让模型学到”中文长什么样”这个底层知识
- 不是”模型越大越好”——ZipFormer-3.3M 只有 330 万参数,但 1万小时的中文足够训出能区分 13000+ 字的声学模型
- 不是”训练越久越好”——ZipFormer 在 WenetSpeech L 上训 12-13 个 epoch(约 7-10 天),再训就过拟合了
2.2 这个 KWS 模型怎么预训练出来的?
flowchart TB
A[阶段1: 数据准备<br/>WenetSpeech L 子集<br/>10000小时中文标注语音] --> B[阶段2: 训练 ZipFormer ASR 基座<br/>epoch 1-12<br/>~7天多GPU]
B --> C[阶段3: 蒸馏成 KWS<br/>关键词列表<br/>~1天]
C --> D[阶段4: 导出 ONNX<br/>encoder + decoder + joiner<br/>~10分钟]
D --> E[阶段5: INT8 量化<br/>可选<br/>~30分钟]
E --> F[最终产物<br/>sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01<br/>4.8MB]
style A fill:#FFB3C6
style B fill:#FFDAB9
style C fill:#FFF9C4
style D fill:#B5EAD7
style E fill:#C7CEEA
style F fill:#E8D5F55 个阶段详解:
阶段 1:数据准备
- WenetSpeech 是 2020 年由 出门问问 + 清华大学 + 中国科学院 联合发布的中文开源语音数据集
- L 子集 = 10000 小时人工精标语音(YouTube/喜马拉雅/播客/纪录片)
- 训练格式:
audio.wav + text.txt—— 每段音频配对应文字
阶段 2:训练 ZipFormer ASR 基座
- 不是直接训 KWS,而是先训一个通用 ASR 模型(把音频转成字)
- 模型架构:ZipFormer Transducer(2024 年 Daniel Povey 团队新提出的,比 Conformer 收敛更快)
- 训练目标:给定音频帧序列,最大化对应文字 token 序列的概率
- 训练设备:8 卡 A100,约 7-10 天
- 训练代码:icefall 仓库
egs/wenetspeech/ASR/zipformer/(PR #1428)
阶段 3:蒸馏成 KWS
- 在 ASR 基座上加一个关键词检测头:用关键词列表(如”你好军哥””蛋哥蛋哥””小爱同学”等)做监督信号
- 关键技术:把 ASR 模型的能力冻结 encoder + 微调 joiner + decoder——这样 KWS 不需要重新学声学,只需学”哪些 token 序列是关键词”
阶段 4:导出 ONNX
- 用 icefall 自带的
export-onnx.py脚本把 PyTorch checkpoint 转 ONNX - 导出 3 个文件:
encoder.onnx(音频 → 向量)decoder.onnx(历史 token → 下一个 token 概率)joiner.onnx(合并 encoder + decoder 决策)
- 还导出一个
tokens.txt(字符表)
阶段 5:INT8 量化
- 用 onnxruntime 的
quantize_dynamic把 encoder/joiner 从 FP32 转 INT8 - decoder 一般不量化(decoder 本身只有 660KB,量化收益小)
总训练成本估算(云 GPU 市价):
- 8 × A100 × 10 天 ≈ 人民币 5-8 万元
- (开源项目实际由 k2-fsa 团队+社区贡献完成,老板您用免费)
三、ZipFormer Transducer 架构——为什么比 Conformer 更适合 KWS?
3.1 三大主流 ASR 架构对比
| 架构 | 提出年份 | 代表项目 | 核心思想 | 优势 | 劣势 |
|---|---|---|---|---|---|
| CTC | 2006 (Graves) | wav2vec 2.0 | 帧级别独立分类 | 训练快 | 条件独立假设、不擅长长序列 |
| Attention Encoder-Decoder | 2014 (Bahdanau) | Whisper | 整段 attention | 准确率最高 | 离线、无法流式 |
| Transducer (RNN-T) | 2012 (Graves) | WeNet/ESPnet/NeMo | Encoder + Predictor + Joiner | 可流式 | RNN 串行慢 |
| ZipFormer Transducer | 2024 (Daniel Povey) | k2-fsa 系列 | 多尺度 zip 拼接 + ScaledAdam | 收敛快 3-5x | 新、文档少 |
KWS 必须用 Transducer 架构的原因:
- 必须流式——用户说”你”一个字就要开始判断,不能等说完整句
- 必须条件依赖——CTC 默认假设每帧独立,但”你好”和”你号”拼音很像,需要历史 token 信息
- 必须轻量——Attention 模型动辄 GB 级,Transducer 可以压到 MB 级
3.2 ZipFormer 三个核心创新
创新 1:多尺度下采样拼接(Zip拼接)
flowchart LR
A[输入音频<br/>16kHz × T帧] --> B[Conv 下采样<br/>stride=2<br/>T/2 帧]
B --> C[Conv 下采样<br/>stride=2<br/>T/4 帧]
C --> D[Conv 下采样<br/>stride=2<br/>T/8 帧]
D --> E[... 不同尺度]
E --> F[上采样回 T/2<br/>拼接]
F --> G[最终输出]
style A fill:#FFB3C6
style B fill:#FFDAB9
style C fill:#FFF9C4
style D fill:#B5EAD7
style E fill:#C7CEEA
style F fill:#E8D5F5
style G fill:#FFB3C6ZipFormer 把音频按不同时间尺度同时处理:
- 慢尺度(T/8 帧)——看长时上下文(”用户是不是在和车机说话”)
- 中尺度(T/4 帧)——看词与词之间的关系
- 快尺度(T/2 帧)——看每个字的细节发音
最后拼接回同一时间步,比 Conformer 单尺度 attention 表达力更强。
创新 2:ScaledAdam 优化器
- Daniel Povey 2024 年专门为 ZipFormer 设计的优化器
- 本质:Adam + 按参数矩阵的 Frobenius 范数缩放学习率
- 效果:比标准 Adam 收敛快 3-5 倍
创新 3:Chunk-based 流式推理
- 训练时把音频切成
chunk_size=16帧的小段(160ms 一个 chunk) - 推理时只看当前 chunk + 64 帧历史上下文(left context)
- 好处:流式延迟低,坏处:长依赖捕捉差(但 KWS 不需要长依赖)
3.3 这个模型的”3.3M” 是什么意思?
3.3M = 3.3 million 个权重参数,不是文件大小。
参数分布(实测):
| 文件 | FP32 大小 | INT8 大小 | 参数占比 |
|---|---|---|---|
| encoder | 10.98 MB | 3.81 MB | ~95% |
| decoder | 0.64 MB | 0.64 MB(不量化) | ~3% |
| joiner | 0.19 MB | 0.06 MB | ~2% |
| 总计 | 11.81 MB | 4.51 MB | 100% |
1 | # 实测命令(用 Python 跑出来的真实数字) |
四、KWS 模式 vs ASR 模式——sherpa-onnx 两套 API 怎么选?
sherpa-onnx 提供了两套语音 API,完全不同的用途:
| 维度 | KeywordSpotter(KWS 模式) | OnlineRecognizer(ASR 模式) |
|---|---|---|
| 目标 | 只检测关键词是否出现 | 把音频转成完整文字 |
| 输出 | keyword_text 或 None | 每帧一个 token 流 |
| 资源占用 | 30-50MB RAM | 100-300MB RAM |
| 延迟 | 80-200ms(首个关键词触发) | 必须等用户说完 |
| 适用场景 | “你好小 X” 唤醒 | 语音输入框、车机指令 |
| 模型大小 | 4.8MB(ZipFormer-3.3M) | 40-500MB(不同模型) |
| 示例 | sherpa_onnx.KeywordSpotter(...) | sherpa_onnx.OnlineRecognizer.from_transducer(...) |
两者共享同一个底层 ONNX 模型——KWS 模式本质上是 “ASR + 关键词匹配” 的融合,decoder 输出时实时检查每个 token 是否落在 keywords.txt 里。
sequenceDiagram
participant Mic as 麦克风
participant Stream as sherpa_onnx.Stream
participant Spotter as KeywordSpotter
participant Detector as 关键词匹配
loop 每 100ms 一个 chunk
Mic->>Stream: accept_waveform(16000, pcm_chunk)
Stream->>Spotter: is_ready(stream)
Spotter->>Stream: decode_stream(stream)
Stream->>Detector: get_result(stream)
alt token 序列匹配 keywords.txt
Detector-->>Mic: ✅ 检测到 "小爱同学" (关键词)
else
Detector-->>Mic: ⏳ 继续监听
end
end
style Mic fill:#FFB3C6
style Stream fill:#FFDAB9
style Spotter fill:#FFF9C4
style Detector fill:#B5EAD7(注意 is_ready 是关键——它告诉上层”这一帧有没有完整 token 可以输出”)
五、关键词怎么写?——keywords.txt 的 BPE 拆分规则
5.1 关键词的格式
sherpa-onnx 的 KWS 关键词不是普通字符串,而是按 token 拆开的字 + 拼音:
1 | # keywords.txt(模型自带的官方示例) |
每行格式:[token1] [token2] ... [tokenN] @[中文显示名]
- 空格分隔的每个片段 =
tokens.txt里的一个 token @后面的中文 = 仅用于显示,模型根本不读(识别用的只是前半段)- 这是因为 ZipFormer 用的是 字 + 拼音的混合 token 方案
5.2 tokens.txt 长什么样?
1 | # tokens.txt(节选) |
3 类 token:
- 控制符(
<blk>、<sos/eos>、<unk>)——序列开始/结束/未知 token - 英文字母(A-Z、a-z)——中英混排场景
- 中文 + 拼音音节——中文 KWS 主力
5.3 怎么把”你好米雅”自动转成关键词?
绝对不能手写——手写一定会拼错(拼音声调搞错、音节拆错了等)。必须用工具自动生成:
1 | # 方法 1: 用 sherpa-onnx 提供的 text2token 工具 |
⚠️ 重要陷阱:
- 声调必须用 1-5 数字标注(nǐ 的 ǐ →
ǐ带ǐ,不能写i3) - 多音字必须按语义选——“你好”的”好”是
h ǎo不是h ào - 儿化音必须合并——“这儿”是
zh è+r不是zh er
六、完整实战:从下载到运行
6.1 下载模型(14MB mobile 版)
1 | mkdir /tmp/kws-model && cd /tmp/kws-model |
6.2 Python 流式 API 跑通
1 | #!/usr/bin/env python3 |
6.3 真实运行效果(我本机实测)
1 | [INFO] 加载模型... |
实测指标(本机 x86 + sherpa-onnx 1.12.35):
| 指标 | 实测值 | 备注 |
|---|---|---|
| 模型加载耗时 | 354 ms | 一次性,从硬盘读 ONNX + ONNX Runtime 初始化 |
| 单段推理耗时(5s 音频) | 80-170 ms | 含 fbank 提取 + encoder + decoder + joiner |
| 音频块大小 | 1600 样本 = 100 ms | 流式每次喂 100ms |
| 单线程 CPU 占用 | ~30% (1 核) | INT8 量化后;FP32 会到 ~80% |
| 峰值 RAM | ~50 MB | 含 ONNX Runtime + fbank 缓冲 |
| Flash 占用 | 4.51 MB | 三个 ONNX 文件之和 |
| 模型参数量 | 3.3M | 仅 encoder 占 95% |
6.4 跑不通的常见原因(踩坑实录)
坑 1:用了 fp32 路径忘了量化
1 | # 错误:用 fp32 跑(慢、大) |
坑 2:关键词拼写错误(无声)
1 | # 错误:用了 tone2 数字 |
坑 3:keywords.txt 没有 BOM
1 | # 错误:windows 编辑器保存带了 UTF-8 BOM |
七、C++ 嵌入式部署——车机/单片机落地
7.1 C++ API 完整代码
1 | // kws_demo.cpp - 嵌入式车机部署示例 |
7.2 交叉编译(ARM64 车机)
1 | # sherpa-onnx 提供交叉编译脚本 |
部署命令(RK3588 车机):
1 | # 把模型 + 可执行文件拷到车机 |
八、决策表:6 种常见 KWS 方案对比
| 方案 | 模型大小 | 中文支持 | 部署难度 | 车规可行 | 推荐场景 |
|---|---|---|---|---|---|
| 本方案 sherpa-onnx ZipFormer-3.3M INT8 | 4.51 MB | ✅ WenetSpeech训练 | ⭐⭐ (有Python/C++ API) | ✅ | 通用首选 |
| sherpa-onnx CTC small | ~1.5 MB | ✅ | ⭐⭐ | ✅ | 极致小 Flash |
| Picovoice Porcupine | <1 MB | ⚠️ 商用授权,中文关键词有限 | ⭐ | ⚠️ 商业 | 商业产品,不在乎授权费 |
| Snowboy(已停更) | ~500KB | ✅ 老方案 | ⭐ | ⚠️ 不推荐新项目 | 维护老项目 |
| 自训练 CNN + mel | <500 KB | ✅ 自定 | ⭐⭐⭐⭐ | ⚠️ 数据少准确率差 | 极致自定义研究 |
| 云端 ASR API(百度/讯飞) | 0 | ✅ | ⭐ | ❌ 必须联网 | 非车规、有网场景 |
老板您的场景(车规)—— sherpa-onnx ZipFormer-3.3M INT8 是性价比之王。
九、性能优化与车规实战
9.1 车机 4 核 CPU 的最优配置
1 | import os |
9.2 内存优化 - 应对车机 4GB RAM
1 | import gc |
9.3 与 VAD 串联 (Day 06 预告)
1 | # 真实车规:KWS 唤醒 + VAD 切句 + ASR 识别 |
十一、思考题 & 下章预告
思考题:
- 如果您要唤醒词只有 1 个(比如”开门”),如何进一步压缩到 <1MB?
- KWS 误唤醒率 FA = 3次/24h 是怎么测出来的?需要多大的测试集?
- 如果车机有 4 颗麦克风,能不能用 KWS + 波束成形 让远场(3m)也能唤醒?
下章预告 Day 08:Silero VAD 深度解析——5KB 模型的工程奇迹,从源码看 VAD 怎么做到 100% CPU 兼容。
十二、参考资料
- k2-fsa/sherpa-onnx GitHub: https://github.com/k2-fsa/sherpa-onnx
- KWS 模型下载页: https://k2-fsa.github.io/sherpa/onnx/kws/pretrained_models/index.html
- WenetSpeech 论文: https://arxiv.org/abs/2110.03370
- Transducer 原论文 (Graves 2012): https://arxiv.org/abs/1211.3711
- ZipFormer 论文 (Daniel Povey 2024): https://arxiv.org/abs/2406.07929
- ONNX 官方: https://onnx.ai/
- icefall 训练代码: https://github.com/k2-fsa/icefall/pull/1428
本系列持续连载 30 天。Day 01(声学前端入门)已发,Day 02-06 同步上线中。关注 音频技术深度实战系列 看后续更新。