遗传算法讲解与实现(python)

Posted _WILLPOWER_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了遗传算法讲解与实现(python)相关的知识,希望对你有一定的参考价值。

引言

遗传算法在我看来是一种调参的时候可以考虑的算法,是一种可以找到全局最优参数的一种方法,当需要调参的数据范围很大的时候,穷举法显然不是一个很好的选择!这里通过一个简单的例子将遗传算法进行实现,以小见大。

介绍

遗传算法通过模拟自然界生物的优胜劣汰进化现象,把需要求解的问题抽象为一个遗传进化问题,把搜索空间映射为遗传空间,把可能的解编码成一个向量(染色体),而向量中的每一个元素则成为基因,通过不断计算各个染色体的适应值,选择最好的染色体,不断选择进化,不断接近我们需要的最优解。

我后面会首先介绍遗传算法的基本运算步骤,并且会解释如何取抽象我们的问题到遗传算法本身中。

遗传算法基本步骤

  1. 选择运算
  2. 交换操作
  3. 变异

选择操作

意义: 从旧的种群中选择适应度高的染色体,为以后的染色体交换,变异产生新的染色体做准备。
这一步是优胜劣汰的具体实现,而选择的方式也有很多,比如:轮转法,精英法等。
具体是个什么意思呢?就是说比如现在你创造了一群人,那么你需要一个方式来决定他们达到什么条件才能够生存下去,如果你设定在团队中前1/n力气大的才能够生存下去,那么你的方式就叫做精英法,只有力气大才能够生存;而如果你设定力气越大,那么生存下去的几率就越大,而不是绝对的力气大才能够生存下去,那么这种就叫做轮转法,相当于你在转转盘选择,占的面积多的(力气大的)自然被选择生存下去的几率就越大!

这个例子的力气大,我们称为适应度函数的目标,你可以决定长得高为适应度函数的目标,那么长得高的自然就是我们后面决定他们是否生存下去的重要指标,而适应度函数就是用已有条件(染色体)评估他力气到底有多大的函数。而这里的一群人就是指的染色体的个数,你可以设定10个人,你也可以设定100个人,你设定的人数越多,那么 染色体也就更加丰富,当然带来的是计算的复杂度。

因此这里我们需要确定的参数:染色体个数 适应度函数 选择策略

还是刚才的例子,我们使用轮转法做一个选择操作:
假定我们设定一群人具体为6人,则染色体我们有6个(假设定这样,不要问为什么一个人就一个染色体…),并且我们通过力气大来决定他们是否生存,力气越大生存下去的几率就越大!

这里染色体的组成是由人的特性组成:如是男是女,是胖是瘦之类的,按照某种映射关系,转换为了当前的二进制。
而适应度函数这里假设也根据某种计算,根据染色体的二进制组成,可以得到该染色体各种特性影响下的力气值。

序号染色体适应度值累计换算为百分比
11101002020+020/67=0.298=29.8%
20100100520+5=255/67=0.074=7.4%
30011001225+12=3712/67=0.179=17.9%
41100113037+30=6730/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>=(UmaxUmin)10m
因此如果需要小数点后四位,我们至少需要17位的二进制
2 17 = 131072 > = ( 9 − 1 ) ∗ 1 0 4 = 80000 2^{17}=131072 >= (9 - 1)*10^4=80000 217=131072>=(91)104=80000
而通过这种表达,我们也可以算出它的精度是多少
这里给出精度计算公式:
精度Q Q = ( U m a x − U m i n ) / 2 l − 1 Q = (U_{max} - U_{min})/2^{l} - 1 Q=(UmaxUmin)/2l1
精度结果
Q = ( 9 − 1 ) / 2 17 − 1 = 6.103515625 e − 05 Q = (9 - 1)/2^{17} - 1 = 6.103515625e-05 Q=(91)/2171=6.103515625e05
这个精度越小当然越好一些,但是带来的则是庞大的二进制位数,计算起来就很麻烦。

因此我们需要如下步骤来进行我们的算法:

结果分析

这是在计算适应度的时候的一个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:以指定概率执行某个函数。

以上是关于遗传算法讲解与实现(python)的主要内容,如果未能解决你的问题,请参考以下文章

遗传算法详解与MATLAB实现

遗传算法详解与MATLAB实现

遗传算法介绍并附上Python代码

使用Python实现的遗传算法 附完整代码

智能算法系列之遗传算法

遗传算法GA--句子配对(Python)