从零玩转遗传算法:Python实战全攻略,小白也能搞定极值/非线性规划/TSP/多目标优化
目录
3. 适应度函数(Fitness):衡量谁才是“最强个体”的标准
六、实战3:经典组合优化——遗传算法解决旅行商(TSP)问题
1.核心概念:帕累托最优(Pareto Efficiency)
一、 序言:为什么你需要“上帝之手”?
在工程与代码的世界里,我们每天都在试图寻找“最优解”:如何让工厂的物流成本最低?如何让机器学习模型的误差最小?如何在一个极其复杂的非线性函数 中找到那座最高的“山峰”?
传统算法的瓶颈:当数学工具在“群山”中迷路
面对这些问题,我们通常会求助于传统的微积分和解析方法,例如梯度下降法或牛顿法。你可以把这些传统算法想象成一个被蒙住双眼的登山者:他只能用脚感受地面的倾斜程度(计算梯度),然后朝着感觉最陡峭的方向向上爬。
这种策略在平滑的单峰地形中非常高效。但在现实世界的复杂问题中,地形往往是崎岖不平、甚至存在断层的。这时,传统算法就会暴露出两个致命的瓶颈:
-
陷入局部最优(Local Optimum): 登山者好不容易爬上了一个山头,便以为自己到达了世界之巅,却不知道真正的珠穆朗玛峰就在几公里外。一旦陷入局部最优,传统算法就彻底“卡死”了。
-
面对不可导函数束手无策: 如果地形充满了悬崖峭壁(函数不连续、不可导),或者是离散的组合问题(比如旅行商要按顺序走过几个城市),登山者根本无法计算“倾斜度”,传统数学工具直接失效。
遗传算法的哲学:优胜劣汰,适者生存
既然一个聪明的登山者搞不定,那如果是一整群随机空投的猴子呢?
这就是遗传算法(Genetic Algorithm, GA)的奇妙哲学。它不再死磕严格的数学推导,而是向大自然借用了一双“上帝之手”——进化论。
遗传算法不去计算什么导数,而是直接在地图上随机生成一批“初始种群”。通过一套残酷但高效的自然法则来筛选它们:
-
适应度评估: 站得越高(目标函数值越好),生存概率越大。
-
交叉与变异: 优秀的个体之间交换“基因”产生后代,并偶尔发生基因突变,赋予它们跳出当前小山头、飞跃到新大陆的能力。
经过一代又一代的繁衍,平庸的个体被淘汰,携带优秀基因的个体不断汇聚。最终,这群生命的后代将自动盘踞在全局最优解的巅峰。
本文看点:从原理到代码的跨越
如果你厌倦了枯燥的数学推导,想要掌握一种通用、强悍且极具画面感的启发式搜索算法,那么这篇文章就是为你准备的。接下来,我将带你完成一次从碳基生物到硅基代码的进化之旅:
-
🧬 拆解黑盒: 用大白话搞懂基因编码、轮盘赌选择、交叉与变异的核心逻辑。
-
⛰️ 降维打击非线性规划: 用 Python 亲手写出跨越局部最优陷阱的代码,找到复杂函数的极值。
-
🗺️ 破解 TSP 难题: 将进化论应用于旅行商问题,寻找多城市间的极速路径。
-
⚖️ 挑战多目标优化: 探索当“鱼和熊掌不可兼得”时,遗传算法如何找到完美的帕累托最优解。
准备好你的 Python 编辑器,让我们开始这场代码世界的物种起源!
二、 核心概念:把进化装进代码里
遗传算法听起来高深,但它的核心其实就是把生物界的“达尔文进化论”翻译成计算机能听懂的语言。在动手写代码之前,我们需要先搞懂几个基础概念的“翻译”工作。
1. 基因与染色体:数据的编码
在生物学中,我们的性状由染色体决定,而染色体由无数个基因组成。在代码世界里:
-
染色体(Chromosome/Individual): 代表问题的一个可行解(也就是一个答案)。
-
基因(Gene): 代表这个答案里的具体参数。
假设我们在玩一个游戏:寻找参数 和
,让某个收益最大化。那么
这个组合就是一条染色体,里面的
和
就是具体的基因。
如何把这些参数写成代码?(编码方式)
-
二进制编码(Binary Encoding): 最经典的玩法,把数字变成
0和1的序列。比如一条染色体长这样:10110100。它非常适合解决“选或不选”的背包问题,或者进行简单的逻辑交叉。 -
实数编码(Real Encoding): 简单粗暴,直接用真实数值。比如染色体是
[3.14, 2.71]。在解决实际的工程函数求最值问题时,实数编码更精确、也更省内存。
2. 种群:寻找答案的“搜索小队”
大自然从来不会只让一只猴子去进化。遗传算法也是一样,我们需要随机生成一大批染色体,这批染色体的集合就叫种群(Population)。
-
种群的大小通常用
表示(比如
,代表有 100 个备选答案)。
-
人多力量大,种群数量越大,搜索范围越广,越不容易漏掉正确答案;但同时,电脑计算的速度也会相应变慢。这就是我们需要权衡的艺术。
3. 适应度函数(Fitness):衡量谁才是“最强个体”的标准
进化论的核心是“适者生存”。那么,谁是“适者”?这就需要一个裁判——适应度函数。
它是一个数学公式,用于给种群中的每一个个体打分。分数越高,说明这个答案越好,它活下来并繁衍后代的概率就越大。
-
求最大值问题: 如果我们的目标是求函数
的最大值,那非常简单,适应度函数
直接等于目标函数就行:
-
求最小值问题: 如果目标是求最小值(比如寻找最短路径,距离越短越好),我们就需要把“越小的值”转换成“越高的分数”。通常的数学做法是取倒数(为了防止分母为 0,通常会加一个极小的常数 c):
4. 进化三部曲:代码里的生生不息
有了种群,也有了打分标准,接下来就是让程序自己去“繁衍”了。从父代到子代,需要经历三个神圣的步骤:
第一步:选择(Selection)—— 轮盘赌与精英保留
我们要从现有的种群中挑选优秀的个体来当“父母”。
-
轮盘赌选择法(Roulette Wheel Selection): 并不是只有第一名才有资格繁衍,而是每个人都有机会,只是适应度越高的人,中奖概率越大。
假设个体
的适应度为
,种群总数为
,那么它被选中的概率
的数学公式为:
(简单理解:把所有人的分数加起来画一个大圆盘,你的分数越高,你占据的扇形面积就越大,飞镖扎中你的概率就越高。)
-
精英保留(Elitism): 为了防止优秀的基因在随机交配中意外丢失,我们通常会直接把这一代里排名第一的“超级精英”,原封不动地复制到下一代中。这是一种兜底策略。
第二步:交叉(Crossover)—— 基因重组产生新可能
选出了父母,接下来就是生孩子。父母双方各自拿出一部分基因进行交换,产生带有双方特征的新个体。
-
比如对于二进制编码
110|00和001|11,如果在第三位进行“单点交叉”,它们的后代就会变成110|11和001|00。这就是算法能不断产生新解的核心动力。
第三步:变异(Mutation)—— 突破局部最优的关键火花
如果只交配不变异,种群最终会变成近亲繁殖,所有人长得一模一样,算法也就彻底“死锁”在了某一个小山头上(陷入局部最优)。
-
变异就是打破僵局的火花: 以极小的概率(通常小于 5%),随机改变个体的一个基因。比如把二进制里的
0突然变成1,或者给实数编码加上一个微小的随机数。 -
正是这种看似错误的“基因突变”,给了算法跳出当前陷阱、飞跃到更高山峰的可能
经过“选择 -> 交叉 -> 变异”这套组合拳,新一代的种群就诞生了! 只要我们写一个循环,让这个过程重复个成百上千次,最后的种群里,往往就藏着我们要找的那个完美答案。
三、 环境准备:工欲善其事
别担心,不需要高配置的电脑,也不需要复杂的服务器,只要你有一台能敲字的电脑,我们就能开始进化。
1. Python 基础环境搭建:简单即正义
如果你是刚接触 Python 的小白,我强烈建议不要去折腾各种复杂的系统环境变量。直接下载安装 Anaconda 或者轻量级的 Miniconda。
它们就像是 Python 世界的“精装修拎包入住”套餐,自带了科学计算最常用的工具包,能帮你完美避开 99% 的版本冲突黑洞。
安装完成后,按住Win+R,打开你的终端(Windows 下是 Anaconda Prompt,Mac/Linux 下是 Terminal),输入这行代码检查一下:
python --version
只要弹出的版本号是 Python 3.8 或以上,恭喜你,数字伊甸园的地基已经打好了!
2. 核心武器库:“纯手工造轮子” vs “开箱即用”
在实现遗传算法时,我们有两条路可以走。为了让你真正掌握这门手艺,本教程会带你把这两条路都走一遍:
安装库一:用 numpy 纯手工打造(新手必练)
遗传算法里有大量的种群数据、基因序列需要处理。如果你用 Python 自带的列表(List)去写个 for 循环慢慢算,电脑估计会累得冒烟。
这时候就需要请出 Python 科学计算的底层核武器——numpy。它极其擅长做大规模的矩阵运算,能让你的算法速度起飞。
-
安装命令:
pip install numpy秒懂时刻:用
numpy一秒钟生成种群! 还记得我们上一节说的“二进制染色体”和“种群”吗?用numpy,只需要一行代码,我们就能凭空创造出 5 个拥有 10 段基因的初始生命体:
import numpy as np
# 生成一个 5行(5个个体) x 10列(10段基因) 的二维数组,数值随机是 0 或 1
population = np.random.randint(0, 2, size=(5, 10))
print("我们的初代种群诞生了:\n", population)
-
运行一下,你就会看到一个满是 0 和 1 的矩阵,这就是我们要进行进化实验的“小白鼠”了!
安装库二:用 mlrose(实战首选)
当你在未来遇到极其复杂的工业级难题(比如我们后面要讲的多目标优化),自己从头写交叉和变异函数就太痛苦了。这时候,我们就需要站在巨人的肩膀上,直接调用成熟的遗传算法框架。
-
mlrose: 易用性极强,新手友好,算法覆盖全面,满足多样化优化需求,适配机器学习场景,无缝集成现有流程
-
安装命令:(你可以先把它们装上备用)
pip install mlrose
四、实战1:基础入门——用遗传算法求解函数最大/最小值
1. 问题描述:连梯度下降都会迷路的“疯狂函数”
假设我们有一个波动极其剧烈、充满陷阱的目标函数:
我们需要在 这个区间内寻找它的全局最大值。
你可以把这个函数想象成连绵起伏的山脉,里面有无数个小山头(局部最优解)。传统的梯度下降算法就像一个被蒙住眼睛的登山者,随便摸到一个上坡就往上爬,极大概率会永远卡在半山腰的某个小土包上。
但遗传算法不怕,我们直接空投一批“数字猴子”,让它们自己去繁衍、去变异、去寻找真正的世界之巅!
2. 代码实现:用 mlrose 优雅地定义“伊甸园”
在运行代码前,请先使用 pip install mlrose_hiive 安装维护版的库(原版 mlrose 已停止更新)。
pip install mlrose_hiive -i https://mirrors.aliyun.com/pypi/simple/
使用 mlrose 的核心逻辑极其清晰:写出打分标准 -> 圈定搜索边界 -> 启动遗传算法。
import mlrose
import numpy as np
import matplotlib.pyplot as plt
# ================= 1. 定义我们要征服的“山脉” =================
def complex_function(state):
# mlrose 传入的是一个数组,哪怕只有一个变量,也要通过索引 [0] 取出来
x = state[0]
return x * np.sin(10 * np.pi * x) + 2
# ================= 2. 告诉 mlrose 如何打分 (定制适应度) =================
# mlrose 提供了 CustomFitness 接口,直接把我们的函数包装成裁判
fitness_cust = mlrose.CustomFitness(complex_function)
# ================= 3. 划定“伊甸园”边界 (定义优化问题) =================
# ContinuousOpt 专门用于解决连续数值(实数)的优化问题
problem = mlrose.ContinuousOpt(
length=1, # 变量的个数:我们只求一个变量 x
fitness_fn=fitness_cust, # 裁判:刚才定义的打分标准
maximize=True, # 目标:求最大值(站在最高的山上)
step=0.001, # 精度:每次探索的最小步长
min_val=-1.0, # ★ 边界下限:搜索范围的起点
max_val=2.0 # ★ 边界上限:搜索范围的终点
)
# ================= 4. 召唤“上帝之手” (运行遗传算法) =================
print("种群已空投,正在疯狂进化中...")
# genetic_alg 函数直接接管了选择、交叉、变异的全部底层工作
best_state, best_fitness, fitness_curve = mlrose.genetic_alg(
problem,
pop_size=100, # 种群规模:空投 100 只猴子
mutation_prob=0.1, # 变异率:10% 的概率发生基因突变
max_iters=50, # 进化代数:繁衍 50 代
curve=True, # ★ 开启记录:记录每一代的最优分数,用于画图
random_state=42 # 随机种子:保证每次运行结果一致,方便调试
)
# ================= 5. 获取结果与可视化 =================
print(f"进化结束!找到的最优横坐标 x: {best_state[0]:.4f}")
print(f"对应的最高山峰值 f(x): {best_fitness:.4f}")
# 画图:看看最终的王者站在哪座山上
x_axis = np.linspace(-1, 2, 500)
y_axis = complex_function([x_axis])
plt.figure(figsize=(12, 5))
# 子图 1:展示目标函数和最终找到的极值点
plt.subplot(1, 2, 1)
plt.plot(x_axis, y_axis, label="Mountain (Target Function)", color='blue', alpha=0.6)
plt.scatter(best_state[0], best_fitness, color='red', s=100, zorder=5, label="Best Solution Found")
plt.title("The Peak Discovered by mlrose")
plt.xlabel("x")
plt.ylabel("f(x)")
plt.legend()
plt.grid(True)
# 子图 2:展示进化的学习曲线 (点点如何汇聚)
plt.subplot(1, 2, 2)
plt.plot(fitness_curve, linewidth=2, color='green')# 直接传入一维数组即可
plt.title("Evolution Fitness Curve")
plt.xlabel("Generations")
plt.ylabel("Best Fitness Score")
plt.grid(True)
plt.tight_layout()
plt.show()
解析:
在 mlrose.ContinuousOpt 中,极其关键的参数是 min_val 和 max_val。这不仅仅是告诉算法从哪里开始找,更是一道“基因锁”。在变异和交叉的过程中,一旦产生的新坐标超出了 [-1, 2] 的范围,算法会自动将其拉回边界。这在解决具有严格物理限制的工程问题时是必不可少的。

