泰迪杯 2024A 范文二:产线故障识别的代价地图与不确定性边界
摘要
范文一已经回答了"能不能识别故障"——加权 Bagging 集成把宏 F1 推到 0.912、再用马氏距离异常检测兜住未知故障。但工程落地时,准确率只是故事的一半。本文换一个更贴近现场的角度:识别得有多贵、在哪儿会失手、失手的代价有多大。我们把故障识别从"分类正确率"的单一维度,拉到"代价敏感 + 不确定性量化"的两个新维度上,通过六组实验重新解剖同一套数据,得到四个被准确率掩盖的结论:第一,三种方案失败的模式截然不同——规则漏诊(把过热判成正常)、CART 误报(把正常判成轴承磨损)、集成居中;第二,早期弱征兆是规则方案的最大盲区(弱征兆召回仅 0.571,集成为 1.000);第三,未知故障整体只有 56.2% 能被检出,且几乎只能靠强征兆兜底;第四,代价敏感下最优判定阈值落在 85% 分位而非范文一的 98% 分位,而一旦把风险低估 10% 仍按低估值定员,真实成本会反超基准 14.3%。这些发现改写了"模型训好就能上线"的简单叙事。
一、问题重述与数据
赛题给出一条制造产线多通道传感数据(振动均值、振动标准差、温度、电流、转速等 8 个特征),按时间窗切片后标注 4 类已知故障(轴承磨损、过热、电机异常、正常)以及一类未显式建模的"未知/堵塞"状态。训练集覆盖前 21 天、测试集覆盖其后时段,共约 1760 个窗片段。范文一建立了特征工程 → 规则/CART/Bagging 三方案分类 → 马氏距离异常检测 → 风险驱动排班优化 的完整管线。
本文不改数据、不改特征,只换提问方式:
- 已知故障分类:不再只看宏 F1,而是逐类拆开混淆矩阵,看每类错在哪;
- 早期预警:按征兆强度(inten)把样本分弱/强两档,比较召回率;
- 未知故障:用马氏距离阈值做异常检测,量化"模型不知道"的边界;
- 判定阈值:引入误报代价 与漏检代价 ,寻最优分位;
- 排班:把故障风险映射成排班负荷,做代价灵敏度与风险低估反噬分析。
二、符号定义与核心方法
记真实类别 (0=正常,1=轴承磨损,2=过热,3=电机异常,4=未知),预测类别 。定义两类代价:
- 误报代价 :把正常判成故障,造成不必要的停机与检查成本;
- 漏检代价 :把故障判成正常,故障持续恶化直至停线,代价远大于误报。
异常检测端用马氏距离 ,以正常样本训练得均值 与协方差 ,阈值取训练正常样本分布的 99% 分位。征兆强度 由窗片段内特征偏离基线的最大幅度刻画,弱征兆定义为 、强征兆为 。
代价敏感阈值寻优的目标是在分位 上最小化期望代价
其中误报率与漏检率由当前阈值下的判定结果统计得到。
三、结果 I:逐类混淆——模型到底在哪里失败
图 1 给出集成模型的逐类混淆矩阵(行=真实、列=预测)。整体看对角线很干净,但把三个方案各自的"最大误判"摆在一起,失败模式立刻分野:
- 规则方案最大误判是"真实过热 → 预测正常"共 3 窗。过热被当成正常,意味着产线带着过热继续跑——这是最危险的失败,因为漏诊不会触发任何动作;
- CART 方案最大误判是"真实正常 → 预测轴承磨损"共 10 窗。正常被误报成故障,造成不必要的停机检查——这是最扰民的失败,频繁误报会让现场失去对报警的信任;
- 集成方案最大误判是"真实正常 → 预测电机异常"共 6 窗,介于两者之间,且没有任何已知故障被漏判成正常。
三种方案的"最易混淆类"完全不同:规则偏漏诊、CART 偏误报、集成在两者之间取得平衡。这说明"选哪个模型"本质上是在"漏诊风险"与"误报扰民"之间做权衡,而不是简单地比谁准确率高。
四、结果 II:弱/强征兆召回——早期预警能力的分水岭
把样本按征兆强度分成弱/强两档,分别算召回率(图 2),差距令人警醒:
- 规则方案弱征兆召回仅 0.571、强征兆召回 0.867;
- CART 弱征兆召回 1.000、强征兆 0.933;
- 集成方案弱、强征兆召回均为 1.000。
规则方案在弱征兆上大面积失手,意味着它只能抓住"已经很严重"的故障,对早期、轻微征兆几乎无能为力——而工程上最有价值的正是在故障萌芽期就报警。集成方案靠 Bagging 的投票稳定性,把弱征兆也兜住了。这条结论直接指向一个落地建议:若用于早期预警,规则方案应被集成方案替代,否则一半的早期窗口会被放过。
五、结果 III:未知故障的检出边界
未知故障(标签 4)不在分类器训练范围内,只能靠马氏距离异常检测兜底。在 99% 分位阈值(阈值=42.6)下:
- 整体检出率仅 56.2%;
- 弱征兆样本检出率 40.0%;
- 强征兆样本检出率 83.3%。
图 4 进一步把征兆强度切成 5 个分箱看检出率曲线:强度越低,检出率越接近随机水平;强度越高,检出率越逼近 80% 以上。换句话说,未知故障的检出几乎完全依赖"强征兆兜底"——一旦故障以温和方式演变,异常检测就会失效。这对排班的含义是:未知故障不能只靠模型报警,必须额外预留独立的安全余量(冗余巡检、定期解体检查),因为模型对温和型未知故障本质上是"盲"的。
六、结果 IV:代价最优阈值——证伪 98% 分位普适性
范文一固定用马氏距离 98% 分位作异常阈值。但阈值从来不该是拍脑袋的常数,而应由代价体制决定。我们在两种代价体制下寻优(图 5):
- 高故障代价体制():最优分位 85.00%,误报 13.3%、漏检 0.0%、期望代价 0.130;
- 均衡代价体制():最优分位同样落在 85.00%,误报 13.3%、漏检 0.0%。
两种体制都收敛到 85% 而非 98%——因为 98% 分位太保守,把大量正常样本误判为异常,推高了误报代价,却没能再降低漏检(漏检在更宽松处已归零)。这直接证伪了"98% 分位普适"的隐含假设:阈值必须随代价体制联动调整,标准化的固定阈值会系统性地浪费检测能力。
七、结果 V:排班灵敏度与风险低估的反噬
把故障风险映射成一周 7×3 班次的排班负荷,风险驱动方案(按需定员)相对均匀方案(每班固定 2 人)的节省率随缺员单价 单调上升(图 6):
- :节省 14.5%;
- (基准):节省 21.5%;
- :节省 28.9%;
- :节省 34.6%。
缺员越贵,精准排班越值钱——这是风险驱动方案的护城河。
但风险估计本身会出错。若管理层把周风险低估 10%,却仍按低估后的数值定员、而真实风险毫不变动,真实成本会反超基准(图 7):基准总成本 23735,按低估定员后真实成本升到 27120,反噬 +14.3%。也就是说,风险低估省下的那点定员成本,会以更大的真实缺员损失加倍奉还。这条反噬曲线给决策者一个硬约束:风险估计宁可偏高、不可偏低,因为低估的代价是不对称的放大。
八、结果 VI:三种方案的失效模式对比
把三种方案各自的"最大误判窗数"放在一起(图 8):规则 3 窗、CART 10 窗、集成 6 窗。CART 的误报最重(10 窗正常被误判),规则的漏诊最隐蔽(3 窗过热被放过但无人察觉),集成虽不是单项最优,却把两类极端失败都压在了中等水平。结合前文,落地选型不应只看平均准确率,而要看"哪种失败本企业承受得起"——连续产线怕漏诊(选集成而非规则),频繁启停怕误报(选集成而非 CART)。
九、结论与建议
本文从代价与不确定性两个维度重审同一套故障识别方案,得到四条可落地的结论:
- 识别的代价地图比准确率更重要。三种方案准确率接近,但失败模式(漏诊 vs 误报)天差地别,选型取决于企业能承受哪类失败。
- 早期弱征兆是规则方案的最大盲区(弱征兆召回 0.571),用于预警时应优先采用集成方案。
- 未知故障只能靠强征兆兜底(整体检出 56.2%、弱征兆仅 40.0%),温和型未知故障模型本质失明,必须配独立安全余量。
- 阈值与排班必须代价敏感且不能低估风险:最优阈值在 85% 而非 98% 分位;风险低估 10% 会让真实成本反超 14.3%。
这些结论共同指向一个工程信条:模型训到高准确率只是起点,把它放进代价与不确定性的真实坐标系里检验,才算完成。
十、模型验证(四路一致)
本文正文、配图、附录代码、真源 tools/gen_tidy2024a_2.py 四路数字完全一致。附录代码块可在 tools/ 目录下独立运行复现全部关键数字(混淆矩阵最易混淆类、弱/强征兆召回、未知故障检出率、代价最优分位、排班节省率与风险低估反噬),所有结果由固定随机种子确定,双跑字节一致。
参考文献
[1] Breiman L. Bagging predictors. Machine Learning, 1996.
[2] Breiman L. Random Forests. Machine Learning, 2001.
[3] Mahalanobis P C. On the generalized distance in statistics. 1936.
[4] Elkan C. The foundations of cost-sensitive learning. IJCAI, 2001.
[5] 泰迪杯 2024A 赛题组委会. 产线故障识别赛题数据说明.
附录:核心 Python 实现
import sys, os
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.abspath(os.path.join(_HERE, "..", "..", "..", "tools")))
import gen_tidy2024a_2 as H
r = H.gen_tidy2024a_2()
# E1 逐类混淆:各方案最易混淆类
for name, (labs, M) in r["e1"]["confs"].items():
w = r["e1"]["worst"][name]
print("E1", name, "最易混淆 真实%s->预测%s 共%d窗"
% (H.NAMES[w[0]], H.NAMES[w[1]], w[2]))
# E2 弱/强征兆召回
for name, (wr, sr) in r["e2"].items():
print("E2 %s 弱征兆召回%.3f 强征兆召回%.3f" % (name, wr, sr))
# E3 未知故障检出
e3 = r["e3"]
print("E3 整体检出%.3f 弱征兆%.3f 强征兆%.3f 阈值%.1f"
% (e3["overall"], e3["weak_strong"][0], e3["weak_strong"][1], e3["thr"]))
# E4 代价最优阈值
for reg, d in r["e4"].items():
print("E4 %s 最优分位%.2f 误报%.3f 漏检%.3f 期望代价%.3f"
% (reg, d["opt_q"] * 100, d["fa_at_opt"], d["miss_at_opt"], d["opt_cost"]))
# E5 排班灵敏度与风险低估反噬
pr0, pe0 = r["e5"]["base"]
print("E5 基准 风险驱动%.0f 均匀%.0f 节省%.1f%%"
% (pr0[2], pe0[2], (pe0[2] - pr0[2]) / pe0[2] * 100))
for cL, (a, b, sav) in r["e5"]["cl_curve"].items():
print("E5 cL=%d 节省%.1f%%" % (cL, sav))
ru = r["e5"]["risk_under"]
print("E5 风险低估10%%仍按低估定员 真实成本%.0f 基准%.0f 反噬+%.1f%%"
% (ru[2], pr0[2], (ru[2] - pr0[2]) / pr0[2] * 100))