人工智能結課作業-遺傳算法/粒子群尋優/蟻群算法解決TSP問題


代碼已經發布到了github:https://github.com/roadwide/AI-Homework

如果幫到你了,希望給個star鼓勵一下

1 遺傳算法

1.1算法介紹

遺傳算法是模仿自然界生物進化機制發展起來的隨機全局搜索和優化方法,它借鑒了達爾文的進化論和孟德爾的遺傳學說。其本質是一種高效、並行、全局搜索的方法,它能在搜索過程中自動獲取和積累有關搜索空間的知識,並自適應的控制搜索過程以求得最優解。遺傳算法操作使用適者生存的原則,在潛在的解決方案種群中逐次產生一個近似最優解的方案,在遺傳算法的每一代中,根據個體在問題域中的適應度值和從自然遺傳學中借鑒來的再造方法進行個體選擇,產生一個新的近似解。這個過程導致種群中個體的進化,得到的新個體比原來個體更能適應環境,就像自然界中的改造一樣。

遺傳算法具體步驟:

1)初始化:設置進化代數計數器t=0、設置最大進化代數T、交叉概率、變異概率、隨機生成M個個體作為初始種群P

2)個體評價:計算種群P中各個個體的適應度

3)選擇運算:將選擇算子作用於群體。以個體適應度為基礎,選擇最優個體直接遺傳到下一代或通過配對交叉產生新的個體再遺傳到下一代

4)交叉運算:在交叉概率的控制下,對群體中的個體兩兩進行交叉

5)變異運算:在變異概率的控制下,對群體中的個體進行變異,即對某一個體的基因進行隨機調整

6經過選擇、交叉、變異運算之后得到下一代群體P1

重復以上(1-6),直到遺傳代數為 T,以進化過程中所得到的具有最優適應度個體作為最優解輸出,終止計算。

旅行推銷員問題(Travelling Salesman Problem TSP):有n個城市,一個推銷員要從其中某一個城市出發,唯一走遍所有的城市,再回到他出發的城市,求最短的路線。

應用遺傳算法求解TSP問題時需要進行一些約定,基因是一組城市序列,適應度是按照這個基因的城市順序的距離和分之一。

1.2實驗代碼

 

import random
import math
import matplotlib.pyplot as plt
#讀取數據
f=open("test.txt")
data=f.readlines()
#將cities初始化為字典,防止下面被當成列表
cities={}
for line in data:
    #原始數據以\n換行,將其替換掉
    line=line.replace("\n","")
    #最后一行以EOF為標志,如果讀到就證明讀完了,退出循環
    if(line=="EOF"):
        break
    #空格分割城市編號和城市的坐標
    city=line.split(" ")
    map(int,city)
    #將城市數據添加到cities中
    cities[eval(city[0])]=[eval(city[1]),eval(city[2])]

#計算適應度,也就是距離分之一,這里用偽歐氏距離
def calcfit(gene):
    sum=0
    #最后要回到初始城市所以從-1,也就是最后一個城市繞一圈到最后一個城市
    for i in range(-1,len(gene)-1):
        nowcity=gene[i]
        nextcity=gene[i+1]
        nowloc=cities[nowcity]
        nextloc=cities[nextcity]
        sum+=math.sqrt(((nowloc[0]-nextloc[0])**2+(nowloc[1]-nextloc[1])**2)/10)

    return 1/sum

#每個個體的類,方便根據基因計算適應度
class Person:
    def __init__(self,gene):
        self.gene=gene
        self.fit=calcfit(gene)
