K-Means+LSTM组合预测:让多步时间序列预测更稳更准
2026/9/17 17:50:47 网站建设 项目流程

简介:一份基于 k-means-LSTM(k均值聚类结合长短期记忆神经网络)的多输入多输出组合预测完整项目文档,面向有一定机器学习和 Python 基础的研究人员、数据科学家及时间序列预测学习者。资源聚焦能源管理、气象预测、金融市场分析、交通流量预测、医疗健康等场景,通过聚类预处理与 LSTM 子集建模提升预测精度和鲁棒性。完整覆盖环境准备、数据准备、算法设计、模型构建、模型评估、优化调整、GUI 设计及代码整合八个阶段,每个环节均配有代码示例与细节说明。包体为单个 docx 文档,大小约 57KB,核心内容以图文和代码形式呈现,便于查阅与复用;目前已有 66 人学习下载。除实现指南外,文档还包含系统化流程解析、技术选型考量、潜在挑战与应对措施,以及可直接运行的完整程序与精美 GUI 设计,读者可从数据加载一路操作至预测输出,是一份能落地的组合预测方案参考资料。

1. 聚类加LSTM,为什么值得把时间序列切成几段再预测

单用LSTM做多输入多输出预测,模型要同时拟合全局趋势、局部波动和跨特征的耦合关系,参数量和训练难度都会明显上升。一个更稳的做法是先用k-means把历史样本按特征空间里的距离分组,让每个LSTM子模型只负责一类形态相似的数据。这样每个子模型的输入分布更集中,梯度更新更平滑,预测精度往往比单一LSTM高出不少。这类组合预测在电力负荷、气象要素、交通流等场景里已经被验证过,不是花哨的堆叠,而是用无监督方法给监督学习做前置分工。

这个项目完整覆盖了数据清洗、k-means聚类、LSTM建模、多步滚动预测、评估和GUI封装。适合有Python和机器学习基础、想把手上的时间序列预测任务落地成可交互程序的开发者。

2. 数据预处理与k-means聚类:不是把数据喂进去就完事

2.1 数据窗口化与归一化的顺序问题

时间序列预测的第一步,是把一维或多维序列转成监督学习格式。常见做法是滑动窗口:用过去window_size个时间步的特征,预测未来一个或多个时间步的目标值。窗口大小直接决定模型能看到多长的历史信息,太小学不到周期,太大引入噪声并放大计算量。

import numpy as np import pandas as pd from collections import deque def create_sequences(data, window_size=24, horizon=3): """ 将多变量时间序列转换为监督学习样本 data: 形状为 (samples, features) 的原始数据 window_size: 用过去多少步做输入 horizon: 预测未来多少步 """ X, y = [], [] for i in range(len(data) - window_size - horizon + 1): X.append(data[i:i + window_size, :]) y.append(data[i + window_size:i + window_size + horizon, :]) return np.array(X), np.array(y) df = pd.read_csv('energy_data.csv', index_col='date', parse_dates=['date']) values = df.values.astype('float32') X, y = create_sequences(values, window_size=24, horizon=3) print(f"输入形状: {X.shape}") # (样本数, 24, 特征数) print(f"输出形状: {y.shape}") # (样本数, 3, 目标特征数)

这里有两个容易踩的坑。第一,窗口化和数据划分必须在归一化之后做,否则测试集信息会通过scaler泄漏到训练集里。第二,horizon参数控制的是预测步长,多步预测时输出层要把每一步解码出来,而不是只输出一个值。

归一化用MinMaxScaler还是StandardScaler,要看数据分布。如果序列里有明显的长尾或异常尖峰,StandardScaler更稳;如果数据本身落在固定区间且没有极端值,MinMaxScaler收敛更快。工业负荷这类带明显周期且偶发尖峰的数据,我一般先用RobustScaler做一遍,再把异常值拉回上下四分位,效果比直接归一化好。

2.2 聚类特征怎么构造

k-means聚类是对样本在特征空间里做划分,但原始时间序列样本是高维的,直接聚类容易受噪声影响。需要先降维或抽特征,常见做法有两种:

  1. 对每个窗口内的数据取统计量:均值、标准差、最大值、最小值、偏度、峰度,拼成一个低维特征向量。
  2. 用PCA或T-SNE先降维,再对降维结果聚类。

