基本原理
螞蟻覓食,通常是一種群體行為,當然一只小螞蟻也是可以找到食物的, 辛苦點、慢點而已;
蟻群覓食,每只螞蟻在經過的路上會留下一種化學物質,稱為 信息素,信息素 會隨着時間 逐漸 揮發,也就是說找到食物越慢,這條路上的信息素 殘留 越少;
其他螞蟻可以 感受到 信息素的存在,並且能測量信息素的濃度;
通常 螞蟻 會 沿着 信息素 最濃的方向 行走,畢竟是前人經驗嘛,這樣這條路上的信息素 越來越濃;
但也會 突發奇想,試着走走 信息素沒那么濃,甚至 沒有信息素的路徑,可能更快找到了食物,那么新路徑 信息素濃度很大,也可能找不到食物,信息素揮發完畢;
如此反復,經過大量螞蟻 重復多次的探索,最終摸索出一條 最短的 路徑;當然 不一定是 全局最短,這點就不解釋了;
整個過程如下圖
整個過程最重要的兩點就是 接下來選擇哪條路 和 記錄並更新每條路信息素的濃度。
狀態轉移概率
決定了 接下來選擇哪條路;
在城市 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問題 代碼