class Group:
    def __init__(self):
        self.GroupSize=100  #種群規模
        self.GeneSize=48    #基因數量,也就是城市數量
        self.initGroup()
        self.upDate()
    #初始化種群,隨機生成若干個體
    def initGroup(self):
        self.group=[]
        i=0
        while(i<self.GroupSize):
            i+=1
            #gene如果在for以外生成只會shuffle一次
            gene=[i+1 for i in range(self.GeneSize)]
            random.shuffle(gene)
            tmpPerson=Person(gene)
            self.group.append(tmpPerson)

    #獲取種群中適應度最高的個體
    def getBest(self):
        bestFit=self.group[0].fit
        best=self.group[0]
        for person in self.group:
            if(person.fit>bestFit):
                bestFit=person.fit
                best=person
        return best
    #計算種群中所有個體的平均距離
    def getAvg(self):
        sum=0
        for p in self.group:
            sum+=1/p.fit
        return sum/len(self.group)
    #根據適應度,使用輪盤賭返回一個個體,用於遺傳交叉
    def getOne(self):
        #section的簡稱,區間
        sec=[0]
        sumsec=0
        for person in self.group:
            sumsec+=person.fit
            sec.append(sumsec)
        p=random.random()*sumsec
        for i in range(len(sec)):
            if(p>sec[i] and p<sec[i+1]):
                #這里注意區間是比個體多一個0的
                return self.group[i]
    #更新種群相關信息
    def upDate(self):
        self.best=self.getBest()
#遺傳算法的類,定義了遺傳、交叉、變異等操作
class GA:
    def __init__(self):
        self.group=Group()
        self.pCross=0.35    #交叉率
        self.pChange=0.1    #變異率
        self.Gen=1  #代數

    #變異操作
    def change(self,gene):
        #把列表隨機的一段取出然后再隨機插入某個位置
        #length是取出基因的長度,postake是取出的位置,posins是插入的位置
        geneLenght=len(gene)
        index1 = random.randint(0, geneLenght - 1)
        index2 = random.randint(0, geneLenght - 1)
        newGene = gene[:]       # 產生一個新的基因序列,以免變異的時候影響父種群
        newGene[index1], newGene[index2] = newGene[index2], newGene[index1]
        return newGene

    #交叉操作
    def cross(self,p1,p2):
        geneLenght=len(p1.gene)
        index1 = random.randint(0, geneLenght - 1)
        index2 = random.randint(index1, geneLenght - 1)
        tempGene = p2.gene[index1:index2]   # 交叉的基因片段
        newGene = []
        p1len = 0
        for g in p1.gene:
              if p1len == index1:
                    newGene.extend(tempGene)     # 插入基因片段
                    p1len += 1
              if g not in tempGene:
                    newGene.append(g)
                    p1len += 1
        return newGene

    #獲取下一代
    def nextGen(self):
        self.Gen+=1
        #nextGen代表下一代的所有基因
        nextGen=[]
        #將最優秀的基因直接傳遞給下一代
        nextGen.append(self.group.getBest().gene[:])
        while(len(nextGen)<self.group.GroupSize):
            pChange=random.random()
            pCross=random.random()
            p1=self.group.getOne()
            if(pCross<self.pCross):
                p2=self.group.getOne()
                newGene=self.cross(p1,p2)
            else:
                newGene=p1.gene[:]
            if(pChange<self.pChange):
                newGene=self.change(newGene)
            nextGen.append(newGene)
        self.group.group=[]
        for gene in nextGen:
            self.group.group.append(Person(gene))
            self.group.upDate()

    #打印當前種群的最優個體信息
    def showBest(self):
        print("第{}代\t當前最優{}\t當前平均{}\t".format(self.Gen,1/self.group.getBest().fit,self.group.getAvg()))

    #n代表代數,遺傳算法的入口
    def run(self,n):
        Gen=[]  #代數
        dist=[] #每一代的最優距離
        avgDist=[]  #每一代的平均距離
        #上面三個列表是為了畫圖
        i=1
        while(i<n):
            self.nextGen()
            self.showBest()
            i+=1
            Gen.append(i)
            dist.append(1/self.group.getBest().fit)
            avgDist.append(self.group.getAvg())
        #繪制進化曲線
        plt.plot(Gen,dist,'-r')
        plt.plot(Gen,avgDist,'-b')
        plt.show()

ga=GA()
ga.run(3000)
print("進行3000代后最優解:",1/ga.group.getBest().fit)

 

1.3實驗結果

下圖是進行一次實驗的結果截圖,求出的最優解是11271

為避免實驗的偶然性,進行10次重復實驗,並求平均值,結果如下。

