资讯动态

蚁群算法入门:蚂蚁系统(AS)原理、实现与调参实战

发布时间:2026/10/14 20:21:21 来源:尧图企业网站定制
蚁群算法这个系列我断断续续折腾了快两年从最早照着论文硬啃公式到后来自己拿TSPLIB的算例一个个跑对比踩过的坑比想象中多得多。蚂蚁系统Ant System简称AS是整个蚁群算法家族的起点1991年由Dorigo在他的博士论文里提出别看它现在被各种改进版本按在地上摩擦但如果你连AS都没吃透后面那些精英策略、最大最小蚂蚁系统、排序蚂蚁系统基本就是空中楼阁。这篇东西我打算把AS从里到外扒一遍包括它为什么这么设计、参数怎么调、代码怎么写、跑不出来结果的时候该往哪个方向排查。适合刚接触群体智能的在校学生、想用元启发式解决路径优化问题的工程师以及跟我一样喜欢刨根问底、不满足于调包的折腾党。1. 蚂蚁系统到底在解决什么问题1.1 从蚂蚁找食物说起先把这个算法的生物学来源讲清楚不然后面看公式会觉得很突兀。真实蚂蚁在觅食过程中会在走过的路径上释放一种叫信息素的化学物质后面的蚂蚁选择路径时会倾向于走信息素浓度高的方向。关键在于较短路径上的蚂蚁往返一次用时更短单位时间内留下的信息素更多于是短路径上的信息素浓度会逐渐累积形成正反馈最终整个蚁群收敛到一条较优路径上。这个机制妙就妙在它不需要任何中心控制纯粹靠个体之间的间接通信完成全局优化。你把它映射到TSP问题上城市就是食物源蚂蚁要做的就是找一条遍历所有城市且总距离最短的闭合路径。每只蚂蚁独立构造一条完整路径走完之后根据路径质量往经过的边上释放信息素路径越短释放越多下一轮蚂蚁再根据信息素和启发式信息做概率选择。1.2 TSP问题为什么适合拿AS来练手TSP是组合优化里最经典的NP难问题n个城市有(n-1)!/2种可能的路径城市数稍微大一点穷举就彻底没戏。但它有个特别适合蚁群算法的性质解可以自然地表示成图的边序列而蚂蚁在图上逐边构造路径的过程跟TSP的解构造过程天然对应。另外TSP的评估函数极其干净就是路径长度求和没有复杂的约束要处理。你拿它来验证算法逻辑对不对非常方便跑出来的结果还能直接跟已知最优解对比心里有底。我建议所有刚入门元启发式的人都拿TSP当第一个练手问题不是因为它简单而是因为它把算法核心逻辑和问题特性剥离得很清楚你能专注在算法本身。1.3 AS与其他ACO变体的关系这里得把谱系理一理不然容易混淆。AS是最基础的版本它有三个变体蚁密系统Ant-Density、蚁量系统Ant-Quantity和蚁周系统Ant-Cycle。前两个是边走边释放信息素第三个是走完一整圈再统一释放。实际用下来蚁周系统效果最好也最常用所以现在大家说AS基本默认指蚁周系统。后来为了提升性能又演化出了精英蚂蚁系统EAS、基于排序的蚂蚁系统ASrank、最大最小蚂蚁系统MMAS等等。这些改进版本的核心思路无非是几个方向增强最优路径的信息素、限制信息素浓度范围防止过早收敛、改进路径选择策略。你把AS搞明白了这些改进版本看一眼就知道它们在干什么。2. 算法核心机制拆解2.1 路径构造的概率选择公式这是整个算法最核心的部分。第k只蚂蚁在城市i时选择下一个城市j的概率由两部分决定信息素浓度τ(i,j)的α次方以及启发式信息η(i,j)的β次方。公式长这样P(i,j) [τ(i,j)^α × η(i,j)^β] / Σ[τ(i,k)^α × η(i,k)^β]分母是对所有允许访问的城市k求和。启发式信息η(i,j)通常取1/d(i,j)也就是距离的倒数距离越短启发值越大。α控制信息素的重要程度β控制启发式信息的重要程度。我一开始不理解为什么要取幂次而不是直接加权后来想明白了取幂次相当于在调节两种信息的锐度。α越大信息素浓度的差异被放大得越厉害蚂蚁越倾向于跟随前人的选择β越大距离的差异被放大蚂蚁越倾向于贪心地选近的城市。这个设计比线性加权灵活得多。2.2 信息素更新的两个阶段信息素更新分两步走先蒸发再释放。蒸发阶段所有边上的信息素乘以(1-ρ)ρ是蒸发率通常取0.1到0.5之间。这一步的作用是防止信息素无限累积同时让算法有机会忘记之前不好的选择保持探索能力。释放阶段每只蚂蚁根据自己的路径长度往经过的边上添加信息素添加量是Q/L_k其中Q是常数L_k是第k只蚂蚁的路径长度。路径越短添加越多。蚁周系统就是所有蚂蚁都走完一轮之后统一更新。# 信息素蒸发 pheromone pheromone * (1 - rho) # 信息素释放 for ant in ants: for i in range(num_cities - 1): a, b ant.path[i], ant.path[i1] pheromone[a][b] Q / ant.path_length pheromone[b][a] Q / ant.path_length # 处理回到起点的最后一条边 a, b ant.path[-1], ant.path[0] pheromone[a][b] Q / ant.path_length pheromone[b][a] Q / ant.path_length注意信息素矩阵是对称的因为TSP的边是无向的。这个细节很容易漏漏了的话蚂蚁会认为从i到j和从j到i是不同的路虽然最终结果可能差不多但逻辑上是错的。2.3 参数体系与默认取值逻辑AS的参数不算多但每一个都影响巨大。我整理了一个表把每个参数的作用和推荐范围列出来参数含义推荐范围影响α信息素权重1~2过大导致早熟收敛β启发式权重2~5过大退化为贪心算法ρ信息素蒸发率0.1~0.5过大收敛慢过小易停滞Q信息素强度常数1~100主要影响数值尺度m蚂蚁数量n~2n太少探索不足太多计算浪费τ₀初始信息素Q/(n×L_nn)影响早期搜索行为Q这个参数其实不太敏感因为它只是整体缩放信息素的数值只要跟初始信息素和蒸发率配合好就行。真正敏感的是α、β和ρ这三个。我后面会专门讲怎么调。3. 从零实现一个能跑的AS3.1 数据结构设计实现之前先把数据结构想清楚不然后面改起来很痛苦。核心需要这几个东西距离矩阵dist_matrixn×n的二维数组dist_matrix[i][j]表示城市i到j的距离信息素矩阵pheromone同样是n×n对称矩阵蚂蚁类Ant包含当前路径path、已访问集合visited、当前路径长度path_length最优解记录best_path和best_length距离矩阵建议一开始就算好存起来不要在迭代过程中反复计算。TSP的距离通常是欧氏距离如果坐标是浮点数算距离的时候记得开方。有些实现为了省时间不开方直接比较平方和但AS的信息素更新依赖真实距离所以还是老老实实开方。import numpy as np def build_distance_matrix(coords): n len(coords) dist np.zeros((n, n)) for i in range(n): for j in range(i1, n): d np.sqrt(np.sum((coords[i] - coords[j]) ** 2)) dist[i][j] d dist[j][i] d return dist3.2 单只蚂蚁的路径构造这是算法的主循环里最内层的逻辑写的时候要注意几个细节。蚂蚁每到一个城市要从还没访问的城市里按概率选下一个。如果剩余城市只有一个直接去就行不用算概率。概率计算时如果分母为0理论上不会但浮点数下可能极小要做保护。def construct_path(ant, pheromone, dist, alpha, beta): n len(dist) start ant.start_city ant.path [start] ant.visited set([start]) for _ in range(n - 1): current ant.path[-1] candidates [c for c in range(n) if c not in ant.visited] if len(candidates) 1: nxt candidates[0] else: probs [] for c in candidates: tau pheromone[current][c] ** alpha eta (1.0 / dist[current][c]) ** beta probs.append(tau * eta) probs np.array(probs) probs probs / probs.sum() nxt np.random.choice(candidates, pprobs) ant.path.append(nxt) ant.visited.add(nxt) # 计算路径长度含回到起点 length 0 for i in range(n - 1): length dist[ant.path[i]][ant.path[i1]] length dist[ant.path[-1]][ant.path[0]] ant.path_length length这里有个性能上的坑每次选下一个城市都要遍历所有未访问城市算概率复杂度是O(n)整个路径构造是O(n²)。对于n100以内的TSP没问题再大就得考虑用候选列表或者邻域剪枝了。3.3 完整的主循环框架把上面的零件拼起来主循环的逻辑就很清晰了def ant_system(coords, num_ants20, num_iter200, alpha1.0, beta3.0, rho0.3, Q100): n len(coords) dist build_distance_matrix(coords) # 初始化信息素 tau0 Q / (n * estimate_nn_length(dist)) pheromone np.full((n, n), tau0) best_path None best_length float(inf) for iteration in range(num_iter): ants [Ant(start_citynp.random.randint(n)) for _ in range(num_ants)] for ant in ants: construct_path(ant, pheromone, dist, alpha, beta) if ant.path_length best_length: best_length ant.path_length best_path ant.path[:] # 信息素蒸发 pheromone * (1 - rho) # 信息素释放 for ant in ants: deposit Q / ant.path_length for i in range(n - 1): a, b ant.path[i], ant.path[i1] pheromone[a][b] deposit pheromone[b][a] deposit a, b ant.path[-1], ant.path[0] pheromone[a][b] deposit pheromone[b][a] deposit return best_path, best_length初始信息素τ₀的设定有个经验公式Q/(n×L_nn)其中L_nn是用最近邻算法得到的路径长度。这样设的好处是让初始信息素跟后续释放的信息素在同一个数量级避免早期搜索完全被启发式信息主导。4. 参数调优的实战经验4.1 α和β的搭配这两个参数是耦合的不能单独调。我的经验是保持α在1附近重点调β。β太小比如1算法接近随机搜索收敛极慢β太大比如8以上蚂蚁基本就是贪心算法容易陷入局部最优。对于城市坐标在[0,100]范围内的TSPβ取3到5之间比较稳。有个快速判断方法跑10次取平均如果最优解波动很大说明β可能偏小探索太随机如果10次结果几乎一样但都不是很好说明β偏大或者α偏大早熟收敛了。4.2 蒸发率ρ的选择ρ控制信息素的遗忘速度。ρ大旧信息素消失快算法探索能力强但收敛慢ρ小信息素累积快收敛快但容易停滞。我一般从0.3开始试如果发现算法很快就收敛到同一个解不再改进就调大到0.5如果跑了很久还在大幅波动就调小到0.1。这里有个反直觉的点ρ并不是越小越好。ρ太小的时候早期随机产生的信息素会长期残留反而干扰后续搜索。我试过ρ0.01结果算法前50代基本在原地打转。4.3 蚂蚁数量的影响蚂蚁数量m的经典推荐值是城市数n。m太小每轮释放的信息素样本不足信息素分布噪声大m太大每轮计算量线性增长但信息素更新的边际收益递减。我实测下来m取n到1.5n之间性价比最高。对于小规模TSPn30m可以取大一点比如2n因为计算量本身不大多放点蚂蚁能增加多样性。对于大规模TSPn100m取n甚至更少把计算预算留给更多迭代次数。5. 常见问题与排查实录5.1 算法完全不收敛怎么办这是新手最常遇到的问题跑了几百代最优解还在乱跳。排查顺序如下第一检查信息素更新是否正确执行。我见过有人把蒸发和释放的顺序写反了先释放再蒸发等于每轮都在削弱信息素算法当然不收敛。正确顺序是先蒸发再释放。第二检查概率计算有没有溢出。当α和β较大时τ^α × η^β可能超出浮点数范围变成inf归一化之后概率就乱了。解决办法是在取幂之前先做归一化或者用对数域计算。第三检查初始信息素是不是设得太小。如果τ₀接近0前几轮蚂蚁基本靠启发式信息走信息素还没来得及累积就被蒸发光了。5.2 收敛太快但解很差这是早熟收敛的典型症状算法陷入局部最优出不来。对策有几个增大ρ加快信息素蒸发削弱已收敛路径的优势减小α降低信息素对路径选择的引导强度增加蚂蚁数量提高每轮的探索多样性引入信息素浓度下限防止某些边上的信息素被蒸发到接近0最后一条其实就是MMAS的核心思想但即使在AS里加一个简单的下限也能明显改善早熟问题。我通常设下限为τ₀的0.01倍。5.3 运行时间过长AS的时间复杂度是O(num_iter × m × n²)n大了之后非常慢。优化方向用numpy向量化概率计算避免Python循环维护未访问城市的动态列表而不是每次遍历所有城市检查visited对于n200的情况考虑只从最近的k个城市里选下一个候选列表策略用局部搜索对每轮最优解做2-opt优化减少所需迭代次数我拿n100的算例测试过纯Python循环跑500代大概要3分钟用numpy向量化之后降到20秒左右差距非常明显。5.4 结果每次都不一样AS是随机算法每次运行结果不同是正常的。但如果波动太大比如最好和最差差了一倍说明参数设置有问题。评估算法性能时应该跑多次取统计量通常跑20到30次看平均值、最好值、最差值和标准差。我一般会记录每次运行的最优解画一个箱线图这样能直观看出算法的稳定性和收敛质量。问题现象可能原因排查方向完全不收敛更新顺序错误/数值溢出检查蒸发释放顺序检查幂运算早熟收敛ρ太小/α太大/m太少调大ρ调小α增加蚂蚁数运行太慢未向量化/无候选列表numpy化加候选列表加局部搜索结果波动大β太小/迭代不足调大β增加迭代次数解质量差参数组合不佳网格搜索α,β,ρ6. 性能提升的几个实用技巧6.1 最近邻初始化在正式跑AS之前先用最近邻算法构造一条路径用它的长度来设定初始信息素τ₀。这样初始信息素的量级就跟后续释放的信息素匹配避免早期搜索的盲目性。最近邻算法本身很简单从某个城市出发每次选最近的未访问城市直到走完所有城市。6.2 2-opt局部搜索嵌入纯AS的局部搜索能力很弱构造出来的路径经常有交叉。在每轮迭代结束后对当前最优路径做2-opt优化能显著提升解质量。2-opt的基本操作是如果路径中有两条边(i,i1)和(j,j1)交叉就把它们替换成(i,j)和(i1,j1)路径长度会缩短。def two_opt(path, dist): improved True while improved: improved False for i in range(1, len(path) - 2): for j in range(i 1, len(path)): if j - i 1: continue # 计算替换前后的距离变化 a, b path[i-1], path[i] c, d path[j], path[(j1) % len(path)] delta (dist[a][c] dist[b][d]) - (dist[a][b] dist[c][d]) if delta 0: path[i:j1] reversed(path[i:j1]) improved True return path注意2-opt的复杂度是O(n²)每次扫描嵌入到AS里会增加不少计算量但通常能减少一半以上的迭代次数总体算下来是划算的。6.3 信息素平滑策略当算法停滞时连续多代最优解不变可以对信息素矩阵做一次平滑把每条边的信息素往平均值拉一点。这相当于给算法一个重启的机会让它跳出当前的局部最优区域。具体做法是pheromone pheromone × (1-λ) mean_pheromone × λλ取0.1到0.3之间。这个操作不能太频繁否则会破坏已经积累的搜索经验。7. 实验结果与分析7.1 小规模算例验证我拿一个20个城市的随机算例做了测试坐标在[0,100]范围内均匀分布。参数设置m20num_iter200α1β3ρ0.3Q100。跑20次的结果如下最优解比最近邻初始解改进了约35%平均解比最优解差约5%标准差约为平均值的3%这个结果说明AS在小规模问题上表现稳定20次运行都能收敛到接近最优的解。但要注意20个城市的TSP穷举也就19!/2种可能AS的优势要到更大规模才能体现。7.2 中等规模对比50个城市的算例上AS跟最近邻和贪心算法对比算法平均解质量运行时间最近邻基准0.1秒贪心比最近邻好8%0.1秒AS200代比最近邻好22%约15秒AS2-opt比最近邻好31%约40秒可以看到AS2-opt的组合在解质量上有明显优势代价是运行时间增加。实际使用时需要根据问题规模和时间预算做权衡。7.3 参数敏感性分析我做了α和β的网格搜索固定ρ0.3结果发现α1, β3 是一个稳健的默认组合α2, β5 在部分算例上表现更好但波动更大α0.5, β2 收敛太慢200代不够α3, β1 早熟严重解质量差这个结果跟文献里的推荐范围基本一致说明AS的参数虽然需要调但并没有想象中那么敏感在合理范围内都能跑出可接受的结果。8. 从AS到更高级变体的演进思路搞懂了AS之后你会发现后面那些改进版本其实都是在解决AS的几个固有缺陷。早熟收敛对应MMAS的信息素上下限机制收敛速度慢对应EAS的精英策略解质量不够对应ASrank的排序加权更新。你不需要一个个去背这些变体的公式只要理解它们各自针对AS的哪个痛点用的时候查一下具体实现就行。我个人在实际操作中的体会是AS最大的价值不在于它能解多好的TSP而在于它提供了一个理解群体智能的极简框架。信息素正反馈、启发式引导、随机探索这三个要素的平衡是所有ACO变体乃至很多其他群体智能算法的共同内核。你把这个内核吃透了再看粒子群、人工蜂群这些算法会发现它们只是在用不同的方式实现同样的平衡。最后分享一个小技巧如果你在调试AS的时候发现结果总是不理想先别急着调参数把每轮迭代的最优解和平均解打印出来画个曲线。如果最优解曲线是阶梯状下降然后长期平坦说明早熟了如果平均解曲线一直在最优解上方很远说明探索太强收敛不够。根据曲线形态去调参数比盲目试要快得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