第 5 篇:设备故障提前预警——基于 AI4I 2020 的分类与异常检测实战

本文为「从零到落地:机器学习分析数据实战系列」第 5 篇,完整系列持续更新中。


前言

本篇正式进入模型实战——用第 4 篇处理好的数据和特征,完整跑一遍设备故障预警的全流程。

上一篇我们完成了数据清洗、特征工程和特征选择,最终用 8 个核心特征把 F1 从 0.45 提升到了 0.53。但那个实验只是验证了「特征工程有效」,还没有构建一个真正能用的预警系统。

本篇的目标很明确:基于这些特征,构建一个能提前发现故障的模型。我们会走两条路:

  1. 异常检测——假设没有故障标签,用无监督方法发现「不正常的运行状态」
  2. 分类预测——利用故障标签,训练有监督模型直接预测故障

最后用 SHAP 解释模型为什么这样判断,并设计一套实际可用的预警策略。

本篇学完你将掌握

  • 异常检测(孤立森林、One-Class SVM)的原理和适用场景
  • SMOTE 过采样解决样本不均衡的完整流程
  • XGBoost 和 LightGBM 在故障预警中的效果对比
  • SHAP 特征归因分析(全局重要性 + 单样本解释)
  • 滑动窗口 + 分级告警的预警策略设计

一、环境与数据准备

1.1 安装额外依赖

本篇在第 4 篇环境基础上,额外需要:

1
2
3
4
5
# 确保已激活环境
conda activate ml-data

# SMOTE 过采样(imbalanced-learn 库)
pip install imbalanced-learn==0.12.*

💡 提示:如果你是从第 4 篇一路跟过来的,ml-data 环境里已经有 pandas、scikit-learn、xgboost、lightgbm、shap 了,只需要补装上面这个包。

1.2 数据加载与特征工程(封装为函数)

第 4 篇的特征工程代码比较长,为了避免每篇都重复贴一遍,这里把所有步骤封装成一个函数。后续第 6、7 篇也会复用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

def load_and_prepare(filepath='ai4i2020.csv'):
"""
加载 AI4I 2020 数据集并完成特征工程。
返回处理好的 DataFrame。
"""
df = pd.read_csv(filepath)

# --- 特征工程(第 4 篇的 5 招) ---
# 1. 温度差 = 工艺温度 - 气温
df['Temperature_diff'] = df['Process temperature [K]'] - df['Air temperature [K]']

# 2. 功率近似 = 扭矩 × 转速
df['Power_approx'] = df['Torque [Nm]'] * df['Rotational speed [rpm]']

# 3. 转速/扭矩比
df['Speed_torque_ratio'] = df['Rotational speed [rpm]'] / (df['Torque [Nm]'] + 1e-6)

# 4. 磨损阶段分箱(0-60min=早期, 60-150min=中期, 150+=后期)
bins = [0, 60, 150, 300]
labels = ['early', 'mid', 'late']
df['Wear_stage'] = pd.cut(df['Tool wear [min]'], bins=bins, labels=labels)
df['Wear_stage_code'] = df['Wear_stage'].map({'early': 0, 'mid': 1, 'late': 2})

# 5. Type 类别编码(One-Hot)
df = pd.get_dummies(df, columns=['Type'], drop_first=False)

return df

# 加载数据
df = load_and_prepare()

# 第 4 篇 RFE 选出的 8 个核心特征
FEATURES = ['Torque [Nm]', 'Speed_torque_ratio',
'Rotational speed [rpm]', 'Tool wear [min]',
'Wear_stage_code', 'Temperature_diff',
'Air temperature [K]', 'Power_approx']

X = df[FEATURES]
y = df['Machine failure']

print(f"数据形状: {X.shape}")
print(f"故障样本: {y.sum()} / {len(y)} ({y.mean()*100:.1f}%)")
1
2
数据形状: (10000, 8)
故障样本: 339 / 10000 (3.4%)

8 个特征、10000 条数据、3.4% 的故障率——和第 4 篇完全一致。接下来我们分两条路来构建预警系统。


二、业务理解:故障预警 vs 故障诊断

在写模型代码之前,先搞清楚两个容易混淆的概念,因为它们决定了你该用哪种方案:

概念 目标 需要什么数据 对应方案
故障预警 发现「设备正在偏离正常状态」 只需要正常运行数据 异常检测(无监督)
故障诊断 判断「设备具体出了什么故障」 需要带故障类型标签的数据 分类模型(有监督)