上圖橫坐標是代數,縱坐標是距離,紅色曲線是每一代的最優個體的距離,藍色曲線是每一代的平均距離。可以看出兩條線都呈下降趨勢,也就是說都在進化。平均距離下降說明由於優良基因的出現(也就是某一段城市序列),使得這種優良的性狀很快傳播到整個群體。就像自然界中的優勝劣汰一樣,具有適應環境的基因才能生存下來,相應的,生存下來的都是具有優良基因的。算法中引入交叉率和變異率的意義就在於既要保證當前優良基因,又要試圖產生更優良的基因。如果所有個體都交叉,那么有些優良的基因片段可能會丟失;如果都不交叉,那么兩個優秀的基因片段無法組合為更優秀的基因;如果沒有變異,那就無法產生更適應環境的個體。不得不感嘆自然的智慧是如此強大。

上面說到的基因片段就是TSP中的一小段城市序列,當某一段序列的距離和相對較小時,就說明這段序列是這幾個城市的相對較好的遍歷順序。遺傳算法通過將這些優秀的片段組合起來實現了TSP解的不斷優化。而組合的方法正是借鑒自然的智慧,遺傳、變異、適者生存。

1.4實驗總結

1、如何在算法中實現"優勝劣汰"?

所謂優勝劣汰也就是優良的基因保留,不適應環境的基因淘汰。在上述GA算法中,我使用的是輪盤賭,也就是在遺傳的步驟中(無論是否交叉),根據每個個體的適應度來挑選。這樣就能達到適應度高得個體有更多的后代,也就達到了優勝劣汰的目的。

在具體的實現過程中,我犯了個錯誤,起初在遺傳步驟篩選個體時,我每選出一個個體就將這個個體從群體中刪除。現在想想,這種做法十分愚蠢,盡管當時我已經實現了輪盤賭,但如果選出個體就刪除,那么就會導致每個個體都會平等地生育后代,所謂的輪盤賭也不過是能讓適應度高的先進行遺傳。這種做法完全背離了"優勝劣汰"的初衷。正確的做法是選完個體進行遺傳后再重新放回群體,這樣才能保證適應度高的個體會進行多次遺傳,產生更多后代,將優良的基因更廣泛的播撒,同時不適應的個體會產生少量后代或者直接被淘汰。

2 、如何保證進化一直是在正向進行?

所謂正向進行也就是下一代的最優個體一定比上一代更適應或者同等適應環境。我采用的方法是最優個體直接進入下一代,不參與交叉變異等操作。這樣能夠防止因這些操作而"污染"了當前最優秀的基因而導致反向進化的出現。

我在實現過程中還出現了另一點問題,是傳引用還是傳值所導致的。對個體的基因進行交叉和變異時用的是一個列表,Python中傳列表時傳的實際上是一個引用,這樣就導致個體進行交叉和變異后會改變個體本身的基因。導致的結果就是進化非常緩慢,並且伴隨反向進化。

3、交叉如何實現?

選定一個個體的片段放入另一個體,並將不重復的基因的依次放入其他位置。

在實現這一步時,因為學生物時對真實染色體行為的固有認識,"同源染色體交叉互換同源區段",導致我錯誤實現該功能。我只將兩個個體的相同位置的片段互換來完成交叉,顯然這樣的做法是錯誤的,這會導致城市的重復出現。

4、在剛開始寫這個算法時,我是半OOP,半面向過程地寫。后續測試過程中發現要改參數,更新個體信息時很麻煩,於是全部改為OOP,然后方便多了。對於這種模擬真實世界的問題,OOP有很大的靈活性和簡便性。

5、如何防止出現局部最優解?

在測試過程中發現偶爾會出現局部最優解,在很長時間內不會繼續進化,而此時的解又離最優解較遠。哪怕是后續調整后,盡管離最優解近了,但依然是"局部最優",因為還沒有達到最優。

算法在起初會收斂得很快,而越往后就會越來越慢,甚至根本不動。因為到后期,所有個體都有着相對來說差不多的優秀基因,這時的交叉對於進化的作用就很弱了,進化的主要動力就成了變異,而變異就是一種暴力算法了。運氣好的話能很快變異出更好的個體,運氣不好就得一直等。

