第 8 篇:运行趋势研判——基于 NASA C-MAPSS 的时序预测实战

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


前言

前四篇都是在「当下」做判断,这篇要进入时间维度——预测设备未来的运行状态和剩余寿命。

回顾一下第 5-7 篇的逻辑链:

  • 第 5 篇:设备现在有没有故障?→ 分类问题
  • 第 6 篇:故障的根因是什么?→ 多分类 + 归因
  • 第 7 篇:当前参数怎么调最优?→ 回归 + 优化

这三篇解决的都是「当下」的问题。但在实际工厂里,运维主管还关心另一个问题:「这台设备还能跑多久?下周需要安排维保吗?」

这就是本篇要解决的问题——运行趋势研判,核心任务是预测设备的剩余使用寿命(RUL, Remaining Useful Life)

和前面几篇不同,本篇使用一个全新的数据集——NASA C-MAPSS FD001 涡扇发动机退化数据集。原因是前四篇的 AI4I 2020 数据集每条记录是独立的(没有时间维度),而 C-MAPSS 是真正的时序退化数据:每台发动机从全新运行到故障,记录了几百个时间步的传感器读数,天然适合做趋势预测。

我们会用三种复杂度递进的方法来做预测,让读者理解每种方法的原理、优缺点和适用场景:

  1. 统计方法(线性回归 / ARIMA)—— 最简单,作为基准线
  2. 机器学习方法(XGBoost + 时序特征)—— 兼顾效果和可解释性
  3. 深度学习方法(LSTM)—— 自动学习时序依赖关系

本篇学完你将掌握

  • NASA C-MAPSS 涡扇发动机退化数据集的结构和使用方法
  • 时间序列特征工程的核心技巧(滑动窗口、退化趋势、交叉特征)
  • ARIMA 的原理和适用边界
  • XGBoost 在时序预测中的特征工程策略
  • LSTM 的原理(为什么它擅长处理序列数据)
  • 三种方法的横向对比与选型建议
  • RUL 预测结果的可视化与预警阈值设计

一、环境与数据准备

1.1 安装额外依赖

1
2
3
4
5
6
7
8
conda activate ml-data

# PyTorch(LSTM 模型需要)
# Windows/Linux CPU 版本即可,如有 NVIDIA GPU 可安装 CUDA 版本加速训练
pip install torch==2.4.*

# ARIMA 模型
pip install statsmodels==0.14.*

💡 提示:其他依赖(pandas、scikit-learn、xgboost、matplotlib、seaborn)在第 4 篇已安装,无需重复。PyTorch 安装如果网络慢,可以用清华镜像源:pip install torch==2.4.* -i https://pypi.tuna.tsinghua.edu.cn/simple

1.2 数据集下载

NASA C-MAPSS 数据集是涡扇发动机退化仿真领域的标准基准数据集,广泛用于 RUL 预测研究。

什么是涡扇发动机?

涡扇发动机(Turbofan Engine)是现代民航客机和军用飞机最常用的动力装置。你可以把它理解为「用风扇吹空气产生推力的喷气发动机」——空气被多级风扇和压气机压缩,与燃料混合燃烧,高温高压气体推动涡轮旋转产生推力。

和汽车发动机不同,涡扇发动机工作在极端环境下(温度 1500°C+、转速 10000+ rpm),维护成本极高。一次非计划停机可能损失数十万美元,因此预测它的剩余使用寿命有巨大的实际价值。

什么是 C-MAPSS?

C-MAPSS(Commercial Modular Aero-Propulsion System Simulation)是 NASA 开发的涡扇发动机仿真器。它模拟了发动机从全新状态逐渐退化,直到无法满足推力需求的全过程。FD001 是 4 个子集之一,只包含一种运行条件和一种故障模式,最适合入门。

数据集结构

文件 内容 尺寸
train_FD001.txt 100 台发动机的退化时序(从健康到故障) 128~362 个时间步/台
test_FD001.txt 100 台发动机的运行序列(在某时刻截断) 31~192 个时间步/台
RUL_FD001.txt 测试集每台发动机的真实 RUL(距故障还有多少步) 100 个值

每条记录包含 21 个传感器读数 + 3 个操作参数,没有列名,用空格分隔。

什么是 RUL(Remaining Useful Life)?

RUL = 剩余使用寿命 = 设备距离故障还有多少个时间步。比如某发动机在时刻 200 时 RUL = 50,意味着再过 50 个时间步它就会故障。RUL 是本篇的目标变量——预测它就是预测「设备还能跑多久」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 下载数据集(NASA 原始链接可能不稳定,备选方案见下方说明)
import urllib.request
import os

data_dir = 'CMAPSSData'
os.makedirs(data_dir, exist_ok=True)

# 如果 NASA 原链接无法访问,可以用 Kaggle 镜像:
# https://www.kaggle.com/datasets/behrad3d/nasa-cmaps
# 下载后解压到 CMAPSSData 文件夹即可
base_url = 'https://ti.arc.nasa.gov/m/project/prognostic-repo/'
files = ['train_FD001.txt', 'test_FD001.txt', 'RUL_FD001.txt']

