这是 AI 实操第 9 天:端到端收官项目。把第 1~8 天学的 PyTorch、MLflow、ONNX、SNPE 串成一条完整的语音指令识别管线,记录从 44% 到 96% 的调优过程与全部踩坑。 本文首发地址 https://h89.cn/archives/675.html 项目地址 https://gitee.com/chenjim/cockpit-ai-from-zero

背景

第 9 天项目目标:训练一个语音指令分类模型(Speech Commands v2 数据集,35 类),然后部署到车机芯片(SNPE)上。

语音指令是车载座舱的核心交互方式:说 "up" 调高音量,说 "go" 导航。模型需要实时(<100ms)运行在车机芯片上,算力有限,所以模型不能太大。

本文记录从 44.53% 到 96.22% 测试准确率的完整调优过程,包含所有踩坑。

项目全景

全流程:

音频文件 → Mel频谱 → 残差CNN → 训练(MLflow追踪) 
                                         ↓
                                ONNX导出 → DLC转换 → INT8量化 → 真机推理
                                                                     ↓
                                                              评测(准确率+混淆矩阵+延迟)

语言:Python 3.13 + PyTorch 2.11 + torchaudio 2.11(CUDA 12.8),设备 RTX 4060 Laptop。

数据集探索:Speech Commands v2

Google Speech Commands v2 是语音分类领域最常用的基准数据集:

属性
指令数 35 个单词(yes/no/up/down/left/right/go/stop…)
训练集 84,843 条
验证集 9,981 条
测试集 11,005 条
采样率 16,000 Hz(16kHz)
音频时长 ~1 秒,部分略长自动截断
目录结构 每个单词一个文件夹,tree/ 下是所有 "tree" 的录音

数据分布基本均衡,每类约 160~425 条测试样本。最少的类(forward、learn 等)约 155 条,最多的类(up、two)约 425 条。

标签列表

35 类:backward, bed, bird, cat, dog, down, eight, five, follow, forward, four, go, happy, house, learn, left, marvin, nine, no, off, on, one, right, seven, sheila, six, stop, three, tree, two, up, visual, wow, yes, zero

V1~V3:试错过程

V1:随手跑(CPU,MFCC + 简单 CNN)

基线:音频转 MFCC(梅尔频率倒谱系数)→ 3 层 CNN 32→64→128,97.6K 参数。

参数
特征 MFCC 40系数 × 32帧
模型 CNN 32→64→128, 97.6K 参数
轮数 8
训练时间 60 分钟(CPU)
测试准确率 44.53%

35 类只到 45%,比随机(2.9%)好但不可用。分析发现:

  • 模型严重偏向 "up" 类,12 个其他类都被误判为 "up"
  • forward 12.3%, on 17.2%——14 个类不到 40%
  • MFCC 默认参数报 warning(mel 滤波器全零)
  • 无 Dropout、无数据增强、无学习率调度

V2:换 GPU 加轮数

| 改动 | CPU → RTX 4060,轮数 8 → 20 | | 训练时间 | 27 分钟(GPU)| | 测试准确率 | 45.88% |

只提升 1.35pp。瓶颈不是算力,是模型本身。

V3:SpecAugment + 残差网络(快速验证)

这里提前引入两个新东西:SpecAugment(随机遮挡频谱区域,增强模型鲁棒性)和残差网络(用残差连接让梯度可以跳过层直接传递,训练更稳定)。先用快速模式(20% 数据,5 轮)验证:

模型 快速模式准确率
旧 CNN 9.5%
残差 CNN + SpecAugment 11.7%

有提升但不明显。SpecAugment 在数据太少时反而压制性能。

V4:根因发现——MFCC 丢帧

核心发现:MFCC 默认 hop_length=100,1秒音频产生 ~160 帧,代码只取了前 32 帧,丢弃了 80% 的时序信息!

hop_length(帧移)是相邻两帧之间的采样点数。hop=100 时 16KHz 音频产生 160 帧,代码取前 32 帧 ≈ 前 0.2 秒的数据做判断——好比一本书只看前两页。

V5(最终方案):MelSpectrogram + 残差 CNN + Mixup

