跳到主要內容

路徑搜尋之演化論

我現在要作一個實驗,模擬一個虛擬的世界,這個世界的構造很簡單,是一張10乘10的地圖,總共100個格子所組成。在這個世界裡面,會隨機產生金幣,機率是一半一半,也就是說大約在這100個格子裡面,平均會有50個金幣。還要模擬某種生物,這種生物的生存目的很簡單,就是儘可能的吃金幣,因為這個世界的食物只有金幣。能夠吃愈多金幣的生物就愈能夠生存下來並把它的基因繼續傳播下去。生活在這個世界的生物就把它命名為吃金幣的人...吃金幣的人在這個世界能作的事情很有限,除了吃金幣外,他只能作上下左右四種方向的移動,每次只能移動一個格子。

這個實驗的目的是想辨法找到一個最佳的路徑搜尋方法,讓吃金幣的人儘可能吃愈到愈多的金幣。為了評估吃金幣的人的路徑搜尋方法有多好,需要有一個可以量化的方法。所以就很簡單的設定,如果吃金幣的人每吃到一個金幣就能獲得10分,分數愈高就表示它的路徑搜尋方法愈好。當然也不能讓吃金幣的人任意的作無限次移動,因為這樣一來就沒有意義了,因為吃金幣的人一定可以吃光世界上所有金幣。所以我們要限制移動次數,比如說200次,每200次移動後再來作評分。

我們將會使用基因算法(genetic algorithm),讓吃金幣的人自行進化,得到一個認為最棒的路徑搜尋方法。

基因算法的主要精神是,對於所要解決的問題以模擬生物界物競天擇適者生存的自然法則,讓所摸擬的人工生物自行進化出在模擬世界中的優勢物種,也就是得到所求的最佳解。不過這裡講到的進化這二個字有誤導的嫌疑,實際上這也是大多數人對於進化及演化之間的誤解。進化這二字裡面隱含了方向性,而事實上演化是沒有方向性的,演化是生物對生存環境的適應。生物會演化成什麼樣子,沒有任何的目的性和方向性,這裡面帶有隨機性,主要取決於環境變因。

自然選擇和變異再加上時間是演化的基本原則,所以我們將要模擬的程式也是使用到這幾個基本原則。雖然實際上的細節,和真實世界生物界的演化並不相同,但因為我們利用這幾個演化的基本原則來模擬我們的世界,結果也會得到類似的結果。我們可以看到我們所模擬的吃金幣的人果真在這個金幣世界裡面,慢慢的演化出愈來愈聰明的路徑搜尋方法。而同自然界的生物演化一樣,你甚至無法知道也無法分析它到底是怎麼辨到的。



前面提到這個世界是個10x10的地圖,共有100個格子,在這個世界面裡有會隨機產生的金幣。如果一個格子上有金幣就不為空,反之表示這個格子是空的狀態,因此每一個格子有二種不同的狀態,所以我們可以用一個boolean值來表示一個格子。不過因為這個世界是有邊界的,為了讓吃金幣的人有辨法意識到邊界的存在,我們也需要把這個資訊編碼進來。所以加上有無金幣及邊界,每一個格子共有3種可能的狀態。我們可以用一個數字表示一個格子的3種不同狀態。

  0: 空
  1: 金幣
  2: 邊界

吃金幣的人在這個世界裡會有一個唯一個位置,我們簡單把座標定為0~99。他能作的動作有5種,吃金幣,往上往下往左或往右移動,移動的時候一次只走一步。因為吃金幣的人只能作4種方向其中一個方向的移動,再加上他目前所在的位置,所以可以說他的可見範圍有5個格子。

    0
  1 X 3
    2

如上,吃金幣的人的當前的位置是X,0123是其它4個方向所在的格子。因為每一個格子有3種可能狀態,5個格子就總共有3^5=243種可能狀態。如果我們把這243種不同狀態分別對應到吃金幣的人可以作的其中一種動作,例如吃或往上移動一格等等,就等於得到一個路徑搜尋的演算法。只要每次根據吃金幣的人的所在位置,以及他的視野上的5個格子的狀態來對這243種狀態表去查詢要作什麼動作,吃金幣的人就會自動的按照這個表的設定動起來。而這個表也就是這個吃金幣的人的基因!每一條基因所編碼的內容是狀態對動作,也就是生物對不同環境狀態作所出的不同反應。這看起來很機械,只是個查表動作。雖然只是機械式的查表,但生物所表現出來的行為就好像具有意識,只要這個表足夠複雜甚至能表現的好像擁有智慧一樣!

  0: 往上移動一格
  1: 往左移動一格
  2: 往下移動一格
  3: 往右移動一格
  4: 吃金幣

這一來每一個吃金幣的人都有一條長度是243個單位的基因,每一個單位就是一個數字,對應吃金幣的人所能執行的5種動作之一。當然243是理論數字,這裡面有一些狀態是不可能出現的而可以排除,但為了簡單起見我們就不特別處理了。

