最近在处理一组设备多传感器数据时,我遇到了一个典型问题:用LSTM做多输出回归,训练集loss一路降到0.05,验证集loss却长期停在0.2附近。一开始我怀疑是模型结构不够深,把LSTM神经元从32加到128,结果验证集loss反而更高。后来我把预测目标单独拆开看,才发现问题不在网络,而在数据本身:这批数据虽然按时间连续采样,但设备实际上会跨越三种完全不同的运行工况——正常负载、满载冲击、停机冷却。三种工况下,输入到输出的映射关系完全不同,混在一起用一个LSTM模型去拟合,模型只能学到一个两头都靠不住的折中结果。
后来我把流程改成“先用K-means对样本做状态聚类,再为每一类分别训练一个LSTM多输出回归模型”,问题才明显改善。这算是我处理时间序列多指标预测时,比较值得记录的一次改造。这篇文章就把这套思路的完整实战过程拆开讲一遍,包括为什么这样做、代码怎么写、参数怎么调,以及哪些地方最容易翻车。
1. 为什么单靠LSTM做多输出回归,常常不够
1.1 多输出回归不是“多一个输出神经元”这么简单
LSTM做多输出回归的典型写法,是在LSTM层之后接一个Dense层,输出维度等于目标数量。这种写法可以跑通,也经常出现在教程里。但真实项目里,多输出回归隐含着一个问题:任务期望一个模型同时学会多个目标变量与输入之间的映射。如果这几个目标变量之间的相对关系在不同状态下会变,一个模型很难同时让它们都准。
举例来说,设备振动和温度,在正常运行下,温度升高,振动可能保持在较低水平;在满载冲击下,温度继续升高,振动会突然变大。同一个输入时序特征,在不同状态下对应的输出关系不同。这个不同无法通过增加LSTM层数或神经元数量解决,因为问题不是模型容量不够,而是训练数据来自多个不同分布。
1.2 单模型隐藏了一个假设:数据属于同一个“运行模式”
如果只训练一个LSTM模型,它在计算损失时会把所有样本的误差平均起来。常见的现象是:
- 某类样本数量多,模型偏向学习那一类;
- 数量少的工况很容易被当成噪声;
- 整体loss看起来可以接受,但分时间段看,某几个时段的预测结果明显偏离。
这在业务上是很大的隐患。工业生产中的异常工况往往就是少数样本,如果模型把它们忽略了,那这套预测就基本失去意义。你真正想捕捉的恰恰是少数状态下的变化规律,而不是把多数状态的平均表现优化得漂亮。
1.3 什么情况下应该考虑“聚类+分组建模”
不是所有多输出回归都需要用K-means。如果数据来源单一,工况稳定,一个LSTM足够。出现下面几个信号,可以试试:
- 把验证集按时间段切块评估,误差忽高忽低;
- 某些输入组合下,预测值长期偏离一个固定偏移;
- 业务上本身就能列举出多种运行模式;
- 样本量足够,按模式拆分后每个子集不会太小。
这些信号说明,问题不是“LSTM不够努力”,而是数据里天然存在多个模式,需要先切分。这也是K-means容易被忽视、但在实践中往往很有效的理由。
2. K-means在这里到底起什么作用
2.1 它不是对“要预测的目标”聚类,而是对“状态特征”聚类
一个常见误区是,看到“聚类+LSTM”,以为是把标签y或者预测目标聚成几类,然后对每一类做分类问题。这里不是。
我们要聚类的是“描述当前运行状态的样本特征”。对时间序列来说,就是每个滑动窗口内提取出来的统计量,比如最近5个时间步里温度的平均值、标准差、最大值、最小值,振动最后时刻的值等。K-means会把这些样本分成K组,每一组对应一种相似的历史状态。然后再看这些分组有没有业务含义,比如是不是分别对应正常、满载、待机。
2.2 K-means和LSTM的三种结合方式
- 方式一:先聚类,再分组建模。对每个簇单独训练LSTM,预测时先用KMeans判断新样本属于哪个簇,再调用对应模型。这是本文采用的方式,逻辑最直接,也最容易排查问题。
- 方式二:把簇编号作为额外输入特征。整体只训练一个LSTM,输入除了原始窗口序列,再加入簇ID的向量。适合不想维护多个模型的情况,但簇的分类信息对LSTM来说只是辅助,模型仍需要在一套参数中同时拟合所有模式。
- 方式三:用聚类结果做预测加权融合。预测时不硬性选择某一个模型,而是根据样本与各簇中心的距离,把多个模型的预测结果做加权。效果可能更平滑,但整体链路复杂,调试成本高。
如果你只想要一个能快速落地的方案,优先尝试方式一。不要一上来就做加权融合。
顺带提一句:搜LSTM相关材料时,常会看到social LSTM。它主要解决多个移动对象之间的交互建模,比如行人轨迹预测,和这里讨论的多输出回归不是同一个问题。如果你要做的是行人相对位置预测,social LSTM才值得关注;如果只是在做设备多指标回归,那不需要引入交互建模。
2.3 为什么选K-means而不是其他聚类
K-means在工业场景里最实用的原因是:可解释、参数少、速度快、结果稳定。它不需要复杂度很高的概率模型,也没有太多超参数需要调。
但它也有短板:
- K值必须提前指定;
- 对噪声和离群点敏感;
- 只能发现凸形簇;
- 对初始化敏感,需要固定随机种子并多次运行。
如果数据状态是连续变化的,比如“负载程度从0到100%平滑变化”,K-means切出来的边界可能是强制的,这时不如先用一个LSTM整体建模,再考虑按负载区间做分段建模。
2.4 这个方案真正改变的是什么
从工作流来看,它把“一个模型负责所有情况”改成“先判断当前处于什么状态,然后让更专注的模型负责预测”。换句话说,K-means不是用来提升LSTM精度的魔法,而是把复杂任务拆解成若干个小任务。拆解之后,每个LSTM模型只需要学一种运行规律,训练和目标都更清晰。
这种工作方式还有一个隐藏好处:当某种新状态出现时,不需要重新训练全套模型。可以先判断新样本是不是明显落在已知簇之外,如果是,再考虑新增一个簇,用新数据单独训练一个模型。从长期维护看,这比反复训练一个大模型更容易控制。
3. 完整实战:K-means聚类+LSTM多输出回归的六步流程
3.1 场景和数据约定
我用一个比较常见的场景来说明:设备每隔一定时间记录一组传感器数据,包括温度、振动、压力三个连续指标。现在要根据最近5个时刻的3个指标,预测下一时刻的3个指标。这是一个典型的多输出回归问题,因为一个样本要输出3个连续值。
下面代码是可以在常见Python 3.9/3.10环境中运行的示例结构。TensorFlow和scikit-learn的版本以你本机实际环境为准,不建议追求最新版本,稳定API即可。
import numpy as np import pandas as pd from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense from tensorflow.keras.callbacks import EarlyStopping3.2 第一步:把原始时间序列构造成滑动窗口样本
假设原始数据是一个二维数组raw_data,每一行是一个时刻,三列分别是三个指标。用窗口长度为input_steps构造样本:
def make_samples(raw_data, input_steps=5): X, y = [], [] for i in range(len(raw_data) - input_steps): X.append(raw_data[i:i + input_steps]) y.append(raw_data[i + input_steps]) return np.array(X), np.array(y)这样,每个样本的输入形状是(input_steps, 3),输出形状是(3,)。y是下一时刻的三个指标。如果你希望预测未来多个时刻,可以把y改成未来若干个时刻的拼接,输出维度会变成(3 * horizon,)。这里先以最简单的“下一步预测”为例。
3.3 第二步:提取聚类特征,做K-means聚类
这里的核心是,聚类特征只能从当前窗口内的历史信息中提取,不能使用未来时刻的数据。一个简单可用的特征是每个指标在窗口内的均值、标准差、最大值减最小值、最后时刻值:
def extract_cluster_features(X): feature_list = [] for window in X: row = [] col_count = window.shape[1] for c in range(col_count): col = window[:, c] row.extend([ col.mean(), col.std(), col.max() - col.min(), col[-1], ]) feature_list.append(row) return np.array(feature_list)然后做标准化和聚类:
cluster_features = extract_cluster_features(X) cluster_scaler = StandardScaler() cluster_features_scaled = cluster_scaler.fit_transform(cluster_features) kmeans = KMeans(n_clusters=3, random_state=42, n_init=10) cluster_labels = kmeans.fit_predict(cluster_features_scaled)这里n_clusters=3对应业务上可能存在的三种工况。先按业务判断给定一个值,再用轮廓系数等指标验证,比纯粹依赖肘部法则更可靠。
注意:K-means的目标是发现不同运行状态,而不是直接优化LSTM的预测误差。聚类特征必须只依赖历史窗口,否则预测阶段根本拿不到同样的信息。
3.4 第三步:为每个簇切分数据并标准化
聚类完成后,cluster_labels给每个样本分配了一个簇ID。接下来把X和y按簇拆分,并对每个簇分别做LSTM输入的标准化。
这里有一个非常容易踩坑的点:如果一开始就对全部数据做标准化,然后用标准化后的数据训练,等于让所有簇共用了同一个输入分布。在预测阶段,新样本进来后,如果它被分到某个簇,理应用该簇自己的输入scaler,而不是全局scaler。更稳妥的做法是,聚类特征和LSTM输入各用一套标准化,并且每个簇维护自己的LSTM输入scaler。
下面是一个完整的按簇切分、标准化、训练模型的过程:
cluster_models = {} cluster_scalers = {} for cluster_id in range(kmeans.n_clusters): idx = np.where(cluster_labels == cluster_id)[0] if len(idx) < 100: print(f"cluster {cluster_id} 样本太少,跳过或使用兜底模型") continue X_cluster = X[idx] y_cluster = y[idx] X_scaler = StandardScaler() original_shape = X_cluster.shape X_cluster_2d = X_cluster.reshape(-1, original_shape[-1]) X_cluster_scaled = X_scaler.fit_transform(X_cluster_2d).reshape(original_shape) cluster_scalers[cluster_id] = X_scaler split_pos = int(len(X_cluster) * 0.8) X_train, X_val = X_cluster_scaled[:split_pos], X_cluster_scaled[split_pos:] y_train, y_val = y_cluster[:split_pos], y_cluster[split_pos:] model = build_lstm(input_steps=5, n_features=3, output_dim=3) model.fit( X_train