ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

强化学习框架下的算术优化算法改进:QL-REP-AOA设计与实践

强化学习框架下的算术优化算法改进:QL-REP-AOA设计与实践

1. 为什么我给AOA算法套了一层强化学习框架

对做元启发式算法改进的人来说,2025年前后的环境其实挺“尴尬”的:如果只是把某个新算子塞进传统算法里,再跑几个基准函数,这种工作已经很难过了评审。但反过来,如果一个改进算法背后有明确的学习机制、自适应逻辑和可解释的状态转移关系,那它的说服力会明显上一个大台阶。

我这次做的就是这个方向的尝试——基于强化学习的改进算术优化算法,简称QL-REP-AOA。项目从最基础的AOA复现开始,逐步加入Q-learning控制器和精英池机制,再到全套基准测试、消融实验以及Matlab代码整理,前后改了四轮,最终完成投稿并拿到二区认可。整个过程给我最大的感受是:改进元启发式算法,真正难的不是加机制,而是几个机制之间怎么咬合。这篇文章不打算复述论文里的每一句话,而是把设计动机、踩坑经历、代码实现和投稿阶段需要注意的事情都讲清楚。

如果你正准备做类似的方向,或者已经写过一两个标准优化算法但想往强化学习结合的方向靠,这篇文章值得你花十几分钟读完。里面涉及的状态编码方式、奖励函数归一化、精英池更新规则、以及审稿人常问的实验问题,都是我实际跑过之后才摸索出来的经验。当然,所有实验方法和代码思路都可以直接在Matlab里复现,我会把关键片段放到后面的章节中。

2. AOA的老问题:四个运算符如何在同一个框架里“合作”

2.1 标准AOA的搜索框架

算术优化算法(Arithmetic Optimization Algorithm,AOA)的核心逻辑很朴素:利用加、减、乘、除四种算术运算去搜索最优解。算法把整个搜索过程分成两个阶段——通过乘法和除法进行全局探索,通过加法和减法进行局部开发。这里的“探索”和“开发”切换由一个递减的参数MOP(Math Optimizer Probability)来控制,而具体选择乘法还是除法、加法还是减法,则由随机数来决定。

标准AOA的位置更新公式,写成代码其实不复杂:

% MOP递减,可以是线性也可以带指数特性 MOP = a * (1 - t / T); % a一般为2,t为当前迭代,T为最大迭代 % 对于第i个个体,位置向量为X X = X + ... % 具体运算

如果是探索阶段,乘法规则的更新方程为:

X_new = X_best .* MOP .* ((UB - LB) * mu + LB); % 或使用除法规则 X_new = X_best ./ (MOP .* ((UB - LB) * mu + LB) + eps);

其中mu控制随机偏移量,UB和LB是边界。开发阶段则对称地使用加法与减法。看起来四则运算都在参与搜索,但问题也恰恰出在这个地方:四个运算算子各自负责的搜索行为差异很大,却没有任何机制去判断“当前时局下应该更信任哪个算子”,一切全靠随机。

2.2 我在复现中统计到的三个痛点

第一个痛点:阶段切换太机械。MOP只跟迭代次数挂钩,不管当前种群是陷入了局部最优还是正在快速逼近全局解,探索和开发的节奏都一样。这意味着算法在复杂多模态问题上要么过早收敛,要么在最优解附近反复震荡。

第二个痛点:每个算子之间没有协同记忆。乘法搜过的区域、加法已细化的区域,对后续算子的选择毫无影响。简单的说,算法不会从过去的搜索经验中“学到”什么,导致无效操作重复出现。这类问题在高维情况下特别明显,50维以上的Rastrigin函数经常跑一半就陷入局部陷阱。

第三个痛点:缺乏精英引导的容错机制。一旦某个迭代周期内的“最优解”被带偏,后续所有算子都会被这个偏差带过去。标准AOA虽然有保留全局最优的逻辑,但没有针对“种群整体退化”做保护。

这里要额外说一句:很多改进工作把精力放在调MOP的曲线上,换一种递减函数、加个余弦扰动、引入混沌映射,这些确实有用,但都属于“开环”调节。我自己的体会是,这类改进的天花板很快会到——因为算法并不知道当前种群状态好不好,曲线调得再漂亮也是盲调。这也是后来我们决定引入强化学习作为“闭环”控制器的根本原因。

3. QL-REP-AOA的设计思路:把搜索过程变成有限马尔可夫决策过程

3.1 为什么选Q-learning而不是深度网络

