MATLAB遗传算法带约束优化:改进实现与调参避坑指南
简介这份资源面向具备一定MATLAB基础、需要处理带约束优化问题的工程人员与算法学习者提供了一套改进遗传算法的完整代码实现。压缩包共9个文件全部为m脚本体积约4KB涵盖种群初始化、适应度计算、竞赛选择、环境选择、交叉、变异以及主流程调度等模块结构紧凑、职责清晰便于逐文件阅读算法细节。其中目标函数计算与约束处理逻辑独立成文件方便替换为具体工程问题选择策略同时包含竞赛选择与环境选择体现了对种群多样性与收敛速度的平衡考虑。已有1712人学习下载说明该实现具备一定的参考价值。读者可借助主入口脚本一键运行观察各代个体信息记录与进化过程并在此基础上调整参数、替换目标函数快速搭建适用于自身优化任务的算法框架同时理解改进遗传算法在约束处理上的工程思路。1. 带约束优化为什么总翻车从一份 MATLAB 遗传算法代码说起做工程优化的人大多有过这种经历目标函数写好了约束也列全了跑标准遗传算法结果要么种群集体撞墙、要么收敛到违反约束的“假最优”要么迭代几百代还在原地打转。带约束的优化问题难点从来不在目标函数本身而在怎么让一群随机个体在可行域边界上“学会走路”。这份 MATLAB 代码精讲资源核心就是一套改进的遗传算法实现专门处理带约束的通用优化问题——等式约束、不等式约束、上下界都能接。它适合两类人一类是写过标准 GA 但被约束处理卡住的开发者另一类是想把优化算法真正落到工程参数整定、结构设计、调度分配场景里的从业者。代码是 MATLAB 写的结构清晰改目标函数和约束函数就能套到自己的问题上不是那种只能跑 demo 的玩具。2. 约束处理机制拆解罚函数、可行性规则与改进点在哪2.1 标准遗传算法处理约束的三种常见思路遗传算法本身是无约束搜索方法处理约束得额外加机制。常见做法有三类。第一类是罚函数法把约束违反量乘一个惩罚系数加进适应度违反越多惩罚越重。优点是实现简单缺点是惩罚系数极难调——太小了种群在不可行域乱逛太大了直接把搜索压死在可行域边缘而且对等式约束几乎无效。第二类是可行性规则Feasible Rule 那套两个个体比较时可行个体永远优于不可行个体都可行比目标值都不可行比违反程度。这个规则不需要调参但有个致命问题——如果初始种群全不可行算法前期没有任何梯度信息等于盲搜。第三类是修复策略把不可行个体投影回可行域但通用问题上投影算子很难写等式约束尤其麻烦。这份代码的改进点在于把可行性规则和自适应罚函数做了混合。它不是单纯用某一种而是根据种群当前的可行比例动态切换策略可行个体少的时候用罚函数引导可行个体多了以后切换到可行性规则精搜。这个思路在工程上很实用因为大多数实际问题初始种群确实很难全部可行。2.2 代码结构与关键函数说明拿到代码包后先看主脚本和几个核心函数的分工。主脚本负责参数配置和主循环调度约束函数单独一个文件适应度计算里嵌了约束处理逻辑。下面这段是适应度评估的核心结构我按自己的理解重写了一遍关键部分逻辑和原代码一致function [fitness, violation] evaluateIndividual(x, objFunc, conFunc, penaltyBase) % x: 决策变量向量 % objFunc: 目标函数句柄 % conFunc: 约束函数句柄返回 [g, h]g0 不等式h0 等式 % penaltyBase: 罚函数基础系数 f objFunc(x); % 计算目标值 [g, h] conFunc(x); % 计算约束违反量 % 不等式约束违反量只取正值部分 gViolation sum(max(0, g)); % 等式约束违反量取绝对值 hViolation sum(abs(h)); totalViolation gViolation hViolation; % 自适应罚系数违反越大惩罚越重但不是线性 if totalViolation 0 penalty penaltyBase * (1 totalViolation)^2; else penalty 0; end fitness f penalty; % 罚后适应度 violation totalViolation; % 单独返回违反量供可行性规则用 end这段代码有几个参数值得说清楚。penaltyBase是罚函数基础系数代码里默认给的是 1e3 量级但实际用的时候要根据目标函数量级调——如果目标函数值在 1e6 级别罚系数至少得 1e8 起步否则约束根本压不住。等式约束的违反量用绝对值求和这是通用做法但如果你的问题里等式约束量纲差异大最好先做归一化再求和不然某个量纲大的约束会主导整个违反量。totalViolation单独返回是为了后面可行性规则比较用这个设计比单纯返回罚后适应度要灵活。2.3 种群初始化与约束满足的平衡初始种群怎么生成直接决定算法前几十代的表现。标准做法是随机均匀采样但带约束问题里这样生成的个体大概率全部不可行。代码里用了一个折中方案先随机生成大种群然后按违反量排序取违反量最小的那部分作为初始种群。这样虽然不能保证初始可行但至少让种群起点靠近可行域。function pop initPopulation(popSize, lb, ub, conFunc, maxTry) % popSize: 目标种群大小 % lb, ub: 变量上下界 % conFunc: 约束函数 % maxTry: 最大尝试倍数 nVar length(lb); candidateNum popSize * maxTry; % 先生成更大的候选池 candidates repmat(lb, candidateNum, 1) ... rand(candidateNum, nVar) .* repmat(ub - lb, candidateNum, 1); violations zeros(candidateNum, 1); for i 1:candidateNum [~, v] evaluateIndividual(candidates(i,:), (x)0, conFunc, 1); violations(i) v; end [~, idx] sort(violations); % 按违反量升序 pop candidates(idx(1:popSize), :); % 取违反最小的 popSize 个 endmaxTry这个参数控制候选池大小默认给 10 到 20 比较合适。太小了筛选效果不明显太大了初始化阶段就耗掉大量计算。注意这里调evaluateIndividual时目标函数传了个空句柄因为初始化阶段只关心违反量不关心目标值。这个细节在代码注释里没写但读的时候能看出来。3. 参数整定与运行流程从配置到出结果的完整链路3.1 关键参数表与推荐取值范围代码里暴露出来的可调参数有十来个但真正影响结果的其实就五六个。下面这张表是我跑过几类问题后总结的推荐范围不是原代码写死的值而是实际调参时的经验区间参数名含义推荐范围调参方向popSize种群规模50200变量维度高就取大maxGen最大迭代代数2001000看收敛曲线定pc交叉概率0.70.95一般不低于 0.7pm变异概率0.010.1维度高取小值penaltyBase罚函数基础系数1e31e8跟目标函数量级匹配maxTry初始化候选倍数1020约束越紧取越大交叉概率和变异概率这两个参数标准 GA 里通常建议 pc 取 0.8 左右、pm 取 1/nVar 左右。但带约束问题里变异率可以适当放大因为需要更多探索来找到可行域边界。我一般会把 pm 设在 0.05 附近比理论值大一些实测收敛更稳。3.2 主循环执行流程与终止条件主循环的结构不复杂但有几个执行顺序上的细节容易写错。正确的顺序是评估当前种群 → 选择 → 交叉 → 变异 → 边界处理 → 评估子代 → 合并择优。下面这段是主循环的骨架for gen 1:maxGen % 评估当前种群 fitness zeros(popSize, 1); violation zeros(popSize, 1); for i 1:popSize [fitness(i), violation(i)] evaluateIndividual(pop(i,:), objFunc, conFunc, penaltyBase); end % 记录最优 [bestFit, bestIdx] min(fitness); bestHistory(gen) bestFit; bestIndividual pop(bestIdx, :); % 选择锦标赛 selected tournamentSelect(pop, fitness, violation, popSize); % 交叉 offspring crossover(selected, pc, lb, ub); % 变异 offspring mutate(offspring, pm, lb, ub); % 边界处理越界拉回 offspring max(offspring, repmat(lb, popSize, 1)); offspring min(offspring, repmat(ub, popSize, 1)); % 精英保留用最优个体替换子代中最差的一个 offspring(1, :) bestIndividual; pop offspring; end精英保留那一步很关键。没有精英保留的话最优解可能在交叉变异中被破坏掉收敛曲线会出现明显震荡。代码里用最优个体直接替换子代第一个个体简单粗暴但有效。终止条件除了最大代数还可以加一个适应度停滞判断——连续多少代最优值变化小于阈值就提前退出省时间。3.3 结果验证怎么确认解是可行的跑完算法拿到最优个体后第一件事不是看目标值多小而是验证约束满足情况。很多人跑完直接看目标值结果解根本不可行白高兴一场。验证方法很简单% 验证最优解的约束满足情况 xBest bestIndividual; [g, h] conFunc(xBest); fprintf(不等式约束最大值: %.6f\n, max(g)); fprintf(等式约束最大绝对值: %.6f\n, max(abs(h))); fprintf(目标函数值: %.6f\n, objFunc(xBest));不等式约束最大值应该小于等于 0等式约束绝对值应该接近 0。如果等式约束差得远说明罚系数不够大或者等式约束处理方式有问题。我一般会要求等式约束残差在 1e-4 以下才算通过工程上更严的场合要压到 1e-6。4. 避坑与排查带约束 GA 最常见的五类翻车现场4.1 种群全部不可行算法原地踏步现象跑了几十代最优个体的违反量几乎不降目标值也没变化。原因通常是初始种群质量太差加上罚系数太小导致不可行个体和可行个体在适应度上区分不开。解决方法是先把penaltyBase调大一到两个数量级同时把maxTry从 10 提到 20让初始种群里有更多低违反量的个体。如果还不行就得检查约束函数本身是不是写错了——比如不等式方向搞反了把g0写成了g0。4.2 收敛到可行解但目标值很差现象约束全部满足但目标函数值比预期差很多。原因一般是种群多样性丢失太快算法过早收敛到可行域某个角落。解决方法是提高变异概率pm或者把交叉概率pc降到 0.7 以下同时检查选择算子是不是压力太大——锦标赛规模如果设成 10选择压力会非常大改成 3 到 5 会好很多。4.3 等式约束始终无法满足现象不等式约束都满足但等式约束残差一直在 1e-2 量级降不下去。原因是罚函数对等式约束的处理方式太弱平方惩罚在残差小的时候梯度也小算法没有动力继续压。解决办法有两个一是把等式约束的罚系数单独放大不和不等式约束共用一个penaltyBase二是改用可行性规则让等式约束违反量在个体比较时占更高权重。4.4 迭代后期适应度震荡不收敛现象收敛曲线在最后几十代上下波动最优值反复跳。原因通常是精英保留没做好或者变异率在后期太大。解决方法是确认精英保留逻辑生效同时可以加一个自适应变异——前期变异率大后期逐步减小。代码里如果没带这个机制自己加一个线性递减就行。4.5 换一个问题就完全跑不动现象在示例问题上跑得很好换成自己的目标函数和约束后效果极差。原因多半是量纲问题——目标函数和约束函数的数值量级差异太大罚系数顾此失彼。解决办法是先做归一化把目标函数和约束都缩放到相近量级再重新调penaltyBase。这一步很多人偷懒不做结果就是反复调参反复翻车。5. 进阶用法自适应罚系数与多问题复用的工程习惯代码跑通只是第一步真正要把它用顺得在自适应机制上做点文章。原代码里的罚系数虽然叫“自适应”但本质上还是基于违反量的平方放大没有根据种群可行比例动态调整。我一般会加一层外层控制每代统计可行个体比例如果可行比例低于 20%就把罚系数乘 1.5如果高于 80%就除以 1.2。这样算法前期有足够压力往可行域走后期又不会因为罚太重而失去探索能力。% 自适应罚系数调整加在主循环内 feasibleRatio sum(violation 1e-6) / popSize; if feasibleRatio 0.2 penaltyBase penaltyBase * 1.5; elseif feasibleRatio 0.8 penaltyBase penaltyBase / 1.2; end penaltyBase min(max(penaltyBase, 1e2), 1e10); % 限幅这段逻辑加进去以后换问题时的调参工作量会小很多。限幅那一步别省不然罚系数可能爆掉或者缩到零。另一个习惯是给每个新问题建一个独立的配置文件把目标函数、约束函数、变量范围、推荐参数都写在一个结构体里主脚本只读配置不写死参数。这样同一套算法代码可以在多个项目间复用不用每次改主脚本。我吃过亏——早期把参数写在主脚本里换问题的时候改漏了一个地方跑了一晚上结果全是错的。从那以后我每次新建问题都强制走一遍配置分离的流程再也没出现过这种低级错误。验证环节也有个技巧除了看收敛曲线我还会把最优解代回约束函数把每个约束的满足情况单独打印出来。有时候总体违反量很小但某个约束其实已经踩线了工程上这种解是不能用的。单独打印能一眼看出哪个约束最紧方便判断结果是否可靠。希望这套代码和上面的调参思路能帮你把带约束优化这件事从“跑不通”变成“跑得稳”。本文还有配套的精品资源点击获取