03-0003 Python遺傳算法解決旅行商問題

0.前言

物競天擇,適者生存
遺傳算法模擬生物進化的歷程,好的個體擁有好的基因,好的基因能夠在物競天擇的環境中被保留,環境一致的情況下,這些個體會越來越多。

1.介紹

1.1算法介紹

遺傳算法(Genetic Algorithm)是模擬達爾文生物進化論的自然選擇和遺傳學機理的生物進化過程的計算模型,是一種通過模擬自然進化過程搜索最優解的方法。

遺傳算法是從代表問題可能潛在的解集的一個種羣(population)開始的,而一個種羣則由經過基因(gene)編碼的一定數目的個體(individual)組成。

每個個體實際上是染色體(chromosome)帶有特徵的實體。染色體作爲遺傳物質的主要載體,即多個基因的集合,其內部表現(即基因型)是某種基因組合,它決定了個體的形狀的外部表現,如黑頭髮的特徵是由染色體中控制這一特徵的某種基因組合決定的。

因此,在一開始需要實現從表現型到基因型的映射即編碼工作。由於仿照基因編碼的工作很複雜,我們往往進行簡化,如二進制編碼,初代種羣產生之後,按照適者生存和優勝劣汰的原理,逐代(generation)演化產生出越來越好的近似解,在每一代,根據問題域中個體的適應度(fitness)大小選擇(selection)個體,並藉助於自然遺傳學的遺傳算子(genetic operators)進行組合交叉(crossover)和變異(mutation),產生出代表新的解集的種羣。

這個過程將導致種羣像自然進化一樣的後生代種羣比前代更加適應於環境,末代種羣中的最優個體經過解碼(decoding),可以作爲問題近似最優解。

以上描述來自於百度,屬於官方描述,其中起牀算法是搜索最優解,而不是直接計算得到最優解。

1.2問題介紹

旅行商問題描述:給定一系列城市和每對城市之間的距離,求解訪問每一座城市一次並回到起始城市的最短迴路。
它是組合優化中的一個NP困難問題,在運籌學和理論計算機科學中非常重要。

本次實驗選擇的是國內的有限個城市,如下圖:
在這裏插入圖片描述

2.流程

如同基因遺傳一般,遺傳算法的過程包括:選擇,交叉,變異三個過程,通過多次迭代,可以逐漸收斂於最優解,但是並不能保證最後一定會得到最優解
理解遺傳算法並不難,困難在代碼的實現

2.1流程圖

在這裏插入圖片描述

流程圖是根據代碼繪製的,其實可以更簡單一些,該算法無非就是選擇交叉變異,然後多次迭代,在自己期望的最後一次迭代末尾,輸入最優的一個解。

2.2選擇過程

選擇過程有不同的選擇策略,在課堂上,老師只講解了比例選擇、輪盤賭兩種方法。

2.2.1輪盤賭選擇法

描述:
輪盤賭選擇法是依據個體的適應度值計算每個個體在子代中出現的概率,並按照此概率隨機選擇個體構成子代種羣。輪盤賭選擇策略的出發點是適應度值越好的個體被選擇的概率越大。因此,在求解最大化問題的時候,我們可以直接採用適應度值來進行選擇。但是在求解最小化問題的時候,我們必須首先將問題的適應度函數進行轉換,以將問題轉化爲最大化問題。
在這裏插入圖片描述

步驟:
1.調整適應度,確保自己想要的最優結果的適應度是最大值,以保證其在選擇區間上佔據最大的比例。[一般可以去倒數。]
2.將種羣的適應度疊加,累次求和,在實操時,需要累次記錄。最終的適應度總值爲1。
3.計算每個個體的累積概率,以此構造一個輪盤。[也可以不計算累積概率,改用搜索算法來確定得到的隨機數是在哪一個範圍之內,屬於哪個個體。]
4.選擇。隨機產生一個[0,1]的隨機數,如果是累積概率,則可以直接判斷是哪個個體,選擇即可,如果不是累積概率,則要多一步操作。
5.重複以上步驟多次,得到所需的個體數量即可。

