一:科学定义
遗传算法是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的一种计算模型,是一种通过模拟自然进化过程搜索最优解的方法。
二:白话遗传算法
如何进行遗传算法呢?简单来说,一开始我们先随机生成一组可行解(种群),其中的每一个解(个体)都是由特定的染色体构成的,这里我们假设染色体只有0、1两种基因。通常需要采用一定的编码规则对染色体进行编码(也就是生物学中的基因型和表现性),常见的编码形式是二进制编码。定义适应性得分规则(比如:解的效果越好,适应性得分越高),应用适应性得分规则对种群中的每个个体进行打分,根据自然选择和优胜劣汰,得分较低的个体就会被淘汰。得分较高的个体就会生存下来,并且产生后代的概率就会很大。接着我们讨论变异:变异包括基因重组和基因突变两种方式。对于基因重组来说,我们在种群中选择一个父本和一个母本,让他们的染色体随机实现重组。对于基因突变来说,在种群中选择个体,随机让其染色体上某处基因进行突变。通过这两种方式,我们可以得到一个新的种群。然后我们继续开始新一轮的计算,迭代一定次数或达到某个终止条件即可退出循环。
如果还不是很理解的话,可以看看下面这个链接。这篇文章生动形象的用袋鼠跳例子讲解了遗传算法:
袋鼠跳讲解遗传算法
三:相关概念
- 个体(染色体):一个染色体代表具体问题的一个解,一个染色体上有许多基因。
- 基因:一个基因代表具体问题的解的一个决策变量,一个染色体上的所有基因就构成了这个问题解的所有决策变量。
- 种群:许多个体(染色体)构成种群。
四:算法流程
4.1:算法流程图
4.2:算法基本步骤
- 种群初始化。我们采用随机的方式生成一个种群,一般大小为100~500。在初始化的时候,要考虑怎样对染色体进行编码(比如:二进制表示基因型,十进制表示表现性)
- 个体适应性得分计算。在进行此步骤之前,一定要考虑清楚如何制定适应性得分评估规则,常见的方式是直接使用目标函数值作为个体适应性得分。
- 选择。根据适应性得分将适应性得分高的个体选择出来。这里通常使用轮盘赌的方式。其基本思想为: 个体被选中的概率与其适应性得分成正比。步骤如下:
(1)首先计算出所有个体的适应度总和。
(2)计算出每个个体的相对适应度大小
(3)随机生成一个0~1的随机数,根据随机数出现在上述的某个概率区域,选择相对应的个体。
如果对轮盘赌方法还不理解的话,不妨看看下面这篇博客:
轮盘赌算法及其实现
- 交叉。此步骤也就是所说的基因重组,需要设定一个阈值(pc:一般为0.4~0.99)来判断是否进行交叉。具体步骤如下:
(1)随机生成一个0~1的数,判断是否进行交叉。
(2)选择父本以及母本,随机生成交叉位点
(3)将父本和母本根据交叉位点切割,然后拼接重组。 - 变异。同样需要设置阈值(pm:一般比较小,因为基因突变的概率比较小)。步骤如下:
(1)随机生成变异位点
(2)将变异位点的值取反。 - 重复2~5步操作,直到迭代结束或完成达到某个终止条件。
五:案例及代码实现
5.1:题目描述
5.2:题目分析
我们首先来画一下这个函数的图像
对这个函数求最值一般的方法肯定是行不通了,简直是太复杂了。别急,这个时候就要派出我们的杀手锏:遗传算法。奇怪,这个问题怎么用遗传算法求解呢?我当时也是一头雾水。我们可以按如下思路进行分析:
首先我们要确定染色体的编码方法,我们前面提到可以用二进制编码,这个题目就是如此的。我们用二进制对染色体进行编码(比如:00101011就代表一条染色体),解码只需要将二进制转化为对应的十进制即可。但是现在问题来了,我们将二进制转化为十进制后是一个整数,如何将其对应到实数横坐标[1,10]中呢?这个过程通常按如下两个步骤进行:
(1)将二进制数转化为对应的十进制
(2)对应区间内的实数,这个步骤使用下面这个公式,其中min表示区间的最小值,max表示区间的最大值,length表示染色体的长度
对这部分转化感兴趣的小伙伴可以看下面这个链接:
遗传算法中二进制转化到指定区间的实数值
下面我们来按照上面的步骤实现该问题的求解
5.3:各部分代码解析
初始化种群
此部分主要生成具有一定大小的种群,其中种群中每个个体染色体的大小也一定。
def init_population(self):
population = [[random.randint(0,1) for j in range(self.chromosome_length)] for i in range(self.population_size)]
return population
染色体的编解码方式
这部分代码将以二进制表示的染色体解码到对应区间的实数范围内,用于解码整个种群的染色体。
def decode_chromosome(self,population):
'''
:param population: 种群
:return: 解码染色体后的值(此例中即:x坐标)
'''
result = [] # 存储解码后染色体的值
for chromosome in population:
temp = 0
for index,coefficient in enumerate(chromosome):
temp += coefficient * (2 ** (self.chromosome_length -1 - index)) # 二进制转化为十进制
result.append((temp * self.gene_max) / (2 ** self.chromosome_length - 1))
return result
下面这部分代码与上面的功能相同,都是解码染色体,只不过它仅解码种群中某一个个体的染色体。
def binary_2_to_10(self,binary):
'''
:param binary: 一条染色体,数组形式
:return: 解码结果
'''
result = 0
for i in range(self.chromosome_length):
result += binary[i] * (2 ** (self.chromosome_length - 1 - i)) # 二进制转化为十进制
result = (result * self.gene_max) / (2 ** self.chromosome_length - 1)
return result
计算种群中每个个体的适应性得分
通过将染色体解码后对应的实数值,计算目标函数的值,进而得到适应性得分。
def adtapt_score(self,population,low_limit=10):
'''
:param population: 种群
:param low_limit: 淘汰的下限,一般设置为0,在此设置为10,加快收敛。注意不要设置过高,否则会影响全局最优搜索
:return: 得分数组
'''
survival_score = []
value = self.decode_chromosome(population)
for x in value:
score = 10 * math.sin(5 * x) + 7 * math.cos(4 * x)
if score > low_limit: # 大于淘汰上限,不淘汰
survival_score.append(score)
else: # 淘汰
survival_score.append(0)
return survival_score
选择(复制)
这部分就是根据适应性得分计算出当前种群中的个体产生下一代的概率,一般来说适应性得分越高,产生下一代的概率就越大。所以在这里我们使用轮盘赌的方法,计算出个体产生下一代的概率,进而生成随机数,看其落在哪个概率区间内,就是相对应的个体产生的子代。
计算累计概率
def cumsum_chance(self,survival_score):
'''
:param survival_score: 得分数组
:return: 累计概率数组
'''
total = sum(survival_score)
chance = [] # 存放每个个体的选择概率
for sco in survival_score:
chance.append(sco / total) # 归一化
cum_chance = [] # 存放累计概率
for i in range(len(chance)):
if i == 0:
cum_chance.append(chance[0])
else:
cum_chance.append(chance[i] + cum_chance[i-1])
return cum_chance
轮盘赌
def roulette_selection(self,survival_score,population):
'''
:param survival_score: 得分数组
:param population: 种群
:return: 选择之后的新种群
'''
cum_chance = self.cumsum_chance(survival_score)
p = [random.random() for i in range(len(population))] # 随机生成概率
'''
注意:不能new_pop = population,这样在改变new_pop的时候,population也会改变
采用new_pop = population[:]则不会发生上述情况
'''
new_pop = population[:]
for i in range(len(p)):
for j in range(len(cum_chance)):
if cum_chance[j] > p[i]:
new_pop[i] = population[j]
break
population = new_pop[:] # 返回新种群
杂交(基因重组)
这里的杂交我们仅考虑相邻两个体之间的杂交。
def crossover(self,population):
'''
:param population: 种群
:return: 杂交之后的新种群
'''
population_length = len(population)
for i in range(population_length - 1):
if random.random() < self.pc:
temp_1 = []
temp_2 = []
cross_point = random.randint(0,self.chromosome_length) # 随机生成拼接点
# 染色体拼接
temp_1.extend(population[i][0:cross_point])
temp_1.extend(population[i + 1][cross_point:self.chromosome_length])
temp_2.extend(population[i + 1][0:cross_point])
temp_2.extend(population[i][cross_point:self.chromosome_length])
population[i] = temp_1[:]
population[i + 1] = temp_2[:]
变异(基因突变)
def mutation(self,population):
'''
:param population: 种群
:return: 突变之后的新种群
'''
population_length = len(population)
for i in range(population_length):
if random.random() < self.pm:
muta_point = random.randint(0,self.chromosome_length - 1)
if population[i][muta_point] == 0:
population[i][muta_point] = 1 # 基因突变
else:
population[i][muta_point] = 0
迭代曲线
整个程序运行完之后,我们不妨绘制迭代曲线来看看最终的求解情况。
def plot_itera(self,itera,result):
'''
:param itera: 迭代次数
:param result: 每一次迭代的最优个体集合
:return:
'''
x = [i for i in range(itera)]
y = [result[i][1] for i in range(itera)]
plt.plot(x,y)
plt.show()
5.4:完整代码示例
import random
import math
import matplotlib.pyplot as plt
class GA:
def __init__(self,population_size,chromosome_length,gene_max,pc,pm):
'''
:param population_size: 种群大小
:param chromosome_length: 染色体长度
:param gene_max: 基因中允许出现的最大值(此例中指x允许的最大值)
:param pc:杂交的概率
:param pm:变异的概率
'''
self.population_size = population_size
self.chromosome_length = chromosome_length
self.gene_max = gene_max
self.pc = pc
self.pm = pm
# 绘制目标函数
def plot_function(self):
x = [i/float(10) for i in range(0,100,1)]
y = [10 * math.sin(5 * x) + 7 * math.cos(4 * x) for x in x]
plt.plot(x,y)
plt.show()
# 画当前种群个体落点情况
def plot_point(self,X,Y):
x = [i / float(10) for i in range(0, 100, 1)]
y = [10 * math.sin(5 * x) + 7 * math.cos(4 * x) for x in x]
plt.plot(x,y)
plt.scatter(X,Y,c='r',s=5)
plt.show()
# 画最终的迭代曲线
def plot_itera(self,itera,result):
'''
:param itera: 迭代次数
:param result: 每一次迭代的最优个体集合
:return:
'''
x = [i for i in range(itera)]
y = [result[i][1] for i in range(itera)]
plt.plot(x,y)
plt.show()
# 初始化种群
def init_population(self):
population = [[random.randint(0,1) for j in range(self.chromosome_length)] for i in range(self.population_size)]
return population
# 解码染色体(其实就是将对应的二进制转化为十进制)
def decode_chromosome(self,population):
'''
:param population: 种群
:return: 解码染色体后的值(此例中即:x坐标)
'''
result = [] # 存储解码后染色体的值
for chromosome in population:
temp = 0
for index,coefficient in enumerate(chromosome):
temp += coefficient * (2 ** (self.chromosome_length -1 - index)) # 二进制转化为十进制
result.append((temp * self.gene_max) / (2 ** self.chromosome_length - 1))
return result
# 将二进制转化为十进制
def binary_2_to_10(self,binary):
'''
:param binary: 一条染色体,数组形式
:return: 解码结果
'''
result = 0
for i in range(self.chromosome_length):
result += binary[i] * (2 ** (self.chromosome_length - 1 - i)) # 二进制转化为十进制
result = (result * self.gene_max) / (2 ** self.chromosome_length - 1)
return result
# 计算每条染色体的适应性分数并且淘汰得分较低的个体
def adtapt_score(self,population,low_limit=10):
'''
:param population: 种群
:param low_limit: 淘汰的下限,一般设置为0,在此设置为10,加快收敛。注意不要设置过高,否则会影响全局最优搜索
:return: 得分数组
'''
survival_score = []
value = self.decode_chromosome(population)
for x in value:
score = 10 * math.sin(5 * x) + 7 * math.cos(4 * x)
if score > low_limit: # 大于淘汰上限,不淘汰
survival_score.append(score)
else: # 淘汰
survival_score.append(0)
return survival_score
# 寻找当前种群中最优个体
def find_best(self,population,survival_score):
'''
:param population: 种群
:param survival_score: 得分数组
:return: 最优个体的染色体,最高适应性分数
'''
max_score = survival_score[0] # 种群最优个体的适应性分数
best_chromosome = population[0] # 种群最优个体的染色体组成
for i in range(1,len(survival_score)):
if survival_score[i] > max_score:
max_score = survival_score[i]
best_chromosome = population[i]
return best_chromosome,max_score
# 计算累计概率
def cumsum_chance(self,survival_score):
'''
:param survival_score: 得分数组
:return: 累计概率数组
'''
total = sum(survival_score)
chance = [] # 存放每个个体的选择概率
for sco in survival_score:
chance.append(sco / total) # 归一化
cum_chance = [] # 存放累计概率
for i in range(len(chance)):
if i == 0:
cum_chance.append(chance[0])
else:
cum_chance.append(chance[i] + cum_chance[i-1])
return cum_chance
# 轮盘赌选择
def roulette_selection(self,survival_score,population):
'''
:param survival_score: 得分数组
:param population: 种群
:return: 选择之后的新种群
'''
cum_chance = self.cumsum_chance(survival_score)
p = [random.random() for i in range(len(population))] # 随机生成概率
'''
注意:不能new_pop = population,这样在改变new_pop的时候,population也会改变
采用new_pop = population[:]则不会发生上述情况
'''
new_pop = population[:]
for i in range(len(p)):
for j in range(len(cum_chance)):
if cum_chance[j] > p[i]:
new_pop[i] = population[j]
break
population = new_pop[:] # 返回新种群
# 杂交
def crossover(self,population):
'''
:param population: 种群
:return: 杂交之后的新种群
'''
population_length = len(population)
for i in range(population_length - 1):
if random.random() < self.pc:
temp_1 = []
temp_2 = []
cross_point = random.randint(0,self.chromosome_length) # 随机生成拼接点
# 染色体拼接
temp_1.extend(population[i][0:cross_point])
temp_1.extend(population[i + 1][cross_point:self.chromosome_length])
temp_2.extend(population[i + 1][0:cross_point])
temp_2.extend(population[i][cross_point:self.chromosome_length])
population[i] = temp_1[:]
population[i + 1] = temp_2[:]
# 突变
def mutation(self,population):
'''
:param population: 种群
:return: 突变之后的新种群
'''
population_length = len(population)
for i in range(population_length):
if random.random() < self.pm:
muta_point = random.randint(0,self.chromosome_length - 1)
if population[i][muta_point] == 0:
population[i][muta_point] = 1 # 基因突变
else:
population[i][muta_point] = 0
def main(self):
self.plot_function() # 首先绘制目标函数
population = self.init_population() # 初始化种群
itera = 500 # 迭代次数
result = [] # 每一次迭代的最优个体集合
for i in range(itera):
survival_score = self.adtapt_score(population) # 计算每条染色体的适应性分数
best_chrom,best_score = self.find_best(population,survival_score) # 选择最优个体
# 存放每次迭代的最优x值和最优y值
result.append([self.binary_2_to_10(best_chrom),best_score])
if i > 490:
self.plot_point(self.decode_chromosome(population),survival_score)
self.roulette_selection(survival_score,population)
self.crossover(population)
self.mutation(population)
# 画最终的迭代曲线
self.plot_itera(itera,result)
if __name__ == '__main__':
population_size = 500
chromosome_length = 10
gene_max = 10
pc = 0.6
pm = 0.01
ga = GA(population_size,chromosome_length,gene_max,pc,pm)
ga.main()
运行结果
迭代一定次数后生存下来的个体(即:我们前面设置的目标函数值>10的个体)
迭代曲线
由上图我们可以发现,迭代一定次数之后,目标函数最优值逐渐收敛在17附近,所以我们就可以认为17是该函数在区间[0,10]的一个最优解了。另外由上图我们还发现,不管迭代多少次,迭代曲线永远在波动,不会完全收敛,这也说明:遗传算法的解决某些问题的时候,虽然有可能没有办法完全收敛,但仍可以给出一个相当不错的解(非常非常非常靠近最优解)。
六:总结
相信经过上面的案例,大家对遗传算法已经有了初步的认识了吧。其实遗传算法本身并不难,难就难在如何将一个问题合理转化为可以用遗传算法解决的问题。所以,大家在平时可以多思考,哪些问题可以用遗传算法解决,如何转化、如何解决等等。关于遗传算法可以在哪些方面应用,我大概总结了一下,有兴趣的朋友不妨实现一下,这会使你对其有更加深刻的认识。