
简介这是面向多目标优化研究者及Matlab初学者的NSGA-II算法完整实现包。算法由Deb等提出通过非支配排序与拥挤距离维护一组Pareto前沿解适合处理目标函数冲突的工程与运筹问题。压缩包内共10个文件其中8个.m脚本覆盖初始化变量、锦标赛选择、模拟二进制交叉、多项式变异、非支配排序、目标评估及染色体替换等核心步骤主程序nsga_22.m可直接运行另含2个xlsx结果表便于查看寻优输出与后续分析。整个压缩包仅660KB轻量易部署。目前已有3333人学习下载。对照代码可以快速掌握非支配排序、精英保留及拥挤距离计算的具体实现并以此为模板改造成自定义目标函数用于教学演示或实际项目中的多目标寻优是理解NSGA-II原理、上手Matlab编程的高质量实用素材。1. NSGA2与Matlab的多目标求解调参一天看不到理想前沿时问题出在哪很多人拿着NSGA2的Matlab代码跑多目标优化第一轮迭代看着还行改两行目标函数之后就完全不是那么回事要么前沿挤在一个角上要么跑了几十代还是一堆重复解。这时候问题往往不在算法本身而在你还没把“非支配排序”和“拥挤度”这两个核心机制吃透。NSGA2是解决多目标优化问题最常用的遗传算法家族成员它的长处不是像加权和那样把多个目标压成一个数而是同时找出一整套有代表性的最优解让决策者后面去挑。这篇笔记把NSGA2的原理、Matlab骨架实现、最常见翻车点和验证习惯一次讲完适合正在做多目标问题但不确定参数和代码怎么落地的开发者。2. 非支配排序与拥挤度NSGA2的两大核心机制是怎么定义的2.1 帕累托支配两个解谁赢谁输的判定规则多目标优化里解之间不是所有时候都有明确的胜负。假设现在有两个目标都是越小越好解A的两个目标值是(2,5)解B是(3,1)A在第一个目标上占优B在第二个目标上占优谁也没完全压过谁。按数学上的说法它们互不支配。这时如果出现一个解C(1,2)它在两个目标上都不比A、B差而且至少有一个目标严格更好那C支配A也支配B。支配关系的定义用一句话说对最小化问题解X支配解Y当且仅当X在所有目标上都不劣于Y且至少在其中一个目标上严格优于Y。注意关键词是“所有目标都不劣”加“至少一个严格优”。只要有一个目标比对方差哪怕其他目标再好也不是支配关系只是互不支配。非支配排序做的事情就是把整个种群按支配关系一层层揭开先找出所有不被任何个体支配的解作为第一层帕累托前沿把这一层暂时拿掉再在剩余个体里找不被任何剩余个体支配的解作为第二层重复这个过程直到所有个体都被分层。层数越靠前说明这个解越“接近最优”。举个例子5个个体在两个最小化目标下的值如下表个体f1f2所在前沿P12.08.01P23.05.01P34.54.01P45.08.02P56.06.02P1、P2、P3之间互不支配谁也没法完全压过另外两个所以它们一起进第一前沿。P4被P1支配f1和f2都比P1差被P2支配被P3支配P5被P2和P3支配但P4和P5谁也不能完全压过谁所以它们进第二前沿。这个分层结果就是选择的基础第一前沿的个体一定比第二前沿优先保留。为什么强调这个机制因为传统加权和法把多个目标乘上权重加起来变成一个数等于人为预设了目标之间的“兑换率”。实际工程里这个兑换率往往拍脑袋定比如成本和性能的关系根本说不准。NSGA2不做这个预设它只维护“哪些解不被其他解压过”这一套排序关系让解的优劣由数据说话而不是由权重说话。2.2 拥挤度距离保证解的分布不扎堆非支配排序只告诉你在同一层里“谁和谁是同一级别”但同一层里几十个解到底保留哪些、淘汰哪些还需要第二个指标——拥挤度距离。拥挤度距离的直观含义是这个解在目标空间里离旁边两个邻居有多远。计算分三步。第一步对同一个前沿内的所有个体按某个目标值从小到大排序第二步排在最前和最后的边界个体的拥挤度直接设为无穷大因为它们无论如何也不会被淘汰淘汰它们会让前沿端点丢失第三步对中间的每个个体取它前后两个邻居在该目标上的差值除以该目标在全体个体中的取值范围得到一个无量纲的相对距离把所有目标的相对距离累加起来就是这个个体的拥挤度。公式表达是d_i sum_j ( f(i1, j) - f(i-1, j) ) / ( fmax_j - fmin_j )其中j遍历所有目标。分母是第j个目标在这一前沿内的最大值减最小值作用是消除不同目标之间的量纲差异。如果一个前沿内某目标的所有值都一样分母为0这时这一维直接跳过不加避免除零。边界个体设无穷大是为了保证每一层前沿两端的极端解一定被活下来。想象一个双目标问题最优前沿是一个从左下到右上的弧线弧线两个端点往往对应牺牲一个目标成全球最值的方案这种方案在中庸解扎堆时很容易被挤掉。不保留端点下一代前沿就会往里收缩跑几十代后整个前沿缩成一个点。这就是很多调参失败案例里“前沿不见了”的直接原因。如果某个前沿里只有两个个体两个都是边界个体拥挤度都是无穷大说明它们都值得保留。2.3 锦标赛选择与精英保留NSGA2不会丢最优解的机制有了非支配层级和拥挤度NSGA2的选择规则就很清楚了任意两个个体比较时先看所在前沿层数小的赢如果两个个体在同一层看拥挤度拥挤度大的赢。这个规则叫二元锦标赛选择整个选择过程的成本很低不需要全局排序。另一个关键设计是精英保留。普通遗传算法生成子代后直接用子代替换父代万一父代里有接近最优的解可能在交叉变异中被破坏。NSGA2采取另一个策略把父代和子代合并成一个2N规模的临时种群对这个合并种群做完整的非支配排序和拥挤度计算然后按“层数优先、拥挤度次之”的顺序排序取前N个作为下一代。这带来的直接好处是上一代哪怕所有个体都进了第二第三前沿只要它们比子代好就能留在下一代里最优解不会因为进化过程中的随机破坏而彻底丢失。这也是“NSGA2”这个二代版本名字里II的核心价值第一版的NSGA没有这个设计种群容易退化。实际编码时这个排序可以一句话实现sortrows([rank, -dist])先按rank升序再按拥挤度降序取前N行就是下一代。能看到NSGA2的整体框架非常简洁一个非支配排序、一个拥挤度计算、一个选择交叉变异的循环所有复杂逻辑都被这三个模块拆干净了。3. Matlab从初始化到迭代一份可直接改目标函数的NSGA2骨架3.1 问题定义与参数设置变量个数、目标个数、边界怎么填先看一个典型问题的定义。假设决策变量有6个都在[0,1]之间目标函数取经典的双目标测试形式第一个目标只与第一个变量有关第二个目标与全部变量有关并且希望两个目标都越小越好。这种测试问题常用来验证NSGA2能不能跑出均匀分布的前沿因为它的真实前沿是已知的跑完能直观看差距。%% 问题定义 D 6; % 决策变量个数 lb zeros(1, D); % 每个变量的下界 ub ones(1, D); % 每个变量的上界 nM 2; % 目标个数这个例子里固定为2 %% NSGA2参数 N 80; % 种群大小保持偶数因为交叉是一对一生成 MaxGen 200; % 迭代代数复杂问题往上加到500甚至1000 pc 0.9; % 交叉概率SBX的默认推荐区间是0.8~0.95 pm 1/D; % 变异概率常见做法是1除以决策变量个数 eta_c 15; % SBX交叉分布指数15~20是经典推荐 eta_m 20; % 多项式变异分布指数20左右用得多 %% 目标函数输入N*D矩阵输出N*2矩阵 g (x) 1 9 * mean(x(:, 2:end), 2); objfun (x) [x(:, 1), g(x) .* (1 - sqrt(x(:, 1) ./ g(x)))];这段代码定义一个函数句柄而不是单独写一个文件好处是后面跑不同测试问题时只需要替换句柄内容。第一行g用了mean函数把所有除x1以外的变量平均贡献到第二个目标里这是经典测试问题的标准构造方式目的是让第二目标与决策变量数量产生耦合变量越多多目标求解难度越大。注意到objfun接受的是一个N×D矩阵返回N×2矩阵。这个设计必须从一开始就保持因为NSGA2的种群里所有个体是同时进化的目标函数一次性批量计算能省下大量循环开销。如果写成每次只接受一个决策向量返回一行目标值后面拥挤度计算和绘图时还要再包一层循环性能差很多。参数的选取逻辑这样理解种群80对于两个目标的问题够用如果目标数增加到三个或四个前沿是一个三维以上的超曲面需要更多个体去填充N至少128甚至256。MaxGen200是在这个简单测试问题上的起步值如果你替换成真实工程问题建议先设500跑一次看前沿是否还在明显变化如果是继续加代数。3.2 快速非支配排序数据结构与Matlab实现非支配排序里最直观的写法是双重循环每个个体和所有其他个体比较支配关系统计谁被谁支配然后逐层剥离。function rank non_dominated_sort(f) % f: N*M矩阵每行是一个个体的M个目标值全部为最小化方向 N size(f, 1); dom_count zeros(N, 1); % 第i个个体被多少个个体支配 dom_set cell(N, 1); % 第i个个体支配了哪些个体 % 第一遍两两比较记录支配关系 for i 1:N for j i1:N if dominates(f(i,:), f(j,:)) dom_set{i} [dom_set{i}, j]; dom_count(j) dom_count(j) 1; elseif dominates(f(j,:), f(i,:)) dom_set{j} [dom_set{j}, i]; dom_count(i) dom_count(i) 1; end end end % 找出第一前沿没有被任何个体支配 front {}; front{1} find(dom_count 0); % 逐层剥离把当前前沿支配的个体计数减1减到0就进下一层 k 1; while ~isempty(front{k}) Q []; for i front{k} for j dom_set{i} dom_count(j) dom_count(j) - 1; if dom_count(j) 0 Q [Q, j]; end end end k k 1; front{k} Q; end front(end) []; % 最后一层必然是空集去掉 % 个体编号转成分层编号 rank zeros(N, 1); for i 1:numel(front) rank(front{i}) i; end end function b dominates(a, b) % a支配b的条件a所有目标不大于b且至少一个小于b b all(a b) any(a b); end这个实现的逻辑分三段。第一段的双重循环建立完整的支配关系图dom_count(i)统计有多少个体支配第i个个体dom_set{i}记录第i个个体支配了谁。第二段是一个循环剥离过程第一前沿是所有dom_count为0的个体接下来把第一前沿支配过的每个个体的dom_count减1一旦某个个体被自己支配的个体数全部扣完说明除了已经去掉的前沿个体外已经没有别的个体能支配它它就进第二前沿。第三段只是把前沿编号回填到rank数组里。dominates子函数里的条件严格符合帕累托支配定义注意这里用的a是行向量b也是行向量all(a b)表示a在所有目标上都不比b差any(a b)表示至少一个目标严格更好。两个条件缺一个都不是严格支配。复杂度方面双重循环是O(N^2)对80个个体的种群完全没问题如果N上到500且目标数又超过3个这层比较会变成主要瓶颈后面排坑那章再聊怎么处理。3.3 拥挤度计算与选择交叉变异主循环剩下那几步拥挤度计算按前面讲的公式实现注意几个边界条件需要单独处理。function dist crowding_distance(f, rank) N size(f, 1); dist zeros(N, 1); unique_fronts unique(rank); for r unique_fronts idx find(rank r); if numel(idx) 2 % 前沿里不超过两个个体时全部视为边界 dist(idx) inf; continue; end nObj size(f, 2); for m 1:nObj [~, order] sort(f(idx, m)); idx_sorted idx(order); fmin f(idx_sorted(1), m); fmax f(idx_sorted(end), m); norm_range fmax - fmin; % 边界个体距离设为无穷大保证前沿端点被保留 dist(idx_sorted(1)) inf; dist(idx_sorted(end)) inf; if norm_range 0 % 这一维所有值相同跳过不做任何贡献 continue; end for i 2:numel(idx_sorted)-1 neighbor_dist f(idx_sorted(i1), m) - f(idx_sorted(i-1), m); dist(idx_sorted(i)) dist(idx_sorted(i)) neighbor_dist / norm_range; end end end end单独说一下if range 0这行。真实工程问题里经常出现某个目标在某一层前沿内总是一个定值比如约束刚好卡死这时分母为0如果不跳过就会出现NaN而NaN在排序里会被Matlab排到最前面导致全部保留顺序错乱。跳过这一维后其他目标仍然正常累计拥挤度行为是合理的。主循环实现包含了比较、交叉、变异、精英保留全链路%% 初始化 Pop lb rand(N, D) .* (ub - lb); Obj objfun(Pop); rank non_dominated_sort(Obj); dist crowding_distance(Obj, rank); %% 迭代主循环 for gen 1:MaxGen % 二元锦标赛选择选出交配池 MatingPool zeros(N, D); for i 1:N cand randi(N, 1, 2); if rank(cand(1)) rank(cand(2)) winner cand(1); elseif rank(cand(1)) rank(cand(2)) winner cand(2); else if dist(cand(1)) dist(cand(2)) winner cand(1); else winner cand(2); end end MatingPool(i, :) Pop(winner, :); end % SBX交叉与多项式变异生成子代 Offspring zeros(N, D); for i 1:N/2 p1 MatingPool(2*i-1, :); p2 MatingPool(2*i, :); c1 p1; c2 p2; for j 1:D if rand() pc u rand(); if u 0.5 beta (2*u)^(1/(eta_c1)); else beta (1/(2*(1-u)))^(1/(eta_c1)); end c1(j) 0.5 * ((1beta)*p1(j) (1-beta)*p2(j)); c2(j) 0.5 * ((1-beta)*p1(j) (1beta)*p2(j)); end end c1 polynomial_mutation(c1, lb, ub, pm, eta_m); c2 polynomial_mutation(c2, lb, ub, pm, eta_m); Offspring(2*i-1, :) c1; Offspring(2*i, :) c2; end % 精英保留父代子代合并后统一排序 PopAll [Pop; Offspring]; ObjAll [Obj; objfun(Offspring)]; rankAll non_dominated_sort(ObjAll); distAll crowding_distance(ObjAll, rankAll); % 综合排序层数升序同一层拥挤度降序 [~, order] sortrows([rankAll, -distAll]); keep order(1:N); Pop PopAll(keep, :); Obj ObjAll(keep, :); rank rankAll(keep); dist distAll(keep); end %% 绘制最终帕累托前沿 plot(Obj(:, 1), Obj(:, 2), o); xlabel(f1); ylabel(f2); title(NSGA2 Pareto Front);SBX交叉里那个beta计算是模拟二进制交叉的标准公式u是一个[0,1]随机数eta_c越大产生的后代越接近父代eta_c越小后代偏离越远。多项式变异同理eta_m控制变异步长。变异函数单独看function x_new polynomial_mutation(x, lb, ub, pm, eta_m) x_new x; for j 1:numel(x) if rand() pm r rand(); if r 0.5 delta (2*r)^(1/(eta_m1)) - 1; else delta 1 - (2*(1-r))^(1/(eta_m1)); end x_new(j) x(j) delta * (ub(j) - lb(j)); x_new(j) min(max(x_new(j), lb(j)), ub(j)); end end end这里对每个决策变量按概率pm独立变异没有变异的变量保持原值。变异后的值用min和max夹回边界这个夹取操作在Matlab里是对标量生效如果x_new是向量就要配合循环或者用数组运算实际使用时注意一下。如果你把这套代码原样跑一遍应该能看到一个从左下往右上分布的散点图前沿点大致均匀铺在一条弧线上。如果出来的图是一坨密集的点或者空荡荡只有一两个点多半出在下面第4章的某个坑里。4. 排查NSGA2最容易翻车的五个地方4.1 可行解全灭约束违反处理不当导致种群全是废物现象加了约束条件后跑几十代画出来的前沿点越来越少最后只剩一两个或者目标函数值出现明显不合理的异常值。原因很多人处理约束时直接把约束写成罚函数加到目标上比如f1 f1 100 * violation。罚系数太小不可行解照样进入前沿罚系数太大可分解和不可行解之间数值差距悬殊拥挤度计算被这一项带偏整个前沿分布一团糟。另一种更隐蔽的原因是选择环节完全没有考虑约束导致子代里掺杂大量不可行解越进化越脏。解决最直接的做法是在锦标赛选择里先比约束违反总量再比分层。给每个个体维护一个CV值表示所有约束违反量之和等于0就是可行解。选择时按以下顺序比较两解CV都为0正常按层数和拥挤度比一个CV为0一个不为0可行解直接赢两个CV都不为0CV小的赢。这个逻辑保证可行解优先但不彻底丢弃所有不可行解给约束边界附近的搜索留了空间。function winner select_tournament(idx1, idx2, rank, dist, CV) if CV(idx1) CV(idx2) winner idx1; elseif CV(idx2) CV(idx1) winner idx2; else % 两者约束状态相同按标准规则比较 if rank(idx1) rank(idx2) winner idx1; elseif rank(idx1) rank(idx2) winner idx2; else if dist(idx1) dist(idx2) winner idx1; else winner idx2; end end end end核心参数是约束违反总量怎么算。常见做法是把每个约束都写成不大于0的形式违反部分max(0, g_i(x))全部累加。这套方案能处理绝大多数带约束工程问题比罚函数可控得多。4.2 帕累托前沿只集中在决策空间某个角落现象前沿点确实存在但全部挤在目标空间的一角弧线应有的两端没铺开看起来像半个前沿。原因第一个可能原因是变量边界写反了或者个别变量上下界设置范围太大让变异很难在小范围内产生有效扰动。第二个原因是变异步长和变量范围不匹配比如一个变量变化范围是0到10000另一个变量是0到1使用统一的eta_m和pm时小范围变量几乎每次变异都直接跳出去再被夹回边界等于失效。解决把决策变量全部归一化到[0,1]区间内参与进化计算完目标函数后再映射回真实取值范围。归一化之后变异和交叉操作的相对步长对所有变量一视同仁不会出现某个变量被边缘化的情况。真实工程里我还要先排查一遍上下界向量lb和ub有没有填反这个低级错误能把前沿彻底带偏却常常是最耗时间的坑。4.3 目标函数量纲差距大导致拥挤度被某一维带走现象第一个目标值的范围是1到10第二个目标值的范围是1000到10000。跑出来的前沿在第二目标方向上铺得很开但第一目标方向几乎只有一两个值前沿看起来像一条竖直线而不是正常弧线。原因拥挤度距离计算里各维目标差值的累加是直接相加的数值大的目标占主导地位。拥挤距离大的个体集中在数值大的目标维度上选择压力就偏向那一维另一维很难被单独保留下来。这是NSGA2一个已知的弱点标准和简化实现里都没有自动解决。解决在计算拥挤度之前对每一维目标做归一化。具体来说读取当前合并种群中该目标的最小值和最大值把目标值线性映射到[0,1]区间然后用归一化后的值计算差距。归一化里注意两点一是要对每次合并后的2N个个体整体做归一化不能只用父代或子代的单独范围二是如果某维范围过于窄最大值最小值差小于某个阈值如1e-6直接把该维设为无贡献防止数值抖动。% 在 crowding_distance 内部对每一维目标做归一化后再累计 for m 1:nObj col f(idx, m); lo min(col); hi max(col); if hi - lo 1e-12 continue; end col_norm (col - lo) / (hi - lo); [~, order] sort(col_norm); idx_sorted idx(order); dist(idx_sorted(1)) inf; dist(idx_sorted(end)) inf; for i 2:numel(idx_sorted)-1 dist(idx_sorted(i)) dist(idx_sorted(i)) ... (col_norm(i1) - col_norm(i-1)); end end这样处理后每一维目标对拥挤度的贡献最大都是1量级不会再被大数值目标绑架。代价是同一前沿内不同代之间的拥挤度数值不再可比但选择比较只在同一时间内进行所以不影响正确性。4.4 迭代到一半种群多样性消失变异率不是越大越好现象前50代前沿还在缓慢变化后面50代完全不动画出来每代结果一模一样或者所有点最后收敛到一个区域。原因变异率设置过低会让种群失去探索能力过早收敛变异率过高会把已经找到的好区域破坏掉让搜索一直在原地踏步。很多人看前沿不动就加大变异率结果更糟。解决调整变异率时不要凭感觉先试两个参考值pm1/D是比较均衡的起点如果D6就大约0.17意味着每个个体平均有不到一个变量发生变异如果前沿收敛过快把变异率提高到2/D给更多探索机会如果收敛不稳定、结果抖动大降到0.5/D。另一个更实用的做法是分阶段降低变异率前一半代数用pm2/D后一半代数用pm0.5/D。if gen MaxGen/2 pm_current 2/D; else pm_current 0.5/D; end这个分段策略有直观道理进化前期需要广泛探索找到有希望的区域后期需要精细挖掘把前期发现的区域做局部搜索。比固定一个变异率跑全程要稳。4.5 Matlab跑一批要两小时向量化带来的提速现象种群规模只有80代数也只有200但整套代码跑完要好几个小时检查后发现目标函数内部用了大量for循环逐个体计算。原因这通常是目标函数写的太“串行”导致的。比如目标函数里对每个个体单独调用一次外部程序或者用了循环算累加而没有利用Matlab的矩阵运算。NSGA2的每一代都要对2N个个体计算一次目标函数如果目标函数单次执行要几毫秒200代80个种群总调用次数在两万次左右累积起来非常可观。解决第一优先级是让目标函数支持批处理输入是N×D矩阵输出是N×M矩阵。所有能用矩阵运算符表达的地方绝对不用循环。算累加用sum或mean算条件用逻辑索引计算时间能降到原来的几十分之一。第二优先级是预分配所有数组尤其是Offspring和MatingPool避免循环里不断拼接数组。Matlab里[A; B]拼接会反复申请内存数量一大就会拖慢整体速度。有一种情况矩阵批处理做不了目标函数必须调用外部商业求解器或仿真软件每次只能算一个方案。这时能做的是把目标函数改成可分块计算的版本用parfor并行跑。这个在后续文章里再细说这里先知道方向即可。5. 跑通之后帕累托前沿的验证与三组复跑习惯5.1 先画图再看数前沿分布和收敛情况的判读法代码跑通后第一件事不是看数据而是画图。双目标问题直接画散点图去看前沿是否有明显边界、端点是否够极端、中段是否均匀。真正的帕累托前沿通常是一条平滑弧线散点分布应尽量贴近这条弧线而不是糊成一片。如果点之间有空档说明某些区域缺少代表性解下一步该调整eta_m增大局部探索如果端点缺失说明边界个体在精英保留时被挤掉了去检查拥挤度的inf边界是否正确设置。三目标以上问题时散点图看不出整体分布建议投影到两两目标构成的平面画多个子图对比同时记录每一代的最小目标值变化曲线确认是否还在下降。如果第150代和200代画出来的图几乎重合说明收敛得差不多了再跑更多代数意义不大。5.2 重复跑N次取中间那次而不是最好那次NSGA2是随机算法初始化种群、锦标赛选择的随机抽取、交叉变异的随机数都导致每次运行结果不同。只跑一次就下结论非常危险因为这次结果可能恰好落在随机性的幸运侧或倒霉侧。我自己的习惯是同一组参数跑5到10次把每次的最终前沿叠加画到一张图上看骨架是否一致。选择哪次作为最终结果也有讲究别取目标函数值综合最好那次那通常是随机性恰好把初始种群撒在了好的区域不代表算法稳定水平。取画出来后最“典型”的一次也就是5次结果的中位数版本它反映的是算法在这组参数下的平均水平。更严格的做法是记录每次结果的超体积指标取指标最接近中位数的那次。这样既避免被单次异常结果欺骗也保留了可复现的固定结果用于后续对比。5.3 用间距与超体积判断这组参数值不值得用看图画线始终有主观成分定量判断参数好坏才能服众。三个指标最常用指标计算方式说明间距指标SP计算相邻前沿点之间的欧氏距离标准差标准差越小说明前沿点分布越均匀超体积HV以参考点为界前沿点与界之间覆盖的体积HV越大说明前沿覆盖范围越广解集越好反转世代距离IGD计算真实前沿每个点到所求前沿最近距离的平均值需要已知真实前沿数值越小越接近真实解集在真实工程问题里真实前沿常常未知IGD用不上但HV和SP可以直接用。HV的计算有一个需要注意的点参考点必须比所有前沿点在每个目标上的值都差一般取目标最大值的1.2倍左右保证所有点都包含在体积范围内。HV的值也受目标量纲影响跨问题对比时要先归一化所有解集再算。有了这三个指标验证就不是玄学同一参数跑10次算平均HV和平均SP两个数一摆就能比较不同参数组合的高下。我自己常用的是先画图看有没有严重畸形再算SP看均匀性最后算HV看覆盖度三者一起通过才算这组参数可靠。每次换目标函数都强制走一遍这一步能挡掉很多调参翻车风险。希望帮到你。本文还有配套的精品资源点击获取