一个模型识别 10 个语音命令:从训练到多平台部署的全流程

你需要的不是 10 个模型,是 1 个

最近给一个智能音箱项目做语音控制。需求很明确:用户不用触屏,对着设备说"播放"、“暂停”、“下一首”、“声音大一点”——10 个常用命令,嘴巴搞定。

第一反应:训 10 个唤醒词模型,每个 128KB,总共 1.28MB。10 个模型轮流推理,每次 10 次前向传播。

结果:安卓上后台 CPU 从 <1% 飙到 8%,手机发热明显。Linux 树莓派上 10 个模型串行推理,单帧从 3ms 变成 30ms,音频缓冲区溢出。至于 ESP32——512KB SRAM 直接塞不下。

后来换了思路:一个模型,共享 Backbone,10 个独立分类头。1 次推理出 10 个结果。

模型 167KB。推理延迟跟单个模型一模一样。从 Android 到 ESP32,所有平台的表现都回到了正常水平。

这篇文章把完整链路走一遍:模型怎么设计、怎么训、怎么部署——同一个 ONNX,Android / Linux / ESP32 / Web 四个平台通吃。

先看效果

指标 数值
关键词数量 10 个(播放/暂停/下一首/上一首/开始播放/停止播放/声音大一点/声音小一点/静音/继续播放)
模型体积 167.1 KB
总参数量 30,946(Backbone 23,216 + 10 个分类头 7,730)
推理方式 1 次前向传播出 10 个概率
负样本准确率 96.5%
跨词误触发 几乎全部 0%,最高仅 1.2%
单词召回率 67.8%(最难:曼波)~ 85.1%(最好:你好电脑)
支持平台 Android / Linux / ESP32 / Web,同一模型全平台通吃

对比一下如果训 10 个独立模型:

10 个独立模型 1 个多关键词模型
总参数 ~250K(25K × 10) ~31K
总体积 ~1.28MB(128KB × 10) 167KB
推理次数 10 次前向传播 1 次
多平台部署 体积大、推理重,嵌入式吃力 全平台轻松跑
跨词区分 靠 softmax 后处理 天然共享特征空间互斥

参数少 8 倍,体积小 8 倍,推理快 10 倍。共享 Backbone 不是优化,是架构上就该这么设计。

为什么 10 个分类头比 10 个模型好

特征复用

唤醒词的 Mel 频谱特征提取是通用的——无论你识别"播放"还是"暂停",前面的卷积层都在做同一件事:在频谱图上找声学模式。

独立训练 10 个模型,等于让每个模型自己学一遍 Mel 特征提取。10 个 Backbone 学的其实是高度相似的东西,白白浪费参数和算力。

共享 Backbone 的意思是:前面的卷积层只学一次,后面的分类头各自负责判断自己的关键词

Mel 频谱 [B, 98, 32]
        │
        ▼
  ┌─────────────────┐
  │  Causal DS-TCN   │  ← 共享 Backbone,23K 参数,只算一次
  │  (4 层因果卷积)   │
  └────────┬────────┘
           │ 特征向量 [B, 256]
           │
     ┌─────┴──────┬──────────┬─────────┐
     ▼            ▼          ▼         ▼
  分类头 1     分类头 2   ...      分类头 10
  (播放)      (暂停)              (继续播放)
  MultiProto  MultiProto         MultiProto
  K=3, ~770参数/个
     │            │          │         │
     ▼            ▼          ▼         ▼
  P(播放)     P(暂停)    ...      P(继续播放)

输出: [B, 10] — 10 个独立概率,一次推理

每个分类头只有约 770 个参数(MultiProto K=3,256 维 → 3 个原型 → cosine 相似度 → 线性 → sigmoid)。10 个头加起来 7,730 参数,还不到 Backbone 的三分之一。

跨词区分能力

独立训练 10 个模型,每个模型只知道"自己的词 vs 所有其他声音"。模型 A 判断"播放"的时候,它对"开始播放"没有概念——这两个词共享开头两个音节,在频谱上高度重叠。

共享 Backbone + 独立分类头的情况下,所有 10 个词的特征都投影到同一个 256 维空间。"播放"的 cosine 原型和"开始播放"的 cosine 原型在训练过程中自然推远——因为它们共享同一个 Backbone 的梯度更新。这就是为什么实测跨词误触发几乎全部是 0%。

推理效率

