K-means+LSTM多输出回归:时间序列预测实战方案 之前在做时序预测项目时经常会遇到一个尴尬的情况整体数据看着有规律但一个模型学完所有样本之后预测精度总是提不上去。后来仔细分析发现数据里其实隐藏着几种不同变化模式被混合在一起模型很难用一个隐状态覆盖所有规律。于是尝试了“K-means 聚类 LSTM”的组合思路先用 K-means 把不同模式的数据分到不同簇再对每个簇分别训练 LSTM 模型最终在多个预测目标上同时输出效果提升非常明显。这篇文章会完整拆解这套实战方案内容包括 K-means 聚类原理、LSTM 多输出回归的思路、完整 Python 代码、可视化分析以及常见坑点。适合有一定 Python 基础、想把聚类和时间序列预测结合起来的读者如果你刚接触 LSTM也能通过代码逐步跑通整个流程。1. 背景与核心概念1.1 为什么把 K-means 和 LSTM 组合起来K-means 是一种无监督学习算法它的目标是把样本划分成 K 个簇使得同一个簇内的样本尽可能相似不同簇之间的样本差异尽量大。它不关心样本的标签是什么只根据特征距离来分组。LSTMLong Short-Term Memory是循环神经网络的一种改进结构专门用来处理序列数据。它通过“门”结构控制信息的遗忘、记忆和输出能够缓解长序列训练中的梯度消失问题适合时间序列预测、文本建模、语音识别等场景。单看两者一个做聚类一个做预测好像没有直接关系。但业务场景中很多序列数据并不是单一模式。比如同一台设备的传感器数据在正常运行、低速运行、故障前期三种状态下曲线形态完全不同。如果只用一条 LSTM 去学习所有状态模型会变得比较“平庸”。更合理的做法是先用 K-means 把历史样本按形态聚类每个簇内部的数据模式更一致再为每个簇训练一个 LSTM 多输出回归模型预测新样本时先判断它属于哪个簇再使用对应模型预测。这样相当于把一个复杂问题拆成了多个相对简单的问题每个子问题都由专门的模型负责。1.2 LSTM 模型能做什么LSTM 模型擅长捕捉序列中的时间依赖关系。它输入的不是单独一个向量而是一段连续时间窗口的数据。比如我们采集了 72 个小时的负荷数据每 1 小时一个点那就可以用前 48 个小时的数据去预测后 24 个小时的数据。这种任务可以看成“多步时间序列预测”如果每个时刻需要预测多个变量例如温度、湿度、压力就变成了“多输出回归”。LSTM 内部通常包含遗忘门决定哪些历史信息需要丢弃输入门决定哪些新信息需要写入记忆输出门决定当前隐状态输出什么。在实际编码中我们通常直接使用 TensorFlow/Keras 提供的LSTM层不需要手动实现门控逻辑。1.3 多输出回归的含义普通回归问题一般输出一个连续值比如预测明天的销售额。多输出回归则是同时预测多个连续值比如同时预测明天的销售额、订单量和退换货数量。在 Keras 中实现多输出回归非常简单只需让最后一层 Dense 层的神经元数量等于输出维度即可。例如model.add(Dense(3))这里的3就表示每个样本输出 3 个数值。2. 环境准备与版本说明本文示例代码在 Windows 11 环境下测试通过Python 使用 3.9 版本。建议使用虚拟环境管理依赖避免不同项目之间的包版本冲突。需要安装的核心库如下库名用途numpy数值计算与模拟数据生成pandas数据处理与 DataFrame 操作matplotlib数据可视化scikit-learnK-means 聚类、数据标准化、评估指标tensorflow构建 LSTM 模型安装命令pip install numpy pandas matplotlib scikit-learn tensorflow版本方面TensorFlow 2.x 都可以运行本文代码。需要注意的是不同版本之间的 API 基本一致但如果你使用的是 TensorFlow 2.10 以下版本keras已经内嵌在tensorflow中如果使用 TensorFlow 2.16keras变成了独立包建议统一安装keras并保持版本匹配。示例项目结构如下kmeans_lstm_demo/ ├── main.py ├── data_generator.py ├── model.py └── requirements.txt为了方便阅读本文会把核心代码集中展示在几个代码块中你可以按文件拆分保存。3. 核心原理拆解3.1 K-means 聚类的思路K-means 的算法流程可以用四个字概括迭代收敛。随机选择 K 个样本作为初始簇中心计算每个样本到 K 个中心的距离把它归到最近的中心重新计算每个簇内所有样本的均值作为新的中心重复步骤 2 和 3直到簇中心不再发生明显变化。在 scikit-learn 中调用方式如下from sklearn.cluster import KMeans kmeans KMeans(n_clusters3, random_state42, n_init10) labels kmeans.fit_predict(X)其中n_init表示 K-means 会运行多次选择惯性最小的结果避免陷入局部最优。random_state用来固定随机种子保证结果可以复现。3.2 时序数据如何聚类时序数据不能直接丢给 K-means因为原始序列长度可能不一致。常见做法有两种方式一基于统计特征聚类对每段序列提取均值、方差、斜率、极值、周期长度等统计特征形成特征向量再对特征向量做 K-means。方式二基于窗口序列聚类把每个滑动窗口内的数据展开成一条平坦向量直接作为聚类特征。这种方式保留了原始形态但要求所有窗口长度一致。本文采用方式二因为它的实现最简单并且与 LSTM 的滑动窗口输入自然对齐。每个样本就是一个长度为lookback的窗口窗口中包含多个特征维度展开后变成 K-means 的输入。3.3 LSTM 多输出模型结构一个标准的多输出 LSTM 回归模型结构如下输入层形状为 (lookback, n_features) ↓ LSTM 层返回隐藏状态 ↓ Dropout 层防止过拟合 ↓ 全连接层Dense(64, activationrelu) ↓ 输出层Dense(n_targets)输入形状里的lookback表示用过去多少个时间步n_features表示每个时间步有多少个特征。输出层的n_targets表示一次预测多少个目标变量。3.4 为什么要先聚类再预测直接用一个 LSTM 模型预测所有数据相当于让模型同时拟合多种变化规律。如果不同模式的样本在特征空间距离较远模型会出现“顾此失彼”的问题。聚类之后每个簇内的数据分布更加集中LSTM 更容易学到簇内共同的时间依赖关系。这种方式在工业设备预测性维护、能源负荷预测、金融时序分析中都有应用价值。当然聚类也不是万能的。它适合数据天然存在多种模式的情况如果数据本来就是单一平稳序列聚类反而会切割上下文导致样本量减少、模型训练不充分。所以使用前要对数据分布有基本判断。4. 完整实战案例下面我们用一个模拟数据来跑通全流程。假设现在有两台设备每台设备有 3 个传感器特征设备的运行模式分为“正常”“剧烈波动”和“缓慢漂移”三种。我们的目标是根据历史 48 个时间步的 3 个特征聚类出不同运行模式使用 LSTM 模型同时预测未来 3 个特征的值。4.1 模拟数据生成为了模拟三种不同模式我们生成三段不同频率和振幅的周期序列并叠加随机噪声。import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error, mean_squared_error from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam # 设置随机种子保证结果可复现 np.random.seed(42) # 生成三种模式的时间序列 def generate_series(mode, length1000): t np.arange(length) if mode normal: # 低频小幅波动 base np.sin(t / 50) * 0.5 elif mode volatile: # 高频大幅波动 base np.sin(t / 5) * 2.0 np.cos(t / 3) * 1.5 elif mode drift: # 缓慢上升趋势 base 0.02 * t np.sin(t / 40) * 0.3 else: raise ValueError(Unknown mode) # 三个特征分别加上不同权重和噪声 data np.zeros((length, 3)) data[:, 0] base np.random.normal(0, 0.1, length) data[:, 1] base * 1.2 np.random.normal(0, 0.15, length) data[:, 2] base * 0.8 np.random.normal(0, 0.08, length) return data # 三段各 1000 个时间步 normal_data generate_series(normal) volatile_data generate_series(volatile) drift_data generate_series(drift) # 拼接在一起并记录真实标签用于后续分析不用于聚类训练 all_data np.vstack([normal_data, volatile_data, drift_data]) true_labels np.array([0] * 1000 [1] * 1000 [2] * 1000) print(all_data.shape) # (3000, 3)这里我们把三种模式的数据顺序拼接实际业务中数据是连续采集的不会提前知道边界。后面聚类时会暴露这个信息但我们不会使用true_labels参与 K-means。4.2 特征工程与 K-means 聚类先用滑动窗口把数据切分成样本。假设用lookback 48则每个样本包含 48 个时间步 × 3 个特征展开后得到 144 维特征向量。def create_windows(data, lookback48): windows [] for i in range(len(data) - lookback): win data[i:i lookback] windows.append(win) return np.array(windows) lookback 48 all_windows create_windows(all_data, lookback) print(all_windows.shape) # (2952, 48, 3) # 聚类输入把窗口展平成向量 X_cluster all_windows.reshape(all_windows.shape[0], -1) print(X_cluster.shape) # (2952, 144) # 标准化避免不同特征量纲影响距离计算 scaler StandardScaler() X_cluster_scaled scaler.fit_transform(X_cluster)接下来用肘部法则确定 K。我们计算不同 K 值下的簇内误差平方和inertia。inertia [] for k in range(2, 7): kmeans_tmp KMeans(n_clustersk, random_state42, n_init10) kmeans_tmp.fit(X_cluster_scaled) inertia.append(kmeans_tmp.inertia_) plt.figure(figsize(8, 4)) plt.plot(range(2, 7), inertia, markero) plt.xlabel(K) plt.ylabel(Inertia) plt.title(Elbow Method for Optimal K) plt.grid(True) plt.show()从图中通常能看到弯曲最明显的位置这里我们已知生成数据有三种模式因此直接选择 K3。实际项目中需要结合业务可解释性和惯性曲线综合判断。kmeans KMeans(n_clusters3, random_state42, n_init10) cluster_labels kmeans.fit_predict(X_cluster_scaled)4.3 聚类效果可视化为了快速验证聚类是否合理可以把每个窗口的聚类结果映射回时间轴并对比真实模式标签。# 每个窗口的起始时间索引从 0 到 2951 start_indices np.arange(len(all_windows)) plt.figure(figsize(12, 4)) plt.scatter(start_indices, cluster_labels, ccluster_labels, cmapviridis, s10) plt.xlabel(Window Start Index) plt.ylabel(Cluster Label) plt.title(K-means Cluster Assignment on Time Windows) plt.colorbar() plt.show()如果聚类有效你会看到前 952 个窗口对应 normal 段中间 952 个窗口volatile 段最后 952 个窗口drift 段分别被分到不同簇边界位置因为窗口跨越了模式切换点会有少量过渡样本。4.4 构造 LSTM 训练数据接下来为每个簇单独准备训练数据。每个样本的输入是连续 48 个时间步的 3 个特征输出是下一个时间步的 3 个特征。def create_xy(windows): X windows[:, :-1, :] # 前 47 步 y windows[:, -1, :] # 最后 1 步的 3 个特征 return X, y # 按聚类标签拆分数据 cluster_to_data {} for label in np.unique(cluster_labels): mask cluster_labels label cluster_windows all_windows[mask] X, y create_xy(cluster_windows) cluster_to_data[label] (X, y) for label, (X, y) in cluster_to_data.items(): print(fCluster {label}: X shape {X.shape}, y shape {y.shape})这里 y 的形状是(样本数, 3)代表每个样本同时预测 3 个特征这就是多输出回归的目标。4.5 构建多输出 LSTM 模型定义一个通用函数输入特征数量和输出目标数量返回一个 LSTM 回归模型。def build_lstm_model(input_shape, n_targets): model Sequential() model.add(LSTM(64, input_shapeinput_shape, return_sequencesTrue)) model.add(Dropout(0.2)) model.add(LSTM(32)) model.add(Dropout(0.2)) model.add(Dense(16, activationrelu)) model.add(Dense(n_targets)) # 多输出 model.compile(optimizerAdam(learning_rate0.001), lossmse, metrics[mae]) return model注意最后一层 Dense 没有激活函数或者使用线性激活。多输出回归任务中输出层一般使用线性激活因为我们要预测连续数值不需要限制在 0 到 1 之间。输入形状为(lookback - 1, n_features)这里因为窗口长度是 48输入用前 47 步所以input_shape(47, 3)。4.6 训练与结果评估为每个聚类簇构建并训练一个独立的 LSTM 模型。这里为了演示先对簇 0 单独训练。X0, y0 cluster_to_data[0] # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split(X0, y0, test_size0.2, random_state42, shuffleFalse) # 标准化注意按特征维度标准化 scaler_x StandardScaler() scaler_y StandardScaler() # 输入数据形状 (样本数, 时间步, 特征数)需要将三维数据reshape后标准化 orig_shape X_train.shape X_train_reshaped X_train.reshape(-1, orig_shape[-1]) X_train_scaled scaler_x.fit_transform(X_train_reshaped).reshape(orig_shape) X_test_reshaped X_test.reshape(-1, orig_shape[-1]) X_test_scaled scaler_x.transform(X_test_reshaped).reshape(X_test.shape) y_train_scaled scaler_y.fit_transform(y_train) y_test_scaled scaler_y.transform(y_test) model build_lstm_model((orig_shape[1], orig_shape[2]), n_targets3) model.summary() history model.fit( X_train_scaled, y_train_scaled, validation_data(X_test_scaled, y_test_scaled), epochs30, batch_size32, verbose1 )训练完成后用测试集进行预测并反标准化。y_pred_scaled model.predict(X_test_scaled) y_pred scaler_y.inverse_transform(y_pred_scaled) y_test_inv scaler_y.inverse_transform(y_test_scaled) # 计算评估指标 mae mean_absolute_error(y_test_inv, y_pred) mse mean_squared_error(y_test_inv, y_pred) print(fCluster 0 - MAE: {mae:.4f}, MSE: {mse:.4f}) # 可视化第一个样本的预测 vs 真实值 plt.figure(figsize(12, 4)) for i in range(3): plt.subplot(1, 3, i 1) plt.plot(range(len(y_test_inv[:50, i])), y_test_inv[:50, i], labelTrue) plt.plot(range(len(y_pred[:50, i])), y_pred[:50, i], labelPred) plt.xlabel(Sample Index) plt.ylabel(fFeature {i 1}) plt.legend() plt.suptitle(Cluster 0: Prediction vs True (first 50 test samples)) plt.tight_layout() plt.show()对于其他簇可以写一个循环来完成同样的训练和测试然后汇总各簇的评估指标。完整循环示例all_metrics {} models {} for label, (X, y) in cluster_to_data.items(): X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, shuffleFalse ) orig_shape X_train.shape X_train_reshaped X_train.reshape(-1, orig_shape[-1]) X_test_reshaped X_test.reshape(-1, orig_shape[-1]) scaler_x StandardScaler() scaler_y StandardScaler() X_train_scaled scaler_x.fit_transform(X_train_reshaped).reshape(orig_shape) X_test_scaled scaler_x.transform(X_test_reshaped).reshape(X_test.shape) y_train_scaled scaler_y.fit_transform(y_train) y_test_scaled scaler_y.transform(y_test) model build_lstm_model((orig_shape[1], orig_shape[2]), n_targets3) model.fit(X_train_scaled, y_train_scaled, validation_data(X_test_scaled, y_test_scaled), epochs30, batch_size32, verbose0) y_pred_scaled model.predict(X_test_scaled, verbose0) y_pred scaler_y.inverse_transform(y_pred_scaled) y_test_inv scaler_y.inverse_transform(y_test_scaled) mae mean_absolute_error(y_test_inv, y_pred) mse mean_squared_error(y_test_inv, y_pred) all_metrics[label] {mae: mae, mse: mse} models[label] model print(fCluster {label} - MAE: {mae:.4f}, MSE: {mse:.4f})保存模型import os os.makedirs(saved_models, exist_okTrue) for label, model in models.items(): model.save(fsaved_models/lstm_cluster_{label}.h5)这样在部署时可以加载对应簇的模型进行预测。5. 常见问题与排查思路在实际运行这套代码时可能会遇到下面这些问题。问题现象常见原因解决思路K-means 聚类结果乱跳每次运行不同没有设置random_state初始簇中心随机固定random_state必要时调大n_init聚类效果差不同模式没分开未对特征做标准化量纲差异大使用StandardScaler对窗口向量标准化LSTM 训练 loss 不下降学习率过大或数据未归一化降低学习率检查输入输出是否标准化预测值全部接近某个常数数据标准化不彻底或 LSTM 学习不足增加训练轮数检查输出层是否有激活函数模型训练速度慢窗口数量大、LSTM 层数多减小lookback减少 LSTM 单元数使用 GPU 或增加batch_size评估指标在不同簇之间差异很大不同簇的数据分布范围不同可以按簇分别报告指标或者使用加权平均对比总效果新数据预测时不知道属于哪类需要先对新数据做同样的窗口化和标准化然后用 K-means 模型predict得到簇标签保存 K-means 模型和 scaler预测时先走同一套流程关于新样本的簇归属部署时流程如下# 假设 new_window 是形状为 (48, 3) 的窗口数组 # 先标准化 new_flat new_window.reshape(1, -1) new_flat_scaled scaler.transform(new_flat) new_cluster kmeans.predict(new_flat_scaled)[0] # 再加载对应模型预测 model_new models[new_cluster] # 注意训练时输入是 47 步所以需要截取前 47 步 input_seq new_window[:-1].reshape(1, 47, 3) input_seq_scaled scaler_x.transform(input_seq.reshape(1, -1)).reshape(1, 47, 3) pred_scaled model_new.predict(input_seq_scaled) pred scaler_y.inverse_transform(pred_scaled)这里需要注意每个簇训练时都有独立的scaler_x和scaler_y生产环境中要把它们一起保存。6. 最佳实践与工程建议6.1 关于聚类效果K-means 对初始值和异常值比较敏感。使用前可以先对窗口特征做异常值剔除例如基于分位数截断极端值。另外聚类数量 K 不光看肘部法则还要考虑业务含义、每个簇的样本量是否足够训练 LSTM。如果某个簇样本太少可以适当合并相似簇或者对少数类簇使用数据增强。6.2 关于滑动窗口长度lookback的选择决定了 LSTM 能看到多长的历史信息。窗口太短会丢失长期依赖窗口太长会增加计算量并引入噪声。通常可以尝试多个候选值比如 24、48、72、96在验证集上比较表现。窗口滑动步长也值得关注。本文使用步长为 1样本之间存在大量重叠会增强数据量但也会导致样本高度相关。如果担心过拟合可以增大步长比如每 3 个时间步采样一个窗口。6.3 关于数据标准化时间序列预测中输入和输出最好分别标准化并且必须使用训练集统计量来变换测试集避免数据泄漏。标准化器需要与模型一起保存否则新数据无法正确还原。6.4 关于模型结构对于多输出回归LSTM 层和 Dense 层的单元数可以根据数据复杂度调整。如果输出目标之间关联紧密LSTM 隐状态可以自动捕捉。如果两个目标量纲差距很大建议先分别做标准化必要时可以拆成多个输出分支每个分支有自己的 Dense 层然后再合并 loss。在 Keras 中多输出分支的写法如下# 多输出分支示例 from tensorflow.keras.layers import Input, LSTM, Dense from tensorflow.keras.models import Model inputs Input(shape(47, 3)) lstm_out LSTM(32)(inputs) # 两个输出分支 output1 Dense(1, namefeature1)(lstm_out) output2 Dense(1, namefeature2)(lstm_out) model Model(inputs, [output1, output2]) model.compile(optimizeradam, lossmse)如果你希望不同目标有不同权重还可以在compile时传入loss_weights。6.5 关于评估与对比建议不要只和真实值画图对比还要和“直接使用一个全局 LSTM 模型”做对比。全局模型的实现方式相同只是不区分聚类标签把所有窗口数据放在一起训练。两者对比后你才能确认“K-means LSTM”是否真的带来了提升。评估指标可以同时使用 MAE 和 RMSE。MAE 容易理解RMSE 对大误差更敏感。如果关心极端情况预测RMSE 更合适。6.6 关于生产部署生产环境下建议使用 TensorFlow Serving 或者 ONNX 转换后部署。每个簇一个模型文件预测前先通过 K-means 路由。为了减少模型数量也可以考虑只对变化差异大的模式分别建模差异小的模式合并处理。日志记录建议包含以下信息样本所属簇模型版本预测值与真实值的误差特征取值是否越界。这样即使预测效果下降也能快速定位是聚类漂移还是模型退化。7. 总结与学习路线本文完整实现了“K-means 聚类 LSTM 多输出回归”的实战流程。你可以从代码中看到核心其实只有四步将时间序列切分成窗口用 K-means 找到不同模式对每个模式分别训练 LSTM预测时先路由到对应模型再得到多目标输出。这套方法的优势在于用聚类把复杂时序数据分解为多个相对简单的子问题让 LSTM 可以在更一致的数据分布上学习。同时多输出回归结构允许我们一次预测多个特征比单独训练多个模型更节省时间也更容易捕捉目标之间的相关性。接下来如果你想继续深入可以从几个方向入手尝试用 K-Shape、DBSCAN 等时序聚类算法替代 K-means比较不同聚类方法对预测效果的影响使用 Attention 机制改造 LSTM提升长序列预测能力引入更多特征如外部温度、节假日构造多变量 LSTM把这个方案应用到真实数据集比如电力负荷、交通流量、股票指数等公开数据。动手实践是掌握这套流程最好的方式。你可以先运行本文代码修改模拟数据中的模式种类、窗口长度和模型层数观察不同设置下的输出差异。遇到问题后再回来看第 5 节的排查表基本可以覆盖大多数常见场景。如果这篇文章对你有帮助建议收藏备用后续做时序项目时可以直接对照实现。