遗传算法小白入门教程:用“自然法则”解决优化问题
1. 背景溯源:从进化论到遗传算法
要理解遗传算法(Genetic Algorithm, GA),先回到达尔文的进化论——生物通过“遗传、变异、自然选择”不断进化,适应环境的个体存活并繁衍,不适应的被淘汰。
20世纪60年代,美国科学家**约翰·霍兰德(John Holland)**受到进化论启发,提出了遗传算法的雏形:用“生物进化”的逻辑解决工程与数学中的优化问题。简单来说,就是把“问题的解”模拟成“生物个体”,通过“自然选择”筛选优秀解,“遗传变异”产生更优解,最终迭代出接近最优的结果。
到20世纪80-90年代,遗传算法逐步成熟,被广泛应用于函数优化、组合调度、机器学习等领域(比如神经网络的超参数调优)。
2. 核心思想:用“生物语言”重构优化问题
遗传算法的本质是**“种群迭代优化”**,核心逻辑可以概括为三句话:
- 解=生物个体:把问题的一个解编码成一个“染色体”(比如二进制串);
- 优解=适者生存:用“适应度函数”给每个解打分,分数越高越“优秀”;
- 进化=遗传+变异:通过“选择(选优秀解当父母)、交叉(父母基因重组生小孩)、变异(小孩基因随机突变)”产生新一代解,迭代后得到更优解。
3. 基础概念定义:解的“生物化”表达
在开始算法前,必须先明确遗传算法的“生物术语”与“问题术语”的对应关系(这是理解后续内容的关键!):
| 生物术语 | 问题术语 | 解释举例 |
|---|---|---|
| 染色体(Chromosome) | 解的编码 | 比如用12位二进制串`001011001011`表示函数`f(x)=x*sin(10πx)+2`的解`x=-0.478` |
| 基因(Gene) | 编码的基本单位 | 二进制串中的某一位(比如`001011001011`的第3位是`1`) |
| 种群(Population) | 一组解的集合 | 比如10个二进制串组成的集合,代表10个候选解 |
| 适应度(Fitness) | 解的优劣评分 | 用目标函数值衡量(比如`f(x)=2.294`就是`x=-0.478`的适应度) |
| 基因型(Genotype) | 编码后的解(二进制/实数串) | `001011001011` |
| 表型(Phenotype) | 解码后的实际解(问题变量) | `x=-0.478` |
4. 算法核心原理:三大遗传操作详解
遗传算法的“进化”过程由选择、交叉、变异三个操作完成,每个操作都有明确的目标:
4.1 选择(Selection):选出“优秀父母”
目标:从当前种群中选出适应度高的个体,作为“父母”来生下一代(保证“优秀基因”传递)。常见方法:
(1)轮盘赌选择(Roulette Wheel Selection)
- 原理:把每个个体的适应度占比当作“轮盘面积”,随机转指针选个体——适应度越高,占的面积越大,被选中的概率越高。
- 计算步骤:
4. 计算种群总适应度:F = Σ_{i=1}^N f(i)(N是种群大小,f(i)是个体i的适应度);
5. 计算个体i的选择概率:p(i) = f(i) / F;
6. 生成0~1的随机数,落在哪个个体的概率区间,就选哪个。 - 例子:若3个个体的适应度是5、3、2,总适应度是10,则概率分别是0.5、0.3、0.2——轮盘上50%的面积属于第一个个体,被选中的概率最高。
(2)锦标赛选择(Tournament Selection)
- 原理:随机选
k个个体(比如k=2),选其中适应度最高的当父母——避免轮盘赌中“极端高适应度个体垄断”的问题。 - 优点:更公平,更适合大规模种群。
4.2 交叉(Crossover):父母基因重组生“小孩”
目标:通过父母基因的交换,产生兼具父母优点的子代(探索新的解空间)。关键:交叉需要“编码方式”匹配——常见编码有二进制编码(适合离散问题)和实数编码(适合连续问题)。
(1)二进制编码的交叉
单点交叉(Single-Point Crossover):
7. 随机选一个“交叉点”(比如二进制串的第5位后);
8. 交换两个父母交叉点后的基因。例子:父代1:
10110 | 101(交叉点在第5位后)父代2:11001 | 010子代1:10110 010(父1前5位+父2后3位)子代2:11001 101(父2前5位+父1后3位)两点交叉(Two-Point Crossover):选两个交叉点,交换中间的基因(更灵活,适合长染色体)。
(2)实数编码的交叉
- 算术交叉(Arithmetic Crossover):用线性组合生成子代,公式为:子代1:
x1' = α·x1 + (1-α)·x2子代2:x2' = α·x2 + (1-α)·x1其中α∈[0,1]是“混合系数”(比如α=0.5就是父母的平均值)。 - 例子:父代1是
x1=1.2,父代2是x2=3.4,α=0.5,则子代1=2.3,子代2=2.3。
交叉概率(Pc):控制交叉操作的频率(通常取0.6~0.9)——概率太高会破坏好基因,太低则探索不足。
4.3 变异(Mutation):给“小孩”随机突变
目标:随机修改子代的某几个基因,避免种群“同质化”(防止陷入局部最优)。常见方法:
(1)二进制编码的变异
- 原理:对每个基因位,以“变异概率(Pm)”翻转(0变1,1变0)。
- 例子:个体
10110101,变异概率0.01——每个位有1%的概率翻转,比如第3位翻转后变成10010101。
(2)实数编码的变异
- 原理:给实数变量加一个小的随机扰动(比如正态分布
N(0,σ²)的随机数)。 - 公式:
x' = x + σ·N(0,1)(σ是扰动幅度,通常取变量范围的1%~5%)。 - 例子:
x=1.234,σ=0.1,随机数是0.05,则变异后x'=1.284。
变异概率(Pm):控制变异频率(通常取0.001~0.05)——概率太高会变成“随机搜索”,太低则无法跳出局部最优。
5. 完整求解步骤:从问题到答案的全流程
现在用**“最大化函数f(x)=x·sin(10πx)+2,x∈[-1,2]”**这个简单例子, step by step 演示遗传算法的求解过程:
5.1 步骤1:问题建模与目标函数定义
首先明确优化目标和约束条件:
- 目标函数:
max f(x) = x·sin(10πx) + 2(要最大化的函数); - 约束条件:
x∈[-1,2](变量范围); - 精度要求:
ε=0.001(x要精确到小数点后3位)。
5.2 步骤2:解的编码(染色体设计)
遗传算法操作的是“染色体”,所以需要把实际解x编码成染色体(关键!)。
(1)选择编码方式
这里选二进制编码(适合连续变量的离散化)。
(2)计算染色体长度
要表示x∈[-1,2]且精度ε=0.001,需要的离散份数是:M = (b - a)/ε + 1 = (2 - (-1))/0.001 + 1 = 3001(a是下限,b是上限)。染色体长度L需要满足2^L ≥ M(二进制能表示的数要覆盖所有份数)。计算得2^12=4096 ≥ 3001,所以L=12(12位二进制串)。
5.3 步骤3:初始化种群
生成N个随机染色体(N是种群大小,通常取50~200,这里取N=10)。比如生成10个12位二进制串:001011001011,110010110010,011001011001, …,101100101100。
5.4 步骤4:适应度函数计算
目标:给每个染色体打分(适应度),衡量解的优劣。
(1)解码:染色体→实际解x
二进制串b = b_{11}b_{10}...b_1b_0(12位,从高位到低位)对应的十进制值是:dec(b) = Σ_{i=0}^{11} b_i·2^i(b_i是第i位的二进制数,0或1)。解码成x的公式:x = a + (b - a)·dec(b)/(2^L - 1)(把二进制值映射到[-1,2]区间)。代入数值:x = -1 + 3·dec(b)/4095(因为2^12-1=4095)。
(2)计算适应度
目标是最大化f(x),所以直接用f(x)作为适应度函数:fit(x) = f(x) = x·sin(10πx) + 2。
例子:染色体001011001011的十进制值dec(b)=715(计算:0*2^11 +0*2^10 +1*2^9 +0*2^8 +1*2^7 +1*2^6 +0*2^5 +0*2^4 +1*2^3 +0*2^2 +1*2^1 +1*2^0=512+128+64+8+2+1=715)。解码得x = -1 + 3*715/4095 ≈ -0.478。适应度fit(x) = -0.478·sin(10π*(-0.478)) + 2 ≈ 2.294。
5.5 步骤5:遗传操作(选择→交叉→变异)
现在用步骤4得到的适应度,进行遗传操作:
(1)选择:轮盘赌选父母
计算10个个体的适应度总和F=Σfit(x_i),再计算每个个体的选择概率p(i)=fit(x_i)/F。比如个体1的适应度是2.294,总和是20,则p(1)=2.294/20≈0.1147。生成随机数选10个父母(因为要保持种群大小10)。
(2)交叉:单点交叉生子代
设置交叉概率Pc=0.8(80%的父母会交叉)。随机选交叉点(比如第5位后),交换父母基因。比如父母1是001011|001011,父母2是110010|110010,交叉后子代1是001011110010,子代2是110010001011。
(3)变异:随机突变子代
设置变异概率Pm=0.01(每个基因位有1%的概率翻转)。比如子代1是001011110010,第3位翻转后变成001010110010。
5.6 步骤6:种群更新与精英保留
目标:保证下一代种群比上一代更优(避免最优解丢失)。
(1)精英保留策略
把上一代适应度最高的2个个体直接保留到下一代(防止优秀解被交叉变异破坏)。
(2)种群更新
将“保留的精英”+“交叉变异后的子代”合并,选适应度最高的10个个体作为下一代种群(保持种群大小不变)。
5.7 步骤7:迭代终止判断
重复步骤4~6,直到满足终止条件(选其中一个即可):
9. 达到最大迭代次数(比如100次);
10. 连续K次迭代适应度变化小于阈值(比如连续10次迭代,最优适应度变化小于0.001);
11. 适应度达到预期值(比如fit(x)≥3.8,因为f(x)的理论最大值约为3.8)。
5.8 步骤8:解码得到最优解
迭代终止后,下一代种群中适应度最高的个体就是“最优解”。比如最后一代的最优染色体是110100110010,解码得x≈1.85,适应度fit(x)≈3.8(接近理论最大值)。
6. 适用边界与局限性:知道什么时候用
遗传算法是启发式算法(Heuristic Algorithm)——不保证得到全局最优,但能在合理时间内找到“较优解”。以下是它的适用场景和不适用场景:
6.1 适合的问题
- 非线性/非凸问题:目标函数有多个局部最优(比如
f(x)=x·sin(10πx)+2有多个峰); - 高维问题:变量数多(比如100个变量的函数优化);
- 无导数问题:无法计算目标函数的梯度(比如黑箱函数,只能算函数值);
- 组合优化问题:如旅行商问题(TSP)、背包问题、调度问题(变量是离散的排列);
- 机器学习超参数优化:比如神经网络的学习率、隐藏层大小(超参数多,无法用梯度下降)。
6.2 不适合的问题
- 凸优化问题:目标函数是凸的(只有一个全局最优),用梯度下降、牛顿法更高效;
- 线性规划问题:有精确的单纯形法(比如
max c^T x s.t. Ax≤b); - 小规模问题:变量少(比如3个变量),精确解容易得到;
- 需要绝对精确解的问题:比如航天工程中的关键参数(必须精确到小数点后10位,遗传算法达不到)。
6.3 局限性
- 随机性:每次运行结果可能不同(需要多次运行取平均);
- 参数敏感:种群大小、交叉/变异概率、迭代次数需要调优(新手可能踩坑);
- 计算成本:高维问题需要大种群和多次迭代,计算量很大;
- 适应度函数依赖:若适应度函数不能准确反映解的优劣,结果会很差(比如目标是最小化成本,但适应度函数用了最大化利润,没转换)。
7. 新手常见误区解答
(1)误区:变异概率越高越好?
错!变异是“小概率扰动”——概率太高会破坏好基因,变成“随机搜索”;太低则无法跳出局部最优(通常取0.001~0.05)。
(2)误区:种群越大越好?
错!种群太大计算量爆炸(比如1000个个体,迭代100次就是10万次计算);太小则缺乏多样性(容易陷入局部最优)(通常取50~200)。
(3)误区:不需要精英保留?
错!精英保留能保证“最优解不丢失”——比如上一代的最优解适应度是3.8,若不保留,交叉变异后可能变成3.5,导致结果变差。
(4)误区:遗传算法能找到全局最优?
错!遗传算法是“近似算法”——只能找到“较优解”,不一定是全局最优(但通过调参和多次运行,能接近全局最优)。
8. 总结:遗传算法的“本质”
遗传算法的核心是**“用种群的多样性探索解空间,用选择压力引导进化方向”**——它模拟了自然选择的“试错+优化”过程,适合解决那些“传统方法搞不定”的复杂问题。
作为新手,建议从简单函数优化(比如本文的f(x))开始练习,再逐步尝试组合优化(比如TSP问题),最后应用到机器学习超参数优化——多练几次,就能掌握遗传算法的精髓!
案例介绍
importnumpyasnpimportrandom# -------------------------- 问题定义模块 --------------------------# 目标函数:max f(x) = x * sin(10 * pi * x) + 2defcalc_fitness(x):returnx*np.sin(10*np.pi*x)+2# 解码函数:二进制染色体 -> 十进制实数x (x ∈ [-1, 2])defdecode(chromosome,a=-1,b=2,L=12):# 将二进制字符串转换为十进制整数dec_value=int(chromosome,2)# 映射到[-1, 2]区间x=a+(b-a)*dec_value/(2**L-1)returnx# -------------------------- 遗传操作模块 --------------------------# 初始化种群:生成N个长度为L的随机二进制字符串definit_population(N,L):population=[]for_inrange(N):# 生成L位随机二进制字符串chromosome=''.join([str(random.randint(0,1))for_inrange(L)])population.append(chromosome)returnpopulation# 轮盘赌选择:根据适应度选择父代defroulette_selection(population,fitness):# 计算总适应度total_fitness=sum(fitness)# 计算选择概率selection_probs=[f/total_fitnessforfinfitness]# 轮盘赌选择selected=random.choices(population,weights=selection_probs,k=len(population))returnselected# 单点交叉:两个父代染色体产生两个子代defsingle_point_crossover(parent1,parent2,pc):ifrandom.random()>pc:# 不交叉,直接返回父代returnparent1,parent2# 随机选择交叉点(1到L-1之间)L=len(parent1)cross_point=random.randint(1,L-1)# 生成子代child1=parent1[:cross_point]+parent2[cross_point:]child2=parent2[:cross_point]+parent1[cross_point:]returnchild1,child2# 二进制变异:随机翻转染色体的基因位defbinary_mutation(chromosome,pm):mutated=list(chromosome)# 字符串转列表以便修改L=len(mutated)foriinrange(L):ifrandom.random()<pm:# 翻转基因mutated[i]='1'ifmutated[i]=='0'else'0'return''.join(mutated)# -------------------------- 主程序模块 --------------------------defmain():# -------------------------- 参数设置 --------------------------a=-1# 变量x的下限b=2# 变量x的上限L=12# 染色体长度(二进制)N=100# 种群大小max_iter=200# 最大迭代次数pc=0.8# 交叉概率pm=0.01# 变异概率elite_num=2# 精英保留数量# 初始化种群population=init_population(N,L)# 迭代优化foriterinrange(max_iter):# 1. 计算适应度fitness=[calc_fitness(decode(chrom,a,b,L))forchrominpopulation]# 2. 精英保留:选择适应度最高的elite_num个个体# 按适应度从大到小排序种群sorted_pop_fit=sorted(zip(population,fitness),key=lambdax:x[1],reverse=True)elite=[chromforchrom,fitinsorted_pop_fit[:elite_num]]# 精英个体# 3. 轮盘赌选择父代parents=roulette_selection(population,fitness)# 4. 交叉操作:生成子代offspring=[]foriinrange(0,N,2):ifi+1<N:parent1=parents[i]parent2=parents[i+1]child1,child2=single_point_crossover(parent1,parent2,pc)offspring.extend([child1,child2])else:# 若种群数量为奇数,直接保留最后一个父代offspring.append(parents[i])# 5. 变异操作offspring=[binary_mutation(child,pm)forchildinoffspring]# 6. 更新种群:精英 + 子代中前N-elite_num个适应度最高的个体# 计算子代的适应度offspring_fitness=[calc_fitness(decode(chrom,a,b,L))forchrominoffspring]# 按适应度从大到小排序子代sorted_offspring=sorted(zip(offspring,offspring_fitness),key=lambdax:x[1],reverse=True)# 选择子代中前N-elite_num个selected_offspring=[chromforchrom,fitinsorted_offspring[:N-elite_num]]# 合并精英和选择的子代,组成新一代种群population=elite+selected_offspring# 迭代结束,输出最优解final_fitness=[calc_fitness(decode(chrom,a,b,L))forchrominpopulation]# 找到最优个体和对应的适应度best_idx=np.argmax(final_fitness)best_chrom=population[best_idx]best_x=decode(best_chrom,a,b,L)best_fit=final_fitness[best_idx]print("最优解:")print(f"染色体:{best_chrom}")print(f"x值:{best_x:.5f}")print(f"适应度值:{best_fit:.5f}")# 运行主程序if__name__=="__main__":main()OK,今天数学建模算法分享就到这里结束了,若内容对大家有所帮助,可以收藏慢慢看,感谢大家支持
本文有若有不足之处,希望各位帅哥美女们能给出宝贵的意见。谢谢大家!
初来乍到,本期制作不易希望各位帅哥美女们能动动小手,三连走一走!!!
如果大家觉得主播写的还不错的话可以关注一下,每日都会有推文的,期待一下明天的干货把,嘿嘿~