简介:这是一份基于声纹 Deep-SVDD 一分类深度学习模型的电机异常检测工具,面向工业设备运维人员、Python 开发者和深度学习初学者。工具以电机运转声音为数据源,训练阶段提取正常音频特征构建超球面,检测阶段通过样本偏离程度判断电机是否异常,可用于产线实时监测与早期故障预警。压缩包共 10 个文件,大小 4.83MB,包含 2 个 Python 源码文件、1 个已训练模型文件 h5、1 个特征缩放器、音频特征文件、阈值参数、界面截图、训练参数说明及打包配置等,代码、权重和说明配套齐全,下载后可直接运行或继续调参。目前已有 2172 人学习下载。资料提供了从 PyQt5 界面操作到数据采集、模型训练与单次/连续检测的完整实现思路,适合想快速落地声音异常检测原型、研究一分类深度学习应用或参考 PyQt5 项目界面设计的读者。
1. 用声纹做电机异常检测,为什么绕不开deep-SVDD
电机正常运转时,声音有相对稳定的节奏和频带包络;一旦轴承磨损、转子偏心、润滑状态变化,声纹会在高频分量或谐波结构上先出问题。基于声纹deep-SVDD深度学习的检测工具,就是采集正常工况下的声音,训练一个只用正常样本的one-class模型,让模型把偏离正常分布的样本判成异常,再用PyQt5+Python封装成桌面检测程序。它适合产线初筛、设备巡检,以及最头疼的“只有正常声音、缺少故障样本”的场景。这类项目最大的误区是拿普通二分类硬套:故障样本不够,模型根本学不到“异常长什么样”。deep-SVDD的思路是反过来,只学正常声纹的紧凑范围,异常自然被甩在范围外。
2. 声纹数据准备:正常声音采集、MFCC特征和定长窗口
2.1 采集正常样本前先记住这三个硬性条件
很多人拿到项目先把网络搭起来,结果回头发现录音数据不能用,白跑一遍。我一般会把录音当成单独一道工序,重点盯三件事。
第一,录音位置要固定。麦克风离电机越近,环境混响越小,但也越容易受气流和机械振动干扰。常见做法是固定在电机外壳旁30厘米左右,尽量正对轴承端盖。不要一会儿放侧面一会儿放正面,这会引入比故障还大的特征差异。
第二,一个工况至少录多段,别只录一条长音频。空载、半载、满载的声音包络完全不同,只靠空载训练,带载状态下模型可能误报。建议每个工况录20到30段,每段2到4秒,这样既方便后续切片,也能让训练集覆盖转速波动和负载波动。
第三,周边人声、气动工具、车辆经过都要避开。deep-SVDD只见过正常电机,它会把你没标注的干扰也当成正常分布的一部分,推理时真实故障反而可能被判成“正常”。
采样率方面,电机异响多数能量在8kHz以下,16kHz采样率基本够用。如果目标偏向轴承早期高频损伤,可以换44.1kHz,但MFCC计算量和模型输入维度都会变大,后面网络的隐层也要跟着调。
2.2 从原始波形到MFCC+Delta:为什么要做这两步
电机声纹检测不直接拿原始波形喂网络,因为波形样本长度不同,而且一维时域点对点比较对转速和相位变化极其敏感。同样的电机,转速差1%,两个波形就错位了,但频谱包络可能差别不大。
所以第一步是把波形转成频谱特征。MFCC虽然是从语音识别里来的,但它背后的mel滤波器组对中低频有更细的分辨率,正好适合电机这种以基频和谐波为主的旋转机械噪声。只取静态MFCC还不够,电机异响在时间包络上会突变,Delta系数描述的是特征随时间的变化率,对打点数、异响脉冲这类短暂事件更敏感。
通常我取13维MFCC加13维一阶Delta,拼成26维。每一帧大约32毫秒,一个2秒样本会得到64帧左右的特征序列。这样一条音频最终变成64×26的特征矩阵,再展平成1664维向量,供deep-SVDD使用。
2.3 标准化和定长切片:把一段音频变成固定形状的训练张量
下面是特征提取的完整过程,直接用librosa读取文件,最后返回固定长度的扁平特征向量。
import librosa import numpy as np def extract_feature(path, sr=16000, n_mfcc=13, max_frames=64): y, sr = librosa.load(path, sr=sr, mono=True) if len(y) < sr * 1: raise ValueError("录音太短,至少1秒以上") mfcc = librosa.feature.mfcc( y=y, sr=sr, n_mfcc=n_mfcc, n_fft=1024, hop_length=512 ) delta = librosa.feature.delta(mfcc, order=1) feat = np.vstack([mfcc, delta]).T # (帧数, 26) if feat.shape[0] >= max_frames: feat = feat[:max_frames] else: pad_len = max_frames - feat.shape[0] feat = np.vstack([feat, np.zeros((pad_len, feat.shape[1]))]) return feat.reshape(-1).astype(np.float32) # 64*26 = 1664 all_feats = [extract_feature(p) for p in normal_files] X = np.stack(all_feats) from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X)这里的n_fft=1024配合hop_length=512,在16kHz采样率下每帧约32毫秒,频率分辨率约15.6Hz。max_frames=64对应约2秒,既能覆盖电机多个旋转周期,又不会让单条样本太长。StandardScaler在全部正常样本上fit,推理时也必须用同一组mean_和scale_,不能在部署时重新fit。
有一点要注意:pad_len补零只适合少量欠长样本。如果一半训练录音都不到2秒,补零会把大量静音当成正常特征,模型会出现“越安静越正常”的假象。这种情况建议提高max_frames,或者重新录音。
3. deep-SVDD训练原理与PyTorch实现:让模型只认识正常电机
3.1 网络不是分类器:Encoder加中心向量的必要性
普通分类网络最后的输出是类别概率,训练必须有正负样本。deep-SVDD最大的区别是把网络当成一个特征编码器,把输入映射到低维空间后,计算它到一个固定中心点的欧氏距离。
训练时只输入正常电机特征,优化目标是让所有正常样本的编码都靠近同一个中心c。推理时把待测样本编码成z,看z到c的距离:距离小判正常,距离大判异常。这样就不需要标注“异常长什么样”,轴承磨损到哪个阶段、异响是连续还是脉冲,都不影响模型判断,只要它的声纹分布偏离了正常基线。
这也是为什么网络最后一层不能接softmax。softmax会把输出限制到0到1之间,反而压缩了异常样本的离群程度。deep-SVDD要的是不设上限的距离值。
3.2 最小实现:DeepSVDD网络、中心初始化和损失函数
网络结构我用三层全连接加ReLU,隐层128维,低维空间8维。输入是1664维的扁平特征。中心向量必须在第一次前向后由正常样本的编码均值固定下来,之后训练过程不再更新c。如果每个epoch都重算中心,目标函数会一直漂移,loss降了但距离分布不稳定。
import torch import torch.nn as nn class DeepSVDDNet(nn.Module): def __init__(self, input_dim=1664, hidden_dim=128, latent_dim=8): super().__init__() self.encoder = nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, latent_dim) ) def forward(self, x): return self.encoder(x) def init_center(net, loader): net.eval() z_list = [] with torch.no_grad(): for batch, in loader: z_list.append(net(batch)) z = torch.cat(z_list, dim=0) c = z.mean(dim=0) c[torch.abs(c) < 1e-6] = 1e-6 # 防止中心为零向量 return c def svdd_loss(z, c, net, alpha=1e-4): dist = torch.sum((z - c) ** 2, dim=1) reg = alpha * sum( w.pow(2).sum() for name, w in net.named_parameters() if "bias" not in name ) return dist.mean() + regsvdd_loss里的L2正则只惩罚权重,不惩罚偏置,这是为了让网络不要靠偏置把所有样本硬拉到同一点。alpha通常取1e-4到1e-3。如果训练后期发现隐层特征方差很小,说明网络在退化,可以把alpha调大一点。
3.3 训练参数怎么定:学习率、batch_size和epoch的选择
特征做完标准化后,直接套TensorDataset和DataLoader。这里不能用DataLoader默认的drop_last=False也没关系,只要每个batch里的数据都是完整样本就行。
from torch.utils.data import TensorDataset, DataLoader X_tensor = torch.tensor(X_scaled, dtype=torch.float32) loader = DataLoader(TensorDataset(X_tensor), batch_size=64, shuffle=True) torch.manual_seed(0) net = DeepSVDDNet() c = init_center(net, loader) optimizer = torch.optim.Adam(net.parameters(), lr=1e-4) for epoch in range(30): net.train() total_loss = 0.0 for batch, in loader: z = net(batch) loss = svdd_loss(z, c, net) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() print(f"epoch {epoch:02d}, loss {total_loss / len(loader):.4f}")这里lr=1e-4是我起手的值,不要一上来用1e-3。deep-SVDD非常容易把所有输入映射到同一个点,学习率太大,loss会掉得很快,但所有样本的距离都变成0,等于白训。
batch_size取64即可。网络只有三层全连接,批量太小会在init_center阶段引入更多随机性。训练轮数30轮左右,正常样本数百条时,再用多几轮容易过拟合到录音通道。判断收敛不能只看loss,要看训练集上距离的方差,如果方差趋近于0,说明网络在退化。
另外,这个网络里不要加Dropout和BatchNorm。Dropout会让推理阶段的输出和训练阶段不一致;BatchNorm在小batch上对均值和方差的估计不稳定,影响中心c的可比性。
3.4 训练结束后要保存什么:模型权重+中心向量+归一化参数
部署时不只是加载模型权重,标准化参数和中心向量也要一起存。很多工具上线后分数漂移,都是因为推理时忘了把特征重新标准化,或者重新加载了另一个scaler。
torch.save({ "model": net.state_dict(), "center": c, "scaler_mean": scaler.mean_, "scaler_scale": scaler.scale_, "feature": { "sr": 16000, "n_mfcc": 13, "max_frames": 64 } }, "motor_svdd.pth")加载时把这些字段都取出来,net.load_state_dict之后,用保存的scaler_mean和scaler_scale做推理前变换。不要直接保存scaler对象,换环境后pickle版本不一致容易出问题,保存numpy数组更稳。
4. PyQt5落地:录音线程、模型推理和异常报警界面
4.1 PyQt5界面为什么要把检测放到QThread里
PyQt5的GUI线程一旦被阻塞,窗口就会转圈、按钮点不动,甚至系统提示“未响应”。如果直接在按钮点击回调里录音2秒再跑模型,界面至少卡死2秒,连续检测场景完全不可用。
正确的做法是把录音、特征提取、PyTorch推理放到一个QThread里,GUI线程只负责收结果和刷新显示。线程通过pyqtSignal把距离分数、是否异常、时间戳发回主界面。
4.2 QThread+pyqtSignal封装连续识别
音频输入我常用sounddevice的InputStream,以阻塞方式每次读取固定时长,简单直接。下面是一个最小可用的检测线程。
import time import numpy as np import sounddevice as sd import torch from PyQt5.QtCore import QThread, pyqtSignal def extract_feature_from_audio(y, sr=16000, n_mfcc=13, max_frames=64): import librosa mfcc = librosa.feature.mfcc( y=y, sr=sr, n_mfcc=n_mfcc, n_fft=1024, hop_length=512 ) delta = librosa.feature.delta(mfcc, order=1) feat = np.vstack([mfcc, delta]).T if feat.shape[0] >= max_frames: feat = feat[:max_frames] else: pad = max_frames - feat.shape[0] feat = np.vstack([feat, np.zeros((pad, feat.shape[1]))]) return feat.reshape(-1).astype(np.float32) class DetectThread(QThread): result_ready = pyqtSignal(float, bool, str) def __init__(self, net, center, scaler, threshold, sr=16000, chunk_sec=2.0, parent=None): super().__init__(parent) self.net = net self.center = center self.scaler = scaler self.threshold = threshold self.sr = sr self.chunk_sec = chunk_sec self._running = True def stop(self): self._running = False def run(self): with sd.InputStream(samplerate=self.sr, channels=1, dtype="float32") as stream: while self._running: audio, _ = stream.read(int(self.sr * self.chunk_sec)) feat = extract_feature_from_audio( audio.reshape(-1), sr=self.sr ) feat = self.scaler.transform(feat.reshape(1, -1)) with torch.no_grad(): z = self.net(torch.from_numpy(feat).float()) score = ((z - self.center) ** 2).sum(dim=1).item() abnormal = score > self.threshold self.result_ready.emit(score, abnormal, time.strftime("%H:%M:%S"))主界面里创建线程后必须把引用存到self上,否则Python垃圾回收会把线程对象回收掉,pyqtSignal也不再触发。每次点击“开始检测”前,先判断上一个线程是否还在运行。
4.3 阈值报警和状态展示的界面逻辑
界面不需要复杂,三个控件就够:一个当前得分标签,一个状态标签,一个停止按钮。连续检测时,刷新频率由chunk_sec决定,2秒一次对现场巡检足够及时。
self.thread = DetectThread(net, center, scaler, threshold) self.thread.result_ready.connect(self.on_result) self.thread.start() def on_result(self, score, abnormal, timestamp): self.lbl_score.setText(f"{score:.3f}") if abnormal: self.lbl_state.setText("异常") self.lbl_state.setStyleSheet("color:red") else: self.lbl_state.setText("正常") self.lbl_state.setStyleSheet("color:green")关窗口时先调stop()再调wait(),并加上3000毫秒超时,避免线程卡在stream.read()里导致进程退不掉。不要用terminate(),它会把录音设备句柄直接干掉,下次启动时设备被占用,只能重启程序。
5. 避坑与排查:声纹deep-SVDD工具从能跑到能用的五个关键问题
5.1 现象:正常声音也被大量误报
采集位置稍微变化,或旁边有人说话、有液压阀排气,模型就会把距离拉大,产生误报。根本原因是训练集只覆盖了一个录音位和少量工况,模型把背景噪声也当成了“正常电机声音的一部分”。
解决思路是固定安装位置,并且在预处理时加一道高通滤波,把50Hz工频干扰和低频气流噪声去掉。我通常会在MFCC之前用scipy.signal.sosfilt做一次80Hz高通。训练数据里还应该混入多台同型号电机、不同时段的录音,让正常分布的边界不是一条窄线。
5.2 现象:异常样本的得分数值和正常样本完全重叠
原因多半是特征分辨率不够,或者窗口太短。只取13维MFCC时,频谱包络被压得很粗,早期轴承异响的能量可能只在某个窄带里变化,MFCC把它平滑掉了。另外1秒窗口只有32帧,转速波动就会被当成异常。
我会把MFCC维度提高到20,再加二阶Delta,窗口提高到2秒以上。如果异常还是很轻微,可以先用FFT查看异常样本和正常样本的频谱差,找到异常突出的频段,再决定要不要用谱峭度或窄带能量特征。
5.3 现象:训练loss很低但检测一塌糊涂
这是deep-SVDD最典型的翻车现场。loss很低不一定代表学到了正常分布,也可能是网络把所有输入都压缩到中心附近,每个样本的距离都接近0,异常样本也不例外。我遇到过隐层128维、latent_dim=4时,训练第10轮loss降到0.002,但测试集里所有得分都小于0.01。
原因是网络容量过大、正则太弱、训练轮数过多,模型找到了“全都映射成同一点”的捷径。排查方法是把训练集和少量异常样本一起过一遍模型,打印正常和异常得分的均值和方差。如果方差极小,就降低隐层维度、加大alpha,并把训练轮数限制在20轮以内。
5.4 现象:PyQt5启动后录音设备被占用
启动检测时提示声卡被占用,通常不是声卡坏了,而是上一次程序异常退出或用了terminate(),InputStream没有正常close。另一方面,某些麦克风设备只支持44100Hz,你硬要开16000Hz,驱动会拒绝。
解决方法是设备枚举后按采样率筛选,注册sd.InputStream前先调sounddevice.query_devices()确认默认设备的default_samplerate。如果确实只支持44.1kHz,就把整个链路统一到44100Hz,MFCC参数不变,但hop_length按比例增大。
5.5 现象:实时检测延迟越来越高
连续检测时,界面从2秒一次慢慢变成5秒一次,是因为录音线程平均处理耗时已经超过2秒,积压的数据越来越多。PyTorch推理本身不快,加上librosa的MFCC每2秒音频要算上百次FFT,CPU稍弱就跑不过来。
先把处理耗时打点,分别测MFCC耗时和模型推理耗时。如果MFCC占了90%以上,可以减小n_fft到800,或把max_frames从64降到48。如果推理耗时高,尝试把网络切到ONNX或TorchScript导出。还有一个容易忽略的点:不要在每次推理时重复加载模型权重,线程启动时加载一次就够了。
6. 进阶验证:用AUC和阈值校准确认检测工具不是玄学
6.1 先攒一组带标签的异常声音
模型训练完,先别急着拿去现场报警。找一组和训练无关的异常录音,比如现场已经确认的轴承故障、润滑不良、转子摩擦声。把这些音频切成和训练相同的2秒长度,用同一套特征提取流程得到推理得分。同时准备一组新的正常录音,也计算得分。两组得分画出来如果完全重叠,说明模型没有学到可区分的信息,直接回头改特征。
6.2 用ROC曲线选阈值
阈值不应该是拍脑袋定的,我用约登指数选。先拿到正常样本得分和异常样本得分,再计算ROC曲线。
import numpy as np from sklearn.metrics import roc_curve, auc normal_scores = np.array(normal_scores) abnormal_scores = np.array(abnormal_scores) y_true = np.array([0] * len(normal_scores) + [1] * len(abnormal_scores)) y_score = np.concatenate([normal_scores, abnormal_scores]) fpr, tpr, thresholds = roc_curve(y_true, y_score) auc_value = auc(fpr, tpr) idx = np.argmax(tpr - fpr) best_threshold = thresholds[idx] print(f"AUC={auc_value:.3f}, threshold={best_threshold:.3f}")这里的得分是到中心的欧氏距离,理论上是越大越异常,所以异常样本排在得分大的方向。选完阈值后,还要回放一遍正常样本,确保误报率在可接受范围。如果AUC低于0.85,就不要强行上线,说明特征或训练数据还有问题。
6.3 现场试运行要统计两个指标
阈值校准是离线做的,现场试运行还要统计实际误报率和漏报率。记录每一条报警对应的得分和录音片段,至少跑一周。出现的误报有两类:一是环境音或设备振动传导,二是电机本身正常但负载波动太大。前者可以加录音位置约束,后者可以把阈值调高或改特征。
我的习惯是每次调完阈值,都把当天的正常和异常录音追加到回放测试集,重新算一次AUC,而不是凭现场感觉随便调。声纹检测工具最难的不是算法,而是让“正常”的边界足够贴现场。希望帮到你。
本文还有配套的精品资源,点击获取