K-means聚类+LSTM多输出回归:时间序列多模式预测实践
不少做时间序列预测的读者都有过这种经历单变量预测勉强能跑通一旦要同时预测多个变量比如气温、湿度、风速、PM2.5浓度模型就开始“顾此失彼”。你盯着 loss 曲线发现整体误差不大但某个输出总是发散换一组数据模型表现又完全不一样。更麻烦的是你的数据里往往混合了好几种运行模式有平稳段、有突变段、有周期性很强的段一个 LSTM 硬去拟合所有模式结果往往是“中庸”。这篇文章要聊的是一种很实用的组合方案用 K-means 聚类先识别数据中的模式再结合 LSTM 做多输出回归。核心判断是当你的时间序列存在多模式、多工况、多变化规律时K-means 不是锦上添花而是帮 LSTM 降低拟合压力的关键预处理。读完这篇文章你能跑通一条完整的 K-means 聚类 LSTM 多输出回归实践链路理解它适合什么场景、不适合什么场景以及在实际项目里怎么避免踩坑。1. 这篇文章真正要解决的问题多输出回归在很多真实业务里是常态。你不仅要预测明天的温度还要同时预测湿度、风力和空气质量不仅要预测设备的某个振动值还要预测几个关键轴承的温度。把这些目标拆开分别训练模型当然可以但工程上不够优雅模型数量多、维护成本高、特征之间的关联也被切断了。所以很自然的想法是训练一个模型让它同时输出多个目标值。但这个想法在 LSTM 上会遇到一个现实问题。LSTM 擅长从历史序列中提取时序依赖可如果你的历史数据里存在多种明显不同的“模式”一个 LSTM 会倾向于把所有模式的平均规律学进去。比如同一个设备在白天平稳运行夜间启动时会有短暂冲击同一片区域平时污染物扩散正常遇到静稳天气会出现持续累积。模式差异大时单一模型要拟合的目标分布其实是多峰的而 LSTM 的 hidden state 有限很难自动区分“当前样本属于哪一种运行状态”。这时候 K-means 的价值就体现出来了。它不负责预测而是先把历史样本按照特征相似度分成几个簇让每个簇里的样本更“同质”。分完之后LSTM 面对的就不再是一个混乱的混合分布而是相对清晰的若干个局部模式。你可以把聚类结果当作额外特征喂给 LSTM也可以为每个簇单独训练一个 LSTM 模型。两种方式都能显著缓解模式冲突问题。所以这篇文章适合这些读者正在做多因子时间序列预测、想引入聚类改进 LSTM 效果、对 sklearn 和 TensorFlow/Keras 有基础了解但没系统跑过组合方案的人。2. 基础概念与核心原理2.1 多输出回归到底是什么回归任务一般是指预测一个连续值比如明天的温度。多输出回归就是同时预测多个连续值比如明天的温度、湿度和风速。用神经网络实现时最常见的方式是在最后一层使用多个神经元的 Dense 层输出维度等于目标数量损失函数用 MSE这样模型就学会了同时优化多个目标。多输出回归和单输出回归的本质差别在于模型不仅要学习“输入特征和目标之间的关系”还要隐式学习“多个目标之间的相关性”。比如温度和湿度往往是负相关的如果模型只单独预测温度或湿度就丢失了这种联合分布信息。多输出模型通过共享底层特征理论上能捕获这种关联。但这也带来了训练难度。不同目标的数值尺度可能完全不同温度在 0-40 之间风速可能到几十PM2.5 可能到几百。如果直接丢给模型数值大的目标会主导 loss。所以多输出回归的工程细节里目标归一化和反归一化是必做的一步不是可选项。2.2 K-means 聚类的真正作用K-means 是最经典的无监督聚类算法。它的目标是把样本划分为 K 个簇使得每个样本到所属簇中心的距离之和最小。流程并不复杂先随机选 K 个中心然后反复执行“分配样本到最近中心、重新计算中心”这两个步骤直到中心不再显著变化。但要注意K-means 是一个基于距离的算法对特征尺度非常敏感。如果某个特征数值范围很大它就会主导距离计算导致聚类结果失真。所以用 K-means 之前对参与聚类的特征做标准化处理是基础操作。在很多人的理解里K-means 是“给无标签数据打标签”的工具。在 K-means LSTM 的组合里这种理解仍然成立但更重要的是另一种视角K-means 可以看作一种特征工程手段它把混合分布拆成若干个更简单的子分布降低后续监督模型的学习难度。2.3 LSTM 处理时间序列的基本原理LSTM 是循环神经网络的一种改进结构。它通过输入门、遗忘门、输出门和细胞状态解决了普通 RNN 在长序列训练中的梯度消失问题能够记忆较长时间范围内的信息。在时间序列预测中LSTM 的输入通常是三维张量形状为(样本数, 序列长度, 特征数)输出可以是单值、多值或多步序列。LSTM 并不是银弹。它需要足够的数据量对数据尺度敏感对超参数也比较敏感。很多人调用 Keras 里的 LSTM 层发现效果不如预期往往不是模型本身的问题而是数据没有经过合理的预处理或者序列样本之间存在明显的模式冲突。2.4 为什么 K-means 能与 LSTM 组合从统计角度看K-means 做的是“横向切分”LSTM 做的是“纵向建模”。K-means 根据特征相似度把样本分成几组LSTM 在每组内部学习时间上的依赖关系。两者组合本质上是把一个复杂的全局问题拆成若干个相对简单的局部问题。从工程应用看组合有三种常见方式。第一种是把聚类标签作为特征拼进 LSTM模型可以从标签中知道当前样本属于哪个簇从而自动调整行为。第二种是把样本按簇拆分每个簇单独训练 LSTM预测时先判断样本属于哪个簇再调用对应模型。第三种是把聚类结果作为集成学习的权重依据比如用“样本到各簇中心的距离”作为软权重融合多个 LSTM 的预测结果。这三种方式没有绝对优劣。特征增强方式实现简单、模型少分簇建模方式效果更细但需要维护多个模型且要求每个簇的数据量足够。这篇文章会把前两种都拆开讲因为它们是落地最常用的。3. 适用场景与方案选型3.1 适合 K-means LSTM 的场景并不是所有时间序列预测都需要 K-means。如果数据本身就是单一模式比如一条平稳的正弦波K-means 反而会增加复杂度。但在以下场景里K-means LSTM 的收益明显。第一个场景是数据存在明显的工况切换。设备运行状态有启动、正常运行、停机、故障等阶段不同阶段的传感器数据分布差异很大。第二个场景是业务存在周期性活动带来的模式变化。比如电商平台的流量有普通日和促销日普通日规律稳定促销日整体脉冲式上涨。第三个场景是多个目标变量之间存在非平稳的相关结构。比如风速会影响污染物扩散但风向一变影响方式就变了。这些场景里一个 LSTM 很难覆盖所有模式K-means 的“分而治之”思路刚好对症。有一个判断标准可以帮你快速决策先拿原始数据直接训练一个 LSTM观察验证集误差是否在某些时间段持续偏高。如果误差大的时段刚好对应某种业务状态或数据形态那就值得试一下 K-means 聚类。3.2 两种组合方式的取舍对比维度聚类特征增强分簇独立建模模型数量1 个K 个实现复杂度较低较高训练数据利用率高所有样本参与训练每个簇只用局部样本模式区分能力中等靠特征引导较强模型完全独立预测时逻辑计算聚类特征输入模型先判断簇再选模型或做集成风险点聚类特征可能被 LSTM 弱化小簇数据不足模型容易过拟合从实际项目角度看我一般建议先试特征增强。它改动小风险低改成模型结构后可以快速验证“聚类信息是否真的对预测有帮助”。如果特征增强之后提升有限再考虑分簇建模。4. 环境准备与数据说明4.1 环境依赖本文的代码基于 Python 实现核心依赖包括pip install numpy pandas scikit-learn tensorflow matplotlib版本方面以你当前的 Python 环境为准不建议追求最新版本。比较稳妥的组合是Python 3.8 及以上scikit-learn 1.xTensorFlow 2.x。如果你用的是 TensorFlow 2.6 以上的版本下面的 Keras API 写法基本通用。由于 LSTM 训练依赖随机数建议在代码开头固定随机种子方便复现import numpy as np import tensorflow as tf import random random.seed(42) np.random.seed(42) tf.random.set_seed(42)4.2 数据格式说明为了演示通用思路这里采用模拟数据两个目标变量它们共享同一个时间轴但存在两种不同的变化模式。前一段是两个信号的平滑周期变化后一段幅度增强并叠加了额外波动。这种设计能模拟真实场景中的“工况切换”。如果使用你自己的数据需要保证格式是二维 DataFrame 或二维 ndarray形状为(样本数, 特征数)。其中特征相当于时间序列的多个维度也包含你要预测的目标变量。后面的处理会把“所有特征一起作为输入下一时刻的所有特征作为输出”。4.3 生成或准备数据import numpy as np import pandas as pd np.random.seed(42) t np.arange(0, 3000) mode np.where(t 1500, 0, 1) # 两个目标变量 y1 np.sin(t * 0.02) np.where(mode 1, 0.3 * np.sin(t * 0.05), 0) y2 0.5 * np.cos(t * 0.02) np.where(mode 1, 0.2 * np.random.normal(0, 1, len(t)), 0) # 额外增加两个输入特征模拟多因子环境 f1 np.sin(t * 0.01) 0.1 * np.random.normal(0, 1, len(t)) f2 np.cos(t * 0.03) 0.1 * np.random.normal(0, 1, len(t)) data pd.DataFrame({ input1: f1, input2: f2, target1: y1, target2: y2 }) print(data.head()) print(data.describe())这里的思路是两个目标变量之间的相关结构在前后两段并不完全相同单一 LSTM 容易学出一个折中效果。聚类的作用就是让模型意识到“当前样本处于模式 0 还是模式 1”。5. 核心流程拆解5.1 滑动窗口构造时间序列预测第一步是把原始序列转成监督学习样本。通常做法是定义seq_len用过去seq_len个时间步的特征预测下一个时间步的目标变量。def create_dataset(data, seq_len20): X, y [], [] data_values data.values for i in range(len(data_values) - seq_len): X.append(data_values[i:i seq_len, :]) y.append(data_values[i seq_len, -2:]) # 预测最后两列target1, target2 return np.array(X), np.array(y)这里有一个容易出错的地方如果原始序列存在时间连续性滑动窗口构造的样本之间不是独立的直接随机划分训练集和测试集会引入数据泄漏。更合理的做法是按时序切分比如前 80% 作为训练集后 20% 作为测试集。5.2 数据归一化LSTM 对输入尺度敏感归一化是必须的。这里我建议把归一化器拆成“输入特征”和“目标变量”两组因为后续反归一化时需要把预测值还原到原始尺度。from sklearn.preprocessing import MinMaxScaler scaler_x MinMaxScaler() scaler_y MinMaxScaler() data_x data[[input1, input2, target1, target2]] data_y data[[target1, target2]] scaled_x scaler_x.fit_transform(data_x) scaled_y scaler_y.fit_transform(data_y) scaled_data np.hstack([scaled_x, scaled_y])注意MinMaxScaler要只用训练集拟合再用训练集拟合好的 scaler 转换测试集。如果在全量数据上 fit测试集信息会提前进入模型严重影响评估可信度。5.3 K-means 聚类的样本特征设计K-means 聚类的是“样本”不是时间步。一个滑动窗口就是一条样本。如果把整个窗口拉平会得到一个seq_len * n_features维度的高维向量聚类效果未必好。更实用的做法是提取窗口级别的统计特征比如每个特征的均值、标准差、最小值、最大值、最后值。这些统计量能很好地刻画“这段序列当前的状态形态”。from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler def extract_window_features(X): samples [] for i in range(X.shape[0]): window X[i] # shape: (seq_len, n_features) feat [] for j in range(window.shape[1]): feat.append(np.mean(window[:, j])) feat.append(np.std(window[:, j])) feat.append(window[-1, j]) samples.append(feat) return np.array(samples) window_feat extract_window_features(X) cluster_scaler StandardScaler() window_feat_scaled cluster_scaler.fit_transform(window_feat) kmeans KMeans(n_clusters2, random_state42, n_init10) cluster_labels kmeans.fit_predict(window_feat_scaled) print(pd.Series(cluster_labels).value_counts())K 值的选择可以先用肘部法则或轮廓系数辅助判断。示例里直接用 2因为模拟数据就是两种模式。真实项目里不要拍脑袋建议画一下聚类成本随 K 变化的曲线。5.4 方案一聚类特征增强 LSTM这个方案的做法是LSTM 的输入仍然是窗口序列但额外把聚类标签作为另一个输入分支让模型知道当前样本属于哪个簇。如果担心 one-hot 稀疏也可以直接把标签当作数值特征或把样本到各簇中心的距离作为特征。from tensorflow.keras.models import Model from tensorflow.keras.layers import Input, LSTM, Dense, Concatenate, Dropout from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping from tensorflow.keras.utils import to_categorical cluster_onehot to_categorical(cluster_labels, num_classes2) def build_feature_enhanced_model(seq_len, n_features, n_outputs, cluster_dim): seq_input Input(shape(seq_len, n_features), nameseq_input) cluster_input Input(shape(cluster_dim,), namecluster_input) x LSTM(64, return_sequencesFalse)(seq_input) x Dropout(0.2)(x) x Dense(32, activationrelu)(x) concat Concatenate()([x, cluster_input]) output Dense(n_outputs, activationlinear, namemulti_output)(concat) model Model(inputs[seq_input, cluster_input], outputsoutput) model.compile(optimizerAdam(learning_rate0.001), lossmse, metrics[mae]) return model这段代码的逻辑是LSTM 负责从时序窗口里提取特征聚类 one-hot 向量提供全局模式信息。两路特征拼接后进入输出层一次预测多输出。如果聚类结果准确模型就相当于有条件地根据模式调整自己的输出相当于在共享 LSTM 的基础上做了隐式分支。5.5 方案二分簇独立 LSTM 模型方案二更直接根据聚类标签把样本拆成 K 份每一份训练一个独立的 LSTM 多输出模型。预测时新样本先生成窗口统计特征然后用同一个 KMeans 模型计算它属于哪个簇再调用对应模型预测。def build_single_lstm_model(seq_len, n_features, n_outputs): model tf.keras.Sequential([ tf.keras.layers.LSTM(64, input_shape(seq_len, n_features)), tf.keras.layers.Dropout(0.2), tf.keras.layers.Dense(32, activationrelu), tf.keras.layers.Dense(n_outputs, activationlinear) ]) model.compile(optimizerAdam(learning_rate0.001), lossmse, metrics[mae]) return model然后做如下循环cluster_models {} for k in range(2): idx np.where(cluster_labels_train k)[0] X_k X_train[idx] y_k y_train[idx] if len(X_k) 10: continue model_k build_single_lstm_model(seq_len, X_train.shape[2], y_train.shape[1]) model_k.fit(X_k, y_k, epochs20, batch_size32, validation_split0.2, verbose0) cluster_models[k] model_k这个方案的好处是每个模型只需要学习某一类模式拟合压力小。问题是每个簇的样本数可能不均衡小簇模型容易过拟合。而且 K 个模型一起训练计算成本高。建议只在方案一效果不够时使用。5.6 模型评估与反归一化无论哪种方案最终都要把预测结果反归一化回原始尺度再计算 RMSE、MAE、MAPE 等指标。这里多输出回归的评估要对每个输出分别计算指标同时还要关注整体平均。不要只看一个目标因为多输出问题里经常出现“某些目标表现好另一些目标被牺牲”的情况。6. 完整示例代码实现为了便于直接复制运行这里把方案一的完整流程合并成一个整体。方案二的核心代码在 5.5 节已经给出实际操作时替换训练循环即可。6.1 导入依赖import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.preprocessing import MinMaxScaler, StandardScaler from sklearn.model_selection import train_test_split from tensorflow.keras.models import Model from tensorflow.keras.layers import Input, LSTM, Dense, Concatenate, Dropout from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping from tensorflow.keras.utils import to_categorical6.2 数据生成np.random.seed(42) t np.arange(0, 3000) mode np.where(t 1500, 0, 1) y1 np.sin(t * 0.02) np.where(mode 1, 0.3 * np.sin(t * 0.05), 0) y2 0.5 * np.cos(t * 0.02) np.where(mode 1, 0.2 * np.random.normal(0, 1, len(t)), 0) f1 np.sin(t * 0.01) 0.1 * np.random.normal(0, 1, len(t)) f2 np.cos(t * 0.03) 0.1 * np.random.normal(0, 1, len(t)) data pd.DataFrame({ input1: f1, input2: f2, target1: y1, target2: y2 })6.3 构造时序样本def create_dataset(data_values, seq_len20): X, y [], [] for i in range(len(data_values) - seq_len): X.append(data_values[i:i seq_len, :]) y.append(data_values[i seq_len, -2:]) return np.array(X), np.array(y) seq_len 20 data_values data.values X, y create_dataset(data_values, seq_len) # 按时序切分 split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:]这里强烈建议不要用train_test_split(random_state...)随机切分。时间序列的滑动窗口样本之间存在前后依赖随机切分会让模型在训练时看到测试附近的信息验证指标虚高。6.4 数据归一化n_features X_train.shape[2] scaler_x MinMaxScaler() scaler_y MinMaxScaler() # 把三维窗口展平再用一个 scaler 处理所有特征维度 X_flat_train X_train.reshape(-1, n_features) X_flat_test X_test.reshape(-1, n_features) scaler_x.fit(X_flat_train) X_train_scaled scaler_x.transform(X_flat_train).reshape(X_train.shape) X_test_scaled scaler_x.transform(X_flat_test).reshape(X_test.shape) scaler_y.fit(y_train) y_train_scaled scaler_y.transform(y_train) y_test_scaled scaler_y.transform(y_test)这段代码需要理解一个细节我是在整个训练集窗口上训练的scaler_x然后再转换训练集和测试集。这符合“只用训练集拟合 scaler”的原则。6.5 聚类并生成聚类特征def extract_window_features(windows): samples [] for i in range(windows.shape[0]): w windows[i] feat [] for j in range(w.shape[1]): feat.append(np.mean(w[:, j])) feat.append(np.std(w[:, j])) feat.append(w[-1, j]) samples.append(feat) return np.array(samples) feat_train extract_window_features(X_train_scaled) feat_test extract_window_features(X_test_scaled) cluster_scaler StandardScaler() feat_train_scaled cluster_scaler.fit_transform(feat_train) feat_test_scaled cluster_scaler.transform(feat_test) k 2 kmeans KMeans(n_clustersk, random_state42, n_init10) cluster_labels_train kmeans.fit_predict(feat_train_scaled) cluster_labels_test kmeans.predict(feat_test_scaled) cluster_onehot_train to_categorical(cluster_labels_train, num_classesk) cluster_onehot_test to_categorical(cluster_labels_test, num_classesk)这段代码里StandardScaler和KMeans都只在训练集上 fit。预测时测试集用同一个聚类模型打标签这是很容易踩坑的地方。如果测试集单独重新做聚类簇的顺序和中心都会变预测阶段根本没法用。6.6 构建和训练模型def build_feature_enhanced_model(seq_len, n_features, n_outputs, cluster_dim): seq_input Input(shape(seq_len, n_features), nameseq_input) cluster_input Input(shape(cluster_dim,), namecluster_input) x LSTM(64, return_sequencesFalse)(seq_input) x Dropout(0.2)(x) x Dense(32, activationrelu)(x) concat Concatenate()([x, cluster_input]) output Dense(n_outputs, activationlinear, namemulti_output)(concat) model Model(inputs[seq_input, cluster_input], outputsoutput) model.compile(optimizerAdam(learning_rate0.001), lossmse, metrics[mae]) return model model build_feature_enhanced_model(seq_len, n_features, y_train.shape[1], k) early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) history model.fit( [X_train_scaled, cluster_onehot_train], y_train_scaled, epochs50, batch_size64, validation_split0.2, callbacks[early_stop], verbose1 )这里有一个建模上的选择validation_split0.2是在训练集末尾切出部分样本做验证因为 Keras 的 validation_split 默认取最后 20%对时间序列来说是可接受的。如果你希望验证集也保证时序顺序也可以手动再切一段。6.7 模型预测和反归一化y_pred_scaled model.predict([X_test_scaled, cluster_onehot_test]) y_pred scaler_y.inverse_transform(y_pred_scaled) y_test_original scaler_y.inverse_transform(y_test_scaled) # 评估指标 from sklearn.metrics import mean_absolute_error, mean_squared_error for i, name in enumerate([target1, target2]): mae mean_absolute_error(y_test_original[:, i], y_pred[:, i]) rmse np.sqrt(mean_squared_error(y_test_original[:, i], y_pred[:, i])) print(f{name}: MAE{mae:.4f}, RMSE{rmse:.4f})反归一化是整个流程里最容易出错的一环。很多读者训练时只对输入做了归一化忘记对输出做归一化结果 loss 巨大或者训练时归一化了预测时却忘了还原导致指标完全不可读。建议把这一段单独封装成函数后续实验直接复用。7. 运行结果与效果验证上面代码运行后终端会输出类似下面的信息Epoch 1/50 ... loss: 0.0231 - mae: 0.1112 - val_loss: 0.0218 - val_mae: 0.1024 Epoch 2/50 ... target1: MAE0.0312, RMSE0.0401 target2: MAE0.0215, RMSE0.0276注意这里的具体数值会受随机种子、TensorFlow 版本、CPU/GPU 影响不必强求一致。重点是要观察两个问题第一个问题是val_loss是否在模型训练后期稳定下降而不是剧烈震荡。如果震荡明显说明学习率可能偏大或者聚类特征和序列特征之间的拼接不够平滑。第二个问题是两个目标的误差是否悬殊。如果 target1 误差明显大于 target2说明这个输出对应的数据变化更复杂可能需要单独调整该输出的权重。更完整的验证方式是画一张预测对比图。把测试集里两个目标的真实值和预测值分别画出来能直观看到模型在模式切换点上的表现。比如在 t1500 附近如果出现明显的偏差很可能就是聚类标签没有正确识别出新模式或者簇边界附近的样本被分到了错误的一侧。plt.figure(figsize(12, 4)) plt.plot(y_test_original[:, 0], labeltrue target1) plt.plot(y_pred[:, 0], labelpred target1) plt.legend() plt.show()如果聚类效果好预测曲线应该能跟随真实曲线的形态变化而不是把所有模式“平均”成一条平滑曲线。8. 常见问题与排查思路问题现象可能原因排查方式解决方案聚类标签大部分集中在某个簇特征区分度不够或 K 值过大打印聚类样本数、可视化降维后的分布增加窗口统计特征或者重新选择 KLSTM loss 不下降输入输出没有归一化或学习率过大检查数据范围打印 loss 曲线用 MinMaxScaler 归一化降低学习率测试集指标远差于验证集时序数据被随机划分或者归一化拟合到了全量数据检查 train_test_split 方式确认 scaler 只在训练集 fit改为按时间切分重新 fit scaler多输出里某个目标误差特别大目标量纲不一致检查每个目标的 RMSE对每个目标单独归一化或调整输出层初始化聚类增强后效果没有提升聚类特征被 LSTM 特征“淹没”单独打印 LSTM 输出和聚类特征的变化范围增加聚类特征权重或改用分簇建模分簇建模时某个簇样本太少K 值过大或真实模式本身不均衡输出每个簇的样本量减少 K或者对小簇做数据增强这里面最隐蔽的问题是归一化泄漏。很多人习惯先对整个数据集做fit_transform再切训练测试集。这在普通机器学习里可能影响不大但在时间序列里你的聚类模型和归一化模型都提前看到了未来信息验证结果会偏乐观。上线后效果大概率打折扣。另一个容易忽略的问题是KMeans.predict在预测阶段只计算样本到已有簇中心的距离它不会重新调整中心。所以你保存模型时一定要把kmeans和几个 scaler 一起保存预测时按同样的顺序调用。9. 最佳实践与工程建议9.1 选择合理的 K 值K 值不是越大越好。K 越大每个簇内样本越少LSTM 越容易过拟合。推荐先用肘部法则观察聚类误差下降的拐点再结合业务判断。比如业务系统明确有日间、夜间、高峰三种模式K3 就比单纯看图表更可靠。9.2 注意聚类与归一化的顺序正确顺序是先切分训练集和测试集再在训练集上拟合归一化器然后用训练集拟合 KMeans最后预测测试集标签。整个过程里测试集只参与 transform 和 predict不参与 fit。这个原则在任何预处理环节都成立。9.3 保存完整 pipeline上线时需要保存的东西不只是 LSTM 模型权重还包括scaler_x、scaler_y、cluster_scaler、kmeans。建议用 joblib 保存 sklearn 对象用model.save()保存 Keras 模型。推理时按顺序加载、按顺序转换不要跳步。import joblib joblib.dump(scaler_x, scaler_x.pkl) joblib.dump(scaler_y, scaler_y.pkl) joblib.dump(cluster_scaler, cluster_scaler.pkl) joblib.dump(kmeans, kmeans.pkl) model.save(lstm_model.h5)9.4 先跑基线模型不要一上来就上 K-means LSTM。先用一个单纯的多输出 LSTM或者甚至用线性回归跑一遍得到基线指标。只有证明聚类确实带来提升才值得为维护成本和推理延迟买单。否则这个组合就是过度设计。9.5 关注簇边界样本K-means 的硬划分在处理簇边界样本时很生硬。一个样本可能既像簇 0 又像簇 1但被强行分到某一个簇。如果你用分簇建模这类样本的预测误差往往会比较大。改进思路有两个一是用“样本到各簇中心的距离”做软标签而不是 one-hot二是对边界样本做轻量集成比如把预测值按距离倒数加权平均。9.6 多输出回归的评估分开做多输出回归不能只看一个综合指标。实际项目里不同的目标具有不同的业务权重。比如空气质量预测里PM2.5 的重要性往往高于湿度。建议为每个目标单独计算 MAE/RMSE并在报告里列出对比表。这样既方便模型调优也方便向业务方解释模型行为。10. 总结与后续学习方向K-means LSTM 的组合并不神秘它本质上是在多模式时间序列里做“先分类后预测”。K-means 负责识别模式LSTM 负责在模式内建模时序依赖多输出回归负责同时预测多个目标。对于工况切换明显、目标变量多且相关结构不稳定的场景这个方案比单纯堆一个复杂 LSTM 更实用也更可控。建议你按文中的方案一先跑通全流程然后结合自己的业务数据尝试调整窗口长度、聚类特征、K 值和 LSTM 结构。等你对性能瓶颈有了手感再考虑把方案一替换成分簇建模或者引入软标签集成、注意力机制、多任务学习。每一步改动都用同一套验证流程来对比不要凭感觉。这篇文章适合收藏备用。遇到多模式时间序列预测或者多输出回归效果不理想时回来对照检查数据划分、归一化、聚类特征和模型结构往往能快速定位问题。