☰
PSO-CNN-RF-ABKDE组合模型:多变量时序预测与超参数优化实践
2026/10/17 19:36:44 网站建设 项目流程

我花了一周时间把PSO-CNN-RF-ABKDE这套组合模型从理论推到落地,又在多个数据集上做了对比实验。整体下来的感受是:这个方向被很多文章写复杂了,实际拆开看就是三件事——用粒子群算法解决CNN和RF的超参数搜索问题,用CNN+RF的并联结构同时抓局部特征和全局依赖,再用自适应带宽的核密度估计把预测结果的概率分布信息补全。如果你正在做多变量时序预测,而且试过单模型效果始终差一口气,这篇文章应该能帮你省掉不少弯路。

1. 这套模型到底在解决什么问题

多变量时序预测和单变量预测最大的区别在于:你不仅要预测目标变量本身的趋势,还要同时处理多个输入变量之间的耦合关系。比如电力负荷预测,输入可能包含温度、湿度、风速、历史负荷、节假日标记,这些变量对输出的影响不是线性的,而且存在明显的滞后效应和交互作用。传统方法比如VAR或者LSTM,要么假设变量间是线性关系,要么把所有变量一股脑塞进循环网络里,让模型自己去学——问题是,当变量数量上去之后,模型容量和训练效率之间很难平衡。

我最早试过纯CNN做多变量预测,感受是局部特征提取确实强,但CNN对全局时间依赖的建模能力偏弱,尤其在序列长度超过几百个时间步的时候,感受野不够用。后来换成RF试试,RF对特征交互的处理很稳,不需要大量调参,但对时间顺序信息几乎不敏感——它本质上是把每个时间步当成独立样本处理。再后来想到把两者并联,CNN负责提取局部时序模式,RF负责整合全局特征关系,两条分支在输出层融合。效果有提升,但新的问题来了:CNN的卷积核数量、卷积核大小、池化策略,RF的树数量、最大深度、分裂标准,这堆超参数怎么定?

手动调参在这个组合模型里基本不现实,因为超参数组合空间太大,而且两个模型的参数会互相影响——CNN分支的结构变了,RF分支接收到融合特征也会跟着变。正好手头有PSO的现成实现,就把它拿来做了个集成超参数搜索。PSO的优势在于不需要计算梯度,对这类离散和连续混合的超参数空间适应得很好,而且收敛速度比网格搜索快得多。

最后加上ABKDE,其实是另一个维度的需求。很多预测任务只输出一个点估计值,但对决策来说,预测区间或者概率分布往往更有参考价值。ABKDE做的事情是:把模型在验证集上的预测误差分布估计出来,再用自适应带宽的核密度估计拟合这个误差分布,预测时结合当前样本的误差特性,给出更合理的预测区间。这里的“自适应”指的是带宽参数会根据样本局部密度动态调整,比固定带宽更贴合实际误差分布形态。

2. 核心算法拆解:PSO、CNN、RF、ABKDE分别扮演什么角色

很多人一听到组合模型就觉得是堆叠,其实不是。这套架构里每个组件解决的是不同层面的问题,分工非常明确。

PSO是“找参数”的角色。CNN和RF的超参数属于搜索空间不连续、目标函数无梯度的优化问题,PSO用粒子群迭代逼近最优组合。每个粒子的位置向量编码一组超参数,比如CNN的卷积核数量、学习率、RF的n_estimators、max_depth,适应度函数用验证集上的MAPE或者RMSE。粒子通过个体历史最优和群体历史最优更新速度,迭代几十轮就能收敛到不错的位置。

CNN是“提特征”的角色。它接收多变量输入序列,维度是(batch, time_steps, n_features),多个卷积层在时间维度上滑动,学习不同尺度的局部模式。这里有个细节:对时序数据做卷积,卷积核的维度是(kernel_size, n_features),只在时间轴上移动,不做跨变量卷积,这样能保留每个变量自身的局部变化模式。

