2026/11/4 14:01:18

从传感器到工单:PHM与RUL预测落地实践

从传感器到工单:PHM与RUL预测落地实践 简介PHM故障预测与健康管理技术文档面向设备运维、可靠性工程及智能制造领域的学习者用于系统梳理从状态监控、故障预测到健康管理的知识框架。压缩包共1个docx文件大小约35.52MB内容围绕传感器数据采集、数据分析与机器学习、故障预测模型、健康管理系统等关键技术展开并覆盖航空航天、汽车制造、能源生产、医疗设备等应用场景同时讨论维护成本、设备可靠性与系统集成等现实问题。已有660人学习适合作为PHM入门阅读、课程资料或方案梳理参考便于读者建立“数据采集—分析建模—预测维护”的完整认知并了解实施中的数据质量、算法复杂性与系统集成挑战。1. 一台减速机的三个数字PHM 到底在算什么某厂一台主减速机巡检工每周拿测振仪抄一次数等振动烈度超标时轴承已经磨了整整两周。这类场景是 PHMPrognostics and Health Management故障预测与健康管理最典型的入场理由。它既不是「坏了再修」也不是「到点就换」而是拿传感器数据算出一个「还能撑多少小时」的数字让备件、停机窗口和人力提前排。适合谁手上有设备、有维修记录、能拿到分钟级甚至秒级运行数据的团队只有一张 Excel 台账、连一条原始波形都翻不出来的先把采集补齐再谈模型。这套资源的落点在一条完整链路上传感器 → 特征表 → 剩余寿命 → 工单。中间任何一环偷懒后面全是玄学。2. 传感器到特征表把原始波形压成能训练的几百列2.1 先定采样率再谈算法PHM 项目里最容易翻车的地方不在模型在采集层。温度是慢变量秒级采样足够振动是快变量轴承外圈剥落产生的冲击重复频率通常在几十到几百赫兹按 1 Hz 采样根本看不见频谱早就混叠成一团噪声。所以采集层必须分层快变量振动、电流高频分量按原始波形落文件保留「黑匣子」出问题能回放慢变量温度、压力、转速、负载进时序库按秒或分钟聚合。比采样率更容易被忽略的是工况变量。同一台设备在空载和满载下的振动 RMS 能差一倍如果不把转速和负载同步采进表后面的特征在不同工况之间根本不可比模型学到的只是「什么时候负载高」不是「什么时候快坏了」。常见做法是每个特征样本都带上工况标签训练和推理时按工况分箱做归一化而不是全局 z-score。物理量建议采样率典型特征对哪类故障敏感振动加速度10–50 kHz 原始波形RMS、峰峰值、峭度、包络谱峰值频率轴承、齿轮、松动驱动电流1–10 kHz电流有效值、谐波幅值电机绕组、转子断条温度1 Hz温升速率、相对环境温差润滑失效、过载转速/负载1–10 Hz工况标签、累计运行小时全类型归一化必需压力/流量1 Hz均值漂移、方差液压、气路堵塞提示采样率不是越高越好。振动原始波形按 25 kHz 存一台设备一天就是几十 GB多数团队撑不过三个月。折中方案是原始波形只在触发条件下留档振动超过阈值的 ±30 秒常规只存降采样后的特征。2.2 时间对齐与滑窗特征多传感器数据进来第一件事是对齐。不同设备、不同网关的时间戳一定会有毫秒到秒级的偏差直接按行拼接等于制造错位特征。下面这段代码把长表timestamp, sensor_id, value转成统一时间栅格的宽表再滑窗提特征import numpy as np import pandas as pd from scipy.signal import hilbert from scipy.stats import kurtosis def load_and_align(path, rule1s, min_valid_ratio0.8): 长表 - 宽表统一到固定时间栅格。 rule: 重采样周期慢变量常用 1s快变量聚合后可用 1min min_valid_ratio: 一行里有效传感器占比低于该值则丢弃防止插值造数据 df pd.read_csv(path, parse_dates[timestamp]) n_sensor df[sensor_id].nunique() wide (df.pivot_table(indextimestamp, columnssensor_id, valuesvalue, aggfuncmean) .resample(rule).mean() # 落到统一栅格 .interpolate(limit5) # 最多补 5 个点缺口大就留 NaN .dropna(threshint(min_valid_ratio * n_sensor))) wide.columns [fs{c} for c in wide.columns] return wide def window_features(wide, win600, step60, fs1.0): 滑窗提时域 包络谱特征。 win: 窗口点数1s 采样下 600 点 10 分钟 step: 步长点数step60 即 90% 重叠样本量够但相邻样本强相关划分时要按设备切 fs: 与重采样后的采样率一致写错包络谱频率全废 rows [] for start in range(0, len(wide) - win, step): seg wide.iloc[start:start win] rec {t_end: seg.index[-1]} for col in seg.columns: x seg[col].to_numpy(dtypefloat) if np.isnan(x).any(): continue rec[f{col}_rms] float(np.sqrt(np.mean(x ** 2))) rec[f{col}_pp] float(x.max() - x.min()) rec[f{col}_kurt] float(kurtosis(x, fisherFalse)) # 包络谱先去均值再做 Hilbert 包络取谱峰对应频率 env np.abs(hilbert(x - x.mean())) spec np.abs(np.fft.rfft(env)) freqs np.fft.rfftfreq(len(env), d1.0 / fs) rec[f{col}_env_peak_f] float(freqs[np.argmax(spec[1:]) 1]) rows.append(rec) return pd.DataFrame(rows).set_index(t_end)这段逻辑有三个参数值得反复调。窗口长度win决定了「多快能发现变化」窗口太短RMS 抖动大误报多窗口太长早期微弱退化被平均掉。经验值是按设备转速折算让窗口覆盖 20–50 个旋转周期。步长step决定样本量和相关性强弱重叠 90% 能快速扩样本但同一台设备的相邻窗口高度相似划分验证集时必须整台设备切出去否则指标虚高得离谱。fs必须和重采样后的实际采样率一致写错一个数量级包络谱峰值频率会整体偏移后面所有基于频率的诊断规则全部失效。2.3 特征筛选先砍方差再砍共线几百列特征里真正有信息量的往往不到二十列。第一步砍掉近似常数列方差接近 0传感器坏了或者量程设置错误第二步按工况分箱做归一化把「负载高导致振动大」和「退化导致振动大」区分开第三步砍共线相关系数超过 0.95 的成对特征只留物理意义更明确的那个。def condition_normalize(feat, cond_cols1, bins5): 按工况变量分箱做 z-score消除负载/转速带来的整体漂移 feat feat.copy() feat[_bin] pd.qcut(feat[cond_col], qbins, duplicatesdrop) cols [c for c in feat.columns if c not in (cond_col, _bin)] out feat.groupby(_bin)[cols].transform( lambda s: (s - s.mean()) / (s.std() 1e-9)) out[cond_col] feat[cond_col] return out.drop(columns[_bin])分箱数bins5是折中分太细每箱样本不足统计量不稳分太粗等于没分。归一化用的均值和方差必须来自训练集落地时固化成一份 JSON线上直接套用绝不能每次推理重新算一遍。2.4 健康指标要能被评价不能只靠眼睛把多维特征融合成一条健康指标曲线HI时很多人挑一条「看起来比较顺」的曲线就定了这是典型的靠眼睛做工程。合格的健康指标有三个可量化属性单调性随时间的增减方向一致、趋势性与时间的相关性、鲁棒性抗随机噪声。三条都能算出来拿数字挑比开会争论靠谱。def monotonicity(x): d np.diff(x) return abs((d 0).sum() - (d 0).sum()) / max(len(d), 1) def trendability(x, t): return abs(np.corrcoef(t, x)[0, 1]) def robustness(x, k5): 滑动中位数残差占比越接近 1 越平滑 med np.array([np.median(x[max(0, i - k): i k 1]) for i in range(len(x))]) return 1 - np.abs(x - med).sum() / (np.abs(x - x.mean()).sum() 1e-9)三个指标都是 0 到 1融合权重按场景给做早期预警的偏重鲁棒性做寿命回归的偏重单调性。这套评分在选特征、选融合方式、比较不同传感器组合时都能直接当裁判用比事后看模型 RMSE 更早发现问题。3. 剩余寿命预测建模标签截断、按设备划分与两条评估线3.1 三条技术路线先看手里有什么PHM 的剩余寿命RUL预测大致三条路。相似度匹配拿当前退化轨迹去历史库里找最像的一段把那段后面还剩多少直接搬过来样本要求低可解释性强缺点是新故障模式没见过就抓瞎适合维修记录厚但数据量不大的场景。物理退化模型用磨损方程、裂纹扩展方程描述退化参数少、外推能力强缺点是建模周期长对多故障耦合几乎无解。数据驱动LSTM、TCN、梯度提升树直接学特征到 RUL 的映射数据够就见效快缺点是对分布漂移敏感工况一变性能掉得厉害。我的选型顺序是先用相似度匹配跑通一条基线拿到一个能对现场交代的数字同时上数据驱动模型做精度上限最后用物理模型给预测值兜底做合理性检查比如预测剩余寿命比设计寿命还长直接判为异常输出。路线数据需求可解释性主要风险相似度匹配中需历史全寿命轨迹高新故障模式无匹配物理退化模型低高建模成本高、耦合难数据驱动高千级以上样本低工况漂移后性能骤降3.2 标签怎么造分段线性与截断设备全寿命数据里前面 60%–70% 的时间基本处于健康状态特征没有明显趋势。如果直接拿「距离失效还有多少周期」当标签这段区域的标签噪声极大模型会去拟合噪声。通行做法是给 RUL 设上限超过就截断成常数让模型只在进入退化期后才开始认真学。def add_rul(df, unit_colunit, cycle_colcycle, max_rul125): 按设备分组构造 RUL 标签并做上截断。 max_rul: 截断上限含义是「超过这个剩余量预测无意义」取值参考退化起始点 df df.sort_values([unit_col, cycle_col]).copy() life df.groupby(unit_col)[cycle_col].transform(max) df[rul] (life - df[cycle_col]).clip(uppermax_rul) return df def split_by_unit(df, val_ratio0.2, seed42): 按设备划分禁止按行随机划分同一设备的相邻窗口高度相似会泄漏 units df[unit].unique() rng np.random.default_rng(seed) rng.shuffle(units) n_val max(1, int(len(units) * val_ratio)) val_units set(units[:n_val]) return df[~df.unit.isin(val_units)], df[df.unit.isin(val_units)]max_rul的取法有两种一是看健康指标开始单调上升的那个点取对应剩余周期二是直接拿领域经验值比如按维护手册的检查间隔折算。这个值调大模型在健康期也会硬学出趋势容易早期误报调小退化后期样本权重相对变大早期预警能力变弱。它本质上是在「早发现」和「少误报」之间选一个工作点。按设备划分这条必须守住。我见过最典型的错误是按行随机切分训练集和验证集里混着同一台设备的相邻窗口验证 RMSE 低到 3 个周期上线后直接翻倍——血泪经验划分代码写错一次后面所有调参都是自欺欺人。3.3 训练与评估RMSE 只是入场券模型本身不复杂梯度提升树在几百列特征上就能给出很强的基线时序模型LSTM/TCN在长序列依赖上更好但训练成本高。真正决定这套东西能不能被现场接受的是评估方式。RMSE 对称地惩罚高估和低估可工程上这两种错误代价完全不同预测剩余寿命偏低最多是多换一次件预测偏高设备在计划停机前就坏了那是事故。import numpy as np from sklearn.ensemble import HistGradientBoostingRegressor def phm_score(y_true, y_pred, a113.0, a210.0): 非对称评分业界常用形式。d0 表示预测偏乐观危险惩罚更重。 a1: 提前预测保守的衰减系数越大惩罚越轻 a2: 滞后预测危险的衰减系数越小惩罚越重 d np.asarray(y_pred) - np.asarray(y_true) s np.where(d 0, np.exp(-d / a1) - 1, np.exp(d / a2) - 1) return float(s.sum()) def train_baseline(train, val, feats, targetrul): model HistGradientBoostingRegressor( max_iter400, learning_rate0.06, max_depth6, min_samples_leaf20, l2_regularization1.0, random_state42) model.fit(train[feats], train[target]) pred model.predict(val[feats]) rmse float(np.sqrt(np.mean((pred - val[target]) ** 2))) score phm_score(val[target].to_numpy(), pred) bias float(np.mean(pred - val[target])) return model, {rmse: rmse, score: score, bias: bias}, pred三个输出一起看。RMSE 给整体精度非对称评分给风险敞口同样的 RMSE评分可能差一倍偏差bias告诉你模型整体偏乐观还是偏保守正值意味着系统性高估剩余寿命必须在上线前压回来。参数上max_depth6配min_samples_leaf20是防过拟合的常规组合样本量上万后可以放到 8 层l2_regularization在特征共线严重时往上加。3.4 从预测值到阈值把周期翻译成班次模型输出的是「还剩多少个周期」现场要的是「还能跑几个班次、下周要不要备件」。中间要做一次翻译周期 × 单周期运行小时 剩余可运行小时再除以日均运行时长得到剩余天数。阈值不拍脑袋定用验证集扫一遍横轴是告警阈值纵轴是误报率健康设备被报警和漏报率真失效没报警选一个误报率能被现场接受的拐点。注意阈值定完要冻结一段时间。现场最怕的是阈值每周调整工人刚建立起对告警的信任一次误报就全毁了后面再准也没人信。4. 从模型文件到工单健康管理系统的接口、版本与告警分级4.1 接口契约先定死模型后换离线跑通 notebook 和上线是两件事。上线第一件事是把输入输出契约固定下来请求进来是一段定长窗口的特征或原始数据指针出去是 RUL 数值、置信区间、告警等级、模型版本。这几个字段一个都不能少尤其是模型版本——没有它事后复盘「这次误报是模型问题还是数据问题」就变成了无头案。from fastapi import FastAPI, HTTPException import joblib, json, numpy as np app FastAPI() model joblib.load(rul_model.pkl) FEATS json.load(open(feature_order.json))[features] # 顺序必须固化 STATS json.load(open(norm_stats.json)) # 训练期均值方差 app.post(/predict) def predict(payload: dict): raw payload.get(features, {}) missing [f for f in FEATS if f not in raw] if missing: raise HTTPException(400, fmissing features: {missing[:5]}) x np.array([[ (raw[f] - STATS[f][mean]) / (STATS[f][std] 1e-9) for f in FEATS ]]) rul float(model.predict(x)[0]) level red if rul 20 else yellow if rul 60 else green return {unit: payload.get(unit), rul: round(rul, 1), level: level, model_version: payload.get(model_version, unknown)}feature_order.json这份文件看着无聊实则是整条链路最关键的后悔药。训练时的列顺序和推理时用的列顺序只要错一位模型不会报错只会安静地输出垃圾。把它和模型一起打包、一起做版本号是成本最低的防线。4.2 离线在线共用一份特征代码训练用 notebook 提特征、上线用另一套 SQL 提特征是 PHM 上线后精度掉一半的头号原因。两份代码哪怕只有插值方式不同一个线性、一个前向填充特征分布就变了。常见做法是把特征计算抽成独立模块离线批处理和在线推理都 import 同一份函数只改数据源适配层。# feature_core.py —— 离线/在线共用不允许复制粘贴出第二份 def build_features(wide_df, win, step, fs): from features import window_features, condition_normalize feat window_features(wide_df, winwin, stepstep, fsfs) return condition_normalize(feat) # 归一化统计量来自训练期文件 # 离线批量跑历史数据落特征表 # 在线每次推理只取最近 win 长度的窗口调用同一函数这条规矩要写进代码评审清单凡是特征计算逻辑出现第二份实现一律打回。4.3 漂移监控与回滚设备大修、换型、换季都会让特征分布变化模型不会自己告诉你它失效了。监控层至少要做两件事一是特征漂移用 PSI 或 KS 检验对比线上最近一周的特征分布和训练分布二是预测分布漂移RUL 输出的均值和中位数如果整体平移多半是输入变了。def psi(base, cur, bins10): 群体稳定性指数0.2 视为显著漂移需人工介入 edges np.quantile(base, np.linspace(0, 1, bins 1)) b np.histogram(base, binsedges)[0] / len(base) 1e-6 c np.histogram(cur, binsedges)[0] / len(cur) 1e-6 return float(np.sum((c - b) * np.log(c / b)))PSI 超过 0.2 就该触发重训评估超过 0.3 直接回滚到上一个稳定版本。回滚能力要在上线第一天就准备好别等出事再补。4.4 告警分级与工单联动告警不是越早越好等级设计要让现场能分级响应。下面这张表是实践中比较稳妥的四级划分阈值按上一章扫出来的工作点填。等级触发条件系统动作现场响应绿RUL 60 周期仅记录不推送无黄20 RUL ≤ 60生成待办周报汇总纳入下次计划检修橙5 RUL ≤ 20自动开工单通知班组长24 小时内确认备件红RUL ≤ 5 或特征越限电话 工单双通道立即降载或停机检查联动的关键是工单里必须带上依据哪几个特征越限、模型版本、预测区间。工人看到「振动峭度从 3.1 涨到 5.8包络谱峰值频率对上了外圈故障频率」配合度完全不一样只写一句「模型预测快坏了」没人会理。5. 五个翻车现场PHM 落地排查清单5.1 指标虚高验证集混进了同一台设备现象离线验证 RMSE 只有 3 个周期评分极低上线一周后误差翻三倍工程师开始怀疑人生。原因按行随机划分数据集同一台设备的相邻滑窗同时出现在训练集和验证集。相邻窗口的特征相关性能到 0.98模型本质上是在背答案。这类泄漏在时间序列里极其隐蔽指标越漂亮越要警惕。解决一律按设备或按时间段整体划分训练集和验证集的设备 ID 不相交。划分完先做一次健全性检查统计两边设备数、样本数、RUL 分布任何一边偏得离谱就重划。另外把「随机划分」这个函数从公共库里删掉断了念想。5.2 归一化泄漏用全体数据算均值方差现象验证集效果好线上推理结果飘忽同一台设备连续两次请求的 RUL 差出几十个周期。原因训练时用全量数据含验证集计算归一化的均值和方差验证阶段等于提前看到了未来信息更糟的是线上每次推理都拿当前 batch 重新算统计量输入变了输出就变模型变成了一个随波逐流的函数。解决归一化统计量只在训练集上算一次固化进norm_stats.json离线在线都读这份文件。代码里加断言推理时如果发现统计量是从当前数据算出来的直接抛异常。这条坑没有技术难度纯粹是纪律问题。5.3 采样率不足包络谱里什么都看不到现象轴承明明已经出现早期剥落特征表里峭度和包络谱峰值毫无反应模型持续报绿。原因振动通道为了省存储被降到 200 Hz 采样而目标故障的特征频率在 800 Hz 以上信号早混叠了。更麻烦的是混叠后的频率会伪装成低频成分看起来像是「有信号」实际全是假的。解决先算清楚要监测的故障特征频率按轴承几何参数和转速折算采样率至少取最高关注频率的 2.56 倍工程上一般留到 5 倍以上。已经降采样的历史数据救不回来只能在采集侧改配置同时把原始波形留档策略补上。5.4 工况被当成退化换负载就报警现象设备一切正常每次产线提产能、负载上来系统就连着报橙警维护班组白跑好几趟。原因特征没有按工况归一化模型学到的是「负载高 → 振动大 → 剩余寿命短」这条伪相关。训练数据里高负载段恰好都出现在寿命末期模型就把两者绑死了。解决把所有工况敏感特征按转速/负载分箱做 z-score训练集里保证各工况都有健康期和退化期的样本。如果某个工况在历史数据里只出现在退化期那这个工况的预测结果必须打上「低置信」标记不能直接进告警通道。5.5 只看 RMSE偏乐观的模型照样上线现象模型 RMSE 达标验收通过。三个月后发生一次非计划停机复盘发现模型在失效前两周还预测剩余 40 多个周期。原因评估只用了对称误差没有看偏差方向。这个模型整体高估剩余寿命RMSE 被大量健康期的准确预测拉低了真正危险的尾部误差被平均掉。解决评估必须三条线一起看RMSE、非对称评分、偏差符号。偏差为正系统性高估时做一次保守校正——按验证集的分位数把预测值往下压宁可早换不可晚换。上线前还要单独看尾部样本真实 RUL 小于 20 周期的那批误差是多少这批才是决定事故率的。6. 进阶把点预测做成区间让备件计划敢用点预测有个天然缺陷现场问「还能撑多久」你答「37 个周期」对方一定追问「准不准」。答不上来这个数字就用不起来。真正能被备件计划采纳的是区间剩余寿命 30 到 45 个周期置信度 90%。做法不复杂用分裂式 conformal 在验证集上标定残差分位数即可。import numpy as np def conformal_interval(model, X_cal, y_cal, X_test, alpha0.1): 分裂式 conformal 区间。alpha0.1 对应 90% 覆盖 前提校准集与测试集可交换设备退化非平稳时需按时间加权 res np.abs(y_cal - model.predict(X_cal)) q np.quantile(res, 1 - alpha) # 有限样本应改用 ceil((n1)(1-alpha))/n 分位 pred model.predict(X_test) return pred - q, pred q, qq就是这轮标定出来的半宽它有一个很好的性质只要校准集和线上数据分布大致一致长期覆盖率就接近 90%。设备退化是非平稳过程工况切换后分布会变所以我一般按月重标一次q比重新训练模型便宜得多效果却立竿见影。区间拿到后决策规则也随之变化备件订购点不看点预测看区间下界。下界低于采购提前期对应的周期数就下单这样即使模型偏乐观也不会因为等件而停机。输出形式现场可执行性主要问题点预测低无法判断可信度一次大偏差就失信固定宽度区间中不同设备宽度不匹配conformal 区间高需定期重标定从那以后我每次交付 RUL 模型都强制走一遍「点预测 区间 尾部误差」三件套少一样就不签字。区间不是给算法工程师看的装饰是让维护计划敢照着排班的依据。希望帮到你。本文还有配套的精品资源点击获取