做工业AI这行的人,几乎都绕不开这样一道坎:模型在实验室里跑得飞快,一到车间就开始闹脾气。最近我完整走了一遍一套物理约束下基于不平衡样本的工业故障诊断与实时部署框架,踩了不少坑,也攒了不少经验,整理出来给同行当个参考。整件事的核心就三个关键词:物理约束、不平衡样本、实时部署。通俗点讲,就是要在绝大多数样本都是正常状态、少数类故障样本稀缺到可怜的情况下,把诊断模型做成一个既不会违背物理规律、又能在工厂工控机上及时出结果的可落地方案。如果你正在做设备状态监测、预测性维护、基于振动信号的故障诊断,或者刚接触这个方向想找一条少踩坑的路径,这篇内容值得花十分钟看一遍。
1. 总体框架与设计思路
1.1 工业现场的数据困境:正常样本占绝大多数
先聊最直接的问题。几乎所有制工业设备健康管理的项目,第一步都会撞上同一个尴尬局面:正常数据太多了,故障数据少得可怜。我经手过的几个现场数据集,正常样本占比普遍在97%以上,剩下的2%到3%里还要散落着五六种故障类型,最少的类别可能只有几十个样本。用这种数据直接训练一个普通的多分类网络,模型大概率学到的是一条很平坦的决策面:把所有输入都判成正常,准确率也能轻松冲到97%以上,看着漂亮,实际上一点用没有。
故障诊断真正的难点,不是把正常样本分对,而是要在千分之一的异常概率里把故障捞出来,还得分清楚到底是轴承外圈坏了、内圈坏了,还是出现了不平衡或不对中。这靠传统堆样本的办法根本不现实。更麻烦的是,工业现场不会等你缺哪类故障就跑去哪台设备上慢慢采集,很多设备一旦出故障就直接停线检修了,你永远等不到所谓的“完整故障样本集”。所以这类项目从第一天起就要接受一个前提:样本不平衡不是临时的脏数据问题,而是项目长期存在的核心约束。
另外一个常被低估的问题是标签质量。有些现场数据其实保存了很多报警记录,但报警记录和真正故障标签并不等价,很可能只是某个测点超了阈值。把这些记录直接当故障标签用,模型会被噪声带偏。我在好几个项目里发现,最后还得靠老师傅的停机和维修记录反推真实故障时间段,再结合波形人工确认,才能得到一份勉强可用的标签集。
1.2 物理约束在这条链路里的位置
纯数据驱动模型有一个老毛病:它只在训练数据覆盖的那些打点上有效,一旦转速、负载、温度这些工况条件往外漂一点,模型就可能给出离谱结论。举个例子,一台离心泵转速从2900转降到1800转,轴承故障特征频率整体会往下移。传统CNN如果没见过对应的转速数据,很容易依据局部噪声强行判出一个故障类别。做工业诊断的人看到这种输出真的会血压升高,因为设备明明一切正常,系统却在瞎叫。
物理约束要解决的,正是这类“不符合常识”的输出。设备结构、传动比、滚动体数量、接触角这些参数都是已知的,故障特征频率和转速之间的关系也有明确的解析公式,比如外圈故障特征频率BPFO、内圈故障特征频率BPFI、滚动体故障特征频率BSF。把这些知识作为先验嵌入模型,相当于给模型划定了一个“物理上允许的输出区间”。超出这个区间的预测,要么在损失函数里受到抑制,要么在特征采集环节被直接规避。
但这里要澄清一点:物理约束不是把机理模型和神经网络简单拼在一起就完事。它要做的是让物理规律参与决策边界的成型,使最终判断在工程上可解释、可复现。我倾向于把它理解为给模型装上一套“常识过滤器”,不是束缚模型的能力,而是帮它过滤掉那些在物理世界根本不存在的虚假模式。
1.3 三层架构:特征层、模型层、部署层
这个框架在整体设计上分了三个层次,每一层对应一个方面的工程问题。特征层解决的是“喂给模型什么”,模型层解决的是“怎么做判断”,部署层解决的是“怎么跑得快”。
特征层的主要工作是对原始振动信号做带通滤波、计算包络谱、根据实时转速做阶次跟踪,把时域信号变换到物理语义比较清晰的频域或阶次域。之所以不直接喂原始波形,是因为工业现场的振动信号很脏,包含着大量齿轮啮合、流体冲击、电气干扰这类无关成分,直接丢给模型只会增加学习负担。做阶次跟踪的另一个好处是能让特征在变转速工况下保持对齐,把因转速变化导致的频率漂移先消掉一部分。
模型层用的是参数量在十几万级别的小型卷积网络,主任务做故障分类,同时加一个辅助头做故障特征频率回归。物理信息作为并联输入和损失函数中的先验约束进入训练,类别不平衡损失和小样本增强也在这一层解决。采用小型网络不是能力不够,而是为了让部署层更轻松。工业场景没有太多空间让你堆几百层的Transformer,推理延迟、内存占用、硬件兼容性都是硬约束。
部署层把训练好的模型导出为ONNX,再量化成INT8或FP16,接入不同的边缘推理引擎,配合滑窗并行机制实时产出诊断结果。分层最大的好处是每一层都能单独验证和替换。特征层出了问题不影响部署层,模型要升级也未必需要动采集端,排障效率会高很多。
2. 物理约束与不平衡样本的技术实现
2.1 把物理规律写成损失函数
物理约束的落地方式有很多种,我这次主要用了三种:输入端的物理特征嵌入、模型结构上的多任务约束、损失函数里的物理一致性惩罚。其中损失函数是最灵活也最容易上手的一种,适合在已有模型上快速做验证。
先说思路。假设我们做的是滚动轴承故障诊断,已知当前转速为( f_r ),滚动体数量为( n ),滚动体直径为( d ),节圆直径为( D ),接触角为( \alpha ),那么外圈故障特征频率可以按:
[ BPFO = \frac{n \cdot f_r}{2} \left(1 - \frac{d}{D} \cos\alpha \right) ]
内圈故障特征频率:
[ BPFI = \frac{n \cdot f_r}{2} \left(1 + \frac{d}{D} \cos\alpha \right) ]
也就是说,给定转速和几何参数,理论上我们能算出故障特征频率应该出现在哪个频段。物理损失要做的事情,就是拿这个“理论预期”去约束模型的输出。如果模型在当前工况下预测了外圈故障,但包络谱里在BPFO附近的频带根本没有明显能量峰,那这个预测就违反了物理规律,需要被惩罚。
我实现的时候,会在特征层算出一个物理特征向量,包含理论特征频率、理论谐波次数、转速归一化后的频率位置,同时让模型额外输出一个“特征频率估计”。物理损失就是计算这个估计值与理论值之间的偏差,超出容差就计入惩罚。代码结构大致是这样:
class FaultDiagnosisLoss(nn.Module): def __init__(self, num_classes, fault_class_id, lambda_phy=0.05): super().__init__() self.fault_class_id = fault_class_id self.lambda_phy = lambda_phy self.cls_loss_fn = BalancedFocalLoss(gamma=2.0) def forward(self, logits, labels, theoretical_freq): cls_loss = self.cls_loss_fn(logits, labels) # 物理一致性约束:当理论特征频率低于可分辨门槛时, # 模型不应坚持给出对应的故障类别判断。 prob = torch.softmax(logits, dim=1) freq_gate = (theoretical_freq > 0.8).float() invalid_prob = (prob[:, self.fault_class_id] * (1 - freq_gate)).mean() return cls_loss + self.lambda_phy * invalid_prob看起来简单,但真正调起来有不少坑。lambda_phy太大会把模型压到“什么都不敢报”,太小又没有约束效果。我在项目里采用的做法是把lambda_phy从相对较高的值开始,前几个epoch让模型先学会物理边界,之后再逐步退火降低,避免约束过强导致分类损失无法反向传播。还有就是不要把所有故障类型都塞进物理损失,先只约束故障特征最明显、解析公式最可靠的外圈和内圈故障,滚动体故障因为特征频率容易受打滑影响,约束太严反而会压制真实的微弱故障。
2.2 用物理知识改善少数类样本的质量
样本不平衡问题的常规解法是重采样和合成样本,但我在这类工业项目里对SMOTE一直保持谨慎态度。原因很简单:时序振动信号的相邻点有极强的自相关性,你在原始时域波形里随便插值制造出来的“合成故障样本”,很可能在物理上根本不可能存在。比如两个不同故障相位样本之间做线性插值,搞出来的波形既不像外圈故障也不像内圈故障,模型学到的纯粹是人工伪影。
正确做法是把合成样本限定在物理特征空间里做。我在框架中对原始振动信号先计算包络谱或阶次谱,得到一组物理语义明确的频域特征,再在特征空间里进行过采样或插值。这样做的好处是合成样本至少落在“可能存在的故障特征”附近,不会产生频率成分上的荒谬组合。另一个更贴近工程的思路是物理模型辅助增强:根据设备当前的转速、负载和几何参数,用简化的故障机理模型生成理论故障信号,再叠加现场采集的真实背景噪声。简单说就是先把正常设备的真实背景噪声记录一批,然后把仿真出来的故障特征频率注入进去,形成“半物理半真实”的故障样本。
我在一个齿轮箱项目里试过这种方法。当时手里真正的齿面磨损故障样本只有七十多个,通过这种物理注入加频域SMOTE的组合,把每一类故障样本平衡到了两千个左右,模型在真实故障测试集上的召回率提升非常明显。但注意,这个方法有一个硬前提:你必须对设备的物理参数和传动链结构掌握得足够准确。参数错了,生成的样本就全错了,而且错误会顺着训练偷偷灌进模型里,比不增强还危险。
故障注入实验也是一个值得提的路径。很多制造企业有自己的故障测试台,通过在轴承外圈激光切割、电火花加工等方式制造可控故障,模拟不同严重程度的损伤。这种数据比任何算法合成都可靠,因为它包含了真实运行磨损过程中的噪声和瞬态成分。我在项目里会把实测台架故障数据和现场正常数据混合使用,比例控制在4比1以内,防止模型过度依赖测试台的固有背景噪声特征。
2.3 模型结构与损失函数怎么配
模型结构方面,我在这个框架里选择的是带多任务输出的浅层一维CNN。主干网络只有五层卷积加全局池化,参数量控制在十五万左右,输出层分为两个头:一个头输出故障类别概率,另一个头输出故障特征频率的回归值。这个结构不是为了追求精度指标上的极限,而是为了让物理约束能够找到稳定的嵌入位置。
类别的分类头使用focal loss来处理极端的样本不平衡。focal loss的核心是压低那些已被正确分类的高置信度样本的权重,让模型把注意力集中在少数类和难分类样本上。参数gamma我通常设在1.5到2.5之间,过大容易让训练变得很不稳定。同时在少数类故障上增加类别权重时,我遇到过一个问题:权重加太多,模型确实更愿意报故障了,但误报也显著增加,现场老师傅会直接想关掉这个系统。这个度需要结合误报惩罚来精细调节。
多任务频率回归头的作用不只是物理约束的落脚点,它还给了我们一个很有用的附加价值:定位故障根源。类别分类告诉你“是外圈坏了”,频率回归告诉你“故障频率大约在137Hz附近”,结合转速算回去,就能大致判断故障位置和严重程度。这对后续维修决策帮助很大,也让模型不再是一个纯粹的黑盒。
最后强调一个工程经验:模型结构不要频繁更换。在数据有限的情况下,换一个结构就相当于换一套超参数组合,你会陷入无休止的调参循环里。初期就确定一个结构,固定下来,把优化精力放在物理特征和损失函数上,收益会大得多。
3. 实时部署的工程化细节
3.1 从训练框架到推理引擎的转换
训练模型用Python很舒服,但部署到车间工控机上就不能这么随意了。我的标准流程是将训练好的PyTorch模型导出为ONNX中间格式,再由目标平台的推理引擎生成最终的可执行程序。导出这一步看起来简单,实际最容易翻车。
第一是输入输出张量的形状必须固定。很多项目里训练时用dynamic_axes=True,让输入长度可以灵活变化,这导致某些推理引擎无法做图优化,延迟直接翻倍。我的建议是在导出时固定输入尺寸,比如[1, 1, 2048],虽然牺牲了一点灵活性,但换来了更好的兼容性和性能。第二是算子兼容性问题。模型里如果用到一些比较冷门的自定义操作,比如自定义RNN单元或者复杂切片,ONNX导出时很可能报错或者输出错误的语义。解决办法是尽量用标准卷积、池化、全连接这些通用算子,训练时就为部署做准备。
导出ONNX的代码并不复杂:
import torch model.eval() dummy_input = torch.randn(1, 1, 2048) torch.onnx.export( model, dummy_input, "fault_model.onnx", input_names=["waveform"], output_names=["logits", "freq_reg"], dynamic_axes=None, opset_version=13 )实际部署时,我会在工控机上分别用CPU计算出来,记录FP32和INT8两种精度下的推理时间,并且保证输出结果的概率分布与训练时的预期一致。不要直接拿未验证的模型上产线,任何一个预处理差异都可能让模型在车间疯狂报警。
3.2 延迟预算与滑窗调度
很多人以为“实时部署”就是模型跑得快,其实真正的瓶颈往往在数据链路。振动数据是无限流式产生的,模型必须在下一个决策周期之前完成推理,否则就来不及发出报警。这里需要算一个严格的延迟预算。
假设采样率为25600Hz,窗口长度为2048点,那么一个窗口覆盖的时间是:
[ T_{window} = \frac{2048}{25600} = 80ms ]
如果滑窗重叠率为75%,那步长是512点,也就是20ms。每过一个步长就需要产出一次新的诊断结果,所以单次推理时间必须严格低于20ms,才能保证不漏掉中间状态。我在有几次项目里测试,一个十万级参数的一维CNN在普通x86工控机上FP32推理大约需要12ms左右,看起来低于20ms,但加上特征计算、预处理和结果通知的时间,已经逼近极限。换成INT8量化之后,推理降到4到5ms,整体链路才真正有了余量。
滑窗调度还有一个容易忽略的问题是缓冲队列。为了不让数据堆积,我会在C++采集层维护一个环形缓冲区,把新到的样本以固定步长推入队列,推理线程与采集线程分离。简单说就是采集线程只负责写缓冲,推理线程一有时间就从缓冲里取最新一帧处理。这样做可以应对采集瞬间的抖动,避免中间某个点卡顿导致后续数据丢失。
3.3 边缘硬件与推理引擎的搭配方案
不同现场对实时性的要求差异很大,硬件的选择最好按场景来定。我整理了三种常用搭配,供参考:
| 部署环境 | 推荐硬件 | 推理引擎 | 实测延迟量级 |
|---|---|---|---|
| 单台设备集中监测 | x86工控机 / 工业PC | OpenVINO + ONNX Runtime | 5~15ms |
| 多台设备嵌入式接入 | ARM Cortex-A平台 | ONNX Runtime / RKNN | 20~80ms |
| 高通道采集与复杂故障诊断 | 边缘GPU盒子 | TensorRT | 2~8ms |
选型要点不是硬件越强越好,而是匹配现场的数据通路。如果设备本身没有边缘计算节点,数据要上传到服务器,那“实时”这个概念就会受网络延迟制约。我见过不少项目把问题搞复杂,其实现场每台泵也就三个测点,一台普通的x86工控机就能轻松扛下来,完全没有必要上GPU。只有在同时处理几十个通道、或者模型复杂度确实降不下来时,才考虑边缘GPU。
还有一点牵扯到可靠性:推理引擎崩溃怎么办。车间环境电磁干扰多、温度高,软硬件都可能难不稳定。要做的不是祈祷它不崩,而是在架构上预留看门狗机制,让主程序异常退出后能自动重启模型服务,并在一分钟内恢复诊断能力。报警状态也要做保持,不能因为推理进程重启而丢掉最后一次的故障判断。
4. 实验评估与踩坑记录
4.1 评估指标怎么定才不算骗自己
样本不平衡背景下,准确率是一个很坑人的指标。如果测试集里正常样本占98%,一个全部判正常的“废柴模型”也有98%的准确率。所以我在这类项目里几乎不看准确率,重点看三个指标:少数类的召回率、误报率(或者说正常样本的精确率)、以及G-Mean。
G-Mean的定义是正常类召回率和故障类召回率的几何平均值,能同时衡量两类数据的表现,比准确率可靠得多。实际应用中还要特别盯住误报率。工业现场的报警是有成本的,误报太多会让运维人员产生“狼来了”效应,真正故障出现时反而没人当回事。我在几个项目里发现,把误报率控制在每台设备每周不超过一条报警,是一个比较切合实际的工程目标。
数据划分也是一个必须小心的环节。振动信号具有强时间连续性,同一段连续运行数据里的训练集和测试集不能随意混切,否则会出现时间泄露:模型在训练时已经见过测试时段相同的噪声和振动特征,评估结果虚高。我的做法是按时间顺序分段,拿前70%的数据训练,中间10%做验证,最后20%做测试,并且保证每个数据集都覆盖到完整的转速和负载范围。
4.2 训练与调参的实际经验
先说物理损失权重。很多文献喜欢把各个损失项写成固定系数,但在工业数据上,固定的lambda_phy往往让训练曲线很不稳定。我采用的是退火策略:前20个epoch用较大的lambda_phy,让模型尽快学会物理边界;之后每10个epoch衰减一次,到训练结束时物理损失大约只占总损失的五分之一。这样可以兼顾初期的稳定性和后期的分类精度。
类别权重方面,我踩过一次很重的坑。当时为了提高故障召回率,把少数类权重调到正常类的8倍,结果模型几乎把所有稍可疑的样本都判成了故障,单台设备一天能发二十多条报警。后来把权重降到3倍左右,同时配合数据层面的增强,情况才稳定下来。记住一个原则:类别权重只是损失函数层面的“软调节”,真正要解决样本稀缺问题,还得让模型见过足够多、足够多样的故障特征。
批大小和学习率的配合也值得一提。工业振动数据往往存在相似样本扎堆的问题,同一设备、同一工况下连续采集的样本高度相似,如果批大小太大,一个批里可能全是来自同一时段的近重复样本,模型更新方向会非常偏。我建议批大小控制在64以内,必要时打乱样本时跨设备、跨工况打乱,而不要只按时间顺序切。
4.3 常见问题排查速查表
整理一份常见问题速查表,都是我实际遇到过的情况,不想让你重走弯路。
| 问题表现 | 可能原因 | 排查与解决办法 |
|---|---|---|
| 模型对少数类故障几乎不报 | 类别权重太低或focal loss难例权重不够 | 检查训练集类别分布,提高少数类权重,适当增大focal loss的gamma |
| 误报比故障报警还多 | 物理约束太弱或类别权重过高 | 拉高物理损失权重,降低少数类权重,检查特征层是否把正常噪声误认为故障特征 |
| 训练集F1很高,测试集一堆漏报 | 时间泄露或测试分布偏移 | 重新按时间划分数据集,确认测试集包含独立的转速和工况区间 |
| 推理时间超出滑窗步长预算 | 模型太大或端侧优化不足 | 试试INT8量化和算子融合,减窗口重叠率,必要时裁剪模型通道数 |
| 量化后故障特征明显变弱 | 校准集与真实运行数据分布不一致 | 重新采集覆盖正常工况和典型故障工况的校准集,样本量至少500~1000段 |
| 物理损失不收敛 | 理论特征频率算错或约束头位置不当 | 先用纯物理信号验证理论公式,在单独的小模型上跑通再加到完整框架里 |
| 部署后结果抖动厉害 | 预处理参数与训练不一致 | 对比训练和推理时的滤波、归一化、滑窗参数,加入简单的自检日志 |
最后再分享一个细节:模型报警条件不要只看类别概率。我在实际程序里要求同时满足两个条件才推送报警,一是特定故障类别的概率高于阈值,二是频率回归头的输出落在对应故障特征频率的合理容差范围内。这个双条件逻辑既简单又有效,能拦掉不少“模型学歪了”产生的低质量报警。
我个人在多次实践中的感受是,物理约束最大的价值并不是提升一两个百分点的精度,而是让模型的行为变得更可预测。故障诊断系统最终要过的是现场信任这道关,老师傅看到报警,敢不敢签字停机巡检,背后比的就是系统的稳定性和可解释性。做技术的人很容易沉迷于指标提升带来的成就感,但真正把系统做上线之后你会发现,一个能解释清楚“为什么报警”的系统,比一个黑盒高精度系统要省心太多。
如果你现在正准备在产线上搭建类似的故障诊断框架,我建议不要一上来就追求更花哨的模型结构,先把“推理速度能不能跟上采集速度”这件事测清楚。很多项目卡住的原因不是精度不够,而是报警出来得太晚,或者报警太多让人没法信。先把链路跑通,把物理约束的边界立住,再一步步优化细节,这条路走起来会稳得多。