基本原理
蚂蚁觅食,通常是一种群体行为,当然一只小蚂蚁也是可以找到食物的, 辛苦点、慢点而已;
蚁群觅食,每只蚂蚁在经过的路上会留下一种化学物质,称为 信息素,信息素 会随着时间 逐渐 挥发,也就是说找到食物越慢,这条路上的信息素 残留 越少;
其他蚂蚁可以 感受到 信息素的存在,并且能测量信息素的浓度;
通常 蚂蚁 会 沿着 信息素 最浓的方向 行走,毕竟是前人经验嘛,这样这条路上的信息素 越来越浓;
但也会 突发奇想,试着走走 信息素没那么浓,甚至 没有信息素的路径,可能更快找到了食物,那么新路径 信息素浓度很大,也可能找不到食物,信息素挥发完毕;
如此反复,经过大量蚂蚁 重复多次的探索,最终摸索出一条 最短的 路径;当然 不一定是 全局最短,这点就不解释了;
整个过程如下图
整个过程最重要的两点就是 接下来选择哪条路 和 记录并更新每条路信息素的浓度。
状态转移概率
决定了 接下来选择哪条路;
在城市 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问题 代码