
简介面向时间序列预测场景提供基于鲸鱼优化算法与卷积双向长短期记忆网络的完整代码实现。适合有一定编程基础的数据分析师和机器学习工程师可应用于股票价格预测、气象数据分析、能耗估计等需要精确时序建模的业务场景。资源包共1个docx文档大小约34KB以程序代码和逐段详解为核心覆盖数据预处理、时间步构造、鲸鱼算法超参优化、卷积特征提取、双向长短时记忆网络训练评估以及界面设计等关键模块。文档从系统设计思路入手逐步展示数据归一化、训练集测试集划分、模型搭建、多指标评估的具体实现同时指出避免过拟合等注意事项并给出引入更多优化算法、支持多变量预测等改进建议。目前已有131人学习下载适合希望快速复现该时序预测方案并用于实际业务的技术人员。1. 先别急着跑代码WOA-CNN-BiLSTM 到底帮你解决了什么问题做时间序列预测的人十有八九都卡在同一个地方模型框架还没定死先被超参数折磨得够呛。滤波器设多少、LSTM 单元设多少、dropout 多少才不过拟合、学习率怎么选每个都要手动试一轮。这套 Python 工程包的核心就是让鲸鱼算法WOA代替你做那套笨拙的手工调参把 CNN 提特征、BiLSTM 建模的能力和群体智能搜索策略捏在一起直接给出一个相对靠谱的超参组合。适合做股票预测、气象数据分析和能耗预测的工程师也适合那些“有数据、有 baseline、想快速看到改进效果”的分析师。前提是你已经装好了 TensorFlow 和 PyQt5如果你还在用 CPU 裸跑长序列建议先看第三节的环境配置不然第一个 epoch 就会让你等到怀疑人生。2. 数据预处理与序列切分90% 的翻车都发生在这一步2.1 先说环境和依赖装对版本少踩一半坑工程包的主流程依赖 numpy、pandas、scikit-learn、matplotlib、TensorFlow 和 PyQt5。我的习惯是先用清华源把六个库一次装齐避免后续缺依赖时东拼西凑pip install numpy pandas matplotlib scikit-learn PyQt5 -i https://pypi.tuna.tsinghua.edu.cn/simple pip install tensorflow -i https://pypi.tuna.tsinghua.edu.cn/simpleTensorFlow 的版本建议直接装 2.10 到 2.13 之间的稳定版太新的版本对 CUDA 要求更苛刻太老的在 Keras API 上会报各种过期警告。numpy 装完顺手跑一句np.__version__如果版本大于 2.0而 TensorFlow 还是 2.12 以下早期会碰到np.float不存在之类的兼容报错解决方式是把 numpy 降到 1.26 再重装一次。环境这部分没什么玄学纯属经验列表先隔离环境、再装包、再跑样例顺序不对就得返工。2.2 数据切分的铁规矩测试集必须“向后看”时间序列和普通回归的本质差别在于顺序不可打乱。项目正文里用了train_test_split但我的经验是它默认的shuffleTrue在这里必须关掉或者干脆手工切片。如果 sklearn 默认去打乱样本模型就会看到未来的片段训练指标好得离谱一到真实部署立刻现原形。from sklearn.preprocessing import MinMaxScaler values data[value].values.reshape(-1, 1) scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(values) train_size int(len(scaled) * 0.8) train scaled[:train_size] test scaled[train_size:]逻辑说明fit_transform只在训练集上执行然后用同一个scaler去 transform 测试集这是归一化的标准写法。很多人图省事先对整条序列fit_transform再切分这会让测试集的极小值和极大值提前进入归一化参数指标虚低我认为这是时间序列里最隐蔽的数据泄露之一后文会专门讲排查方法。MinMaxScaler的feature_range表示把数据映射到 0 到 1 的区间对带长尾的序列也可以改为 StandardScaler看数据分布来定。归一化不是可选项。CNN-BiLSTM 里用的激活函数和梯度更新机制对输入尺度很敏感原始序列如果是几千到几万的数量级梯度很容易振荡收敛速度会慢一个量级。切完训练集后我通常会让train_size占比在 0.7 到 0.8 之间给模型留足样本也保证测试集足够长能看出趋势上的误差累积。2.3 滑动窗口函数从单步到多步的改造项目里给了一个典型的时间步切分函数用time_step 10生成输入输出对。这个函数能用但在工程上可以做得更通用一点至少支持多变量输入和单变量多步输出否则后续换数据集时你还得再改一遍。def make_sequences(data, time_step10, n_out1): X, y [], [] for i in range(len(data) - time_step - n_out 1): X.append(data[i:itime_step, 0]) y.append(data[itime_step:itime_stepn_out, 0]) return np.array(X), np.array(y) X_train, y_train make_sequences(train, time_step10, n_out1) X_test, y_test make_sequences(test, time_step10, n_out1) print(X_train.shape, y_train.shape)参数说明time_step是滑动窗口的宽度也就是用过去多少个时刻预测未来n_out1表示单步预测后续改多步预测时只需把n_out调到目标步数。采样方式是滑窗去重采样步长默认是 1若数据量极大、序列本身高频可以加入step参数做降采样工程包里保留了这一步的扩展位。切完窗口后需要 reshape 成[样本数, 时间步, 特征数]的格式因为 Conv1D 要求第三个维度是通道数单变量就是 1多变量改成对应列数即可。time_step 的选择有一定规律可循日频数据一般取 5 到 20周频取 4 到 8分钟级数据可以取几十甚至上百。默认给 10 比较中庸但如果你的序列有明显周期比如电力负荷的日周期至少要让窗口覆盖一个周期长度。窗口太短模型记不住周期窗口太长训练样本数量骤减这一步没有标准答案只能对照业务周期去试。3. WOA 优化器详解鲸鱼算法到底在优化什么怎么编码3.1 鲸鱼算法是思想不是黑匣子鲸鱼优化算法Whale Optimization Algorithm模仿座头鲸的泡泡网捕食行为。核心机制有三个包围猎物、螺旋气泡网攻击、随机搜索。它的优势是不依赖梯度信息对 CNN-BiLSTM 这种“超参数离散组合 训练代价高”的问题比网格搜索更省时比随机搜索更聪明因为每迭代一轮当前最优个体和群体位置信息都会被利用搜索方向会收敛。WOA 的位置更新公式并不复杂a随迭代从 2 线性减到 0A 2a·r - a用来控制探索和开发。当|A| 1时偏向局部开发|A| 1时偏向全局探索。C 2·r是随机系数影响位置更新的幅度。这套机制决定了它适合低维度连续参数寻优比如 filters、units、dropout、learning rate 这类连续或准连续超参。3.2 参数边界决定上限我把超参空间这样设WOA 搜索空间设计比算法本身更重要。如果边界太宽搜索效率低边界太窄搜出来的组合不一定比手工调优更好。我按工程包默认再加上经验给出这样一套边界参数搜索范围解码方式filters16 ~ 128取整到 8 的倍数kernel_size2 或 3固定不参与搜索BiLSTM units32 ~ 128取整到偶数dropout0.1 ~ 0.5保留两位小数learning rate1e-4 ~ 1e-2对数均匀采样kernel_size 我不放进搜索空间因为它改变卷积输出的时间步长度和后续 LSTM 对齐有关系改动它容易让模型直接报 shape 错误。filters 和 units 的取值需要转成整数WOA 默认是在连续空间里优化位置向量解码时必须做取整操作否则 Keras 会直接扔 TypeError。3.3 WOA 主循环代码与一次完整演化import numpy as np class WOA: def __init__(self, fitness_func, dim, lb, ub, n_whales10, max_iter15, seed42): self.fitness_func fitness_func self.dim dim self.lb np.array(lb) self.ub np.array(ub) self.n_whales n_whales self.max_iter max_iter self.rng np.random.default_rng(seed) def optimize(self): pos self.rng.uniform(self.lb, self.ub, (self.n_whales, self.dim)) fitness np.array([self.fitness_func(p) for p in pos]) best_idx np.argmin(fitness) best_pos pos[best_idx].copy() best_score fitness[best_idx] for t in range(self.max_iter): a 2 - 2 * t / self.max_iter for i in range(self.n_whales): r1, r2 self.rng.random(), self.rng.random() A 2 * a * r1 - a C 2 * r2 p self.rng.random() if p 0.5: if abs(A) 1: D abs(C * best_pos - pos[i]) pos[i] best_pos - A * D else: rand_whale self.rng.integers(0, self.n_whales) D abs(C * pos[rand_whale] - pos[i]) pos[i] pos[rand_whale] - A * D else: D abs(best_pos - pos[i]) pos[i] D * np.exp(1) * np.cos(2 * np.pi * 1) best_pos pos[i] np.clip(pos[i], self.lb, self.ub) fitness np.array([self.fitness_func(p) for p in pos]) for i in range(self.n_whales): if fitness[i] best_score: best_score fitness[i] best_pos pos[i].copy() return best_pos, best_score逻辑说明fitness_func是你要优化的目标函数输入一组超参返回验证集上的误差越小越好。每次迭代先计算a的线性衰减值然后对每个鲸鱼个体按概率选择螺旋更新或包围更新最后把位置裁剪进边界再重算适应度。seed参数很关键鲸鱼算法本身是随机搜索类算法不固定种子时每次跑出的超参和结果都不一样后文避坑章我会专门说。参数说明lb和ub是上述搜索边界的数组比如lb[16, 32, 0.1, 1e-4]对应的就是 filters、units、dropout、learning rate 的下界。n_whales建议 10 到 15太多会让评估次数暴涨max_iter建议 15 到 30迭代太深收益递减。每个个体评估一次就意味着训练一个 CNN-BiLSTM 模型所以 15 次迭代乘 10 个个体等于要训 150 个模型。我一般会先在验证集上用 20 个 epoch 快速粗筛选出最优组合后再用完整训练集和 100 个 epoch 做最终训练时间成本能省一半以上。4. 模型搭建与训练CNN-BiLSTM 每一步都要知道自己在干什么4.1 卷积层在对时序做什么很多人以为 Conv1D 只能处理图像这是个误解。在时间序列里Conv1D 其实是在沿着时间轴做滑动窗口卷积从局部片段中提取显著特征比如陡升陡降的拐点、局部峰值。kernel_size2表示每个卷积核看相邻两个时刻的数值这和time_step10的粗粒度语义不同前者提取的是微观局部特征后者决定模型能看到多远的历史。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, Bidirectional, LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam def build_cnn_bilstm(time_step, filters64, kernel2, units50, dropout0.2, lr1e-3): model Sequential() model.add(Conv1D(filtersfilters, kernel_sizekernel, activationrelu, input_shape(time_step, 1))) model.add(Bidirectional(LSTM(units, return_sequencesTrue))) model.add(Dropout(dropout)) model.add(Bidirectional(LSTM(units))) model.add(Dropout(dropout)) model.add(Dense(1)) model.compile(lossmean_squared_error, optimizerAdam(learning_ratelr), metrics[mae]) return model逻辑说明卷积层的输出维度是(batch, time_step - kernel 1, filters)送入 BiLSTM 前必须保证时间步维度还足够大。return_sequencesTrue是为了让第一层 BiLSTM 输出完整的序列给第二层 BiLSTM如果这里写错第一层就直接输出最后一个时间步的隐藏状态第二层就接收不到完整序列模型表达能力大打折扣。最后一层 Dense 输出单步预测值。参数说明filters 控制在卷积层能提取多少种特征units 是 LSTM 隐状态的维度决定 LSTM 记住多少历史信息dropout 是随机失活比例0.2 是经验折中值既可以抑制过拟合又不至于让模型欠拟合。Adam 的learning_rate默认是 0.001但时间序列任务里我经常发现 0.0005 更稳尤其是高噪声序列学习率过大时损失曲线会在最优值附近反复横跳。4.2 BiLSTM 的“双向”在预测里的正确用法双向 LSTM 在训练时会让每个时间步同时看到过去和未来的上下文信息这对序列分类和序列标注是天然优势但放到时间序列预测里很多人心里会犯嘀咕预测未来时不可能看到未来双向是不是作弊我解释一下这里的设计逻辑本模型的输入是一个固定的历史窗口比如time_step10窗口内部的时间步之间是完整的双向 LSTM 在窗口内部互相“看”是没问题的模型要预测的目标在窗口之外并不是窗口内部的某一点。也就是说模型没有偷看未来的真实值只是充分挖掘了窗口内的上下文关系。如果哪天改成编码器-解码器结构做多步预测就一定要谨慎解码阶段不能再用 BiLSTM 看到未来时间步。我在实际项目里也试过把第二层 BiLSTM 换成单向 LSTM大部分数据集上效果差别不大但换了之后参数数量更少、训练更快。如果数据量不大单层 BiLSTM 就足够不必叠两个双向层不然过拟合风险高训练时间还长。4.3 训练配置epochs、早停与学习率调度的组合项目正文直接用model.fit(epochs100)但在真实数据上固定 100 个 epoch 不是好习惯。有的数据 40 轮就过拟合了有的 80 轮还在降。我更推荐把早停和学习率衰减加上让训练自己决定什么时候收手。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) lr_scheduler ReduceLROnPlateau(monitorval_loss, patience5, factor0.5, min_lr1e-5) history model.fit( X_train, y_train, validation_split0.15, epochs100, batch_size32, callbacks[early_stop, lr_scheduler], verbose1 )逻辑说明validation_split0.15表示从训练集尾部切 15% 做验证集注意它是从尾部切不是随机切这符合时间序列的顺序性。EarlyStopping 监控验证集损失连续 10 轮没有下降就停止训练restore_best_weightsTrue保证停止后恢复历史最优权重避免最后几轮过拟合的权重复盖好结果。参数说明patience10容忍 10 个 epoch 的波动数据噪声大的序列可以放宽到 15ReduceLROnPlateau在验证损失连续 5 轮不降时把学习率减半每次减半直到min_lr1e-5。batch_size 在长序列上我倾向于 32 或 16太大的 batch 会让梯度方向过于平滑小 batch 反而有正则化效果但训练时间会变长。批次大小、学习率和 dropout 三者互相牵制改其中一个往往要连带调整另外两个。5. WOA-CNN-BiLSTM 常见问题排查四个我真实踩过的坑5.1 现象一训练集上 RMSE 很低测试集上高到离谱原因数据泄露几乎可以锁定。最常见的就是在切分前对整条序列做了fit_transform或者train_test_split开了默认的shuffleTrue让模型提前看到了未来的数值范围。解决归一化必须切分后再做严格按 2.2 的写法先切分再 fit 训练集再 transform 测试集。切分时手工用数组切片不要用 sklearn 的train_test_split就算用也必须传shuffleFalse。另外检查一下创建序列的函数是否把y混进了X我碰到过因为列索引写错导致目标值泄漏进特征的情况。5.2 现象二WOA 每次跑出的最优参数和指标都不一样原因鲸鱼算法是随机初始化种群没有固定种子时每次搜索路径都不同TensorFlow 本身的 GPU 算子也带随机性即使参数相同训练结果也会有波动。解决在 WOA 类里把seed固定同时给 TensorFlow 设置全局随机种子。我一般会在脚本开头加上tf.random.set_seed(42)和np.random.seed(42)这样至少保证单机单卡环境下稳定可复现。更稳妥的做法是同一个最优参数在验证集上重复跑三次取均值作为最终评估指标单次结果不能作为选型依据这就是调参界的“三局两胜”原则。5.3 现象三反归一化后测试集预测曲线整体偏移RMSE 却显示正常原因这是我在工程包代码里发现的一个隐蔽坑。项目正文的反归一化用的是完整序列的scaler去 inverse_transform但训练集和测试集归一化区间不同完整序列 scaler 的最小值和最大值来自所有数据和训练集 scaler 不一致导致预测值被映射到了错误的量纲区间。如果指标计算也用了反归一化后的预测值和原始 y 去比数值就会失真。解决按 2.1 的做法分别保留scaler_train和scaler_test训练集预测用训练集 scaler 反算测试集预测用测试集 scaler 反算。绘图时注意横轴对齐用np.arange(time_step, time_step len(train_pred))来画否则整条曲线会向左偏移一个窗口的长度看起来像是预测滞后真实值实际上是横轴标错了。5.4 现象四PyQt5 界面点击“训练模型”后窗口直接假死原因训练模型是耗时操作PyQt5 的主线程负责刷新界面。如果你在按钮回调里直接调model.fit主线程被阻塞窗口事件循环跑不动表现就是窗口未响应。解决把模型训练放到子线程用QThread承载训练函数训练结束后通过信号把结果传回 GUI。工程包里给了按钮回调的简化版但实际接入时一定要按下面这个结构改from PyQt5.QtCore import QThread, pyqtSignal class TrainThread(QThread): finished pyqtSignal(dict) def run(self): history model.fit(X_train, y_train, epochs50, verbose0) metrics {loss: history.history[loss][-1]} self.finished.emit(metrics)逻辑说明TrainThread继承QThread把model.fit放进重写的run方法主线程立刻返回不会阻塞 UI。finished信号携带训练结果主界面连接这个信号后可以更新标签或绘图。这里有个容易忽略的细节TensorFlow 在子线程里跑时模型和数据最好在子线程里重新加载不要直接引用主线程的 session否则偶发的不稳定错误会出现。6. 进阶用法多步预测与模型对比验证技巧工程包的默认实现是单步预测但实际业务里很少只预测下一个点更多是要预测未来 5 天、未来 30 天。滚动单步预测是常见做法把前一步的预测值作为下一步的输入逐步滚动下去。def recursive_forecast(model, last_window, n_steps, scaler): predictions [] current last_window.copy() for _ in range(n_steps): pred model.predict(current[np.newaxis, :, :], verbose0) predictions.append(pred[0, 0]) current np.roll(current, -1, axis0) current[-1, 0] pred[0, 0] return np.array(predictions).reshape(-1, 1)这个方法的缺点是误差会累加预测步数越长曲线越平滑甚至趋向一个常数值这是所有自回归递归模型的通病。如果你的应用要求高精度多步预测建议改多输出结构让最后一层 Dense 的单元数等于n_out损失函数用 MSE 即可训练目标变成一步输出未来多个时刻的值误差不会被递归放大。我最后讲一个实用的验证习惯任何 WOA 跑出来的最优模型都要和三个基线做对比第一个是历史均值预测未来全等于最近 N 步均值第二个是纯 LSTM第三个是单向 CNN-LSTM。对比指标不只看 RMSE还要看R2和方向准确率。如果 WOA-CNN-BiLSTM 打不过历史均值那说明数据本身可预测性太弱什么高级模型都白搭。from sklearn.metrics import r2_score baseline_pred np.full_like(y_test, np.mean(y_test)) r2_baseline r2_score(y_test, baseline_pred) r2_model r2_score(y_test, test_predict) print(fBaseline R2: {r2_baseline:.4f}, Model R2: {r2_model:.4f})对比的意义在于告诉你模型的价值边界。R2 很接近 0 甚至为负说明模型没有学习到有效规律这时候不要调整 WOA 参数而是回数据本身检查平稳性、周期性或特征工程。从那以后我每次做时序预测都强制走一遍这套流程先切片、固定种子、跑三个基线、再上 WOA最后用验证集三次重复确认指标波动范围才敢把结果汇报出去。时间序列预测没有标准答案希望这份工程化的代码能帮你的模型少翻几次车也希望你能在复现的过程中把它调成适合自己的形状。本文还有配套的精品资源点击获取