遗传算法Python实战 001.hello world
前言
本系列文章,来自Clinton Sheppard的著作:Genetic Algorithms with Python
本书可可以在亚马逊官方网站上获取:
https://www.amazon.com/Genetic-Algorithms-Python-Clinton-Sheppard/dp/1540324001
文中的所有代码,可以在github上获取到源码:
https://github.com/handcraftsman/GeneticAlgorithmsWithPython
另外,系列文章在虾神的gitee上进行同步直播:
https://gitee.com/godxia/python_genetic_algorithm
关于虾神与本系列
看虾神的文章的同学,肯定已经习惯了虾神那无所不在的表情包,但是这个系列为什么没有一个表情包,甚至图片都很少呢?因为这是虾神首次试用markdown的方式写文章,所有的图片都要先上传到gitee,才能引用,比较麻烦,所以能少用图片就少用图片了。
另外,虾神的主要吃饭的活是GIS,但是作为一个计算机专业出身的老码农,算法一直是虾神的基础爱好,从以前的文章中,教大家手算插值就可以看出来,虾神最喜欢的做的事情就是把各种复杂的算法,掰扯碎了为止。而且正好遇上了这段时间需要用遗传算法去解决一部分参数优化的问题,所以干脆就整出了这一系列的文章。
为什么要写这个系列?
都已经有书了,而且都有源码了,为什么还多此一举的写这个系列呢?答案是本书目前还没有中文版,一部大部头的英文版著作,虽然说里面的英文都挺简单的(计算机英语就没有复杂的),但是说算法本来就是比较复杂的东西……哪怕是中文的算法书,能读到融会贯通的人也不多,更别说英文版的了,所以虾神准备发挥自己科普小能手的功力,先自己从英文版这个地雷阵里面滚过去,给大家开一条路,然后大家踩着这条小路过来就好了。
当然,原书中还有不少代码写得比较晦涩(作者用了大量Python特有的语法编写模式,阅读起来会比较痛苦),那么虾神也会用最简单的小白方式重写一部分代码,主要是方便阅读理解(当然这样一改,效率可能就会降低了)。
当然,鼓励有兴趣的同学去阅读原书,跑原书提供的代码。
关于遗传算法
本系列文章完全没有涉及任何算法的原理,没有说教,也没有流程图,更没有高大上的数学公式和推导,完全就是用实战的方式,通过可以直接运行和看到结果的十几个小项目,用实战的方式来解答遗传算法:
-
是这样用的
-
居然还可以这样用?
-
我去这样用也行?
所以,遗传算法的原理就大家可以直接自己去找,互联网上大把的多。而遗传算法的各种细节、核心、实现在本系列文中会逐步给大家揭晓。
关于实战
什么叫做实战?李云龙面对日本鬼子的时候,不会纠结对方是军事院校的高材生,而自己大字不识几个;也不会纠结对方是日本军界的名将之花,而自己只是大别山区十里八乡有名的篾匠……
实战就是用最快最有效的方式,击倒敌人,所以本系列文章中,出来就直接上代码,而且保证每一份代码都是可直接运行的——甚至不用去安装各种依赖包,用最基础的Python环境就可以直接运行所有的功能并且能够直观看见最终的结果。
练武的核心目的并不是修身养性,也不是强身健体——而是给你在走投无路的时候,一个暴起一击,血溅五步的机会。
所以,本系列文章讲究的就是两个字:实战,行不行,用代码说话。
本系列文章的约定
- 本系列文章以markdown方式编写,所以就不做各种复杂的排版了
- 代码以Jupyter notebook方式提供,使用Python 3.6 及以上版本运行。
- 里面如果有提供的数据,真实性和有效性均不做承诺,仅可用于学习和演示,不能用于论文写作、出版、研究等用途。
进入正题
猜数字小游戏
小时候我们都玩过一个游戏,就是一个人在1-10之间指定一个数字,然后另外一个人猜,看几次可以猜中。一般来说,怎么猜也不会超过10次……
游戏过程通常如下:
A: 1
B: 错
A:2
B:错
……
A只要从1到10,依次说一遍,肯定能找到……当然,你要是用上各种复杂的博弈论思维来思考的话,当我没说(最简单的博弈,就是选择从1开始递增,还是选择从10开始递减)……
但是如果游戏的规则,从1-10,拉伸到1-1000,甚至1-10000呢……好吧,就变成了一个无聊的折磨人的问题了。因为不管我们怎么猜测,结果就对或者错,没有办法从反馈中得到的信息来提高我们的猜测能力。
所以游戏稍微修改一下——比如A猜过之后,B会告诉他,你猜测的这个数字比他指定是数字是过大还是过小,比如这样:
A:150
B:太小了
A:300
B:太大了
……
那么这种玩法,自然就多了很多意思了……一方可以根据另外一方的反馈,动态调整自己的答案,以更快的接近答案。
而等我们学习了二分查找之后,发现,实际上会很快就能找到:
#在1-10000之间,生成一个随机数,然后用折半查找法来进行查询
import random
mi,mx = 1,10000
r = random.randint(mi,mx)
x,y = mi,mx
i = 0
while(True):
v = int((x+y)/2)
i+=1
print(i,v)
if v == r:
print("------------",r)
break
if v < r :
x,y = v,y
else:
x,y = x,v
这个代码执行1000次,发现不管生成的数字是哪个,最少5次,最多14次,就一定能够将其查找出来,执行分布如下:
为什么二分查找法会比依次迭代强上这么多呢?答案是我们会记忆上一次分析的结果,以对本次的分析进行参考和修正。
这就是遗传算法的一个基本原理:
可以利用以往分析的结果,来对目前的分析进行修正——只是遗传算法更加复杂,它通过对问题空间的随机探索和以进化过程为主要手段,模拟基因的遗传、杂交、(随机)变异,来对一些复杂问题进行求解,以提高我们解决问题的能力。
但是遗传算法相对于算法界那些各种惊才绝艳天才设计不同,它非常的傻——他没有智能,也不会学习,而且它会重复的试错——但是也恰恰因为他没有经验限制和边界,所以往往会通过尝试一些看似不可能的可能,从而得到一些更有启发性的结果。
遗传算法的核心:
遗传算法会进行知情猜测——即每一次尝试都需要给出明确的更好或者更坏的评价。
猜密码小游戏
我们把猜数字这个游戏,修改成一个更加复杂点的游戏,比如猜密码,我设定这一个密码,比如就叫做“Hello World!",这样一共是12个字符(10个字母加两个标点符号)组成,现在我给你问题空间,共计26个小写字母+26个大写字母+空格+感叹号+逗号+句号,一共是56个字符,要在里面找到我设定的密码。
而你每次猜测之后,我都会告诉你,你这次猜测对了几个字符(对的意思是字符和位置都得是正确的),但是不会告诉你对的是哪几个,如下:
A:h
B:0个字符
A:hel
B: 2个字符(el对了)
A:Heloo
B:4个字符(Hel o对了)
A:Hello World.(11字符)
……
括号里面的内容是不会告诉A的,只是为了告诉读者们这个游戏规范
可以看见,最高就是12,等于12就表示你答对了所有的密码。
那么你来玩这个游戏,大约需要多少次呢?
从理论上来看,是一个标准的排列组合问题,从56个字符里面,挑选12个进行组合可以重复的话,一共会有—10E22这么多种组合……
如果传统方式进行迭代查找的话,大家可以试试……
而下面我们来看看怎么通过遗传算法来进行求解:
遗传算法需要先定义我们的原始基因,比如我们把最终结果"Hello world!"这个字符串看成是我们的要进化的终极目的,那么组成这个目的的基础,也就是我们的基因就是26个小写字母+26个大写字母+4个标点符号:
target = "Hello World!"
geneset = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!.,"
接下去,定义第一个方法,这个方法用于以上基因中,随机抽取一些基因组合成一个个体。
def generate_parent(length):
genes = []
while len(genes) < length:
sampleSize = min(length - len(genes), len(geneSet))
genes.extend(random.sample(geneSet, sampleSize))
return ''.join(genes)
然后,定义一个验证函数,返回你生成的这个个体,有多少个基因与最终个体相同
def get_fitness(guess, target):
return sum(1 for expected, actual in zip(target, guess)
if expected == actual)
这是一个变异函数,模拟在进化的过程中,基因会发生突变。
主要逻辑是从基因库里面随机选择一个基因,然后在父本基因里面进行替换,生成一个新的字符串
def mutate(parent):
index = random.randrange(0, len(parent))
childGenes = list(parent)
newGene, alternate = random.sample(geneSet, 2)
childGenes[index] = alternate if newGene == childGenes[index] else newGene
return ''.join(childGenes)
打印出随机生成的字符串以及有多少个字符猜对了,以及所用的时间
import datetime
def display(guess):
timeDiff = datetime.datetime.now() - startTime
fitness = get_fitness(target,guess)
print("{0}\t{1}\t{2}".format(guess, fitness, str(timeDiff)))
第一次运行,先随机生成一个字符串,作为起始值
random.seed()
startTime = datetime.datetime.now()
bestParent = generate_parent(len(target))
bestFitness = get_fitness(target,bestParent)
display(bestParent)
开始迭代,迭代逻辑如下:
1、对父本字符串进行进化变异
2、对每次进化的字符串进行验证,保留验证结果更好的字符串进行进化
3、重复迭代,直到最后找到结果
while True:
child = mutate(bestParent)
childFitness = get_fitness(target,child)
if bestFitness >= childFitness:
continue
display(child)
if childFitness >= len(bestParent):
break
bestFitness = childFitness
bestParent = child
执行结果如下:(随机生成的,所以每次执行过程可能都不同)
YOcUCHXSejh 0 0:00:00
YOclCHXSejh 1 0:00:00
YeclCHXSejh 2 0:00:00.001023
YeclCHWSejh 3 0:00:00.001023
YeclC WSejh 4 0:00:00.001023
YeclC WSrjh 5 0:00:00.001993
YeclC WSrjh! 6 0:00:00.001993
YeclC WSrlh! 7 0:00:00.003029
Yeclo WSrlh! 8 0:00:00.003029
Yeclo Worlh! 9 0:00:00.003029
Heclo Worlh! 10 0:00:00.008974
Heclo World! 11 0:00:00.009972
Hello World! 12 0:00:00.025929
当然,你可以把密码修改得更复杂,比如虾神最喜欢的指环王电影第一部的开场诗:
The world is changed.
I feel it in the water.
I feel it in the earth.
I smell it in the air.
Much that once was.
is lost.
For none now live who remember it.
,U!OlRmdhfaqS YAcIbZwHykvKVtsBTWjinFrJgXM.ueoNGCQEzLxDPpsAVGwrKLT ZQPRiNHfqtelD!domJXS.bWxgahBcpUyznMCIFYjvkEO,ukbBXWRICoJpTAw,FzqPlegfMKdj!sQc NynaDhuUGrSi 1 0:00:00
,U!OlRmdhfaqS YAcIbZwHykvKVtsBTWjinFrJgXM.ueoNGCQEzLxDPpsA GwrKLT ZQPRiNHfqtelD!domJXS.bWxgahBcpUyznMCIFYjvkEO,ukbBXWRICoJpTAw,FzqPlegfMKdj!sQc NynaDhuUGrSi 2 0:00:00.001005
,U!OlRmdhfaqS YAcIbZwHykvKVtsBTWjinFrJgXM.ueoNGCQEzLxDPpsA GwrKLT ZQPRiNHfqtelD!domJXS.bWxgahBcpUyznMCIFYjvkEO,ukbBXWRICoJpTAw,FzqPlegfMKdj!sQc NynaDbuUGrSi 3 0:00:00.001994
,U!OlRmdhfaqS YAcIbZwHykvKVtsBTWjinFrJgXM.ueoNGCQEzLxDPpsA Gwr LT ZQPRiNHfqtelD!domJXS.bWxgahBcpUyznMCIFYjvkEO,ukbBXWRICoJpTAw,FzqPlegfMKdj!sQc NynaDbuUGrSi 4 0:00:00.002991
,U!OlRmdhfaqS YAcIbZwHykvKVtsBTWjinFrJgXM.ueoNGCQEzLxDPpsA Gwr LT ZQPRiNHfqtelD!domJXS.bWxgahBcpUyzhMCIFYjvkEO,ukbBXWRICoJpTAw,FzqPlegfMKdj!sQc NynaDbuUGrSi 5 0:00:00.003989
……
The world is changed. I feel it in the water. G feel it in Ghe earth. I smell it in the air. Much that once was. is lost. For none now live who rememberGiS. 152 0:00:00.801853
The world is changed. I feel it in the water. G feel it in the earth. I smell it in the air. Much that once was. is lost. For none now live who rememberGiS. 153 0:00:00.820839
The world is changed. I feel it in the water. G feel it in the earth. I smell it in the air. Much that once was. is lost. For none now live who remember iS. 154 0:00:00.825819
The world is changed. I feel it in the water. G feel it in the earth. I smell it in the air. Much that once was. is lost. For none now live who remember it. 155 0:00:00.870705
The world is changed. I feel it in the water. I feel it in the earth. I smell it in the air. Much that once was. is lost. For none now live who remember it. 156 0:00:00.898627
从最早的随机字符串开始,总共经过5200多次进化,最终生成了指环王的开场诗
所以,按照随机宇宙理论,只要猴子足够多,是否能够打出莎士比亚全集来呢?
(不过真实情况下,猴子只会按住一个按键不放,然后在屏幕上打出一连串的jjjjjjjj……)
还有的话,按照遗传算法,你得对猴子进行奖励,如果他答对了一个字符或者一个词,你得奖励它们一根香蕉,这样才符合遗传算法的核心——必须具备进化验证功能。
待续未完
作者的github上的源代码,把上面的算法给抽象封装成了可供通用的类,而且进行了多次重复验证统计,我这里就不展开源码讲了,有兴趣的同学自行去访问作者的github。
因为某些情况github网络不好,所以同学们可以访问虾神的gitee:
https://gitee.com/godxia/python_genetic_algorithm