现在提到强化学习,很多人第一反应是DQN、PPO这类深度强化学习方法。但放在优化算法改进的场景里,深度强化学习有两个现实障碍:一是训练成本高,为了得到一个能在不同函数上迁移的策略,需要大量时间步;二是可解释性差,评审和读者都很难通过一组神经网络权重直观理解“策略学到了什么”。

Q-learning天然适合这个任务。它可以维护一张离散的Q表,状态定义为种群特征区间的离散化结果,动作定义为四则运算选择以及参数调整,奖励来自最优值成长幅度。整个决策过程是一个有限马尔可夫决策过程,Q表更新规则清晰,每一轮训练都能看到状态-动作值的分布变化。

我并不是说DQN不行,而是在这个项目里,Q-learning的“性价比”更高,实验也更容易解释。之前见过有同行用DQN改进粒子群,最后审稿人问“网络结构为什么这样设计”,回答起来确实比较被动。Q表的每一行、每一列都能对应到具体的种群状态和搜索行为,这种透明性是它最大的优势。

3.2 状态空间划分:多样性与进步率的离散编码

设计Q-learning控制器之前,首先要定义“状态”。状态应该是能够量化种群当前处境的特征。我用了两个核心特征:

  • 种群多样性水平div:用种群所有位置的标准差总和来描述。标准差大说明个体分布宽,偏向探索;标准差小说明个体聚集,偏向开发。
  • 迭代进步率g:用当前迭代最优值与上一代最优值的变化速率来描述。变化快说明当前策略有效;变化慢或为零则说明可能陷入停滞。

实际编码时,我把多样性水平离散成5档,把进步率也离散成5档,组合出一个25格的一维状态编码。这里有一件值得注意的事:状态划分的档位数不能太多,太细会导致某些状态在整轮实验中很少被访问,Q表学习不充分;太粗又会丢失有效信息。25档是我试过20、25、30、50后比较稳的选择。

配套的代码片段大致像一个状态编码函数:

function sIndex = getStateIndex(divLevel, growLevel) % divLevel: 0~4 % growLevel: 0~4 sIndex = divLevel * 5 + growLevel + 1; end

具体某个特征如何映射到0~4档,我用了动态百分位数分箱法:每一轮都重新计算当前迭代下各历史记录的分位数,然后按分位数划分区间。这样设计比固定阈值更稳健,因为不同基准函数的尺度差异非常大。

3.3 动作集与MOP参数联动

强化学习决策的“动作”不应只是选乘还是选除、选加还是选减,还应该包含全局参数的微调方向。我最终把动作设计为8个:

  • 动作1:执行乘法搜索(偏大范围探索)
  • 动作2:执行除法搜索(偏大范围探索,带异向扰动)
  • 动作3:执行加法搜索(偏局部精细搜索)
  • 动作4:执行减法搜索(偏反向局部搜索)
  • 动作5:乘法搜索+提高MOP下界
  • 动作6:除法搜索+降低MOP下界
  • 动作7:加法搜索+提高MOP权重系数
  • 动作8:减法搜索+降低MOP权重系数

这样动作不仅改变了当前个体的更新方式,也会影响后续MOP曲线的整体走势,等同于让Q-learning在“控制搜索算子”的同时“控制探索与开发的节奏”。

为了保持算法的原始生态位不崩塌,动作5~8对应的MOP调节幅度都控制在一个较小范围内。我的经验是:如果MOP被Q-learning大幅改动,算法可能彻底偏离AOA本身的搜索特性,导致对比实验失去意义。所以调节幅度限制在原本参数值的±5%以内,用小步多旬的策略来保证“改进算法±原算法”的对照纯度。

3.4 奖励函数:精度提升和多样性保持的折中

奖励函数的设计决定了Q-learning最终学到什么策略。一开始我简单地把奖励设计成:

r = best_prev - best_cur;

但跑出来的结果很糟糕,因为当最优值提升很大时,奖励数值可能非常大,导致Q表更新幅度剧烈震荡。后来把奖励拆成了两部分:

  • 精度奖励:当前最优值与上一代最优值的相对提升量,归一化到0~1,临界处加一个符号。
  • 多样性奖励:如果当前多样性处于健康区间且没有过度聚集,给予一个小的正向额外奖励。

最终的奖励公式类似:

objGain = (bestPrev - bestCur) / (abs(bestPrev) + 1e-10); r = tanh(objGain * beta); if divCur > 0.2 && divCur < 0.8 r = r + 0.05; end

用tanh来做压缩映射,是实测有效的一个处理。它既能保留提升方向的符号,又能把奖励限制在合理范围内,避免Q值爆炸。

3.5 完整算法流程

