遺傳算法的C語言實現(一):以非線性函數求極值為例
以前搞數學建模的時候,研究過(其實也不算是研究,只是大概了解)一些人工智能算法,比如前面已經說過的粒子群算法(PSO),還有著名的遺傳算法(GA),模擬退火算法(SA),蟻群算法(ACA)等。當時懂得非常淺,只會copy別人的代碼(一般是MATLAB),改一改值和參數,東拼西湊就拿過來用了,根本沒有搞懂的其內在的原理到底是什麽。這一段時間,我又重新翻了一下當時買的那本《MATLAB智能算法30個案例分析》,重讀一遍,發現這本書講的還是非常不錯的,不僅有現成算法的MATLAB實現,而且把每一種算法的實現原理都基本講清楚了,我再次看的時候,以前不懂的(也許以前太急功近利了)現在居然基本全部弄懂了。真是讓我感到非常開心~~~所以我萌發了一個沖動,想用現在自己比較熟悉的C語言,把這本書上的案例重新實現一遍,徹底弄懂每個算法的原理。我打算不定期地更新幾篇來介紹其中幾種常見的智能算法和其應用(當然我也不止會看這本書,還會參考別的書籍和論文),盡量把每種算法的原理給講明白,也算是對我接觸這些智能算法的一個總結吧。
今天首先介紹遺傳算法(genetic algorithm,GA)。
遺傳算法是一種進化算法,其基本原理是模仿自然界中的生物“物競天擇,適者生存”的進化法則,把問題參數編碼為染色體,再利用叠代的方式進行選擇、交叉、變異等運算法則來交換種群中染色體的信息,最終生成符合優化目標的染色體。
在遺傳算法中,染色體對應的是數據或者數組,通常是由一維的串結構數據來表示,串上各個位置對應基因的的取值。基因組成的串就是染色體,或者稱為基因型個體。一定數量的個體組成了種群,種群中個體數目的大小稱為種群大小,也稱為種群規模,而各個個體對環境的適應程度稱為適應度。
標準遺傳算法的步驟如下:
(1) 編碼:遺傳算法在搜索解空間之前需要將解數據表示成遺傳空間的基因型串結構數據,這些串結構數據的不同組合構成了不同的染色體。
(2)初始化:即生成初始種群。具體的做法是隨機生成N個初始的染色體(解空間的解),每一個染色體其實就相當於一個個體,N個個體構成了一個初始種群。遺傳算法以這N個個體作為初始值開始進化。
(3) 適應度評估:適應度表明個體或者解的優劣性。不同的問題,定義適應度函數的方式也不同。比如如果是求一個函數的最大值,那麽一般某個解對應的函數值越大,那麽這個個體(解)的適應度也就越高。
(4)選擇:選擇的目的是為了從當前種群中選出優良的個體,使它們有機會成為父代繁殖下一代子孫。遺傳算法通過選擇過程體現這一思想,進行選擇的原則是適應性強的個體為下一代貢獻一個或者多個後代的概率大。這體現了達爾文的適者生存原則。
(5)交叉:交叉操作是遺傳算法中最主要的遺傳操作。通過交叉可以得到新一代個體,新個體組合了其父代的個體特征。交叉體現了信息交換的思想。
(6)變異:變異首先在群體中隨機選擇一個個體,對於選中的個體以一定的概率(通常是比較小的概率,這與自然界一致,自然界的變異都是小概率事件)隨機改變染色體中某個基因的值。
有的時候除了選擇選擇、交叉、變異這三種操作之外,我們還會針對具體的問題加入其它的操作(比如逆轉之類),但是選擇、交叉、變異是所有的遺傳算法都共同的擁有的遺傳操作。
本文以遺傳算法常見的一個應用領域——求解復雜的非線性函數極值問題為例,來說明如何用代碼(這裏是用C語言,當然你可以換做別的任何一種語言)來具體實現上述的遺傳算法操作。求解的函數來自《MATLAB智能算法30個案例分析》,即:
f = -5*sin(x1)*sin(x2)*sin(x3)*sin(x4)*sin(x5) - sin(5*x1)*sin(5*x2)*sin(5*x3)*sin(5*x4)*sin(5*x5) + 8
其中x1,x2,x3,x4,x5是0~0.9*PI之間的實數。該函數的最小值為2,當x1,x2,x3,x4,x5都取PI/2時得到。
使用C語言實現的遺傳算法求解代碼如下:
/* * 遺傳算法求解函數的最優值問題 * 參考自《MATLAB智能算法30個案例分析》 * 本例的尋優函數為:f = -5*sin(x1)*sin(x2)*sin(x3)*sin(x4)*sin(x5) - sin(5*x1)*sin(5*x2)*sin(5*x3)*sin(5*x4)*sin(5*x5) + 8 * 其中x1,x2,x3,x4,x5是0~0.9*PI之間的實數。該函數的最小值為2,當x1,x2,x3,x4,x5都取PI/2時得到。 * update in 16/12/3 * author:Lyrichu * email:[email protected] */ #include<stdio.h> #include<stdlib.h> #include<math.h> #include<time.h> #define PI 3.1415926 //圓周率 #define sizepop 50 // 種群數目 #define maxgen 500 // 進化代數 #define pcross 0.6 // 交叉概率 #define pmutation 0.1 // 變異概率 #define lenchrom 5 // 染色體長度 #define bound_down 0 // 變量下界,這裏因為都相同,所以就用單個值去代替了,如果每個變量上下界不同,也許需要用數組定義 #define bound_up (0.9*3.1415926) // 上界 double chrom[sizepop][lenchrom]; // 種群數組 double fitness[sizepop]; //種群每個個體的適應度 double fitness_prob[sizepop]; // 每個個體被選中的概率(使用輪盤賭法確定) double bestfitness[maxgen]; // 每一代最優值 double gbest_pos[lenchrom]; // 取最優值的位置 double average_best[maxgen+1]; // 每一代平均最優值 double gbest; // 所有進化中的最優值 int gbest_index; // 取得最優值的叠代次數序號 // 目標函數 double fit_func(double * arr) { double x1 = *arr; double x2 = *(arr+1); double x3 = *(arr+2); double x4 = *(arr+3); double x5 = *(arr+4); double func_result = -5*sin(x1)*sin(x2)*sin(x3)*sin(x4)*sin(x5) - sin(5*x1)*sin(5*x2)*sin(5*x3)*sin(5*x4)*sin(5*x5) + 8; return func_result; } // 求和函數 double sum(double * fitness) { double sum_fit = 0; for(int i=0;i<sizepop;i++) sum_fit += *(fitness+i); return sum_fit; } // 求最小值函數 double * min(double * fitness) { double min_fit = *fitness; double min_index = 0; static double arr[2]; for(int i=1;i<sizepop;i++) { if(*(fitness+i) < min_fit) { min_fit = *(fitness+i); min_index = i; } } arr[0] = min_index; arr[1] = min_fit; return arr; } // 種群初始化 void init_chrom() { for(int i=0;i<sizepop;i++) { for(int j=0;j<lenchrom;j++) { chrom[i][j] = bound_up*(((double)rand())/RAND_MAX); } fitness[i] = fit_func(chrom[i]); // 初始化適應度 } } // 選擇操作 void Select(double chrom[sizepop][lenchrom]) { int index[sizepop]; for(int i=0;i<sizepop;i++) { double * arr = chrom[i]; fitness[i] = 1/(fit_func(arr)); // 本例是求最小值,適應度為目標函數的倒數,即函數值越小,適應度越大 } double sum_fitness = 0; for(int i=0;i<sizepop;i++) { sum_fitness += fitness[i]; // 適應度求和 } for(int i=0;i<sizepop;i++) { fitness_prob[i] = fitness[i]/sum_fitness; } for(int i=0;i<sizepop;i++) { fitness[i] = 1/fitness[i]; // 恢復函數值 } for(int i=0;i<sizepop;i++) // sizepop 次輪盤賭 { double pick = ((double)rand())/RAND_MAX; while(pick < 0.0001) pick = ((double)rand())/RAND_MAX; for(int j=0;j<sizepop;j++) { pick = pick - fitness_prob[j]; if(pick<=0) { index[i] = j; break; } } } for(int i=0;i<sizepop;i++) { for(int j=0;j<lenchrom;j++) { chrom[i][j] = chrom[index[i]][j]; } fitness[i] = fitness[index[i]]; } } // 交叉操作 void Cross(double chrom[sizepop][lenchrom]) { for(int i=0;i<sizepop;i++) { // 隨機選擇兩個染色體進行交叉 double pick1 = ((double)rand())/RAND_MAX; double pick2 = ((double)rand())/RAND_MAX; int choice1 = (int)(pick1*sizepop);// 第一個隨機選取的染色體序號 int choice2 = (int)(pick2*sizepop);// 第二個染色體序號 while(choice1 > sizepop-1) { pick1 = ((double)rand())/RAND_MAX; choice1 = (int)(pick1*sizepop); //防止選取位置過界 } while(choice2 > sizepop-1) { pick2 = ((double)rand())/RAND_MAX; choice2 = (int)(pick2*sizepop); } double pick = ((double)rand())/RAND_MAX;// 用於判斷是否進行交叉操作 if(pick>pcross) continue; int flag = 0; // 判斷交叉是否有效的標記 while(flag == 0) { double pick = ((double)rand())/RAND_MAX; int pos = (int)(pick*lenchrom); while(pos > lenchrom-1) { double pick = ((double)rand())/RAND_MAX; int pos = (int)(pick*lenchrom); // 處理越界 } // 交叉開始 double r = ((double)rand())/RAND_MAX; double v1 = chrom[choice1][pos]; double v2 = chrom[choice2][pos]; chrom[choice1][pos] = r*v2 + (1-r)*v1; chrom[choice2][pos] = r*v1 + (1-r)*v2; // 交叉結束 if(chrom[choice1][pos] >=bound_down && chrom[choice1][pos]<=bound_up && chrom[choice2][pos] >=bound_down && chrom[choice2][pos]<=bound_up) flag = 1; // 交叉有效,退出交叉,否則繼續下一次交叉 } } } // 變異操作 void Mutation(double chrom[sizepop][lenchrom]) { for(int i=0;i<sizepop;i++) { double pick = ((double)rand())/RAND_MAX; // 隨機選擇一個染色體進行變異 int choice = (int)(pick*sizepop); while(choice > sizepop-1) { pick = ((double)rand())/RAND_MAX; int choice = (int)(pick*sizepop); // 處理下標越界 } pick = ((double)rand())/RAND_MAX; // 用於決定該輪是否進行變異 if(pick>pmutation) continue; pick = ((double)rand())/RAND_MAX; int pos = (int)(pick*lenchrom); while(pos > lenchrom-1) { pick = ((double)rand())/RAND_MAX; pos = (int)(pick*lenchrom); } double v = chrom[i][pos]; double v1 = v - bound_up; double v2 = bound_down - v; double r = ((double)rand())/RAND_MAX; double r1 = ((double)rand())/RAND_MAX; if(r >= 0.5) chrom[i][pos] = v - v1*r1*(1-((double)i)/maxgen)*(1-((double)i)/maxgen); else chrom[i][pos] = v + v2*r1*(1-((double)i)/maxgen)*(1-((double)i)/maxgen); // 註意這裏寫法是不會越界的,所以只用一次變異就可以了 } } // 主函數 int main(void) { time_t start,finish; start = clock(); // 程序開始計時 srand((unsigned)time(NULL)); // 初始化隨機數種子 init_chrom(); // 初始化種群 double * best_fit_index = min(fitness); int best_index =(int)(*best_fit_index); gbest = *(best_fit_index+1); // 最優值 gbest_index = 0; average_best[0] = sum(fitness)/sizepop; //初始平均最優值 for(int i=0;i<lenchrom;i++) gbest_pos[i] = chrom[best_index][i]; // 進化開始 for(int i=0;i<maxgen;i++) { Select(chrom); // 選擇 Cross(chrom); //交叉 Mutation(chrom); //變異 for(int j=0;j<sizepop;j++) { fitness[j] = fit_func(chrom[j]); } double sum_fit = sum(fitness); average_best[i+1] = sum_fit/sizepop; // 平均最優值 double * arr = min(fitness); double new_best = *(arr+1); int new_best_index = (int)(*arr); // 新最優值序號 if(new_best < gbest) { gbest = new_best; for(int j=0;j<lenchrom;j++) { gbest_pos[j] = chrom[new_best_index][j]; } gbest_index = i+1; } } finish = clock(); // 程序計算結束 double duration = ((double)(finish-start))/CLOCKS_PER_SEC; printf("程序計算耗時:%lf秒\n.",duration); printf("遺傳算法進化了%d次,最優值為:%lf,最優值在第%d代取得,此代的平均最優值為%lf.\n",maxgen,gbest,gbest_index,average_best[gbest_index]); printf("取得最優值的地方為(%lf,%lf,%lf,%lf,%lf).\n",gbest_pos[0],gbest_pos[1],gbest_pos[2],gbest_pos[3],gbest_pos[4]); return 0; }
說明:
(1)這裏是求函數的最小值,函數值越小的個體適應度越大,所以這裏采用倒數變換,即把適應度函數F(x)定義為1/f(x)(f(x)為目標函數值),這樣就保證了適應度函數F(x)越大,個體是越優的。(當然我們已經保證了f(x)始終是一個正數)
(2) 選擇操作采用的是經典的輪盤賭法,即每個個體被選為父代的概率與其適應度是成正比的,適應度越高,被選中的概率越大,設pi為第i個個體被選中的概率,則
pi=Fi/(∑Fi),Fi為第i個個體的適應度函數。
(3)交叉操作:由於個體采用實數編碼,所以交叉操作也采用實數交叉法,第k個染色體ak和第l個染色體al在第j位的交叉操作方法為:
akj=aij(1-b)+aijb; alj=alj(1-b)+akjb,其中b是[0,1]之間的隨機數。k,l,j都是隨機產生的,即隨機選取兩個父代,再隨機選取一個基因,按照公式完成交叉操作。
(4)變異操作:第i個個體的第j個基因進行變異的操作方法是:
r>=0.5時,aij=aij+(amax-aij)*f(g)
r<0.5時,aij=aij+(amin-aij)*f(g)
其中amax是基因aij的上界,amin是aij的下界,f(g)=r2*(1-g/Gmax)2,r2是一個[0,1]之間的隨機數,g是當前叠代次數,Gmax是進化最大次數,r是[0,1]之間的隨機數。
可以看出,當r>=0.5時,aij是在aij到amax之間變化的;r<0.5時,aij是在amin到aij之間變化的。這樣的函數其實可以構造不止一種,來完成變異的操作。只要符合隨機性,以及保證基因在有效的變化範圍內即可。
我在ubuntu16.04下,使用gcc編譯器運行得到的結果如下:
從結果上來看,種群數目為50,進化代數為500時,得到的最優解為2.014102已經非常接近實際的最優解2了,當然,如果把種群數目再變大比如500,進化代數變多比如1000代,那麽得到的結果將會更精確,種群數目為500,進化代數為1000時的最優解如下:
可以看出,增加種群數目以及增加進化代數之後,最優值從2.014102變為2.000189,比原來的精度提高了兩位,但是相應地,程序的運行時間也顯著增加了(從0.08秒左右增加到2秒左右)。所以在具體求解復雜問題的時候(比如有的時候會碰到函數參數有幾十個之多),我們就必須綜合考慮最優解的精度以及運算復雜度(運算時間)這兩個方面,權衡之後,取合適的參數進行問題的求解。
當然,這裏只是以求解多元非線性函數的極值問題為例來說明遺傳算法的具體實現步驟,遺傳算法的實際運用非常廣泛,不僅僅可以求解復雜函數極值,而且在運籌學等眾多領域有著非常廣泛的應用,也經常會和其他的智能算法或者優化算法結合來求解問題。這個後面我還會再較為詳細地論述。
遺傳算法的C語言實現(一):以非線性函數求極值為例