项目里更常用第一种,因为统计量带业务含义,后续可以针对每个聚类的典型形态做解释。

from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler def extract_cluster_features(X): """ 从滑窗样本中抽取统计特征用于聚类 X: (samples, window_size, features) """ n_samples = X.shape[0] features = np.zeros((n_samples, 6)) for i in range(n_samples): features[i, 0] = np.mean(X[i]) features[i, 1] = np.std(X[i]) features[i, 2] = np.max(X[i]) features[i, 3] = np.min(X[i]) features[i, 4] = float(np.argmax(X[i])) # 峰值位置 features[i, 5] = float(np.argmin(X[i])) # 谷值位置 return features cluster_feats = extract_cluster_features(X) scaler = StandardScaler() cluster_feats_scaled = scaler.fit_transform(cluster_feats) # 选择最佳聚类数 inertia = [] for k in range(2, 11): km = KMeans(n_clusters=k, random_state=42, n_init=10) km.fit(cluster_feats_scaled) inertia.append(km.inertia_) print("不同k值的惯量:", inertia)

聚类数k的选择没有绝对标准,通常结合肘部法则和轮廓系数一起看。k太小,子集内部仍然混杂多种形态;k太大,每个子集样本量不足,LSTM训练不充分。项目里如果样本量在几千到几万之间,k取4到8比较合理,每个子集至少保留几百条样本才够训练一个像样的LSTM。

2.3 对每个聚类分别划分训练集和测试集

聚类完成后,要保证每个聚类内部都按时间顺序切分训练和测试,不能混着切。时间序列数据天然有顺序依赖,如果同一个聚类的样本在时间上有交叠,随机划分会引入数据泄漏。

from sklearn.model_selection import train_test_split cluster_assignments = km.fit_predict(cluster_feats_scaled) cluster_datasets = {} for c in np.unique(cluster_assignments): indices = np.where(cluster_assignments == c)[0] X_c = X[indices] y_c = y[indices] # 按时间顺序切分,不打乱 split_idx = int(len(X_c) * 0.8) X_train, X_test = X_c[:split_idx], X_c[split_idx:] y_train, y_test = y_c[:split_idx], y_c[split_idx:] cluster_datasets[c] = { 'X_train': X_train, 'y_train': y_train, 'X_test': X_test, 'y_test': y_test } print(f"聚类 {c}: 训练 {len(X_train)} 条, 测试 {len(X_test)} 条")

这样每个LSTM子模型只在自己的数据子集上训练,推理时把新样本的统计特征先送入聚类模型确定归属,再路由到对应的LSTM做预测。整体上,训练阶段比单一LSTM多花一点时间,但推理阶段反而因为输入分布更集中而更快收敛。

3. LSTM建模与多输入多输出预测:把序列模型改造成多路输出

3.1 网络结构设计

LSTM适合处理时间序列的关键在门控机制:输入门决定当前信息写入多少,遗忘门决定历史状态保留多少,输出门控制状态对外输出。多输入多输出场景下,输入层同时接收多个特征字段,输出层需要同时产生多个预测目标的时间步序列。

构建方式有两种主流选择。一种是序列到序列结构,编码器和解码器都用LSTM,适合输入输出长度不一致的复杂映射。本项目里输入是window_size步的历史特征,输出是horizon步的目标序列,输入输出窗口长度不同,简单堆一个LSTM加Dense层也可以,但效果不如序列到序列稳定。

