蟻群算法【1】基本原理


基本原理

螞蟻覓食,通常是一種群體行為,當然一只小螞蟻也是可以找到食物的, 辛苦點、慢點而已;

蟻群覓食,每只螞蟻在經過的路上會留下一種化學物質,稱為 信息素信息素 會隨着時間 逐漸 揮發,也就是說找到食物越慢,這條路上的信息素 殘留 越少;

其他螞蟻可以 感受到 信息素的存在,並且能測量信息素的濃度;

通常 螞蟻 會 沿着 信息素 最濃的方向 行走,畢竟是前人經驗嘛,這樣這條路上的信息素 越來越濃;

但也會 突發奇想,試着走走 信息素沒那么濃,甚至 沒有信息素的路徑,可能更快找到了食物,那么新路徑 信息素濃度很大,也可能找不到食物,信息素揮發完畢;

 

如此反復,經過大量螞蟻 重復多次的探索,最終摸索出一條 最短的 路徑;當然 不一定是 全局最短,這點就不解釋了;

整個過程如下圖

整個過程最重要的兩點就是 接下來選擇哪條路 和 記錄並更新每條路信息素的濃度

 

狀態轉移概率

決定了 接下來選擇哪條路;

在城市 i 時 選擇 城市 j 的概率,有了所有 可選城市 的概率后,可采用 輪盤賭 等方式 選出 下一個城市;

也可采用其他方式選擇下一個城市,比如 模擬退火算法代替 輪盤賭,就實現了 混合優化算法

 𝑃𝑖𝑗𝑘 狀態轉移概率,代表 第𝑘只螞蟻在城市𝑖選擇城市𝑗的概率;

allowed 代表 可選擇的城市;

𝜏𝑖𝑗 城市𝑖 𝑗 之間存留的信息素;

𝛼信息素啟發因子,控制着信息素𝜏𝑖𝑗 對路徑選擇的影響程度,值越大,越依賴信息素,探索性降低,值越小,蟻群搜索的范圍減少,容易陷入局部最優;

η𝑖𝑗 城市 𝑖 𝑗 之間的能見度,反映了由城市 𝑖到城市 𝑗 的啟發程度,一般取 𝑑𝑖𝑗 的倒數;

𝑑𝑖𝑗 城市 𝑖 𝑗 之間的距離;

𝛽期望值啟發因子,控制着η𝑖𝑗 能見度的影響程度,其大小反應了在道路搜索中 先驗性、確定性等因素的強弱;

𝜌信息素揮發系數,影響信息素揮發的快慢;1-𝜌信息素殘留系數;(1-𝜌)𝜏𝑖𝑗  在該回合前城市 𝑖 𝑗 間殘留的信息素, ∆𝜏𝑖𝑗k 該回合新增的信息素;

𝑘 代表第𝑘只螞蟻;

 

信息素更新策略

信息素更新公式,符號意義同上

m 表示 螞蟻數量

 

根據 Δ𝜏𝑖𝑗k (t, t+1) 的更新方式不同,可將蟻群算法分為3類:蟻密算法、蟻量算法、蟻周算法

1. 蟻密算法(Ant-Density模型)

每只螞蟻經過城市 i j 時,對邊 eij 所貢獻的信息素為常量,每個單位長度為 Q

 

2. 蟻量算法(Ant-Quantity模型)

每只螞蟻在經過城市 i j 時,對邊 eij 所貢獻的信息素為變量,Q/dij,dij 表示 城市 i j 間的距離

 

3. 蟻周算法(Ant-Cycle模型)

上述兩種模型,對兩城市之間 eij 邊上信息素貢獻的增量在螞蟻經過邊的同時完成,而蟻周模型對邊信息素的增量是在本次循環結束時才進行更新調整

一只螞蟻在經過城市i j 時,對邊上信息素貢獻的增量為每單位長度 Q/Lk,Lk為螞蟻在本次循環走出路徑的長度      【經測試,這里的 每單位長度 可以忽略,直接每條邊 增加 Q/L 就行了】

 

 

