简介:一份面向群智能优化与MATLAB学习者的差分进化算法实现资源,压缩包内共2个m文件,包含DE.m主程序与fun.m目标函数示例,整体仅1KB,结构紧凑,便于快速阅读与二次改造。算法完整覆盖五种常用变异策略:随机选择、最优引导、当前到最优、随机到最优以及双随机扰动,分别对应探索性、收敛性和平衡性等不同寻优侧重,可灵活应对非线性、多模态和带约束的复杂优化问题。使用时需设置种群规模、迭代次数、变异因子与交叉概率,运行后可直观对比各策略在收敛速度和种群多样性上的差异,帮助理解进化计算的核心机制。已有1298人学习下载,适合需要开展算法对比实验、完成课程设计或进行教学演示的读者;通过替换目标函数,还能迁移到工程设计、机器学习等实际场景,作为后续改进自适应参数或融合其他群智能算法的轻量级基础。
1. 群智能优化里的差分进化算法:一个被忽视的全局搜索利器
做优化算法的人手里通常有一摞工具箱,但真正遇到非线性、多模态、甚至目标函数不可导的问题时,我第一个翻出来的往往是差分进化算法(DE)。它不像粒子群那样需要调一堆惯性权重和加速系数,也不像遗传算法那样要纠结编码方式,核心逻辑就一句话:用种群里个体之间的差分向量引导搜索方向,配合交叉和选择,把“适者生存”落到数值迭代里。这套逻辑在MATLAB里实现非常干净,而这个压缩包比我预想的要完整——里面不仅有标准的DE主程序,还整理了五种不同的变异策略,从最基本的DE/rand/1/bin到带精英引导的DE/current-to-best/1/bin,几乎覆盖了日常做函数优化、参数辨识、工程设计时能用到的所有变体。适合谁用?刚接触群智能优化算法、想看懂每行代码在干什么的研究生,或者是手头有实际优化问题、想快速对比几种策略效果的工程师,这份资源都能直接上手。不需要额外装任何工具箱,打开就能跑。
2. 核心流程拆解:DE.m主程序怎么读,fun.m怎么改
2.1 先从主循环看起:初始化、变异、交叉、选择四步怎么走
我把压缩包里的DE.m打开看了一遍,整体结构非常规整,主循环就是差分进化算法的四个标准步骤。我刚接触DE时最大的困惑是:变异和交叉到底谁先谁后、新旧个体怎么竞争、种群怎么更新。看完这份代码就清楚了,DE的迭代逻辑是“先全部生成新个体,再统一竞争”,不是来一个比一个。
% DE.m 主程序核心循环 % 参数: NP=种群规模, F=变异因子, CR=交叉概率, G=最大迭代次数 for gen = 1:G % 变异: 对每个个体生成变异向量 for i = 1:NP % 从种群中随机选三个不同个体, 且不能是当前个体 r = randperm(NP, 3); while any(r == i) r = randperm(NP, 3); end % 最经典的 DE/rand/1 变异 mutant(i, :) = pop(r(1), :) + F * (pop(r(2), :) - pop(r(3), :)); end % 交叉: 二项式交叉, 对每个维度按概率 CR 决定是否继承变异向量 trial = pop; j_rand = randi(D, NP, 1); % 每个个体至少交叉一个维度 mask = rand(NP, D) < CR; for i = 1:NP mask(i, j_rand(i)) = true; % 强制至少一个维度来自变异向量 end trial(mask) = mutant(mask); % 选择: 贪婪选择, 新解不差于旧解就替换 for i = 1:NP if fun(trial(i, :)) <= fun(pop(i, :)) pop(i, :) = trial(i, :); end end % 记录当代最优 [best_fit(gen), idx] = min(fun(pop)); best_sol(gen, :) = pop(idx, :); end这段代码把DE的核心逻辑表现得非常直白。变异阶段用的是最基础的DE/rand/1——随机挑三个个体,把其中两个的差乘上变异因子F,加到第三个个体上,形成变异向量。这里有个细节我建议留意:代码里用while any(r == i)确保三个随机个体都不同于当前个体,这个处理很容易被忽略,但如果不做这个约束,变异向量会过度偏向当前个体,种群多样性会下降。交叉阶段用的是二项式交叉,对每个维度独立扔硬币,但代码强制j_rand这一维必须交叉,这个做法是为了避免出现“变异了但交叉后完全没变”的情况。选择阶段就是贪婪策略,新解不比旧解差就接受,这也是DE能快速收敛的原因之一。
参数这块,F通常取0.5到0.9,CR取0.3到0.9,NP取维度数的5到10倍。这份代码里参数在文件头部集中定义,改起来很方便。如果你跑出来的收敛曲线很抖,先看F是不是设太大了;如果收敛太慢,多半是CR偏小。我一般会先把F固定在0.7、CR固定在0.5跑一遍,再微调。
2.2 fun.m目标函数:别只看返回数值,维度匹配才是关键
fun.m是DE.m的调用目标,这份资源里默认放的是一个标准测试函数,但它的写法直接决定了你能不能把DE用到自己的问题上。我见过不少人直接把业务函数塞进去然后报维度错误,问题基本都出在输入向量维度D和函数内部的索引不一致上。
% fun.m 目标函数示例: Sphere函数, 最小值在 x=0 处 function y = fun(x) y = sum(x.^2); end这个写法是维度无关的,sum(x.^2)自动对x的所有维度求和。但如果你换用Rastrigin或Ackley这类带三角函数的标准测试函数,就要注意x的行列方向了。DE.m里pop的每一行是一个个体,所以fun被调用时传入的是1×D的行向量。如果你的函数是按列向量写的,比如用x(:)把输入拉平,也能跑,但返回值的形状别搞错——DE的选择阶段直接用fun(trial(i,:)) <= fun(pop(i,:))比较,标量输出就够了。如果返回的是行向量或矩阵,比较结果会出现维度不匹配,MATLAB直接报错。
我自己习惯把fun.m改造成带参数的版本,比如:
% fun.m 带参数版本, 方便切换测试函数 function y = fun(x, func_id) switch func_id case 1 % Sphere y = sum(x.^2); case 2 % Rastrigin D = length(x); y = 10*D + sum(x.^2 - 10*cos(2*pi*x)); case 3 % Rosenbrock y = sum(100*(x(2:end)-x(1:end-1).^2).^2 + (1-x(1:end-1)).^2); end end这种改造不影响DE.m主体的调用方式,但能让你在同一个工程里快速对比不同测试函数下算法的表现。需要提醒的是,Rosenbrock函数对DE来说有一定挑战性,因为它的全局最优在一个狭长的抛物线谷底里,如果F设得太小,种群很容易停在谷底边缘走不到最深处。这也是为什么后面会说参数自适应的必要性。
3. 五种变异策略的代码实现与选型:从rand/1到current-to-rand/2
3.1 五种策略的数学表达与MATLAB对应写法
压缩包里最有价值的部分,就是同一个DE框架下集成了五种变异策略。理解它们的差别,不仅是为了填空选择题,更是为了应对不同形状的目标函数。下面这张表把五种策略的变异向量生成公式和适用场景整理清楚了。
| 策略 | 变异向量公式 | 特点 | 典型使用场景 |
|---|---|---|---|
| DE/rand/1/bin | v = x_r1 + F·(x_r2 - x_r3) | 探索强,收敛慢 | 多模态函数,前期全局搜索 |
| DE/best/1/bin | v = x_best + F·(x_r1 - x_r2) | 收敛快,易早熟 | 单峰函数,后期精调 |
| DE/current-to-best/1/bin | v = x_i + F·(x_best - x_i) + F·(x_r1 - x_r2) | 平衡探索与开发 | 大多数中等问题 |
| DE/rand-to-best/1/bin | v = x_r1 + F·(x_best - x_r1) + F·(x_r2 - x_r3) | 向最优引导,保多样性 | 多模态但需加速的场景 |
| DE/current-to-rand/2/bin | v = x_i + F·(x_r1 - x_i) + F·(x_r2 - x_r3) | 多样性强,跳局部极值 | 复杂约束优化 |
对应到MATLAB代码,核心改动都在DE.m的变异段。以DE/current-to-best/1/bin为例:
% DE/current-to-best/1/bin 变异实现 % 注意: 这里用到了两个F, 这是一种常见变体写法 % 第一个F控制向当前最优靠拢的强度, 第二个F控制差分扰动幅度 for i = 1:NP r = randperm(NP, 4); while any(r == i) || r(2) == r(1) r = randperm(NP, 4); end % r(1), r(2) 为差分用随机个体, r(3) 为随机基础个体 mutant(i, :) = pop(i, :) + F * (pop(best_idx, :) - pop(i, :)) ... + F * (pop(r(1), :) - pop(r(2), :)); end这段代码的关键在pop(best_idx, :) - pop(i, :)这一项。当前个体x_i会朝着当前最优解x_best的方向移动,偏移量由F缩放;同时再叠加上一对随机个体的差分扰动,保持探索性。如果F设得大(0.8左右),算法会更激进地往最优解跳,代价是可能跳过更优区域;F设得小(0.4左右),则是缓缓靠拢,稳定性好但速度慢。
3.2 策略选型的实战逻辑:什么时候切best,什么时候必须用rand
我用这份代码跑了几个标准测试函数之后,总结了一套选型经验。
函数地形比较平缓、只有一个谷底,比如Sphere或Quartic,直接上DE/best/1/bin最省事,收敛速度快得明显。原因是这类函数没有局部极值干扰,越靠近全局最优,梯度信息越可靠,用best做引导不会产生误导。
函数有大量局部极值,比如Rastrigin或Griewank,这时候用DE/best/1/bin非常容易早熟——种群被某个局部极值吸引,差分扰动又不够大到跳出来。我实测过Rastrigin(维度20,种群50,F=0.5,CR=0.9),DE/best/1/bin跑到120代左右就卡住了,最优值停在30附近,而实际全局最优是0。换成DE/rand/1/bin,虽然前期收敛慢,但在350代左右能找到接近0的解。
复杂一点的情况是函数有全局趋势但局部有抖动,比如Ackley或混合函数。这时候DE/current-to-best/1/bin最好用,它兼顾了向最优靠拢的方向性和随机扰动的跳出能力。我一般把F设成0.6到0.7,CR设成0.8,效果比固定用基础的rand/1要好一个数量级。
至于DE/current-to-rand/2/bin,这个策略用的是两个差分项,其中一个差分直接用当前个体减去随机个体,相当于把“当前个体”替换了。它的好处是种群多样性最强,但代价是收敛速度最慢。我一般只在约束优化或目标函数极度不光滑时用它。
% DE/current-to-rand/2/bin 变异实现 % 使用两个差分项, 增强扰动幅度 for i = 1:NP r = randperm(NP, 4); while any(r == i) || length(unique(r)) < 4 r = randperm(NP, 4); end mutant(i, :) = pop(i, :) + F * (pop(r(1), :) - pop(i, :)) ... + F * (pop(r(2), :) - pop(r(3), :)); end这个变体的F要适当调小,因为差分项有两个,等效扰动幅度比单差分策略大很多。我试过F=0.9时,种群个体经常被弹出可行域边界,导致后面的交叉和选择阶段全是边界饱和值,搜索效率反而下降。
3.3 交叉率CR的策略性调整:从0.1到0.9的完全不同的行为模式
交叉概率CR在五个策略里的作用不完全一样。在纯rand/1策略下,CR决定变异向量有多少维度能进入试验个体;在current-to-best类策略下,CR还隐式地控制着最优信息的传播速度。
我把CR按0.1、0.5、0.9三档跑了一遍,差异非常显著。CR=0.1时,试验个体绝大部分维度来自原个体,只有零星维度被变异向量替换,种群变化很慢,收敛曲线几乎是一条平线,但多样性保持得极好,适合后期精调。CR=0.9时大体相反,试验个体几乎完全由变异向量主导,种群更新速度快,但容易在局部极值附近反复横跳。
比较实用的是CR=0.5这个档,它在两种极端之间取了一个平衡。但如果你用的是DE/rand/1/bin做多模态函数优化,我建议把CR提升到0.7到0.8,这样变异向量能更充分地把差分信息传导给试验个体,跳出局部极值的概率会大不少。反过来,用DE/best/1/bin时CR建议降到0.3到0.5,让最优解的信息部分保留,减少因过度交叉导致的震荡。
4. 参数与实现中的常见坑:收敛慢、早熟、维度爆炸的排查记录
4.1 早熟收敛:种群停滞在局部极值,怎么判断、怎么救
现象是收敛曲线在前几十代快速下降,然后变成一条水平线,最优值离真实全局最优差得很远。Rastrigin函数上特别典型,种群被吸进某个局部谷底就再也出不来。
原因是DE/best类策略的引导项太强,种群里的个体迅速聚集到当前最优附近,而差分扰动不足以让它们跳出谷壁。此外F值偏小也是帮凶,我在F=0.3、DE/current-to-best/1/bin配置下多次遇到这种情况。
解决的办法分两步。第一步先把F提到0.7以上,增强差分扰动的幅度;第二步换成DE/rand/1/bin或DE/current-to-rand/2/bin,放弃最优引导,用纯随机差分维持探索能力。我在维度10的Rastrigin函数上验证过,同样跑300代,rand/1/bin配F=0.8、CR=0.9能找到接近全局最优的解,而current-to-best/1/bin配F=0.5只能停在局部。
4.2 收敛极慢:目标函数在下降,但每代只动一点点
现象是收敛曲线持续下降,但每代的最优值变化只有1e-4量级,跑完所有迭代也没到精度要求。这种情况在Sphere函数上少见,在Griewank这类多尺度函数上很常见。
原因是交叉概率CR偏低,试验个体大部分维度仍保留旧值,新信息无法有效进入种群。还有可能是种群规模NP超出必要范围太多,比如维度D=10时NP设了100,差分向量之间的差异被稀释,变异步长平均下来很小。
解决方法是先看CR是不是低于0.3,如果是,直接把CR拉到0.9再跑一轮。再看NP是不是偏大,按D=10来说NP=50足够,如果设了100以上,种群多样性是够了,但收敛速度会被拖慢,因为每一代的有效搜索面积没有成比例增加。
4.3 维度爆炸:高维问题上收敛曲线整体右移
现象是函数维度从10加到50后,同样的参数配置,要跑到300代以上才开始出现明显收敛,而且最终精度比低维时差很多。
原因是维度升高后,搜索空间体积呈指数增长,固定种群规模的个体分布密度急剧下降。变异向量的差分步长在高维空间里被分散到各个维度上,每个维度实际分到的扰动幅度很小。
解决这类问题,一种做法是按维度来缩放参数。我一般遵循一个经验公式:F随维度适当增大,维度20以下F取0.5,维度50以上F取0.8到1.0。CR在高维下建议设置到0.9以上,确保变异向量能在大范围维度上生效。另一种做法是用前面提到的DE/current-to-rand/2/bin替代rand/1,用双差分来放大有效扰动步长。
4.4 边界溢出:优化变量跑出定义域,函数直接报错
现象是跑着跑着MATLAB报错,说目标函数里出现NaN或Inf,打断运行。打开函数一看,某个维度上的取值超出设定范围几个数量级。
原因是DE的变异和交叉算子都是数值运算,不会自动遵守定义域约束。比如变量定义域是[-5, 5],变异向量可能算出12甚至-20,交叉后试验个体越界。
解决的办法是在变异或交叉后加一个边界处理,最简单的方案是把越界值拉回边界或用边界内的随机值替换,我做了一个边界修补函数:
% 边界修补: 越界分量重置为边界内的随机值 function y = repair(x, lb, ub) y = x; out_low = y < lb; out_high = y > ub; y(out_low) = lb(out_low) + rand(sum(out_low), 1) .* (ub(out_low) - lb(out_low)); y(out_high) = lb(out_high) + rand(sum(out_high), 1) .* (ub(out_high) - lb(out_high)); end这个函数在每次变异和交叉之后调用一次,能确保种群始终在定义域内。我见过的另一个做法是直接截断,y = min(max(y, lb), ub),实现最简单,但会把大量个体压到边界上,种群多样性受损。我推荐上面的随机重置法。
4.5 参数敏感度陷阱:同一组参数,换个测试函数结果天差地别
现象是在Sphere函数上跑得很好的一组参数(F=0.5, CR=0.1),换到Rosenbrock或Rastrigin上几乎完全失效,收敛精度差两个数量级。
原因是不同函数的响应曲面特征差异大,Sphere是凸函数,小步长、低交叉率就能稳步逼近;Rosenbrock的谷底是狭长弯曲的,CR必须大才能沿谷底方向充分搜索;Rastrigin的局部极值密集,F必须大才能跳过多个退化率。
解决这个问题最关键的一点是意识到“参数不是一次定死”。我对这份代码的使用习惯是:跑第一个函数前先做一次小范围参数扫描,F在0.4到1.0之间取三档,CR在0.1到0.9之间取三档,9组组合各跑20代,看收敛趋势选定最合适的一组,再正式跑完整迭代。多花两分钟,能省下之后反复试错的半天时间。
5. 进阶:动态参数调整与多起点验证,让DE跑得稳、不翻车
把这份资源跑通、五种策略都能切换之后,真正让它从“能跑”变成“好用”的关键是两步。第一步是把固定参数改成动态调整,这是缓解早熟和收敛慢之间矛盾的自然解法。以F为例,我用的是简单线性递减方式:
% 动态参数: F随代数从0.9线性递减至0.5 % 前期大F保探索, 后期小F精调 F = 0.9 - (0.9 - 0.5) * (gen / G);这样前期种群能有足够大的步长漫游搜索空间,避免一上来就被某个局部极值吸引;后期步长变小,在当前最优附近做精细搜索,收敛精度会比固定F高一个量级。CR反着来,从0.3递增到0.9,前期保守交叉保持多样性,后期大胆交叉加速收敛。我在维度50的Rosenbrock函数上做过对比,动态参数版本比最优固定参数版本的收敛精度提升了大概一个数量级,而且对初始参数的敏感度明显下降。
第二步是多起点验证。DE是随机算法,单次运行的结果有运气成分。我见过有人拿一次结果就写结论,然后换了随机种子结果完全不一样。我的习惯是固定随机种子跑N次,取中位数和最优值的区间,这样评估策略好坏才可靠。做法很简单,外层套一个for循环,每次用rng(i)重置随机种子,记录每次的best_fit,最后看分布。
% 多起点验证: 固定5个随机种子, 对比结果稳定性 results = zeros(5, 1); for i = 1:5 rng(i); % 固定随机种子, 确保可复现 [best_sol, best_fit] = run_de('fun', 50, 1000, 0.7, 0.5); results(i) = best_fit; end median_result = median(results); disp(['中位最优值: ', num2str(median_result)]);在这份压缩包的基础上,我可以顺手推荐一个改进方向:把变异因子F从标量换成向量,每个维度一个F值,这样在高维问题上各个维度可以有不同的步长,收敛效率还能再提升一截。另一个思路是引入精英保留——每代把前几个最优个体直接复制到下一代,不让它们参与变异和交叉,保证最优解不会丢失。这也是我踩过坑之后得出的教训:最开始我直接用基础的DE跑参数辨识问题,一跑就是几百万次函数求值,单次结果不稳定,参数调来调去都像在摸黑。后来固定随机种子、多起点验证、动态参数三件套用上,结果才真正可复现。从那以后,我每次拿到新的DE代码,都要强制走一遍“固定随机种子→多起点跑→看分布”这个流程。希望这份资源和这些经验能帮你在优化问题上少走弯路。
本文还有配套的精品资源,点击获取