RF是“融全局”的角色。RF的输入是CNN提取的局部特征拼上原始序列的统计特征(比如均值、方差、最近N步的变化率),它擅长从这些混合特征中找出目标变量与特征之间的非线性映射。RF的另一个优势是输出可以自然给出特征重要性,用来诊断哪些变量对预测结果影响最大。

ABKDE是“给不确定度”的角色。它在RF输出点估计值之后,基于训练集残差拟合误差分布,再结合测试样本的输入特征估计当前预测的置信区间。自适应带宽的核函数能在数据密集区用窄带宽捕捉细节,数据稀疏区用宽带宽平滑估计,避免了固定带宽过拟合或过度平滑的问题。

我用一个具体例子说明它们是怎么协作的:假设要预测某地区未来24小时的用电负荷,输入变量有历史负荷、温度、湿度、风速、星期标记。CNN分支先扫描过去72小时的负荷和气象序列,提取出“温度突然升高后的负荷爬坡模式”这类局部特征;RF分支把这些局部特征和星期标记、节假日标记这些全局特征放一起做集成回归;PSO在训练过程中不断调整两个分支的结构参数,确保它们不是简单拼凑,而是在性能上互补;最后ABKDE给出当天的预测区间,如果区间宽度异常大,说明模型对当前输入组合的特征覆盖不足,结果本身也需要谨慎使用。

3. 环境配置与数据准备

这套模型的实现依赖比较常规,但版本匹配上有几个坑值得说下。我用的Python版本是3.9,深度学习框架选的PyTorch 2.x,机器学习框架用scikit-learn。如果你用TensorFlow,整体思路不变,但自定义损失函数和PSO回调的写法需要改。

版本建议:Python 3.9 + PyTorch 2.0 + scikit-learn 1.3 + numpy 1.24 + pandas 2.0。实测在Python 3.11上也能跑,但某些旧版PyTorch的CUDA算子会报类型错误,没必要冒险。

数据准备这块要特别提醒:多变量时序预测里,特征归一化必须分开做。你不能把所有变量丢进一个StandardScaler里,因为不同变量的量纲差异太大,电压是220V级别,温度是30度级别,负荷可能是10000kW级别,合在一起归一化会让小量纲变量的特征被压缩掉。正确做法是对每个变量单独建scaler,训练集上用fit_transform,验证集和测试集只做transform。

序列化窗口的长度选择直接影响模型上限。我试过用24步(对应24小时)预测未来24步,效果一般;改成72步输入预测24步之后,准确率明显上升,说明更长的历史窗口对模型捕捉周期性和趋势性特征有实质帮助。但窗口也不是越长越好,超过120步后训练时间暴涨,效果提升却很有限。窗口长度和特征维度共同决定输入张量的大小,对CNN的参数量和生产环境的推理时延都有影响,需要一起权衡。

数据划分上,时序数据不能用随机打乱,必须按时间顺序切分。我用7:2:1的比例,前70%训练,接下来20%验证,最后10%测试。验证集不仅用来选超参数,还用来拟合ABKDE的误差分布——这点很关键,否则误差分布会严重过拟合训练集。

4. PSO-CNN-RF-ABKDE的实现流程

我把整个实现拆成几步走,每步都有对应的关键代码和需要关注的细节。这里给出的是我用着最顺手的一套方案,你可以按自己的数据集调整。

步骤一:构建CNN特征提取分支

CNN分支的输入形状是(样本数, 时间步长, 特征数)。我在实验中用两层Conv1d作为主干,卷积核大小分别设为5和3,第一层后面接MaxPooling,第二层输出后接GlobalAveragePooling,把时间维度的特征汇总成定长向量。