五、实战2:进阶应用——遗传算法求解非线性规划问题
1. 业务场景设定:智能投资组合优化
为了让例子更有趣,我们假设你手里有 100万元 资金,需要分配到 5 个不同的项目(A, B, C, D, E)中。
-
目标:最大化总收益(每个项目收益率是非线性的)。
-
0-1 变量约束:每个项目要么投,要么不投(由一个开关变量控制)。
-
多种约束:
-
预算约束:总投资额不能超过 100 万。
-
必选约束:项目 A 必须投(因为它最稳)。
-
多样性约束:至少要投 3 个项目。
-
2. 核心难点:如何处理“约束”?
遗传算法本身像是一个“盲人摸象”,它不知道什么是规则。我们使用罚函数法(Penalty Function):
如果一个个体违反了约束,我们就从它的“适应度分数”里扣掉一大笔分(惩罚)。这样,在进化的过程中,违反规则的个体就会被自然淘汰。
3.代码实现:
import mlrose_hiive as mlrose
import numpy as np
# ==========================================
# 1. 基础数据准备
# ==========================================
# 假设有 5 个项目,每个项目的预期收益率和所需成本
yield_rates = np.array([0.12, 0.15, 0.08, 0.20, 0.10])
costs = np.array([30, 40, 20, 50, 25]) # 单位:万元
budget_limit = 100 # 总预算限制
# ==========================================
# 2. 定义带约束的适应度函数
# ==========================================
def fitness_func(state):
"""
state: 0-1 向量,例如 [1, 0, 1, 1, 0] 表示选择项目 A, C, D
"""
# 计算目标函数:总收益 (非线性:收益随投资项目数量有开方增长效应)
# 基础收益 = 选中的项目收益之和
base_profit = np.sum(state * yield_rates)
# 非线性调整 (模拟规模协同效应)
total_profit = base_profit * (np.sum(state) ** 0.5)
# --- 约束处理 (罚函数法) ---
penalty = 0
# 约束 A: 预算约束 (总投资 <= 100万)
current_cost = np.sum(state * costs)
if current_cost > budget_limit:
penalty += 100 # 违规惩罚
# 约束 B: 必选约束 (项目 A 索引为 0,必须投)
if state[0] == 0:
penalty += 50
# 约束 C: 多样性约束 (至少投 3 个项目)
if np.sum(state) < 3:
penalty += 50
# --- 关键修正:防止负数适应度 ---
# 原始分值 = 收益 - 惩罚
raw_score = total_profit - penalty
# 使用偏移量 (Offset) 确保结果永远为正
# 我们加一个足够大的基准值 (例如 500),让最差的情况也是正数
final_score = raw_score + 500
return final_score
# ==========================================
# 3. 初始化优化问题
# ==========================================
# 定义自定义适应度对象
fitness_cust = mlrose.CustomFitness(fitness_func)
# 定义离散优化问题 (0-1变量)
# length=5 (5个项目), max_val=2 (取值0或1), maximize=True (求最大收益)
problem = mlrose.DiscreteOpt(length=5,
fitness_fn=fitness_cust,
maximize=True,
max_val=2)
# ==========================================
# 4. 运行遗传算法 (GA)
# ==========================================
best_state, best_fitness, fitness_curve = mlrose.genetic_alg(
problem,
pop_size=200, # 种群规模
mutation_prob=0.1, # 变异概率
max_attempts=100, # 连续100次无改良则停止
max_iters=1000, # 最大迭代次数
curve=True, # 记录进化曲线
random_state=42
)
# ==========================================
# 5. 结果展示与验证
# ==========================================
# 注意:输出的 best_fitness 包含了我们加的 500 偏移量,需要减回来
actual_profit_score = best_fitness - 500
print("=" * 30)
print("遗传算法寻找的最优方案:")
print(f"项目选择状态 (0/1): {best_state}")
print(f"最终适应度评分 (已减偏移): {actual_profit_score:.4f}")
print(f"该方案总成本: {np.sum(best_state * costs)} 万元")
# 验证约束是否达成
project_names = ['项目A', '项目B', '项目C', '项目D', '项目E']
chosen = [project_names[i] for i in range(len(best_state)) if best_state[i] == 1]
print(f"选择的项目清单: {chosen}")
print("=" * 30)
4. 代码深度解读
A. 状态空间定义 (DiscreteOpt)
在 mlrose 中,我们设置 length=5 代表 5 个决策变量。max_val=2 结合 DiscreteOpt 意味着变量的取值范围是 [0, 2) 的整数,即只能取 0 或 1。这完美解决了 0-1 变量 的定义。
B. 适应度函数的逻辑
这是遗传算法的“大脑”。
-
目标函数:
total_yield = ... * (np.sum(state)**0.5)。这里使用了根号,使其变成了非线性问题。在实际工程中,这可以是任何复杂的数学公式。 -
惩罚机制:注意我们设置了
maximize=True(求最大值)。因此,当方案违反约束时,我们要减去一个很大的常数。如果你的问题是求最小值,则应该加上惩罚。
C. 遗传算法参数
-
pop_size (种群):就像生物进化,方案越多,基因多样性越好,越容易找到全局最优解,但计算变慢。
-
mutation_prob (变异):防止算法“思想僵化”。它会让某些方案随机改变(比如原本投 A 变成不投),从而跳出局部最优陷阱。
5. 结果分析与优势
可行性验证:你会发现输出的结果中,项目 A 一定被选中,且总金额不会超过 100。这就是罚函数强迫算法学习到了“规则”。