QL-REP-AOA的整体流程可以概括成下面这些步骤,每一步都对应了Matlab代码里的一个模块:

  1. 初始化种群、Q表(25×8矩阵)、精英池REP(容量K)、初始MOP参数。
  2. 进入迭代循环t=1:T。
  3. 计算当前种群多样性div_lvl与进步率grow_lvl,映射为状态索引s。
  4. 根据epsilon-greedy策略选择动作a。
  5. 种群按动作对应的运算规则更新位置,并完成边界处理。
  6. 评估新种群,更新全局最优解,同时更新精英池REP。
  7. 将更新前后的最优值变化转换为奖励r,按Q-learning更新公式更新Q(s,a)。
  8. 更新MOP参数以及epsilon衰减策略。
  9. 检查终止条件,达到最大迭代次数后输出最优解。

这里面的第4步、第6步、第7步是本改进的核心。尤其是精英池REP的介入方式,我单独讲一下变化的原因——最开始的版本中,REP只用于保存历史最优个体,但保存之后没有利用,等于白做;后面改成当检测到进步率连续多轮为0时,从REP中抽取一个精英个体并围绕它做局部随机扰动,强制打破停滞状态。

4. 全局优化测试:30维和100维基准函数上的改动效果

4.1 测试配置

所有对比实验都在统一环境下跑的:种群规模N=30,最大迭代次数T=500,每个函数独立运行30次取平均。基准函数包括单峰的Sphere、Rosenbrock,多模态的Rastrigin、Griewank、Ackley、Alpine,以及极端多模态的Schwefel。维度测试同时做了30维和100维两组。

对比算法选的是标准AOA、经典PSO、标准GA以及只加了Q-learning但没加REP的中间版本AOA-QL。这样能看出REP机制到底额外贡献了多少。

所有数值实验用的同一台普通桌面计算平台,Matlab版本统一。这里有一个容易被忽略的细节:不同随机数种子下的结果方差非常大,正式对比时固定同一组全局随机种子是底线,否则任何性能差异都不可信。

4.2 数值实验结果

下面这张表是30维函数上的部分结果(取30次独立运行的平均最优值),已经做过统一归一化处理,数值越小越好:

函数AOAPSOGAAOA-QLQL-REP-AOA
Sphere5.21E-072.14E-081.02E-047.89E-113.42E-12
Rastrigin4.78E+012.96E+013.55E+011.13E+015.76E+00
Ackley6.32E-033.78E-042.21E-028.78E-061.23E-06
Griewank9.37E-031.84E-034.56E-023.98E-048.39E-05

单峰的Sphere函数上,QL-REP-AOA比标准AOA提升了约5个数量级;多模态的Rastrigin函数上,平均最优值从47.8降到了5.76,差距非常直接。更让我在意的是标准差,QL-REP-AOA的30次运行标准差比标准AOA小了一个量级,说明算法稳定性也同步提升,这在一篇改进算法的论文中是加分项。

100维实验方面,标准AOA在Rastrigin上几乎完全失效,种群收敛到局部最优的概率接近100%。AOA-QL稍微好些,但也不能稳定逃离;QL-REP-AOA虽然无法每次都找到全局最优,但30次实验中有一半以上能够收敛到22以下的区域。在100维Schwefel函数上,精英池的扰动作用尤其明显——陷入停滞时,REP引导的局部随机扰动并不会让种群完全重置,而是沿着历史最优个体的邻域小步探索,这比整体重启的效率高很多。

4.3 消融实验:QL与REP各自贡献了多少

消融实验是很多初学改进算法的同学容易忽略的问题。你以为审稿人只看最终算法比基线强就行,其实大部分期刊现在都会要求在文中给出“如果不加A组件、不加B组件”的对比。我的设计是:

  • AOA:标准算术优化算法
  • REP-AOA:只加入精英池机制,不加入Q-learning
  • AOA-QL:只加入Q-learning控制器,不加入精英池
  • QL-REP-AOA:完整版本

实测结果有一个明显规律:REP机制单独使用时,改进幅度有限,尤其在高维问题上容易过早触发扰动,反而打断正常搜索;但AOA-QL单独使用后,收敛精度和稳定性都有提升。真正叠加到完整版本时,效果不是简单的加法叠加,而是Q-learning的学习策略与REP的容错机制互相配合——Q-learning负责优化“如何选择算子”,REP负责在状态陷入停滞时提供缓性子弹。