防止局部最優解的解決方法是增大種群規模,這樣就會有更多的個體變異,就會有更大可能性產生進化的個體。而增大種群規模的弊端是每一代的計算時間會變長,也就是說這兩者是相互抑制的。巨大的種群規模雖然最終能避免局部最優解,但是每一代的時間很長,需要很長時間才能求出最優解;而較小的種群規模雖然每一代計算時間快,但在若干代后就會陷入局部最優。

猜想一種可能的優化方法,在進化初期用較小的種群規模,以此來加快進化速度,當適應度達到某一閾值后,增加種群規模和變異率來避免局部最優解的出現。用這種動態調整的方法來權衡每一代計算效率和整體計算效率之間的平衡。

2 粒子群尋優

2.1算法介紹

粒子群算法(particle swarm optimizationPSO)的思想源於對鳥/魚群捕食行為的研究,模擬鳥集群飛行覓食的行為,鳥之間通過集體的協作使群體達到最優目的。

粒子群尋優算法作以下假設:

每個尋優的問題解都被想像成一只鳥,稱為"粒子"。所有粒子都在一個D維空間進行搜索。

所有的粒子都由一個fitness function 確定適應值以判斷目前的位置好壞。

每一個粒子必須賦予記憶功能,能記住所搜尋到的最佳位置。

每一個粒子還有一個速度以決定飛行的距離和方向。這個速度根據它本身的飛行經驗以及同伴的飛行經驗進行動態調整。

傳統的粒子群尋優算法的位置更新公式如下

每一只鳥會根據自身速度慣性、自身最佳位置和群體最佳位置來決定下一時刻的速度(包括大小和方向),並根據速度來更新位置。

當應用PSO來解決TSP問題時需要進行一些改進,將多維的城市列表信息轉換為一種坐標信息,並在此基礎上定義相應的速度、加速度等。這些研究在卞鋒的《粒子群優化算法在TSP中的研究及應用》和《求解TSP的改進QPSO算法》兩篇文章中有詳細介紹。其中最重要的一個概念是交換序,相當於傳統PSO中的速度。在進行了這些改進之后就可以將PSO算法應用於TSP問題中了。

2.2實驗代碼

 

import random
import math
import matplotlib.pyplot as plt
#讀取數據
f=open("test.txt")
data=f.readlines()
#將cities初始化為字典,防止下面被當成列表
cities={}
for line in data:
    #原始數據以\n換行,將其替換掉
    line=line.replace("\n","")
    #最后一行以EOF為標志,如果讀到就證明讀完了,退出循環
    if(line=="EOF"):
        break
    #空格分割城市編號和城市的坐標
    city=line.split(" ")
    map(int,city)
    #將城市數據添加到cities中
    cities[eval(city[0])]=[eval(city[1]),eval(city[2])]
#計算適應度,也就是距離分之一,這里用偽歐氏距離
def calcfit(addr):
    sum=0
    for i in range(-1,len(addr)-1):
        nowcity=addr[i]
        nextcity=addr[i+1]
        nowloc=cities[nowcity]
        nextloc=cities[nextcity]
        sum+=math.sqrt(((nowloc[0]-nextloc[0])**2+(nowloc[1]-nextloc[1])**2)/10)
    #最后要回到初始城市
    return 1/sum

#生成交換序的函數,交換后數組b變為a,也就是a-b的結果
def switchB2A(a,b):
    #防止傳進來的b被更改
    tmpb=b[:]
    q=[]
    for i in range(len(a)):
        if(a[i]!=tmpb[i]):
            j=b.index(a[i])
            q.append([i,j])
            #剛學的簡潔的交換list的方法
            tmpb[j],tmpb[i]=tmpb[i],tmpb[j]
    return q

#w*v,w是一個數,v是一個數組。w乘v的數組長度,然后對結果取整,取數組的前這么多個元素
def multiply(w,v):
    l=int(w*len(v))
    res=v[0:l]
    return res