第三個步驟所描述的搜索算法:

# 插值搜索
# 輸入:列表,要搜索的值,最低索引,最高索引
# 輸出:相等時的索引,或者接近該值左側的索引
# 備註:Python裏面的遞歸有次數限制(99次)
def InsertionSearch(list,value,low,high):
    mid=low+int((value-list[low])/(list[high]-list[low])*(high-low))
    if list[mid]==value:
        return mid
    if list[mid]>value:
        if list[mid-1]<value:
            return mid-1
        else:
            return InsertionSearch(list,value,low,mid-1)
    if list[mid]<value:
        if list[mid+1]>value:
            return mid
        else:
            return InsertionSearch(list,value,mid+1,high)

2.2.2比例選擇法

描述:
選擇所有個體中適應度佔前60%的個體作爲優秀個體加入下一代,其他的個體沒有交配的權利,直接捨棄。
步驟:
1.將羣體進行排序。
2.按照自己設置的適應度升序或者降序,取前60%以保留。
3.從這60%的個體裏面隨機複製個體,填補丟棄的40%個體。
4.每一輪操作一次。

2.2.3精英保留策略

描述:
精英個體,不老不死,只能被取代,不能遺傳。
步驟:
在其他選擇策略的基礎之上,保留最優的個體,保證每每一輪這個個體都會被留下來,用臨時變量保存,或者是不進行選擇交叉變異,只要能留下,即可。
[這裏有一種賭的思想,只要最優解出現,就一定會被保留,就能夠得到正確結果,但是也有可能最優解在遺傳過程之中根本就沒有出現過。]

2.3交叉過程

交叉過程是說任意任意兩個個體交換基因段的過程。

在這裏插入圖片描述
在上述過程中,說是要交叉兩端的,檢測兩端的是否有重複,有重複就要與重複位置的對應位置互換,直到沒有重複爲止。
爲了方便代碼實現,將中間部分進行交換,作爲循環的標尺,每次對兩端進行檢測,直到沒有重複爲止。

2.3變異

自我感覺變異過程是爲了添加噪聲。

描述:對於一個基因段的某個兩個位置進行變異,也就是改變數值,但是需要保證變異之後一個基因內沒有重複個體。
所以,比較好的方法直接選擇兩個位置進行交換。

備註:關於適應度的計算,這裏就不作介紹了,不同的實驗有不同的適應度,這也是解題的關鍵,倘若發現某個問題的適應度不會計算,那麼這個時候就應該放下自己所思考的其他東西,首先弄明白適應度的計算公式。

3.源碼

源碼是可以直接運行的,複製粘貼保存即可。
自我感覺備註寫的很完整,也很全面,是比較同意能夠看懂的。
實驗中調用了numpy的庫,不懂的可以去看一看,它對於處理矩陣相關的東西很在行。

import numpy as np
import math
import random

# 適應度
def fitnessFunction(pop,num,city_num,distance):
    length=city_num
    for i in range(num):
        dis=0
        for j in range(length-1):
            dis+=distance[int(pop[i][j])][int(pop[i][j+1])]
        dis+=distance[int(pop[i][j+1])][int(pop[i][0])]
        pop[i][-1]=20000/dis

# 選擇
def choiceFuction(pop):
    numlist=[0]
    length=len(pop)
    allnum=0
    for i in range(0,length):
        allnum+=pop[i][-1]
        numlist.append(allnum)

    temppop=[]                                                      #用於臨時的種羣數據
    max = pop[np.argmax(pop[:, -1])]                                #最優的一個個體
    for i in range(0,length):
        rannum=random.random()*numlist[-1]                          #0~1的小數乘上最大的數字,得到[0,max]上的一個點
        ranIndex=InsertionSearch(numlist,rannum,0,length-1)         #調用搜索函數
        temppop.append(pop[ranIndex-1])                             #將一個個的數據添加到
    pop=np.array(temppop)                                           #將pop索引指向臨時數據,並且此處應該清理原來的pop空間纔對
    pop[0]=max.copy()                                               #保證第一個元素就是最優的元素
    
    return pop
    