六、实战3:经典组合优化——遗传算法解决旅行商(TSP)问题
1. 什么是 TSP 问题?(小白秒懂版)
想象你是一个快递员,今天要送 8 个地方。
-
规则:你必须经过所有城市,每个城市只能去一次,最后回到起点。
-
目标:怎么走总路程最短?
为什么它难? 城市只有 10 个时,路线有 360 万种;城市增加到 30 个时,组合数量比宇宙中的原子还多!常规算法算不动,而遗传算法(GA)通过模拟“优胜劣汰”,能在极短时间内找到一个非常接近完美的方案。
2. 核心难点:编码与算子(防坑指南)
在之前的例子中,我们用的是 0-1 编码。但在 TSP 中,我们需要整数编码(例如:[0, 2, 3, 1] 代表访问城市的顺序)。
-
普通变异的麻烦:如果你随机把一个 2 改成 3,路径就变成了
[0, 3, 3, 1]。坏了!城市 3 去了两次,城市 2 丢了。 -
mlrose_hiive 的妙处:它内置了
TSPOpt类,会自动处理这些逻辑,确保变异和交叉后得到的依然是一条合法(不重复、不遗漏)的路径。
3. 完整 Python 代码实现
我们将模拟 8 个城市在地图上的坐标,并寻找最短路径。
import mlrose_hiive as mlrose
import numpy as np
import matplotlib.pyplot as plt
# 1. 准备工作:定义 8 个城市的坐标 (x, y)
city_coords = [
(10, 40), (40, 10), (50, 20), (80, 50),
(20, 60), (70, 20), (30, 90), (90, 10)
]
# 2. 定义适应度函数:mlrose 专门为 TSP 准备了 TravellingSales(coords)
# 它会自动根据坐标计算欧几里得距离
fitness_coords = mlrose.TravellingSales(coords=city_coords)
# 3. 定义优化问题对象
# length: 城市数量
# fitness_fn: 上面定义的距离计算公式
# maximize: False (因为我们想让路程越短越好,即最小化)
problem = mlrose.TSPOpt(length=len(city_coords),
fitness_fn=fitness_coords,
maximize=False)
# 4. 运行遗传算法
best_state, best_fitness, fitness_curve = mlrose.genetic_alg(
problem,
pop_size=200, # 种群规模(200个随机路线开始进化)
mutation_prob=0.2, # 变异概率(略高一点有利于打破僵局)
max_attempts=100, # 如果100次尝试都没缩小距离,就停下
random_state=42
)
# 5. 输出结果
print("最优路径序列:", best_state)
print("最短总路程:", best_fitness)
# 6. 路径可视化(让结果一目了然)
ordered_coords = [city_coords[i] for i in best_state]
ordered_coords.append(ordered_coords[0]) # 回到起点
x, y = zip(*ordered_coords)
plt.figure(figsize=(8, 6))
plt.plot(x, y, 'o-', mfc='r', label='Path')
for i, txt in enumerate(best_state):
plt.annotate(f"City {txt}", (city_coords[txt][0]+1, city_coords[txt][1]+1))
plt.title(f"TSP Optimization Result (Total Distance: {best_fitness:.2f})")
plt.legend()
plt.grid()
plt.show()
4. 代码深度解读
A. TSPOpt 的整数编码
在底层,mlrose 将路径表示为一个排列(Permutation)。比如 [0, 4, 6, 2, 1, 5, 7, 3]。遗传算法在“交配”产生后代时,会使用特殊的顺序交叉(Order Crossover),确保每个城市 ID 在数组中只出现一次。
B. 适应度函数(距离最小化)
在 TSP 中,适应度就是总路径长度。
-
以往我们要“最大化收益”,这次我们设置
maximize=False。 -
mlrose会自动计算每一段(x1, y1)到(x2, y2)的距离并求和。
C. 变异操作的进化
对于 TSP,变异通常不是改变数值,而是交换(Swap)。比如随机挑选路径中的两个点交换位置。这就像你在地图上试着把两个景点的访问顺序对调,看看会不会缩短路程。
5. 结果分析