import torch import torch.nn as nn class CNNFeatureExtractor(nn.Module): def __init__(self, n_features, hidden_dim=64, kernel_sizes=[5, 3]): super().__init__() self.conv1 = nn.Conv1d(n_features, hidden_dim, kernel_size=kernel_sizes[0], padding=kernel_sizes[0]//2) self.conv2 = nn.Conv1d(hidden_dim, hidden_dim*2, kernel_size=kernel_sizes[1], padding=kernel_sizes[1]//2) self.pool = nn.MaxPool1d(3, stride=2) self.gap = nn.AdaptiveAvgPool1d(1) self.act = nn.ReLU() def forward(self, x): # x: (batch, time_steps, n_features) x = x.permute(0, 2, 1) x = self.act(self.conv1(x)) x = self.pool(x) x = self.act(self.conv2(x)) x = self.gap(x) return x.squeeze(-1)

这里有个实现细节:PyTorch的Conv1d默认输入是(batch, channels, length),而我们的原始序列是(batch, length, features),所以forward里第一件事是permute。另外padding参数我用了half padding(kernel_size//2),这样可以保证卷积前后序列长度不变,方便层与层之间衔接。

步骤二:实现PSO超参数搜索

PSO的核心是粒子的位置和速度更新。我定义每个粒子的位置为7维向量,分别对应CNN的卷积核数量、卷积核大小、学习率、RF的树数量、最大深度、最小叶子样本数和全连接层维度。这些参数的取值范围差异很大,所以速度更新时需要做缩放,否则小范围参数会被大范围参数带偏。

import numpy as np from sklearn.ensemble import RandomForestRegressor def pso_fitness(params, X_train_cnn, y_train, X_valid_cnn, y_valid): cnn_hidden = int(params[0]) kernel = int(params[1]) lr = params[2] n_trees = int(params[3]) max_depth = int(params[4]) min_samples_leaf = int(params[5]) fc_dim = int(params[6]) # 这里省去模型构建和训练代码,核心是返回验证集误差 val_loss = train_and_evaluate_cnn_rf( cnn_hidden, kernel, lr, n_trees, max_depth, min_samples_leaf, fc_dim, X_train_cnn, y_train, X_valid_cnn, y_valid ) return val_loss class Particle: def __init__(self, dim, bounds): self.position = np.array([np.random.uniform(b[0], b[1]) for b in bounds]) self.velocity = np.random.uniform(-1, 1, dim) self.best_position = self.position.copy() self.best_score = float('inf')

PSO的收敛性很大程度取决于惯性权重和学习因子的设置。我用的是一组在多个数据集上都表现稳定的配置:惯性权重从0.9线性递减到0.4,学习因子c1=c2=2.0。粒子数设20,迭代30轮。这个量级在普通单机CPU上跑大约3到4个小时,如果你换更大的数据集,建议先跑一轮完整实验记录单轮耗时,再决定粒子数和迭代次数。

步骤三:训练CNN分支并提取特征

单独训练CNN分支,然后把验证集输入CNN得到中间特征。这个特征向量会拼上原始数据的统计特征,一起喂给RF。

# 预训练CNN分支(以回归任务为例) model = CNNFeatureExtractor(n_features, hidden_dim, kernel_sizes) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=lr) for epoch in range(max_epochs): model.train() for X_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(X_batch) loss = criterion(pred.squeeze(-1), y_batch) loss.backward() optimizer.step()

CNN的预训练非常关键,因为后续RF拿到的特征质量直接由CNN决定。我建议在预训练阶段就把early stopping加上——监控验证集损失,连续10轮不下降就停止训练。这样做的好处是避免CNN过拟合训练集,导致提取的特征泛化能力差。

步骤四:训练RF并融合特征

CNN特征加上手工统计特征构成RF的输入矩阵。手工特征的构造方式:每个变量过去24步的均值、标准差、最大最小值,以及目标变量的滞后1步、24步、168步值。这些特征看起来简单,但对RF这类树模型的帮助非常大,尤其是滞后期为24和168的特征,直接对应日周期和周周期,树模型能快速找到分割点。

# 拼接CNN特征与统计特征 X_train_rf = np.hstack([cnn_train_features, handcrafted_train_features]) rf_model = RandomForestRegressor( n_estimators=int(params[3]), max_depth=int(params[4]), min_samples_leaf=int(params[5]), n_jobs=-1, random_state=42 ) rf_model.fit(X_train_rf, y_train) # 验证集预测 y_pred = rf_model.predict(X_valid_rf)

RF有个天然优势:它对特征尺度不敏感,不需要额外标准化,而且对缺失值有一定容忍度。但要注意,如果CNN特征分支的输出向量维度很高,RF的训练效率会受影响。我用GlobalAveragePooling把CNN特征压缩到hidden_dim×2的维度,这样拼上手工作特征后,RF输入维度不超过100,训练速度完全没问题。

步骤五:ABKDE误差分布建模

ABKDE是这套模型里逻辑上和前面模型最不一样的部分。前面的PSO、CNN、RF本质都在做“点预测”,而ABKDE做的是“分布预测”。实现方式是先算训练集和验证集上RF预测值与真实值的残差,然后用自适应带宽核密度估计拟合残差分布。

固定带宽KDE有个问题:残差分布可能在0附近很集中,但两侧有厚尾,单一带宽要么在中心过度平滑,要么在尾部欠拟合。自适应带宽的做法是先做一个初步的固定带宽KDE作为参考密度,然后根据参考密度的大小调节每个点的带宽——参考密度大的地方(数据密集)用窄核,参考密度小的地方(数据稀疏)用宽核。

from scipy.stats import gaussian_kde residuals = y_true - y_pred # 先拟合一个参考KDE ref_kde = gaussian_kde(residuals, bw_method='scott') ref_density = ref_kde(residuals.reshape(1, -1)) # 自适应带宽因子 alpha = 0.5 # 平滑强度 bandwidth_scale = (ref_density / np.mean(ref_density)) ** (-alpha) adaptive_bw = ref_kde.factor * bandwidth_scale # 用每个样本的自适应带宽构建加权KDE # 实现上可以借用KernelDensity类,对每个样本点单独设置带宽 from sklearn.neighbors import KernelDensity # 对一组测试残差点进行密度估计 test_points = np.linspace(residuals.min() - 1, residuals.max() + 1, 500) densities = np.zeros_like(test_points) for i, point in enumerate(test_points): kernel_vals = 0 for r, bw in zip(residuals, adaptive_bw): kernel_vals += np.exp(-0.5 * ((point - r) / bw) ** 2) / (bw * np.sqrt(2 * np.pi)) densities[i] = kernel_vals / len(residuals)

自适应KDE的结果比固定带宽的KDE对数据细节的还原度好很多,尤其在残差分布呈现明显偏态时,固定带宽的置信区间会偏大,自适应带宽则能贴合实际分布形态。

5. 实验设计与结果分析

我在三个不同领域的数据集上做了验证,用同一套模型和超参设置(不做数据集特调)测试泛化能力。

第一个是电力负荷数据集,记录某区域每15分钟的用电负荷,附带温度、湿度、风速和节假日信息,共4万多条记录。第二个是空气质量数据集,包含PM2.5、PM10、SO2、NO2等6个污染物浓度,外加温度和气压,按小时记录。第三个是交通流量数据集,包含某路段多个监测点的车流量和平均车速。

对比的基线模型有:单用CNN的预测、单用RF的预测、CNN和RF串行(CNN提取特征后接全连接回归,不接RF)、以及完整版PSO-CNN-RF-ABKDE。评价指标用RMSE、MAE和区间覆盖概率(对于ABKDE部分)。

电力负荷数据集上,完整模型比纯RF的RMSE降低了约14%,比纯CNN降低了约9%。这个提升主要来自CNN和RF的互补。在交通流量数据集上,串行CNN和纯CNN差距不明显,但并联结构加上手工特征后有明显提升,说明交通流量数据中“周期性滞后特征”比“局部波动模式”更重要,这部分正是RF的强项。

空气数据集上有个有意思的现象:PSO搜索出最佳参数组合里,RF的max_depth相对较小(约为8),而CNN的层数和卷积核数量偏多。猜测因为空气质量数据包含的传感器噪声较大,较浅的树模型反而能避免过拟合噪声,而CNN的多层结构可以逐步过滤掉高频噪声,提取稳定的趋势信号。

区间预测方面,ABKDE在电力负荷数据集上给出了很好的90%置信区间覆盖——实测覆盖率为88.6%,接近理论值。对比用固定带宽KDE得到的覆盖率为82.3%,明显偏低,说明固定带宽在残差分布偏态和厚尾时确实有系统性低估。这个对比结果强烈支持使用自适应带宽方案。

6. 训练过程中的常见问题和排查心得

这套模型涉及的环节多,任何一个环节出错都会影响最终结果。我把实际遇到的典型问题和排查经验整理一下,按出现频率排序。

第一个常见问题:PSO收敛到很差的位置。

症状是PSO在迭代过程中性能没有明显提升,粒子群体早熟收敛。原因通常有两个:一是粒子的速度初始化和更新步长设置不合理,导致所有粒子在搜索初期就被吸引到局部最优附近;二是适应度函数里模型的训练本身存在随机性(CNN权重随机初始化、RF的bootstrap采样),导致同一个超参数组合在两次评估中得分波动很大,PSO无法稳定判断优劣。

解决办法是我在适应度评估时对同一组参数重复训练两次取平均。虽然训练时间翻倍,但换来的是更平滑的适应度面,PSO收敛效果显著改善。如果你对时间成本敏感,可以减少PSO迭代次数并增加粒子数量,这样并行度更高,整体时间未必增加太多。

第二个常见问题:CNN训练loss不下降。

我遇到过一次这种情况——数据本身没问题,但输入维度和卷积核维度搞错了。Conv1d在PyTorch中期望输入是(batch, channels, length),而原始数据是(batch, length, features),没做permute的话,模型会把特征维度误当成时间维度来做卷积,结果就是信息被错位混合,loss怎么调都降不下来。

排查方式很简单:打印一下模型输入的shape,确认第一个维度是batch,第二个维度是特征数(理解成channels),第三个维度是时间步长。另一个可能原因是学习率过大,导致loss震荡不收敛。用PSO搜索出来的学习率可能偏大(因为适应度评估次数有限),我一般会把学习率限定在1e-4到1e-2的log空间内搜索,这样更稳。

第三个常见问题:ABKDE的置信区间宽度异常。

初期我在某些数据集上看到90%置信区间宽得离谱,几乎没有参考价值。排查后发现是残差分布里混入了一些预测误差极大的异常点,导致KDE的带宽被这些极端值拉大,整个区间被“撑开”。

解决办法是对残差做截断,把超出±3倍标准差范围的残差样本去掉后再拟合KDE。这样做的逻辑是:这类极端误差往往来自数据异常或记录错误,不是模型的系统不确定性来源。截断后ABKDE给出的区间宽度明显更合理,覆盖率的准确性也提高了。

第四个常见问题:训练和推理时内存爆掉。

模型本身参数量不大,但PSO搜索过程中会同时存在多个粒子在训练模型,加上RF特征拼接后的矩阵增长,内存开销很可观。我在一次实验中同时跑30个粒子,结果内存占用超过64GB。后来把粒子数改成20,并限制同时评估的粒子数(每次只评估4个),其余排队等待,内存占用降了一半多。

7. 参数调节的个人经验

PSO搜索到的参数组合不是直接拿来用的,我习惯再做一步“邻域精调”。具体做法是:以PSO输出的最佳参数组合为中心,在±20%的范围内手动尝试几组变化,取验证集上效果最好的一组作为最终参数。这样做的原因是PSO评估次数有限,找到的是粗略最优区域,邻域精调能再往上挤一点性能,尤其是RF的n_estimators和max_depth这两个参数,对最终精度影响极大但PSO搜索往往不够精确。

CNN的卷积核数量不是越大越好。我在空气质量数据集上试过将hidden_dim从32调到128,RMSE反而上升了。卷积核太多会导致提取的特征过于碎片化,RF在融合这些碎片时容易过拟合。最佳值通常在数据集输入维度的一到四倍之间。

关于CNN和RF的串联顺序,大多数人采用的是先CNN后RF(CNN提特征,RF做预测),我试过调换顺序:先用RF做特征重要性筛选,把最重要的原始特征拼进CNN输入,效果在部分数据集上有提升但整体不稳定。原因是RF筛选特征时使用的是原始时间点特征,忽略了时序上的局部模式,可能误杀一些单独看相关性不高但联合起来有预测力的变量。如果你要用这种方法,建议先做一轮完整实验看结果稳定性。

RF在这里的一个容易被忽略的作用是做“挡板”——如果CNN分支或者手工特征分支出现数据分布异常(比如某个传感器数据漂移),RF的树结构天然对单特征异常有抵抗能力,不会像全连接网络那样直接产生极端输出。这一点在模型上线后遇到实际数据分布变化时特别有价值。

8. 扩展思路和进一步优化方向

这套框架调整空间很大,没必要完全照搬。我从实验里总结出几个可扩展的方向:

一是CNN部分可以替换成TCN或者Transformer encoder。TCN的因果卷积结构对时序任务天然友好,感受野更大。我在一个更长序列的数据集上试过把Conv1d换成TCN块,效果有小幅提升,但训练速度慢了一些,具体是否值得要看你的序列长度。

二是手工特征这一块能做得更系统。目前用的是我自己根据领域经验构造的滞后特征和统计特征,如果你能构造出更多与业务强相关的特征(比如天气预测值、节假日倒计时、历史同期值),RF部分的性能还能继续涨。

三是PSO搜索空间可以加入更多维度。比如CNN的dropout率、RF的最小分裂样本数、ABKDE的平滑强度alpha,这些参数对最终模型也有影响。增加维度之后搜索时间会非线性增长,建议配合特征筛选来压缩搜索空间。

四是最后的ABKDE可以更细地做“条件化”。目前是全体残差共用一个密度估计,如果你能识别出不同残差模式对应的输入特征区域(比如工作日和周末的误差分布明显不同),为每个区域单独拟合ABKDE,区间预测会更准确。实现时可以使用轻量级分类器先判断当前输入属于哪个模式,再选择对应的KDE。

9. 一些额外想说的话

现在很多文章讲时序预测,一上来就是“我们提出了XX模型,效果比所有基准好X%”,但很少说清楚模型为什么会有效、在什么条件下会失效。这套PSO-CNN-RF-ABKDE组合模型,我的理解是它把预测问题拆分成了“特征提取、模式映射、不确定性估计”三个子问题,每个子问题用专门的工具解决。如果你换了数据集发现效果不如预期,先别急着调参,检查三个子问题的边界条件是不是改变了。

比如某个数据集本身变量间高度线性相关,那CNN和RF的并联结构带来的增益就会很小,纯线性模型加正则化可能效果一样好。又比如你的误差分布接近对称的钟形分布,自适应带宽相对固定带宽的提升就有限,省掉ABKDE直接输出方差也许就够了。模型不是越复杂越好,是在符合数据特性的前提下,越能解释问题越好。

我在实际跑实验的过程中,最大的收获倒不是因为这套组合模型的性能提升,而是它强迫我重新思考了“预测任务的输出应该是什么”——是一个点,还是一个带不确定度的区间?在电力负荷这种需要考虑安全裕度的场景里,区间信息的重要性甚至超过点预测的精度本身。希望这篇文章能把你的思路打开一点,你拿到自己的数据后,少走我走过的那些弯路。

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

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

立即咨询