

工业现场出异常时,工程师的第一句话往往不是"快算频谱",而是——"这个以前见过吗?"
如果见过,问题基本就解决了一半:上次是什么原因、怎么处理的、换了什么件,照方抓药。这些经验存在于哪里?大部分在老师傅的脑子里、在车间的聊天记录里、在几份没人再翻的故障报告里。老师傅休假,经验就暂时不可用;老师傅离职,经验就永久流失。
而这些经验其实有一个天然的载体——历史数据本身。每一次异常的波形都存在库里,缺的只是两样东西:一是把"找相似"变成一个可执行的查询而不是人肉翻曲线;二是把诊断结论和处理措施挂到数据上沉淀下来,让下一次异常能自动检索到。
本文用 DolphinDB 实现这套能力:事件片段的提取与归一化、相似度的计算方法(相关系数/欧氏距离/DTW)、两级检索的 SQL 实现、以及"检索→标注→入库"的案例库闭环。核心思路不复杂:波形先变成可比的向量,再用"粗筛 + 精排"两级检索找到历史相似案例——把组织记忆从人脑搬进数据库。
先把这个需求的价值说透,再谈实现。
三种流失的共同根因:经验和数据没有连在一起。波形躺在时序库里,结论躺在文档里,中间没有桥。
补上这座桥之后,日常的工作流变成这样:
新异常发生
│
▼
提取事件片段 ──▶ 相似性检索 ──▶ 命中:复用诊断结论和处理措施
│ │
│ ▼
│ (确认无误,直接处置)
▼
未命中 ──▶ 人工诊断 ──▶ 处置 ──▶ 入库(波形特征 + 结论 + 措施)
用得越久,库越全,命中越多,诊断越快——这是一个复利型资产:第一天没什么用,一年后新人也能像老工程师一样"记得"每一次历史异常。
相似性检索的地基,是让两段波形处在"可比"的状态。原始数据直接比是不行的,要先过三道处理。
异常发生时,有价值的不是全天数据,而是告警时刻附近的一段。以告警时间戳为锚点,向前回溯一段固定时长(比如 60 秒):
// 从告警点回溯 60 秒的原始片段
seg = select ts, rms
from loadTable("dfs://iot", "vibration_rms")
where deviceId = "PUMP-007"
and ts between (datetime(2026.03.15 03:15:00) - 60) : datetime(2026.03.15 03:15:00)回溯长度要覆盖"异常发展全过程"——太短只有故障瞬间,太长混入大量正常段,都会稀释相似性信号。振动类异常 60 秒通常够,具体看故障发展节奏。
两段不同设备、不同采样状态的片段,点数大概率不一样——而长度不一的序列没法直接算相似度。解决方法是重采样到固定点数:把片段按时间均分成 N 个桶,每桶取均值,得到定长序列:
// 重采样到固定 120 点:60 秒均分成 120 个 500ms 桶
resampled = select avg(rms) as v
from loadTable("dfs://iot", "vibration_rms")
where deviceId = "PUMP-007"
and ts between (datetime(2026.03.15 03:15:00) - 60) : datetime(2026.03.15 03:15:00)
group by bar(ts, 500)bar(ts, 500) 把时间戳按 500 毫秒对齐分桶,avg 取桶内均值。不管原始是 10 点还是 6000 点,出来都是约 120 个点。
不同设备的振动基准不同:A 泵正常 RMS 在 0.5,B 泵在 2.0。直接比数值,幅值差异会淹没形状差异——而诊断经验恰恰主要藏在形状里(突发型?渐进型?周期性脉冲?)。

z-score 归一化把每段波形变换成"以自身均值为零点、自身波动为单位"——即对每个点做 (v − 均值) / 标准差——幅值信息抹掉,形状保留。DolphinDB 把这个常用变换封装成了内置函数 zscore,一行完成:
// z-score 归一化:内置函数,等价于 (v - avg(v)) \ std(v)(同为样本标准差)
normalized = select zscore(v) as v from resampled到这里,任何异常片段都变成了一段定长、去量纲、只含形状的序列——可比了。
波形可比之后,"像不像"需要一个量化标准。常用的有三把尺子,各有分工。
皮尔逊相关系数衡量两段序列的形状一致度,取值 -1 到 1:接近 1 表示同涨同跌形状几乎一致,0 表示无关。

