目录

一、 序言:为什么你需要“上帝之手”?

二、 核心概念:把进化装进代码里

1. 基因与染色体:数据的编码

2. 种群:寻找答案的“搜索小队”

3. 适应度函数(Fitness):衡量谁才是“最强个体”的标准

4. 进化三部曲:代码里的生生不息

三、 环境准备:工欲善其事

1. Python 基础环境搭建:简单即正义

2. 核心武器库:“纯手工造轮子” vs “开箱即用”

四、实战1:基础入门——用遗传算法求解函数最大/最小值

1. 问题描述:连梯度下降都会迷路的“疯狂函数”

2. 代码实现:用 mlrose 优雅地定义“伊甸园”

五、实战2:进阶应用——遗传算法求解非线性规划问题

1. 业务场景设定:智能投资组合优化

2. 核心难点:如何处理“约束”?

3.代码实现:

4. 代码深度解读

A. 状态空间定义 (DiscreteOpt)

B. 适应度函数的逻辑

C. 遗传算法参数

5. 结果分析与优势

六、实战3:经典组合优化——遗传算法解决旅行商(TSP)问题

1. 什么是 TSP 问题?(小白秒懂版)

2. 核心难点:编码与算子(防坑指南)

3. 完整 Python 代码实现

4. 代码深度解读

A. TSPOpt 的整数编码

B. 适应度函数(距离最小化)

C. 变异操作的进化

5. 结果分析

七、实战4:高阶拓展——遗传算法求解多目标优化问题

1.核心概念:帕累托最优(Pareto Efficiency)

2. 实战场景:云服务器配置选择

3. 完整 Python 代码实现

4. 代码解读

A. 权重分配 (Weighting)

B. 归一化 (Normalization)

C. 帕累托前沿的意义

5. 结果分析


一、 序言:为什么你需要“上帝之手”?

在工程与代码的世界里,我们每天都在试图寻找“最优解”:如何让工厂的物流成本最低?如何让机器学习模型的误差最小?如何在一个极其复杂的非线性函数 f(x) 中找到那座最高的“山峰”?

传统算法的瓶颈:当数学工具在“群山”中迷路

 面对这些问题,我们通常会求助于传统的微积分和解析方法,例如梯度下降法或牛顿法。你可以把这些传统算法想象成一个被蒙住双眼的登山者:他只能用脚感受地面的倾斜程度(计算梯度),然后朝着感觉最陡峭的方向向上爬。

这种策略在平滑的单峰地形中非常高效。但在现实世界的复杂问题中,地形往往是崎岖不平、甚至存在断层的。这时,传统算法就会暴露出两个致命的瓶颈:

  1. 陷入局部最优(Local Optimum): 登山者好不容易爬上了一个山头,便以为自己到达了世界之巅,却不知道真正的珠穆朗玛峰就在几公里外。一旦陷入局部最优,传统算法就彻底“卡死”了。

  2. 面对不可导函数束手无策: 如果地形充满了悬崖峭壁(函数不连续、不可导),或者是离散的组合问题(比如旅行商要按顺序走过几个城市),登山者根本无法计算“倾斜度”,传统数学工具直接失效。

遗传算法的哲学:优胜劣汰,适者生存

既然一个聪明的登山者搞不定,那如果是一整群随机空投的猴子呢?

这就是遗传算法(Genetic Algorithm, GA)的奇妙哲学。它不再死磕严格的数学推导,而是向大自然借用了一双“上帝之手”——进化论

遗传算法不去计算什么导数,而是直接在地图上随机生成一批“初始种群”。通过一套残酷但高效的自然法则来筛选它们:

  • 适应度评估: 站得越高(目标函数值越好),生存概率越大。

  • 交叉与变异: 优秀的个体之间交换“基因”产生后代,并偶尔发生基因突变,赋予它们跳出当前小山头、飞跃到新大陆的能力。

经过一代又一代的繁衍,平庸的个体被淘汰,携带优秀基因的个体不断汇聚。最终,这群生命的后代将自动盘踞在全局最优解的巅峰。

本文看点:从原理到代码的跨越

如果你厌倦了枯燥的数学推导,想要掌握一种通用、强悍且极具画面感的启发式搜索算法,那么这篇文章就是为你准备的。接下来,我将带你完成一次从碳基生物到硅基代码的进化之旅:

  • 🧬 拆解黑盒: 用大白话搞懂基因编码、轮盘赌选择、交叉与变异的核心逻辑。

  • ⛰️ 降维打击非线性规划: 用 Python 亲手写出跨越局部最优陷阱的代码,找到复杂函数的极值。

  • 🗺️ 破解 TSP 难题: 将进化论应用于旅行商问题,寻找多城市间的极速路径。

  • ⚖️ 挑战多目标优化: 探索当“鱼和熊掌不可兼得”时,遗传算法如何找到完美的帕累托最优解。

准备好你的 Python 编辑器,让我们开始这场代码世界的物种起源!

二、 核心概念:把进化装进代码里

遗传算法听起来高深,但它的核心其实就是把生物界的“达尔文进化论”翻译成计算机能听懂的语言。在动手写代码之前,我们需要先搞懂几个基础概念的“翻译”工作。

1. 基因与染色体:数据的编码