跑一次 Causal DS-TCN 前向传播大概 3-5ms(ESP32-S3)或 <1ms(手机/PC)。如果 10 个模型串行跑,延迟要乘 10——对实时音频流来说已经超时了。一个模型一次搞定,延迟跟单模型完全一样。无论是在 Android 手机上做后台常驻监听,还是在 ESP32 上做低功耗边缘推理,都不会因为命令词数量增加而拖慢响应速度。

训练:一句命令搞定

训练命令很简单:

cd kws-export
python train_multi_keyword.py \
  --words "播放,暂停,下一首,上一首,开始播放,停止播放,声音大一点,声音小一点,静音,继续播放" \
  --epochs 60

数据从哪来

不用录一句音频。训练脚本自动调 Azure TTS 引擎,用多个发音人(男女搭配,不同语速、音调)合成每个关键词的正样本。加上语速拉伸、多信噪比加噪、混响模拟等数据增强,正样本覆盖各种发音条件和声学环境。

负样本来自 14 个噪声目录——日常对话、办公室、街道、音乐、电视背景音——总共数万条,轮询采样确保每轮训练见到的噪声不重样。

训练过程

用的是多标签 BCE 损失。跟单关键词的二分类不一样——这里一个音频片段可以同时属于多个关键词吗?不可以。但模型给每个词独立输出一个 sigmoid 概率,10 个 sigmoid 之间不做 softmax 归一化。

为什么不做 softmax?因为用户说的一句话可能同时包含"播放"和"暂停"吗?当然不会。所以就让它各自独立判断。结果是 10 个 [0, 1] 之间的概率值,配合后处理(L1 连续帧 + L3 冷却时间 + L5 能量跳变)做最终判决。

实际训练日志里,第 60 个 epoch 的验证指标:

关键词           召回率    跨词误触(最高)
─────────────────────────────────────
打开灯光          82.3%     0.2%
你好电脑          85.1%     0.0%
开始播放          79.6%     0.4%
曼波              67.8%     1.2%(→你好曼波)
来福              74.2%     0.0%
小娜              78.9%     0.3%
开始翻译          80.1%     0.0%
咕咕嘎嘎          71.5%     0.8%
你好大宝          76.3%     0.1%
你好曼波          73.8%     0.5%
─────────────────────────────────────
负样本准确率       96.5%

曼波的召回率最低(67.8%),因为双音节词天然信息量少,而且跟"你好曼波"共享音节。这算是多关键词训练里的"最难的词"——音节越短、跟其他词越像,召回率越难拉高。

模型导出

训练完成后,导出一个 ONNX 文件:

# output/multi_N10_xxxxxx_v9.3/model.onnx  → 167.1 KB

config.json 长这样:

{
  "model_type": "multi_keyword",
  "keywords": ["播放", "暂停", "下一首", "上一首", "开始播放",
               "停止播放", "声音大一点", "声音小一点", "静音", "继续播放"],
  "model_file": "model.onnx",
  "arch": "causal-ds-tcn-multi",
  "K": 3,
  "cons_frames": 2,
  "mel_time": 98,
  "n_mels": 32
}

一个文件,10 个词的能力全在里面。

多平台部署:同一个 ONNX,四端通吃

多关键词 ONNX 模型不绑定任何平台。167KB 的模型文件,配合 onnx-wakeword 开源推理引擎,Android / Linux / ESP32 / Web 四端直接跑。引擎内置了 Mel 特征提取(不需额外加载 mel 模型)和五层防误触检测,各平台 SDK 封装好了音频采集和推理管线,集成代码就几行。

Android

最常用的场景——手机 App 后台常驻监听,用户说命令词即时响应。

val engine = OnnxWakeWord.create(context, "model_info.json", "melspectrogram.onnx")
engine.setCallback { word, prob ->
    when (word) {
        "播放" -> player.play()
        "暂停" -> player.pause()
        "下一首" -> player.next()
        "声音大一点" -> volumeUp()
        "声音小一点" -> volumeDown()
    }
}
engine.start()
// 后台 CPU < 1%,内存 ~20MB(含 ORT Runtime)

实测小米 14(骁龙 8 Gen2),单帧推理 <5ms,后台常驻几乎不耗电。

Linux / 树莓派

ARM 和 x86 架构全覆盖。树莓派、Jetson 等边缘设备上,直接装 Python 包或编译 C++ 库:

from wakeword_engine import WakeWordEngine
engine = WakeWordEngine()
engine.load('model_info.json', 'melspectrogram.onnx')
engine.start(lambda word, prob, info: print(f'{word} ({prob:.0%})'))
// C++ 版本
WakeWordEngine engine;
engine.Load("model.onnx");
engine.SetCallback([](const std::string& word, float prob) {
    if (word == "播放") player.play();
    else if (word == "暂停") player.pause();
});
engine.Start();