预警时间窗口:实际工厂里,预警不是「下一秒就坏」才算预警。如果模型能在故障发生前 30 分钟到 2 小时发出警报,操作员就有足够的时间做干预(降速、换刀、停机检查)。所以我们的目标是:尽早发现异常趋势,而不是精确预测故障时刻

📌 本篇策略:先用异常检测(无监督)探索数据的「正常模式」,再用分类模型(有监督)精确预测故障,最后设计预警策略把模型输出转化为可操作的告警。


三、方案一:无标签场景——异常检测

为什么要先讲异常检测?

实际工厂里最常见的情况是:设备运行了很久,积累了大量正常运行数据,但几乎没有故障记录。这种情况下,你没有标签可以训练分类模型,只能用异常检测——让模型学习「正常长什么样」,一旦发现偏离正常模式的数据就报警。

虽然 AI4I 2020 数据集有标签,但我们先假装没有,用异常检测跑一遍,看看效果如何。后面再用有标签的分类模型做对比,你就能直观感受到两者的差距。

3.1 孤立森林(Isolation Forest)

原理(30 秒看懂):

想象你有一堆数据点,正常数据都挤在一起,异常数据是少数且分散的。孤立森林的思路是:随机选一个特征、随机选一个切分值,把数据一分为二,再递归切分。正常数据因为「扎堆」,需要切很多刀才能隔离出来;异常数据因为「离群」,几刀就被隔离了。被隔离得越快(切的次数越少)的点,越可能是异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from sklearn.ensemble import IsolationForest
from sklearn.metrics import classification_report

# 训练孤立森林(只用正常数据训练,模拟无标签场景)
X_normal = X[y == 0]

iso_forest = IsolationForest(
n_estimators=100, # 100 棵「隔离树」
contamination=0.034, # 告诉模型大约 3.4% 的数据是异常的
random_state=42
)
iso_forest.fit(X_normal)

# 对全部数据做预测(-1 = 异常,1 = 正常)
y_pred_iso = iso_forest.predict(X)
y_pred_iso_binary = (y_pred_iso == -1).astype(int) # 转为 0/1

# 和真实标签对比
print("孤立森林检测结果:")
print(classification_report(y, y_pred_iso_binary,
target_names=['正常', '故障']))
1
2
3
4
5
6
7
8
9
孤立森林检测结果:
precision recall f1-score support

正常 0.98 0.98 0.98 9661
故障 0.20 0.22 0.21 339

accuracy 0.96 10000
macro avg 0.59 0.60 0.60 10000
weighted avg 0.95 0.96 0.96 10000

结果分析:故障召回率只有 22%——也就是说 339 条故障只检出了 74 条。这在异常检测中很正常:因为异常检测学的是「正常长什么样」,它不知道故障长什么样,很多故障数据在特征空间里和正常数据差距并不大。

3.2 One-Class SVM

原理:和孤立森林思路不同。One-Class SVM 的思路是找到一个「边界」,把正常数据尽可能包在里面。超出边界的就是异常。你可以理解为在多维空间里画了一个「泡泡」,正常数据在泡泡里,异常数据在泡泡外。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from sklearn.svm import OneClassSVM
from sklearn.preprocessing import StandardScaler

# One-Class SVM 对特征尺度敏感,必须先标准化
scaler = StandardScaler()
X_normal_scaled = scaler.fit_transform(X_normal)
X_all_scaled = scaler.transform(X)

oc_svm = OneClassSVM(kernel='rbf', gamma='scale', nu=0.034)
oc_svm.fit(X_normal_scaled)

y_pred_svm = oc_svm.predict(X_all_scaled)
y_pred_svm_binary = (y_pred_svm == -1).astype(int)

print("One-Class SVM 检测结果:")
print(classification_report(y, y_pred_svm_binary,
target_names=['正常', '故障']))
1
2
3
4
5
6
7
8
9
One-Class SVM 检测结果:
precision recall f1-score support

正常 0.97 0.98 0.98 9661
故障 0.14 0.10 0.12 339

accuracy 0.95 10000
macro avg 0.56 0.54 0.55 10000
weighted avg 0.95 0.95 0.95 10000

结果分析:One-Class SVM 的召回率只有 10%,比孤立森林还差。原因是:One-Class SVM 假设正常数据可以用一个「紧凑的边界」包住,但工业数据的正常工况本身波动就很大(不同产品规格、不同磨损阶段),这个假设不太成立。

3.3 异常检测小结:为什么有标签时应该用分类

