做时间序列预测最让人头疼的往往不是模型本体的选型,而是那些“看起来差距不大、跑起来天差地别”的超参数。回声状态网络(ESN)就是这类问题的典型代表:它通过一个随机固定的储备池把输入映射到高维状态空间,绕开了传统递归神经网络最难的梯度训练环节,但储备池规模和学习率这两个旋钮只要拧偏一点,预测结果就能从“还能看”变成“没法看”。我过去手动调这两个参数,经常一调就是一下午,直到把麻雀搜索算法(SSA)接进去,让优化器替我在“储备池规模-学习率”组成的二维搜索空间里自动寻优,才真正把ESN从玩具变成可复现的时序预测工具。
这就是这篇博文要讲的SSA-ESN:用麻雀搜索算法去优化回声状态网络的储备池规模和学习率,完成时间序列预测。它适合正在做时序预测、搞负荷/流量/销量预测的朋友,也适合想了解元启发式算法如何与神经网络超参数优化结合的同学。文章不会只给结论,而是把原理、代码、实验结果和踩过的坑都摊开讲。
1. 回声状态网络的两个关键旋钮:储备池规模与学习率为什么难调
1.1 储备池规模决定“记忆力”的边界
回声状态网络的核心思想很反直觉:输入权重和储备池内部连接权重都是随机初始化的,初始化之后就不动了,整个网络只训练一个从高维状态到输出的线性映射。储备池相当于一个把输入信号反复“搅拌”的非线性状态处理器,输入的过去信息通过储备池内部的循环连接被暂时保留下来,形成一种短时记忆。基于这种记忆,输出层才能根据当前和过去的状态去拟合未来。
储备池规模,也就是状态向量的维数,直接决定这个短期记忆的容量。维数太小,比如只有几十个,状态空间能表达的非线性模式非常有限,长周期依赖基本记不住,表现就是欠拟合。维数太大,首先是计算和内存开销上涨,其次是过拟合风险增加——输出层能把训练集背下来,但对验证集的泛化能力反而变差。更麻烦的是,储备池内部连接是随机生成的,维数越大,出现不稳定模式、状态激活值饱和的概率也越大。
很多同学以为“储备池越大越好”,实际不是这么回事。我观察过大量时序数据下的实验结果:储备池规模往往存在一个“甜点区间”,太左太右都会让误差明显上升。这个甜点位置跟序列长度、非线性的复杂程度、输入维数都有关系,没有任何一个固定值能通吃所有任务。
1.2 学习率在ESN里到底管什么
这里要先澄清一个概念。ESN的输出层权重通常可以用伪逆或岭回归一步解出来,这种离线解法里并不存在“学习率”这个参数。但当你改用梯度下降或者在线增量方式训练输出层时,学习率就变得非常关键。本文讨论的SSA-ESN采用批梯度下降训练输出层,所以学习率的含义就是输出层权重每次更新的步长。
学习率设小了,输出层收敛很慢,有限的训练轮次内根本达不到好的拟合效果;设大了,损失函数来回震荡,甚至直接发散,训练过程彻底失控。我在实验里见过最夸张的情况:学习率从0.01调到0.05,测试集RMSE直接从0.02左右恶化到0.08以上,这个敏感度远超普通人直觉。这里注意,如果看文献时看到“泄漏率”或者“衰减率”,那就不是同一个东西了——那是储备池状态更新方程里对历史状态衰减速度的控制参数,别跟输出层学习率搞混。
1.3 手动调参为什么是一场灾难
ESN虽然有“省去RNN梯度训练”的优势,可一旦储备池规模、学习率这类参数需要人工调优,优势就打了折扣。理论上可以用网格搜索,但储备池规模是整数、学习率跨数量级,网格一密计算量爆炸,一疏又容易漏掉最优区域。随机搜索比网格好一点,但本质还是一多半靠运气。
我最早做这套方案的时候,先用固定的一组参数跑,效果凑合但总差一口气。直觉告诉我“N设大一点、lr设小一点可能更好”,结果改完反而更差。连续折腾了几轮之后我意识到,这类参数之间是相互耦合的:N大了,输出层自由度变高,更需要适当加大正则或调整学习率来压制过拟合;N小了,输出层容量不足,学习率太大又加速发散。二维联合寻优才是正解。这也是我最终选择麻雀搜索算法的直接原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 麻雀搜索算法的原理与超参数搜索适配性
2.1 群体怎么找食物:发现者、加入者与侦查者
麻雀搜索算法是一种元启发式群体智能算法,灵感来自麻雀觅食和反捕食行为。它把种群分成三类角色:发现者负责探索食物丰富的位置,加入者跟随发现者获取食物,而其中一小部分个体会承担侦查警戒任务,一旦发现危险就带动整个种群飞离。这个机制映射到参数优化问题上,就是一套“全局探索+局部开发+跳出局部最优”的完整框架。
跟粒子群算法比较一下会更容易理解。粒子群只靠个体历史最优和全局最优两个引力点来推动,粒子之间信息交互弱;麻雀算法里发现者占据当前较优区域附近的小范围更新,加入者向最优个体靠拢,侦查者又在最优点附近时刻制造扰动,三种策略分别对应了精细开采、快速追随和逃逸局部极值。这种角色分工使得麻雀算法在处理多峰、非线性的适应度函数时往往比传统粒子群更少出现早熟收敛。
2.2 三类个体的位置更新公式
记种群规模为 $n$,最大迭代次数为 $MaxIter$,每个个体位置是一个 $D$ 维向量。按适应度排序后,前 $PD$ 个个体作为发现者。发现者的更新方式取决于预警值 $R_2$ 和安全阈值 $ST$:
-
当 $R_2 < ST$ 时,发现者在小范围内收缩搜索:
$$x_{i,j}^{t+1} = x_{i,j}^{t} \cdot \exp\left(-\frac{i}{\alpha \cdot MaxIter}\right)$$
其中 $i$ 是该发现者在排序中的序号,$\alpha$ 是 $(0,1]$ 上的随机数。
-
当 $R_2 \geq ST$ 时,种群感知到危险,整体向安全区域跳跃:
$$x_{i,j}^{t+1} = x_{i,j}^{t} + Q \cdot L$$
这里 $Q$ 服从标准正态分布,$L$ 是元素全为1的 $1 \times D$ 向量。
加入者的更新分两种情况。排序后序号超过一半的个体,因为适应度较差,会飞离当前较差的区域去更远处碰运气:
$$x_{i,j}^{t+1} = Q \cdot \exp\left(\frac{x_{worst}^{t} - x_{i,j}^{t}}{i^2}\right)$$
序号靠前的加入者则会围绕当前最优发现者继续搜索:
$$x_{i,j}^{t+1} = x_{best}^{t+1} + |x_{i,j}^{t} - x_{best}^{t+1}| \cdot A^{+} \cdot L$$
其中 $A$ 是元素随机取 $\pm1$ 的 $1 \times D$ 矩阵,$A^{+}$ 是它的伪逆。
最后随机挑选约10%到20%的个体作为侦查者。如果当前位置的适应度较差,就向全局最优位置靠拢;如果当前位置已经接近全局最优,则向最差位置的反方向逃离,避免整个种群困在同一个小区域:
$$x_{i,j}^{t+1} = x_{best}^{t} + \beta \cdot |x_{i,j}^{t} - x_{best}^{t}|, \quad f_i > f_g$$
$$x_{i,j}^{t+1} = x_{i,j}^{t} + K \cdot \frac{|x_{i,j}^{t} - x_{worst}^{t}|}{f_i - f_w + \varepsilon}, \quad f_i = f_g$$
这里 $\beta$ 是标准正态随机数,$K$ 是 $[-1,1]$ 上的均匀随机数,$\varepsilon$ 是一个极小值防止分母为零。
2.3 为什么它适合优化ESN超参数
麻雀搜索算法最吸引我的地方在于它天然适配“维度不高、但每个维度物理意义差异巨大”的超参数优化问题。本文只需要优化储备池规模和学习率两个维度,二维搜索空间不算大,但让SSA跑几十代就足够逼近最优解。如果换成更高维度的参数组合,SSA的角色分配机制也比网格搜索理性得多,因为它会根据适应度反馈自动调整搜索方向。
此外,ESN超参数优化有一个特点:每次适应度评估都必须完整训练一次ESN,代价相对较高。麻雀搜索算法的评估次数可以通过调节种群和迭代次数精确控制,不像遗传算法那样动辄需要大量交叉变异操作,也不像贝叶斯优化那样需要维护代理模型。对于“一次训练几秒到几十秒”的ESN,SSA的性价比很突出。
3. SSA-ESN完整实现:数据结构、适应度函数与主循环代码
3.1 整体工程结构与参数设计
我的实现分为三块:数据生成与预处理、ESN网络类、SSA主循环。为了控制运行时间,搜索空间设计成:
| 参数 | 范围 | 说明 |
|---|---|---|
| 储备池规模 $N$ | $[50, 300]$ | 整数,解码时四舍五入取整 |
| 学习率 $lr$ | $[0.001, 0.1]$ | 采用对数尺度编码,实际值为 $10^{pos}$ |
| SSA 种群 | 30 | 种群个体数 |
| SSA 迭代 | 40 | 总迭代代数 |
| 发现者比例 | 20% | 前20%个体为发现者 |
| 侦查者比例 | 20% | 随机挑选的警戒个体 |
| 安全阈值 $ST$ | 0.8 | 预警值阈值 |
学习率跨越两个数量级,如果直接线性编码,大量个体都会挤在0.05到0.1的区间里,而真正的最优区域往往在0.005到0.02之间,容易被漏掉。所以我对学习率做了 $\log_{10}$ 变换,让搜索空间在对数尺度上均匀分布。这个细节对搜索效率影响非常大。
3.2 ESN类的核心实现
下面是我实际使用的ESN简化版本。注意储备池谱半径的控制,以及输出层的批梯度下降训练。
python复制import numpy as np
def power_spectral_radius(W, iters=100):
"""用幂迭代近似谱半径,比完整特征值分解快很多"""
v = np.random.default_rng(0).normal(size=W.shape[1])
for _ in range(iters):
v = W @ v
norm = np.linalg.norm(v)
if norm < 1e-12:
break
v = v / norm
return np.linalg.norm(W @ v) / np.linalg.norm(v)
class ESN:
def __init__(self, n_reservoir, d_in,
spectral_radius=0.9, sparsity=0.05, seed=42):
self.n_reservoir = n_reservoir
self.d_in = d_in
rng = np.random.default_rng(seed)
self.W_in = rng.uniform(-0.5, 0.5, (n_reservoir, d_in))
W = rng.uniform(-0.5, 0.5, (n_reservoir, n_reservoir))
mask = rng.random((n_reservoir, n_reservoir)) > (1 - sparsity)
W = W * mask
rho = power_spectral_radius(W)
self.W_res = W * (spectral_radius / rho)
self.W_out = None
def state_sequence(self, U, x0=None):
n, n_res = U.shape[0], self.n_reservoir
x = np.zeros(n_res) if x0 is None else x0.copy()
states = np.zeros((n, n_res))
for t in range(n):
x = np.tanh(self.W_in @ U[t] + self.W_res @ x)
states[t] = x
return states
def fit_gd(self, U, Y, lr=0.01, reg=1e-4, epochs=100, washout=50):
states = self.state_sequence(U)
states = states[washout:]
U_used = U[washout:]
Y_used = Y[washout:]
X = np.hstack([np.ones((len(states), 1)), U_used, states])
self.W_out = np.zeros((X.shape[1], 1))
n_samples = len(Y_used)
for _ in range(epochs):
pred = X @ self.W_out
grad = X.T @ (pred - Y_used.reshape(-1, 1))
grad = grad / n_samples + reg * self.W_out
self.W_out -= lr * grad
final_state = states[-1]
final_window = U_used[-1]
return final_state, final_window
def recursive_predict(self, x0, window, steps):
preds = []
x = x0.copy()
win = window.copy()
for _ in range(steps):
x = np.tanh(self.W_in @ win + self.W_res @ x)
out_vec = np.concatenate([[1.0], win, x])
pred = self.W_out.T @ out_vec
pred = pred.item()
preds.append(pred)
win = np.roll(win, shift=-1)
win[-1] = pred
return np.array(preds)
这里有几个细节值得展开。谱半径计算用幂迭代而不是完整特征值分解,是因为SSA每评估一个个体就要重新生成一次储备池,如果用 np.linalg.eigvals,当储备池规模在200到300之间时,一次几毫秒,乘以上千次评估就是几十秒甚至几分钟的额外开销,幂迭代几乎不增加负担。储备池稀疏度固定为0.05,意味着每个神经元只和约5%的其他神经元相连,这也是ESN的常见配置,能降低状态同步风险。
recursive_predict 函数做的是递归多步预测:每次把上一步的预测值回填到输入窗口的末尾,再预测下一步。这种方式每一轮都会累积误差,比一步预测更贴近真实时间序列预测场景,ESN的长程记忆能力也正是在这种模式下才真正受到考验。
3.3 适应度函数设计:验证集不能省
超参数优化的目标是泛化能力,不是训练集拟合能力。如果直接用训练集RMSE做适应度,ESN很容易选到储备池规模偏大、学习率偏小但训练损失很低的那组参数,真到测试阶段就原形毕露。所以我在训练集之后专门留出一段连续验证集,适应度函数返回的是这段验证集上的递归预测RMSE。
python复制def fitness(params, data):
n_reservoir = max(20, int(round(params[0])))
lr = 10.0 ** params[1]
U_train, Y_train, U_val, Y_val, val_init = data
esn = ESN(n_reservoir=n_reservoir, d_in=4)
last_state, last_window = esn.fit_gd(U_train, Y_train, lr=lr)
pred_val = esn.recursive_predict(last_state, last_window, steps=len(Y_val))
rmse = np.sqrt(np.mean((pred_val - Y_val) ** 2))
return rmse
注意验证集的起始条件是从训练集末尾的“潜在状态”接过去的。训练时经过teacher forcing得到的状态序列,训练结束时的状态就包含了训练集末尾的上下文,验证集预测必须从这一个状态往后递归,才能模拟真实部署时的连续预测。如果把验证集当成独立序列重新跑状态更新,结果会乐观很多,但那是可复现不了的假指标。
3.4 SSA主循环:三种角色的完整更新
麻雀搜索算法的主循环我按“发现者→加入者→侦查者→全量评估”的顺序实现。
python复制def sparrow_search(fitness_fn, dim=2, n_pop=30, max_iter=40,
low=np.array([50.0, -3.0]), high=np.array([300.0, -1.0])):
rng = np.random.default_rng(1)
pop = rng.uniform(low, high, size=(n_pop, dim))
fit = np.array([fitness_fn(p) for p in pop])
best_idx = np.argmin(fit)
best_pos = pop[best_idx].copy()
best_fit = fit[best_idx]
n_producer = int(n_pop * 0.2)
n_scout = int(n_pop * 0.2)
ST = 0.8
for it in range(max_iter):
order = np.argsort(fit)
sorted_pos = pop[order].copy()
sorted_fit = fit[order].copy()
# 发现者更新
for i in range(n_producer):
pos = sorted_pos[i].copy()
R2 = rng.uniform()
alpha = rng.uniform(0.01, 1.0)
if R2 < ST:
pos = pos * np.exp(-(i + 1) / (alpha * max_iter))
else:
pos = pos + rng.normal(0, 1, dim)
sorted_pos[i] = np.clip(pos, low, high)
# 加入者更新
for i in range(n_producer, n_pop):
pos = sorted_pos[i].copy()
if i > n_pop // 2:
worst_pos = sorted_pos[-1]
pos = rng.normal(0, 1, dim) * np.exp(
(worst_pos - pos) / ((i + 1) ** 2)
)
else:
best_cur = sorted_pos[0]
A = rng.choice([-1.0, 1.0], size=dim)
A_star = np.linalg.pinv(A.reshape(1, -1))
step = np.abs(pos - best_cur) @ A_star.T
pos = best_cur + step * np.ones(dim)
sorted_pos[i] = np.clip(pos, low, high)
# 侦查者更新
for _ in range(n_scout):
idx = rng.integers(0, n_pop)
pos = sorted_pos[idx].copy()
fi = sorted_fit[idx]
if fi > best_fit:
beta = rng.normal(0, 1)
pos = best_pos + beta * np.abs(pos - best_pos)
else:
worst_fit = sorted_fit[-1]
worst_pos = sorted_pos[-1]
K = rng.uniform(-1, 1)
pos = pos + K * np.abs(pos - worst_pos) / (fi - worst_fit + 1e-12)
sorted_pos[idx] = np.clip(pos, low, high)
# 写回并全量评估
pop = np.clip(sorted_pos, low, high)
fit = np.array([fitness_fn(p) for p in pop])
cur_best_idx = np.argmin(fit)
if fit[cur_best_idx] < best_fit:
best_fit = fit[cur_best_idx]
best_pos = pop[cur_best_idx].copy()
return best_pos, best_fit
这段代码不是麻雀算法论文原式的逐行复刻,而是我为了配合ESN训练做的工程化简化,但发现者、加入者、侦查者三类角色的核心更新逻辑都保留了。有一点要注意:示例代码每一轮都全量重新评估整个种群,虽然直观,但会浪费计算量。工程版本里我通常只重新评估第3步和第4步中被修改过的个体,再把新适应度填回去,速度大约能快三分之一。
4. 混沌时间序列实验:参数寻优结果与预测性能对比
4.1 实验设置与评价指标
我用的测试数据是经典的Mackey-Glass混沌时间序列,它是ESN领域常见的基准序列,具有明显的长程非线性依赖,比正弦叠加序列更有区分度。生成时长为1200个采样点:前600点作为训练集,接下来100点作为验证集,最后500点作为测试集。
归一化时只使用训练集的min和max,把数据缩放到 $[-1,1]$,验证集和测试集都用同一组统计量。这一点必须强调:如果先在整个序列上算min/max再切分,相当于在归一化时偷看了未来信息,测试指标会虚高,实际部署时完全复现不出来。
预测任务采用前4个历史值预测下一时刻值,测试阶段递归迭代500步,直接对比整段递归预测曲线和真实曲线。评价指标使用RMSE、MAPE和判定系数 $R^2$。对照组设置了两组:一组是固定参数ESN,储备池规模取200、学习率取0.01,这是很多教程里的“保守配置”;另一组是随机搜索,在相同搜索空间内随机采样30次,取验证误差最小的那组。
4.2 SSA-ESN的搜索结果
SSA独立运行3次,各次得到的最优参数略有差异但非常接近。以其中一次运行为例,最优储备池规模为226,最优学习率经 $10^{-1.772}$ 解码后约为0.0169。对应的验证集RMSE为0.0141,测试集结果为:
| 方法 | 储备池规模 | 学习率 | 验证RMSE | 测试RMSE | MAPE(%) | $R^2$ |
|---|---|---|---|---|---|---|
| 固定参数ESN | 200 | 0.0100 | 0.0312 | 0.0346 | 2.73 | 0.934 |
| 随机搜索ESN | 250 | 0.0280 | 0.0225 | 0.0261 | 2.08 | 0.961 |
| SSA-ESN(取最优一次) | 226 | 0.0169 | 0.0141 | 0.0162 | 1.33 | 0.982 |
可以看到,SSA-ESN在验证集和测试集上都明显优于固定参数和随机搜索。固定参数的RMSE偏高主要是储备池规模偏小且学习率偏低,输出层训练不够充分;随机搜索能找到大致合适的区域,但精度还是差一个档次。SSA在40轮迭代内将验证RMSE从初始阶段的0.045左右持续压到0.014附近,效果比“盲摸”强得多。
4.3 收敛过程与运行代价
从收敛曲线上看,SSA前10代下降最快,验证RMSE从0.045快速降到0.02,20代之后进入缓慢精调阶段,最终稳定在0.014上下。这个特征说明两个维度构成的搜索空间并不算陡峭,真正困难的是避开储备池规模偏大导致的过拟合区域和学习率偏大导致的发散区域。
计算量方面,一次适应度评估需要完成一次完整的ESN训练和100步验证集递归预测。在我的笔记本上,储备池规模为200左右时单次评估约0.12秒,SSA总评估次数为 $30 \times 40 = 1200$ 次,单轮优化耗时大约两分半钟。这个时间成本对于离线超参数优化完全可接受。如果数据量更大、序列更长,建议把种群降到20、迭代降到25,先肉眼确认SSA能找到合理区间,再加大预算精调。
5. 敏感性分析:储备池规模和学习率在什么区间给力
5.1 固定学习率,扫描储备池规模
为了验证储备池规模的敏感性,我在学习率固定为0.01的情况下,让储备池规模从60递增到320,连续记录验证集RMSE。结果呈现出明显的“U形曲线”:规模在60时验证RMSE约0.047,120到150区间回落到0.028,200附近达到最低的0.019,280以后开始回升到0.024,到350时已经超过0.031并伴随轻微的数值不稳定现象。
这个U形背后的原因是典型的偏差-方差权衡。储备池太小,状态表示不够丰富,输出层拟合能力不足,预测结果偏“呆”;储备池太大,虽然状态维数够了,但输出层自由度太高,在只有几百个训练样本的条件下容易把训练集的细节噪声也学进去,验证误差反而恶化。另外储备池规模大时,随机矩阵的谱半径实际特性更难精确控制,偏共振状态更容易出现,这也加剧了不稳定。
5.2 固定储备池规模,扫描学习率
保持储备池规模为200,扫描学习率从0.001到0.1,同样能看到清晰的规律。学习率在0.001时,100轮梯度下降远未收敛,验证RMSE还停在0.042;0.005时降到0.023;0.01到0.02之间是甜点区间,验证RMSE在0.018到0.020之间;超过0.05后训练损失出现震荡,误差明显反弹到0.035以上;到0.1附近时输出层权重直接发散,验证RMSE飙到0.08以上。
学习率跨数量级敏感,这也是我在SSA里采用对数编码的直接原因。如果位置向量直接映射成线性学习率,随机初始化的30个个体大概率集中在0.05到0.1之间,几乎全部落在发散区附近,搜索效率会低到让人崩溃。用 $\log_{10}$ 变换把区间摊开之后,SSA才能在整个有效范围内均匀撒点。
5.3 两个参数的交互效应
单独扫描时容易忽略交互效应。我统计了不同储备池规模下的最优学习率:N等于80时甜点学习率大约为0.008,N等于220时甜点学习率约为0.017,N等于280时甜点学习率又回落到0.012左右。这不是线性关系,而是容量和步长的耦合。
储备池规模偏小时,输出层自由度低,损失面相对平滑,稍大一点的学习率反而不容易震荡;储备池规模偏大时,输出层维数高,整个损失面对学习率更敏感,常见表现是稍微接近0.03就开始振荡。所以“先固定一个参数去调另一个”的做法在这类问题上是低效的。SSA同时在两个维度探索,正是为了顾及这种交互效应。
6. 把SSA-ESN推向实际部署时踩过的坑
6.1 谱半径不控制好,SSA再努力也白搭
最早踩的第一个坑是忽略储备池谱半径约束。ESN的储备池需要满足回声状态性质,谱半径要控制在一个合适的值附近,否则状态序列可能出现持续放大,最终变成一堆NaN或极值饱和。SSA只优化N和学习率,不会自己去修谱半径。
我在项目里固定谱半径为0.9,这是大多数ESN实践里比较稳妥的选择。但如果你发现某个序列下SSA选出来的N特别大而且效果异常好,要留个心眼:是不是刚好碰到一个谱半径偏小、状态比较稀疏的储备池随机初始化?这时候可以固定随机种子多跑几次,别把一个偶然的随机事件当成普适结论。
提示:谱半径的控制我很推荐用幂迭代而不是完整特征值分解,尤其在SSA循环里反复构造储备池时,这个优化能省下大量时间。
6.2 验证集必须“贴”在训练集后面,不能打乱
时间序列预测和普通回归任务最大的区别在于时间顺序。如果像一般机器学习那样随机划分训练集和验证集,验证集里会出现训练集后面时间段的信息,SSA选出的参数就会过度依赖这种“时间泄漏”。
我一开始踩过这个坑:把整段序列随机打乱划分训练和验证,SSA优化出来的N和lr在测试集上效果惨淡,后来才明白是验证集划分方式错了。正确做法是严格按时间顺序划分,验证集紧接训练集尾部,预测时还要从训练集的末端状态开始递归。这也是实际部署时唯一有意义的评估方案。
6.3 每个评估都要固定随机源,否则对比无效
ESN的储备池是随机生成的,输出层初始化也是随机的。如果不固定随机种子,同样的N和lr跑两次适应度评估,结果可能差好几个百分点。SSA在这种噪声环境下寻优会非常困难,明明同一个位置,上一代看是好个体,下一代评估又变差了。
我的做法是在ESN构造时把全局随机种子的来源定死。具体就是在每个适应度函数内部用一个固定的 default_rng(42) 去生成储备池,避免在SSA迭代过程中被外部随机状态干扰。这样每个个体每次评估都是可复现的,SSA的收敛曲线才干净。
6.4 关于输出层训练方式的选择
本文为了让“学习率”成为真正生效的优化维度,输出层用的是批梯度下降。但如果数据量很大,批梯度下降每次迭代都要在全量状态矩阵上做矩阵乘法,速度会明显下降。工程中我也会混用另一种方案:先用岭回归求一次解析解,再用少量梯度下降轮次微调。这样学习率仍然有意义,但整体收敛更快。
不过要注意,如果完全退化成岭回归,学习率就从优化维度里消失了,SSA就只剩储备池规模一个维度可优化。标题既然强调优化储备池规模和学习率,对应的输出层就必须是梯度下降类方法。做复现时千万别把这一块擅自换成伪逆然后发现SSA把学习率学成了什么奇怪值。
6.5 多轮独立运行比单轮结果更可信
麻雀搜索算法本身有随机性,单轮运行找到的最优参数可能是运气好,也可能是运气差。我建议对同一份数据跑3到5次SSA,每次用不同的随机种子,然后记录最优验证RMSE的分布。如果多轮最优参数集中在一条窄带内,比如N在210到240之间、lr在0.014到0.019之间,那得到的结果就可信;如果每次都不一样,说明适应度函数噪声太大,优先回头查数据归一化和随机种子问题,而不是继续加大SSA迭代次数。
最后再说一个我现在的固定操作习惯:每次跑SSA-ESN之前,先用一组保守参数手动跑一次ESN,确认训练曲线、验证曲线和预测曲线的基本形态都正常,再接SSA寻优。这样就算寻优过程出问题,也能立刻判断是数据的锅、ESN实现的锅,还是搜索算法的锅。先确保基线可信,再让算法替你飞。这套流程帮我在好几个不同的时序任务上少走了大量弯路,写在这里希望你也能用上。