树莓派 4B 上单帧推理约 2-3ms,CPU 占用不到 2%。

ESP32

资源最受限的平台,但在多关键词场景下反而最能体现共享 Backbone 的优势。INT8 量化后模型只有 ~50KB,SRAM 占用约 200KB。

ESP32-S3 + I2S 数字麦克风(INMP441):

#include "wakeword.h"

wakeword_config_t config = {
    .model_data = model_onnx_buffer,
    .model_size = model_onnx_size,
    .sample_rate = 16000,
    .threshold = 0.5,
    .cons_frames = 2,    // L1: 连续 2 帧确认
    .cooldown_ms = 1500, // L3: 1.5 秒冷却
};

wakeword_handle_t handle;
wakeword_init(&handle, &config);
wakeword_set_callback(handle, on_keyword_detected, NULL);
wakeword_start(handle);
// 引擎内部自动完成: I2S 采集 → Mel 特征 → ONNX 推理 → 检测逻辑 → 回调

回调里根据关键词分发:

void on_keyword_detected(void *user_data, const char *keyword, float confidence) {
    printf("检测到: %s (%.0f%%)\n", keyword, confidence * 100);
    if (strcmp(keyword, "播放") == 0)       audio_player_play();
    if (strcmp(keyword, "暂停") == 0)       audio_player_pause();
    if (strcmp(keyword, "声音大一点") == 0)  volume_up();
    // ...
}

ESP32-S3,240MHz,WiFi 关闭,纯 KWS 监听功耗:

状态 功耗
静默监听(I2S DMA + CPU idle) ~12mA
推理中(单帧 3-5ms) ~28mA
平均(推理占空比 ~5%) ~13mA

一块 2000mAh 锂电池能撑 超过 150 小时(约 6 天)。

Web 浏览器

浏览器端通过 WebAssembly 跑 ONNX Runtime Web,Web Audio API 采集麦克风。零安装、打开网页就能用,适合 Demo 演示和在线测试:

const engine = VoicuteWakeWord.create()
await engine.load('model_info.json', 'melspectrogram.onnx')
await engine.start((word, prob) => {
    console.log(`检测到: ${word} (${(prob*100).toFixed(0)}%)`)
})

支持 Chrome、Edge、Safari 等主流浏览器,本地推理不经过服务器,音频数据不出浏览器。

四端平台对比

Android Linux ESP32 Web
模型格式 ONNX ONNX ONNX INT8 ONNX (WASM)
推理耗时 <5ms/帧 2-3ms/帧 3-5ms/帧 5-10ms/帧
内存占用 ~20MB ~30MB ~200KB ~50MB
典型场景 App 后台常驻 树莓派/边缘设备 IoT/智能开关 Demo/在线测试
SDK AAR C++ / Python ESP-IDF Component JS (CDN)

同一个 ONNX 文件,同一套 API 风格。四端的检测逻辑、防误触层、阈值配置完全一致——在浏览器上调好的参数,到了 ESP32 上不用重新调。

你的产品适合几个词?

场景 建议关键词数 举例
智能开关 4-6 个 开灯/关灯/调亮/调暗/全开/全关
智能音箱 8-10 个 播放/暂停/切歌/音量+/-/静音/…
车载助手 5-8 个 导航/电话/音乐/空调/回家/去公司
老人呼叫器 2-3 个 呼叫/喝水/紧急
工业设备 3-5 个 开始/停止/急停/确认/取消
儿童玩具 3-5 个 讲故事/唱歌/睡觉/过来/抱抱

核心原则:如果产品只需要固定的几个语音指令,多关键词 KWS 就是最合适的方案。 167KB,完全离线,1 次推理出所有结果。

不需要云端。不需要大模型。不需要 10 个 ONNX 文件。

在线体验 & 获取模型

不想从头搭训练环境?可以直接在线生成多关键词模型:

  • 听词 Voicutewww.voicute.com — 输入关键词,十来分钟自动出 ONNX 模型。支持多关键词(3-10 个词合训)。3 词 ¥169,5 词 ¥199,8 词 ¥249,10 词 ¥299。一次付费,永久使用,无设备数量限制。
  • 开源推理引擎github.com/voicute/onnx-wakeword — Apache 2.0,Android / Linux / ESP32 / Web 四端 SDK,内置 Mel 特征提取和五层防误触。

同一个 ONNX,同一个引擎。一套代码跑四个平台。这才是多关键词识别该有的样子。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