改进组合

改动 说明
MFCC → Log-Mel 频谱 Log-Mel(取对数的梅尔频谱),跳过 DCT 压缩,保留完整频谱
hop_length 100 → 512 精确对齐 32 帧,一分不丢
简单 CNN → 残差 CNN 4 个 ConvBlock + 残差连接(Residual Connection,让梯度可以跳过层直接传递)+ Dropout(0.5),1.2M 参数
加 Mixup Mixup(把两个训练样本按比例混合,相当于免费扩增数据)
AdamW + 余弦退火 AdamW(带权重衰减的 Adam 优化器变体),余弦退火(学习率按余弦曲线平滑降到 0 的调度方式),防止过拟合
SpecAugment SpecAugment(随机遮挡频谱的频率/时间区域,增强模型鲁棒性)
单样本归一化 每条 Log-Mel 独立归一化到 [0,1],提升训练稳定性

训练曲线分析

从 MLflow 提取的逐 epoch 数据:

Epoch  Val Acc  Train Loss  Learning Rate
    1    90.70%      1.7725      0.998e-03
    5    94.57%      0.9444      0.962e-03
   10    95.70%      0.8501      0.854e-03
   15    96.04%      0.8207      0.691e-03  ← 15 轮后接近饱和
   20    96.10%      0.7600      0.500e-03
   25    96.19%      0.6997      0.309e-03
   29    96.42%      0.7141      0.175e-03  ← 最优
   30    96.42%      0.7106      0.146e-03
   ...
   40    96.31%      0.6966      0.000e+00

关键观察:

  • Epoch 1 就 90.7%——特征换成 Mel 后效果立竿见影
  • Epoch 15 后进入平台期,从 15 到 40 只涨 0.4%
  • 余弦退火让 LR 从 1e-3 平滑降到 0,后期精细调节
  • 训练损失从 1.77 降到 0.70,没有过拟合(验证和测试一致)

因此默认轮数设为 20 轮(~25 分钟),后 20 轮的 0.4% 收益不值得翻倍的时间。

代码架构拆解

1. 数据管道 (train_sc.py: SpeechCommandsMel)

# 波形加载 → 单声道 → 定长 1秒
waveform = raw_dataset[idx]          # [1, 采样点]
waveform = pad_or_truncate(waveform) # 统一 16000 点

# Log-Mel 频谱(n_fft=1024 保证 hop_length=512 能稳定输出 32 帧)
mel = MelSpectrogram(n_fft=1024, hop_length=512, n_mels=64)
mel_feat = mel(waveform)             # [1, 64, 32]
mel_feat = AmplitudeToDB(mel_feat)   # 对数压缩

# 单样本归一化
mel_feat = (mel_feat - min) / (max - min)  # [0, 1]

# SpecAugment(训练时)
mel_feat = FrequencyMasking(mel_feat)  # 随机遮频带
mel_feat = TimeMasking(mel_feat)       # 随机遮时间段

2. 残差 CNN 模型 (train_sc.py: ResidualCNN)

输入 [1, 64, 32]
  → ConvBlock(1→32, pool)   # [32, 32, 16]
  → ConvBlock(32→64, pool)  # [64, 16, 8]
  → ConvBlock(64→128, pool) # [128, 8, 4]
  → ConvBlock(128→256)      # [256, 8, 4]
  → AdaptiveAvgPool → Flatten → Dropout(0.5) → Linear(256, 35)

每个 ConvBlock = Conv → BN(BatchNorm,批归一化)→ ReLU → Conv → BN + 残差连接 → ReLU → (MaxPool)。

残差连接解决梯度消失,让 4 层卷积训练稳定;Dropout(0.5) 防过拟合。

ResidualCNN 网络结构

3. Mixup 增强 (train_sc.py: mixup_data)

lam = Beta(0.2, 0.2).sample()       # 插值系数
idx = randperm(batch_size)           # 随机配对
mixed = lam * x + (1-lam) * x[idx]   # 混合输入
loss = lam * CE(y, y1) + (1-lam) * CE(y, y2)  # 混合损失