#鳥個體的類,實現鳥位置的移動
class Bird:
    def __init__(self,addr):
        self.addr=addr
        self.v=0
        #初始化時自己曾遇到得最優位置就是初始化的位置
        self.bestAddr=addr
        #初始狀態沒有速度
        self.v=[]
        self.fit=calcfit(self.addr)
        self.bestFit=self.fit

    #根據交換序移動位置
    def switch(self,switchq):
        for pair in switchq:
            i,j=pair[0],pair[1]
            self.addr[i],self.addr[j]=self.addr[j],self.addr[i]
        #交換后自動更行自己的成員變量
        self.upDate()

    #更新鳥自身相關信息
    def upDate(self):
        newfit=calcfit(self.addr)
        self.fit=newfit
        if(newfit>self.bestFit):
            self.bestFit=newfit
            self.bestAddr=self.addr

    #變異操作
    #設置變異后避免了所有鳥都聚集到一個離食物近,但又不是最近的地方,並且就停在那里不動了
    def change(self):
        i,j=random.randrange(0,48),random.randrange(0,48)
        self.addr[i],self.addr[j]=self.addr[j],self.addr[i]
        self.upDate()
    #貪婪倒立變異
    def reverse(self):
        #隨機選擇一個城市
        cityx=random.randrange(1,49)
        noxcity=self.addr[:]
        noxcity.remove(cityx)
        maxFit=0
        nearCity=noxcity[0]
        for c in noxcity:
            fit=calcfit([c,cityx])
            if(fit>maxFit):
                maxFit=fit
                nearCity=c
        index1=self.addr.index(cityx)
        index2=self.addr.index(nearCity)
        tmp=self.addr[index1+1:index2+1]
        tmp.reverse()
        self.addr[index1+1:index2+1]=tmp
        self.upDate()

#種群的類,里面有很多鳥
class Group:
    def __init__(self):
        self.groupSize=500  #鳥的個數、粒子個數
        self.addrSize=48    #位置的維度,也就是TSP城市數量
        self.w=0.25 #w為慣性系數,也就是保留上次速度的程度
        self.pChange=0.1    #變異系數pChange
        self.pReverse=0.1   #貪婪倒立變異概率
        self.initBirds()
        self.best=self.getBest()
        self.Gen=0
    #初始化鳥群
    def initBirds(self):
        self.group=[]
        for i in range(self.groupSize):
            addr=[i+1 for i in range(self.addrSize)]
            random.shuffle(addr)
            bird=Bird(addr)
            self.group.append(bird)
    #獲取當前離食物最近的鳥
    def getBest(self):
        bestFit=0
        bestBird=None
        #遍歷群體里的所有鳥,找到路徑最短的
        for bird in self.group:
            nowfit=calcfit(bird.addr)
            if(nowfit>bestFit):
                bestFit=nowfit
                bestBird=bird
        return bestBird
    #返回所有鳥的距離平均值
    def getAvg(self):
        sum=0
        for p in self.group:
            sum+=1/p.fit
        return sum/len(self.group)
    #打印最優位置的鳥的相關信息
    def showBest(self):
        print(self.Gen,":",1/self.best.fit)
    #更新每一只鳥的速度和位置
    def upDateBird(self):
        self.Gen+=1
        for bird in self.group:
            #g代表group,m代表me,分別代表自己和群組最優、自己最優的差
            deltag=switchB2A(self.best.addr,bird.addr)
            deltam=switchB2A(bird.bestAddr,bird.addr)
            newv=multiply(self.w,bird.v)[:]+multiply(random.random(),deltag)[:]+multiply(random.random(),deltam)
            bird.switch(newv)
            bird.v=newv
            if(random.random()<self.pChange):
                bird.change()
            if(random.random()<self.pReverse):
                bird.reverse()
            #順便在循環里把最優的鳥更新了,防止二次遍歷
            if(bird.fit>self.best.fit):
                self.best=bird

Gen=[]  #代數
dist=[] #距離
avgDist=[]  #平均距離
#上面三個列表是為了畫圖
group=Group()
i=0
#進行若干次迭代
while(i<500):
    i+=1
    group.upDateBird()
    group.showBest()
    Gen.append(i)
    dist.append(1/group.getBest().fit)
    avgDist.append(group.getAvg())
#將過程可視化
plt.plot(Gen,dist,'-r')
plt.plot(Gen,avgDist,'-b')
plt.show()

 

2.3實驗結果

下面是進行500次迭代后的結果,求出的最優解是11198