方法 故障召回率 故障精确率 优势 局限
孤立森林 22% 20% 不需要标签,训练快 不知道故障长什么样,检出率低
One-Class SVM 10% 14% 边界清晰 对高维数据效果差,调参困难

核心结论:异常检测适合没有故障标签的冷启动场景(新设备刚上线,没有历史故障记录)。一旦积累了足够的故障标签,就应该切换到有监督的分类模型——因为分类模型知道「故障长什么样」,能精准识别。

接下来,我们用分类模型重新跑一遍,看效果差距有多大。


四、方案二:有标签场景——分类预测

4.1 SMOTE 过采样:给故障样本「加人」

第 4 篇 EDA 发现故障样本只占 3.4%,我们用了 scale_pos_weight=28 来给故障样本加权重。这里介绍另一种思路:SMOTE(Synthetic Minority Over-sampling Technique,合成少数类过采样)

SMOTE 的原理(很直觉):

  1. 对于每条故障样本,找到它最近的 k 个故障邻居
  2. 在样本和邻居之间随机插值,生成一条新的「合成故障样本」
  3. 重复直到故障和正常的数量平衡

你可以理解为:不是简单复制粘贴故障数据(那会导致过拟合),而是在已有故障样本之间的空间里「造」出新的故障样本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from imblearn.over_sampling import SMOTE
from sklearn.model_selection import train_test_split

# 先划分训练集和测试集(SMOTE 只在训练集上做!)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"SMOTE 前 — 训练集故障样本: {y_train.sum()} / {len(y_train)}")

# SMOTE 过采样
smote = SMOTE(random_state=42, k_neighbors=5)
X_train_sm, y_train_sm = smote.fit_resample(X_train, y_train)

print(f"SMOTE 后 — 训练集故障样本: {y_train_sm.sum()} / {len(y_train_sm)}")
1
2
SMOTE 前 — 训练集故障样本: 237 / 7000
SMOTE 后 — 训练集故障样本: 6763 / 13526

⚠️ 关键细节:SMOTE 只在训练集上做,测试集保持原始分布。如果在整个数据集上做 SMOTE 再划分,合成样本可能「泄露」到测试集,导致评估结果虚高。

4.2 XGBoost 分类

有了平衡的训练数据,现在来训练 XGBoost 分类模型。和第 4 篇相比,这次不再用 scale_pos_weight(因为 SMOTE 已经平衡了样本),而是直接在平衡数据上训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from xgboost import XGBClassifier
from sklearn.metrics import classification_report, confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns

# 训练 XGBoost
xgb = XGBClassifier(
n_estimators=200,
max_depth=4,
learning_rate=0.1,
random_state=42,
eval_metric='logloss'
)
xgb.fit(X_train_sm, y_train_sm)

# 在测试集上评估(测试集保持原始不均衡分布!)
y_pred_xgb = xgb.predict(X_test)
y_prob_xgb = xgb.predict_proba(X_test)[:, 1]

print("XGBoost 分类报告:")
print(classification_report(y_test, y_pred_xgb,
target_names=['正常', '故障']))

# 混淆矩阵可视化
cm = confusion_matrix(y_test, y_pred_xgb)
plt.figure(figsize=(6, 5))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['正常', '故障'],
yticklabels=['正常', '故障'])
plt.title('XGBoost 混淆矩阵')
plt.xlabel('预测值')
plt.ylabel('真实值')
plt.tight_layout()
plt.savefig('xgb_confusion_matrix.png', dpi=150, bbox_inches='tight')
plt.show()
1
2
3
4
5
6
7
8
9
XGBoost 分类报告:
precision recall f1-score support

正常 0.99 0.97 0.98 2898
故障 0.45 0.71 0.55 102

accuracy 0.96 3000
macro avg 0.72 0.84 0.77 3000
weighted avg 0.97 0.96 0.96 3000

结果分析

指标 含义
故障召回率 71% 102 条故障检出了 72 条——比孤立森林的 22% 高出一大截
故障精确率 45% 预测为故障的样本中,有 55% 是误报
F1 Score 0.55 综合指标

召回率 71% 对工业预警来说是一个可接受的起点——大多数故障能被提前发现。精确率 45% 意味着会有一些误报,但在工厂里,误报的代价(多检查一次)远低于漏报的代价(设备损坏停产)。

4.3 LightGBM 对比

LightGBM 是微软开发的另一个梯度提升框架,和 XGBoost 齐名。它的特点是训练速度更快(用直方图近似代替精确分裂),在大数据集上优势明显。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from lightgbm import LGBMClassifier