# 插值搜索
# 輸入:列表,要搜索的值,最低索引,最高索引
# 輸出:相等時的索引,或者接近該值左側的索引
# 備註:Python裏面的遞歸有次數限制(99次)
def InsertionSearch(list,value,low,high):
    mid=low+int((value-list[low])/(list[high]-list[low])*(high-low))
    if list[mid]==value:
        return mid
    if list[mid]>value:
        if list[mid-1]<value:
            return mid-1
        else:
            return InsertionSearch(list,value,low,mid-1)
    if list[mid]<value:
        if list[mid+1]>value:
            return mid
        else:
            return InsertionSearch(list,value,mid+1,high)

# 交叉變異(此處的交叉有兩種情況)
# 輸入:種羣,交叉概率,城市數量(列),變異概率,種羣數量(行)
# 輸出:無輸出,是一個過程(先交叉,再變異)
# 備註:不同的交叉策略,相鄰,隨機,配對
def matuingFuction(pop, pc, city_num, pm, num):
    #產生兩個不同的索引
    rancon=True
    while rancon :
        p1=random.randint(1,city_num-1)
        p2=random.randint(0,city_num-2)
        if p1==p2:
            rancon=True
        elif p1<p2:
            rancon=False
        else:
            temp=p1
            p1=p2
            p2=temp
        pass

    # 交叉,變異(每一對交換,1 and 2, 3 and 4, and so on)
    # for i in range(1,num-1,2):
    #     # for j in range(0,num-1):
    #     #     if i!=j:
    #     #         pc2=random.random()
    #     #         if(pc2<pc):
    #     #             matuting(pop[i],pop[j],p1,p2)
    #     pc2=random.random()
    #     if pc2<pc:
    #         matuting(pop[i],pop[i+1],p1,p2)
    #     # print("poobefore:",pop[i])
    #     variationFunction(pop[i],pm,city_num)
    #     # print("pooend:",pop[i])
    #     variationFunction(pop[i+1],pm,city_num)

    # 交叉,變異(相鄰兩個進行交換)
    for i in range(1,num-1):
        pc2=random.random()
        if pc2<pc:
            matuting(pop[i],pop[i+1],p1,p2)
        variationFunction(pop[i],pm,city_num)

# 交叉過程
# 輸入:第一行數據x1,第二行數據x2,左側斷點,右側斷點
# 輸出:驚醒過交叉的兩行數據
# 備註:由於函數是單次檢測,所以需要調用兩次內部函數
def matuting(x1, x2, p1, p2):
    length=len(x1)-1
    temp=x1[p1:p2].copy()
    x1[p1:p2]=x2[p1:p2]
    x2[p1:p2]=temp

    #自定義交換函數,需要調用兩次纔能夠將其完全的排列好
    def change(x1,x2,p1,p2):
        #檢查元素
        isrepeat=True
        while isrepeat:
            countTrue=0
            for i in range(p1,p2):
                for j in range(0,p1):
                    if int(x1[j])==int(x1[i]):
                        isrepeat=True
                        countTrue+=1
                        x1[j]=x2[i]
                for k in range(p2,length):
                    if int(x1[k])==int(x1[i]):
                        isrepeat=True
                        countTrue+=1
                        x1[k]=x2[i]
            if countTrue==0:
                isrepeat=False
    
    #調用交換函數,兩次
    change(x1,x2,p1,p2)
    change(x2,x1,p1,p2)   

# 變異過程
# 輸入:一行數據,變異概率,城市數量
# 輸出:行內交換元素的一行數據
# 備註:似乎數據交換不需要定義臨時變量
def variationFunction(list_a, pm, city_num):
    if random.random()<pm:
        i=random.randint(0,city_num-1)
        isequal=True
        while isequal:
            j=random.randint(0,city_num-1)
            if(j==i):   isequal=True
            else:       isequal=False
            pass
        #交換兩個數據
        list_a[i],list_a[j]=list_a[j],list_a[i]         