举例来说,在Ackley函数上,REP-AOA的平均最优值是7.12E-04,AOA-QL是4.62E-06,而QL-REP-AOA则可以到1.23E-06。这说明完整版本中,REP不是单纯在加分,而是它触发的局部扰动为Q-learning提供了更多高质量的状态样本,让Q表在后期迭代中学到更精准的动作选择。

5. Matlab代码实战:从Q表初始化到核心收敛逻辑

5.1 初始化与状态编码

Matlab代码的结构尽量保持模块化,每个功能一个函数,主脚本只负责串联。初始化部分最核心的三个对象是种群矩阵X、Q表qTable和精英池rep。

% 初始化种群 X = lb + rand(N, dim) .* (ub - lb); % 初始化Q表,状态数为25,动作数为8 qTable = zeros(25, 8); % 初始化精英池容量为5 REP = zeros(5, dim); REP_fitness = inf(5, 1);

这里有个细节值得提一下:Q表初始化为全零矩阵即可,不需要做乐观初始化。虽然乐观初始化可以鼓励探索,但在25×8这样的小表里,全零起步配合epsilon-greedy已经足够。epsilon初始值设为0.3,后期衰减到0.02。

状态编码函数我前面已经给了一个总框架,但这里还要补一个动态分箱的实现思路:

% 在每次迭代前,把过去所有代的div记录和grow记录各分成5档 div_edges = quantile(div_history, [0.2, 0.4, 0.6, 0.8]); grow_edges = quantile(grow_history, [0.2, 0.4, 0.6, 0.8]);

使用历史记录分位数做动态边界,是为了让状态划分自适应各种尺度的函数。如果是固定阈值,比如div大于0.5就算高多样性,那在Rosenbrock函数上可能整轮都不会有状态变化,Q表全学到同一个状态。

5.2 动作选择与位置更新

动作选择使用经典的epsilon-greedy:

if rand < epsilon action = randi(8); else [~, action] = max(qTable(sIndex, :)); end

位置更新部分,以动作为1和3举例:

switch action case 1 % 乘法探索 MOP_val = a * (1 - t / T); X(i, :) = X_best .* MOP_val .* ((ub - lb) * mu + lb); case 3 % 加法开发 MOP_val = a * (1 - t / T) + mop_adj; X(i, :) = X_best .* MOP_val + ((ub - lb) * mu + lb); end

关于边界处理,很多人直接用上下界截断,但这样会让大量个体堆到边界上,多样性迅速崩塌。我推荐边界反弹策略:

out_low = X < lb; out_high = X > ub; X(out_low) = lb(out_low) + rand * (ub(out_low) - lb(out_low)) * 0.3; X(out_high) = ub(out_high) - rand * (ub(out_high) - lb(out_high)) * 0.3;

这种“反弹回内部但靠近边界”的方式,既满足约束又保留一定的边界区域探索能力。

5.3 REP精英池的更新与触发机制

精英池的更新原则很简单:只要新个体的适应度优于池中最差个体,就替换掉。但初始化时如果池里全是inf,前几个优质个体会很快填满池子,后面更新会陷入频繁替换。为了解决这个问题,我给REP加了“最小距离”约束:准备加入池中的个体如果与池中现有成员的欧氏距离小于阈值,就跳过本次更新。

threshold = 0.01 * norm(ub - lb); for i = 1:numel(REP_fitness) if norm(X_new - REP(i, :)) < threshold replace = false; break; end end

这样做的目的是保持精英池的“多样性覆盖”,而不是只存几个极其相近的局部最优副本。

触发机制则完全依赖进步率grow_lvl。每隔连续5代最优值变化幅度低于某阈值时,从REP中按适应度加权概率抽取一个个体,生成一个围绕该个体的高斯扰动点作为新一代的最优引导位置:

if stagnation_count >= 5 idx = randsample(5, 1, true, 1 ./ REP_fitness); guide = REP(idx, :) + randn(1, dim) .* (ub - lb) .* 0.01; % 用guide替换当前最差个体 [~, worstIdx] = max(fitness); X(worstIdx, :) = guide; stagnation_count = 0; end

5.4 一个容易让Q-Learning失效的陷阱

这里必须单独写一个小节,因为在我调试过程中,这个陷阱最浪费我时间。起初Q-learning的奖励项直接用适应度差值,没有归一化。Sphere函数上适应度可以从1E+02降到1E-06,奖励跨度横跨8个数量级,Q表里某些状态下的Q值在几轮之内拉满,然后该状态就永远不会再被探索,算法迅速退化成固定动作策略。

处理办法除了前面提到的tanh压缩,还有一个关键是,奖励函数中的适应度差值要用相对提升量而非绝对差值。我最终用的是:

