资讯动态

改进遗传算法求解带约束优化问题:MATLAB实现与避坑指南

发布时间:2026/10/14 1:26:24 来源:尧图企业网站定制
简介这份资源面向具备一定MATLAB基础、需要解决带约束优化问题的工程人员与学生提供一套改进遗传算法的完整代码实现。压缩包共9个文件全部为.m脚本体积约4KB涵盖种群初始化、适应度计算、竞赛选择、环境选择、交叉、变异及主流程调度等模块结构紧凑、职责分明。其中目标函数计算与约束处理独立成文件便于替换为具体工程问题选择策略同时引入竞赛选择与环境选择兼顾收敛速度与种群多样性记录模块可追踪每代个体的适应度与基因编码方便分析进化过程。运行主入口即可观察算法在各阶段的行为并通过调整参数适配不同优化任务。目前已有1712人学习下载作者结合自身工程经验给出了改进思路适合希望快速上手遗传算法、理解约束处理与选择机制细节的读者参考借鉴。1. 带约束优化为什么总在罚函数上翻车做过工程优化的人大多有过这种经历目标函数写得清清楚楚约束条件也列得明明白白可一跑遗传算法要么种群集体飞出可行域要么收敛到某个违反约束的“假最优”上调半天参数还是玄学。问题往往不在遗传算法本身而在约束处理方式。带约束的优化问题本质是在可行域内找极值而标准遗传算法只认适应度不认边界。罚函数法是最常见的补丁但固定罚因子的做法在工程里极易翻车罚小了约束形同虚设罚大了可行域边缘的优良个体被一棍子打死搜索直接退化成随机游走。这篇笔记围绕改进的遗传算法求解带约束优化问题展开重点讲清楚三件事约束到底怎么处理才稳、改进点落在哪几个算子、MATLAB 里怎么把它跑通并验证。适合已经写过基础遗传算法、被约束问题卡过、想找一套通用可复用方案的工程师和研究生。下面所有代码都是可复现的最小实现参数含义和调整方向会逐个说明不堆砌花哨技巧只讲能落地的部分。2. 约束处理的三种主流路线与选型理由在动手改代码之前得先把约束处理的路线选清楚。选错了路线后面算子改得再花哨也是白费。工程上常见的有罚函数法、可行性规则、以及多目标转化法三者适用场景差别很大。2.1 罚函数法实现最简单但罚因子是命门罚函数法的思路是把约束违反量折算成惩罚项加进目标函数把约束问题变成无约束问题。静态罚函数形式简单但罚因子需要反复试动态罚函数让罚因子随迭代增大前期允许探索、后期强制可行是工程里更稳的选择。我一般用动态罚函数作为基线因为它对初始罚因子不敏感容错率高。function fitness dynamicPenalty(x, objFun, constraints, t, maxGen) % x: 决策变量向量 % objFun: 目标函数句柄 % constraints: 约束函数句柄返回违反量向量0 为满足 % t: 当前代数, maxGen: 最大代数 obj objFun(x); viol constraints(x); viol(viol 0) 0; % 只累计违反量 totalViol sum(viol); if totalViol 0 fitness obj; % 可行解直接用目标值 else % 罚因子随代数指数增长前期宽松后期严格 penalty 1e2 * (t / maxGen)^2; fitness obj penalty * totalViol; end end这段代码的关键在罚因子的增长曲线。(t/maxGen)^2是二次增长前期增长慢给种群留出探索空间后期增长快把种群压回可行域。初始系数1e2要根据目标函数量级调整如果目标值在 1e6 量级这个系数得同步放大否则惩罚项被目标值淹没。判断标准很简单跑完后看最优解的违反量如果大于 1e-4说明罚因子偏小。2.2 可行性规则不调罚因子但容易早熟可行性规则的核心是三条比较准则可行解优于不可行解两个可行解比目标值两个不可行解比违反量。它彻底绕开了罚因子调参实现也干净。但问题在于当种群长期没有可行解时选择压力全压在违反量上多样性掉得飞快容易早熟收敛到一个局部可行点。我一般只在约束边界清晰、可行域占比不算太小的场景用它。2.3 多目标转化把约束当第二个目标把约束违反量当成第二个目标用多目标思路处理能得到一组权衡解。好处是信息量大坏处是计算量翻倍而且最终还得从 Pareto 前沿里挑一个解工程上多一道决策成本。除非你确实需要看约束与目标的权衡关系否则不建议一上来就用。三条路线的对比如下路线调参难度早熟风险计算开销适用场景动态罚函数中低低通用推荐基线可行性规则低高低可行域占比大多目标转化高低高需要权衡信息选型结论默认用动态罚函数打底如果发现种群多样性掉得太快再叠加可行性规则的比较准则做混合。下面章节的改进算法就基于这个组合。3. 改进遗传算法的四个关键算子基础遗传算法在带约束问题上表现差根子在四个算子初始化、选择、交叉、变异。逐个改每个改动都要有明确目的不能为了改而改。3.1 初始化可行域引导 随机兜底纯随机初始化在可行域占比小的场景下可能几千个个体里没一个可行的算法起步就废了。改进做法是先做可行域引导采样再用随机个体兜底保证多样性。function pop initPopulation(nPop, nVar, lb, ub, constraints, maxTry) % nPop: 种群规模, nVar: 变量维度 % lb/ub: 下界/上界, constraints: 约束函数 % maxTry: 单个体最大尝试次数 pop zeros(nPop, nVar); for i 1:nPop found false; for k 1:maxTry cand lb (ub - lb) .* rand(1, nVar); if all(constraints(cand) 0) pop(i, :) cand; found true; break; end end if ~found % 兜底用违反量最小的候选保证不空转 best cand; bestViol sum(max(constraints(cand), 0)); for k 1:20 cand lb (ub - lb) .* rand(1, nVar); v sum(max(constraints(cand), 0)); if v bestViol best cand; bestViol v; end end pop(i, :) best; end end endmaxTry一般设 100 到 200太小了引导采样没意义太大了在可行域极小时浪费时间。兜底逻辑保证每个个体都是当前尝试里违反量最小的避免种群被完全不可行的个体占满。这一步在可行域占比低于 5% 的问题上效果最明显。3.2 选择可行性优先的锦标赛标准锦标赛只比适应度在约束问题里会把违反约束但目标值好看的个体选进来。改成可行性优先的两级比较先看是否可行可行解永远赢不可行解同可行性下再比适应度或违反量。function parent selectTournament(pop, fit, viol, k) % pop: 种群, fit: 适应度, viol: 违反量 % k: 锦标赛规模 n size(pop, 1); idx randperm(n, k); best idx(1); for i 2:k a idx(i); if viol(a) 0 viol(best) 0 best a; % 可行解优先 elseif (viol(a) 0) (viol(best) 0) if viol(a) 0 if fit(a) fit(best), best a; end % 都可行比目标 else if viol(a) viol(best), best a; end % 都不可行比违反量 end end end parent pop(best, :); endk取 2 到 4 比较合适。k 越大选择压力越大收敛快但多样性差k 越小探索性强但收敛慢。带约束问题我一般取 3兼顾两者。注意这里的fit是原始目标值不是加了罚函数的适应度罚函数只用于最终排序选择阶段用可行性规则更干净。3.3 交叉模拟二进制交叉加边界修复实数编码下模拟二进制交叉SBX是常用选择分布指数控制子代靠近父代的程度。交叉后子代可能越界必须做边界修复否则约束违反量会突然爆炸。function [c1, c2] sbxCross(p1, p2, lb, ub, eta) % eta: 分布指数越大子代越靠近父代 n length(p1); c1 zeros(1, n); c2 zeros(1, n); for i 1:n if rand 0.5 if abs(p1(i) - p2(i)) 1e-14 if p1(i) p2(i) y1 p1(i); y2 p2(i); else y1 p2(i); y2 p1(i); end randV rand; beta 1 2 * (y1 - lb(i)) / (y2 - y1); alpha 2 - beta^(-(eta 1)); if randV 1 / alpha betaq (randV * alpha)^(1 / (eta 1)); else betaq (1 / (2 - randV * alpha))^(1 / (eta 1)); end c1(i) 0.5 * ((y1 y2) - betaq * (y2 - y1)); c2(i) 0.5 * ((y1 y2) betaq * (y2 - y1)); else c1(i) p1(i); c2(i) p2(i); end else c1(i) p1(i); c2(i) p2(i); end % 边界修复 c1(i) min(max(c1(i), lb(i)), ub(i)); c2(i) min(max(c2(i), lb(i)), ub(i)); end endeta取 15 到 20 是常见范围值越大子代越贴近父代探索能力下降。边界修复用简单的截断虽然会损失一点分布信息但胜在稳定工程上够用。如果问题对边界敏感可以换成反射修复但实现复杂度上升。3.4 变异多项式变异配自适应概率多项式变异在实数编码里表现稳定变异概率用自适应策略前期高、后期低兼顾探索和收敛。function x polyMutation(x, lb, ub, eta, pm) n length(x); for i 1:n if rand pm delta1 (x(i) - lb(i)) / (ub(i) - lb(i)); delta2 (ub(i) - x(i)) / (ub(i) - lb(i)); randV rand; mutPow 1 / (eta 1); if randV 0.5 xy 1 - delta1; val 2 * randV (1 - 2 * randV) * xy^(eta 1); deltaq val^mutPow - 1; else xy 1 - delta2; val 2 * (1 - randV) 2 * (randV - 0.5) * xy^(eta 1); deltaq 1 - val^mutPow; end x(i) x(i) deltaq * (ub(i) - lb(i)); x(i) min(max(x(i), lb(i)), ub(i)); end end end自适应变异概率按pm 0.2 * (1 - t/maxGen) 0.05计算前期约 0.25后期降到 0.05。eta取 20 左右。这个策略在多个测试函数上比固定概率稳尤其是约束边界附近后期小概率变异能避免把已经可行的解又踢出去。4. 在 MATLAB 里跑通完整流程算子都齐了接下来把它们串成完整流程并给出一个可复现的测试用例。整个主循环的结构是初始化、评价、选择、交叉、变异、合并、环境选择。4.1 主循环骨架与参数配置function [bestX, bestF, history] constrainedGA(objFun, constraints, ... nVar, lb, ub, opts) % opts: 参数结构体 nPop opts.nPop; maxGen opts.maxGen; etaC opts.etaC; etaM opts.etaM; pc opts.pc; pop initPopulation(nPop, nVar, lb, ub, constraints, 200); history zeros(maxGen, 2); % [最优目标, 最优违反量] for t 1:maxGen % 评价 fit zeros(nPop, 1); viol zeros(nPop, 1); for i 1:nPop fit(i) objFun(pop(i, :)); v constraints(pop(i, :)); viol(i) sum(max(v, 0)); end % 记录 [~, bi] min(fit 1e6 * viol); history(t, :) [fit(bi), viol(bi)]; % 生成子代 offspring zeros(nPop, nVar); for i 1:2:nPop p1 selectTournament(pop, fit, viol, 3); p2 selectTournament(pop, fit, viol, 3); if rand pc [c1, c2] sbxCross(p1, p2, lb, ub, etaC); else c1 p1; c2 p2; end pm 0.2 * (1 - t / maxGen) 0.05; c1 polyMutation(c1, lb, ub, etaM, pm); c2 polyMutation(c2, lb, ub, etaM, pm); offspring(i, :) c1; if i 1 nPop, offspring(i 1, :) c2; end end % 合并父子代环境选择 combined [pop; offspring]; nComb size(combined, 1); cfit zeros(nComb, 1); cviol zeros(nComb, 1); for i 1:nComb cfit(i) objFun(combined(i, :)); v constraints(combined(i, :)); cviol(i) sum(max(v, 0)); end % 可行性优先排序 rankKey cfit 1e6 * (cviol 0) cviol; [~, order] sort(rankKey); pop combined(order(1:nPop), :); end % 输出最优 fit zeros(nPop, 1); viol zeros(nPop, 1); for i 1:nPop fit(i) objFun(pop(i, :)); v constraints(pop(i, :)); viol(i) sum(max(v, 0)); end [~, bi] min(fit 1e6 * viol); bestX pop(bi, :); bestF fit(bi); end参数配置建议nPop取 100 到 200maxGen取 200 到 500etaC取 15etaM取 20pc取 0.9。这些值在多数中等规模问题上不用大调。rankKey里的1e6是可行性优先的硬阈值保证可行解永远排在不可行解前面量级要大于目标函数的可能范围。4.2 一个带约束测试用例用一个经典带约束问题验证目标函数是二次型约束包含线性不等式和变量边界。% 目标函数 objFun (x) (x(1) - 2)^2 (x(2) - 3)^2 (x(3) - 1)^2; % 约束x1 x2 x3 6, x1*x2 8, 其余为边界 constraints (x) [6 - (x(1) x(2) x(3)); % 0 表示满足 x(1) * x(2) - 8]; nVar 3; lb [0 0 0]; ub [5 5 5]; opts struct(nPop, 120, maxGen, 300, ... etaC, 15, etaM, 20, pc, 0.9); [bestX, bestF, history] constrainedGA(objFun, constraints, ... nVar, lb, ub, opts); fprintf(最优解: %s\n, mat2str(round(bestX, 4))); fprintf(最优目标: %.6f\n, bestF); fprintf(最终违反量: %.2e\n, history(end, 2));跑完后检查三件事最终违反量是否接近 0最优解是否落在约束边界附近history曲线是否在后期趋于平稳。如果违反量停在 1e-2 量级下不去多半是罚因子或可行性优先阈值的问题回到第 2 章调。4.3 收敛曲线与结果验证验证不能只看最终值要看收敛过程。把history画出来观察目标值和违反量两条曲线的走势。figure; subplot(2,1,1); plot(history(:,1), b-, LineWidth, 1.2); xlabel(代数); ylabel(最优目标值); title(目标值收敛曲线); grid on; subplot(2,1,2); semilogy(history(:,2) 1e-12, r-, LineWidth, 1.2); xlabel(代数); ylabel(违反量(对数)); title(约束违反量收敛曲线); grid on;健康的曲线形态是目标值前期快速下降后期小幅震荡收敛违反量在几十代内降到 1e-6 以下并保持。如果违反量曲线反复抬头说明变异概率偏大或可行性优先阈值不够硬。如果目标值早早平掉但违反量还高说明种群过早挤到不可行区域需要加大初始化引导力度。5. 避坑与排查五个真实踩过的坑这一章记录我在实际项目里踩过的坑每条按现象、原因、解决写清楚都是血泪经验。5.1 种群集体不可行算法空转现象跑了几十代违反量曲线纹丝不动最优解全是不可行个体。原因可行域占比太小纯随机初始化几乎采不到可行解选择压力又全压在违反量上种群多样性迅速丢失。解决启用第 3.1 节的可行域引导初始化maxTry提到 300同时在选择阶段保留一定比例的不可行个体别让可行解一出现就把其他个体全淘汰。5.2 收敛到违反约束的假最优现象目标值很好看但违反量始终在 1e-3 量级解不可用。原因罚因子太小惩罚项被目标值淹没算法把违反约束当成了可接受的代价。解决把动态罚函数的初始系数按目标函数量级放大或者直接用可行性优先的硬阈值排序让可行解无条件胜出。判断标准是最终违反量必须小于 1e-6。5.3 后期震荡不收敛现象目标值在最后几十代来回跳没有稳定趋势。原因变异概率没有自适应后期还在用大变异把已经收敛的优良个体反复破坏。解决用pm 0.2*(1-t/maxGen)0.05的自适应策略后期变异概率降到 0.05 左右。如果还震荡检查交叉分布指数etaC是不是太小调大到 20。5.4 边界修复引入额外违反现象交叉变异后子代违反量突然比父代还大。原因SBX 交叉产生的子代可能越界没做边界修复就直接评价越界量被算进约束违反。解决交叉和变异后都做截断修复如第 3.3 节代码所示。注意修复要在评价之前顺序不能反。5.5 参数换一个问题就失效现象在测试函数上跑得好好的参数换个问题就崩。原因种群规模和代数没随问题维度调整低维问题的参数直接搬到高维问题上搜索能力不够。解决nPop按10*nVar起步maxGen按200 50*nVar起步再根据收敛曲线微调。别指望一套参数打天下通用算法也得做参数适配。6. 进阶技巧把约束违反量用起来前面讲的都是怎么让算法找到可行最优解这一章换个角度约束违反量本身携带的信息其实可以用来加速搜索和做验证。这是我在多个项目里反复验证过的一个技巧成本低、收益明显。核心思路是维护一个违反量的历史档案。每次评价个体时除了记录目标值和违反量还把违反量按约束分量拆开存下来。跑完后分析哪个约束分量最难满足、哪个分量在后期还在波动。难满足的分量说明可行域边界紧可以在初始化时针对性加密采样后期波动的分量说明变异还在破坏它可以对该分量对应的变量降低变异概率。function archive evaluateWithArchive(pop, objFun, constraints, archive) % 在评价的同时记录分约束违反量 n size(pop, 1); for i 1:n obj objFun(pop(i, :)); v constraints(pop(i, :)); v(v 0) 0; archive.obj(end1) obj; archive.viol(end1) sum(v); archive.detail(end1, :) v; % 分约束记录 end end跑完后用mean(archive.detail)看每个约束的平均违反量用std(archive.detail(end-100:end, :))看后期波动。平均违反量大的约束在初始化时把采样点往它的边界附近靠后期波动大的约束把相关变量的变异概率乘 0.5。这个调整不需要改算法结构只在参数层面动风险低。另一个用法是验证。最终解的违反量如果某个分量刚好在 1e-7 量级说明这个约束是紧约束解落在边界上符合预期如果某个分量是 0 但离边界很远说明这个约束是松的可以检查是不是约束写错了或者量级不对。这种逐分量的检查比只看总违反量靠谱得多能提前发现建模错误。我自己的习惯是任何带约束的优化任务跑完必看分约束违反量紧约束和松约束各是哪些心里有数才敢把结果交出去。有几次就是靠这个检查发现约束不等式方向写反了不然拿着一个违反物理意义的解去汇报后果比调参翻车严重得多。这套流程不复杂但能省下大量返工时间希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