# 主函數
def main():
    # 初始化
    pop = []                      # 存放訪問順序和每個個體適應度@
    num = 700                     # 初始化羣體的數目@
    city_num = 10                 # 城市數目@
    pc = 0.9                      # 每個個體的交配概率S@
    pm = 0.3                      # 每個個體的變異概率@
    dis="0,118,1272,2567,1653,2097,1425,1177,3947,1574,118,0,1253,2511,1633,2077,1369,1157,3961,1518,1272,1253,0,1462,380,1490,821,856,3660,385,2567,2511,1462,0,922,2335,1562,2165,3995,933,1653,1633,380,922,0,1700,1041,1135,3870,456,2097,2077,1490,2335,1700,0,2311,920,2170,1920,1425,1369,821,1562,1041,2311,0,1420,4290,626,1177,1157,856,2165,1135,920,1420,0,2870,1290,3947,3961,3660,3995,3870,2170,4290,2870,0,4090,1574,1518,385,993,456,1920,626,1290,4090,0"
    # #獲取輸入的十個城市之間距離,並將它們轉化爲numpy的array,並重新reshape成10*10的二維數組distance@
    # distance1 =  input().split(",")
    distance1=dis.split(",")
    distance2 = np.array(distance1,dtype=np.int)
    distance = distance2.reshape((10, 10))    

    for i in range(num):
        pop.append(np.random.permutation(np.arange(0,city_num)))    # 假設有10個城市,初始羣體的數目500個@

    zero = np.zeros((num,1))                                        # 返回給定形狀和類型的新數組,用零填充。二維數組,num行2列@
    pop = np.column_stack((pop, zero))                              # 矩陣的拼接@
    fitnessFunction(pop, num, city_num, distance)                   # 爲初始種羣賦予自適應度@
    pop=choiceFuction(pop)

    # 遺傳算法迭代,250爲迭代次數,同學們可以對其進行調整@
    for i in range(250):
        matuingFuction(pop,pc,city_num,pm,num)                      # 交叉變異
        fitnessFunction(pop,num,city_num,distance)                  # 計算適應度
        pop=choiceFuction(pop)                                      # 選擇

    #輸出自適應度最大解個體並計算其路徑長度@
    max = pop[np.argmax(pop[:, -1])]
    sum = 0
    for x2 in range(city_num - 1):
       sum += distance[int(max[x2])][int(max[x2 + 1])]
    sum += distance[int(max[9])][int(max[0])]
    print(sum)

# 運行函數
if __name__=="__main__":
    main()

# 值類型:int, float, bool, str, tuple
# 引用類型:list, dict, set

4.結果

結果很簡單,就是一個最優解,這個結果不是很穩定,大部分時候會輸出12055,有的時候也會輸出12062,看命吧
如果想要更仔細的觀察遺傳的過程,可以在每次選擇的時候輸出此最好的適應度。
如果想要更精確的得到結果,可以調整參數或者是修改選擇策略,也可以調整適應度的計算公式。

在這裏插入圖片描述

5.總結

打字看心情,突然心情不好不知道作何總結。
此次遺傳算法實驗是針對於全連通圖的旅行商問題的一種解決策略,將一個完整迴路的距離的相關函數作爲實用度,將各個城市作爲基因,將每一種迴路作爲一個個體,模擬遺傳進化,最終趨向最優解或者就是最優解。

遺傳算法的不足之處

  1. 遺傳算法的效率會比其他傳統的優化方法低。
  2. 遺傳算法容易過早的收斂。[我以爲只這是局部最優化問題 ]
  3. 對於算法精度、可行度、計算複雜性等方面沒有有效的定量分析方法。

一些學習遺傳算法的鏈接:

  1. 旅行商(TSP)問題專題——多種方法對比
  2. 【算法】超詳細的遺傳算法(Genetic Algorithm)解析
  3. 遺傳算法詳解(GA)(個人覺得很形象,很適合初學者)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章