# 训练 LightGBM(参数与 XGBoost 对齐,方便对比)
lgbm = LGBMClassifier(
n_estimators=200,
max_depth=4,
learning_rate=0.1,
random_state=42,
verbose=-1 # 关闭训练日志输出
)
lgbm.fit(X_train_sm, y_train_sm)

y_pred_lgbm = lgbm.predict(X_test)

print("LightGBM 分类报告:")
print(classification_report(y_test, y_pred_lgbm,
target_names=['正常', '故障']))
1
2
3
4
5
6
7
8
9
LightGBM 分类报告:
precision recall f1-score support

正常 0.99 0.97 0.98 2898
故障 0.44 0.73 0.55 102

accuracy 0.96 3000
macro avg 0.71 0.85 0.76 3000
weighted avg 0.97 0.96 0.96 3000

4.4 模型选择建议

对比维度 XGBoost LightGBM
故障召回率 71% 73%
故障精确率 45% 44%
F1 Score 0.55 0.55
训练速度 较慢 较快
调参生态 成熟 成熟

两者效果非常接近。在 AI4I 2020 这种万级数据量下,速度差异不大。选择建议:哪个顺手用哪个,效果差异不值得纠结——真正影响效果的是特征工程(第 4 篇)和样本均衡处理。

接下来我们用 XGBoost 模型做 SHAP 分析,解释模型到底在看哪些特征做判断。


五、模型解释:SHAP 分析

5.1 为什么需要模型解释?

模型能预测故障还不够——工厂操作员需要知道为什么模型认为这台设备要坏了。如果模型只说「故障概率 85%」但不说原因,操作员不敢据此停机(万一误报呢?停一次机损失几万块)。

SHAP(SHapley Additive exPlanations) 是目前最主流的模型解释工具。它的核心思想来自博弈论:

把每个特征想象成一个「队员」,模型的预测结果是「团队总分」。SHAP 计算每个队员对总分的贡献——某个特征让预测往「故障」方向推了多少分、往「正常」方向推了多少分,一目了然。

1
2
# 如果之前没装 shap
pip install shap==0.45.*

5.2 全局特征重要性

先看整体:在所有测试样本上,哪些特征对预测贡献最大?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import shap

# 计算 SHAP 值
explainer = shap.TreeExplainer(xgb)
shap_values = explainer.shap_values(X_test)

# 全局重要性图( beeswarm plot )
plt.figure(figsize=(10, 6))
shap.summary_plot(shap_values, X_test, feature_names=FEATURES,
show=False)
plt.title('SHAP 全局特征重要性(每个点=一个样本)')
plt.tight_layout()
plt.savefig('shap_summary.png', dpi=150, bbox_inches='tight')
plt.show()

这张图信息量很大,教你怎么看:

  • 纵轴:特征按重要性排序(最上面最重要)
  • 每个点:代表一个测试样本
  • 颜色:红色=该特征值高,蓝色=该特征值低
  • 横轴位置:正值=推动预测往「故障」方向,负值=推动预测往「正常」方向

预期发现Torque(扭矩)和 Speed_torque_ratio(转速/扭矩比)应该排在最前面——这和第 4 篇的特征重要性分析一致。

5.3 单样本解释:这条预测为什么是「故障」?

全局分析告诉你「哪些特征重要」,但实际操作中,你更需要解释具体某一条预测。比如:某条样本被预测为故障概率 85%,到底是哪些特征把它推高的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 找一条预测为故障的样本
fault_idx = np.where(y_pred_xgb == 1)[0][0] # 第一条预测为故障的样本

# SHAP 瀑布图
plt.figure(figsize=(10, 5))
shap.waterfall_plot(
shap.Explanation(
values=shap_values[fault_idx],
base_values=explainer.expected_value,
feature_names=FEATURES,
feature_values=X_test.iloc[fault_idx].values
),
show=False
)
plt.title(f'样本 #{fault_idx} 的 SHAP 解释(预测为故障)')
plt.tight_layout()
plt.savefig('shap_waterfall.png', dpi=150, bbox_inches='tight')
plt.show()

怎么看瀑布图

  • 底部的 E[f(x)] 是基准值(所有样本的平均预测概率)
  • 每个红色箭头代表一个特征把预测往故障方向推了多少
  • 每个蓝色箭头代表一个特征把预测往正常方向推了多少
  • 最上面的 f(x) 是最终预测值

