进化策略的数学基础:四大核心机制
大家好,我是你们的老朋友。今天咱们来聊聊进化策略的数学基础。说实话,很多初学者一听到「数学基础」四个字就头大,觉得全是公式推导。其实不然,进化策略的数学核心,说白了就是四个问题:怎么评价个体好坏?怎么产生新个体?怎么组合两个个体?怎么淘汰差的个体?
我当年刚入行时,也在这四个问题上栽过跟头。有一次做参数优化,适应度函数设计得不对,结果算法跑了三天三夜,愣是没找到最优解。后来才发现,问题出在适应度函数上。嗯,咱们今天就好好聊聊这四个核心机制。
核心要点:适应度函数、变异算子、重组算子、选择机制,这四者构成了进化策略的数学骨架。缺一不可,顺序也不能乱。
2.1 适应度函数设计
适应度函数,就是用来评价个体「好坏」的标尺。你想想看,如果没有这把尺子,算法怎么知道哪个个体该保留,哪个该淘汰?
我个人习惯把适应度函数分为两类:
- 直接型:目标函数本身就是适应度。比如求最大值问题,f(x) 越大越好。
- 间接型:需要把目标函数转换成适应度。比如求最小值问题,通常用 1/f(x) 或 -f(x) 来转换。
这里有个坑,我必须要提醒大家。我曾经在项目中遇到过一个问题:适应度函数的值域范围太大,导致算法早期就收敛到局部最优。为什么会这样?因为适应度差异过大,好的个体太突出,差的个体直接被忽略,多样性就没了。
避坑指南:我曾经吃过一次大亏。做多目标优化时,直接把两个目标函数相加作为适应度。结果两个目标的量纲差了一千倍,大目标完全主导了进化方向。后来我改用归一化处理,才解决了问题。
适应度函数设计的原则,我总结为三点:
- 单调性:适应度值必须与个体优劣正相关
- 区分度:不同个体之间要有足够的差异
- 稳定性:不能因为微小变化导致适应度剧烈波动
来看一个简单的代码示例:
# 适应度函数设计示例
def fitness_function(x):
# 假设我们要最大化 f(x) = -x^2 + 10x + 5
# 这是一个开口向下的抛物线,最大值在 x=5 处
return -x**2 + 10*x + 5
# 归一化处理,避免值域过大
def normalized_fitness(x):
raw = -x**2 + 10*x + 5
# 映射到 [0, 1] 区间
return (raw - (-20)) / (30 - (-20)) # 假设值域在 [-20, 30]
2.2 变异算子原理
变异算子,就是给个体「加点随机扰动」。你想想看,如果没有变异,所有个体都一模一样,那还进化个啥?
在进化策略中,最常用的变异算子是高斯变异。说白了,就是在当前个体的基础上,加上一个服从正态分布的随机数。
我记得有一次做参数调优,变异步长设得太大了。结果算法就像喝醉了酒,个体到处乱跳,根本收敛不了。后来我把步长设小,效果立竿见影。
变异算子的数学表达式很简单:
# 高斯变异
def gaussian_mutation(individual, sigma=0.1):
"""
individual: 当前个体(实数向量)
sigma: 变异步长(标准差)
"""
mutated = [x + random.gauss(0, sigma) for x in individual]
return mutated
实战技巧:我个人习惯用自适应变异步长。算法初期步长大一些,增加探索能力;后期步长小一些,精细搜索。你可以试试 1/t 衰减策略,其中 t 是进化代数。
变异算子的核心参数有两个:
| 参数 | 作用 | 常见取值 |
|---|---|---|
| 变异概率 | 控制个体发生变异的可能性 | 0.1 ~ 0.5 |
| 变异步长 | 控制变异幅度的大小 | 0.01 ~ 0.2(归一化后) |
2.3 重组算子原理
重组算子,就是「生娃」的过程。两个父本个体,通过某种方式组合,产生子代个体。这玩意儿在遗传算法里叫交叉,在进化策略里叫重组。
常用的重组算子有两种:
- 离散重组:每个基因位随机从两个父本中选择一个
- 中间重组:每个基因位取两个父本的平均值
我刚开始做进化策略时,总觉得离散重组更好,因为它保留了父本的多样性。后来发现,对于连续优化问题,中间重组往往更稳定。你想想看,平均值能平滑掉极端值,不容易产生「怪胎」。
# 离散重组
def discrete_recombination(parent1, parent2):
child = []
for p1, p2 in zip(parent1, parent2):
if random.random() < 0.5:
child.append(p1)
else:
child.append(p2)
return child
# 中间重组
def intermediate_recombination(parent1, parent2):
child = [(p1 + p2) / 2 for p1, p2 in zip(parent1, parent2)]
return child
注意:我曾经在项目中遇到过一个问题——重组算子产生的子代,适应度反而比父本还差。这是因为重组破坏了父本中已经形成的优良基因组合。解决办法是引入精英保留策略,把最好的父本直接复制到下一代。
2.4 选择机制原理
选择机制,就是「优胜劣汰」的具体实现。进化策略中最经典的选择机制是 (μ, λ) 选择和 (μ + λ) 选择。
这两种选择的区别,我一句话就能说清楚:
- (μ, λ) 选择:从 λ 个子代中选出 μ 个最好的,父本全部淘汰
- (μ + λ) 选择:从 μ 个父本和 λ 个子代中,一起选出 μ 个最好的
我个人更倾向于 (μ, λ) 选择。为什么?因为它能更好地保持种群多样性。父本全部淘汰,意味着算法不会「恋旧」,更容易跳出局部最优。
但 (μ + λ) 选择也有它的优势——收敛速度更快。因为好的父本可以一直保留,不会丢失已经找到的优秀解。
# (μ, λ) 选择
def mu_lambda_selection(population, offspring, mu):
"""
population: 父本种群(会被全部淘汰)
offspring: 子代种群
mu: 要选择的个体数量
"""
combined = offspring # 只从子代中选择
combined.sort(key=lambda ind: ind.fitness, reverse=True)
return combined[:mu]
# (μ + λ) 选择
def mu_plus_lambda_selection(population, offspring, mu):
combined = population + offspring # 父本和子代一起选
combined.sort(key=lambda ind: ind.fitness, reverse=True)
return combined[:mu]
核心总结:选择机制决定了算法的「探索」与「利用」平衡。(μ, λ) 偏向探索,(μ + λ) 偏向利用。具体用哪个,取决于你的问题特性。
知识体系总览
下面这张图,是我手绘的进化策略数学基础框架。你可以把它当作一张「地图」,随时回来对照。
好了,以上就是进化策略的四大数学基础。适应度函数是「标尺」,变异和重组是「引擎」,选择机制是「方向盘」。四者配合好了,算法才能跑得又快又稳。
我建议你动手写一个简单的进化策略代码,把这四个模块都实现一遍。纸上得来终觉浅,绝知此事要躬行。遇到问题随时回来翻翻这张图,应该能帮你理清思路。