下面是一段長度是243的可能基因,不同位置的數字對應不同狀態的反應動作,而.是無動作的遮罩表示這個位置的基因是冗餘。

30.44.11.13.04.13.33.30....12.22.41.20.40.11.34.02....32.40.24.10.03.02.33.42....01.40.24.14.31.44.32.10....31.12.00.11.40.31.14.01....04.31.33.44.13.43.10.42....11.23.11.01.01.12.24.34....11.02.40.33.00.22.32.34...............................

模擬的方法如下:

  1. 一次模擬200個吃金幣的人,每一個初代吃金幣的人都隨機給一組基因。
  2. 讓吃金幣的人根據他自己的基因設定走個200步,最後再看看得分是多少。
  3. 這裡面有個扣分的機制,如果吃金幣的人走出邊界了要扣分,因為這是不允許的。或者如果吃金幣的人作了吃金幣的動作,但那個格子裡面沒有金幣也要扣分,因為他亂吃。這個扣分的機制等於是讓不好的基因淘汰的機制,給他一個生存的壓力。
  4. 上面讓每個吃金幣的人走200步的模擬,每一個吃金幣的人都作100次,然後再統計一個平均分數。
  5. 針對每個吃金幣的人的平均分數作排序,由高到低。可以知道這200個吃金幣的人的基因,誰優誰劣。
  6. 對這200個吃金幣的人兩兩配對來產生下一代。配對選擇的方法是,得分愈高的也就是基因愈優的就愈容易被選擇。得分低的也還是有可能被選擇,只不過機率較低,這樣一來也可以讓那些得分低,但在未來有潛力的基因有機會出頭。
  7. 配對的方法是隨機選擇一個基因中間位置,將二個親代基因切斷並交換。當然還有其它配對方法,這裡只選擇最簡單的一種。
    
    00003333333
    44448888888
     =>
    00008888888
    44443333333
    
    
  8. 新的子代基因再以一定機率產生突變。
  9. 跳至2繼續模擬。
下圖是某個吃金幣的人的成長曲線圖,水平軸是時間,垂直軸是分數,Y-軸上的數字是平均最高分數。上面的點線是最大值,下面的點線是種群平均值。


這是某個吃金幣的人大約迭代了3000次的結果。


上圖分別是另外二個吃金幣的人迭代了約4000次及6000次的結果。

可以看到這三個吃金幣的人的成長曲線和速度都不同,但他們都是朝特定方向進化。我會說進化,是因為他們的演化的確是有方向性的,而這個方向是我刻意控制的,也就是讓這些吃金幣的人想盡辨法得高分。

在這些模擬裡面,有時候也會出現類似進入演化的死胡同的現象。這樣的現象是因為在模擬的過程中過早把那些具有潛力的基因給淘汰掉了,因為分數不夠而被選擇的機會太少。即使後來因為突變而又產生這些基因,也因為種群數量太少而沒有辨法讓這些基因傳播開來。結果就是化大概只能永遠停留在某個高度,不能再進一步成長。


留言

這個網誌中的熱門文章

以lex/yacc實作算式計算機

前面我們透過 手工的方式 實作了一個簡易的算式計算機,現在我們要開始使用工具來作同樣的事,比較看看手工和使用工具有什麼不同的差別。首先要介紹的就是lex&yacc。 lex & yacc lex(Lexical Analyzar)及yacc(Yet Another Compiler Compiler)是用來輔助程式設計師製作語法剖析器的程式工具。lex的工作就是幫助我們將輸入的資料文字串流分解成一個個有意義的token,而yacc的工作就是幫我們分析這些token和我們定義的規則作匹配。下圖中所表示的是使用lex及yacc的一般工作流程。 首先看到yacc會讀入一個.y檔案,這裡.y檔案的內容就是我們使用類似(E)BNF語法定義的語法規則,yacc會分析這些語法規則後,幫我們產生可以用來解析這些規則的程式碼,而這個檔案一般名稱預設為y.tab.c,產生的程式碼裡面最重要的一個的函式叫作yyparse。 同yacc類似,lex也會讀入一個.l的檔案,這個檔案裡面定義的是如何從文字流裡解出token的規則,使用的方法是常規表示式(regular expression)。在圖的左側中間我們還可以看到有一個叫作y.tab.h的檔案從yacc產生出來並餵給lex作輸入,這個檔案是yacc根據在讀入的.y檔裡面所定義的token代號所產生出來的一個header,這樣yacc及lex產生出來的程式碼裡面就可以使用共通定義的代碼而不必各寫個的。lex分析過.l檔案後也會產生一個一般預設叫作lex.yy.c的原始碼檔案,裡頭最重要的一個函式叫作yylex。 最後,我們把yacc產生出來的y.tab.c還有lex產生出來的lex.yy.c,以及其它我們自己撰寫的原始碼檔案一起拿來編譯再作連結,最後產生出來的就是一個可以用來解析我們定義的語法的解析器工具。以上是整個lex及yacc的使用流程概觀。 常規表示式 在正式使用lex之前,我們首先來對常規表示法作一個基本的認識。常規表示法是一種用來表示字串樣式(pattern)的中繼語言,就好比前文所介紹的(E)BNF表示式一樣,都是用來描述其它語言的語言,只不過用途不太一樣罷了。 常規表示式使用一些中繼符號(meta-symbol)以及ASCII字元定義字串樣式,以下列出一些常規表示式所使用的符號。 . 表示除了換行字元...