4. 余弦退火学习率调度

AdamW 基础 LR=1e-3,CosineAnnealing 从 1e-3 → 0 平滑衰减。后期 LR 很小(~1e-5),让模型做精细调整而非大幅震荡。

为什么 Mel 比 MFCC 强?

Mel 频谱 vs MFCC 对比

MFCC: 音频 → 频谱 → Mel频谱 → log → DCT压缩 → 40维系数
Mel:  音频 → 频谱 → Mel频谱 → log → 64维频谱(保留完整信息)

MFCC 中的 DCT(离散余弦变换)是语音识别时代的产物——为了压缩到 13~40 维给 GMM/HMM 用。对 CNN 分类来说,这个压缩丢弃了高频细节,而这些细节恰好是区分近音词(tree vs three, off vs up)的关键。

知乎上有个比方:MFCC 是画人像素描(只保留关键轮廓),Mel 是高清照片(保留所有纹理)。分类任务要的是纹理,不是轮廓。

评测结果(真实数据)

总体指标

11,005 条测试集,Batch Size=1(模拟端侧逐条推理):

指标
总体准确率 96.22% (10,589/11,005)
验证准确率 96.42%
训练/验证/测试差异 <0.2%(无过拟合)

每类准确率

正确/总数 准确率 说明
stop 409/411 99.51% 最清晰词
yes 415/419 99.05% 第二个音辨识度高
no 401/405 99.01%
left 407/412 98.79%
six 388/394 98.48%
eight 401/408 98.28%
right 389/396 98.23%
seven 398/406 98.03%
zero 409/418 97.85%
nine 399/408 97.79%
two 413/424 97.41%
five 434/445 97.53%
backward 162/165 98.18%
...
tree 165/193 85.49% 最差,和 three 混淆
learn 140/161 86.96% 次差,和 left/no 混淆
forward 139/155 89.68% 和 four/follow 混淆

混淆矩阵 Top 15 分析

真实 → 预测为 次数 原因分析
off → up 20 off 尾音 /f/ 和 up 首音 /ʌ/ 高频特征相似
tree three 18 近音词,辅音 tr- vs thr-
forward → four 11 开头音节 "for-" 完全一致
learn → left 8 开头 "le-" 发音相似
three tree 8 反过来也一样
go → no 7 尾音 oʊ 相近
wow → one 6 首音 w- 容易混
two → go 5 快速发音的前奏
down → no 5 尾音 aʊn→oʊ
on → one 5 on 尾音 n 被当成 one 的后半
on → up 5 短单词,能量包络相似
off → on 4 短单词,元音 /ɒ/ 相近
dog → go 4 dog 尾音 /g/ + 停顿被当成了 go

最有规律的混淆模式:

  • tree ↔ three:英语中唯一的最小对立对,辅音簇 tr- vs thr-
  • forward → four/follow:音节匹配导致前半段就走偏
  • off → up:无摩擦音的短单词之间混淆
  • 数字系列:单音节数字容易互相串(one/two/go/no)

推理延迟(RTX 4060, Batch=1)

统计量
均值 5.65 ms
P50(中位数) 3.16 ms
P90 5.31 ms
P99 57.78 ms
最小 1.60 ms
最大 87.72 ms

P50 只有 3.16ms,远低于车载端侧通常要求的 <100ms。GPU 推理不是瓶颈,瓶颈在特征提取(Mel 频谱计算)。

注意:这是 PC GPU 延迟,真机(手机 DSP/CPU)的延迟会高很多,需要 SNPE 量化到 INT8 后重新测试。

模型偏置分析

模型预测分布基本均匀,没有明显偏置:

预测最多的词 预测次数 占比
up 451 4.1%
five 445 4.0%
two 432 3.9%
no 426 3.9%
stop 422 3.8%

最差 5%~4%,没有严重偏置,对比 V1 的 "up 垄断" 是质的飞跃。

部署管道

训练完成后,模型需要部署到车机芯片(高通 SNPE):

1. ONNX 导出

dummy = torch.randn(1, 1, 64, 32)  # [batch, channel, mel, time]
torch.onnx.export(model, dummy, "speech_cmd_cnn.onnx",
    input_names=["mfcc_input"],
    output_names=["logits"],
    opset_version=13)

