遗传算法之扇贝的进化
目录
1. 背景故事
扇贝的进化讲述了一个有趣的故事:
从前在海岸边有一群扇贝在生活繁衍。它们自然是衣食不愁,连房子也有了着落。它们担忧的只有一件事:每隔一段时间,总有一个人来挖走它们之中的一部分。但扇贝们不知道的是,这人的家族图腾是Firefox的图标,所以他总是选择那些贝壳花纹长得比较不像Firefox图标的扇贝。
这种状况持续了好几十万代。大家应该也猜到扇贝们身上发生什么事情了:它们的贝壳上都印着很像Firefox图标的图案。
2. 进化论
达尔文进化论很好的解释了上述扇贝的问题:自然选择。自然选择讲究的是物竞天择、适者生存。在扇贝生存的环境中,只有更像Firefox图标的扇贝才能容易活下来。根据遗传学来讲,活下来的扇贝的后代继承了更像像Firefox图标的基因,在基因表达后更像Firefox。3. 遗传算法
基于自然选择和遗传定理,人们提出了一种新的算法--遗传算法。 遗传算法是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法。 其主要特点是直接对结构对象进行操作,不存在求导和函数连续性的限定;具有内在的隐并行性和更好的全局寻优能力;采用概率化的寻优方法,不需要确定的规则就能自动获取和指导优化的搜索空间,自适应地调整搜索方向。 遗传算法以一种群体中的所有个体为对象,并利用随机化技术指导对一个被编码的参数空间进行高效搜索。其中,选择、交叉和变异构成了遗传算法的遗传操作;参数编码、初始群体的设定、适应度函数的设计、遗传操作设计、控制参数设定五个要素组成了遗传算法的核心内容。4. 遗传算法过程
遗传算法的大致过程可以参考下图: ![在这里插入图片描述](https://img-blog.csdnimg.cn/47c44d0d0d2e4af596618bd159d23e1b.webp#pic_center)4.1 编码
编码的过程就是寻找一种对问题潜在解的数字化形式,即通过数字的形式定义问题的解。生物的性状是基因表的结果。
在扇贝的进化这一问题中,我们使用一个带颜色的三角形代表一个基因,多个三角形的融合代表生物的形状,即扇贝的外在图案。简单来说,我们通过n个三角形来拟合扇贝图案。编码就是使用数据结构来表示这n个三角形的形状和颜色。然后定义如何使用这n个三角形来拟合。
4.2 初始化种群
随机初始化一个种群,种群大小一般在20-200之间。这个种群作为第0代,种群中的不同个体有着不同的性状,和Firefox图标的相似度有大有小。
4.3 个体适应度
在生物上个体适应度指的是生物个体对环境的适应性度量。在扇贝的进化中指的是评估扇贝和理想中的个体(Firefox图标)的相似性。在评估相似性时,都是以基因表达后的扇贝个体进行比较,即通过比较扇贝图片和Firefox图标的相似性,相似性越高,则适应度越大。
4.4 选择
根据适应度函数来比较所有个体的适应性,通过一定的选择规则保留最优的个体,淘汰其他适应性低的个体。在扇贝的进化问题中,保留和Firefox图标最为相像的m个个体,淘汰其他的个体。
4.5 交叉
对保留的个体进行交配产生下一代,来保持种群的大小。通常通过交叉基因的方式来得到下一代。交叉方式分为以下三种:
- 单点交叉:交换两个父体某一位置p的基因;
- 多点交叉:交换两个父体位置(p, q)之间的基因;
- 均匀交叉:两个父体各有一半基因组成新的个体基因。
4.6 变异
对于个体的基因,按照一定的概率进行随机改变。这个概率一般在0.001到0.1之间。变异的目的是为了通过随机改变来产生更接近理想目标的个体。
4.7 演化
对于初始种群,按照适应度淘汰劣势个体,父代交叉染色体后得到子代,子代经过变异后加入到种群中去,保持种群数量的稳定。经过几十万代的演化,种群中的个体会向目标个体靠近。
5. 代码实现
5.1 开发环境
- 编程语言:Python
- 第三方库:pillow8.2.0, numpy1.20.1
5.2 数据结构和函数实现
5.2.1 基因表示
定义一个单独的类Gene表示基因,该基因用三角形表示,其中三角形的位置和大小使用三个顶点表示,然后使用颜色进行填充,颜色使用(R,G,B,a),其中a表示透明度,透明度在100左右三角形融合效果更好。
class Gene():
'''
使用三角形的位置和颜色表示基因
A,B,C三个顶点的坐标以及颜色的RGBA值
width和height表示目标图片的宽和高
'''
def __init__(self, width, height):
self.A = (randint(0, width), randint(0, height))
self.B = (randint(0, width), randint(0, height))
self.C = (randint(0, width), randint(0, height))
self.color = (randint(0, 255), randint(0, 255), randint(0, 255), randint(90, 110))
5.2.2 遗传算法GA
定义一个遗传算法GA,分别使用函数来模拟生物遗传的染色体、个体、适应性函数、交叉、变异和迭代,其具体内容如下:
定义了种群数量、个体的基因数量、基因变异概率、繁衍代数以及要观察的最优个体的代。
class GA():
def __init__(self, ideal_path, generation_save, save_path, population_size = 20, gene_number = 100, generation_number = 100000, mutate_rate = 0.05):
# 目标图片的RGBA
self.ideal_individual = Image.open(ideal_path).convert('RGBA')
# 目标图片的宽
self.width = self.ideal_individual.width
# 目标图片的高
self.height = self.ideal_individual.height
# 要保存最优个体的代的集合
self.generation_save = generation_save
# 保存的位置
self.save_path = save_path
# 种群数量大小
self.population_size = population_size
# 基因数量
self.gene_number = gene_number
# 基因变异概率
self.mutate_rate = mutate_rate
# 繁衍的代数
self.generation_number = generation_number
5.2.3 生成个体的所有基因
def get_genes(self):
'''
self.gene_number 表示染色体的基因数
'''
genes = []
for i in range(0, self.gene_number):
genes.append(Gene(self.width, self.height))
return genes
5.2.4 生成个体
在生成个体时,利用pillow第三方库的绘图工具ImageDraw.Draw进行绘制。分别绘制m个三角形,然后利用RGBA图像的RGB值和透明度A,进行图像的混合,这里使用alpha_composite函数对两张RGBA图片进行混合,最终得到个体。
def get_individual(self, genes):
'''
生成个体
'''
individual = Image.new("RGBA", size=(self.width, self.height))
individual_draw = ImageDraw.Draw(individual)
# 初始化画布,使用纯白填充
individual_draw.polygon([(0, 0), (self.width, 0), (self.width, self.height), (0, self.height)],
fill = (255, 255, 255, 255))
# 对于每个三角形,先在空白画布上画下,再和之前的进行融合
for gene in genes:
gene_show = Image.new("RGBA", size=(self.width, self.height))
gene_draw = ImageDraw.Draw(gene_show)
gene_draw.polygon([gene.A, gene.B, gene.C],
fill = gene.color)
individual = Image.alpha_composite(individual, gene_show)
return individual
5.2.5 适应性函数
通过计算个体和理想个体的图片RGB值的欧式距离代表个体和理想个体的相似性。将个体和理想个体的RGB值转化为一个一维数组,然后使用numpy计算,这里算的是欧式距离的平方(没有进行开方运算)。
def calculate_adapt_grades(self, individual):
'''
计算适应性函数
通过计算图片的RGB值的欧式距离来比较相似性
'''
array1 = np.array(self.ideal_individual, dtype=np.float32)[:,:,:-1]
array2 = np.array(individual, dtype=np.float32)[:,:,:-1]
return np.sum(np.square(np.subtract(array1, array2)))
5.2.6 基因变异
以一定概率对基因进行随机变异,变异概率通常在0.001-0.1之间。
def mutate(self, gene):
'''
对gene按照变异概率mutate_rate进行变异,得到变异mutate_gene
'''
if random() < self.mutate_rate:
mutate_gene = Gene(self.width, self.height)
return mutate_gene
return gene
5.2.7 染色体交叉
对于两个父体,使用多点交叉,得到子代的染色体。这里随机两个数值,交换这两个位置之间的基因。
def cross(self, parent1, parent2):
'''
对两个父体进行多点交叉
'''
child = []
random_a = randint(0, self.gene_number)
random_b = randint(0, self.gene_number)
start = min(random_a, random_b)
end = max(random_a, random_b)
for i in range(0, 100):
if i > start and i < end:
child.append(parent2[i])
else:
child.append(parent1[i])
return child
5.2.8 迭代演化
def generate(self):
# 1. 初始化种群
parent_genes = []
for i in range(self.population_size):
parent_genes.append(self.get_genes())
# 2. 计算适应度,挑选最优的两个作为父代,然后淘汰其他的个体
adapt_grades = []
for i in range(self.population_size):
adapt_grades.append(self.calculate_adapt_grades
(self.get_individual(parent_genes[i])))
adapt_grades_sorted = sorted(adapt_grades)
parent = []
parent.append(parent_genes[adapt_grades.index(adapt_grades_sorted[0])])
parent.append(parent_genes[adapt_grades.index(adapt_grades_sorted[1])])
for generation in range(1, self.generation_number + 1):
child_genes = []
child_genes.append(parent[0])
child_genes.append(parent[1])
# 3. 父代进行交叉得到子代
for i in range(self.population_size - 2):
child_genes.append(self.cross(parent[0], parent[1]))
# 4. 子代进行变异后加入到种群中
for i in range(2, self.population_size):
for j in range(self.gene_number):
child_genes[i][j] = self.mutate(child_genes[i][j])
adapt_grades = []
for i in range(self.population_size): adapt_grades.append(self.calculate_adapt_grades(self.get_individual(child_ genes[i])))
adapt_grades_sorted = sorted(adapt_grades)
parent[0] = child_genes[adapt_grades.index(adapt_grades_sorted[0])]
parent[1] = child_genes[adapt_grades.index(adapt_grades_sorted[1])]
# 每100代打印代数
if generation % 100 == 0:
print("%d", generation)
# 对于generateion_save代打印其适应度,并将结果图片保存
if generation in self.generation_save:
print('第%d代的adapt_grades:\t%d' %(generation,
self.calculate_adapt_grades(self.get_individual(parent[0]))))
save_img = self.get_individual(parent[0])
save_img.save(os.path.join(self.save_path, '%d.png' % (generation)))
5.3 结果展示
-
理想个体(Firefox图标)
-
演化结果(下标表示代数)
完整代码
from PIL import Image, ImageDraw
from random import randint, random
import numpy as np
import os
class Gene():
'''
使用三角形的位置和颜色表示基因
A,B,C三个顶点的坐标以及颜色的RGBA值
width和height表示目标图片的宽和高
'''
def __init__(self, width, height):
self.A = (randint(0, width), randint(0, height))
self.B = (randint(0, width), randint(0, height))
self.C = (randint(0, width), randint(0, height))
self.color = (randint(0, 255), randint(0, 255), randint(0, 255), randint(90, 110))
class GA():
def __init__(self, ideal_path, generation_save, save_path, population_size = 20,
gene_number = 100, generation_number = 100000, mutate_rate = 0.05):
self.ideal_individual = Image.open(ideal_path) # 目标图片的RGBA
self.width = self.ideal_individual.width # 目标图片的宽
self.height = self.ideal_individual.height # 目标图片的高
self.generation_save = generation_save # 要保存最优个体的代的集合
self.save_path = save_path # 保存的位置
self.population_size = population_size # 种群数量大小
self.gene_number = gene_number # 染色体的基因数量
self.mutate_rate = mutate_rate # 基因变异概率
self.generation_number = generation_number # 繁衍的代数
def get_genes(self):
'''
生成染色体
'''
genes = []
for i in range(0, self.gene_number):
genes.append(Gene(self.width, self.height))
return genes
def get_individual(self, genes):
'''
生成个体
'''
individual = Image.new("RGBA", size=(self.width, self.height))
individual_draw = ImageDraw.Draw(individual)
individual_draw.polygon([(0, 0), (self.width, 0), (self.width, self.height), (0, self.height)],
fill = (255, 255, 255, 255))
for gene in genes:
gene_show = Image.new("RGBA", size=(self.width, self.height))
gene_draw = ImageDraw.Draw(gene_show)
gene_draw.polygon([gene.A, gene.B, gene.C],
fill = gene.color)
individual = Image.alpha_composite(individual, gene_show)
return individual
def mutate(self, gene):
'''
对gene按照变异概率mutate_rate进行变异,得到变异mutate_gene
'''
if random() < self.mutate_rate:
mutate_gene = Gene(self.width, self.height)
return mutate_gene
return gene
def calculate_adapt_grades(self, individual):
array1 = np.array(self.ideal_individual, dtype=np.float32)[:,:,:-1]
array2 = np.array(individual, dtype=np.float32)[:,:,:-1]
return np.sum(np.square(np.subtract(array1, array2)))
def cross(self, parent1, parent2):
'''
对两个父体进行多点交叉
'''
child = []
random_a = randint(0, self.gene_number)
random_b = randint(0, self.gene_number)
start = min(random_a, random_b)
end = max(random_a, random_b)
for i in range(0, self.gene_number):
if i > start and i < end:
child.append(parent2[i])
else:
child.append(parent1[i])
return child
def generate(self):
parent_genes = []
for i in range(self.population_size):
parent_genes.append(self.get_genes())
adapt_grades = []
for i in range(self.population_size):
adapt_grades.append(self.calculate_adapt_grades(self.get_individual(parent_genes[i])))
adapt_grades_sorted = sorted(adapt_grades)
parent = []
parent.append(parent_genes[adapt_grades.index(adapt_grades_sorted[0])])
parent.append(parent_genes[adapt_grades.index(adapt_grades_sorted[1])])
for generation in range(1, self.generation_number + 1):
child_genes = []
child_genes.append(parent[0])
child_genes.append(parent[1])
for i in range(self.population_size - 2):
child_genes.append(self.cross(parent[0], parent[1]))
for i in range(2, self.population_size):
for j in range(self.gene_number):
child_genes[i][j] = self.mutate(child_genes[i][j])
adapt_grades = []
for i in range(20):
adapt_grades.append(self.calculate_adapt_grades(self.get_individual(child_genes[i])))
adapt_grades_sorted = sorted(adapt_grades)
parent[0] = child_genes[adapt_grades.index(adapt_grades_sorted[0])]
parent[1] = child_genes[adapt_grades.index(adapt_grades_sorted[1])]
if generation % 100 == 0:
print('第',generation, '代了')
if generation in self.generation_save:
print('第%d代的adapt_grades:\t%d' %(generation, self.calculate_adapt_grades(self.get_individual(parent[0]))))
save_img = self.get_individual(parent[0])
save_img.save(os.path.join(self.save_path, '%d.png' % (generation)))
if __name__ == '__main__': # 运行主函数
ideal_path = 'E:/test/qq.png'
generation_save = [1, 50, 100, 200, 400, 1000, 2000, 4000, 10000, 20000, 50000, 100000]
save_path = 'E:/test'
ga = GA(ideal_path, generation_save, save_path)
ga.generate()