猜數字遊戲 (電腦猜人)

前幾天午睡時突然被告知要參加公司內部的程式設計比賽,題目是用C寫一支文字模式的4位數字猜數字遊戲,由使用者來猜電腦的數字。在上星期時其實就已經有公佈了,但我沒有注意到所以是臨時加入,還好這是個簡單的題目,不用花多少時間就可以寫出來。 規則: - 這是一對一比賽,雙方各選擇一4位數字,不讓對方知道。 - 4位數字由數字0至9組成,每位數不得重複。 - 雙方輪流猜對方的數字,直到一方猜中為止。 - A方猜B方的數字後,B方根據A方的猜測回答幾A幾B。 - 一個A表示猜中一個數字且位置正確,一個B表示猜中一個數字但位置不正確。 - 當一方猜中4A0B時即表示猜中對方全部4個數字且位置正確,贏得比賽。 - 例:B的謎底是4208,底下箭頭左測是A的猜測,箭頭右測是B的回答。    1234 ==> 1A1B    5678 ==> 1A0B    2406 ==> 1A2B    ...    4208 ==> 4A0B ; 寫個程式讓玩家來猜電腦的數字不難,不過我從來沒有寫過讓電腦來猜玩家數字的版本,所以花了點時間想想怎麼寫。 研究後歸納出二個點。 1, 使用窮舉法將所有可能數字組合列出。 2, 每次猜測後根據結果排除不可能是答案的組合,重複這個動作直到猜中答案為止。 第1點只是實作問題,第2點概念也很簡單,但要過濾不是答案的組合根據的是什麼?乍看之下沒什麼頭緒,不過想通之後就非常簡單了。 它的基本原理如下:假如謎底是4561,如果猜1524則會得到1A2B。從相反的角度來看,如果謎底是1524,則猜4561時也會得到1A2B的回答。 利用這個方法,每一次猜測一個數字X後,再以這個數字當作答案,來和所有剩下來的候選答案作比對,如果得到的結果(幾A幾B)和數字X是一樣的話,就把這個數字保留下來繼續作為候選答案,否則就過把這個數字過濾掉。下一把,繼續從候選答案裡選一個出來猜,重複上面的動作,直到猜中為止。 ; C++ STL的algorithm裡有個叫作next_permutation的函數,可以用來生成排列。 #include <iostream> #include <algorithm> using namespace std; int main () {   int myints[] = {1,2,3};  ...

KillSudoku 4顆星精彩數獨詳解 - 鍊技巧

這題數獨(sudoku)題目估計為4+顆星,有點難度。解題需要應用多種技巧,過程非常精彩有趣,是個好題。 底下使用 KillSudoku 作詳細圖解。 1,使用基本排除法則,可以簡單填入6個數字。到此為止,開始使用 候選數法 來解題。如下所示,為填入6個數字後的狀態圖。 2,如下圖,使用進階排除法,在第9列和第4行可以先排除幾個候選數。 3,如圖,在第2行有一個 Naked Subset (3,4),可以對3,4候選數作排除。附帶提一下,反過來看在同一行裡面也可以說有另一個Hidden Subset(2,5,8)存在。Naked Subset和Hidden Subset常是一體二面同時存在,只不過對我們來說,Naked Subset是相對比較容易看的出來。 排除第2行的3,4後,又可以對第2列以外的3作排除,如下圖。 4,接著,在第5行又發現了一個 Naked Subset (3,7,8)。 對第5行三個Subset以外的候選數3,7,8作排除後,又接著產生可以對第5行以外的3作排除。 5,這一題解到此為止,開始進入高潮。大部份能解到3顆星題目的人,猜想應該就此卡住。以下開始需要應用更高級的鍊技巧,才能夠繼續進行。 應用X-Chains鍊技巧,可以找到一條由4條強連結組成的鍊,可以排除候選數2。這裡的鍊指的是由2條以上的強連結組成,而所謂的強連結是指在同一行、或同一列或同一個Box裡,由唯二的候選數構成的連結。如上圖中的第9行中,只有二個2,這二個2構成一條強連結。為什麼說這是一條強連結?因為在這條連結的AB二個端點中,肯定會有一個2存在,要麼是A點要麼是B點。鍊技巧就是將多條強連結串連起來作候選數排除的技巧,而X-Chains是高級的鍊技巧裡面的基本技巧。 接上圖,這樣一來就又可以應用基本排除方法,填入3個數字,如下圖所示。 6,接下來就是本題最精彩的部份,以下需要連續找到3條鍊,才能繼續往下解。 7,找出3條鍊後,剩下來的部份就沒什麼特別的了,只需要應用基本法就能把所有剩餘數字填完。