4、唤醒词模型架构(CNN)
为什么CNN适合唤醒词任务?
这个问题我当年刚接触语音唤醒时也琢磨过。你想想看,唤醒词本质上是在一段连续语音流里,找到特定的那几个音节。这跟图像识别其实有异曲同工之妙。
图像里,CNN通过卷积核捕捉局部特征——比如边缘、纹理。语音里呢?我们把声音转成频谱图后,局部模式就变成了特定频率在某个时间段的能量分布。比如“你好”这个词,它的声母韵母在频谱上会形成独特的纹理。
CNN的平移不变性在这里特别管用。不管你说“你好”是说快了点还是慢了点,卷积核都能在时间轴上滑动匹配到它。我在项目里试过用全连接网络做唤醒词,效果差得离谱——稍微换个口音就崩了。换成CNN后,鲁棒性明显上了一个台阶。
核心优势总结:
- 局部感受野:捕捉音素级别的时频模式
- 参数共享:同一个卷积核扫描整个频谱,参数量可控
- 层次化特征:底层学边缘,高层学音素组合
- 计算高效:相比RNN,CNN可以并行计算
经典CNN架构(2-3层卷积+池化)
嗯,这里要注意,唤醒词模型不需要太深。我见过有人一上来就堆10层ResNet,结果训练慢、过拟合严重,效果还不如3层的小网络。
我个人习惯用2到3层卷积,配合池化层降维。下面这个结构我在多个项目里验证过,稳定可靠:
输入: (T, F, 1) # 时间帧×频率维×单通道
│
├─ Conv2D(32, kernel=(3,3), padding='same')
│ ├─ BatchNorm
│ ├─ ReLU
│ └─ MaxPool2D(pool=(2,2))
│
├─ Conv2D(64, kernel=(3,3), padding='same')
│ ├─ BatchNorm
│ ├─ ReLU
│ └─ MaxPool2D(pool=(2,2))
│
├─ Conv2D(128, kernel=(3,3), padding='same')
│ ├─ BatchNorm
│ ├─ ReLU
│ └─ GlobalAvgPool2D() # 全局平均池化替代Flatten
│
└─ Dense(2, softmax) # 唤醒/非唤醒二分类
我的经验之谈:最后一层用全局平均池化代替Flatten,能大幅减少参数量。我之前有个模型用Flatten后参数量飙到500万,换成全局平均池化直接降到80万,效果还更好。
为什么用2-3层?说白了,唤醒词就那么几个音节,3层卷积提取的特征已经足够区分了。再多一层,边际效益递减,反而容易学到噪声里的偶然模式。
输入张量形状设计
这是最容易踩坑的地方。我见过不少新手直接把原始音频扔进CNN,结果维度对不上报错。咱们得把音频处理成图像格式。
标准的输入形状是:(时间帧数, 频率维数, 通道数)
具体怎么来的?
- 时间帧:比如1秒的音频,用25ms窗长、10ms步长做STFT,得到约100帧
- 频率维:如果FFT点数是512,那频率维就是257(取一半)
- 通道数:单通道用1,如果用LogMel特征就是1,如果用MFCC+Delta+DeltaDelta就是3
举个例子,我常用的配置:
采样率: 16000 Hz
窗长: 25ms (400点)
步长: 10ms (160点)
FFT点数: 512
Mel滤波器组: 40
输入形状: (98, 40, 1)
# 98帧 = (1000ms - 25ms) / 10ms + 1
# 40维 = Mel滤波器个数
# 1通道 = 单特征
我曾经踩过的坑:一开始我直接用原始波形做输入,形状是(16000, 1)。结果CNN根本学不到东西,因为原始波形里没有明显的局部模式。后来换成频谱图,准确率直接从60%跳到92%。记住:CNN吃的是图,不是波形。
还有个细节:时间帧数要固定。我习惯把唤醒词片段统一裁剪到1秒,不够的补零,超出的截断。这样输入形状就是固定的,方便batch训练。
如果你用多通道特征,比如MFCC+一阶差分+二阶差分,那通道数就是3。相当于给CNN提供了“速度”和“加速度”信息,模型能学得更准。不过通道数不是越多越好,我试过加到5个通道,效果提升微乎其微,训练时间倒是翻倍了。
最后说一句:输入形状设计要跟你的模型结构匹配。比如池化层步长是2,那时间帧数最好是2的幂次,不然最后一层维度对不上。我一般用96帧或128帧,省心。