一只小螞蟻的尋路經歷

簡單起見,我們只用一只小螞蟻覓食,原理懂了,代碼只是浮雲,偷個懶;

代碼其實 是經典的旅行商問題,從 起點出發,經過所有城市后,回到起點,找最短路徑

import cv2 as cv
import numpy as np
import matplotlib.pylab as plt


ALPHA = 1
BETA = 1
RHO = 0.5
Q = 100.

city_num = 50
ant_num = 50

distance_x = [
    178, 272, 176, 171, 650, 499, 267, 703, 408, 437, 491, 74, 532,
    416, 626, 42, 271, 359, 163, 508, 229, 576, 147, 560, 35, 714,
    757, 517, 64, 314, 675, 690, 391, 628, 87, 240, 705, 699, 258,
    428, 614, 36, 360, 482, 666, 597, 209, 201, 492, 294]
distance_y = [
    170, 395, 198, 151, 242, 556, 57, 401, 305, 421, 267, 105, 525,
    381, 244, 330, 395, 169, 141, 380, 153, 442, 528, 329, 232, 48,
    498, 265, 343, 120, 165, 50, 433, 63, 491, 275, 348, 222, 288,
    490, 213, 524, 244, 114, 104, 552, 70, 425, 227, 331]

position = list(zip(distance_x, distance_y))

distance = np.ones(shape=(city_num, city_num))
pheromone = np.ones(shape=(city_num, city_num))
for i in range(city_num):
    for j in range(city_num):
        distance[i, j] = round(np.sqrt(pow(position[i][0] - position[j][0], 2) +
                                       pow(position[i][1] - position[j][1], 2)), 1)
print(distance)


class ANT(object):
    # 螞蟻
    def __init__(self, id, start, train=False, method='Cycle'):
        self.id = id
        self.start = start                  # 起點
        self.mile = 0
        self.city_index = start
        self.passed = [self.city_index]     # 經過的站點  index
        self.allowed = [True] * city_num    # 可行站點
        self.allowed[self.city_index] = False
        self.train = train
        self.method = method
        if method not in ['Density', 'Quantity', 'Cycle']:
            raise 'method is not supported'

    def select_next_city(self):
        # pijk
        p_allowed = []
        city_allowed = []
        for ind, city in enumerate(self.allowed):
            if city:    # 可行走
                p = pow(pheromone[self.city_index, ind], ALPHA) * pow(1 / distance[self.city_index, ind], BETA)
                p_allowed.append(p)
                city_allowed.append(ind)

        # 無路可走
        if not p_allowed: return

        if self.train:
            # 輪盤賭
            index = np.random.choice(city_allowed, size=1, replace=True, p=np.array(p_allowed) / sum(p_allowed))[0]
            return index
        else:
            return city_allowed[np.argmax(p_allowed)]   # 最優路徑

    def move(self, city_index):
        '''
        city_index: 新的站點, None 表示回到起點
        '''
        if city_index is not None:  # city_index 可能等於0
            self.allowed[city_index] = False  # 新的城市不再可行
        else:
            city_index = self.start
        self.passed.append(city_index)     # 新的城市加入已經過序列

        # 單步更新信息素
        if self.train:
            if self.method == 'Density':
                pheromone[self.city_index, city_index] = pheromone[self.city_index, city_index] * RHO + Q
            if self.method == 'Quantity':
                pheromone[self.city_index, city_index] = pheromone[self.city_index, city_index] * RHO + \
                                                     Q / distance[self.city_index, city_index]

        self.mile += distance[self.city_index, city_index]  # 更新里程
        self.city_index = city_index       # 更新節點

    def path(self):
        global pheromone
        while 1:
            index = self.select_next_city()
            self.move(index)
            if index is None:   # 全部節點走完
                break

    def update_pheromone(self):
        # 回合更新信息素
        global pheromone
        for ind, val in enumerate(self.passed[ :-1]):
            pheromone[val, self.passed[ind + 1]] = pheromone[val, self.passed[ind + 1]] * RHO \
                                                   + (Q / self.mile) # * distance[val, self.passed[ind + 1]]
            # 注意下面這句,表示正反權重相同,可根據實際情況調整
            pheromone[self.passed[ind + 1], val] = pheromone[val, self.passed[ind + 1]]

