遗传算法(Genetic Algorithm, GA)是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法。
GA属于元启发式算法,类似的还有蚁群算法、模拟退火等等。其本质上来说都属于随机搜索方法,理论上无穷时间条件下可以找到最优解(废话,无穷时间枚举也找到最优解了),但无法证明有限时间内其找到的解一定是最优解,也就是说找到的解有可能是局部最优解。实际应用中,往往只要找到的解满足需求,其实并不介意是否是全局最优解,故并不影响其广泛的应用。(无法证明能在多项式时间内找到最优解的问题称为NP-Hard,往往这类问题需要大量应用搜索方法来寻找最优(或局部最优)解)
GA有其独特的优点:
其缺点和局限性:
这里列出几个常用名词及定义:
个体(individual):每个个体代表一个可行解。
种群(population):个体的集合,可以看做是解的集合。
繁衍代数(generation):生物每一次繁衍看做一次迭代,类似生物每一个新的种群诞生都是新的一代。
进化(evolution):种群逐渐适应生存环境,品质不断得到改良(生物的进化是以种群的形式进行的)。在具体问题中就表现为越来越接近最优解。
基因(gene):每个生物体都有独特的DNA遗传信息,用基因来作为个体的标签,区别每个个体。
编码(coding):将个体编码成基因的形式。如二进制编码,把解编码为0-1二进制。
解码(decoding):编码的逆操作。
适应度(fitness):度量某个物种对于生存环境的适应程度,具体问题中即衡量解的质量。
选择(selection):自然选择,优胜劣汰,按适应度大小从种群中随机选择若干个体用于产生下一代。(优秀个体有更大概率被选择,本着优秀个体必然含优秀基因的原则)
交叉(crossover):被选中的个体进行基因重组或杂交,组成新的个体。
变异(mutation):在基因重组过程中(很小的概率)产生某些复制差错,变异产生新的染色体,表现出新的性状。
遗传算法(GA)流程图:
为了形象的理解,这里还是以一个具体的函数求解为例。问题如下:
f ( x ) = 4 x sin ( 10 x ) − 5 x cos ( 3 x ) , 0 < x < 10 f(x) = 4x\sin (10x) - 5x\cos (3x),0 < x < 10 f(x)=4xsin(10x)−5xcos(3x),0<x<10,求函数在定义域内极小值。函数图像如下:
可以看到函数f(x)在定义域(0,10)内有多个局部极值,且存在唯一极小值,函数解析解显然无法得到,同时梯度下降(GD)也容易陷入局部极值。
针对这个问题,各个模块定义如下:
python 实现代码如下:
import numpy as np
import random
from matplotlib import pyplot as plt
class GA():
def __init__(self,pop_size=100,gens_max=200,interval=[0,10],cross_rate=0.8,mutation_rate=0.003):
"""
pop_size : 种群数,每次迭代解的个数
gens_max : 最大迭代次数
interval : 解空间,即定义域,这里x=[0,10]
cross_rate : 染色体交叉概率
mutation_rate : 染色体发生变异概率,逃出局部极值,太小容易陷入局部极值,太大收敛速度慢
"""
self.DNA_size = 17 #染色体长度,这里取0-100000,2^16<100000<2^17
self.n = 4 #精度,n=4 表示小数点后4位,即0.0000-10.0000
self.pop_size = pop_size # 每一代种群数量,即每次迭代解的数量
self.generation_max = gens_max # 最多迭代次数
self.cross_rate = cross_rate # 染色体交叉概率
self.mutation_rate = mutation_rate #染色体变异概率
self.lower_bound = interval[0] #下界
self.upper_bound = interval[1] #上界
self.age = 0 #当前迭代次数
# 在区间[0,10]产生解,这里为了方便去掉小数点,在0-100000间生产100个随机数。
self.cur_gen = np.random.randint(self.lower_bound,self.upper_bound**(self.n+1),self.pop_size) #初始化随机化产生100个均匀分布的随机解
def encode(self,x):
"""
input : x 一组整数解
output : x_encode 一组二进制解
"""
x_encode=[]
for i in range(len(x)):
x_encode.append(bin(x[i])[2::])
if len(x_encode[i]) < self.DNA_size: #如果长度不足17位,前面补0,补足17,方便后续计算
for j in range(self.DNA_size-len(x_encode[i])):
x_encode[i]='0'+x_encode[i]
return x_encode
def decode(self, x_encode):
"""
input : x_encode 一组二进制解
output : x 一组整数解
"""
x=[]
for i in range(len(x_encode)):
x.append(int(x_encode[i],2))
return x
def evaluate(self,x):
"""
f(x)=4*x*sin(10x)-5*x*cos(3x),0
x=np.array(x)/10**self.n
x[x<self.lower_bound] = 0
x[x>self.upper_bound] = 0
return np.exp(-(4*x*np.sin(10*x)-5*x*np.cos(3*x)))
def select(self,pop):
"""
自然选择,轮盘赌选择模式,适应度越大,越有概率被选到。
input : pop_old 一组解
output : pop_new 根据适应度挑选的一组解
"""
fitness = self.evaluate(pop)
# 将pop按顺序从0-99编号,这里选取的是编号,在编号中按适应度大小选取100个元素,每一个元素选取的概率为p,可以被重复选
pop_new = np.random.choice(pop, size=self.pop_size, replace=True, p=fitness/np.sum(fitness))
return pop_new
def crossover(self,pop_bin):
"""
input : pop_bin 二进制表示的一组解
output : pop_newgens_bin 染色体交叉后的解
"""
pop_newgens_bin=[]
for i in range(len(pop_bin)):
if np.random.rand() < self.cross_rate :
obj = pop_bin[np.random.randint(0,self.pop_size)] #随机选一个交叉对象,也可以选到自己
cross_point = random.randint(0,self.DNA_size) #在DNA片段中随机选一个交叉点
pop_newgens_bin.append(pop_bin[i][0:cross_point] + obj[cross_point::]) #从交叉点往后交换基因片段
else:
pop_newgens_bin.append(pop_bin[i])
return pop_newgens_bin
def mutate(self,pop_bin):
"""
在
input : pop_bin 二进制表示的一组解
output : pop_newgens_bin 染色体变异后的解
"""
pop_newgens_bin=[]
for i in range(len(pop_bin)):
if np.random.rand() < self.mutation_rate :
mutation_pos = np.random.randint(0,self.DNA_size) # 随机选取某一个基因位置发生变异
temp = list(pop_bin[i])
temp[mutation_pos] = str(1-int(temp[mutation_pos]))
pop_newgens_bin.append(''.join(temp))
else:
pop_newgens_bin.append(pop_bin[i])
return pop_newgens_bin
def evolve(self,epsilon=10**-6):
fitness=[]
while self.age<self.generation_max:
fitness.append(np.mean(self.evaluate(self.cur_gen)))
new_gens_temp = self.select(self.cur_gen)
cur_gen_bin = self.encode(new_gens_temp)
cur_gen_bin = self.crossover(cur_gen_bin)
cur_gen_bin = self.mutate(cur_gen_bin)
new_gens = self.decode(cur_gen_bin)
self.cur_gen = new_gens
self.age+=1
arr_var = np.var(np.array(self.cur_gen)/10**self.n)
if arr_var < epsilon:
break
self.plot_func(fitness)
return np.mean(np.array(self.cur_gen)/10**self.n)
def plot_func(self,fitness):
num=np.linspace(1,len(fitness),len(fitness))
plt.plot(num,fitness)
if __name__ == "__main__":
pop = GA(pop_size=100,gens_max=500,interval=[0,10],cross_rate=0.8,mutation_rate=0.05)
x = pop.evolve(epsilon=10**-6)
plt.show()
print('迭代次数 :', pop.age)
print('函数极小值:',[x,4*x*np.sin(10*x)-5*x*np.cos(3*x)])
(1)encode编码: 这里采用的是二进制编码,二进制编码的字符串长度与问题所求解的精度有关。需要保证所求解空间内的每一个个体都可以被编码。
优点:编、解码操作简单,遗传、交叉便于实现
缺点:长度大,搜索的解容易超出可行域
其他编码方法:格雷码、浮点数编码、符号编码、多参数编码等
(2)evaluate fitness 适应度评估: 遗传算法中,一个个体(解)的好坏用适应度函数值来评价,在本问题中,f(x)就是适应度函数,用函数值来表示适应度(fitness)。当然,适应度值越大,解的质量越高。适应度函数是遗传算法进化的驱动力,也是进行自然选择的唯一标准,它的设计应结合求解问题本身的要求而定。
注意:在轮盘赌计算选择概率时,需要保证同号。故这里在函数的返回值中增加 y = e − f ( x ) y = {e^{ - f(x)}} y=e−f(x)将函数值映射到 y = e − x y = {e^{ - x}} y=e−x上。当然可以选择不同的映射函数。
(3)select 选择算子: 通过选择算子,模拟优胜劣汰,适应度越高越容易被选中,即更容易繁衍。常见选择算法:轮盘赌 : f i ∑ f i \frac{{{f_i}}}{{\sum {{f_i}} }} ∑fifi, f i {{f_i}} fi表示第i个个体的适应度, ∑ f i {\sum {{f_i}} } ∑fi表示所以个体适应度总和。可以加上精英保护机制(前几名报送),加快收敛速度。
注意:这里用适应度计算概率需保证同号,故取指数保证非负。
(4)Cross_over交叉运算: 将选中的优秀个体进行两两随机交配,基因片段随机交换,不同的交换方式略有不同。这里随机选取基因片段进行整体交换。
常用的交叉方式:
(5)mutate 基因变异: 个体染色体编码串中的某些基因座上的基因值用该基因座的其他等位基因来替换,从而形成一个新的个体。就遗传算法运算过程中产生新个体的能力方面来说,交叉运算是产生新个体的主要方法,它决定了遗传算法的全局搜索能力;而变异运算只是产生新个体的辅助方法,但也是必不可少的一个运算步骤,它决定了遗传算法的局部搜索能力。交叉算子与变异算子的共同配合完成了其对搜索空间的全局搜索和局部搜索,从而使遗传算法能以良好的搜索性能完成最优化问题的寻优过程。