遺傳算法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