遗传算法讲解与实现(python)
Posted _WILLPOWER_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了遗传算法讲解与实现(python)相关的知识,希望对你有一定的参考价值。
引言
遗传算法在我看来是一种调参的时候可以考虑的算法,是一种可以找到全局最优参数的一种方法,当需要调参的数据范围很大的时候,穷举法显然不是一个很好的选择!这里通过一个简单的例子将遗传算法进行实现,以小见大。
介绍
遗传算法通过模拟自然界生物的优胜劣汰进化现象,把需要求解的问题抽象为一个遗传进化问题,把搜索空间映射为遗传空间,把可能的解编码成一个向量(染色体),而向量中的每一个元素则成为基因,通过不断计算各个染色体的适应值,选择最好的染色体,不断选择进化,不断接近我们需要的最优解。
我后面会首先介绍遗传算法的基本运算步骤,并且会解释如何取抽象我们的问题到遗传算法本身中。
遗传算法基本步骤
- 选择运算
- 交换操作
- 变异
选择操作
意义: 从旧的种群中选择适应度高的染色体,为以后的染色体交换,变异产生新的染色体做准备。
这一步是优胜劣汰的具体实现,而选择的方式也有很多,比如:轮转法,精英法等。
具体是个什么意思呢?就是说比如现在你创造了一群人,那么你需要一个方式来决定他们达到什么条件才能够生存下去,如果你设定在团队中前1/n力气大的才能够生存下去,那么你的方式就叫做精英法,只有力气大才能够生存;而如果你设定力气越大,那么生存下去的几率就越大,而不是绝对的力气大才能够生存下去,那么这种就叫做轮转法,相当于你在转转盘选择,占的面积多的(力气大的)自然被选择生存下去的几率就越大!
这个例子的力气大,我们称为适应度函数的目标,你可以决定长得高为适应度函数的目标,那么长得高的自然就是我们后面决定他们是否生存下去的重要指标,而适应度函数就是用已有条件(染色体)评估他力气到底有多大的函数。而这里的一群人就是指的染色体的个数,你可以设定10个人,你也可以设定100个人,你设定的人数越多,那么 染色体也就更加丰富,当然带来的是计算的复杂度。
因此这里我们需要确定的参数:染色体个数
适应度函数
选择策略
还是刚才的例子,我们使用轮转法做一个选择操作:
假定我们设定一群人具体为6人,则染色体我们有6个(假设定这样,不要问为什么一个人就一个染色体…),并且我们通过力气大来决定他们是否生存,力气越大生存下去的几率就越大!
这里染色体的组成是由人的特性组成:如是男是女,是胖是瘦之类的,按照某种映射关系,转换为了当前的二进制。
而适应度函数这里假设也根据某种计算,根据染色体的二进制组成,可以得到该染色体各种特性影响下的力气值。
序号 | 染色体 | 适应度值 | 累计 | 换算为百分比 |
---|---|---|---|---|
1 | 110100 | 20 | 20+0 | 20/67=0.298=29.8% |
2 | 010010 | 05 | 20+5=25 | 5/67=0.074=7.4% |
3 | 001100 | 12 | 25+12=37 | 12/67=0.179=17.9% |
4 | 110011 | 30 | 37+30=67 | 30/67=0.447=44.7% |
解释一下为什么要有累计和百分比这两项,这个主要是为了轮转法编程实现考虑的。如下:
让后续轮转能够依据适应度的面积来选择染色体
因此选择的方式如下:
随机产生一个数N,数的范围是0<N<适应度函数累计最大值(这里是67),然后选择累计值大于等于N的那个染色体,比如随机数为35,那么选择的染色体就是3号,因为3号的累计为37>35,如果随机数是44,那么选择的染色体就是4号。通过这种方式,来选择染色体,直到达到你设定的初始种群大小。
交换操作
交换操作模拟染色体交换,将染色体上面的基因按照一定的规则进行交换,达到染色体创新的目的。
方法:单点交叉,多点交叉。
比如这里用简单的单点交叉,并且位置选择为1/2节点。
被选择染色体:
父染色体1:110 | 100
父染色体2:010 | 010
上面黄色标出来的位置即为交换位置,然后交换:
子染色体1: 110 | 010
子染色体2: 010 | 100
变异
变异操作模拟生物基因的突变.在染色体二进制编码中变异的操作为,1变成0;或0变成1。
突变产生染色体的多样性,避免进化中早期成熟,陷入局部极值点,突变的概率很低。
这个就根据你自己的来定
遗传算法的简单实现
这里我们来实现找到一个函数的最大值。
函数为:y=1/2*x+sin(3x),范围为x∈[1-9]
可以看到该函数会出现多个极大值,也就是我们常说的局部最优点。
因此根据步骤来我们首先要确定他的适应度函数,种群大小以及选择策略。
适应度函数: 因为我们这里是求函数的最大值,因此这里适应度函数的目标就是求最大值,而适应度函数就是函数本身!
种群大小: 这里设置为100
选择策略: 轮转法
确定为以上值后,我们要建立函数x值对应的二进制编码:
注意
解释一下为什么要转换为二进制进行操作:
二级制相对于十进制有更大的搜索范围以及更多的图式,并且操作也更加简单。
我们如果设定求解精度为小数点后4位,则我们需要多少位的二进制才能够表示我们呢?
给出如下公式:
其中l为二进制的位数,m为精度
2
l
>
=
(
U
m
a
x
−
U
m
i
n
)
∗
1
0
m
2^l >= (U_{max} - U_{min})*10^m
2l>=(Umax−Umin)∗10m
因此如果需要小数点后四位,我们至少需要17位的二进制
2
17
=
131072
>
=
(
9
−
1
)
∗
1
0
4
=
80000
2^{17}=131072 >= (9 - 1)*10^4=80000
217=131072>=(9−1)∗104=80000
而通过这种表达,我们也可以算出它的精度是多少
这里给出精度计算公式:
精度Q
Q
=
(
U
m
a
x
−
U
m
i
n
)
/
2
l
−
1
Q = (U_{max} - U_{min})/2^{l} - 1
Q=(Umax−Umin)/2l−1
精度结果
Q
=
(
9
−
1
)
/
2
17
−
1
=
6.103515625
e
−
05
Q = (9 - 1)/2^{17} - 1 = 6.103515625e-05
Q=(9−1)/217−1=6.103515625e−05
这个精度越小当然越好一些,但是带来的则是庞大的二进制位数,计算起来就很麻烦。
因此我们需要如下步骤来进行我们的算法:
结果分析
这是在计算适应度的时候的一个dataframe,因为适应度函数计算出来有负数的,因此这里采用的做法,是将整体往上抬高(都加上负数最大那个数的绝对值),这是处理适应度函数为负数的一个方法。
这是最后程序选出来的最优点(图片上红色的点),可以看到结果是非常不错的。
下图是种群平均适应随着遗传代数的增加,其值的变化,可以看见在25代左右其实已经开始收敛了。
程序源码
# -*- encoding: utf-8 -*-
'''
@Description: 遗传算法的一个例子
@Date :2021/10/27 14:57:16
@Author :willpower
@version :V1.0
'''
#%%
import numpy as np
from numpy.lib.function_base import average
import pandas as pd
import matplotlib.pyplot as plt
import random
zhongqun_cnt = 100 # 种群染色体个数
p_jiaohuan = 60 # 交换的概率为百分之60
p_bianyi = 10 # 变异的概率为百分之10
x_min = 1 # 自变量最低为1
x_max = 9 # 自变量最高为9
rotate_cnt = 200 # 遗传代数
#%%
# 定义函数表达式
def calc(x):
return 1/2*x+np.sin(3*x)
#%%
# 定义一个映射函数 将规定范围的 10进制映射为2进制
# (不是简单的映射,而是相同拉伸) 这是一个比例问题
def dec2bin(x):
return bin(int((2**17)*((x-1)/8)))
# 和上述函数相反
def bin2dec(x):
x1 = int(x, 2)
return x1/(2**17)*8+1
#%%
# 首先展示整个图形
x = np.linspace(x_min,x_max,100)
y = calc(x)
plt.plot(x, y)
plt.legend()
plt.show()
# %%
# 0-1
# 2^17-9
#随机选择初始轮转法需要使用的zhongqun_cnt个基因
init_population = np.random.uniform(x_min,x_max,zhongqun_cnt)
# %%
# 适应度函数
def fitness(x):
return calc(x)
#%%
# 选择函数,通过轮转法选出zhongqun_cnt个初始种群
# 输入为随机的上一代,输出为轮转法选出来的基因(10进制)
def select(x):
data = pd.DataFrame(x, columns=['ori_data'])
data['fitness'] = fitness(data['ori_data'])
# 因为适应度有负数,因此我整体往上移动,让其都为正(往上移动最低的那个负数即可)
data['fitness_processed'] = data['fitness'] + abs(data['fitness'].min())
# 添加累计 , 注意左闭右开
data['leiji'] = [sum(data['fitness_processed'][0:i]) for i in range(1, data.shape[0] + 1 )]
print(data)
# 开始选择,随机产生范围为0-(zhongqun_cnt-1)号的累计数值的随机数,取
# 大于等于随机数的累计数所对应的编号,这样取zhongqun_cnt个
select_dict = []
while len(select_dict) < data.shape[0]:
suijishu = np.random.uniform(data['leiji'][0], data['leiji'][data.shape[0] - 1], 1)[0]
print(suijishu)
xuanzeshu = data[data['leiji'] >= suijishu]['ori_data'].iloc[0]
print(xuanzeshu)
select_dict.append(xuanzeshu)
return select_dict
# 基因交换操作, 这里采用单点交换,因为二进制为17位
# 因此这里随机交换二基因的中间
# 输入为待交换的两个基因,输出为交换完成的两个基因(2进制输入)
def exchangge(x, y, div = 0.5):
weishu = len(x) - 2
# 减去2是因为0b会占用2个位置
percent = int(div * weishu)
# 开始组合
x1 = x[:2+percent] + y[-(weishu-percent):-1] + y[-1]
# print(x1)
y1 = y[:2+percent] + x[-(weishu-percent):-1] + x[-1]
# print(y1)
return x1, y1
# 基因变异操作,这里采用随机让第最低取反
# 输入为二进制
#%%
def variation(x, y):
x1 = x[:-1] + str((int(x[-1]) + 1)%2)
print(x1)
y1 = y[:-1] + str((int(y[-1]) + 1)%2)
print(y1)
return x1, y1
# 概率执行函数
def random_run(probability):
"""以probability%的概率执行func(*args)"""
list = []
for i in range(probability):
list.append(1)#list中放入probability个1
for x in range(100 - probability):
list.append(0)#剩下的位置放入0
a = random.choice(list)#随机抽取一个
return a
#%%
# 主函数
# 确定初始种群
selected = select(init_population)
# 开始进入优胜劣汰30轮
average_genes = []
average_genes.append(fitness(average(selected)))
while rotate_cnt:
rotate_cnt -= 1
# 概率交换
index_x = random.randint(0, 19)
index_y = random.randint(0, 19)
# 如果抽中了,则进行交换
if random_run(p_jiaohuan):
x1, y1 = exchangge(dec2bin(selected[index_x]), dec2bin(selected[index_y]))
selected[index_x], selected[index_y] = bin2dec(x1), bin2dec(y1)
# 概率变异(如果抽中了,则进行变异)
index_x = random.randint(0, 19)
index_y = random.randint(0, 19)
if random_run(p_bianyi):
x2, y2 = variation(dec2bin(selected[index_x]), dec2bin(selected[index_y]))
selected[index_x], selected[index_y] = bin2dec(x2), bin2dec(y2)
# 再次选择
selected = select(selected)
average_genes.append(fitness(average(selected)))
#%%
plt.plot(list(range(1, len(average_genes) + 1)),average_genes)
# %%
# 将最佳点的位置打印出来看一下
x = np.linspace(x_min,x_max,100)
y = calc(x)
plt.plot(x, y)
plt.plot(average(selected), fitness(average(selected)), marker='o', color='red')
plt.legend()
plt.show()
结语
可能描述的不是很清晰,主要是因为遗传算法可变的东西实在太多了,选择方法、交换方法以及变异方法等等。因此没有绝对的最好,只有你对问题本身的理解越深,你抽象此问题到遗传算法上面表现才越好!还有就是,只有你认真下来将遗传算法简单的实现了,你才会发现它核心的原理其实并不难理解,因此如果要设计此算法,还是动手写一下比较好。
参考
以上是关于遗传算法讲解与实现(python)的主要内容,如果未能解决你的问题,请参考以下文章