1 簡介
根據達爾文的進化論,生物種羣從低級、簡單的類型逐漸發展成爲高級、複雜的類型。各種生物要生存下去就必須進行生存鬥爭,具有較強生存能力的生物個體容易存活下來,並有較多的機會產生後代;具有較低生存能力的生物則被淘汰,或者產生後代的機會越來越少,直至消亡。
遺傳算法借鑑了生物界自然選擇、遺傳變異機制,將種羣代表一組問題的解,通過對當前種羣施加選擇、交叉和變異等一系列遺傳操作,從而產生新一代的種羣,並逐漸使種羣進化到包含近似最優解的狀態。遺傳算法能夠解決傳統方法感到困難的許多複雜優化問題。
遺傳算法很有趣,上過生物課的人很容易理解它,但是要設計一個好的遺傳算法卻並不容易。當看到誰養魚這個問題時,覺得可以用遺傳算法來試一試,現在我已經用Java實現了遺傳算法,但是實驗結果並不是很理想,權當做了一次實驗證明遺傳算法並不適合這類問題吧(當然也有可能是我的遺傳算法沒有設計好)。儘管這次實驗結果不甚理想,下面還是介紹一下如何實現遺傳算法。
設計一個遺傳算法主要有五大要素:編碼方式、種羣規模的設定、適應度函數的設計、遺傳算子的設計和終止條件的設定。
2 編碼
當用遺傳算法解決問題時,必須在問題空間和遺傳算法的個體基因結構之間建立聯繫,即編碼和解碼的方法。編碼的策略對於遺傳算子,尤其是交叉、變異算子的功能和設計有很大的影響。編碼應滿足三個原則:
- 完備性:原問題空間中的點都能成爲編碼後的點。
- 健全性:編碼後的空間中的點能對應原問題空間所有的點。
- 非冗餘性:編碼前後空間的點一一對應。
我採用了誰養魚(二)中介紹的方法進行編碼。對於每一類屬性(顏色、國籍、飲料、寵物和香菸),都先將其五種可能分別映射到1、2、3、4、5,如此一類屬性的排列就映射成了1、2、3、4、5的排列,再使用誰養魚(二)中的方法映射到[0,119]的整數域。每一個[0,119]的整數可以用1個字節表示,因此我們用5個字節就可以表示一個可能的解。在這裏將解稱爲個體,將五個字節分別稱爲顏色染色體、國籍染色體、飲料染色體、寵物染色體和香菸染色體,而每個字節代表了該染色體上的基因排列,注意[120,127]之間的基因排列是無效的。
五種屬性我是按照紅黃藍綠白,英挪瑞德丹,貓狗魚馬鳥,水咖啤奶茶,Dunhill、Blends、PallMall、Prince、BlueMaster的順序映射到1、2、3、4、5的,因此誰養魚(一)中給出的最終解的編碼是<30, 43, 22, 17, 0>。
這種編碼方式顯然滿足了三個原則,但是也存在問題,比如最優解(匹配15條線索)和近似最優解(匹配絕大多數線索)的編碼之間似乎沒有什麼聯繫,我找不到將近似最優解進行很小變動就得到最優解的方法,整個算法的過程有點類似隨機的搜索。
3 種羣規模
種羣中個體的數目稱爲種羣規模,在執行算法之前,必須已經有一個若干初始解組成的初始種羣。在工程問題中,往往並不存在問題空間的先驗知識,所以很難確定最優解的數量及其在可行解空間中的情況。所以一般是隨機產生初始種羣,種羣規模通常在幾十到幾百之間。種羣規模過小會限制羣體的多樣性,導致搜索過早收斂;規模過大,導致計算量增加,會削弱算法的效率。
我採用了隨機生成初始種羣的方法,而種羣規模設爲50左右。
4 適應度函數
爲了執行適者生存的原則必須對個體的適應性進行評價。適應度函數體現了個體的生存環境,根據適應度函數計算出個體的適應度,就可以判斷它在此環境下的生存能力。一般來說較好的個體基因結構具有較高的適應度函數值,即可以獲得較高的評價。由於適應度函數是種羣中個體生存機會的唯一確定性指標,所以適應度函數的設計直接決定了種羣的遺傳行爲。適應度函數的設計一般需要滿足以下條件:
- 連續、非負。
- 儘可能簡單。
- 對某一類具體問題,應儘可能通用。
對於特殊設計的遺傳算法,也不必完全遵守上述規則。我以誰養魚(三)爲基礎,將個體能匹配的線索數作爲適應度函數的主體,匹配的線索越多適應度就越高,這種方法非常自然也很簡單。
5 遺傳算子
標準的遺傳算子包括選擇、交叉和變異三種,它們構成了遺傳算法的核心,使得算法具有強大的搜索能力。
5.1 選擇算子
選擇算子就是確定父代種羣中哪些個體可以遺傳到下一代種羣中,它根據個體的適應度決定被選擇的概率。目前常用的方法有適應度比例選擇、輪盤式選擇和競爭式選擇等形式。我採用的是輪盤式選擇,即根據個體適應度大小分配輪盤面積,面積代表了個體被挑選到交配池中的概率。具體的實現方法可以先計算種羣中個體適應度之和fitnessAmount,產生一個0到fitnessAmount的隨機數,遍歷種羣,累計個體的適應度,遇到的第一個累計適應度大於隨機數的個體就被選中了,一直重複直到交配池達到種羣規模。
/*
* 輪盤賭選擇個體進行交叉
* 單獨測試能反映優勝劣汰
* */
public void select(){
Iterator<Individual> iter = this.iterator();
//計算種羣適應度總和
fitnessAmount=0;
while(iter.hasNext()){
Individual indi = iter.next();
fitnessAmount+=indi.fitness;
}
//不斷選擇個體進入交配池
int number = this.size();
while(this.size()<number+God.SIZE){
double points;
Individual winner=null;
points = God.RAN.nextDouble()*fitnessAmount;
double amount=0;
for(int i=0;i<number;++i){
Individual indi=get(i);
amount+=indi.fitness;
if(amount>=points){
winner=(Individual)indi.clone();
break;
}
}
this.add(winner);
}
this.removeRange(0,number);
}
5.2 交叉算子
交叉算子是遺傳算法最主要的操作,它模仿自然界有性繁殖的基因重組過程,其作用就是將選擇操子選出來的優良基因遺傳到下一代種羣中。通常的做法是:隨機確定一個或多個位置爲交叉點,由此將一對父體的基因序列分爲有限個片段,再以一定概率交換相應片段得到新的個體。根據交叉點的數量可以分爲單點交叉、多點交叉和均勻交叉等。交叉的概率太高,則優良物種被取走的速度越快,產生新物種的速度也越快;交叉的概率太低,則搜索會停滯不前。因爲交叉前已經做過選擇,所以較一般隨機算法更好。
我在實現時以染色體爲單位進行單點交叉,交叉的概率Pc設爲0.9左右。注意到交叉可能產生不符合[0,119]範圍的染色體,遇到這種情況就重新交叉。
/*
* 每個染色體都是單點交叉
* */
public void recombine(Individual indi){
//遍歷個體的染色體
for(int i=0;i<size();++i){
int x,y;
do{
x=this.get(i);
y=indi.get(i);
double point = God.RAN.nextDouble();
if(point>God.Pc)
continue;
int mask=(int)Math.pow(2, (int)((point/God.Pc)*God.CHROMOSOME_LENGTH)+1)-1;
//注意java中“-”的優先級高於“&”
x = (x-(x&mask))+(x&mask)^(y&mask);
y = (y-(y&mask))+(x&mask)^(y&mask);
x = (x-(x&mask))+(x&mask)^(y&mask);
}while(x>God.CHROMOSOME_UPPERLIMIT||y>God.CHROMOSOME_UPPERLIMIT);
this.set(i, x);
indi.set(i, y);
}
}
5.3 變異算子
完全依靠選擇和交叉操作可能導致無法創造出具有新特性的個體,這有點類似Packing問題裏的局部最小值陷阱?所以引入突變使個體跳出局部解範圍,產生全局最優解。變異操作通常是將個體基因序列中的某些基因位上的基因值用該基因位的其他等位基因來替換,從而產生新個體。變異的概率一般取值很小,否則就等於隨機搜索了。
我將每條染色體發生變異的概率Pm定爲0.04,這樣一個個體發生變異的概率大概是18%,當個體的適應度較小時,將變異率修正爲1。注意到變異可能產生不符合[0,119]範圍的染色體,遇到這種情況就重新變異。
public void mutate(){
double pm = God.Pm;
if(fitness<0.6)
pm=1;
for(int i=0;i<size();++i){
int x;
do{
x=this.get(i);
double point = God.RAN.nextDouble();
if(point>God.Pm)
continue;
x=x^(1<<(int)(God.CHROMOSOME_LENGTH*point/pm));
}while(x>=God.CHROMOSOME_UPPERLIMIT);
set(i, x);
}
}
6 終止條件
一般爲遺傳算法設定一個最大代數作爲終止條件,這種辦法很簡單,但是需要多次調試才能找到合適的代數。
7 實驗結果
我將種羣規模設爲50,最大代數設爲50000,然後輸入不同數量的線索進行測試:
- 測試10條以內的線索可以很快找到解,一般運行10000代以內;
- 測試11條線索大多數情況都找到解,通常需要運行2、3萬代;
- 測試12、13條線索可能找到解,概率在50%左右;
- 測試14條線索偶爾能得到解,大多數是有一條線索未匹配;
- 測試全部15條線索還未得到解,大多數有一線索未匹配。
當我不限制代數測試15條線索時,通常會收斂到匹配14條線索就停滯不前了。
8 討論
遺傳算法適合那種解空間特別大,又找不到合適的算法可以短時間內解決的問題,例如NP難度問題。誰養魚這個問題的解空間是 120^5=24,883,200,000,暴力解法當然可以嘗試,但是就不好玩了。這個問題我用遺傳算法並沒有得到好的結果,是否有其它更好的算法我會繼續關注,而且這次遺傳算法的設計還有很多不盡如人意的地方,還希望能有高人指點一二。
參考文獻
[1] 韓慧等。數據倉庫與數據挖掘。清華大學出版社。