為避免計算過程的偶然性,下面進行10次重復實驗並求平均值。

上圖橫坐標是迭代次數,縱坐標是距離,紅色曲線是每次迭代最接近食物的鳥(也就是本次迭代的最優解,食物的位置也就是最優解城市序列所構成的坐標),藍色曲線是每次迭代所有鳥的平均距離。可以看出不同於遺傳算法,最初的最優解具有波動性,並不是一直下降的(遺傳算法之所以一直下降是因為每次都保留的最優個體直接傳到下一代)出現這種情況的原因是,最開始階段所有鳥都是隨機分散的,大家離食物的距離都差不多,就算是距離食物最近的鳥其能提供的信息的參考價值也不大。所以在開始的一段時間內最優位置的鳥在波動,而到后期,當食物位置更加確定之后,其波動性就消失了。

從趨勢來看,無論是每次迭代的最優距離還是大家的平均距離,整體都是呈現下降趨勢的,也就是說整個群體都是在朝着食物的位置移動。

2.4實驗總結

1、在閱讀完卞鋒的兩篇文章,並用他的方法將PSO應用於解決TSP問題后,讓我認識到原來一種算法並不是拘泥於解決特定的一類問題,將算法與實際情況相結合,然后進行抽象和類比就能應用於新的問題的解決。這種抽象和類比的思維非常讓我驚訝,我要好好學習。

2、引入遺傳算法的變異操作

起初完成PSO時,測試發現很容易陷入離最優解較遠的局部最優。在參考卞鋒的《求解TSP的改進QPSO算法》后引入相關變異操作,從而解決了這個問題。

他提出的貪婪倒立變異很有意思。貪婪倒立變異是指找到一個城市,再找到離他最近的城市,然后將城市序列中兩個城市之間的序列進行倒序排列。這樣能夠在實現優化了所選兩個城市的距離的同時,保證其他城市順序盡量不變化。

不知道作者是怎么想到這種變異方法的。我在看完文章后首先想到的是染色體變異中的倒位,也就是染色體中的一段旋轉180度放回原來位置。染色體的倒位變異和作者提到的貪婪倒立變異非常相似。可能作者也是借鑒自然界的染色體變異吧,再次感嘆自然界的智慧無窮。

傳統的PSO沒有變異操作,這種引入變異的操作是借鑒遺傳算法的。可見吸收借鑒其他算法的精華,能夠提升自身算法的效率。

3 蟻群算法

3.1算法介紹

蟻群算法(Ant Colony Optimization, ACO),是一種用來在圖中尋找優化路徑的機率型算法,對螞蟻行為進行模仿抽象。在求解旅行推銷員問題時,螞蟻隨機從某一城市出發,根據城市間距離與殘留信息素濃度按概率選擇下一城市,螞蟻走完所有的城市后,在走過的路徑上留下信息素,螞蟻走的總路程越少,留下的信息素就越多。多次循環后,最好的路徑就有可能被篩選出。

其核心原理在於單位時間內通過短距離的路徑的次數要多,也就會留下更濃信息素,而螞蟻會選擇信息素濃的路徑,這樣根據信息素的濃度就能找到最短路徑,非常適合解決TSP問題。

3.2實驗代碼

 

import math
import random
import matplotlib.pyplot as plt
#讀取數據
f=open("test.txt")
data=f.readlines()
#將cities初始化為字典,防止下面被當成列表
cities={}
for line in data:
    #原始數據以\n換行,將其替換掉
    line=line.replace("\n","")
    #最后一行以EOF為標志,如果讀到就證明讀完了,退出循環
    if(line=="EOF"):
        break
    #空格分割城市編號和城市的坐標
    city=line.split(" ")
    map(int,city)
    #將城市數據添加到cities中
    cities[eval(city[0])]=[eval(city[1]),eval(city[2])]
#計算適應度,也就是距離分之一,這里用偽歐氏距離
#用於決定釋放多少信息素
def calcfit(addr):
    sum=0
    for i in range(-1,len(addr)-1):
        nowcity=addr[i]
        nextcity=addr[i+1]
        nowloc=cities[nowcity]
        nextloc=cities[nextcity]
        sum+=math.sqrt(((nowloc[0]-nextloc[0])**2+(nowloc[1]-nextloc[1])**2)/10)
    #最后要回到初始城市
    return 1/sum
