遺傳算法(Genetic Algorithm)又叫基因進化算法,或進化算法。屬於啓發式搜索算法一種,這個算法比較有趣,並且弄明白後很簡單,寫個100-200行代碼就可以實現。在某些場合下簡單有效。本文就花一些篇幅,儘量白話方式講解一下。
首先說一下問題。在我們學校數據結構這門功課的時候,時常會有一些比較經典的問題(而且比較複雜問題)作爲學習素材,如八皇后,揹包問題,染色問題等等。上面列出的幾個問題都可以通過遺傳算法去解決。本文列舉的問題是TSP(Traveling Salesman Problem)類的問題。
TSP問題實際上是”哈密頓迴路問題”中的”哈密頓最短迴路問題”.如下圖,就是要把下面8個城市不重複的全部走一遍。有點像小時候玩的畫筆畫遊戲,一筆到底不能重複。TSP不光是要求全部走一遍,並且是要求路徑最短。就是有可能全部走一遍有很多走法,要找出其中總路程最短的走法。
和這個問題有點相似的是歐拉回路(下圖)問題,它不是要求把每個點都走一遍,而是要求把每個邊都不重複走一遍(點可以重複),當然歐拉回路不是本算法研究的範疇。
本文會從TSP引申出下面系列問題
1、 TSP問題:要求每個點都遍歷到,而且要求每個點只被遍歷一次,並且總路程最短。
2、 最短路徑問題:要求從城市1 到城市8,找一條最短路徑。
3、 遍歷m個點,要求找出其距離最短的路線。(如果m=N總數,其實就是問題1了,所以問題1可以看成是問題3的特例 )。
遺傳算法的理論是根據達爾文進化論而設計出來的算法: 人類是朝着好的方向(最優解)進化,進化過程中,會自動選擇優良基因,淘汰劣等基因。
在上面tsp問題中,一個城市節點可以看成是一個基因,一個最優解就是一條路徑,包含若干個點。就類似一條染色體有若干基因組成一樣。所以求最短路徑問題,可以抽象成求最優染色體的問題。
遺傳算法很簡單,沒有什麼分支判斷,只有兩個大循環,流程大概如下
流程中有幾個關鍵元素:
1、 適度值評估函數。這個函數是算法的關鍵,就是對這個繁衍出來的後代進行評估打分,是優秀,還是一般,還是很差的畸形兒。用這個函數進行量化。在tsp中,路徑越短,分數越高。函數可以可以這樣 fitness = 1/total_distance. 或者 fitness = MAX_DISTANCE – total_distance. 不同的計算方法會影響算法的收斂速度,直接影響結果和性能。
2、 選擇運算規則: 又稱選擇算子。對應着達爾文理論中適者生存,也有地方叫着精英主義原則,意思就是隻有優秀的人才有更大的機率存活下來,擁有交配權。有權利擁有更多後代,傳承下自己血脈基因。和現實中很相像,皇帝權臣遺留下來的子孫後代比較多。選擇方法比較多。最常見的是round robin selection 算法,即輪盤賭算法, 這個算法比較簡單有效。選擇算法目前已有的有10來種之多。各種不同業務可以按需選擇。
選擇公式如下:
- //選擇運算---輪盤賭,此算法要求不能有負數.
- int32_t Genetic::Selection(Genome & selGenome)
- {
- //生成一個隨機浮點數
//本算法在輪盤賭算法上加上了選擇概率,提高最大可行解入圍概率 - double ftmp = (((random())%100001)/(100000 + 0.0000001));
- if( ftmp > 0.9 )
- {
- GetBestGenome(selGenome);
- return ESUCCESS;
- }
- //生成一個【0, m_dTotalFitness】之間的隨機浮點數
- double dRange = (((random()+ random())%100001)/(100000 + 0.0000001)) * m_dTotalFitness;
- double dCursor = 0.0;
- size_t i = 0;
-
- for(i = 0; i < m_vGenome.size(); ++i)
- {
- dCursor += m_vGenome[i].dFitness;
-
- if (dCursor > dRange)
- {
- break;
- }
- }
- selGenome = m_vGenome[i];
-
- return ESUCCESS;
- }
3、 交叉運算規則:又稱交配規則,交叉算子。對應遺傳學中的精子和卵子產生的受精卵含有精子的部分基因,也含有卵子的部分基因的現象。就像孩子有點像父親,又有點像母親的規律。交叉運算算法更多。作者可以天馬行空的自己去想象。只要達到交叉結果中含有父母的基因就可以。最常見的是k-opt 交換。其中k可以是 1,2,3….等等。簡稱單點交換,兩點交換,3點交換等等:
單點交換
其中修復重複基因根據業務需要看是否需要。
兩點交換
4、 變異運算規則:又叫變異算子。在人類遺傳進化過程中。會發生一些基因突變。這些突變有可能是好的突變,有可能是壞的突變。像癌細胞就是壞的突變。愛因斯坦的大腦估計是好的突變。突變方法也是可以天馬行空的自己去發揮創造。
這裏討論一下,爲什麼要有突變這道流程呢。從人類進化角度來說。人類基因有數十萬種,在遠古交流比較少的年代。都是部落內部通婚,但是整個部落內部居民可能都缺少某種好的基因,這樣無論他們怎麼交配,都不會產生好的基因,那麼他們需要引入好的基因,於是和其他部落通婚。引入其他自己沒有的基因,其實對於這個種羣來說這就是一次基因變異。如果是好的變異,那麼這個後代就很優秀,結果就是會產生更多子孫,把這個好的變異基因傳承下去,如果不是好的變異基因,自然而然會在前面選擇算子下淘汰,就是現實生活中的所謂的年幼夭折,癡呆無後,或先天畸形被淘汰,不會傳承下去。
從計算機算法角度看:所有的啓發式算法無外乎2種手段結合。局域搜索和全域搜索。局域搜索是在鄰域範圍內找出最優解。對應的是選擇算子和交叉算子。在自己部落裏面找最優秀的人。如果只有局域搜索的話,就容易陷入局域最優解。算法結果肯定是要找出全域最優解。這就要求跳出局域搜索。我們稱之爲“創新”。創新就是一次打破常規的突破——就是我們的“變異”算子。
這裏拿最短路徑路徑舉例子,求點1到點8之間的最短路徑, 初始解是1——2——3——6——8
內變異:所謂內變異就是在自己內部發生變異。嚴格來說其實不是一種變異。但是它又是一種比較有效的手段。
外變異:外變異是引入創新,突破傳統的質的飛躍, 也是啓發算法中所謂的全域搜索。下面是充當前基因中引入外部基因(當前集合的補集)。
結尾:遺傳算法除了上述這些幾個主要算子之外,還有一些細節。如交叉概率pc,變異概率pm,這些雖然都是輔助手段,但是有時候對整個算法結果和性能帶來截然不同的效果。這也是啓發式算法的一個缺點。參數需要不停的在實踐中摸索,沒有萬能的推薦參數。
還有細心的讀者可能發現幾個疑問,就是最短路徑中變異或交叉結果可能產生無效解,如前面最短路徑 1——6——3——2——8. 其中1和6之間根本沒有通路。碰到這種情況,可以拋棄這條非法解,重新生成一條隨機新解(其實這也是一次變異創新)。或者自己修復成可行解。反正框框在那裏。具體手段可以自己天馬行空發揮。
另一個比較實際的問題是:在最短路徑中並不知道染色體長度是多少,不錯。大部分人還是用定長染色體去解決問題,這樣性能低下。算法不直觀。這時候可以使用變長染色體來解決。其實我建議不管何種情況,都設計變長染色體模式。因爲定長也是變長的一種特例。使用變長可以解決任何問題。不管是tsp還是最短路徑問題。
還有一個編解碼問題,就是把現實問題轉換成基因,這些問題都比較容易解決,最簡單的就是直接用數組下標表示。
最後附上遺傳算法簡單參考程序MATLAB版本:
該程序是遺傳算法優化BP神經網絡函數極值尋優:
%% 該代碼爲基於神經網絡遺傳算法的系統極值尋優
%% 清空環境變量
clc
clear
%% 初始化遺傳算法參數
%初始化參數
maxgen=100; %進化代數,即迭代次數
sizepop=20; %種羣規模
pcross=[0.4]; %交叉概率選擇,0和1之間
pmutation=[0.2]; %變異概率選擇,0和1之間
lenchrom=[1 1]; %每個變量的字串長度,如果是浮點變量,則長度都爲1
bound=[-5 5;-5 5]; %數據範圍
individuals=struct('fitness',zeros(1,sizepop), 'chrom',[]); %將種羣信息定義爲一個結構體
avgfitness=[]; %每一代種羣的平均適應度
bestfitness=[]; %每一代種羣的最佳適應度
bestchrom=[]; %適應度最好的染色體
%% 初始化種羣計算適應度值
% 初始化種羣
for i=1:sizepop
%隨機產生一個種羣
individuals.chrom(i,:)=Code(lenchrom,bound);
x=individuals.chrom(i,:);
%計算適應度
individuals.fitness(i)=fun(x); %染色體的適應度
end
%找最好的染色體
[bestfitness bestindex]=min(individuals.fitness);
bestchrom=individuals.chrom(bestindex,:); %最好的染色體
avgfitness=sum(individuals.fitness)/sizepop; %染色體的平均適應度
% 記錄每一代進化中最好的適應度和平均適應度
trace=[avgfitness bestfitness];
%% 迭代尋優
% 進化開始
for i=1:maxgen
i
% 選擇
individuals=Select(individuals,sizepop);
avgfitness=sum(individuals.fitness)/sizepop;
%交叉
individuals.chrom=Cross(pcross,lenchrom,individuals.chrom,sizepop,bound);
% 變異
individuals.chrom=Mutation(pmutation,lenchrom,individuals.chrom,sizepop,[i maxgen],bound);
% 計算適應度
for j=1:sizepop
x=individuals.chrom(j,:); %解碼
individuals.fitness(j)=fun(x);
end
%找到最小和最大適應度的染色體及它們在種羣中的位置
[newbestfitness,newbestindex]=min(individuals.fitness);
[worestfitness,worestindex]=max(individuals.fitness);
% 代替上一次進化中最好的染色體
if bestfitness>newbestfitness
bestfitness=newbestfitness;
bestchrom=individuals.chrom(newbestindex,:);
end
individuals.chrom(worestindex,:)=bestchrom;
individuals.fitness(worestindex)=bestfitness;
avgfitness=sum(individuals.fitness)/sizepop;
trace=[trace;avgfitness bestfitness]; %記錄每一代進化中最好的適應度和平均適應度
end
%進化結束
%% 結果分析
[r c]=size(trace);
plot([1:r]',trace(:,2),'r-');
title('適應度曲線','fontsize',12);
xlabel('進化代數','fontsize',12);ylabel('適應度','fontsize',12);
axis([0,100,0,1])
disp('適應度 變量');
x=bestchrom;
% 窗口顯示
disp([bestfitness x]);