def show(path):
    img = np.zeros(shape=(np.max(distance_y) + 50, np.max(distance_x) + 50, 3)).astype(np.uint8)
    for i in position:
        cv.circle(img, i, 2, [0, 0, 150], 8)    # 描點
        cv.putText(img, '%s,%s' % i, i, cv.FONT_ITALIC, 0.3, [255, 100, 100])
    cv.circle(img, position[path[0]], 4, [0, 255, 0], 8)    # 起點

    for ind, val in enumerate(path[ :-1]):
        cv.line(img, position[val], position[path[ind + 1]], [255, 255, 255], 2)    # 划線
        cv.imshow('temp', img)
        cv.waitKey(1)


if __name__ == '__main__':
    ### test ANT

    # test select_next_city
    ant = ANT(2, 10)
    index = ant.select_next_city()
    print(index)

    start = 20  # 起點
    miles = []
    # train
    for _ in range(2000):
        ant = ANT(3, start, train=True, method='Cycle')   # Density Quantity Cycle
        ant.path()
        if ant.method == 'Cycle':
            ant.update_pheromone()
        # print(pheromone)

        # test
        ant = ANT(3, start, train=False)
        ant.path()
        miles.append(ant.mile)
    # 收斂性
    plt.plot(miles, '+-')
    plt.show()
    # 最優路徑
    show(ant.passed)
    cv.waitKey(0)

收斂速度

最優路徑

 

特點

采用 一種 正反饋 機制,使得算法收斂

1. 每個螞蟻可以實時改變周圍環境,蟻密模型 和 蟻量模型 都是 實時改變 信息素的,單步更新,蟻周算法是 回合更新

2. 整個搜索過程每只螞蟻完全獨立,可采用分布式計算方式,提高搜索效率 

 

3. 蟻群算法 容易陷入 局部最優,早熟現象

 

總結

在調參上,基本參考 蟻群算法原理及其應用[1]

設置了早熟停止迭代,因此可能出現城市數與耗時不成正比的情況;

Ant-Cycle模型耗時比Ant-Quantity模型耗時短,但Ant-Quantity模型更容易陷入局部最優值,所以,更容易出發早熟停止迭代機制(本文未提供結果對比說明,感興趣的同學,可以利用提供的源碼自行驗證); 
Ant-Cycle模型信息素的更新是以整條路徑 ​ [公式] 為基礎,路徑中的某段路長短不影響其路徑的信息素計算; 
Ant-Quantity模型信息素的更新是以路徑中的某段路 ​ [公式] 為基礎,整條路徑長短不影響其路徑的信息素計算; 
Ant-Cycle模型處理“病態問題”[2][3](P113)比Ant-Quantity模型優,因為Ant-Cycle模型信息素的計算是不受路徑中的某段路長短的影響,但現實中很少有這種奇怪性質的“病態問題”;

算法實現是比較粗糙的,如最后評價函數應該是:模型尋優次數+尋優迭代步數+運行時間,因此關於細節方面,感興趣的同學可詳讀下文提供的參考文獻;

 

優化方向