它对幅值天然不敏感(我们在归一化后计算,双重保险),最契合"这两次故障是不是同一种模式"的判断。DolphinDB 内置 corr 函数:
corr(newSeg`v, oldSeg`v) // 两段归一化序列的形状相似度除了整段波形,每个事件还可以提取一组标量特征(均值水平、峰值因子、峭度、主频……)——峭度 kurtosis、偏度 skew、频谱 fft,DolphinDB 都有内置函数,特征提取同样是几行 SQL 的事。特征之间的欧氏距离衡量两个事件在"特征空间"里的远近,计算量小——适合先在整库上快速粗筛。
// 特征距离:每个特征先做 z-score 标准化(去量纲),再算平方和开根号
sqrt(pow(f1 - q1, 2) + pow(f2 - q2, 2) + pow(f3 - q3, 2))相关系数要求两段波形严格逐点对齐。但有些故障模式会出现"形状一样、节奏不同"——比如同样的冲击群,A 设备 3 秒内出现 5 次,B 设备拉长到 5 秒。逐点对齐的相关系数会把这种相似判低。
DTW(Dynamic Time Warping,动态时间规整)允许时间轴非线性伸缩后再匹配,能抓住"节奏不同、模式相同"的相似。代价是计算量大得多,且容易把不相似的也"扭"出高分。工程上的务实做法:先用相关系数覆盖大多数场景,DTW 作为特定故障模式(变转速设备、间歇性冲击)的补充,不作为默认。
单独用哪把尺子都不理想:全库逐个算相关系数太慢,只算特征距离不够准。工程解法是两级检索:特征距离粗筛全库,相关系数精排前几十名。
先设计案例库——每条历史案例一行,存特征、片段定位和诊断结论:
// 案例库:特征列(入库时已标准化)+ 片段定位 + 诊断与处置
dbCase = database("dfs://iot_case", VALUE, 2026.01.01..2026.12.31)
caseSchema = table(1:0,
`caseId`deviceId`eventTs`f_rmsStd`f_crest`f_kurtosis`f_peakFreq`diagConclusion`actionTaken,
[LONG, SYMBOL, DATETIME, DOUBLE, DOUBLE, DOUBLE, DOUBLE, SYMBOL, STRING])
dbCase.createPartitionedTable(caseSchema, "caseLib", `eventTs)设计要点:
eventTs + deviceId 是片段的定位符——原始波形不复制进案例库,精排时按定位符回原始表取,避免数据冗余diagConclusion 和 actionTaken 是经验的载体——检索的终点不是"找到了相似波形",而是"上次怎么处理的"新异常来了,先算它的特征,然后在整库上算距离、取前 20:
// 新事件标准化后的特征: f_rmsStd=-0.3, f_crest=1.2, f_kurtosis=0.8, f_peakFreq=-0.5
candidates = select caseId, deviceId, eventTs, diagConclusion, actionTaken,
sqrt(pow(f_rmsStd - (-0.3), 2) + pow(f_crest - 1.2, 2)
+ pow(f_kurtosis - 0.8, 2) + pow(f_peakFreq - (-0.5), 2)) as featDist
from loadTable("dfs://iot_case", "caseLib")
order by featDist
limit 20这一步是标准的 SQL 向量化运算,几十万条案例也就是秒级的事。特征选择建议用有物理含义的指标(峰值因子、峭度、主频)而不是原始数值——它们对转速、负载变化的抗干扰性好,粗筛不容易把真正相似的筛掉。
对粗筛出的 20 个候选,按 deviceId + eventTs 回原始表取片段,走第二节的"重采样 + 归一化"流程,再逐个算相关系数:
// 片段处理函数(第二节的三道工序收成一个函数)
def extractNormSeg(device, eventTs, lookbackSec, n) {
span = (lookbackSec * 1000) \ n
seg = select avg(rms) as v
from loadTable("dfs://iot", "vibration_rms")
where deviceId = device
and ts between (eventTs - lookbackSec) : eventTs
group by bar(ts, span)
return select zscore(v) as v from seg
}
// 新事件片段(先提出来,循环外算一次)
newSeg = extractNormSeg("PUMP-007", datetime(2026.03.15 03:15:00), 60, 120)
// 对 20 个候选逐个精排
scored = select caseId, deviceId, eventTs, diagConclusion,
corr(newSeg`v, extractNormSeg(deviceId, eventTs, 60, 120)`v) as shapeSim
from candidates
// 形状相似度排序,取前 5 展示
top5 = select * from scored where shapeSim > 0.85 order by shapeSim desc limit 5shapeSim > 0.85 是经验阈值:0.85 以上基本可确认"同一种模式",0.7~0.85 值得人工比对,0.7 以下参考价值有限。阈值要结合自己数据的噪声水平校准。

