简介:差分进化算法是源于生物进化理论的全局搜索方法,适用于非线性、多模态和约束优化问题。这份MATLAB代码压缩包实现了五种典型变异策略,面向需要掌握智能优化算法的高校学生、科研人员以及工程开发者。压缩包共2个m文件,包含差分进化算法主程序与目标函数示例,体积仅1KB,结构简洁,便于直接运行和二次开发。已有1298人学习下载。通过对比DE/rand/1/bin、DE/best/1/bin、DE/current-to-best/1/bin等方案,读者可以直观理解不同变异策略如何影响收敛速度、探索与开发平衡以及跳出局部最优的能力。代码实现了初始化、变异、交叉、选择等核心流程,替换目标函数即可适配实际优化任务,也可作为参数调优和算法改进的实验基础,为工程设计、机器学习、信号处理等场景提供可复用的算法模板。
1. 差分进化算法:一份 Matlab 压缩包里的五种变异策略
我最早接触差分进化算法(DE)是在一次函数优化测试里,当时用遗传算法迭代到后半程,收敛曲线迟迟压不下去,换成 DE 之后很快就出了结果。这个压缩包就是一套纯 Matlab 实现的 DE 主程序加目标函数,跑通一个优化问题只需要两份脚本。
差分进化算法是群智能优化里性价比最高的那一种:不需要梯度,不依赖初始值,核心逻辑只有变异、交叉、选择三步,却在非线性、多模态函数上表现出很强的全局搜索能力。
适合正在做参数估计、工程设计优化的人,也适合想对比五种变异策略收敛表现的研究者。压缩包内置的 DE/rand/1/bin 到 DE/current-to-rand/2/bin 五种策略,照着跑一遍就能直观感受到策略取舍对收敛速度的影响。
2. 项目结构与调用链:DE.m 与 fun.m 的分工协作
2.1 压缩包里的两个文件:算法主体与目标函数的边界
解压之后你会发现,整个压缩包的目录结构清爽到有点出乎意料:
差分进化算法/ ├── DE.m # 差分进化算法主函数,内含五种变异策略分支 └── fun.m # 目标函数,按需替换成你自己的优化问题DE.m 是算法主体,负责种群初始化、变异、交叉、选择这四个步骤的循环控制,同时用分支结构承载五种变异策略。fun.m 是问题模型,输入一个个体向量,返回对应的适应度值。这种「算法与问题分离」的设计是智能优化代码的通用做法——算法文件从头到尾不关心你在优化什么,它只负责根据 fun.m 返回的值决定要不要保留新个体。
这里隐含了一个约定:fun.m 的输入维度必须和 DE.m 里设置的 dim 维度参数一致,否则矩阵运算会直接报维度不匹配。我一般会在 DE.m 开头加一行断言检查维度,后面换测试函数时能省很多排查时间。两个文件虽然少,却构成了一个完整闭环:改问题只动 fun.m,改搜索行为只动 DE.m,互不干扰。
2.2 四步主循环:初始化、变异、交叉、选择的执行顺序
DE 的算法骨架不长,核心流程可以用下面这段结构概括:
for gen = 1:max_iter for i = 1:pop_size % 1. 变异:基于差分向量生成突变个体 v v = mutation(pop, i, F, best_idx, strategy); % 2. 交叉:按概率 CR 混合目标个体与突变个体 u = crossover(pop(i, :), v, CR); % 3. 选择:比较新旧个体的适应度,保留更优者 if fun(u) < fun(pop(i, :)) pop(i, :) = u; end end % 记录当代最优值,用于后续画收敛曲线 end每次迭代分成三步:变异用个体间的差分向量加扰动生成新个体,交叉把新个体和目标个体按维度混搭,选择则通过比较适应度决定谁活到下一代。外层循环控制代数,内层循环遍历种群里的每个个体,这三步对所有策略都通用,差别只在变异公式本身。
参数的行为直接影响收敛效果,几个核心参数的取值区间和作用如下:
| 参数 | 常见取值 | 影响方向 |
|---|---|---|
| pop_size 种群规模 | dim 的 5~10 倍 | 太小多样性不足,太大单次迭代计算量线性上涨 |
| max_iter 最大迭代数 | 100~1000 | 决定搜索预算,过小来不及收敛 |
| F 变异因子 | 0.4~1.0 | 控制差分扰动幅度,太小早熟,太大发散 |
| CR 交叉概率 | 0.1~0.9 | 控制突变向量被继承的维度比例 |
| strategy 策略编号 | 1~5 | 对应五种变异策略,影响探索与开发的平衡 |
F 和 CR 对收敛行为的影响最大,而且两者之间还有耦合:F 偏小时 CR 不宜过大,否则种群会被少数几个强个体快速同化。这套参数调起来有些玄学,但理解每个参数的控制对象之后,至少能知道翻车时该动哪个旋钮。
2.3 从压缩包到第一次运行:最小可执行流程
把文件放进 MATLAB 当前工作目录后,命令行里按下面的方式调用即可:
% 命令行运行示例 fun = @fun; % 目标函数句柄 dim = 10; % 问题维度 lb = -5 * ones(1, dim); % 变量下界 ub = 5 * ones(1, dim); % 变量上界 [best_x, best_f] = DE(fun, dim, lb, ub, 50, 500, 0.8, 0.5, 1);这里 50 是种群规模,500 是迭代次数,0.8 是变异因子,0.5 是交叉概率,最后的 1 表示使用 DE/rand/1/bin 策略。跑通后先看 best_f 的数值是否合理,再把每代最优值画成收敛曲线,判断算法是否在前几十代就停滞。如果 fun.m 是你自己的目标函数,记得保证它接收一个 1×dim 的行向量并返回标量,这是整套代码能跑起来的前提。
3. 五种变异策略拆解:从公式到适用场景的选择框架
3.1 基础对比:rand/1 与 best/1 的探索开发之争
策略命名其实是一套规则:DE/被选中的基础向量/参与差分的向量个数/交叉方式。DE/rand/1/bin 是最经典的版本,变异公式为 v = r1 + F * (r2 - r3),三个随机个体做差分成扰动,再叠加到一个随机基础向量上。因为基础向量和差分向量全部随机,种群在搜索早期能覆盖尽量大的空间,适合多模态函数,代价是收敛速度偏慢,后期容易在最优解附近徘徊。
DE/best/1/bin 把基础向量换成当前最优个体:v = best + F * (r1 - r2)。这样一来,每一代的新个体都围绕最优解附近生成,收敛速度显著提升,但也意味着如果 best 落在局部最优,整个种群会被快速吸过去,全局搜索能力被大幅削弱。这两种策略是探索与开发的两个极端,实际使用时我一般先用 rand/1 跑几十代做全局探测,再切换 best/1 加速收敛。
需要注意,best/1 里差分向量的两项 r1 和 r2 同样必须互不相同,否则差分退化为零向量,变异就变成了 best 的克隆,种群多样性瞬间归零。这是所有 DE 策略共用的一个坑,后面还会再提。
3.2 折中方案:current-to-best 与 rand-to-best 的引导逻辑
DE/current-to-best/1/bin 和 DE/rand-to-best/1/bin 是上面两个极端的折中。current-to-best 的变异向量是 x_i + F * (best - x_i) + F * (r1 - r2),当前个体和最优个体之间的差距直接作为引导项,保证每个个体都在往 best 方向靠拢,同时保留一项随机差分来维持探索能力。这个策略的优点是对最优点附近的搜索更精细,适合目标函数较为平滑、需要快速定位的情况。
rand-to-best 则在公式里拆成两部分:一个随机基础向量,加上 best 与随机个体的差分组合。相比 current-to-best,它对 best 的依赖弱一些,种群多样性保持得更好。两种策略说到底是把那一个差分项分配到「向最优靠拢」和「保持探索」两个目标上,比例由 F 控制。我的血泪经验是:用这两类策略时 F 不要设得太大,否则向最优靠拢的引导会被随机扰动淹没,最终表现跟 rand/1 没有本质区别。
这两类策略适合中等维度、地形相对平滑的问题,它们在收敛速度和全局性之间找到了一个比较实用的平衡点。如果不知道问题地形长什么样,从 current-to-best 起步通常比从 best/1 起步更稳妥。
3.3 双差分与选型:current-to-rand/2 的适用边界
DE/current-to-rand/2/bin 是五种策略里唯一使用两个差分项的组合策略,变异公式为 v = x_i + F * (r1 - r2) + F * (r3 - r4)。四个随机个体分成两组差分,扰动幅度天然大于单差分策略,种群在搜索空间中的散布范围更大。对于存在大量局部极小值的多模态函数,这种策略能让个体更容易冲出局部陷阱,代价是收敛到精确最优解的精度可能下降。
从工程选型的角度看,五种策略没有一个绝对最优,它们只是在探索与开发这两个维度上选择了不同的平衡点。把问题维度、函数形态、允许的迭代次数三者列出来,再决定用哪套策略会更高效:
| 策略 | 变异公式要点 | 探索/开发倾向 | 典型适用场景 |
|---|---|---|---|
| DE/rand/1/bin | r1 + F*(r2-r3) | 探索为主 | 多模态函数、未知地形 |
| DE/best/1/bin | best + F*(r1-r2) | 开发为主 | 迭代次数紧张、地形平滑 |
| DE/current-to-best/1/bin | x_i + F*(best-x_i) + F*(r1-r2) | 开发为主、保留探索 | 中等维度精细寻优 |
| DE/rand-to-best/1/bin | r1 + F*(best-r2) + F*(r3-r2) | 探索开发均衡 | 维度较高但需要引导 |
| DE/current-to-rand/2/bin | x_i + F*(r1-r2) + F*(r3-r4) | 探索为主 | 局部极小值密集的函数 |
维度高、地形复杂优先选 rand/1 或 current-to-rand/2;迭代次数紧张优先选 best/1;地形相对平滑则两个混合策略性价比更高。
4. 核心代码逐段拆解:变异、交叉、选择的实现细节
4.1 函数入口与种群初始化:参数从哪来到哪去
先看 DE.m 的函数头部和初始化部分:
function [best_x, best_f] = DE(fun, dim, lb, ub, pop_size, max_iter, F, CR, strategy) % 差分进化算法主函数 % 输入参数: % fun : 目标函数句柄 % dim : 问题维度 % lb, ub : 变量下界与上界,标量或长度为 dim 的行向量 % pop_size : 种群规模 % max_iter : 最大迭代次数 % F : 变异因子,常见取值 [0.4, 1.0] % CR : 交叉概率,常见取值 [0.1, 0.9] % strategy : 变异策略编号 1~5 % 输出参数: % best_x : 找到的最优个体 % best_f : 对应的最优适应度 % 在可行域内均匀初始化种群 pop = lb + rand(pop_size, dim) .* (ub - lb); best_f = inf; end这个函数签名是标准的 DE 入口,把策略编号作为参数传入,内部再用分支选择。种群初始化用 rand 生成 [0,1) 均匀分布随机数,再映射到 [lb, ub] 区间。注意 lb 和 ub 可以用长度为 dim 的行向量对每个维度单独设界,也可以传标量统一设界,代码里用的是逐元素乘法,因此两种写法都能正确广播。
种群规模 pop_size 我一般取 dim 的 5~10 倍,太小多样性不足,太大会让单次迭代的计算量线性上涨。best_f 初始化为 inf 是为了让第一代任意有限适应度都能替换它,这是最小化问题里常见的初始化写法。
4.2 变异与交叉的代码实现:五种策略的分支结构
变异是整个实现的核心,五种策略共用一套交叉和选择逻辑,只有变异公式不同:
% 变异:按策略编号生成突变向量 switch strategy case 1 % DE/rand/1/bin r = randperm(pop_size, 3); v = pop(r(1), :) + F * (pop(r(2), :) - pop(r(3), :)); case 2 % DE/best/1/bin r = randperm(pop_size, 2); v = pop(best_idx, :) + F * (pop(r(1), :) - pop(r(2), :)); case 3 % DE/current-to-best/1/bin r = randperm(pop_size, 2); v = pop(i, :) + F * (pop(best_idx, :) - pop(i, :)) + F * (pop(r(1), :) - pop(r(2), :)); case 4 % DE/rand-to-best/1/bin r = randperm(pop_size, 3); v = pop(r(1), :) + F * (pop(best_idx, :) - pop(r(2), :)) + F * (pop(r(3), :) - pop(r(2), :)); case 5 % DE/current-to-rand/2/bin r = randperm(pop_size, 4); v = pop(i, :) + F * (pop(r(1), :) - pop(r(2), :)) + F * (pop(r(3), :) - pop(r(4), :)); end % 交叉:按维度均匀交叉,jrand 保证至少一维来自突变向量 jrand = randi(dim); u = pop(i, :); for j = 1:dim if rand() <= CR || j == jrand u(j) = v(j); end end这里用 randperm 而不是 randi 取随机索引,是为了保证同一个策略里参与运算的个体互不相同,避免差分向量退化为零向量。每行公式都能对应到上一章的数学表达式,逐个核对就能看出五种策略的差异点在哪里:基础向量选谁、差分项用几个、best 参与的程度有多大。
交叉逻辑里有两个关键细节。jrand 是 DE 的经典设置,它保证新个体至少有一维来自突变向量,防止整轮迭代完全没有发生交叉导致种群停滞。另一个细节是 u 先复制目标个体 pop(i, :),再按维度覆盖,这保证了未满足交叉条件的维度完整继承原有基因。交叉完成后,最好加一行边界截断 v = min(max(u, lb), ub),把越界个体拉回可行域再进入评估,否则界外解可能在选择阶段被保留下来。
4.3 选择算子与收敛记录:如何判断算法是否早熟
选择算子用的是最朴素的贪心策略:
% 选择:贪心保留更优个体 f_new = feval(fun, u); % 评估新个体 f_old = feval(fun, pop(i, :)); % 评估旧个体 if f_new < f_old pop(i, :) = u; % 新个体更优则替换旧个体 end % 记录本轮最优值与最优个体索引 [best_f, best_idx] = min(arrayfun(fun, pop));注意比较的是目标函数值本身,如果你的 fun.m 写的是求最大值,需要在入口把目标函数取负号转成最小化问题,否则选择方向就反了。fun.m 的编写自由度很高,但必须保证输入是一个 1×dim 的行向量、返回一个标量,这个约定一旦打破,arrayfun 会直接报错。feval 对函数句柄求值是 MATLAB 里的稳妥写法,比直接写 fun(u) 更不容易出现变量名冲突。
我习惯把每代的 best_f 存进数组而不是只保留最后一个值,运行结束后画一条收敛曲线。曲线如果在头几十代就趋于水平、而最终精度又明显偏离已知最优,基本可以判定早熟,这时优先调大 F 或换成探索性更强的策略,而不是盲目加大迭代次数。这一步能省下大量无效的重复实验。
5. 运行避坑与常见问题排查:DE 实战中的五个典型翻车现场
5.1 维度不匹配:Matrix dimensions must agree
现象是运行 DE.m 时 MATLAB 直接抛出 "Matrix dimensions must agree",红色报错指向变异那一行的向量加减。
原因是 fun.m 里定义的变量维度和 DE.m 传入的 dim 参数不一致,比如目标函数只用了 x(1) 和 x(2) 两个变量,但 dim 传成了 3,种群矩阵的列数就对不上。解决方法是统一入口:在 DE.m 初始化后加一行 assert(size(pop, 2) == dim) 做显式检查,同时在 fun.m 里按 x(1), x(2) 的方式索引变量。我后来所有这类代码都强制在初始化后打印一次 size 信息,这个坑基本不再踩。
5.2 种群早熟:收敛曲线平坦得像没有进化
现象是迭代到几十代后 best_f 几乎不变,种群个体之间距离极小,最终解离已知最优差得很远。
原因是 F 取值过小,差分向量扰动不足,所有个体被快速吸附到某个局部最优附近,也就是 DE 里的早熟现象。解决方法是先把 F 提高到 0.8 以上重跑一轮,如果曲线恢复下降趋势,再用自适应策略在迭代中动态调低 F。另一个排查点是随机索引是否发生重叠,两个差分个体相同会让变异向量退化,同样会造成曲线早平,改用 randperm 就能从源头排除。
5.3 CR 设成 1 后算法变成了随机搜索
现象是 CR=1 且收敛结果剧烈跳动,最终精度反而不如默认的 0.5。
原因是 CR 控制的是每个维度被突变向量覆盖的概率,CR=1 时新个体几乎完全等于突变向量,选择算子退化成「随机生成一堆个体挑最优」,搜索没有方向性。解决方法是把 CR 控制在 0.3~0.9 之间,并且保留 jrand 那维度强制交叉,保证目标个体的部分优秀基因能被继承。对于多模态问题,我一般从 CR=0.5 起步,效果不佳再逐次调整,而不是一步拉满。
5.4 换测试函数后同一组参数失效
现象是 fun.m 换成另一个测试函数后,原本好用的 F、CR 组合突然不收敛。
原因不是代码坏了,而是不同函数的地形特征差异很大。Rosenbrock 这类窄谷函数需要小步长精细搜索,适合大种群小 F;Rastrigin 这类大量局部极小值的函数需要大扰动跳过陷阱,适合小种群大 F。解决方法是把种群规模、F、CR 三组参数放进循环里做一次粗粒度扫描,先跑小迭代次数快速筛出几组候选,再对候选参数做完整迭代对比。这比拍脑袋定参数再反复试错高效得多。
5.5 最优解越界:缺少边界截断
现象是结果的某些维度恰好贴着 ub 上界,甚至数值上略微超过边界,导致后续代入物理模型时报错。
原因是个体在变异阶段生成了界外解,选择阶段只要界外解的适应度更小就会被保留,算法全程没有做边界处理。解决方法是每次变异和交叉后、评估适应度前,加一行 u = min(max(u, lb), ub) 做边界截断,把越界个体拉回可行域内再参与评估。这个操作会略微影响边界附近解的搜索,但对于绝大多数实际问题,硬约束比在界外多找零点几个百分点的精度更重要。
6. 二次开发:把固定策略升级成自适应差分进化框架
6.1 自适应参数与策略池:让 F、CR 自己进化
拿到这份代码后最容易上手的改造方向,是把写死的 F、CR 变成随迭代动态变化的值。常见做法是引入记忆机制——每代结束后统计成功替换了旧个体的那些 F 与 CR 值,下一代用这些成功值的加权平均来生成参数。这样前期自动保持高扰动,后期自动收敛到小步长,能省掉大量手工调参时间。
策略池改造更直接:把 switch 分支改成按概率从 1~5 中抽取策略编号,给每种策略记录历史成功次数,成功率高的策略被抽中的概率提高。每代更新权重的逻辑大致是:
% 每代结束,按历史成功率更新策略抽签概率 for s = 1:5 success_rate(s) = success_count(s) / max(used_count(s), 1); end prob = success_rate / sum(success_rate); next_strategy = randsample(1:5, 1, true, prob);6.2 验证方法与改造习惯
改造后的验证流程,我会固定一套测试函数集,分别跑原始版本和自适应版本,对比同一个 max_iter 下的收敛曲线和最终精度,确认自适应版本不会因为策略切换引入不稳定。只跑一个函数就下结论是最容易翻车的做法,至少要覆盖一个平滑函数和一个多模态函数。从那以后,我每次拿到新的智能优化算法代码,都强制先跑一遍基线、再做参数扫描、最后才动策略逻辑,这套习惯帮我避开了无数个「改完反而更差」的坑。希望帮到你。
本文还有配套的精品资源,点击获取