import tensorflow as tf from tensorflow.keras.models import Model from tensorflow.keras.layers import Input, LSTM, Dense, RepeatVector, TimeDistributed def build_seq2seq_mimo(input_steps, n_features, horizon, n_targets, lstm_units=64): """ 基于编码器-解码器的LSTM多输入多输出模型 input_steps: 输入时间步数 n_features: 输入特征数 horizon: 预测步数 n_targets: 目标变量个数 """ encoder_inputs = Input(shape=(input_steps, n_features)) encoder_lstm = LSTM(lstm_units=lstm_units, return_state=True) encoder_outputs, state_h, state_c = encoder_lstm(encoder_inputs) encoder_states = [state_h, state_c] decoder_inputs = RepeatVector(horizon)(encoder_outputs) decoder_lstm = LSTM(lstm_units=lstm_units, return_sequences=True) decoder_outputs = decoder_lstm(decoder_inputs, initial_state=encoder_states) decoder_dense = TimeDistributed(Dense(n_targets)) outputs = decoder_dense(decoder_outputs) model = Model(encoder_inputs, outputs) model.compile(optimizer='adam', loss='mse', metrics=['mae']) return model model = build_seq2seq_mimo( input_steps=X_train.shape[1], n_features=X_train.shape[2], horizon=y_train.shape[1], n_targets=y_train.shape[2] ) model.summary()

这段代码里的RepeatVector是把编码器最后一个时间步的输出复制成horizon份,作为解码器每一步的输入。TimeDistributed保证Dense层对每个解码时间步独立作用,输出形状是(batch_size, horizon, n_targets),正好对应多步多变量的预测目标。

如果输出只是未来一个时间步的多个变量,可以不加解码器,直接在LSTM后接Dense层,输出维度设为n_targets。预测维度更高时再升级到上面的seq2seq结构。

3.2 损失函数与评估指标的选择

多输出回归任务默认用MSE做损失函数,但它对异常值惩罚过重,容易让模型过度拟合少数极端样本。实际使用中,我通常配合平滑的HuberLoss,它在线性区域表现为MAE,在残差较大时退化为MSE,对噪声更鲁棒。

model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss=tf.keras.losses.Huber(delta=1.0), metrics=['mae']) history = model.fit( X_train, y_train, validation_data=(X_test, y_test), epochs=80, batch_size=64, callbacks=[ tf.keras.callbacks.EarlyStopping( monitor='val_loss', patience=10, restore_best_weights=True ), tf.keras.callbacks.ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=5, min_lr=1e-5 ) ], verbose=1 )

learning_rate初始值设为1e-3,配合ReduceLROnPlateau在验证损失停滞时自动降半,比手动调整省事得多。EarlyStoppingpatience设置成10,防止验证曲线反复震荡时过早停掉。restore_best_weights=True很重要,否则模型参数会停在最后一轮而不是最好的一轮,对LSTM这类训练后期容易过拟合的网络来说,这个设置能挽回不少精度。

3.3 多步预测时的两种调用方式

推理阶段有两种方式:单步滚动预测和直接多步预测。直接多步预测用上文seq2seq结构一次输出整个horizon,速度快但误差会随着预测长度累积。单步滚动预测每步把预测值拼到输入后面,再预测下一步,这个方式理论上更稳,但推理耗时是直接预测的horizon倍。

def recursive_multistep_predict(model, X_input, horizon, n_features): """ 滚动多步预测 X_input: (1, window_size, n_features) """ current_input = X_input.copy() predictions = [] for step in range(horizon): # 输入形状调整为 (1, window_size, n_features) y_step = model.predict(current_input, verbose=0) # (1, 1, n_targets) y_step = y_step[0, 0, :] # (n_targets,) predictions.append(y_step) # 把新预测拼接到输入,丢弃最老的时间步 new_step = np.roll(current_input, shift=-1, axis=1) new_step[0, -1, :] = y_step current_input = new_step return np.array(predictions)

滚动预测的风险是误差会随时间步累积,如果第1步预测偏了,第2步的输入就已经被污染。所以实际部署时,我通常先用直接多步预测跑一遍,再对误差超过阈值的样本做滚动修正,取两者加权平均。这个技巧在负荷预测里能额外把MAE压低1%到3%。

4. GUI设计:用Tkinter把整个预测流程封装成桌面工具

4.1 界面布局与交互逻辑

项目里GUI用了Tkinter加matplotlib嵌入的方案。Tkinter是Python标准库,不需要额外安装,打包成exe也方便。整体交互流程是:点击按钮加载CSV数据,选择聚类数,模型开始训练,训练过程中实时刷新损失曲线,训练结束选择一条测试样本查看预测对比图,最后可以导出预测结果到CSV。