for f in files:
url = base_url + f
filepath = os.path.join(data_dir, f)
if not os.path.exists(filepath):
try:
urllib.request.urlretrieve(url, filepath)
print(f'下载完成: {f}')
except Exception as e:
print(f'下载失败: {f},请手动从 Kaggle 下载')
else:
print(f'已存在: {f}')

二、业务理解:趋势研判解决什么问题?

2.1 RUL 预测 vs 故障分类

前四篇用 AI4I 2020 数据集做的是分类问题——设备「有没有故障」「是哪种故障」。答案是离散的标签。

本篇做的是回归问题——预测一个连续的数值:RUL。区别在于:

维度 故障分类(第 5 篇) RUL 预测(本篇)
目标 判断「有没有故障」 预测「还能跑多久」
输出 0/1 标签 连续数值(时间步数)
数据类型 独立样本 时间序列
决策支持 要不要报警 什么时候安排维保

2.2 为什么 RUL 比「会不会坏」更有用?

知道「设备快要坏了」只是第一步。运维主管真正需要的是时间信息

  • RUL = 200 步 → 设备还很健康,正常使用,安排到下次计划停机时检修
  • RUL = 50 步 → 提前准备备件,安排维保人员
  • RUL = 10 步 → 立即停机检修,避免灾难性故障

RUL 预测让维护从「被动响应」变成「主动规划」,这正是预测性维护(Predictive Maintenance) 的核心价值。


三、数据加载与探索

3.1 加载数据

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
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

# ---------- 加载训练集 ----------
train_df = pd.read_csv('CMAPSSData/train_FD001.txt', sep=r'\s+', header=None)
train_df.columns = ['engine_id', 'cycle', 'setting1', 'setting2', 'setting3'] + \
[f's{i}' for i in range(1, 22)]

# ---------- 加载测试集 ----------
test_df = pd.read_csv('CMAPSSData/test_FD001.txt', sep=r'\s+', header=None)
test_df.columns = ['engine_id', 'cycle', 'setting1', 'setting2', 'setting3'] + \
[f's{i}' for i in range(1, 22)]

# ---------- 加载测试集的 RUL 标签 ----------
rul_true = pd.read_csv('CMAPSSData/RUL_FD001.txt', sep=r'\s+', header=None)
rul_true.columns = ['RUL']

print(f"训练集: {train_df.shape[0]} 行, {train_df['engine_id'].nunique()} 台发动机")
print(f"测试集: {test_df.shape[0]} 行, {test_df['engine_id'].nunique()} 台发动机")
1
2
训练集: 20631 行, 100 台发动机
测试集: 13096 行, 100 台发动机

3.2 理解数据结构

1
2
3
4
5
6
7
8
9
10
11
# Setting 列是行号,没有信息量,丢弃
train_df.drop('Setting', axis=1, errors='ignore', inplace=True)
test_df.drop('Setting', axis=1, errors='ignore', inplace=True)

# 操作设置参数(飞行条件)
setting_cols = ['setting1', 'setting2', 'setting3']
# 传感器读数
sensor_cols = [f's{i}' for i in range(1, 22)]

print("训练集基本信息:")
print(train_df[['engine_id', 'cycle'] + sensor_cols[:5]].describe().round(2))

📌 关键概念:每台发动机对应一个连续时间序列,从 cycle=1 运行到故障发生。训练集记录了完整的退化过程,测试集只记录到某个中间时刻,需要预测距故障还有多远(RUL)。

3.3 可视化退化过程

先看几台发动机的传感器变化趋势,直观感受「退化」长什么样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 选 3 台有代表性的发动机(不同退化长度)
engines = [1, 20, 50]
# 选 4 个变化明显的传感器
key_sensors = ['s2', 's3', 's4', 's11']
titles = ['传感器 s2 (温度相关)', '传感器 s3 (压力相关)',
'传感器 s4 (转速相关)', '传感器 s11 (温度相关)']

for ax, sensor, title in zip(axes.flat, key_sensors, titles):
for eng_id in engines:
eng_data = train_df[train_df['engine_id'] == eng_id]
ax.plot(eng_data['cycle'], eng_data[sensor], label=f'发动机 {eng_id}')
ax.set_xlabel('运行周期 (cycle)')
ax.set_ylabel('传感器读数')
ax.set_title(title)
ax.legend()
ax.grid(True, alpha=0.3)

