遗传算法(Genetic Algorithm,GA)是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法。
遗传算法是从代表问题可能潜在的解集的一个种群(population)开始的,而一个种群则由经过基因(gene)编码的一定数目的个体组成。每个个体实际上是染色体(chromosome)带有特征的实体。染色体作为遗传物质的主要载体,即多个基因的集合,其内部表现(即基因型)是某种基因组合,它决定了个体的形状的外部表现,如黑头发的特征是由染色体中控制这一特征的某种基因组合决定的。因此,在一开始需要实现从表现型到基因型的映射即编码工作。由于仿照基因编码的工作很复杂,我们往往进行简化,如二进制编码,初代种群产生之后,按照适者生存和优胜劣汰的原理,逐代(generation)演化产生出越来越好的近似解,在每一代,根据问题域中个体的适应度(fitness)大小选择(selection)个体,并借助于自然遗传学的遗传算子(genetic operators)进行组合交叉(crossover)和变异(mutation),产生出代表新的解集的种群。这个过程将导致种群像自然进化一样的后生代种群比前代更加适应于环境,末代种群中的最优个体经过解码(decoding),可以作为问题近似最优解。
下面通过求解与上一篇文章(点击可跳转)相同的一个问题来进一步说明算法介绍和实现过程:
采用顺序编码时,需要注意的是合法性问题,即编码是否符合实际,例如在TSP问题中,采用顺序编码在交叉变异时可能会出现重复走到一个城市的情形,这种情况即为不合法。针对这种情况在交叉方式上有相应的研究,在 2 2 2中有说明。
在本题中,采用二进制编码的方式。为了保证编码后对应的数值在可行域范围内,采取了编码后再进行一步线性变换的操作。
编码方式 | 适用问题 |
---|---|
二进制编码 | 背包问题、函数优化问题、指派问题 |
整数编码 | 时间优化问题 |
顺序编码 | 指派、旅行商(TSP)问题,调度问题 |
交叉变异的方式交叉和变异能够保证种群的多样性。交叉是指一对父代个体之间染色体进行交叉,交叉率通常设为0.8-0.9,用pc表示。对于二进制及整数编码,交叉方式有单切点交叉、双切点交叉、均匀交叉;对于顺序编码,有部分映射交叉、顺序交叉和循环交叉,这些方式可保证交叉的合法性。
变异是指染色体上的基因发生突变,在生物群体中发生概率较小,因此变异率设定在0.05一下,用pm表示。对于二进制及整数编码,变异方式为位变异;对于顺序编码,变异方式有插入、交换、翻转。
进行适应度的计算与标定。一般来说,适应度可以以所求目标函数值作为参考,对目标函数值域的某种映射变换称为适应度标定。适应度标定很重要,直接影响算法收敛速度以及是否可以找到最优解。下面介绍两种常用标定方法。
线性标定: F = a f + b F=af+b F=af+b, F F F为适值函数, f f f为目标函数。对于 m a x f ( x ) maxf(x) maxf(x),则 F = f − f m i n + ξ F=f-f_{min}+\xi F=f−fmin+ξ;对于 m i n f ( x ) minf(x) minf(x),则 F = − f + f m a x + ξ F=-f+f_{max}+\xi F=−f+fmax+ξ。
动态线性标定: F = a k f + b k F=a^kf+b^k F=akf+bk, k k k为迭代次数, F F F为适值函数, f f f为目标函数。对于 m a x f ( x ) maxf(x) maxf(x),则 F = f − f m i n k + ξ k F=f-f_{min}^k+\xi^k F=f−fmink+ξk;对于 m i n f ( x ) minf(x) minf(x),则 F = − f + f m a x k + ξ k F=-f+f_{max}^k+\xi^k F=−f+fmaxk+ξk。其中 ξ 0 = 0 \xi^0=0 ξ0=0, ξ k = ξ k − 1 ∗ r \xi^k=\xi^{k-1}*r ξk=ξk−1∗r, r ∈ [ 0.9 , 0.999 ] r\in[0.9,0.999] r∈[0.9,0.999]。关于 ξ k \xi^k ξk,可以认为 ξ k \xi^k ξk的加入使得使最坏个体仍有繁殖的可能,它的值随 k k k的增大而减小,更加符合自然界的规律。
轮盘赌和随机遍历抽样有相似的地方,这两种方法都需要将个体的适应度归一化到[0,1]区间内,在这里做一下区分:
轮盘赌,顾名思义是通过转盘,随机选取个体,即指针落到某一个个体对应的累积适应度区间内,即选择该个体。这可以通过生成[0,1)伪随机数,然后找到该伪随机数的对应区间,即可确定选择的个体。
随机遍历抽样是假定有 n n n个需要选择的个体,那么选择个体的指针就有 n n n个,指针之间的距离为 1 n \frac{1}{n} n1,第一个指针在[0, 1 n \frac{1}{n} n1]随机确定。本题采用了轮盘赌的选择策略。
在求解这个问题时发现,每一次的运行结果不同,但是发现有一次的结果比Matlab求解的结果更小,一度怀疑哪个地方出错了。在了解了Matlab求解方法后发现,其实Matlab的结果也并不是遍历可行域得出来的结果,而是以一定的步长求解,例如如果图像在某一点处有一个尖峰,但是Matlab遍历时并没有看到这一点,这就有可能会漏掉某些更小的解。所以Matlab的结果也不能百分百作为标准,只能当作一种参考。同时在求解时也应当有一定的“自信”:关于一个问题,如果目标函数和约束条件没有写错,利用GA,SA等优化算法,求出来的解肯定会在可行域内且在函数上,这些启发式算法并不会凭空产生一个不在函数上的解。
求解方法 | 最优点 | 函数值 |
---|---|---|
Matlab求解 | ( − 1.62 , 3.04 ) , ( − 1.62 , − 3.04 ) (-1.62,3.04),(-1.62,-3.04) (−1.62,3.04),(−1.62,−3.04) | 41.3205 |
模拟退火(SA) | ( − 1.5449 , − 3.1257 ) (-1.5449,-3.1257) (−1.5449,−3.1257) | 41.3472 |
遗传算法(GA) | ( − 1.569 , − 3.104 ) (-1.569,-3.104) (−1.569,−3.104) | 41.334 |
模拟退火中关键的地方有初温、末温、内层循环遍历次数以及温度变化控制参数,当然,那些产生新解的邻域操作也很重要。其中初温、末温、内层循环遍历次数、温度变化参控制数应该与要解答的问题的规模有关。要注意的是,\textbf{SA中温度并不用于产生新解,只是用于决定是否该接受较差的新解,而GA告诉我们的就是如何去产生新解。}而且GA的灵感都来自大自然,无论是交叉还是变异亦或是选择过程都充满了哲理,其实大部分现代优化算法的理念都是靠近自然。
python实现的GA代码如下:
import random
import math
import numpy as np
import matplotlib.pyplot as plt
population_size = 50 #种群大小
generation = 200 #代数
code_len = 10 #二进制编码长度
pc = 0.6 # 交配概率
pm = 0.01 # 变异概率
gene_population_1 = [] #种群的基因库
decode_value_1 = [] #解码后的变量
gene_population_2 = [] #种群的基因库
decode_value_2 = [] #解码后的变量
fitness = [] #函数值(原始适应值)
fitness_standard = [] #标定后适应值
select_possibility = []
sum_pob = []
random_selection = []
def encode(gene_population):
for i in range(population_size):
gene = []
for j in range(code_len):
gene.append(random.randint(0,1))
gene_population.append(gene)
#print(gene_population)
def decode(gene_population,decode_value):
decode_value.clear()
for i in range(population_size):
value = 0
for j in range(code_len):
value = value + gene_population[i][j] * (2 ** (code_len - 1 - j))
value = value * 10 / (2 ** (code_len) - 1) - 5
decode_value.append(value)
#适应值的处理可以再考虑一下
def fitness_process(decode_value_1,decode_value_2):
fitness.clear()
fitness_standard.clear()
sum_fitness = 0
for i in range(population_size):
#function = - (decode_value_1[i] - 1)**2- (decode_value_2[i] - 1)**2 + 1 #测试
function = -(6*decode_value_1[i]/(2 + decode_value_1[i] ** 2 + decode_value_2[i] ** 2) + 5*math.sin(decode_value_1[i]) +3*math.cos(decode_value_2[i]) + 50)
fitness.append(function)
fitness_standard.append(function)
#找最小值
min_fitness = fitness[0]
#min_id = 0
for k in range(population_size):
if (min_fitness > fitness[k]):
min_fitness = fitness[k]
#min_id = k
#适应值修正————线性标定
zeta = 2
#tao = 0.95
for m in range(population_size):
fitness_standard[m] = fitness[m] - min_fitness + zeta
#zeta = zeta ** (tao)
#找最大值
max_fitness = fitness_standard[0]
max_id = 0
for j in range(population_size):
sum_fitness = sum_fitness + fitness_standard[j]
if (max_fitness < fitness_standard[j]):
max_fitness = fitness_standard[j]
max_id = j
return (sum_fitness,max_id)
def roulette_select(sum_fitness,gene_population):
sum_pob.clear()
random_selection.clear()
for i in range (population_size):
possibility = fitness_standard[i]/sum_fitness
select_possibility.append(possibility)
pob = 0
for i in range(population_size):
pob = pob + select_possibility[i]
sum_pob.append(pob)
sum_pob[-1]=1
for i in range(population_size):
random_selection.append(random.random())
#random_selection.sort()
gene_population_new = []
#random_selection_id = 0
# for i in range(population_size):
# while(( random_selection_id < population_size) and (random_selection[random_selection_id] < sum_pob[i])):
# gene_population_new.append(gene_population[i])
# random_selection_id += 1
#轮盘赌
for i in range(population_size):
for j in range(population_size):
if((random_selection[i]>sum_pob[j])and(random_selection[i]<=sum_pob[j + 1])):
gene_population_new.append(gene_population[j + 1])
gene_population = gene_population_new
# 进行基因的变异
def mutation(gene_population):
for i in range(population_size):
if random.random() < pm:
mutation_point = random.randint(0, code_len - 1)
if gene_population[i][mutation_point] == 0:
gene_population[i][mutation_point] = 1
else:
gene_population[i][mutation_point] = 0
# 进行交配过程
def crossover(gene_population):
for i in range(0, population_size - 1, 2):
if random.random() < pc:
# 随机选择交叉点
change_point = random.randint(0, code_len - 1)
temp1 = []
temp2 = []
temp1.extend(gene_population[i][0: change_point])
temp1.extend(gene_population[i+1][change_point:])
temp2.extend(gene_population[i+1][0: change_point])
temp2.extend(gene_population[i][change_point:])
gene_population[i] = temp1
gene_population[i+1] = temp2
#开始进化
best_fitness_all = []
best_fitness_all.append(0)
encode(gene_population_1)
encode(gene_population_2)
for i in range(generation):
decode(gene_population_1,decode_value_1)
decode(gene_population_2,decode_value_2)
sum_fitness,max_id = fitness_process(decode_value_1,decode_value_2)
best_fitness_all.append(-fitness[max_id])
roulette_select(sum_fitness,gene_population_1)
roulette_select(sum_fitness,gene_population_2)
crossover(gene_population_1)
mutation(gene_population_1)
crossover(gene_population_2)
mutation(gene_population_2)
print('(x1,x2)为(%s,%s),最小值为%s'%(decode_value_1[max_id],decode_value_2[max_id],-fitness[max_id]))
plt.plot(range(1, len(best_fitness_all)+1), best_fitness_all)
plt.xlabel('最优解更新次数',fontproperties='SimHei')
plt.ylabel('每一代的最优解', fontproperties='SimHei')
plt.show()