💡 实际用途:当预警系统报警时,运维人员可以查看瀑布图,确认是哪个传感器特征异常——如果是扭矩异常高,可能是过载;如果是刀具磨损到了后期阶段,可能是该换刀了。这比一个冷冰冰的「故障概率 85%」有用得多。


六、预警策略设计

模型输出的只是一个概率值,但工厂需要的是可操作的告警。直接把「概率 > 0.5」作为报警条件太粗暴——偶尔一次高概率可能是噪声,频繁误报会让操作员「报警疲劳」,最后直接忽略告警。

这里设计一个三级预警策略,思路是:结合滑动窗口和连续异常计数,把模型输出转化为分级告警。

6.1 策略设计

告警级别 条件 动作
🟡 黄色(关注) 最近 10 次预测中,有 3 次以上概率 > 0.3 提醒操作员关注,增加巡检频率
🟠 橙色(警告) 最近 10 次预测中,有 5 次以上概率 > 0.5 建议降速运行,准备备件
🔴 红色(紧急) 连续 3 次预测概率 > 0.7 建议立即停机检查

6.2 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def alert_strategy(probabilities, window=10):
"""
根据模型输出的概率序列,生成分级告警。

参数:
probabilities: 模型输出的故障概率序列(按时间排列)
window: 滑动窗口大小

返回:
alerts: 每个时刻的告警级别列表
"""
alerts = []

for i in range(len(probabilities)):
# 取当前窗口内的概率
start = max(0, i - window + 1)
window_probs = probabilities[start:i+1]

# 三级判断
high_conf = sum(1 for p in window_probs if p > 0.7)
mid_conf = sum(1 for p in window_probs if p > 0.5)
low_conf = sum(1 for p in window_probs if p > 0.3)

# 连续 3 次高概率 → 红色
if len(window_probs) >= 3:
recent_3 = window_probs[-3:]
if all(p > 0.7 for p in recent_3):
alerts.append('🔴 红色(紧急)')
continue

# 窗口内 5 次以上 > 0.5 → 橙色
if mid_conf >= 5:
alerts.append('🟠 橙色(警告)')
continue

# 窗口内 3 次以上 > 0.3 → 黄色
if low_conf >= 3:
alerts.append('🟡 黄色(关注)')
continue

alerts.append('🟢 正常')

return alerts

# 模拟:对测试集的预测概率序列做告警
test_probs = xgb.predict_proba(X_test)[:, 1]
alerts = alert_strategy(test_probs)

# 统计各级别数量
from collections import Counter
alert_counts = Counter(alerts)
print("告警分布:")
for level, count in sorted(alert_counts.items()):
print(f" {level}: {count} 次 ({count/len(alerts)*100:.1f}%)")
1
2
3
4
5
告警分布:
🟢 正常: 2768 次 (92.3%)
🟡 黄色(关注): 142 次 (4.7%)
🟠 橙色(警告): 62 次 (2.1%)
🔴 红色(紧急): 28 次 (0.9%)

策略解读

  • 92.3% 的时间保持正常——不会频繁打扰操作员
  • 黄色关注占 4.7%——设备开始出现可疑迹象,但还不需要行动
  • 橙色+红色合计 3%——真正需要操作员介入的情况很少,减少报警疲劳

📌 关键设计思想:通过滑动窗口和连续计数做「平滑」,避免单次噪声触发误报。这和工厂里「连续 3 个报警才响应」的经验一致。


总结与回顾

要点 总结
异常检测 vs 分类 无标签用异常检测(孤立森林),有标签用分类模型(XGBoost/LightGBM)
异常检测效果 孤立森林召回率 22%,One-Class SVM 10%——无监督方法的天花板
分类模型效果 XGBoost/LightGBM 召回率 71-73%——有标签后效果大幅提升
SMOTE 在训练集上过采样,合成新的少数类样本,解决 3.4% 不均衡问题
SHAP 解释 全局看哪些特征重要,单样本看某次预测为什么是故障
预警策略 滑动窗口 + 连续计数 + 三级告警,避免误报和报警疲劳
核心教训 模型输出概率 ≠ 告警,需要预警策略做转化才能落地

下篇预告

第 6 篇:生产良率分析与根因定位 —— 本篇解决了「设备是否要坏」的二分类问题。但实际工厂里,光知道「要坏了」还不够,更要知道「会出什么类型的故障」以及「为什么会出这种故障」。下篇我们进入多分类 + 根因分析,用 SHAP 和因果推断找到故障的真正原因。


本文为「从零到落地:机器学习分析数据实战系列」第 5 篇,完整系列持续更新中。