#計算兩個城市的距離,用於啟發信息計算
def calc2c(c1,c2):
    #cities是一個字典,key是城市編號,value是一個兩個元素的list,分別是x y的坐標
    return math.sqrt((cities[c1][0]-cities[c2][0])**2+(cities[c1][1]-cities[c2][1])**2)


#方便從1開始,所以0-48共49個數字
#全部初始化為1,否則后面的概率可能因為乘以0而全為0
#信息素濃度表
matrix=[[1 for i in range(49)] for i in range(49)]

#螞蟻的類,實現了根據信息素和啟發信息完成一次遍歷
class Ant:
    def __init__(self):
        #tabu是已經走過的城市
        #規定從第一個城市開始走
        self.tabu=[1]
        self.allowed=[i for i in range(2,49)]
        self.nowCity=1
        #a,b分別表示信息素和期望啟發因子的相對重要程度
        self.a=2
        self.b=7
        #rho表示路徑上信息素的揮發系數,1-rho表示信息素的持久性系數。
        self.rho=0.1
        #本條路線的適應度,距離分之一
        self.fit=0
    #計算下一個城市去哪
    def next(self):
        sum=0
        #用一個數組儲存下一個城市的概率
        p=[0 for i in range(49)]
        #計算分母和分子
        for c in self.allowed:
            tmp=math.pow(matrix[self.nowCity][c],self.a)*math.pow(1/calc2c(self.nowCity,c),self.b)
            sum+=tmp
            #此處p是分子
            p[c]=tmp
        #更新p為概率
        for c in self.allowed:
            p[c]=p[c]/sum
        #更新p為區間
        for i in range(1,49):
            p[i]+=p[i-1]
        r=random.random()
        for i in range(48):
            if(r<p[i+1] and r>p[i]):
                #i+1即為下一個要去的城市
                self.tabu.append(i+1)
                self.allowed.remove(i+1)
                self.nowCity=i+1
                return
    #將所有城市遍歷
    def tour(self):
        while(self.allowed):
            self.next()
        self.fit=calcfit(self.tabu)


    #更新信息素矩陣
    def updateMatrix(self):
        #line儲存本次經歷過的城市
        line=[]
        for i in range(47):
            #因為矩陣是對陣的,2-1和1-2應該有相同的值,所以兩個方向都要加
            line.append([self.tabu[i],self.tabu[i+1]])
            line.append([self.tabu[i+1],self.tabu[i]])
        for i in range(1,49):
            for j in range(1,49):
                if([i,j] in line):
                    matrix[i][j]=(1-self.rho)*matrix[i][j]+self.fit
                else:
                    matrix[i][j]=(1-self.rho)*matrix[i][j]
    #一只螞蟻復用,每次恢復初始狀態
    def clear(self):
        self.tabu=[1]
        self.allowed=[i for i in range(2,49)]
        self.nowCity=1
        self.fit=0


#蟻群算法的類,實現了算法運行過程
class ACO:
    def __init__(self):
        #初始先隨機N只螞蟻
        self.initN=200
        self.bestTour=[i for i in range(1,49)]
        self.bestFit=calcfit(self.bestTour)
        self.initAnt()

    def initAnt(self):
        i=0
        tmpAnt=Ant()
        print(self.initN,"只先鋒螞蟻正在探路")
        while(i<self.initN):
            i+=1
            tmpTour=[i for i in range(1,49)]
            random.shuffle(tmpTour)
            tmpAnt.tabu=tmpTour
            tmpAnt.allowed=[]
            tmpAnt.updateMatrix()
            tmpFit=calcfit(tmpAnt.tabu)
            if(tmpFit>self.bestFit):
                self.bestFit=tmpFit
                self.bestTour=tmpAnt.tabu
            tmpAnt.clear()

    #n為螞蟻數量
    def startAnt(self,n):
        i=0
        ant=Ant()
        Gen=[]  #迭代次數
        dist=[] #距離,這兩個列表是為了畫圖
        while(i<n):
            i+=1
            ant.tour()
            if(ant.fit>self.bestFit):
                self.bestFit=ant.fit
                self.bestTour=ant.tabu
            print(i,":",1/self.bestFit)
            ant.clear()
            Gen.append(i)
            dist.append(1/self.bestFit)
        #繪制求解過程曲線
        plt.plot(Gen,dist,'-r')
        plt.show()
            #達到閾值后更改策略,對信息素的傾向更大
        #     if(1/self.bestFit<36000):
        #
        #         ant.a=5
        #         ant.b=2
        #         break
        # while(i<n):
        #     i+=1
        #     ant.tour()
        #     if(ant.fit>self.bestFit):
        #         self.bestFit=ant.fit
        #         self.bestTour=ant.tabu
        #     print(i,":",1/self.bestFit)
        #     ant.clear()


