启发式算法(heuristic algorithm)——Python实现
运筹学--Operations Research (O.R.),有时也称为数学规划、最优化理论,是人工智能的“引擎”,因为几乎所有人工智能的问题最后都会转化为求解优化问题。几年前流行的支持向量机(SVM,二次规划问题)如此,近几年席卷全球的深度学习(DL)的参数优化(训练)也是(高度复合函数无约束优化问题)。现今在社会经济管理、生命科学等领域中,决策环境越来越复杂,众多因素有着错综复杂的相互作用,复杂的决策过程致使建立模型的难度越来越高,决策变量的量级、取值、目标的复杂多样、约束的非线性程度都是经典优化算法难以求解的,这样启发式算法就应运而生。启发式算法通常是以问题为导向的(Problem Specific),也就是说,没有一个通用的框架,每个不同的问题通常设计一个不同的启发式算法,由于这些问题都是NP-Hard(要求得全局最优解通常需要指数级算法复杂度,不存在多项式时间算法)的,人们一般会根据特定的问题设计只针对该问题的启发式算法。
一、精确算法与启发式算法
在数学优化和计算机科学中,启发式算法用于在经典方法效果不佳时更快地解决问题或找到近似解。这是通过牺牲最优性、完整性、准确性或精度换取速度来实现的。启发式算法给出的答案是具有偶然性的,因为启发式方法仅仅告诉我们该如何去找答案,而不直接告诉我们答案是什么。某些启发式算法具有强大的基础理论,它们要么是从理论中以自上而下的方式推导出来的,要么是基于实验或现实世界的数据得出的。其他的只是基于现实世界观察或经验的经验法则。
1.1 最优化模型结构
一般而言,算法所需要解决的问题,都能分成三个部分:
目标:什么是目标呢?简单点说就是要优化的东西,比如运筹学的背包问题中,要优化的就是所选物品的价值,使其最大。
决策:顾名思义就是根据目标所作出的决策,比如在这里就是决定选取哪些物品装进我们的背包。
约束:那么何又为约束呢?就是说再进行决策时必须遵循的条件,比如上面的背包问题,我们所选取的物品总的重量不能超过背包的容量。要是没有容量的约束,小学生才做选择呢,我全都要!
1.2 启发式算法
启发式算法是针对精确算法而言的。
精确算法:找到最优解并评估最优性,有严格的最优化数学理论支撑。到目前为止,已提出的精确算法种类较多,有单纯形法、分支定界法、割平面法、整数规划算法和动态规划算法等。一般可用软件为MATLAB,LINGO,Python等。(Exact optimization methods ,Jourdan et al,2009: ‘Exact methods find the optimal solution and assess its optimality’.)
启发式算法(heuristic algorithm):在问题结构不良的情况下,为得到近似可用的解,分析人员必须运用自己的感知和洞察力,从与其有关而较基本的模型及算法中寻求其间的联系,从中得到启发,去发现适于解决该问题的思路和途径,这种方法称为启发式方法。由此建立的算法称为启发式算法,可为复杂的优化问题快速生成近似最优解,许多启发式算法是相当特殊的,依赖于某个特定问题。启发式策略在一个寻求最优解的过程中能够根据个体或者全局的经验来改变其搜索路径,当寻求问题的最优解变得不可能或者很难完成时,启发式策略就是一个高效的获得可行解的办法。
启发式算法一般用于解决NP-hard问题,其中NP是指非确定性多项式。因为传统算法是把各种可能性都一一进行尝试,最终能找到问题的答案,但对于NP问题,传统算法花费几乎无穷的时间和算力才能求得答案。而启发式方法则限制了搜索空间,大大减少尝试的数量,能迅速地达到问题的解决。但由于这种方法具有尝试错误的特点,所以也有失败的可能性。
举个通俗易懂的例子:
驾驶汽车到达某人的家,写成算法是这样的:沿167 号高速公路往南行至Puyallup;从South Hill Mall 出口出来后往山上开 4.5 英里; 在一个杂物店旁边的红绿灯路口右转,接着在第一个路口左转;从左边褐色大房子的车道进去,就是North Cedar 路714 号。
用启发式方法来描述则可能是这样:找出上一次我们寄给你的信,照着信上面的寄出地址开车到这个镇;到了之后你问一下我们的房子在哪里。 这里每个人都认识我们——肯定有人会很愿意帮助你的;如果你找不到人,那就找个公共电话亭给我们打电话,我们会出来接你。
1.3 启发式算法特点
启发式算法一般用于解决NP-hard问题,例如,著名的推销员旅行问题(Travel Saleman Problem or TSP):假设一个推销员需要从南京出发,经过广州,北京,上海,…,等 n 个城市, 最后返回香港。 任意两个城市之间都有飞机直达,但票价不等。假设公司只给报销 C 元钱,问是否存在一个行程安排,使得他能遍历所有城市,而且总的路费小于 C?推销员旅行问题显然是 NP 的。因为如果你任意给出一个行程安排,可以很容易算出旅行总开销。但是,要想知道一条总路费小于 C 的行程是否存在,在最坏情况下,必须检查所有可能的旅行安排。
启发式算法的基本特点: 复杂度不确定,但是在实际中往往也能得到一个比较好的效果。(1)随机初始可行解;(2)给定一个评价函数(常常与目标函数值有关);(3)邻域,产生新的可行解;(4)选择和接受解得准则;(5)终止准则。 其中(4)集中反映了启发式算法的克服局部最优的能力。
启发式算法目前缺乏统一、完整的理论体系,启发式算法的提出就是根据经验提出,没有什么坚实的理论基础。启发式算法中的参数对算法的效果起着至关重要的作用,如何有效设置参数是经验活但还要悟性,只有try again。启发算法缺乏有效的迭代停止条件。还是经验,迭代次数100不行,就200,还不行就1000,启发式算法收敛速度的研究等。应用启发式算法,你就知道没有完美的东西,要快你就要付出代价,就是越快你得到的解也就越差。启发式算法中,不能保证找到最佳解决方案,局部最优值的陷入无法避免。启发式本质上是一种贪心策略,这也在客观上决定了不符合贪心规则的更好(或者最优)解会错过。其特点是在解决问题时,利用过去的经验,选择已经行之有效的方法,而不是系统地、以确定的步骤去寻求答案。
二、启发式算法的发展
启发式算法的计算量都比较大,所以启发式算法伴随着计算机技术的发展,取得了巨大的成就。40年代:由于实际需要,人们已经提出了一些解决实际问题快速有效的启发式算法。50年代:启发式算法的研究逐步繁荣起来。随后,人们将启发式算法的思想和人工智能领域中的各种有关问题的求解的收缩方法相结合,提出了许多启发式的搜索算法。其中贪婪算法和局部搜索等到人们的关注。60年代: 随着人们对数学模型和优化算法的研究越来越重视,发现以前提出的启发式算法速度很快,但是解得质量不能保证。虽然对优化算法的研究取得了很大的进展,但是较大规模的问题仍然无能为力(计算量还是太大)。70年代:计算复杂性理论的提出。NP完全理论告诉我们,许多实际问题不可能在合理的时间范围内找到全局最优解。发现贪婪算法和局部搜索算法速度快,但解不好的原因主要是他们只是在局部的区域内找解,得到的解不能保证全局最优性。由此必须引入新的搜索机制和策略,才能有效地解决这些困难问题,这就导致了超启发式算法(meta-heuristic algorithms)的产生。
Holland模拟地球上生物进化规律提出了遗传算法(Genetic Algorithm),它的与众不同的搜索机制引起了人们再次引发了人们研究启发式算法的兴趣,从而掀起了研究启发式算法的热潮。80年代以后: 模拟退火算法(Simulated Annealing Algorithm),人工神经网络(Artificial Neural Network),禁忌搜索(Tabu Search)相继出现。最近,演化算法(Evolutionary Algorithm), 蚁群算法(Ant Algorithms), 拟人拟物算法,量子算法等油相继兴起,掀起了研究启发式算法的高潮。由于这些算法简单和有效,而且具有某种智能,因而成为科学计算和人类之间的桥梁。在一些寻找最优解问题中,传统的能够求出最优解的精确式算法(如分支界定、动态规划等)花费的时空过大。启发式算法(heuristic algorithm)是一种能以更快速度找出问题近似最优解的方法。启发式算法通过牺牲一些精度来换取较小的时空消耗,可以看作是解决问题的捷径。
三、常用的启发式算法
启发式算法的主要思想来自于人类经过长期对物理、生物、社会的自然现象仔细的观察和实践,以及对这些自然现象的深刻理解,逐步向大自然学习,模仿其中的自然现象的运行机制而得到的。
算法名称 | 启发 | 原理 |
---|---|---|
SA模拟退火算法 | 高温金属退火过程中分子的随机扰动范围随温度的降低而减少 | 1.如何逃离局部最优到达全局最优,在局部最优点一步走多远 2.解的变化范围随温度的降低而减少+使用metropolis principle采纳不好的解 |
GA遗传算法 | 生物进化过程中染色体的选择(适者生存),交叉复制(交配)和变异(基因变异)的过程 | 0. 考虑了种群最优解,在种群最优解基础之上进行操作,期待产生更好的解 1. 选择:适应能力强的个体被选择的概率大-适应度大的染色体被选择的概率大 2. 交叉复制:选择的2个个体进行交配产生子女-选择的2个染色体某1段交换剩下的复制产生子染色体 3. 变异:交叉复制的染色体可能发生基因变异 |
DE差分进化算法 | 生物进化过程中染色体的变异(染色体向量做差分运算),交叉复制(原染色体和变异染色体)和选择(原染色体和交叉复制染色体)的过程 | 0. 考虑了种群解信息,在种群解信息基础之上进行操作,期待产生更好的解 1. 变异:随机选择种群中的三条染色体进行向量差分产生变异产色体 2. 交叉复制:原染色体与变异染色体根据交叉因子进行交叉复制 3. 选择:原染色体和交叉复制的染色体对比选择适应度大的作为子染色体 |
PSO粒子群优化算法 | 模拟鸟群的觅食行为,每只鸟以一定的速度飞行寻找食物,鸟会考虑自己找到的食物和群体找到的食物 | 1. 考虑了个体最优解,群体最优解,个体惯性 2. 鸟的速度更新考虑了以前的速度,自己找到的食物和鸟群找到的食物 |
ACO蚁群优化算法 | 模拟了蚁群寻找食物的过程,蚂蚁在能见度内寻找食物时会在地上留下信息素,后面的蚂蚁在能见度内会根据信息素浓度选择走那条路,有食物的路蚂蚁走的多信息素浓度高,食物最好的地方蚂蚁去的最多信息素浓度最高 | 0. 个体使用信息素进行信息共享》进而决定路径选择》最终导致信息素浓度越高食物越好的概率越大 1. 信息共享:使用信息素进行信息共享 2. 觅食规则:能见度方向寻找食物;往信息素浓度高的方向寻找食物;往没有障碍物的方向移动 3. 能见度:与距离成反比,距离太远蚂蚁就看不见了,看不见就到不了 4. 信息素:信息素浓度=信息素减量+信息素增量 5. 路径选择概率:有能见度和信息素共同决定 |
ABC人工蜂群算法 | 模拟了蜜蜂采蜜过程:工蜂负责采蜜;侦查蜂负责在田野里寻找蜜源(全局搜索);采蜜蜂负责保留蜜源信息和招募跟随蜂;跟随蜂负责在它选择的采蜜蜂周围寻找新蜜源(局部搜索) | 0. 分工合作,侦查蜂-全局搜索,跟随蜂-局部搜索,采蜜蜂-保留好的解 1. 开始大家都是工蜂 2. 派出一部分工蜂作为侦查蜂,在田野里寻找蜜源 3. 蜜源平均质量以上的侦查蜂变采蜜蜂,平均质量一下的侦查蜂变工蜂 4. 采蜜蜂跳舞招募跟随蜂(从工蜂中派出一部分工蜂作为跟随蜂)在采蜜蜂蜜源周围寻找新蜜源 5. 如果跟随蜂找到的新蜜源质量比采蜜蜂好,跟随蜂变成采蜜蜂,后面可以去招募跟随蜂;跟随蜂蜜源质量好于原采蜜蜂,采蜜蜂蜜源被跟随蜂选择一定次数后蜜源质量仍然低于跟随蜂蜜源把这些采蜜蜂变成工蜂 |
AFSA人工鱼群算法 | 模拟的鱼群在觅食时的行为 寻找:在视野范围内随机游动寻找更好的食物;追尾:往视野范围内食物更好的鱼游去;聚集:往视野范围内鱼群中心靠近(认为,鱼越多的地方食物越好);移动:在视野范围内随机移动 |
0. 每条鱼都在进行局部搜索,最终达到群体最优(不一定全局最优) 1. 寻找:在视野范围内寻找更好的食物+避免拥挤 2. 追尾:向视野范围内食物更好的鱼游去+避免拥挤 3. 聚集:向视野范围内小鱼群中心游去+避免拥挤 4. 移动:视野范围内随机移动+避免拥挤 |
GWO灰狼优化算法 | 模拟了灰狼的狩猎行为:构建灰狼等级结构;狩猎过程中头狼负责寻找最好的猎物并接收其他狼的汇报信息;其他狼负责向头狼靠近并通过叫声汇报猎物位置 | 0. 解的等级结构 1. 构建灰狼等级结构 2. 根据适应度(α,β,δ)更新头狼)的位置(灰狼有非常严格的社会等级层次制度,种群中第一层的领导者,称为α;第二层是 α 的智囊团队,称为β;第三层是δ,δ听从α和β的决策命令,主要负责侦查、放哨、看护等事务;最底层是ω,主要负责种群内部关系的平衡。 3. 根据所有狼(包括 ω)与头狼(α,β,δ)的距离更新所有狼的位置 |
这几种启发式算法都有一个共同的特点:从随机的可行初始解出发,用迭代改进的策略,去逼近问题的最优解。
四、启发式算法Python实现
4.1 粒子群优化算法
from sko.PSO import PSO
import matplotlib.pyplot as plt
def demo_func(x):
x1, x2, x3 = x
return x1 ** 2 + (x2 - 0.05) ** 2 + x3 ** 2
pso = PSO(func=demo_func, dim=3, pop=40, max_iter=150, lb=[0, -1, 0.5], ub=[1, 1, 1], w=0.8, c1=0.5, c2=0.5)
pso.run()
print('best_x is ', pso.gbest_x, 'best_y is', pso.gbest_y)
plt.plot(pso.gbest_y_hist)
plt.show()
4.2 模拟退火算法
#模拟退火算法
import numpy as np
from scipy import spatial
import matplotlib.pyplot as plt
import sys
file_name = sys.argv[1] if len(sys.argv) > 1 else 'nctu.csv'
points_coordinate = np.loadtxt(file_name, delimiter=',')
num_points = points_coordinate.shape[0]
distance_matrix = spatial.distance.cdist(points_coordinate, points_coordinate, metric='euclidean')
distance_matrix = distance_matrix * 111000 # 1 degree of lat/lon ~ = 111000m
def cal_total_distance(routine):
'''The objective function. input routine, return total distance.
cal_total_distance(np.arange(num_points))
'''
num_points, = routine.shape
return sum([distance_matrix[routine[i % num_points], routine[(i + 1) % num_points]] for i in range(num_points)])
# %%
from sko.SA import SA_TSP
sa_tsp = SA_TSP(func=cal_total_distance, x0=range(num_points), T_max=100, T_min=1, L=10 * num_points)
best_points, best_distance = sa_tsp.run()
print(best_points, best_distance, cal_total_distance(best_points))
# %% Plot the best routine
from matplotlib.ticker import FormatStrFormatter
fig, ax = plt.subplots(1, 2)
best_points_ = np.concatenate([best_points, [best_points[0]]])
best_points_coordinate = points_coordinate[best_points_, :]
ax[0].plot(sa_tsp.best_y_history)
ax[0].set_xlabel("Iteration")
ax[0].set_ylabel("Distance")
ax[1].plot(best_points_coordinate[:, 0], best_points_coordinate[:, 1],
marker='o', markerfacecolor='b', color='c', linestyle='-')
ax[1].xaxis.set_major_formatter(FormatStrFormatter('%.3f'))
ax[1].yaxis.set_major_formatter(FormatStrFormatter('%.3f'))
ax[1].set_xlabel("Longitude")
ax[1].set_ylabel("Latitude")
plt.show()
# %% Plot the animation
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
best_x_history = sa_tsp.best_x_history
fig2, ax2 = plt.subplots(1, 1)
ax2.set_title('title', loc='center')
line = ax2.plot(points_coordinate[:, 0], points_coordinate[:, 1],
marker='o', markerfacecolor='b', color='c', linestyle='-')
ax2.xaxis.set_major_formatter(FormatStrFormatter('%.3f'))
ax2.yaxis.set_major_formatter(FormatStrFormatter('%.3f'))
ax2.set_xlabel("Longitude")
ax2.set_ylabel("Latitude")
plt.ion()
p = plt.show()
def update_scatter(frame):
ax2.set_title('iter = ' + str(frame))
points = best_x_history[frame]
points = np.concatenate([points, [points[0]]])
point_coordinate = points_coordinate[points, :]
plt.setp(line, 'xdata', point_coordinate[:, 0], 'ydata', point_coordinate[:, 1])
return line
ani = FuncAnimation(fig2, update_scatter, blit=True, interval=25, frames=len(best_x_history))
plt.show()
总结
在问题结构不良的情况下,为得到近似可用的解,分析人员必须运用自己的感知和洞察力,从与其有关而较基本的模型及算法中寻求其间的联系,从中得到启发,去发现适于解决该问题的思路和途径,这种方法称为启发式方法,由此建立的算法称为启发式算法。优点:(1)计算步骤简单,要求的理论基础不高,可由未经过高级训练的人员实现;(2)比优化方法常可减少大量的计算工作量,从而显著节约开支和时间;(3)易于将定量和定性分析相结合。不足:(1)不能保证求的最优解。(2)表现不稳定,启发式算法在同一问题的不同实例计算中会有不同的效果,有些很好,而有些则很差。在实际应用中,这种不稳定性造成计算结果不可信,可能造成管理的混乱。(3)算法的好坏依赖于实际问题,算法设计者的经验和技术,这点很难总结规律,同时使不同算法之间难以比较。