基本原理

蚂蚁觅食,通常是一种群体行为,当然一只小蚂蚁也是可以找到食物的, 辛苦点、慢点而已;

蚁群觅食,每只蚂蚁在经过的路上会留下一种化学物质,称为 信息素信息素 会随着时间 逐渐 挥发,也就是说找到食物越慢,这条路上的信息素 残留 越少;

其他蚂蚁可以 感受到 信息素的存在,并且能测量信息素的浓度;

通常 蚂蚁 会 沿着 信息素 最浓的方向 行走,毕竟是前人经验嘛,这样这条路上的信息素 越来越浓;

但也会 突发奇想,试着走走 信息素没那么浓,甚至 没有信息素的路径,可能更快找到了食物,那么新路径 信息素浓度很大,也可能找不到食物,信息素挥发完毕;

 

如此反复,经过大量蚂蚁 重复多次的探索,最终摸索出一条 最短的 路径;当然 不一定是 全局最短,这点就不解释了;

整个过程如下图

整个过程最重要的两点就是 接下来选择哪条路 和 记录并更新每条路信息素的浓度

 

状态转移概率

决定了 接下来选择哪条路;

在城市 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问题    代码