七、实战4:高阶拓展——遗传算法求解多目标优化问题
1.核心概念:帕累托最优(Pareto Efficiency)
小白只需要记住一句话:“鱼和熊掌不可兼得”时的那个平衡点。
-
支配关系:如果方案 A 在所有目标上都比方案 B 好,我们就说 A “支配”了 B。
-
帕累托最优解:如果一个方案已经好到“如果不牺牲目标 1,就无法提升目标 2”的程度,它就是一个最优解。
-
帕累托前沿(Pareto Front):所有这些最优解组成的集合。在坐标系上,它们通常连成一条优美的曲线。
2. 实战场景:云服务器配置选择
假设你要租用云服务器:
-
目标 1:最小化成本(越省钱越好)。
-
目标 2:最大化稳定性(停机时间越短越好)。 我们要从 10 种不同的配置组合中,选出那些最划算的“平衡点
3. 完整 Python 代码实现
虽然 mlrose_hiive 原生主要是为单目标设计的,但我们可以通过**加权求和法(Weighted Sum Model)**来模拟多目标搜索,这是小白理解多目标优化最简单的切入点。
import mlrose_hiive as mlrose
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ["Fangsong"]
# 可选:解决负号显示问题
plt.rcParams['axes.unicode_minus'] = False
# ==========================================
# 1. 模拟数据:10个服务器配置 [成本, 稳定性评分]
# ==========================================
# 我们希望:成本越低越好,稳定性越高越好
raw_configs = np.array([
[10, 20], [20, 40], [30, 60], [40, 75], [50, 85],
[60, 90], [15, 30], [25, 55], [35, 70], [45, 82]
])
# 为了防止不同量纲(比如成本是几千,稳定性是0-1)导致权重失效
# 我们对数据进行预处理:将所有数据缩放到 0-1 之间
costs = raw_configs[:, 0]
stabilities = raw_configs[:, 1]
# 归一化公式:(当前值 - 最小值) / (最大值 - 最小值)
norm_costs = (costs - costs.min()) / (costs.max() - costs.min())
norm_stabilities = (stabilities - stabilities.min()) / (stabilities.max() - stabilities.min())
# ==========================================
# 2. 定义带保护机制的适应度函数
# ==========================================
def multi_objective_fitness(state, w_stability, n_costs, n_stabilities):
"""
state: 0-1 向量
w_stability: 稳定性的权重 (0到1之间)
"""
# 约束:必须且只能选一个配置
if np.sum(state) != 1:
return 0.001 # 报错关键修正:返回极小的正数,绝不返回负数
idx = np.where(state == 1)[0][0]
# 目标1:最小化成本 (1 - 归一化成本 = 越省钱分数越高)
cost_score = 1.0 - n_costs[idx]
# 目标2:最大化稳定性
stability_score = n_stabilities[idx]
# 加权合成总分
w_cost = 1.0 - w_stability
raw_score = (w_stability * stability_score) + (w_cost * cost_score)
# 报错关键修正:加上一个保底偏移量,确保哪怕 raw_score 为 0,最终也是正数
return raw_score + 10.0
# ==========================================
# 3. 模拟不同权重下的“帕累托搜寻”
# ==========================================
pareto_results = []
# 生成从 0 到 1 的 10 种不同权重偏好
weights = np.linspace(0, 1, 10)
print("正在搜索帕累托最优解...")
for w in weights:
# 重新包装适应度函数,传入当前的权重 w
fitness_cust = mlrose.CustomFitness(
multi_objective_fitness,
w_stability=w,
n_costs=norm_costs,
n_stabilities=norm_stabilities
)
# 定义优化问题
problem = mlrose.DiscreteOpt(length=10, fitness_fn=fitness_cust, maximize=True, max_val=2)
# 运行遗传算法
best_state, _, _ = mlrose.genetic_alg(
problem,
pop_size=50,
mutation_prob=0.1,
max_attempts=10,
random_state=42
)
# 记录该权重下的最优配置坐标
best_idx = np.where(best_state == 1)[0][0]
pareto_results.append(raw_configs[best_idx])
# 去重处理,得到唯一的帕累托最优解集
pareto_results = np.unique(np.array(pareto_results), axis=0)
# 按成本排序方便画图
pareto_results = pareto_results[pareto_results[:, 0].argsort()]
# ==========================================
# 4. 结果可视化
# ==========================================
plt.figure(figsize=(10, 6))
# 绘制所有方案
plt.scatter(raw_configs[:, 0], raw_configs[:, 1], c='lightgray', s=100, label='所有可选配置')
# 绘制帕累托前沿
plt.plot(pareto_results[:, 0], pareto_results[:, 1], 'r--o', linewidth=2, label='帕累托前沿 (最优平衡点)')
plt.xlabel('成本 (越低越好)')
plt.ylabel('稳定性评分 (越高越好)')
plt.title('多目标优化结果:成本与稳定性的博弈')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()
print("优化完成!红线上的点即为您在不同偏好下的最佳选择。")
4. 代码解读
A. 权重分配 (Weighting)
多目标优化的最直接方法就是给每个目标发一张“打分表”。
-
如果你更看重钱(成本),就把成本的权重调高。
-
如果你更看重服务质量(稳定性),就把稳定性的权重调高。 代码通过
np.linspace(0, 1, 10)模拟了 10 种不同的偏好,从而搜寻出一组最优解。
B. 归一化 (Normalization)
成本可能是几千块,而稳定性评分只是 0-100。如果不处理,算法会为了节省 1 块钱而牺牲掉巨大的稳定性。在复杂的 NSGA-II 算法中,会自动处理这种拥挤度计算,而我们在简化版中通过手动设置权重来平衡。
C. 帕累托前沿的意义
运行代码后你会看到红色虚线。这条线上的每一个点都是**“在当前投入下能买到的最稳配置”**。
-
如果你想省钱,选线左边的点;
-
如果你追求极致稳定,选线右上的点;
-
线以下的灰色点是“垃圾方案”,因为总能找到一个比它更便宜且更稳的红色点。
5. 结果分析

更多推荐



所有评论(0)