简介:面向需要开展时间序列预测、模型优化对比的科研与工程人员,代码包整合了SSA麻雀算法、KAN网络与Transformer的完整预测流程。模型采用麻雀搜索算法对KAN与Transformer的关键超参数进行寻优,支持在Python 3.9与TensorFlow 2.15环境中直接运行,并附带data.xlsx时间序列数据集,适合用于负荷、流量、股价等单变量或多变量序列的预测建模与算法验证。包内共9个文件,包含1个主程序.py、1个数据xlsx以及5个xml工程配置等,整体约405KB,结构紧凑,便于快速导入PyCharm等环境运行。已有118人学习下载;作者为博客专家认证的机器学习方向创作者,具备多年算法仿真经验,代码注释与运行框架相对规范,可作为入门KAN、Transformer组合优化及麻雀算法的参考实现。
1. 为什么把SSA、KAN和Transformer绑在同一个时间序列任务上
被超参数折磨过的人,大概率见过这种场景:同一个Transformer时间序列预测任务,手动把学习率从1e-4调到3e-4,验证集RMSE能差出百分之十几;窗口长度换一下,结果可能直接掉一个档次。SSA麻雀算法+KAN+Transformer时间序列预测这个组合,核心价值不是把三个热点名词堆在一起,而是把问题分层解决:Transformer负责捕获时间步之间的全局依赖,KAN用可学习基函数替换原本的固定激活层,增强非线性拟合能力,SSA在外面搜索最优超参数组合。这个方法适合已经能跑通基础预测流程、现在被精度和调参瓶颈卡住的工程师,而不只是给热点模型做加法。
2. SSA麻雀算法、KAN与Transformer各自的边界,和组合逻辑
在贴代码之前,先把三者各自的边界说清楚。很多人把这三个模型直接串成一个训练管线,但没想明白谁在优化哪个层级,最后训练不收敛都不知道是哪一个环节出了问题。
2.1 SSA的发现者-加入者-警戒者结构如何跳出局部最优
麻雀搜索算法以麻雀觅食行为为框架。种群里一部分个体是“发现者”,负责在大范围里搜索食物并给其他个体指引方向;其余是“加入者”,围绕发现者标记的较优区域继续迭代;同时保留少数“警戒者”,一旦感知到风险就改变位置,带动整个种群跳出当前位置。
跳出局部最优主要靠两个机制。第一,警戒者会向最差位置或最优位置两个方向移动,产生明显扰动;第二,排名靠后的加入者会直接放弃当前区域,在搜索空间里重新随机分布。这两点比粒子群、遗传算法那种“只往历史最优靠”的策略更不容易卡死。核心更新逻辑如下:
- 发现者:预警值 R2 小于安全阈值 ST 时,按指数衰减向原点收缩;否则直接加上一个随机扰动向更广区域搜索。
- 加入者:排名在后一半的个体会飞到远处重新找食物;排名靠前的则向当前最优发现者附近靠拢。
- 警戒者:适应度较差的飞向当前最优解,适应度较好的做一次步长扰动。
标准实现里 ST 通常取 0.8,R2 是每次随机生成的预警值,Q 是标准正态随机数。SSA 对这两个固定参数的敏感度不高,真正影响效果的是种群数量和迭代轮数,这个会在第 5 章给出经验范围。
2.2 KAN的可学习基函数,比MLP好在哪
KAN 源于柯尔莫哥洛夫-阿诺德表示定理:任何 n 元连续函数在理论上都能分解成若干一元函数叠加复合的形式。普通 MLP 的神经元里是“固定激活函数 + 可学习权重”,KAN 把这条反过来——权重变成一组可学习基函数的组合,激活的形状本身也参与训练。
工程落地有两种常见路线。一种是 B 样条基函数,更接近论文原意,曲线光滑、可以通过网格扩展逐步细化;另一种是径向基 RBF,实现简单、收敛稳定,在数据量不大时两者差距很小。RBF 版本更适合先把流程跑通,后续想提升精度再换 B 样条。
KAN 带来的收益通常体现为:同等参数量下非线性拟合能力更强,在高度非线性的时序数据上训练波动更小。但它不是白拿的,网格大小、基函数数量这些超参数要显式配置,这也是本方案交给 SSA 去自动搜索的原因之一。
2.3 Transformer在时间序列预测里看什么
Transformer 最关键的是多头自注意力机制,它让序列中任意两个位置之间可以直接计算依赖权重,而不是像 RNN 那样只能沿着时间方向逐步递推。时序数据里正好有两种依赖需要这种能力:一是相邻步的短期惯性,二是季节性或者业务周期造成的远距离关联。
Encoder-only 结构是时间序列预测里最常见的做法:输入一段固定长度的窗口,经过多头注意力和前馈网络后,取最后一个时间步的隐藏状态,接一个线性层映射到未来多个预测步。相比 Encoder-Decoder 结构,这种方式在单步预测和短预测长度时收敛更快,也更不容易出现误差累积。
2.4 协同点划清楚:SSA管外层搜索,KAN替换内层非线性
三者分工可以简化成一张表:
| 组件 | 作用层级 | 核心输入 | 输出 |
|---|---|---|---|
| SSA | 超参数空间 | 待搜索参数向量 | 一组最优超参数 |
| KAN | 模型内部非线性层 | 中间特征 | 增强非线性后的特征 |
| Transformer | 时间步间建模 | 窗口序列 | 最终预测值 |
这里有一个常见误区:SSA 不是用来替换梯度下降的,也不参与网络权重反向传播。Transformer 和 KAN 仍然用 AdamW 这类优化器训练,SSA 只在更高一层寻找“窗口长度、学习率、KAN 基函数数量”这些配置。如果把 SSA 嵌到权重更新里,搜索维度爆炸,完全没必要。
3. 手写一份Encoder-only Transformer时间序列预测的PyTorch基线代码
要验证 KAN 和 SSA 的贡献,必须先有一个能复现的 Transformer 基线。下面这套是工程上比较省时间的最小实现:数据用带趋势、周期和噪声的合成序列,读者可以无改动直接跑通;换成自己的 CSV 数据时,只需要替换数据加载这一段。
3.1 滑窗切分与标准化
时间序列切分必须按时间顺序,不能随机打乱。窗口按“输入前 48 步、预测后 12 步”来切,步长设为 1,让相邻窗口之间有重叠,相当于做了数据增强。
import numpy as np def make_synthetic_ts(n=1500, trend=0.02, period=24, noise=0.6): t = np.arange(n) ts = 10 + trend * t + 3 * np.sin(2 * np.pi * t / period) ts += np.sin(2 * np.pi * t / (period * 7)) ts += np.random.randn(n) * noise return ts.astype(np.float32) def create_sequences(ts, window_size=48, pred_len=12, step=1): X, y = [], [] for i in range(0, len(ts) - window_size - pred_len, step): X.append(ts[i : i + window_size]) y.append(ts[i + window_size : i + window_size + pred_len]) return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32) ts = make_synthetic_ts() X_train, y_train = create_sequences(ts[:1200]) X_val, y_val = create_sequences(ts[1200:]) mean, std = ts[:1200].mean(), ts[:1200].std() X_train = (X_train - mean) / std y_train = (y_train - mean) / std X_val = (X_val - mean) / std逻辑说明:create_sequences 按时间顺序滑动生成样本,每个样本用前 window_size 个点预测后 pred_len 个点。代码里特意把标准化统计量只在训练集上计算,验证集直接复用训练集的 mean 和 std,否则验证时等于提前用了未来信息,上线后结果会明显变差。
参数说明:trend 控制线性趋势幅度,period 控制主周期长度,noise 控制噪声强度。实际业务数据替换时,把 make_synthetic_ts 的返回值换成读入的 csv 列,同时保证 ts 是一维 float32 数组即可。
3.2 最小Transformer结构:位置编码与多头注意力
import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=2048): super().__init__() pe = torch.zeros(max_len, d_model) pos = torch.arange(0, max_len).unsqueeze(1).float() div = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(pos * div) pe[:, 1::2] = torch.cos(pos * div) self.register_buffer('pe', pe) # (max_len, d_model) def forward(self, x): return x + self.pe[:x.shape[1]] class TransformerTS(nn.Module): def __init__(self, window_size=48, pred_len=12, d_model=64, nhead=4, num_layers=2, d_ff=128, dropout=0.1): super().__init__() self.input_proj = nn.Linear(1, d_model) self.pos = PositionalEncoding(d_model) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=d_ff, dropout=dropout, batch_first=True) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers) self.head = nn.Linear(d_model, pred_len) def forward(self, x): # x: (batch, window_size, 1) h = self.input_proj(x) # (batch, window, d_model) h = self.pos(h) h = self.encoder(h) # (batch, window, d_model) return self.head(h[:, -1, :]) # (batch, pred_len)这段代码里,输入先经过一个线性投影把 1 维特征映射到 d_model 维,然后叠加位置编码,再进入 TransformerEncoder。取最后一个时间步的隐藏状态做预测,是因为在无掩码的 encoder 里,最后一个位置能通过注意力看到整段窗口的信息,同时保留了最新的时间语义。
参数说明:d_model 是嵌入维度,nhead 必须能整除 d_model;d_ff 是前馈网络的中间维度;batch_first=True 让张量形状是 (batch, seq_len, d_model),方便和 PyTorch 标准接口对齐。训练时用 AdamW,学习率 1e-3,weight_decay 设 1e-4,batch_size 取 64,训练 50 轮。
3.3 评估指标:RMSE、MAE和滚动预测的坑
def calc_rmse(pred, true): return np.sqrt(np.mean((pred - true) ** 2)) y_pred, y_true = [], [] model.eval() with torch.no_grad(): for i in range(X_val.shape[0]): xb = torch.from_numpy(X_val[i : i + 1]) pred = model(xb).squeeze(0).numpy() y_pred.append(pred) y_true.append(y_val[i]) y_pred = np.array(y_pred) y_true = np.array(y_true)逻辑说明:逐条滑动验证集窗口,记录模型在每个窗口上的 12 步预测结果。这样评估的 RMSE 更接近真实上线时的预测误差。
参数说明:RMSE 对异常值敏感,MAE 更直观,MAPE 在序列值接近 0 时不稳定。时间序列验证还有一个常见错误:把验证集整体随机打乱再做评估。时间序列是强时序依赖的,只要窗口内部包含了验证集后面的信息,结果就不可信。保留一段按原始顺序的验证集是底线。
4. 用KAN层替换Transformer前馈网络(FFN),两个改造点
TransformerEncoderLayer 内部本来是 Linear + GELU + Linear 的结构。我们要做的,是把这一整段换成带可学习基函数的 KAN 层。注意力部分保留不变,因为 KAN 替换的是非线性前馈部分,不是注意力计算。
4.1 KANLayer可运行实现:RBF基版本
先给出一份不依赖任何额外库、在 PyTorch 里直接能跑的 KAN 层:
import torch import torch.nn as nn class KANLayer(nn.Module): def __init__(self, in_dim, out_dim, num_basis=16, gamma=2.0): super().__init__() self.num_basis = num_basis self.gamma = gamma centers = torch.linspace(-1.0, 1.0, num_basis) self.register_buffer('centers', centers) # (num_basis,) self.coeffs = nn.Parameter( torch.zeros(in_dim, out_dim, num_basis)) # (in_dim, out_dim, num_basis) nn.init.normal_(self.coeffs, mean=0.0, std=0.1) def forward(self, x): # x: (batch, in_dim) d_sq = (x.unsqueeze(-1) - self.centers).pow(2) # (batch, in_dim, num_basis) phi = torch.exp(-self.gamma * d_sq) # RBF基函数值 return torch.einsum('bin,ion->bo', phi, self.coeffs)逻辑说明:KANLayer 先计算输入 x 到每个固定中心 c_i 的径向距离,得到一组高斯基函数值 phi;然后通过可学习的系数矩阵把基函数加权融合成输出。相当于每个输入输出连接上都有一条“可塑的激活曲线”,而不是用固定的 ReLU。
参数说明:num_basis 是基函数数量,控制拟合精细度;gamma 控制高斯核宽度,gamma 太大时每个基函数只影响很窄的区域,gamma 太小时所有基函数几乎重合。coeffs 的参数数量是 in_dim × out_dim × num_basis,比普通线性层的 in_dim × out_dim 多出 num_basis 倍,这是后面调参要重点控制的地方。
4.2 把Transformer的FFN替换成KAN
常见做法是双 KAN 层模拟原 FFN 的“升维再降维”结构:
class KANTransformerBlock(nn.Module): def __init__(self, d_model, num_basis=16, gamma=2.0, dropout=0.1, nhead=4, expansion=2): super().__init__() self.norm1 = nn.LayerNorm(d_model) self.attn = nn.MultiheadAttention(d_model, nhead, batch_first=True) self.norm2 = nn.LayerNorm(d_model) self.kan1 = KANLayer(d_model, d_model * expansion, num_basis, gamma) self.kan2 = KANLayer(d_model * expansion, d_model, num_basis, gamma) self.dropout = nn.Dropout(dropout) def forward(self, x): h = self.norm1(x) h = self.attn(h, h, h)[0] x = x + self.dropout(h) h2 = self.kan1(self.norm2(x)) h2 = torch.relu(h2) h2 = self.kan2(h2) return x + self.dropout(h2)逻辑说明:注意力层之后,原来的线性前馈层被替换成两层 KAN,中间插入一个 ReLU 提供非线性门控。这个结构与原版 FFN 最大的差异是:中间维度的每个神经元不再是一次线性变换,而是基函数组合后的结果,拟合复杂周期和突变模式时表现更灵活。
参数说明:expansion 对应原 FFN 的 dim_feedforward 倍数,设为 2 表示把中间特征维度放大到 d_model 的两倍。如果数据量很小,expansion 可以降到 1,直接用单层 KAN 替换,避免参数量爆炸。
4.3 改造后的参数量和训练行为变化
对比一个具体例子:d_model=64,num_basis=16,expansion=2 时:
- 原 FFN 参数:64×128 + 128×64 ≈ 16K
- 双 KAN 层参数:64×128×16 + 128×64×16 ≈ 262K,约是原来的 16 倍
参数量放大以后,模型拟合能力上升,但过拟合风险同步上升。这个阶段我一般会把 weight_decay 从 1e-4 提高到 1e-3,并把 dropout 从 0.1 调到 0.2。如果沿用 FFN 时代的超参数,通常会先看到训练 loss 降得很快的假象,但验证 loss 迟迟不下,这是 KAN 集成后最容易踩的坑。
5. 用SSA麻雀算法为Transformer+KAN调参
KAN 引入后多出了 num_basis 和 gamma 两个超参数,加上原本的窗口长度、学习率、层数、dropout,人工调节已经很难兼顾。此时用 SSA 做外层自动化搜索是合理选择。
5.1 SSA主循环的Python实现
以下是一份可以直接复用的 SSA 实现,只依赖 numpy,模型训练部分仍用 PyTorch:
import numpy as np def ssa_optimize(fitness_fn, bounds, dim, pop_size=8, max_iter=15, ST=0.8, PD=0.2, SD=0.1): lb = np.array([b[0] for b in bounds]) ub = np.array([b[1] for b in bounds]) X = np.random.uniform(lb, ub, (pop_size, dim)) f = np.array([fitness_fn(x) for x in X]) best_x = X[int(np.argmin(f))].copy() best_f = f.min() n_pd = max(int(pop_size * PD), 1) n_sd = max(int(pop_size * SD), 1) for t in range(max_iter): order = np.argsort(f) worst = order[-1] best = order[0] # 发现者更新 for pos, idx in enumerate(order[:n_pd], start=1): alpha = np.random.uniform() if np.random.rand() < ST: X[idx] *= np.exp(-pos / (alpha * max_iter + 1e-10)) else: X[idx] += np.random.randn(dim) # 加入者更新 for pos, idx in enumerate(order[n_pd:], start=n_pd): if pos > pop_size / 2: X[idx] = np.random.randn(dim) * np.exp( (X[worst] - X[idx]) / (pos ** 2 + 1e-10)) else: A = np.random.choice([-1, 1], size=(dim, dim)) A_plus = A.T @ np.linalg.inv(A @ A.T) # dim较小时可逆 X[idx] = X[best] + np.abs(X[idx] - X[best]) @ A_plus # 警戒者更新 for idx in np.random.choice(pop_size, n_sd, replace=False): beta = np.random.randn() if f[idx] > best_f: X[idx] = X[best] + beta * np.abs(X[idx] - X[best]) else: k = np.random.uniform(-1, 1) X[idx] += k * (np.abs(X[idx] - X[worst]) / (f[idx] - f[worst] + 1e-8)) X = np.clip(X, lb, ub) f = np.array([fitness_fn(x) for x in X]) if f.min() < best_f: best_f = f.min() best_x = X[int(np.argmin(f))].copy() return best_x, best_f逻辑说明:整个循环分成三块。发现者根据预警值决定是局部收缩还是全局随机扰动,负责探索新区域;加入者分两类,排名靠前的向最优解靠拢,排名靠后的跳出局部重新分布;警戒者随机选取并通过自身适应度判断飞向最优点还是产生扰动。每次迭代结束后统一做边界裁剪,然后重新计算适应度。
参数说明:pop_size 是麻雀数量,建议 8 到 20。取值不能太大,因为每只麻雀都要完整训练一次模型,种群翻倍训练时间也翻倍。max_iter 是 SSA 迭代轮数,15 到 30 比较常见。ST 取 0.8,决定发现者进入报警模式的概率阈值。dim 是要优化的超参数个数,不要超过 6 个。
5.2 待优化超参数与取值范围
不是所有超参数都值得交给 SSA,像 nhead 和 batch_size 这种固定影响小的参数可以写死。推荐把以下 6 个参数送进 SSA:
| 参数 | 连续范围 | 解码方式 |
|---|---|---|
| log_lr | [-9, -4] | 10 的该次方作为学习率 |
| window_size | [16, 120] | 四舍五入取整 |
| d_model | [32, 192] | 取整到 8 的倍数 |
| num_layers | [1, 3] | 四舍五入取整 |
| num_basis | [8, 32] | 四舍五入取整 |
| dropout | [0.05, 0.4] | 线性映射 |
学习率用 log 范围而不是线性范围,是因为学习率跨数量级变化时对训练影响最大,线性搜索浪费大量样本在无用区间。d_model 取 8 的倍数可以保证注意力头整除。
5.3 适应度函数设计
适应度函数决定 SSA 向哪个方向搜索。这里用验证集 RMSE 作为适应度,越小越好:
def fitness_fn(x): log_lr, ws, dm, nl, nb, dp = x config = { 'window_size': int(round(ws)), 'd_model': int(round(dm / 8) * 8), 'num_layers': int(round(nl)), 'num_basis': int(round(nb)), 'dropout': np.clip(dp, 0.05, 0.4), 'lr': 10 ** np.clip(log_lr, -9, -4), } return quick_train_evaluate(config, max_epochs=10) def quick_train_evaluate(config, max_epochs=10, eval_size=200): model = build_transformer_kan(config) # 组装KAN Transformer opt = torch.optim.AdamW(model.parameters(), lr=config['lr'], weight_decay=1e-4) for epoch in range(max_epochs): for xb, yb in train_loader: opt.zero_grad() loss = nn.MSELoss()(model(xb), yb) loss.backward() opt.step() val_rmse = calc_rmse_on_tail(model, X_val, y_val, eval_size) # 取尾部200个窗口 return val_rmse逻辑说明:quick_train_evaluate 只做 10 轮快速训练,然后取验证集末尾 200 个窗口评估。这样单次适应度计算耗时可控,SSA 每轮 8 个个体、15 次迭代就是 1200 次快速训练,在单卡 GPU 上大约几十分钟到两个小时。
参数说明:max_epochs 设太小会得到噪声极大的适应度,导致搜索方向不稳定;设太大则总时间不可接受。10 到 12 轮是比较平衡的选择。最终拿到 best_x 后,再用完整训练轮数精训一次,结果才作为最终上报值。
5.4 SSA迭代监控与收敛判断
在 ssa_optimize 的外层记录每轮 best_f,可以观察收敛过程。前 3 次迭代 best_f 下降不明显是正常的,因为初始种群分散,适应度普遍较差。如果 10 轮之后 still 没有变化,优先检查 fitness_fn 是否返回了稳定值;最简单的方式是先固定同一组参数连续跑三次 fitness_fn,如果三次 RMSE 波动超过 5%,说明快速评估的 epoch 数太少,要往上加。
另外一点:种群里所有麻雀共享同一个随机种子初始化模型并不合适,建议每只麻雀用不同的 seed,保证初始模型有足够的多样性,否则 SSA 搜索到的“最优”其实只对某一个模型初始化有效。
6. 验证KAN与SSA作用的三个方法,和三个调优细节
6.1 账要按三组对照算
组合模型是否真的有效,至少要跑三组实验:纯 Transformer 基线、Transformer + KAN 但不用 SSA(超参数与基线一致)、Transformer + KAN + SSA 完整方案。只贴完整方案的结果无法拆解贡献。
实际项目里常见的结论是:KAN 单独替换后提升有限,甚至因为参数量增加而过拟合,完整方案反而掉点;加上 SSA 搜索之后才看到明显改善。这说明 KAN 把超参空间变宽了,人工调参难度上升,SSA 的价值在此时才真正体现。
6.2 参数敏感性怎么快速评估
保持 SSA 搜索到的最优配置,每次只改一个参数,重新训练并记录验证 RMSE。按经验,以下三个参数的敏感性最高:
- num_basis:从 16 改到 8 时,参数量减半,过拟合缓解,验证 RMSE 往往先下降;改到 32 时训练时间和显存明显上升。
- log_lr:跨越一个数量级就会导致训练发散,或者收敛速度慢一半以上。
- window_size:比最优值少一半以后,对长周期成分的预测误差显著增加。
6.3 三个能直接用上的调优细节
第一,固定随机种子。PyTorch、numpy、random 三个库都要设置。SSA 每次执行都会因模型初始化不同产生微小方差,只有固定住种子,不同配置之间的对比才有意义。
第二,进入 KANLayer 前的输入要落在 [-1, 1] 附近。RBF 中心是在 -1 到 1 之间均匀分布的,如果标准化不彻底,部分输入会落到所有中心的最外侧,梯度接近 0,这些维度基本学不动。在 KANLayer 之前加 LayerNorm 或 Tanh 压缩一下会更稳。
第三,KAN 层不一定要替换所有 Transformer 层。窗口较短、数据量有限时,只把最后一层的前馈网络换成 KAN,前面层保留原始 FFN,效果往往更好,训练时间能省 30% 到 40%。这里建议用“SSA 只优化 num_basis,是否全层替换固定为 False”的配置先试,再逐步放开。
验证最终模型时,除了看 RMSE,还要看最后一个预测时段的残差曲线里是否还有明显的周期性成分。如果残差还残留周期性,说明模型没有把对应频率成分学进去,此时优先检查 window_size 是否覆盖了至少两个完整周期。
本文还有配套的精品资源,点击获取