1. 蟻群算法在性能和局部尋優能力,遠勝於遺傳算法,上文提到,尋優30個候選城市耗時要求是3秒(同事使用Scala10個線程),而本人利用python實現的蟻群算法尋優90個候選城市耗時也不到3秒,但工業應用上使用遺傳算法遠高於蟻群算法(本人了解的),主要是因為遺傳算法擁有超強的擴展性靈和活性強,使得其應用非常廣泛:函數優化、組合優化、生產調度、自動控制、機器人學、圖像處理、人工生命、遺傳編程、機器學習等,就其路徑規划根據染色體交叉方式不同得到不同啟發式遺傳算法:單點交叉、雙點交叉、均勻交叉、匹配交叉、順序交叉、循環交叉、貪婪式交叉、旋轉交叉、混合蛙跳[4]、DPX[5]等等,數不勝數; 
其中貪婪式交叉、旋轉交叉、混合蛙跳、DPX本人均實現過,開始的性能和尋優能力不及蟻群算法,而遺傳算法組合能力是非常強的,比較容易跳出局部最優值,可優化空間大,長期迭代中,各方面指標是優於蟻群算法,因此實際工業應用場景之中主要是以啟發式遺傳算法為主;
啟發式遺傳算法不但需要有強算法能力,還需非常熟悉工業場景,直接進場的話,實現效果可能不佳,因此,長期工業應用首選啟發式遺傳算法,短期快速上線首選蟻群算法(ps,在路徑規划專項任務下,還是蟻群算法簡單好用,易於實現);

2. 蟻群算法容易陷於局部最優值(下圖),A、B、C均為局部最優值,假設B點為全局最優; 
若蟻群算法根據信息素從O點到達A點時,可能是無法跳出局部最優,因為蟻群在t時刻游走的路線受t-1時刻信息素的限制,而t-1時刻游走的線路受t-2時刻信息素的限制等等,因此t時刻要想跳出局部最優值A點,很難通過調參解決,這也可以解析每一次蟻群算法跑出來的結果有差異,且根據路徑圖做前后對比的話,每次結果路徑可能有明顯差異,遺傳算法也有類似情況(但比較穩定); 
第5點提到,啟發式遺傳算法比較容易跳出局部最優值,是因為啟發式遺傳算法中的適應度越高的染色體交叉屬於局部搜索(即在A區域內搜索),適應度一般的染色體可能在鞍點或C點,其交叉可能就是全局搜索關鍵就是如何設置適應度高的染色體交叉和適應度較高、一般染色體交叉,太過於隨機,則會降低收斂速度,還有一個關鍵點就是在染色體交叉后如何保留種群的多樣性(小生環境),詳看請看參考文獻[3]; 
啟發式,根據優化目標調整算法或多算法組合,蟻群算法根據優化目標調整算法可能性不大,不可能把螞蟻調整為會飛,那就是粒子群算法了,多算法組合是蟻群算法的一個優化方向,如模擬退火+蟻群算法、爬山算法+蟻群算法,模擬退火和爬山算法跳出局部最優值的好幫手,而且計算快,算法組合后,性能受影響小,筆者用類似的方法解決了背包問題+商旅問題;

 

融合算法 也稱 雙層啟發式算法,具體用法見我的其他博客

 

實踐經驗

在更新信息素時,所有螞蟻未經過的路也 揮發 信息素,效果比 不揮發好

pheromone *= RHO
for ind2, val in enumerate(path[: -1]):
    pheromone[val, path[ind2 + 1]] += Q / fit

這是因為 新加的信息素 可能還沒有 揮發的多,相當於信息素還減少了,沒有正反饋作用了 

 

 

 

 

參考資料:

https://www.jianshu.com/p/6d16573ef675  蟻群算法

https://www.jianshu.com/p/9ef24ad65191  蟻群算法及其應用實例

https://www.jianshu.com/p/e6a20de60797  數學建模學習筆記(一) 蟻群算法(MATLAB)

https://blog.csdn.net/qq_33829154/article/details/85258615  蟻群算法

https://zhuanlan.zhihu.com/p/351466641  蟻群算法(ant colony algorithm)python實現專項解決商旅問題(TSP)    理論較強

https://www.cnblogs.com/bokeyuancj/p/11798635.html    精英螞蟻系統

https://www.jianshu.com/p/a8acd65aba79  python3使用蟻群+鄰域搜索算法解決帶有起點和終點的TSP問題    代碼


免責聲明!

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



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