import tkinter as tk from tkinter import ttk, messagebox, filedialog from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure class PredictorApp: def __init__(self, root): self.root = root self.root.title("k-means-LSTM 组合预测系统") self.root.geometry("1280x800") # 顶部控制区 control_frame = ttk.LabelFrame(root, text="参数配置") control_frame.pack(fill=tk.X, padx=10, pady=5) ttk.Label(control_frame, text="聚类数 k:").grid(row=0, column=0, padx=5, pady=5) self.k_var = tk.IntVar(value=5) ttk.Spinbox(control_frame, from_=2, to=10, textvariable=self.k_var, width=5).grid(row=0, column=1) ttk.Label(control_frame, text="LSTM单元数:").grid(row=0, column=2, padx=5) self.units_var = tk.IntVar(value=64) ttk.Spinbox(control_frame, from_=16, to=128, textvariable=self.units_var, width=5).grid(row=0, column=3) ttk.Button(control_frame, text="加载数据", command=self.load_data).grid(row=0, column=4, padx=10) ttk.Button(control_frame, text="开始训练", command=self.train_model).grid(row=0, column=5, padx=10) ttk.Button(control_frame, text="导出结果", command=self.export_result).grid(row=0, column=6, padx=10) # 图表区 self.fig = Figure(figsize=(10, 6), dpi=100) self.ax_loss = self.fig.add_subplot(211) self.ax_pred = self.fig.add_subplot(212) self.canvas = FigureCanvasTkAgg(self.fig, master=root) self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=10, pady=5)

GUI界面的核心是回调函数。点击“开始训练”后,界面需要同步更新模型状态和图表,不能用普通循环阻塞主线程。多线程里调用matplotlib更新时,注意用after方法把绘图请求投递回主线程,否则会出现图形闪烁或卡死。

4.2 线程管理与训练日志输出

LSTM训练动辄几十秒到几分钟,必须放到后台线程运行,否则界面会显示“无响应”。训练过程中的日志要流式输出到文本框里,让用户知道进度。

import threading import sys from tkinter.scrolledtext import ScrolledText class TextRedirector: def __init__(self, widget, tag="stdout"): self.widget = widget self.tag = tag def write(self, text): self.widget.insert(tk.END, text) self.widget.see(tk.END) self.widget.update_idletasks() class PredictorApp: def train_model(self): k = self.k_var.get() units = self.units_var.get() # 后台线程执行训练 self.thread = threading.Thread( target=self._train_worker, args=(k, units), daemon=True ) self.thread.start() def _train_worker(self, k, units): sys.stdout = TextRedirector(self.log_text) sys.stderr = TextRedirector(self.log_text, tag="stderr") # 此处调用聚类和LSTM训练流程 model, history = self.run_cluster_lstm(k, units) self.root.after(0, self._update_plot, history, model)

sys.stdout重定向到文本框属于技巧性做法,能捕获model.fit()内部的进度条输出,但注意重定向要在子线程内做,主线程的stdout不能受影响。

4.3 展示预测效果

预测对比图的设计上,收益最大的是把真实值和预测值画在同一坐标系,然后标注区间,而不是只画曲线。多输出场景里输出维度多,图太密看不清,常见做法是每个目标变量单独一个子图,或选择两个主要目标展示,次要目标只显示误差指标。

matplotlib嵌入Tkinter后,无论要画多少张图,都要在同一个Figure对象下复用坐标轴,不要每次新建Figure,否则卡顿非常明显。训练完成后画一次,用户点击预测按钮时只更新Line2D对象的y数据,不重建画布。

5. 调优、防过拟合与部署:让模型在真实场景里站得住

5.1 超参数组合的搜索策略

LSTM + k-means的超参数空间比单一模型更宽:除了窗口大小、LSTM层数和单元数、学习率,还要额外搜索聚类数k。不建议网格搜索,时间成本太高。我一般用贝叶斯优化,或者退一步做随机采样加早停。优先固定窗口大小和聚类数,先粗调LSTM结构,再回来微调k。

聚类数k对最终精度的影响是非线性的。k增大会让子集更纯净,但某些子集的样本量可能不足以训练复杂网络。一个相对稳的经验是:先画聚类惯量曲线找到明显的肘部,再在肘部前后各取一个值对比验证集loss,选低的那个。