导出的 ONNX 只包含 Conv/BatchNorm/ReLU/Add/Pool 等标准算子,opset 13 兼容 SNPE 2.x。Mel 特征提取在 Python 侧完成,不进 ONNX。

2. ONNX → DLC(SNPE 专有格式)

snpe-onnx-to-dlc \
    --input_network speech_cmd_cnn.onnx \
    -d mfcc_input 1,1,64,32 \
    --output_path speech_cmd_cnn.dlc

3. INT8 量化

# 首先生成 500 条校准数据(从训练集取)
python3 prep_calibration_sc.py 500

# 然后量化
snpe-dlc-quantize \
    --input_dlc speech_cmd_cnn.dlc \
    --output_dlc speech_cmd_cnn_int8.dlc \
    --input_list calibration_list.txt \
    --act_bitwidth 8 \
    --use_per_channel_quantization

量化校准数据也是 Log-Mel 特征(64×32=2048 float32),写入 .raw 文件共 8KB/条。

4. 真机推理(adb)

adb push model.dlc /data/local/tmp/
adb shell "cd /data/local/tmp && snpe-net-run \
    --container model.dlc --input_list input_list.txt"

踩坑大全

1. MFCC hop_length 的隐藏坑(最致命)

MFCC 默认 hop_length=100 → 16KHz 音频产生 160 帧 → 代码只取前 32 帧。

症状:模型学了 0.2 秒音频做分类,相当于盲人摸象。 解决:换 MelSpectrogram,设置 hop_length=512 精确对齐 32 帧。

2. Windows FFmpeg DLL 依赖

torchaudio 2.11+ 在 Windows 上强制依赖 FFmpeg 的 DLL 文件来加载音频:

if os.name == "nt":
    os.add_dll_directory(r"D:\tools\ffmpeg-8.1.2-full_build-shared\bin")

必须import torchaudio 之前调用。下载 FFmpeg "full-build-shared" 版本。

3. --quick 模式不能代表全量结果

SpecAugment 在小数据(20%)上表现更差,但在全量数据上效果好。快速模式用来调试 bug,不要用来评估方案。

4. 损失很低 ≠ 准确率高

快速模式下训练损失 0.17 但验证准确率只有 19.6%——典型的过拟合信号。

5. Python 3.13 + CUDA 12.8 兼容性

pip 安装时 torch==2.11.0+cu128 的 +cu128 本地版本标签在 MLflow 中会生成警告(pip requirement 没有本地标签),不影响运行。

6. 训练中断恢复

40 轮原计划跑完,第 37 轮被超时中断(~55min),最佳模型已保存。PyTorch checkpoint 机制天然支持恢复——每次 epoch 结束保存最佳,不用担心突然中断。

最终对比

版本 设备 特征 模型 准确率 关键教训
v1 CPU MFCC 简单 CNN 97K 44.5% 基线
v2 GPU MFCC 简单 CNN 97K 45.9% 算力不是瓶颈
v3 GPU MFCC 残差 CNN 1.2M 11.7%* 小数据不可信
v5 GPU Log-Mel 残差 CNN 1.2M + Mixup 96.22% 特征决定天花板

*快速模式(20% 数据)下的结果;同模式下旧 CNN 为 9.5%

参数分布说明

最终模型(ResidualCNN)参数共约 122 万,分布如下:

模块 参数量 占比
ConvBlock (1→32) Conv2d×2 + BN×2 + skip 9,664 0.8%
ConvBlock (32→64) Conv2d×2 + BN×2 + skip 57,600 4.7%
ConvBlock (64→128) Conv2d×2 + BN×2 + skip 229,888 18.8%
ConvBlock (128→256) Conv2d×2 + BN×2 + skip 918,528 75.0%
Head AdaptiveAvgPool + Linear(256→35) 8,995 0.7%
合计 1,224,675 100%