最终交给工程师的是这样一张表:相似案例 + 上次的诊断结论 + 上次的处理措施 + 相似度。它的价值不在算法多精巧,在于把"问老师傅"变成了"查库"——而且老师傅每次的结论都留在库里,不会休假,不会离职。
检索只解决"命中"的情况。让案例库持续增值,靠闭环。
新异常检索不到相似案例,走人工诊断。诊断处置完成后,把这次事件入库——特征算好、片段定位、结论措施写全:
def archiveCase(device, eventTs, conclusion, action) {
// 特征提取:滑动窗口时域特征(kurtosis、skew 等)+ 频谱主频(fft)——内置函数,常规计算,略
// 入库
loadTable("dfs://iot_case", "caseLib").tableInsert(
table(rand(long(1e9), 1) as caseId, tableObject...)
)
}入库的关键纪律是结论质量:diagConclusion 用受控词表(轴承外圈故障 / 不对中 / 松动……)而不是自由文本——自由文本检索出来了也聚合不了,受控词表才能支撑后续按故障类型统计。
案例库第一天是空的。冷启动的办法是把历史告警记录批量过一遍:每条历史告警按 eventTs 提取片段、算特征入库;有故障报告的对着报告补结论,没有的先标 UNKNOWN 入库——至少波形特征可比了,结论慢慢补。
一个运行了两三年的平台,历史告警里往往藏着几百个现成案例,冷启动并不需要等新故障发生。
检索命中但结论错了(现场处置发现上次诊断不对),要更新案例记录而不是只改脑子。案例库的可靠性靠这个维护——错误结论被检索复用两次,工程师就不再信这个库了。

z-score 用片段自身的均值和标准差(第二节的做法),不要用全厂全局统计——全局基准会把"高振动设备"和"低振动设备"的正常差异放大成形状差异,干扰匹配。先用自身归一化抹掉基准差异,让形状说话。
忘了重采样直接 corr,两段长度不同的序列会直接报错或得到无意义的值。任何进精排的片段,先过"定长 N 点"这道工序,N 固定统一(比如都用 120),不同 N 的序列之间依然不可比。
转速、负载变化会改变波形的数值特征。粗筛特征如果选了受工况影响大的(比如原始 RMS 均值),换工况的相似模式会被筛掉。优先选相对稳定的形态类特征:峭度(冲击敏感性)、峰值因子(冲击/均值比)、主频(转速归一化后)。必要时把工况标签(转速段)作为粗筛的过滤条件。
库会随时间膨胀出三类问题:重复案例(同一故障多次入库,可合并)、错误结论(靠反馈修正)、过时结论(设备改造后旧案例不再适用,要标记失效)。定期(比如每季度)做一次案例评审,比让库烂掉再重建便宜得多。
工业数据平台做了很多"向前看"的事:实时告警、趋势预测、健康评分。这篇做的是一件"向后看"的事——借用 DolphinDB 把历史经验变成可检索的资产。
技术上,整条链路没有一个复杂算法:片段提取是普通查询,重采样是 bar 分桶,归一化是内置 zscore,粗筛是特征距离,精排是 corr。组合起来,解决的却是工业现场持续了几十年的问题——诊断经验跟着人走。
四个收尾要点:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。