def evaluate_k(k, X, y, window_size): """快速对比不同k值下的验证集MAE""" cluster_feats = extract_cluster_features(X) km = KMeans(n_clusters=k, random_state=42, n_init=10) labels = km.fit_predict(cluster_feats) total_mae = 0.0 for c in np.unique(labels): idx = np.where(labels == c)[0] X_c, y_c = X[idx], y[idx] split = int(len(X_c) * 0.8) X_tr, X_te = X_c[:split], X_c[split:] y_tr, y_te = y_c[:split], y_c[split:] model = build_seq2seq_mimo( input_steps=window_size, n_features=X_tr.shape[2], horizon=y_tr.shape[1], n_targets=y_tr.shape[2], lstm_units=32 # 快速验证用小网络 ) model.fit(X_tr, y_tr, epochs=30, batch_size=32, verbose=0) mae = model.evaluate(X_te, y_te, verbose=0)[1] total_mae += mae * len(X_c) return total_mae / len(X) k_scores = [evaluate_k(k, X, y, window_size=24) for k in range(3, 9)] print("不同聚类数的MAE:", k_scores)

5.2 过拟合信号怎么识别

k-means-LSTM的过拟合有特殊的信号:训练损失下降正常,但某个聚类的测试损失明显高于其他聚类。这说明该子集样本量不足或内部仍有异常样本。优先检查这个聚类内的样本分布,把样本量最少的几个子集合并;如果所有聚类表现均衡但整体过拟合,再从Dropout和权重衰减两个方向补强。

from tensorflow.keras.layers import Dropout, Bidirectional def build_regularized_model(input_steps, n_features, horizon, n_targets, lstm_units=64): encoder_inputs = Input(shape=(input_steps, n_features)) encoder_lstm = Bidirectional( LSTM(lstm_units, return_sequences=True, dropout=0.3, recurrent_dropout=0.2) )(encoder_inputs) encoder_lstm = LSTM(lstm_units, return_state=True, dropout=0.3)(encoder_lstm) # 后续结构不变

recurrent_dropout是LSTM专用的正则化手段,对外部特征噪声依赖强的场景很有效。但注意它只加在循环权重上,不能替代普通的dropout。两者的值不太建议都超过0.3,否则欠拟合风险会上升,尤其是样本量不足500的子集。

5.3 模型持久化与实时服务接入

训练好的每个聚类子模型和聚类器需要一起持久化,部署时才能做路由推理。项目里用Keras保存每路模型,用pickle保存聚类器和scaler,推理时按顺序做归一化、聚类分配、路由预测。

import joblib # 保存阶段 for c, model in cluster_models.items(): model.save(f'lstm_cluster_{c}.h5') joblib.dump(km_scaler, 'cluster_scaler.pkl') joblib.dump(kmeans_model, 'kmeans_model.pkl') joblib.dump(feature_scaler, 'feature_scaler.pkl') # 推理阶段 def predict_new(X_new): X_scaled = feature_scaler.transform(X_new) feats = extract_cluster_features(X_scaled) feats_scaled = cluster_scaler.transform(feats) label = kmeans_model.predict(feats_scaled)[0] return cluster_models[label].predict(X_scaled)

部署到生产环境时,可以把这个推理流程包装成Flask或FastAPI接口,每个聚类的模型单独加载到内存,避免每次请求都重新推理聚类归属。GPU部署时注意LSTM模型的batch size要固定,否则TensorRT或ONNX导出时可能报维度不匹配。

5.4 在线学习与模型更新策略

这个项目当前用的是离线训练加定期重训模式,但实际业务场景里数据分布会漂移。日常使用中,我建议每积累500到1000条新样本就重新评估一次各聚类子集的MAE,如果某个子集连续三次评估MAE都上升,就只重训这个子集,不用全量重跑。关键是用joblib把增量数据缓存下来,避免频繁重训消耗过多算力。

这类组合模型最重要的维护思路:k-means负责分流,LSTM负责拟合,当数据形态发生变化时,先看样本是否落到了新的聚类边缘,再决定是调整聚类数还是重新初始化子模型。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询