© Andrew Kirillov 2006, Conmajia 2012
安德魯·基里洛夫 著,Conmajia 譯
作者簡介:
安德魯·基里洛夫是一名高級軟件工程師。安德魯是著名的圖像、數學、神經網絡編程框架 AForge.NET 的作者。
原文鏈接:點擊訪問
演示DEMO:點擊下載
源代碼:點擊下載
簡介
人們在進化計算領域進行了非常多的研究工作,總結出了大量的進化算法。研究者對這些方法進行了廣泛的鑽研,並嘗試將它們應用到眾多不同領域的任務中。有一個眾所周知的事實,那就是許多科研問題使用傳統方法,都不可能在一個合理的時間范圍內得出准確的結果。也有許多問題沒有一個形式化的解決方法,這使得人們很難——甚至是不可能——用傳統方法來解決這些問題。一個典型的例子就是“旅行商問題”(Traveling Salesman Problem,TSP)。TSP要求在給定數量的城市之間找到一條最短的路徑,使得旅行商能訪問所有的城市,並且每個城市只訪問一次,最后回到出發的城市。對於這樣的問題,很多時候我們可以使用進化計算方法,在可以接受的時間范圍內得到一個較好的解。使用進化計算方法,並不能保證得到問題的精確解,而是找到一個最接近最佳答案的“足夠好”的解。這就是為什么這類方法越來越多的被用於解決很多不同的問題——這些問題往往是不能(或者很難)用傳統方法求解的。
本文討論了一個用C#實現的進化計算類庫。該類庫實現了數個流行的進化算法,如遺傳算法(GA)、遺傳編程(GP)和基因表達式編程(GEP)。該類庫可以用於求解多種不同的實際問題,其用法通過以下4個例子進行演示:
- 函數優化
- 符號回歸計算
- 時間序列預測
- 旅行商問題
該類庫的設計思想是保證其靈活性和可重用性,以便能將其用於解決不同的問題。本文不會討論進化算法的詳細內容,取而代之地,本文簡要介紹了相關的算法,並在文后提供了一系列參考資料,以便感興趣的讀者深入研究。
進化計算
遺傳算法的歷史始於20世紀60年代,John Holland在他的工作中首先提出了基於進化的遺傳算法(GA)思想。從那時開始,許多研究者開始加入到進化計算領域,由此產生了很多不同的算法[1]。這些算法被廣泛地研究,並應用於大量不同問題求解。到了今時今日,人們仍在各自領域中繼續研究這些算法,也使得這些算法能夠解決更多的新的問題。
GA算法基於達爾文關於生物繁殖和遺傳的自然選擇法則,如交叉(重組)和變異。該方法處理一定量種群的個體(染色體),其中每一個都編碼有問題的可能解。GA染色體是由固定長度的串組成(一組二進制位、數字,等等),這使得遺傳算子實現起來非常簡單。染色體初始化數量是隨機的,但之后就開始用交叉、變異、選擇等遺傳算子進行進化。
最簡單的交叉算子是單點交叉——在兩個染色體中隨機選擇一點進行交叉(交換剩余部分):
另一個著名的交叉算子是兩點交叉——選擇染色體中兩個隨機點,並交換兩點間的部分。事實上,根據求解的問題不同,除了這兩種常用的交叉算子外,GA算法中還有很多其他的交叉算子。需要注意的一個問題是,上述兩種經典交叉算子完全不能直接應用於問題求解中,應用時需要使用它們各自針對不同問題的特定變體。
變異算子處理單一的染色體,僅是簡單地隨機改變該染色體。單點變異算子只改變染色體中的一個基因:
如同交叉算子,變異算子同樣擁有大量針對特定問題和類型的變體。
所以,在初始人口創建之后,GA算法的每次迭代都包括有以下步驟:
- 交叉——選擇隨機個體並應用交叉算子
- 變異——選擇隨機個體並對其應用變異算子
- 計算每個個體的適合度
- 選擇——為下一代選擇個體
該算法可能在指定數量的迭代之后,或是找到一個足夠好的解之后停止。計算染色體的適合度和具體問題相關——適合度表示該染色體“好”的程度。染色體適合度越高,表示其越“好”,也就越有可能被選入下一代(生存幾率更高——譯注)。
有幾種選擇算子,其主要思想就是給予適合度高的優秀個體更多機會來選擇個體進入下一代。其中最為著名的是Elitism算法(精英算法)——選擇一定數量的最優染色體進入下一代。
1992年John Koza提出了一項具有重大意義的新成果——遺傳編程(GP)[2]。在GP中,單個人口成員(染色體)不再像GA中那樣,是編碼了問題可能解的固定長度線性字符串,而是可以執行並求解問題的程序。這些程序在GP中被表示成不同大小和形狀的解析樹,這樣使得這些方法可以更加靈活地應用於求解多種問題。染色體的表現差異可以說是GP算法和GA算法最主要甚至是唯一的差別之處。GP算法中,基本的達爾文適者生存思想仍然相同,但在變異算子、交叉算子以及適合度計算方面則和GA算法相比,有一定的變化。在GP算法中,變異算子並非通過改變某一個體的基因實現,而是重新生成一個樹節點,以此作為染色體樹上某一子樹。交叉算子也是如此——染色體互相交換子樹(可能在尺寸、形狀上均不相同),而非交換同樣長度的兩部分。然而,仍舊需要對染色體進行相同的檢查以確保它們不會生長得太長。
在GP算法中計算適合度,並不是僅僅將染色體作為參數傳遞給某個計算適合度的算法就完了,而是執行代表染色體的程序,然后根據程序輸出來計算適合度。
2001年Candida Ferreira介紹了另一種被稱為基因表達式編程(GEP)的方法[3]。該方法和遺傳編程以及遺傳算法均有相似之處。一方面,該方法仍舊采用輸出求解結果的程序操作,就如GP算法一般。但在程序的表現上,GEP有所不同。染色體在該方法中不再表示成樹,而是和GA算法一樣采用固定長度的線性表示。這種染色體表現形式上的變化,使得類似變異和交叉這樣的遺傳算子更加簡單。但是會使用一些很小的約束來確保算子的安全。
* + / a b c a
染色體表現形式 (a) GP算法 (b) GEP算法
上圖展示了GP算法和GEP算法中染色體的不同表現形式。兩個染色體都編碼了相同的程序——算數表達式(a+b)*(c/a)。GP算法中是以解析樹的形式表示的,而GEP算法則以從左上到右下的順序線性表示的解析字符串。可以很容易的把GEP字符串轉換回一顆解析樹,然后仍按照從左上到右下的順序加以填充,並確定每個函數的參數數量。
使用類庫
類庫基於靈活、可重用的思想設計,可以用於求解多種問題。類庫的代碼不依賴於任何特定問題,而是實現了進化計算以及遺傳算法、遺傳編程和基因表達式編程等相關算法的通用概念。進化計算中的實體如人口、染色體、選擇方法和適合度函數是作為單獨的類(Class)實現的,以便能方便的進行組合來求解特定問題。大多數情況下,類庫的使用者只需要為其待求解問題定義一個適合度計算函數,然后定義染色體類型、選擇算法和一些其他參數,如人口大小、變異和交叉概率,等等。如果待求解問題需要一些特殊的染色體或遺傳算子的變體,如變異和交叉,使用者可以通過實現IChromosome接口實現自己的染色體類,或是通過繼承已有的染色體類達到此目的。選擇算法和適合度計算函數與此類似——通過實現ISelectionMethod和IFitnessFunction接口創建自定義選擇算法和適合度計算函數。使用者通過上述方法創建的自定義類,擴展了類庫功能,和原有類一起,用於求解特定問題。
為了演示類庫的使用方法,下面給出4個使用不同進化計算算法的例子:
- 函數優化(遺傳算法)
- 符號回歸計算(遺傳編程和基因表達式編程)
- 時間序列預測(遺傳編程和基因表達式編程)
- 旅行商問題(遺傳算法)
函數優化
函數優化是演示遺傳算法的經典問題。使用本文介紹的類庫求解該類問題,你只需要在優化范圍內定義一個優化函數,然后創建一個遺傳種群(人口——譯注),指定進化算法所需的參數:
1 // 定義優化函數 2 public class UserFunction : OptimizationFunction1D 3 { 4 public UserFunction( ) : 5 base( new DoubleRange( 0, 255 ) ) { } 6 7 public override double OptimizationFunction( double x ) 8 { 9 return Math.Cos( x / 23 ) * Math.Sin( x / 50 ) + 2; 10 } 11 } 12 ... 13 // 創建遺傳種群 14 Population population = new Population( 40, 15 new BinaryChromosome( 32 ), 16 new UserFunction( ), 17 new EliteSelection( ) ); 18 // 運行一代 19 population.RunEpoch( );
上面的例子創建了一個數量為40的染色體種群,每個染色體是長度為32bit的二進制串,使用了針對一維的精英選擇方法和適合度計算函數。在上述(以及其他)的例子中,沒有針對遺傳算法(或其他某種算法)的明確的區別,他們之間具有很多共同之處。比如,種群創建的方法在所有本文提到的遺傳算法中都是相同的。染色體是使用何種類型算法的決定因素,它定義了問題解的表現形式以及遺傳算子的實現方式。
符號回歸計算(近似解)
符號回歸問題的目的是找到針對輸入數據的最佳近似函數。通常人們利用遺傳編程或基因表達式編程算法來解決這類問題。使用這兩種算法都可以找到一個函數,該函數以X值和一些常數為參數,輸出一個接近真實值的Y值。
實際解題的代碼和上一個例子的代碼非常相似,其中種群類和選擇方法類都是一樣的。很顯然,由於染色體不同,這兩個例子唯一不同的部分就是適合度計算函數。在解題時,如果使用遺傳編程算法,則在代碼中用GPTreeChromosome類創建染色體。如果使用基因表達式編程算法,則用GEPChromosome類。
1 // 需要近似求解的函數(輸入數據) 2 double[,] data = new double[5, 2] { 3 {1, 1}, {2, 3}, {3, 6}, {4, 10}, {5, 15} }; 4 // 創建種群 5 Population population = new Population( 100, 6 new GPTreeChromosome( new SimpleGeneFunction( 6 ) ), 7 new SymbolicRegressionFitness( data, new double[] { 1, 2, 3, 5, 7 } ), 8 new EliteSelection( ), 9 0.1 ); 10 // 運行一代 11 population.RunEpoch( );
在上面的例子中,需要近似求解的函數是用二位數組來表示(X,Y)值對的。一個有趣的現象是,上面例子中Population類構造函數的最后一個參數——該參數的值表示10%(即0.1——譯注)的新種群由隨機的染色體組成,而剩余的90%則為當前代的成員。
時間序列預測
時間序列預測問題創建了一個基於函數歷史值來預測函數未來值的模型。為了完成這個任務,使用訓練數據來創建(訓練)該模型,直到其開始基於訓練集產生符合要求的結果。該模型用於預測函數未來值。
1 // 需要預測的時間序列 2 double[] data = new double[13] { 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, 56, 67, 79 }; 3 // 常數 4 double[] constants = new double[10] { 1, 2, 3, 5, 7, 11, 13, 17, 19, 23 }; 5 // 滑動窗大小 6 int windowSize = 5; 7 // 創建種群 8 Population population = new Population( 100, 9 new GPTreeChromosome( new SimpleGeneFunction( windowSize + constants.Length ) ), 10 new TimeSeriesPredictionFitness( data, windowSize, 1, constants ), 11 new EliteSelection( ) ); 12 // 運行一代 13 population.RunEpoch( );
可以看到,時間序列預測問題的代碼幾乎和符號回歸計算問題代碼完全一致——只是修改了適合度計算函數和算法的一些參數。這說明本文提出的類庫使用簡單,具有高度可重用性。
旅行商問題
旅行商問題目標是在城市之間找到一條最短路徑,使得旅行商從一個城市出發,不重復地訪問每一個城市,最后回到起點。這類問題也被稱為NP困難問題。如果使用傳統方法進行求解,在城市數量較大的情況下,解題可能花費極長的時間。然而,可以使用遺傳算法,在合理的時間范圍內得到該問題的一個相當接近准確解的結果。
1 // 創建種群 2 Population population = new Population( populationSize, 3 new PermutationChromosome( citiesCount ), 4 new TSPFitnessFunction( map ), 5 new EliteSelection( ) 6 );
值得注意的是,作為類庫的組成部分,PermutationChromosome類要求創建新的適合度計算函數(TSPFitnessFunction)並使用現有代碼其余部分來求解新的問題。然而該算法的性能還有進一步提高的余地。默認的通用PermutationChromosome類可以得到問題解,但是使用從該類派生的新類並重寫交叉算子可以獲得更好的性能。自定義的TSPChromosome類(參考源代碼)實現了所謂的貪婪交叉算法,允許在更短時間內找到更好的問題解。
多年來,遺傳算法總是能為旅行商問題解出最佳的結果。旅行商問題非常有名且流行,每年還會專門舉辦求解該問題的競賽,以期找到更好的算法。眾所周知,20世紀90年代末,出現了另一種性能更好的旅行商問題求解算法。新算法基於蟻群思想,同樣來自人工智能領域的研究成果。(參考螞蟻系統和蟻群系統算法)
結論
以上的4個例子展示了本文類庫的最初目標——靈活、可擴展、可重用,並且使用簡單。盡管不能覆蓋進化計算的各個方面,也仍有許多工作需要完善,但是該類庫已經可以用於許多不同問題的求解,而且非常容易就可以擴展該類庫以求解新的問題。通過研究設計該類庫,我不禁對進化算法有了更深入的了解,而且幫助我更好的研究了遺傳編程和基因表達式編程算法。
參考文獻
[1] Ajith Abraham, Nadia Nedjah and Luiza de Macedo Mourelle, Evolutionary Computation: from Genetic Algorithms to Genetic Programming // Genetic Systems Programming: Theory and Experiences, volume 13 of Studies in Computational Intelligence, pages 1-20. Springer, Germany, 2006.
[2] John R. Koza, Genetic Programming // Version 2 – Submitted August 18, 1997 for Encyclopedia of Computer Science and Technology.
[3] Ferreira, Gene Expression Programming: A new adaptive algorithm for solving problems // Complex Systems, Vol. 13, No. 2, pp. 87–129, 2001.
歷史
[4.8.2012] - 完成翻譯
[2.8.2012] - 開始翻譯本文
[16.10.2006] - 發表本文
許可證
本文及其附屬的任何源代碼和文件均以GNU General Public License(GPLv3)許可證發布。
(全文完)
© Andrew Kirillov 2006, Conmajia 2012