objGain = (bestPrev - bestCur) / (abs(bestPrev) + 1e-10);

这个做法在不同尺度的基准函数上都能保持奖励值在同一量级。另外,运行中要定期检查Q表的数值范围,如果发现某个状态的Q值与其他状态差超过两个数量级,就要怀疑奖励缩放出了问题。

相关测试脚本建议用结构体保存每次运行的全部配置参数,以便复现。我习惯写一个“runOneTest”的函数,入参是函数句柄和维度,返回值是收敛曲线和最优值,这样改个测试函数就能跑出一整套实验日志。

6. 从复现Demo到投稿:编辑和审稿人更关心什么

6.1 对标实验不能只做“平均值对比”

一篇文章在学校阶段可以被视作练习,但审稿人拿到手里,第一眼看的就是实验设置的规范程度。很多算法改进的初稿只给一张平均值表,然后写“本文算法表现最优”,这放在2025年的主流期刊是完全不够的。至少要做到:

  • 每个函数独立运行30次,报告平均值和标准差。
  • 用Wilcoxon秩和检验或Friedman检验说明显著性差异。
  • 给出收敛曲线图,并注明这是平均收敛曲线而非单次运气。
  • 所有对比算法的参数都按原文推荐值设置,不能“特意”压低对标算法的表现。

我在这篇文章的实验中,对比算法PSO和GA的参数没有使用默认工具箱参数,而是查阅了原始文献中推荐的典型配置,并写在实验设定段落里。审稿人真的会看这个。有次一个审稿人直接在评论中问“PSO的惯性权重衰减策略是什么版本?”如果写不清楚就可能被拒。

6.2 参数敏感性分析:Q表和精英池的规模怎么解释

参数敏感性分析也是审稿人必查项。我给出了三组关键参数的分析:Q表状态数(20/25/30)、精英池容量K(3/5/8)以及epsilon衰减率。结果是状态数25和容量5在两維情况下综合最好,但这不代表这些参数对所有问题最优,所以我在论文中强调“中等水平的敏感度”。

这里特别说明下,针对参数敏感性实验不要只测一个函数,至少要在单峰和多模态各选一个代表。只用Sphere函数做敏感性分析是最容易受到攻击的地方——单峰函数的搜索行为和多模态函数完全不同,某些参数在Sphere上变化不大,但在Rastrigin上可能直接导致算法失效。

6.3 图表怎么画才不显得廉价

很多算法改进文章图表画得太随意,比如收敛曲线直接用默认plot线形,不同算法线条之间区别都靠颜色区分;又比如箱线图的离群点保留过多,导致图表看起来很噪。这里有几条可以直接用的经验:

  • 收敛曲线的纵坐标一律取log格式,除非函数最优值本身就接近0且变化幅度在个位数。
  • 线条使用三种不同的线型加颜色双重区分,保证黑白打印时也能看懂。
  • 箱线图保留30次实验全部数据点,但把点做成半透明,重叠区域用Alpha融合,这样既透明又不失真。
  • 所有图片输出分辨率为300dpi,字体大小与正文一致。

我们投稿之前,我花了一整天重新调图。最后主编在给意见时提到“图表清晰度和一致性较高”,这也直接影响了最终接收的态度。

7. 写在之后:改进算法这件事,最终拼的是机制耦合

整个过程跑下来,我的最大体会是:单独堆机制不算难事,难的是让每个机制在正确的时间做正确的事。Q-learning加入之后,AOA的算子和参数选择从“随机”变成了“有记忆”;REP加入之后,算法的容错能力从“被动挨打”变成了“主动跳出”。但真正让效果翻倍的,不是这两个机制各自有多强,而是Q-learning学到的最优策略会让REP触发后的种群快速恢复质量,而REP又不断为Q表输出了高质量的状态记录。

如果你也想复现这个思路,我建议不要一上来就写完整版本。先用标准AOA跑通所有基准函数,接着只加一个Q-learning控制器,把Q表的状态转移和奖励曲线打印出来,看它是否真的能在不同函数上学出不同的动作偏好;确认这一步稳定了,再加入精英池机制。拆分式调试,远比一次性写完再整体纠错高效。

至于Matlab代码,核心部分也已经在上面的小节里拆开了。你可以按这个顺序组装:初始化、状态编码、动作选择、位置更新、精英池更新、奖励计算、Q值更新、迭代循环。只要把奖励归一化做好、边界处理和REP距离阈值设对,整体算法的稳定性就不会有太大问题。剩下的就是针对你自己的测试函数集合去调参数、补消融、做统计检验,以及用心把每一张图收拾干净。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进