Head 部分参数极少(0.7%),但它是整个模型的"决策层":

  • AdaptiveAvgPool2d(1):把最后卷积层输出的 256 张特征图各自取全局平均值,变成 256 个数值,不增加参数
  • Flatten:把 [256,1,1] 展平成 256 维向量,不增加参数
  • Dropout(0.5):训练时随机丢弃一半神经元,防止过拟合;推理时关闭,不增加参数
  • Linear(256, 35):真正的分类层,256 个输入 → 35 个输出,参数 = 256×35 + 35 = 8,995

最后一块(128→256)独占 75% 参数量,因为 Conv2d 参数量与输入输出通道数乘积成正比。模型从 32×16 逐渐下采样到 8×4,最后用 256 通道补偿空间分辨率损失。

总结

  1. 特征工程决定天花板——Mel→MFCC 的差距远大于模型架构的差距
  2. 每次只改一个变量——MFCC→Mel + 简单CNN→残差 + 加Mixup 分步验证,知道哪个改动有效
  3. 看数据,不要只看汇总——V1 看似 45%,实际 14 个类不到 40%,"up" 垄断问题汇总指标看不出来
  4. 端侧部署前置考虑——ONNX opset 版本、量化方案在模型设计阶段就要确定
  5. MLflow 记录一切——5 次运行的参数、指标、模型全在数据库里,事后分析不靠记忆
  6. 20 轮够了——训练曲线在 15 轮后平坦,不要盲目加轮数

MLflow 实验记录

实验名: speech-commands
数据库: day09-H-e2e-voice-cmd/mlflow.db
查看: cd day09-H-e2e-voice-cmd && mlflow ui --port 17893
运行 说明 Run ID
v1 CPU, 8轮, MFCC+简单CNN, 44.5% 4a61403961a14b41950f69677b56fe97
v2 GPU, 20轮, MFCC+简单CNN, 45.9% 06ebe665be434e518e71fb49a9dc4277
v3 GPU, --quick, 残差CNN+SpecAugment 2ba7c8672d1149b298450b8c7d317266
v4 GPU, 40轮, Mel+残差CNN+Mixup, 96.47% 37d7832bb7bf4472ad313721add0807d
v5 GPU, 20轮, 同方案, 验证96.42%/测试96.22% e71b9562c1cc40b7ba848f9dd23d779e

下一步计划

  • 针对混淆矩阵里的 tree/threeforward/fourlearn/left 等近音词,调整 SpecAugment 强度或尝试子词建模
  • 尝试更轻量的模型结构(Depthwise Separable Conv 或 MobileNet 风格 CNN),降低端侧延迟
  • 训练流程自动写入 speech_cmd_cnn.onnx,与 convert.sh 无缝衔接
  • 部署实测记录已整理为 blog-speech-commands-deployment.md

系列文章

本系列「车载端侧 AI 工程化从零上手」共 10 篇,建议按序阅读:

  1. 零基础用 PyTorch 识别手写数字:MNIST 实战入门
  2. 把 PyTorch 模型变成 ONNX:导出、验证和可视化一次学会
  3. MLflow 实验追踪:从入门到上手
  4. 用 MNN 在 CPU 上跑 AI 推理:阿里端侧推理框架上手记
  5. 小白用 TensorRT 给模型加速:ONNX 转 Engine 踩坑实录
  6. 零基础上手:高通 SNPE 模型转换实战(从 ONNX 到 DLC)
  7. 零基础搞懂 SNPE 模型量化:INT8 精度损失 = 0% 的秘密
  8. Python 工程化重构:从 print 脚本到 pytest 项目
  9. 车载语音指令识别:从 44% 到 96% 的调优之路
  10. 车载语音指令识别:SNPE 转换与真机部署实测记录

本文链接:车载语音指令识别:从 44% 到 96% 的调优之路 - https://h89.cn/archives/675.html

版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。

标签: ONNX, PyTorch, MLflow, SNPE, 语音指令识别, Speech Commands v2, 车机芯片, 车载座舱

🎓 呈言英语 智能英语学习平台
📚单词学习 🎧听说练习 📖阅读理解 ✏️拼写练习 🌟 AI智能推荐 · 科学记忆曲线
🚀 立即开始免费学习

添加新评论