在生物学中,我们的性状由染色体决定,而染色体由无数个基因组成。在代码世界里:

  • 染色体(Chromosome/Individual): 代表问题的一个可行解(也就是一个答案)。

  • 基因(Gene): 代表这个答案里的具体参数

假设我们在玩一个游戏:寻找参数 xy,让某个收益最大化。那么 [x,y]这个组合就是一条染色体,里面的 xy就是具体的基因。

如何把这些参数写成代码?(编码方式)

  • 二进制编码(Binary Encoding): 最经典的玩法,把数字变成 01 的序列。比如一条染色体长这样:10110100。它非常适合解决“选或不选”的背包问题,或者进行简单的逻辑交叉。

  • 实数编码(Real Encoding): 简单粗暴,直接用真实数值。比如染色体是 [3.14, 2.71]。在解决实际的工程函数求最值问题时,实数编码更精确、也更省内存。

2. 种群:寻找答案的“搜索小队”

大自然从来不会只让一只猴子去进化。遗传算法也是一样,我们需要随机生成一大批染色体,这批染色体的集合就叫种群(Population)

  • 种群的大小通常用 N 表示(比如N=100,代表有 100 个备选答案)。

  • 人多力量大,种群数量越大,搜索范围越广,越不容易漏掉正确答案;但同时,电脑计算的速度也会相应变慢。这就是我们需要权衡的艺术。

3. 适应度函数(Fitness):衡量谁才是“最强个体”的标准

进化论的核心是“适者生存”。那么,谁是“适者”?这就需要一个裁判——适应度函数

它是一个数学公式,用于给种群中的每一个个体打分。分数越高,说明这个答案越好,它活下来并繁衍后代的概率就越大。

  • 求最大值问题: 如果我们的目标是求函数 f(x)的最大值,那非常简单,适应度函数 F(x) 直接等于目标函数就行:

    F(x)=f(x)

  • 求最小值问题: 如果目标是求最小值(比如寻找最短路径,距离越短越好),我们就需要把“越小的值”转换成“越高的分数”。通常的数学做法是取倒数(为了防止分母为 0,通常会加一个极小的常数 c):

    F(x)=\frac{1}{f(x)+c}

4. 进化三部曲:代码里的生生不息

有了种群,也有了打分标准,接下来就是让程序自己去“繁衍”了。从父代到子代,需要经历三个神圣的步骤:

第一步:选择(Selection)—— 轮盘赌与精英保留

我们要从现有的种群中挑选优秀的个体来当“父母”。

  • 轮盘赌选择法(Roulette Wheel Selection): 并不是只有第一名才有资格繁衍,而是每个人都有机会,只是适应度越高的人,中奖概率越大

    假设个体i的适应度为F_{i},种群总数为 N,那么它被选中的概率 P_{i}的数学公式为:

    P_{i}=\frac{F_{i}}{\sum_{j=1}^{N}F_{j}}

    (简单理解:把所有人的分数加起来画一个大圆盘,你的分数越高,你占据的扇形面积就越大,飞镖扎中你的概率就越高。)

  • 精英保留(Elitism): 为了防止优秀的基因在随机交配中意外丢失,我们通常会直接把这一代里排名第一的“超级精英”,原封不动地复制到下一代中。这是一种兜底策略。

第二步:交叉(Crossover)—— 基因重组产生新可能

选出了父母,接下来就是生孩子。父母双方各自拿出一部分基因进行交换,产生带有双方特征的新个体。

  • 比如对于二进制编码 110|00001|11,如果在第三位进行“单点交叉”,它们的后代就会变成 110|11001|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. 问题描述:连梯度下降都会迷路的“疯狂函数”

假设我们有一个波动极其剧烈、充满陷阱的目标函数:

f(x)=x\cdot sin(10\pi x)+2

我们需要在 x\in [-1,2]这个区间内寻找它的全局最大值。

你可以把这个函数想象成连绵起伏的山脉,里面有无数个小山头(局部最优解)。传统的梯度下降算法就像一个被蒙住眼睛的登山者,随便摸到一个上坡就往上爬,极大概率会永远卡在半山腰的某个小土包上。

但遗传算法不怕,我们直接空投一批“数字猴子”,让它们自己去繁衍、去变异、去寻找真正的世界之巅!

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_valmax_val。这不仅仅是告诉算法从哪里开始找,更是一道“基因锁”。在变异和交叉的过程中,一旦产生的新坐标超出了 [-1, 2] 的范围,算法会自动将其拉回边界。这在解决具有严格物理限制的工程问题时是必不可少的。

五、实战2:进阶应用——遗传算法求解非线性规划问题

1. 业务场景设定:智能投资组合优化

为了让例子更有趣,我们假设你手里有 100万元 资金,需要分配到 5 个不同的项目(A, B, C, D, E)中。

  • 目标:最大化总收益(每个项目收益率是非线性的)。

  • 0-1 变量约束:每个项目要么投,要么不投(由一个开关变量控制)。

  • 多种约束

    1. 预算约束:总投资额不能超过 100 万。

    2. 必选约束:项目 A 必须投(因为它最稳)。

    3. 多样性约束:至少要投 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. 目标 1:最小化成本(越省钱越好)。

  2. 目标 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. 结果分析

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