plt.suptitle('涡扇发动机退化过程:3 台发动机的 4 个关键传感器', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

从图中可以观察到三个关键现象:

  1. 单调退化:大多数传感器呈现单调上升或下降趋势,说明退化是不可逆的
  2. 退化速度不一致:前期变化缓慢,后期加速恶化——这是典型的退化特征
  3. 不同发动机退化长度不同:有的 128 步就故障,有的 362 步才故障,说明个体差异很大

3.4 RUL 标签分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 测试集的 RUL 分布
plt.figure(figsize=(10, 4))
plt.hist(rul_true['RUL'], bins=30, color='steelblue', edgecolor='white')
plt.xlabel('RUL(剩余时间步)')
plt.ylabel('发动机数量')
plt.title('测试集 RUL 分布')
plt.axvline(rul_true['RUL'].mean(), color='red', linestyle='--',
label=f'均值 = {rul_true["RUL"].mean():.0f}')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

print(f"RUL 范围: {rul_true['RUL'].min()} ~ {rul_true['RUL'].max()}")
print(f"RUL 均值: {rul_true['RUL'].mean():.1f}")
print(f"RUL 中位数: {rul_true['RUL'].median():.1f}")

EDA 小结:C-MAPSS FD001 数据有三个特点:(1) 传感器呈单调退化趋势;(2) 不同发动机寿命差异大(128~362 步);(3) RUL 分布偏右(很多发动机在 RUL 较大时被截断)。这些特点会影响后面的特征工程策略。


四、特征工程:时间序列预测的关键

第 4 篇我们说过:「数据和特征决定了模型的上限」。在时间序列预测中,特征工程更加关键——好的时序特征能让简单模型打败复杂模型。

基于 EDA 的发现,我们构造三类特征:

类别 构造方法 目的
退化趋势特征 滑动窗口均值/标准差 平滑短期噪声,捕捉长期趋势
当前状态特征 原始传感器读数 反映设备当前健康状况
交叉特征 传感器之间的差值 捕捉多个传感器协同退化的模式
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
55
56
57
def create_features(df, sensor_cols, window_sizes=[5, 10, 20]):
"""
为每台发动机创建时序特征。

参数:
df: 原始数据 DataFrame
sensor_cols: 传感器列名列表
window_sizes: 滑动窗口大小列表
"""
feature_dfs = []

for engine_id, group in df.groupby('engine_id'):
group = group.sort_values('cycle').copy()

# (1) 操作参数的独热编码(捕捉不同运行条件)
group['setting_cluster'] = (
group['setting1'].round(1).astype(str) + '_' +
group['setting2'].round(1).astype(str)
)

# (2) 原始传感器读数 —— 反映当前状态
# 已经存在,无需额外处理

# (3) 滑动窗口特征 —— 平滑噪声 + 捕捉趋势
for w in window_sizes:
for col in sensor_cols:
# 窗口均值:平滑短期波动,反映「这段时间的平均水平」
group[f'{col}_rmean_{w}'] = group[col].rolling(
window=w, min_periods=1
).mean()
# 窗口标准差:衡量波动幅度,波动增大可能是退化加速的信号
group[f'{col}_rstd_{w}'] = group[col].rolling(
window=w, min_periods=1
).std().fillna(0)

# (4) 交叉特征:关键传感器的差值
# 温度-压力差:两者协同变化比单独变化更能反映退化
group['s2_minus_s3'] = group['s2'] - group['s3']
group['s4_minus_s11'] = group['s4'] - group['s11']

feature_dfs.append(group)

result = pd.concat(feature_dfs)

# 独热编码操作条件
result = pd.get_dummies(result, columns=['setting_cluster'], drop_first=True)

return result

# 创建特征
train_feat = create_features(train_df, sensor_cols)
test_feat = create_features(test_df, sensor_cols)

# 统计特征数量
feature_cols = [c for c in train_feat.columns
if c not in ['engine_id', 'cycle']]
print(f"总特征数: {len(feature_cols)}")
1
总特征数: 172

📌 为什么构造三类特征? 原始传感器读数反映「当前状态」;滑动窗口均值平滑了短期噪声,让模型看到「这段时间的平均水平」;窗口标准差衡量「波动幅度」——波动突然增大往往是退化加速的信号。交叉特征则捕捉了多个传感器的协同变化模式,比单看一个传感器更有区分力。

4.1 计算训练集的 RUL 标签

训练集没有直接的 RUL 标签,但每个序列的最后一个 cycle 就是故障时刻。所以 RUL = 该发动机最大 cycle - 当前 cycle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def add_rul_labels(df):
"""为训练集计算 RUL 标签。"""
df = df.copy()
# 每台发动机的最大 cycle = 故障时刻
max_cycles = df.groupby('engine_id')['cycle'].transform('max')
# RUL = 距故障还有多少步
df['RUL'] = max_cycles - df['cycle']
return df

train_feat = add_rul_labels(train_feat)

print("RUL 标签示例(发动机 1 的前 5 行和后 5 行):")
eng1 = train_feat[train_feat['engine_id'] == 1]
print(eng1[['engine_id', 'cycle', 'RUL']].head())
print("...")
print(eng1[['engine_id', 'cycle', 'RUL']].tail())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
RUL 标签示例(发动机 1 的前 5 行和后 5 行):
engine_id cycle RUL
0 1 1 191
1 1 2 190
2 1 3 189
3 1 4 188
4 1 5 187
...
engine_id cycle RUL
187 1 188 4
188 1 189 3
189 1 190 2
190 1 191 1
191 1 192 0

可以看到:发动机 1 运行了 192 个时间步后故障。第 1 步时 RUL = 191(距离故障还有 191 步),最后一步 RUL = 0(已经故障)。这就是我们要预测的目标变量。

4.2 划分训练集和验证集

1
2
3
4
5
6
7
8
9
10
11
12
# 按发动机 ID 划分:前 80 台训练,后 20 台验证
train_engines = list(range(1, 81))
val_engines = list(range(81, 101))

X_train = train_feat[train_feat['engine_id'].isin(train_engines)][feature_cols]
y_train = train_feat[train_feat['engine_id'].isin(train_engines)]['RUL']

X_val = train_feat[train_feat['engine_id'].isin(val_engines)][feature_cols]
y_val = train_feat[train_feat['engine_id'].isin(val_engines)]['RUL']

print(f"训练集: {len(X_train)} 样本 ({len(train_engines)} 台发动机)")
print(f"验证集: {len(X_val)} 样本 ({len(val_engines)} 台发动机)")
1
2
训练集: 16525 样本 (80 台发动机)
验证集: 4106 样本 (20 台发动机)

⚠️ 为什么按发动机划分,而不是随机划分? 时间序列数据中,同一个发动机的相邻时间步高度相关。如果随机划分,相邻时间步可能分别出现在训练集和验证集中,导致数据泄露——模型「偷看」了验证集的信息,评估结果虚高。按发动机划分确保训练和验证完全独立。


五、方案一:统计方法(基准线)

5.1 为什么先做基准线?

在跑复杂模型之前,先用最简单的方法建立一个基准线(baseline)。基准线的价值在于:如果后面的复杂模型打不过它,就说明复杂模型可能在过拟合,或者特征工程有问题。

最简单的思路:用线性回归拟合每个传感器的退化趋势,然后外推预测 RUL。

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
from sklearn.linear_model import LinearRegression

def predict_rul_linear(train_data, test_data, sensor_cols):
"""
对每台测试发动机,用线性回归外推退化趋势,预测 RUL。
思路:找到退化最快的传感器,拟合它的趋势线,外推到「故障阈值」。
"""
predictions = []

for engine_id in test_data['engine_id'].unique():
eng_train = train_data[train_data['engine_id'] == engine_id].sort_values('cycle')
eng_test = test_data[test_data['engine_id'] == engine_id].sort_values('cycle')

if len(eng_train) == 0 or len(eng_test) == 0:
predictions.append(0)
continue

# 用 s11(温度传感器,退化最明显)拟合线性趋势
X = eng_train['cycle'].values.reshape(-1, 1)
y = eng_train['s11'].values

model = LinearRegression()
model.fit(X, y)

# 获取故障阈值 = 训练集最后时刻的传感器值
failure_threshold = eng_train['s11'].iloc[-1]

# 预测测试集最后时刻的传感器值会何时达到阈值
last_cycle = eng_test['cycle'].iloc[-1]
current_value = model.predict([[last_cycle]])[0]

# 斜率(每步退化多少)
slope = model.coef_[0]
if abs(slope) < 1e-6:
predictions.append(100) # 几乎不退化,给一个大 RUL
else:
# 外推:还需要多少步达到阈值
rul = (failure_threshold - current_value) / slope
predictions.append(max(0, rul))

return np.array(predictions)

# 线性回归预测
rul_linear = predict_rul_linear(train_df, test_df, sensor_cols)

5.2 评估指标

RUL 预测用三个指标评估,每个指标关注不同方面:

指标 公式含义 看什么
RMSE 预测误差的平方均值的平方根 整体误差大小,对大误差敏感
MAE 预测误差的绝对值均值 平均偏了多少步
Score 非对称惩罚函数(后面详细解释) 工业场景最关心的指标
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
def evaluate_rul(y_true, y_pred, label='模型'):
"""RUL 预测评估:RMSE、MAE、Score。"""
y_true = np.array(y_true, dtype=float)
y_pred = np.array(y_pred, dtype=float)

# RMSE:均方根误差
rmse = np.sqrt(np.mean((y_true - y_pred) ** 2))

# MAE:平均绝对误差
mae = np.mean(np.abs(y_true - y_pred))

# Score:非对称评分函数
# 预测偏晚(d > 0)惩罚更重,因为实际中「低估寿命」比「高估寿命」更危险
d = y_pred - y_true
score = np.sum(
np.where(d < 0, np.exp(-d / 13) - 1, # 预测偏早(保守):轻罚
np.exp(d / 10) - 1) # 预测偏晚(危险):重罚
)

print(f"\n{label}:")
print(f" RMSE: {rmse:.2f}")
print(f" MAE: {mae:.2f}")
print(f" Score: {score:.2f}")

return rmse, mae, score

# 评估基准线
_ = evaluate_rul(rul_true['RUL'], rul_linear, '线性回归(基准线)')

📌 线性回归的局限:它假设退化是匀速的(线性),但实际退化往往「前期缓慢、后期加速」——这种非线性趋势用直线拟合不好。但它作为基准线足够了:后面的模型只要比它好,就说明捕捉到了更多信息。

5.3 ARIMA 时序预测

ARIMA 是传统时序分析的经典方法。它能捕捉趋势、季节性和自相关性,比线性回归更灵活。

什么是 ARIMA?

ARIMA(AutoRegressive Integrated Moving Average,自回归积分滑动平均)是时序预测最常用的统计模型,由三部分组成:

  • AR(自回归):用过去 p 个时刻的值预测当前值——「昨天的状态影响今天」
  • I(差分积分):对数据做 d 次差分,把非平稳序列变成平稳的——去掉趋势后才能用统计模型
  • MA(滑动平均):用过去 q 个时刻的预测误差修正当前预测——「过去的偏差可以纠正当下」

参数写作 ARIMA(p, d, q)。比如 ARIMA(1,1,1) 表示用 1 步自回归 + 1 次差分 + 1 步滑动平均。

ARIMA 只能对单条序列做预测,所以需要为每台发动机分别建模。这里用训练集中的一台发动机做演示:

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
from statsmodels.tsa.arima.model import ARIMA

# 选发动机 1 的 s11 传感器做 ARIMA 演示
eng1_train = train_df[train_df['engine_id'] == 1].sort_values('cycle')
sensor_series = eng1_train['s11'].values

# 用前 80% 数据训练,后 20% 验证
split = int(len(sensor_series) * 0.8)

try:
model = ARIMA(sensor_series[:split], order=(1, 1, 1))
fitted = model.fit()

# 向后预测
forecast = fitted.forecast(steps=len(sensor_series) - split)

# 可视化
plt.figure(figsize=(12, 4))
plt.plot(range(len(sensor_series)), sensor_series, 'b-', label='真实值')
plt.plot(range(split, len(sensor_series)), forecast, 'r--', label='ARIMA 预测')
plt.axvline(x=split, color='gray', linestyle=':', label='训练/测试分割')
plt.xlabel('运行周期')
plt.ylabel('传感器 s11')
plt.title('ARIMA 预测传感器退化趋势(发动机 1)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

print(f"ARIMA(1,1,1) 参数摘要:")
print(fitted.summary().tables[1])
except Exception as e:
print(f"ARIMA 拟合出错: {e}")
print("提示:某些序列可能不适合 ARIMA,可以尝试调整 order 参数")

📌 ARIMA 的优势与局限:优势是能捕捉时间依赖性(趋势 + 自相关),比线性回归更灵活。局限是:(1) 需要对每台发动机分别建模,无法共享参数;(2) 只适合平滑趋势,对非线性退化(后期突然加速)效果有限。下一节的 XGBoost 会解决这两个问题。


六、方案二:机器学习方法(XGBoost + 时序特征)

6.1 为什么 XGBoost 适合时序预测?

XGBoost 本身不是时序模型,但配合好的时序特征(滑动窗口均值、标准差等),它可以学习特征和 RUL 之间的复杂非线性关系。

和 ARIMA 相比,XGBoost 的优势是:

  1. 所有发动机共享一个模型——训练一次,预测所有
  2. 能捕捉非线性关系——自动学习「后期退化加速」的模式
  3. 特征重要性可解释——知道哪些传感器和特征最重要
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
from xgboost import XGBRegressor
from sklearn.preprocessing import StandardScaler

# 标准化特征(XGBoost 对尺度不太敏感,但标准化后更稳定)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)

# XGBoost 回归
xgb_model = XGBRegressor(
n_estimators=300, # 300 棵树
max_depth=6, # 每棵树最深 6 层
learning_rate=0.05, # 学习率(步长收缩系数),越小越稳定
subsample=0.8, # 每棵树用 80% 样本,防过拟合
colsample_bytree=0.8, # 每棵树用 80% 特征,防过拟合
random_state=42,
n_jobs=-1
)

xgb_model.fit(X_train_scaled, y_train)

# 验证集预测
y_pred_xgb = xgb_model.predict(X_val_scaled)
y_pred_xgb = np.clip(y_pred_xgb, 0, None) # RUL 不能为负

_ = evaluate_rul(y_val, y_pred_xgb, 'XGBoost')

📌 为什么效果比 ARIMA 好? XGBoost 用 172 个特征(包含滑动窗口统计量和交叉特征)来预测 RUL,能捕捉到「传感器后期加速退化」这种非线性模式。而 ARIMA 只用一条传感器序列的历史值,信息量有限。

6.2 特征重要性分析

XGBoost 的另一个优势是可解释性——可以看哪些特征对预测贡献最大:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 特征重要性 Top 15
importance = pd.Series(
xgb_model.feature_importances_, index=feature_cols
).sort_values(ascending=False)

plt.figure(figsize=(10, 6))
importance.head(15).plot(kind='barh', color='steelblue')
plt.gca().invert_yaxis()
plt.xlabel('特征重要性')
plt.title('XGBoost 特征重要性 Top 15')
plt.tight_layout()
plt.show()

# 区分原始特征 vs 构造特征
print("Top 15 特征分类:")
for name, val in importance.head(15).items():
feat_type = '构造特征' if '_rmean_' in name or '_rstd_' in name or 'minus' in name else '原始特征'
print(f" {name:<25s} {val:.4f} ({feat_type})")

通常 Top 15 中大部分是构造特征(滑动窗口均值/标准差),而不是原始传感器读数——这再次印证了第 4 篇的结论:特征工程比原始数据更有价值。


七、方案三:深度学习方法(LSTM)

7.1 LSTM 是什么?为什么适合时序?

LSTM(Long Short-Term Memory,长短期记忆网络)是一种专门处理序列数据的神经网络。

通俗理解:假设你在看一部电影,要判断当前场景的情绪。你不需要记住每一帧画面,只需要记住最近几个关键情节。LSTM 的工作方式类似——它通过「记忆门」机制,自动决定哪些信息该记住、哪些该忘记。

LSTM 的核心是三个门控机制:

作用 通俗理解
遗忘门 决定丢弃哪些旧信息 「这件事不重要了,忘掉」
输入门 决定存入哪些新信息 「这个新信息要记住」
输出门 决定输出哪些记忆 「根据已有记忆,给出判断」

和前面的方法相比,LSTM 的优势是自动学习时间依赖关系——不需要手动构造滑动窗口特征,它能自己从原始序列中学习到「看多远」「关注哪些时间步」。

7.2 数据准备:构造序列窗口

LSTM 的输入是 3D 张量:(样本数, 时间步长, 特征数)。需要把数据切成固定长度的序列窗口:

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
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

# ---------- 序列窗口化 ----------
def create_sequences(data, targets, seq_length=30):
"""
将时间序列切分为固定长度的滑动窗口。

参数:
data: 特征矩阵 (n_samples, n_features)
targets: 目标值 (n_samples,)
seq_length: 窗口长度(用多少个时间步预测 RUL)

返回:
sequences: (n_windows, seq_length, n_features)
labels: (n_windows,)
"""
sequences = []
labels = []
for i in range(len(data) - seq_length + 1):
sequences.append(data[i:i + seq_length])
labels.append(targets[i + seq_length - 1]) # 窗口最后一步的 RUL
return np.array(sequences), np.array(labels)

seq_length = 30 # 用 30 个连续时间步预测 RUL

X_train_seq, y_train_seq = create_sequences(
X_train.values, y_train.values, seq_length
)
X_val_seq, y_val_seq = create_sequences(
X_val.values, y_val.values, seq_length
)

print(f"序列形状: {X_train_seq.shape}")
print(f"每个样本 = {seq_length} 个时间步 × {X_train_seq.shape[2]} 个特征")

# ---------- 标准化 ----------
n_samples, n_steps, n_features = X_train_seq.shape
train_flat = X_train_seq.reshape(-1, n_features)

seq_scaler = StandardScaler()
train_flat_scaled = seq_scaler.fit_transform(train_flat)
X_train_seq_scaled = train_flat_scaled.reshape(n_samples, n_steps, n_features)

val_flat = X_val_seq.reshape(-1, n_features)
val_flat_scaled = seq_scaler.transform(val_flat)
X_val_seq_scaled = val_flat_scaled.reshape(
X_val_seq.shape[0], n_steps, n_features
)

📌 为什么用 30 步窗口? 太短(如 5 步)模型看不到退化趋势,太长(如 100 步)引入过多噪声且增加计算量。30 步在本数据集上是比较好的平衡点,实际项目中可以通过交叉验证选择。

7.3 定义 LSTM 模型

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
class RUL_LSTM(nn.Module):
"""用于 RUL 预测的 LSTM 模型。"""

def __init__(self, input_size, hidden_size=64, num_layers=2, dropout=0.2):
super().__init__()
# LSTM 层:处理序列数据
self.lstm = nn.LSTM(
input_size=input_size, # 每个时间步的特征数
hidden_size=hidden_size, # LSTM 隐藏状态维度
num_layers=num_layers, # 堆叠 2 层 LSTM
batch_first=True, # 输入格式 (batch, seq, feature)
dropout=dropout # 层间 Dropout 防过拟合
)
# 全连接层:把 LSTM 输出映射到 RUL 预测值
self.fc = nn.Sequential(
nn.Linear(hidden_size, 32),
nn.ReLU(),
nn.Dropout(0.1),
nn.Linear(32, 1)
)

def forward(self, x):
# LSTM 处理序列
lstm_out, _ = self.lstm(x)
# 取最后一个时间步的输出(包含了整个序列的信息)
last_output = lstm_out[:, -1, :]
# 全连接层预测 RUL
prediction = self.fc(last_output)
return prediction.squeeze(-1)

# 检查模型参数量
model = RUL_LSTM(input_size=n_features)
total_params = sum(p.numel() for p in model.parameters())
print(f"LSTM 模型参数量: {total_params:,}")

7.4 训练与评估

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
# 转换为 PyTorch 张量
X_train_tensor = torch.FloatTensor(X_train_seq_scaled)
y_train_tensor = torch.FloatTensor(y_train_seq)
X_val_tensor = torch.FloatTensor(X_val_seq_scaled)
y_val_tensor = torch.FloatTensor(y_val_seq)

# DataLoader(小批量训练)
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)

# 训练配置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = RUL_LSTM(input_size=n_features).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.MSELoss()

# 训练循环
n_epochs = 50
best_val_loss = float('inf')

for epoch in range(n_epochs):
model.train()
train_loss = 0
for X_batch, y_batch in train_loader:
X_batch, y_batch = X_batch.to(device), y_batch.to(device)
optimizer.zero_grad()
pred = model(X_batch)
loss = criterion(pred, y_batch)
loss.backward()
optimizer.step()
train_loss += loss.item()

# 每 10 个 epoch 在验证集上检查
if (epoch + 1) % 10 == 0:
model.eval()
with torch.no_grad():
val_pred = model(X_val_tensor.to(device))
val_loss = criterion(val_pred, y_val_tensor.to(device))
print(f"Epoch {epoch+1}/{n_epochs} | "
f"训练损失: {train_loss/len(train_loader):.4f} | "
f"验证损失: {val_loss.item():.4f}")

# 最终预测
model.eval()
with torch.no_grad():
y_pred_lstm = model(X_val_tensor.to(device)).cpu().numpy()
y_pred_lstm = np.clip(y_pred_lstm, 0, None)

_ = evaluate_rul(y_val_seq, y_pred_lstm, 'LSTM')

📌 LSTM vs XGBoost 的核心区别:XGBoost 需要我们手动构造时序特征(滑动窗口均值、标准差等),LSTM 直接从原始序列中学习时间依赖。LSTM 的优势是自动化程度高,但代价是训练更慢、可解释性更弱。


八、三种方案横向对比

8.1 非对称评分函数的可视化

在对比之前,先解释 Score 指标。它使用非对称惩罚函数:预测偏晚(低估 RUL)比预测偏早(高估 RUL)惩罚更重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 可视化非对称惩罚函数
d = np.linspace(-50, 50, 200)
penalty = np.where(d < 0, np.exp(-d / 13) - 1, np.exp(d / 10) - 1)

plt.figure(figsize=(10, 5))
plt.plot(d, penalty, 'b-', linewidth=2)
plt.axhline(y=0, color='gray', linestyle='--', alpha=0.5)
plt.axvline(x=0, color='gray', linestyle='--', alpha=0.5)

# 标注两侧含义
plt.annotate('预测偏早(保守)\n→ 轻罚', xy=(-30, 5), fontsize=11,
ha='center', color='green')
plt.annotate('预测偏晚(危险)\n→ 重罚', xy=(30, 10), fontsize=11,
ha='center', color='red')

plt.xlabel('预测偏差(预测值 - 真实值)')
plt.ylabel('惩罚值')
plt.title('RUL 预测的非对称评分函数')
plt.grid(True, alpha=0.3)
plt.show()

为什么用非对称惩罚? 在实际工业中,RUL 预测偏早(高估寿命、提前换零件)只是浪费一些零件成本;但预测偏晚(低估寿命、没及时维修)可能导致设备在空中停机——后果严重得多。非对称惩罚让模型倾向于「保守」预测,宁可早换也不冒险。

8.2 三种方法对比表

1
2
3
4
5
6
7
8
9
10
11
12
13
# 汇总三种方法的评估结果(示例数值,实际以运行结果为准)
comparison = pd.DataFrame({
'方法': ['线性回归(基准线)', 'ARIMA', 'XGBoost', 'LSTM'],
'类型': ['统计-线性', '统计-非线性', '机器学习', '深度学习'],
'RMSE': [40.5, 35.2, 28.3, 25.1],
'MAE': [32.1, 28.7, 20.5, 18.2],
'Score': [25000, 18000, 8500, 6200],
'训练时间': ['< 1 秒', '~ 30 秒', '~ 5 秒', '~ 3 分钟'],
'可解释性': ['高', '中', '高', '低'],
})

print("\n三种方案对比:")
print(comparison.to_string(index=False))
方法 类型 RMSE ↓ Score ↓ 训练时间 可解释性
线性回归 统计-线性 ~40 ~25000 < 1 秒
ARIMA 统计-非线性 ~35 ~18000 ~30 秒/台
XGBoost 机器学习 ~28 ~8500 ~5 秒
LSTM 深度学习 ~25 ~6200 ~3 分钟

📌 对比分析

  1. 线性回归最差——因为退化不是线性的,直线拟合不了「后期加速」的模式
  2. ARIMA 比线性好——能捕捉时间依赖,但每台发动机要单独建模,扩展性差
  3. XGBoost 效果不错——172 个时序特征提供了丰富信息,且一个模型搞定所有发动机
  4. LSTM 最好——自动从序列中学习退化模式,不需要手动特征工程

但注意:效果越好,代价越高(训练时间、可解释性、部署复杂度)。

8.3 可视化对比:同一台发动机的预测

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
def compare_predictions_on_engine(engine_id, models_dict, val_data, feature_cols):
"""在同一台发动机上对比不同模型的 RUL 预测。"""
eng_data = val_data[val_data['engine_id'] == engine_id].sort_values('cycle')
actual_rul = eng_data['RUL'].values

fig, ax = plt.subplots(figsize=(12, 5))
ax.plot(range(len(actual_rul)), actual_rul, 'k-', linewidth=2, label='真实 RUL')

colors = ['blue', 'green', 'red', 'orange']
for (name, model_obj), color in zip(models_dict.items(), colors):
if name == 'LSTM':
# LSTM 需要序列窗口
seq_data, _ = create_sequences(
eng_data[feature_cols].values, actual_rul, 30
)
if len(seq_data) == 0:
continue
scaled = seq_scaler.transform(seq_data.reshape(-1, seq_data.shape[2]))
scaled = scaled.reshape(seq_data.shape)
model_obj.eval()
with torch.no_grad():
pred = model_obj(
torch.FloatTensor(scaled).to(device)
).cpu().numpy()
offset = 30 - 1
ax.plot(range(offset, offset + len(pred)), pred, '--',
color=color, label=name, linewidth=1.5)
else:
scaled = scaler.transform(eng_data[feature_cols].values)
pred = model_obj.predict(scaled)
ax.plot(range(len(pred)), pred, '--',
color=color, label=name, linewidth=1.5)

ax.set_xlabel('运行周期(验证集中的序号)')
ax.set_ylabel('RUL(剩余时间步)')
ax.set_title(f'发动机 {engine_id}:三种模型 RUL 预测对比')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# 对比发动机 85 的预测效果
compare_predictions_on_engine(
85,
{'XGBoost': xgb_model, 'LSTM': model},
train_feat,
feature_cols
)

九、预测结果应用:预警阈值设计

9.1 RUL 预测不是终点

模型预测出 RUL 只是第一步。在实际应用中,还需要把预测结果转化为可操作的预警策略——和第 5 篇的分级告警类似,但这次是基于连续的 RUL 数值。

9.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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def rul_early_warning(engine_data, model, feature_cols, scaler,
thresholds={'red': 20, 'orange': 50, 'yellow': 100}):
"""
基于 XGBoost RUL 预测的三级预警系统。

参数:
engine_data: 单台发动机的时序数据
model: 训练好的 XGBoost 模型
feature_cols: 特征列名列表
scaler: 标准化器
thresholds: 三级告警阈值(时间步数)
"""
features = engine_data[feature_cols].values
scaled = scaler.transform(features)
rul_pred = model.predict(scaled)

# 滑动平均平滑预测(消除单步噪声,窗口 = 5)
rul_smooth = pd.Series(rul_pred).rolling(window=5, min_periods=1).mean().values

# 分级告警
alerts = []
for i, rul in enumerate(rul_smooth):
if rul <= thresholds['red']:
alerts.append({
'step': i, 'rul': rul, 'level': 'red',
'message': f'🔴 紧急:预计 {rul:.0f} 步后故障,立即停机检修!'
})
elif rul <= thresholds['orange']:
alerts.append({
'step': i, 'rul': rul, 'level': 'orange',
'message': f'🟠 警告:预计 {rul:.0f} 步后故障,安排维保计划'
})
elif rul <= thresholds['yellow']:
alerts.append({
'step': i, 'rul': rul, 'level': 'yellow',
'message': f'🟡 注意:预计 {rul:.0f} 步后故障,准备备件'
})
else:
alerts.append({
'step': i, 'rul': rul, 'level': 'green',
'message': f'🟢 正常:预计 {rul:.0f} 步后故障'
})

return rul_pred, rul_smooth, pd.DataFrame(alerts)


# 在验证集的一台发动机上演示
eng_val = train_feat[train_feat['engine_id'] == 85].sort_values('cycle')
rul_pred, rul_smooth, alert_df = rul_early_warning(
eng_val, xgb_model, feature_cols, scaler
)

# 可视化预警效果
fig, ax = plt.subplots(figsize=(14, 6))

actual = eng_val['RUL'].values
ax.plot(range(len(actual)), actual, 'k-', linewidth=2, label='真实 RUL')
ax.plot(range(len(rul_pred)), rul_pred, 'b--', alpha=0.5, label='原始预测')
ax.plot(range(len(rul_smooth)), rul_smooth, 'b-', linewidth=2, label='平滑预测')

# 绘制告警区域
ax.axhspan(0, 20, alpha=0.1, color='red', label='🔴 紧急 (≤20)')
ax.axhspan(20, 50, alpha=0.1, color='orange', label='🟠 警告 (≤50)')
ax.axhspan(50, 100, alpha=0.1, color='gold', label='🟡 注意 (≤100)')

ax.set_xlabel('运行周期')
ax.set_ylabel('RUL(剩余时间步)')
ax.set_title('发动机 85:RUL 预测与三级预警')
ax.legend(loc='upper left', fontsize=9)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# 打印告警状态变化
print("预警状态变化:")
level_changes = alert_df[alert_df['level'] != alert_df['level'].shift()]
for _, row in level_changes.iterrows():
print(f" 步骤 {row['step']:>3d} | {row['message']}")

📌 预警策略的关键:不是阈值怎么定,而是「触发告警后怎么办」。三级告警的价值在于给运维人员分级响应的时间窗口:黄色时准备备件、橙色时安排人员、红色时立即行动。没有响应预案的告警只是噪音。


总结与回顾

要点 总结
数据集 NASA C-MAPSS FD001:100 台涡扇发动机退化时序数据,21 个传感器
特征工程 172 个特征 = 原始读数 + 滑动窗口均值/标准差 + 交叉特征
线性回归 RMSE ~40,基准线,假设退化是线性的(不准确但有参考价值)
ARIMA RMSE ~35,能捕捉时间依赖,但需要每台发动机单独建模
XGBoost RMSE ~28,一个模型搞定所有发动机,特征重要性可解释
LSTM RMSE ~25,自动学习时序依赖,但训练慢、可解释性低
核心结论 没有最好的模型,只有最适合场景的模型
预警策略 三级告警(黄/橙/红)+ 平滑预测 + 响应预案

选型建议

场景 推荐方法 原因
快速验证 / 资源有限 XGBoost + 时序特征 效果好、训练快、可解释、部署简单
追求最优精度 LSTM 自动学习时序模式,精度最高
需要统计严谨性 ARIMA 有置信区间,适合报告场景
只需要基准线 线性回归 最简单,用来检验其他模型是否值得用

下篇预告

第 9 篇:模型部署与全链路优化 —— 前四篇(故障预警、良率分析、工艺优化、趋势研判)覆盖了工业 ML 的四大业务场景。但模型做得再好,不能上线也是白搭。下篇我们进入工程落地环节:模型序列化(ONNX)→ API 服务化(FastAPI)→ 容器化部署(Docker)→ 实时监控 → A/B 测试。


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