a=ACO()
a.startAnt(1000)

 

3.3實驗結果

下面是放置1000只螞蟻的結果,計算出最優解是10967.

為防止實驗的偶然性,下面進行10次重復實驗並求平均值

上圖橫坐標是放出的螞蟻數,縱坐標是信息素濃度最大的路徑的距離。可以看出其收斂得非常快。

3.4實驗總結

1、群算法的不難看出其非常適合求解最短距離問題,因此在三個算法中,ACO無論是算法的穩定性,還是最優解的距離長度都是最好的。不僅如此,它的運行速度也是另外兩者無法匹敵的。還有,GAPSO都要維持一個群體,而ACO只需要一只螞蟻就能完成任務,占用內存更小。可見,ACO在算法的時間復雜度,空間復雜度,解的質量等各個方面都完勝另外兩個算法。

2、初始狀態時每條路的信息素濃度相等,所以螞蟻的選擇會有隨機性。隨機的選擇也需要在每個路口進行判斷,而這種判斷是無意義,因為並不能選出信息素濃度高的。所以,我在初始時將若干只螞蟻指定隨機的幾條路,這樣能防止無意義的判斷,並在算法正式開始時各條路上已經存在具有指示性的信息素。

3、螞蟻選擇路線並不是只依靠信息素,還有一個啟發信息。啟發信息也就是距離下一個城市的距離,螞蟻會趨向於選擇更近的城市。

我在進行信息素和期望啟發因子的重要程度參數調優時發現期望啟發因子的重要程度會影響解的收斂速度。當啟發信息的權重越大,收斂得就會越快,而當啟發信息權重很小時,就會收斂得很慢。通過觀察公式,不難發現,啟發信息權重越大,算法就越趨向於貪婪算法,也就是說一部分城市序列得最優解可以用貪婪算法求。這可以給遺傳算法和粒子群尋優的初始群體的確定一定的啟發。也就是說貪婪算法求出的位置比隨機分布更接近解,從而節省從隨機位置到趨近解這一過程所消耗的時間。

同時,對於蟻群算法的優化,我提出一種猜想。初始階段較大的啟發信息權重有助於解的快速收斂,而到后面時則會影響找到最優解。所以可以在初始階段設置較大的啟發信息權重,讓解快速收斂,到了后面再降低其權重,讓算法變為以信息素為主要導向的,以此來加快逼近最優解的速度。也就是說在兩個階段動態調整信息素和期望啟發因子的重要程度。

4、一只螞蟻是如何實現眾多螞蟻的行為的?

真實世界的螞蟻是在同一時間有蟻群螞蟻經過幾條路線,因為單位時間內通過的螞蟻數量不同,而造成每條路的信息素濃度不同。而在算法實現時並沒有模擬很多螞蟻。

算法所模擬的是一只螞蟻,在走完全程時,對其經過的所有城市之間施加相同的信息素值,這個值是該次行程的距離分之一。每次進行的只是一只螞蟻。

仔細想想我們就會發現其實兩者所達到的效果是一樣的。盡管一個是同時有很多螞蟻在行走,另一個是同一時間只有一只螞蟻在行走。但是他們施加信息素的方式也不同,一個是經過螞蟻的多少決定,另一個是該條路的距離決定。這兩方面的不同所導致的結果是一樣的,並且模擬一只螞蟻更加簡便,容易實現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM