简介:基于MFCC+CNN的无人机声音识别系统,是一套面向人工智能、通信工程、自动化等专业学生及科研人员的深度学习毕设/课设完整项目。资源包共16个文件,以Python源码为主:9个脚本分别承担数据加载、模型定义、训练、验证与推理等职责,配合1份Markdown说明文档和配置文件,整体压缩包仅37KB,轻量而结构完整。项目目前已有117人学习下载。除代码外,还提供数据目录与详细文档,可直接运行验证,也便于拆解学习MFCC音频特征提取和CNN分类模型构建的完整流程;代码注释清晰、模块划分明确,适合初学者从零入门,也可作为毕业设计、课程设计或项目立项演示的基础工程,方便在此基础上二次开发。
1. 无人机声音识别系统:从这包资料能拿到什么、能做到什么程度
一架无人机从几百米外飞进视野,肉眼要等它靠近才发现,雷达对这类小目标也容易漏检,但麦克风往往更早听到旋翼切割空气的声音。把“听”做成系统,就是标题里的无人机声音识别:MFCC把音频变成特征图,CNN在特征图上判断“有无人机”还是“没有”。这套方案不依赖专用硬件,一台普通电脑配个USB麦克风就能跑通,适合做毕设、声学检测预研和低空安防演示。拿到资料齐全的zip,别急着跑代码,先确认数据格式、标签对齐方式和文档里的训练前提,能省很多返工。下面按特征怎么提、网络怎么建、坑在哪展开,给出的参数都是能直接抄作业的默认值。
2. 把声音变成特征图:MFCC提取的完整参数与代码
2.1 为什么选MFCC:无人机声音恰好落在它分辨率最好的区间
无人机声音不像人声那样有规整的基频和共振峰,它主要由两类成分构成:一是旋翼叶片切割空气产生的节拍性低频噪声,频率集中在几十到几百赫兹;二是电机高速旋转带来的高频啸叫,通常到两三kHz。这两段能量恰好落在梅尔刻度分辨率较好的区域,也就是说MFCC对无人机的辨识信息最敏感。
另一个选MFCC而不是原始波形的理由在维度上。一段3秒、16kHz采样的音频有48000个采样点,直接丢给一维CNN也能学,但计算量浪费在大量相位信息上,而这些相位恰恰在远场环境里最容易被混响和噪声污染。线性频谱保留了完整的谐波细节,噪声鲁棒性一般;梅尔频谱做了对数压缩,已经不错;MFCC再做DCT去相关,把冗余信息进一步压缩成几十维系数,尺寸小、噪声稳,最适合做CNN输入。
| 特征类型 | 每帧维度 | 噪声稳健性 | 直接喂CNN的适配度 |
|---|---|---|---|
| 原始波形 | 1(采样点) | 差 | 低 |
| 线性频谱 | 1025 | 中 | 中 |
| 梅尔频谱 | 128 | 较好 | 较好 |
| MFCC | 40 | 好 | 最好 |
实际工程里我也见过直接用梅尔频谱做输入的方案,效果不算差;但MFCC在样本量不大时更容易收敛,因为去相关之后特征冗余少。资料包如果以MFCC为主,那整体技术路线就是“声学特征提取 + 深度学习CNN分类”的经典组合。
2.2 MFCC提取参数:一次把采样率、帧长、维度的取舍讲清
下面这段代码就是整个系统的特征入口,我一般把采样率固定到16000,时长统一到3秒,静态MFCC取40维。代码里的每个参数后面都会有说明,直接复制就能用。
import librosa import numpy as np def extract_mfcc(path, sr=16000, duration=3.0, n_mfcc=40): y, _ = librosa.load(path, sr=sr, mono=True, duration=duration) # 统一音频长度,短了补零,长了截断 target_len = int(sr * duration) if len(y) < target_len: y = np.pad(y, (0, target_len - len(y))) else: y = y[:target_len] mfcc = librosa.feature.mfcc( y=y, sr=sr, n_mfcc=n_mfcc, n_fft=2048, hop_length=512, fmin=200, fmax=8000, ) # 按MFCC系数维度做标准化,不是把整张图拉平 mfcc = (mfcc - mfcc.mean(axis=1, keepdims=True)) / (mfcc.std(axis=1, keepdims=True) + 1e-6) return mfcc这里的几个参数直接影响后面CNN输入的尺寸和判别效果:
- sr=16000:奈奎斯特频率是8000Hz,恰好覆盖无人机声音的主要能量范围。原始录音如果是44.1kHz,librosa.load会自动重采样,省掉手动处理。
- duration=3.0:3秒是折中值。太短旋翼节拍只出现一两次,特征不足;太长模型判断滞后,部署时报警延迟感人。
- n_mfcc=40:语音识别常用13维,那是为说话人识别优化的;环境声事件分类主流用40维。信息更多,CNN第一层自己会筛。
- n_fft=2048:大约46ms窗长,低频分辨率约7.8Hz,能分辨旋翼叶片的通过频率。窗太长时间分辨率变差,太短低频会糊。
- hop_length=512:帧移约32ms,3秒音频约得到94帧。这个时间轴分辨率对整段判别足够。
- fmin=200, fmax=8000:200Hz以下的风噪和直流分量直接滤掉,8000是采样率一半的上限,避免混叠区干扰。
标准化这行是按每一行MFCC系数做均值方差归一化,而不是把整个矩阵拉平后算一个均值和方差。不同维度的倒谱系数取值范围差异很大,拉平处理会让低能量维度被高能量维度淹没。
注意:代码里是样本内标准化,生产部署时要在训练集上统计全局均值和方差,推理时复用。这点第4章专门讲。
2.3 把静态MFCC叠加成一阶、二阶差分:三通道特征图
单一静态MFCC只反映当前帧的谱包络,无人机声音的辨识信息有很大一部分在“变化”里,比如旋翼节拍的周期性起伏。主流做法是把静态MFCC和一阶差分、二阶差分叠成三个通道,正好对应CNN图像输入的RGB三通道。
import librosa import numpy as np def build_feature(path): mfcc = extract_mfcc(path) # (40, T) delta1 = librosa.feature.delta(mfcc) # 一阶差分 delta2 = librosa.feature.delta(mfcc, order=2) # 二阶差分 feat = np.stack([mfcc, delta1, delta2], axis=0) # (3, 40, T) return feat.astype(np.float32)delta计算的是MFCC系数随时间的变化斜率,delta2是变化率的变化率,类似图像里的边缘信息。模型自己也能通过卷积学出这些变化,但直接把差分特征喂进去,在样本量不大时收敛快很多。这也是为什么很多声音分类项目一定要在特征阶段做这一步,而不是交给网络去硬学。
通道顺序放在axis=0,也就是(3, 40, T)。PyTorch的Conv2d输入格式就是(B, 3, H, W),这里H是40维频率方向,W是T帧时间方向,完全对得上。T会随音频长度变化,CNN后面用AdaptiveAvgPool处理,所以不用强制每段音频帧数一致。
2.4 数据增强:让模型在真实噪声环境里不翻车
资料包里的数据再怎么全,也很难覆盖真实部署时的底噪、距离和飞行速度差异。训练阶段不加增强,验证集准确率可能不错,一到现场就崩,这是声音分类最常见的高分低能。
def augment_audio(y, sr): # 随机增益,模拟距离远近 y = y * np.random.uniform(0.85, 1.2) # 高斯白噪声,模拟环境底噪 noise = np.random.randn(len(y)) * np.random.uniform(0.002, 0.01) y = y + noise # 时间拉伸,模拟飞行速度导致的时长变化 rate = np.random.uniform(0.92, 1.08) y = librosa.effects.time_stretch(y, rate=rate) return y噪声的幅度参数要克制,0.002到0.01对应信噪比大约40dB到26dB,太大会把旋翼节拍淹没。时间拉伸rate在0.92到1.08之间,拉伸太多会让特征图时间轴失真,CNN反而学到拉伸伪影。增强在训练时随机做,验证集和测试集保持原始数据,否则看不出模型真实水平。
进阶做法是SpecAugment:在提取完MFCC特征之后,随机遮掉特征图上的一小段时间或几个系数维度行,让模型不依赖某个固定的频率带。这个技巧对防过拟合很有效,代码也简单,就是在build_feature返回前对矩阵做随机mask。
3. 用CNN做分类:小网络结构、训练超参和推理文件
3.1 为什么是CNN:对比SVM和RNN之后的选择
MFCC特征图是二维矩阵,横轴时间、纵轴频率。卷积核扫过的是“某个频率带在一小段时间里的变化模式”,旋翼的节拍性起伏和电机啸叫的谐波结构都恰好是这种局部二维模式。这是CNN在声学分类任务里成立的根本原因,和图像分类的逻辑完全一致。
SVM不是不能做,但需要把40×94的特征图拉平成3760维向量,空间结构全丢,分类边界只能靠全局统计量撑;RNN擅长一维时序建模,但这里时间轴只有94帧,三通道的谱图信息密度比序列顺序更重要,用LSTM反而容易过拟合并拖慢训练。真正适合这个任务的就是小型CNN,“深度学习CNN识别声音事件”是这一票方案里落地最稳的。
3.2 网络结构设计:两个卷积块加全局平均池化
数据量几千到几万样本的规模,不适合搬ResNet。我常用的结构是两个“卷积+BN+ReLU+池化”块,最后接全局平均池化和一个线性分类层,参数少、收敛快,还能接受任意时间长度。
import torch.nn as nn class DroneAudioCNN(nn.Module): def __init__(self, in_channels=3, n_classes=2): super().__init__() self.block1 = nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2) ) self.block2 = nn.Sequential( nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2) ) self.pool = nn.AdaptiveAvgPool2d((1, 1)) self.dropout = nn.Dropout(0.3) self.classifier = nn.Linear(64, n_classes) def forward(self, x): x = self.block1(x) # (B, 32, 40, T) -> 池化后 (B, 32, 20, T/2) x = self.block2(x) # (B, 64, 20, T/2) -> 池化后 (B, 64, 10, T/4) x = self.pool(x).flatten(1) # (B, 64) x = self.dropout(x) return self.classifier(x)为什么用两个3×3堆叠而不是一个5×5:两个3×3的等效感受野是5×5,但参数量只有后者的72%左右,非线性还多一层。BN放卷积之后、ReLU之前是常规位置,能避免内部协变量偏移,训练曲线明显更稳。
每层尺寸变化:输入(3, 40, 94),block1池化后是(32, 20, 47),block2池化后是(64, 10, 23),最后全局池化成64维向量。第二层通道数从32翻到64,是图像分类里的标准做法,特征图尺寸减半、通道数加倍,信息量不丢。如果训练集只有两三千样本,把第二层降到48或把Dropout提高到0.4,过拟合会明显缓解。
3.3 训练超参数与类别不平衡处理
特征图本身很小,显存不是瓶颈,默认参数可以直接套。
| 超参数 | 取值 | 备注 |
|---|---|---|
| 优化器 | AdamW | lr=1e-3, weight_decay=1e-4 |
| batch_size | 32 | 特征图小,显存压力低 |
| epochs | 40 | 配早停,patience=6 |
| 学习率调度 | ReduceLROnPlateau | factor=0.5, patience=5 |
| 损失函数 | CrossEntropyLoss | 类别不平衡时加class weight |
训练循环不需要花哨,关键是每轮打印loss和准确率,方便判断收敛状态。
def train_one_epoch(model, loader, opt, criterion): model.train() total_loss, correct, total = 0.0, 0, 0 for x, y in loader: opt.zero_grad() out = model(x) loss = criterion(out, y) loss.backward() opt.step() total_loss += loss.item() * len(y) correct += (out.argmax(dim=1) == y).sum().item() total += len(y) return total_loss / total, correct / totalloss.backward()计算梯度,opt.step()更新参数,每个batch清零梯度避免累积。argmax得到预测类别,和真实标签比较统计准确率。如果样本不平衡,比如正样本“有无人机”只有负样本的五分之一,直接交叉熵会让模型学会“无脑判负类”,因为负类贡献了绝大多数loss。
from sklearn.utils.class_weight import compute_class_weight import numpy as np weights = compute_class_weight('balanced', classes=np.unique(train_labels), y=train_labels) weight_tensor = torch.from_numpy(weights).float() criterion = nn.CrossEntropyLoss(weight=weight_tensor)compute_class_weight按类别频率反比计算权重,少数类loss被放大。二分类场景也可以直接用正负样本数量比值手动构造,但sklearn这行更省事,代码里别漏了torch.from_numpy转成float张量。
3.4 模型推理:除了权重还要保存标准化参数
训练完只存一个state_dict是很多初学者会犯的错。MFCC标准化用的均值和方差如果来自训练时的样本内统计,推理时换一段音频,特征分布会漂移。正确做法是保存模型权重、训练集统计量、特征参数三样东西。
def predict(wav_path, model, mean, std): feat = build_feature(wav_path) # (3, 40, T) feat = (feat - mean) / std # 复用训练集统计量 x = torch.from_numpy(feat[None]).float() # (1, 3, 40, T) with torch.no_grad(): prob = torch.softmax(model(x), dim=1).numpy()[0] return probmean和std的来源是训练集:遍历所有训练样本,对每个样本提取的MFCC矩阵按通道和频率维度求均值方差,最后存成.npy。推理时load进来,和模型权重放在同一个目录。这一步是声音分类最容易忽略的后悔药,不保存推理就玄学。
4. 训练翻车的常见坑与排查清单:特征、标签、部署各环节
4.1 五个典型坑:现象、原因、解决
第一个坑:训练集准确率90%以上,验证集一直75%上下。现象是loss掉得很快,验证集提升缓慢甚至越训越差。原因是数据太干净,模型记住了训练集的固定增益和底噪。解决方法是把数据增强加回训练环节,混入不同环境录音,并观察验证集loss在哪个epoch开始回升,回头调整增强强度。
第二个坑:正负样本数量悬殊,模型永远判“无无人机”。现象是混淆矩阵里绝大多数样本落在负类,精确率看着还行,召回率几乎为零。原因是默认交叉熵在少数类上的梯度被稀释。解决方法是加class weight,或者在每个batch里保证正样本占比,比如采样时对正样本过采样。还要注意判别阈值:正样本只占训练集5%时,0.5这个默认阈值根本不适用。
第三个坑:训练和验证不错,换个麦克风或录音设备就翻车。现象是本地测试准确率95%,拿到实际现场设备后掉一大截。原因是不同录音设备频响和增益不一致,训练集只来自同一个来源。解决方法是训练数据混入至少两个设备的录音,推理端先做响度归一化,MFCC标准化复用训练统计量。不要一味加数据,先把设备差异处理掉。
第四个坑:解压资料包后报UnicodeDecodeError或路径读不出来。现象是代码读取wav或csv时崩溃,中文目录名乱码。原因是Windows下ZIP文件名编码处理不同,GBK和UTF-8混用;还有一种zip伪加密的情况,打开时弹密码框但数据实际没加密。解决方法是改用7-Zip或Bandizip解压,遇到伪加密用Bandizip的“修复压缩包”功能;代码里所有路径用pathlib.Path拼接,不硬编码中文路径;读CSV时显式指定encoding='utf-8'或'gbk'。这个坑在所有zip资料包里出现频率最高,不解决后面步骤全被卡住。
第五个坑:MFCC标准化顺序错了,换段音频特征分布漂移。现象是同一条推理样本单独测试概率忽高忽低。原因是第2章代码里的样本内标准化受当前音频增益影响,推理时被噪声干扰。解决方法是训练集统计全局均值和方差,推理复用同一组。标准化放在特征提取之后、模型推理之前,不要指望BatchNorm代劳,因为BN的统计量在推理时是固定值,无法适配每段音频的不同尺度。
4.2 评估指标别只盯着准确率:混淆矩阵和分类报告
类别不平衡时准确率是骗人的,需要看混淆矩阵和分类报告。
from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_pred = np.argmax(all_probs, axis=1) print(classification_report(y_true, y_pred, target_names=['no_drone', 'drone'])) print(confusion_matrix(y_true, y_pred))classification_report会输出精确率、召回率、F1三项,重点关注drone这一行。召回率低说明漏报多,精确率低说明误报多。混淆矩阵能直观看到正样本被分到哪一类。对无人机声音识别来说,漏报比误报更危险,所以评估优先级是“召回率优先,再看精确率”。
4.3 训练曲线像心电图:学习率、批大小怎么排查
训练loss上下剧烈跳动是新手最容易慌的场景。原因基本是学习率太大或batch太小导致梯度噪声大。先用1e-3配合ReduceLROnPlateau跑一轮,如果还震荡就降到3e-4。另一个常见现象是训练到中途loss突然变NaN,多半是输入音频里有一段静音,MFCC取log时算出-inf,进网络就炸。解决方法是提取特征前对音频做一次RMS检查,能量过低的样本直接丢弃或补最小噪声。
还有一类是loss在降但准确率不动,这种多半是类别不平衡把指标掩盖了,去调阈值而不是改网络结构。训练曲线只要不是垂直跳水或长期横盘,一般都有救。
5. 从模型到可用系统:双流特征、阈值和连续帧判定的进阶技巧
5.1 双流特征:Log-Mel和MFCC一起喂给两个分支
MFCC信息紧凑但丢了一部分谱细节,实战里如果想再压误报,可以同时提取Log-Mel频谱和MFCC,分别过两个卷积分支,在全局池化后把特征拼接起来再分类。这个做法能同时利用Mel谱的完整谐波信息和MFCC的紧凑表达,代价是训练时间大概翻倍。如果当前准确率已经到95%以上,不建议加,性价比不高。
5.2 阈值不是0.5:用验证集选点
输出概率默认和0.5比较,但正负样本不均衡时这个阈值往往不是最优。用验证集算一次precision-recall曲线,按业务需求选阈值。
from sklearn.metrics import precision_recall_curve p, r, thr = precision_recall_curve(y_true, drone_prob) # 期望召回率0.95时,选第一个满足条件的阈值 thr_use = thr[np.argmax(r >= 0.95)]如果无人机漏报代价高,阈值往下压到0.3到0.4;如果误报频繁干扰值守,往上抬到0.6以上。每次更新训练数据后重新定一次阈值。
5.3 流式滑窗:连续N帧判定再报警
单帧3秒音频的置信度偶尔会闪断,实际系统里我习惯用滑窗加连续判定,避免偶发噪声触发误报。
from collections import deque conf_queue = deque(maxlen=3) def on_frame(prob_drone): conf_queue.append(prob_drone) if len(conf_queue) == 3 and all(x > thr_use for x in conf_queue): alert()连续3帧都超过阈值才触发告警,比单次判断稳得多。告警后再用一段5秒整录音做复核,进一步压低误报。
我第一次做这类声音识别项目时只存模型不存标准化统计量,换台电脑推理全崩;后来定死规矩,训练结束先跑一次全局统计脚本,把mean、std和模型版本号一起归档。另一个教训是别看到准确率95%就收工,打印混淆矩阵看少数类,很多时候问题藏在数字后面。希望帮到你。
本文还有配套的精品资源,点击获取